diff --git a/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-2-release-to-type-light.png b/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-2-release-to-type-light.png new file mode 100644 index 0000000000..4065f46b22 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-2-release-to-type-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-3-released-and-7-clipboard-dark.png b/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-3-released-and-7-clipboard-dark.png new file mode 100644 index 0000000000..1adede5a1c Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-3-released-and-7-clipboard-dark.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-3-released-and-7-clipboard-light.png b/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-3-released-and-7-clipboard-light.png new file mode 100644 index 0000000000..8413d5e4fe Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-3-released-and-7-clipboard-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-9-peek-preview-light.png b/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-9-peek-preview-light.png new file mode 100644 index 0000000000..227ea8a643 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/04ef-2364-9-peek-preview-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-1-pull-up-to-type-dark.png b/.github/pr-screenshots/2364-pull-to-compose/2364-1-pull-up-to-type-dark.png new file mode 100644 index 0000000000..f2aff6b7dc Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-1-pull-up-to-type-dark.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-1-pull-up-to-type-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-1-pull-up-to-type-light.png new file mode 100644 index 0000000000..8df199ff5b Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-1-pull-up-to-type-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-2-release-to-type-dark.png b/.github/pr-screenshots/2364-pull-to-compose/2364-2-release-to-type-dark.png new file mode 100644 index 0000000000..7f658f4531 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-2-release-to-type-dark.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-2-release-to-type-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-2-release-to-type-light.png new file mode 100644 index 0000000000..e62919368a Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-2-release-to-type-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-3-released-kbd-dark.png b/.github/pr-screenshots/2364-pull-to-compose/2364-3-released-kbd-dark.png new file mode 100644 index 0000000000..f562957c67 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-3-released-kbd-dark.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-3-released-kbd-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-3-released-kbd-light.png new file mode 100644 index 0000000000..729594ed40 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-3-released-kbd-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-4-short-pull-no-kbd-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-4-short-pull-no-kbd-light.png new file mode 100644 index 0000000000..44552f5ae4 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-4-short-pull-no-kbd-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-5-history-drag-no-kbd-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-5-history-drag-no-kbd-light.png new file mode 100644 index 0000000000..1b13352789 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-5-history-drag-no-kbd-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-7-paste-row-dark.png b/.github/pr-screenshots/2364-pull-to-compose/2364-7-paste-row-dark.png new file mode 100644 index 0000000000..c7d16b6f4e Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-7-paste-row-dark.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-7-paste-row-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-7-paste-row-light.png new file mode 100644 index 0000000000..580c76323a Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-7-paste-row-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-7-pasted-tile-dark.png b/.github/pr-screenshots/2364-pull-to-compose/2364-7-pasted-tile-dark.png new file mode 100644 index 0000000000..d276e056d4 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-7-pasted-tile-dark.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-7-pasted-tile-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-7-pasted-tile-light.png new file mode 100644 index 0000000000..061b45cc37 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-7-pasted-tile-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-8-double-tap-removed-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-8-double-tap-removed-light.png new file mode 100644 index 0000000000..718cc0200d Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-8-double-tap-removed-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-8-double-tap-thumbs-dark.png b/.github/pr-screenshots/2364-pull-to-compose/2364-8-double-tap-thumbs-dark.png new file mode 100644 index 0000000000..bc43972d08 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-8-double-tap-thumbs-dark.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-8-double-tap-thumbs-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-8-double-tap-thumbs-light.png new file mode 100644 index 0000000000..7411032616 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-8-double-tap-thumbs-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-9-peek-preview-dark.png b/.github/pr-screenshots/2364-pull-to-compose/2364-9-peek-preview-dark.png new file mode 100644 index 0000000000..14666ec62b Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-9-peek-preview-dark.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-9-peek-preview-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-9-peek-preview-light.png new file mode 100644 index 0000000000..b48939c5e5 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-9-peek-preview-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/2364-9-peek-render-error-light.png b/.github/pr-screenshots/2364-pull-to-compose/2364-9-peek-render-error-light.png new file mode 100644 index 0000000000..c52fd05403 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/2364-9-peek-render-error-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/9ac7-7-clipboard-panel-dark.png b/.github/pr-screenshots/2364-pull-to-compose/9ac7-7-clipboard-panel-dark.png new file mode 100644 index 0000000000..1927a3d91b Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/9ac7-7-clipboard-panel-dark.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/9ac7-7-clipboard-panel-light.png b/.github/pr-screenshots/2364-pull-to-compose/9ac7-7-clipboard-panel-light.png new file mode 100644 index 0000000000..815c8b189e Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/9ac7-7-clipboard-panel-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/9ac7-7-clipboard-panel-reduce-transparency-light.png b/.github/pr-screenshots/2364-pull-to-compose/9ac7-7-clipboard-panel-reduce-transparency-light.png new file mode 100644 index 0000000000..ead7cfda4a Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/9ac7-7-clipboard-panel-reduce-transparency-light.png differ diff --git a/.github/pr-screenshots/2364-pull-to-compose/9ac7-chats-needs-you-and-activity-number-light.png b/.github/pr-screenshots/2364-pull-to-compose/9ac7-chats-needs-you-and-activity-number-light.png new file mode 100644 index 0000000000..9c60f26096 Binary files /dev/null and b/.github/pr-screenshots/2364-pull-to-compose/9ac7-chats-needs-you-and-activity-number-light.png differ diff --git a/apps/mobile/DESIGN.md b/apps/mobile/DESIGN.md index 74aaf7882f..c43648a255 100644 --- a/apps/mobile/DESIGN.md +++ b/apps/mobile/DESIGN.md @@ -148,6 +148,7 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w - A collapsed Channel section shows one count chip for everything inside it: Needs you first, otherwise the unread sum, leaving muted conversations out. An expanded section that holds Bots waiting for the Human says how many, in `warning`. - Opening a conversation marks it read on the Host (`channelMarkRead`) whenever the Human is at the newest message, so the chip clears on the phone and on desktop. - Swipe for pin and mute; long-press opens a native menu. +- On iOS a long-press on a Host conversation row peeks at the chat, like Messages: the row lifts into the system context-menu preview (expo-router `Link.Preview`) showing the conversation itself, and tapping the preview opens it. A plain tap opens the conversation as before. **Count chip** @@ -167,6 +168,7 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w - Activity rows do not navigate on tap. Long-press (or the accessibility details action) opens the message sheet with the complete plain-text content in a scrollable, selectable block capped at `messageSheet.textHeightRatio` of the window height, then Copy/Share and an explicit Open conversation, Open memory or Open new Bot action when a destination exists; there are no reactions. Consecutive posts by the same Bot into the same Channel on the same day fold into a counted row whose menu opens the latest post. Memory opens the Bot's memory files, not a commit diff. Bot-to-Bot DMs remain read-only. Keep full actor/action/detail text in the accessibility label and menu even when the compact memory line uses only the icon and commit subject. - A top-to-bottom list (not inverted), oldest first, on one code path for iOS and Android. It opens hidden and is shown once it has been scrolled to the end and its content has settled for a frame, so it never flashes the oldest messages first. Short conversations sit at the bottom, above the composer. Older history loads when the Human nears the top (`onStartReached`), and `maintainVisibleContentPosition` keeps the messages on screen where they were while it is prepended. Messages from the same author within 5 minutes form a group, and a time separator appears after a 30-minute gap. - At the newest message the list follows a streaming reply: content growth pins it to the end with a non-animated scroll, at most once per frame. Once the Human begins a drag or scrolls up into history, nothing moves the list under them: the visual playback cursor holds while received text continues to queue and persist. Only finishing a return gesture near the bottom or choosing the jump button resumes playback; a layout-driven scroll callback can neither rearm nor release following. Only a drag, the status-bar scroll to top or a jump to a linked message leaves the newest message, so the inset adjustment, content-size reports and `maintainVisibleContentPosition` corrections while the list first fills cannot strand it at the oldest message. A round jump button with a ⌄ then floats at the trailing edge `space.x2` above the composer, aligned with its trailing edge, like Grok (a 36pt Liquid Glass circle like **+** on iOS 26 with Reduce Transparency off, otherwise `raised`), with an accent badge counting the Bot messages that arrived since they left (99+ at most), like the Web Client's "Return to latest messages". Tapping it, sending a message, or scrolling back down to the newest message follows again and hides the button. + - **Pull up to type (iOS).** At the newest message, pulling the list further up past its end (the rubber-band bounce) and letting go focuses the composer and raises the keyboard, like pull-to-refresh in reverse. A drag counts only when it begins at the newest message (within the follow slop), so reading history never triggers it. A capsule with a keyboard symbol fades in above the composer as the pull grows, reading "Pull up to type" and switching to "Release to type" with one light haptic once the pull passes `pullToCompose.threshold` (40pt of offset, about 95pt of finger travel against the rubber band); letting go follows the newest message again first, so the keyboard does not cover it; easing back below it before release cancels. It is off where there is no composer (a Bot-to-Bot DM) or the Host is not connected. Android's overscroll stretches without moving the offset, so there is nothing to measure while the finger pulls: on Android an upward flick at the newest message (finger speed at least `pullToCompose.flick`, 1 dp/ms, with the list still at the newest message on release) focuses the composer with the same haptic, and no hint is drawn. - Settings → **Message display** offers **Streaming** and **Complete bubbles**, saved on this device. Streaming uses a local playback queue with a short prebuffer and reserve, releasing graphemes on a bounded clock. Rate changes follow queue depth gradually; clauses, sentences and paragraphs add progressively longer pauses. Android fades the active prose tail on the UI thread (180ms fade, up to 72ms stagger, at most 48 animated spans, with up to eight new spans per update); earlier text stays visible. Source commitment preserves the displayed cursor and row while the rest drains, then enables message actions. Historical messages open directly. Reduce Motion skips the fade. Code/table/native-only Markdown keep their established renderer; their content still follows the playback cursor. Complete bubbles keeps the working indicator and reveals each committed Channel Message independently. Switching modes changes presentation only; received drafts, committed messages and interruption semantics remain owned by Conversation and the Host. A draft-only append updates its row subscription while the timeline snapshot and historical message references stay stable. The bottom fade belongs to the keyboard-sticky composer. - Android Markdown uses synchronous native text/layout for prose, headings, lists, quotes, code and tables. Completed source blocks retain their keys and avoid rerendering when only the tail changes. Images and native-only extensions retain the enriched renderer. Drafts and committed messages keep the same renderer to avoid a settlement-only layout switch; iOS keeps its existing enriched implementation. - Append-only Android Markdown reparses the changing suffix with boundary lookback, retaining completed block identities. Blocks retain the lexer result with its exact source; rendering and window-support checks reuse it only while that source matches. A repaired streaming tail or a source-only block uses a fresh parse. Corrections and document-wide references rebuild the document. Prose reveal segmentation likewise retains a grapheme boundary and revisits the changing suffix, including a joined emoji or combining mark; it must not repeatedly segment the complete paragraph. @@ -187,6 +189,7 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w - Then the actions in groups, each row with a leading system icon (`RowIcon` symbols: an SF Symbol on iOS, a Material Symbol on Android) and its label: **Copy** and **Share** (the system share sheet) in one group; when an activity has a destination, **Copy** sits on its own and **Open …** joins **Share** in the second group. On iOS each group is a `raised` card at radius `lg` with hairline dividers; on Android the groups are `ListItem`s separated by dividers. - Every size follows the system text size: rows have a touch-height minimum and grow with their padded label, icons take their size from the text style, and each reaction circle is at least touch-sized and grows with its emoji. The iOS content reports its ideal height so the fitted sheet never compresses it. - A swipe down, a tap on the scrim, Android Back or any action closes the sheet. On iOS, Share and Open wait until the sheet has fully dismissed, so the next presentation does not collide with it. + - Double-tapping a bubble that can take reactions adds 👍 (or takes yours back), like a tapback in Apple Messages or Google Messages, with a light haptic. The first tap does nothing on its own, so a single tap stays free; taps more than 300ms apart (`doubleTap.window`) do not count. - A message that is still sending, failed, or is a card has no reactions, and neither does a read-only Bot DM or an activity row. Bubble text is not selectable, so the long-press always reaches this sheet; screen readers reach it through the bubble's "Message actions" action. - Delivery: pending shows at 70% opacity, failed offers "Not sent · Tap to retry", and sent shows nothing. - **Action cards** sit in the chat stream, and the same rules apply wherever they appear (DM, Activity, Human Inbox, web Inbox). Show only what the Human needs to decide; everything else waits behind Details. @@ -211,6 +214,7 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w - Android follows Material 3 (Google Messages): **+** is a standard icon button with no container and a borderless ripple, and the field is a filled capsule on the `raised` fill with no edge and no glass. The mic, Send and expand buttons ripple too. - **+** sits outside the capsule, bottom-aligned as the field grows. While dictating it becomes **×** (cancel), and while the mic is held, a red dot. With one trailing button (the mic or Send) the capsule is one row: the field and the button. When the text no longer fits that one row, or with more (attached files, or the mic next to Send), the capsule becomes a rounded rectangle with the same corner radius and stacks like the web composer: the field on top at full width, and a row below with expand on the left and the buttons on the right. - **+** opens Photos, Camera and File below the composer. Picked files show as removable tiles inside the capsule, up to 10 per message. Send appears once there is text or a file. + - While the field is focused and the clipboard holds an image, a panel above the composer, styled like the @ suggestions (Liquid Glass on iOS 26, otherwise the `sheet` layer), reads "Image on the clipboard" with a paste control. On iOS it is the system Paste button (`UIPasteControl`, through `expo-clipboard`'s `ClipboardPasteButton`, image only, icon only at accessibility text sizes), so no paste-permission prompt appears. On Android it is a text `Button` "Paste image" that reads the clipboard; Android only shows its own brief "pasted from your clipboard" notice. The image attaches as a JPEG tile and the row hides until something new is copied. The @ suggestions take that slot first. A text field cannot take an image paste on its own in React Native, so this is the paste path. - Sending uploads each file to the Host first. A retry keeps the uploads that already finished. - Nothing sits under the composer, so the chat stays clean. A DM's Model and Workspace settings live in the Bot profile, in a **Bot settings** group under the profile actions: a **Model** row (the Bot's current model name, or "Loading…", "No model set" or "Unavailable") and a **Workspace** row (the folder count, "N folders"), each with a chevron. - **Model** opens the native Models and presets page inside the profile drawer: Orchestrator, Assignment default, allowed Assignment models, reasoning effort, restore global default, save as preset and apply a shared preset. The searchable chooser pushes inside that same drawer and returns only after an accepted save and canonical refresh. Editing only the Orchestrator preserves Assignment policy. Assignment edits retain other allowed models, efforts and notes; the current default cannot be removed until another is chosen. Existing preset copies and per-Bot plans retain the Host's revision checks. Global default updates use the Host's existing profile-default contract; mobile does not create a separate global Assignment authority. diff --git a/apps/mobile/app/(tabs)/(home)/index.tsx b/apps/mobile/app/(tabs)/(home)/index.tsx index 891e6a312d..3481fd2db0 100644 --- a/apps/mobile/app/(tabs)/(home)/index.tsx +++ b/apps/mobile/app/(tabs)/(home)/index.tsx @@ -36,8 +36,9 @@ import { HeaderSearch, HeaderSearchButton } from '../../../src/ui/shell/header-s import { HomeHeader } from '../../../src/ui/shell/home-header'; import { HostDrawerEdge } from '../../../src/ui/shell/host-drawer'; import { SettingsButton } from '../../../src/ui/shell/settings-button'; +import { ConversationPeek, conversationPeek } from '../../../src/ui/shell/conversation-peek'; import { useFloatingTabInset } from '../../../src/ui/shell/floating-tab-inset'; -import { openConversation } from '../../../src/ui/shell/open-conversation'; +import { conversationHref, openConversation } from '../../../src/ui/shell/open-conversation'; import { useChromeScroll, useScrollChrome } from '../../../src/ui/shell/scroll-chrome'; import { t } from '../../../src/ui/strings'; @@ -354,11 +355,17 @@ const HomeContent = memo(function HomeContent({ } renderItem={({ item }) => item.type === 'computer' ? ( - openConversation(router, item.conversation, saved?.hostId)} - /> + conversationPeek ? ( + + + + ) : ( + openConversation(router, item.conversation, saved?.hostId)} + /> + ) ) : ( { if (limit <= 0) return []; if (source === 'file') { diff --git a/apps/mobile/src/ui/chat/clipboard-image.tsx b/apps/mobile/src/ui/chat/clipboard-image.tsx new file mode 100644 index 0000000000..b4f0fa33a0 --- /dev/null +++ b/apps/mobile/src/ui/chat/clipboard-image.tsx @@ -0,0 +1,144 @@ +import * as Clipboard from 'expo-clipboard'; +import { useState, useSyncExternalStore } from 'react'; +import { AppState, Platform, View } from 'react-native'; + +import { useDesignTheme } from '../design/theme'; +import { edge, radius, space } from '../design/tokens'; +import { useLiquidGlass } from '../primitives/drawer-material'; +import { Button } from '../primitives/button'; +import { Text } from '../primitives/text'; +import { PhotoIcon } from '../shell/icons'; +import { t } from '../strings'; +import { ComposerFieldMaterial } from './composer-chrome'; + +type ClipboardState = { image: boolean; generation: number }; + +let state: ClipboardState = { image: false, generation: 0 }; +const readClipboard = () => state; + +function subscribeClipboard(notify: () => void) { + let active = true; + let changed = false; + let latest = 0; + const subscription = Clipboard.addClipboardListener((event) => { + changed = true; + state = { + image: event.contentTypes.includes(Clipboard.ContentType.IMAGE), + generation: state.generation + 1, + }; + notify(); + }); + const refresh = (returning: boolean) => { + const request = ++latest; + changed = false; + void Clipboard.hasImageAsync().then( + (image) => { + if (!active || request !== latest || changed) return; + if (image === state.image && !(returning && image)) return; + state = { image, generation: returning ? state.generation + 1 : state.generation }; + notify(); + }, + () => {}, + ); + }; + const foreground = AppState.addEventListener('change', (next) => { + if (next === 'active') refresh(true); + }); + refresh(false); + return () => { + active = false; + subscription.remove(); + foreground.remove(); + }; +} + +const unsubscribed = () => () => {}; + +const nativePasteButton = Platform.OS === 'ios' && Clipboard.isPasteButtonAvailable; + +export const clipboardPasteAvailable = nativePasteButton || Platform.OS === 'android'; + +const IMAGE = { format: 'jpeg', jpegQuality: 0.8 } as const; + +async function readClipboardImage(onPaste: (data: string) => void, onUnreadable: () => void) { + const image = await Clipboard.getImageAsync(IMAGE).catch(() => null); + if (image === null) onUnreadable(); + else onPaste(image.data); +} + +export function useClipboardImage(watching: boolean) { + const current = useSyncExternalStore( + watching && clipboardPasteAvailable ? subscribeClipboard : unsubscribed, + readClipboard, + ); + const [used, setUsed] = useState(-1); + return { + offered: watching && clipboardPasteAvailable && current.image && used !== current.generation, + used: () => setUsed(current.generation), + }; +} + +export function ClipboardImageOffer({ + onPaste, + onUnreadable, +}: { + onPaste(data: string): void; + onUnreadable(): void; +}) { + const theme = useDesignTheme(); + const glass = useLiquidGlass(); + const corner = glass ? radius.xl : radius.lg; + return ( + + {glass ? : null} + + {t.clipboardImage} + + {nativePasteButton ? ( + { + if (data.type === 'image') onPaste(data.data); + else onUnreadable(); + }} + /> + ) : ( + +