Repository navigation
iOS parity round 5: speed and native polish - #269
Conversation
⌘N, ⌘K and ⌘↩ now reach the app in a development build through modules/key-commands, with titles in the ⌘ overlay. One registry decides which screen a press belongs to, and screens listen only while in view. Installs expo-quick-actions, expo-core-spotlight 2.x and expo-share-intent so the round-4 quick actions and Spotlight switch on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment The share extension (expo-share-intent, development builds with the App Group) takes a web page into Add link, which starts the import, and other text into New note. Spotlight indexes the Current Debate's cases, attacks, brainstorms and factoids, and forgets them when the Current Debate changes, on sign-out and after Delete all my data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each is a Reanimated FlatList (ListScreen) with the card drawn per row, so rows still slide into a deleted row's gap. A row fades up when it arrives in the list, not every time it scrolls back into view (row-arrivals.ts). A shared page or note reaches its sheet only through the share hand-off, so a debatelab://add-link?url= link from elsewhere cannot start an import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
React Compiler was skipping Ask, Crossfire and Sparring, so every token and clock tick re-rendered the whole screen. Ask's streamed text is now a live value that only the answer being written reads, and its saved messages are memoized; Crossfire's clock ticks in its own component and the screen compiles again. Refs written during render and hooks used before their functions are declared are gone from all three. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saved work (Discuss, Share, Delete), History rows (Discuss and Share beside Delete), Library sources (Copy and Share beside the switch and Delete) and the Debate tab's recent sources (Copy, Share) offer their menu as accessibility actions, so a VoiceOver user swipes up or down instead of needing the long press. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build day now signs the share extension too, and the credentials keys use the Xcode target names prebuild writes (DebateOwl, not DebateLab). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
The streaming phase came from an external store, whose updates render on their own before the finished message lands. It is plain state again, so both change in one render, and ending a stream leaves its text in place until the next one starts. The share extension's URL now waits in share-inbox.ts, which imports nothing, so +native-intent.tsx stays as light as deep-links.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Only the share extension's own URL (key, UUID nonce, #text or #weburl) reaches expo-share-intent, whose reader force-unwraps the fragment: a crafted debatelab://dataUrl= link opens the Debate tab instead of crashing the app. - A shared item belongs to the account it was read under; if that account signs out first it is dropped, never opened for the next one. - Spotlight calls run in one queue and a clear cancels indexing asked for before it, so a signed-out account's documents cannot be indexed again. - History's Add to debate keeps the debate picked and the request in flight in the screen, so a row scrolled away and back loses neither. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r History - Share reads run one after another, and the account a read started under stays its owner until the next read, so two reads across an account switch cannot hand the first account's item to the second. - Adding unsorted work to a debate drops History's running load only while the unsorted list is on screen; after switching to a debate's History, that debate's load still lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oads after a round trip - Spotlight read the Debate tab's side lists, which leave out neutral work (every attack and factoids set) and hold three to five items. It now reads the debate's newest 40 documents of every side, only in builds that have Spotlight. - An attach that finishes after History went to a debate and back to the unsorted list, before that list loaded, reads the list again instead of dropping the load running for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…light Retry and the reload after a delete run without the focus guard, and an unmounted dashboard keeps its old Current Debate, so a late document read could index debate A over B. Indexing now also requires the dashboard to still be mounted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 5 of iOS parity: speed and native polish. Stacked on #266 (base
native-ios/parity-r3r4-combined). Nothing on the server changes, and there was no EAS build.What changed
Long lists draw only what is on screen. History, the Library (phone, and the iPad list pane) and saved chats (the iPad pane and the phone sheet) are now Reanimated FlatLists through one
ListScreencomponent. Each row draws its part of the card. Rows still slide into a deleted row's gap. A row fades up when it arrives in the list, not every time it scrolls back into view (row-arrivals.ts, tested). Shimmer placeholders, empty states and Reduce Motion behave as before. Pull to refresh is new on History and the Library.I used FlatList, not FlashList. FlashList recycles cells, and these rows hold swipe, press and link-preview state that would leak between items. Chat transcripts stay scroll views. Crossfire and Sparring rounds are bounded by their format. Ask's messages are memoized instead, because a chat is tens of messages and a virtualized chat with scroll-to-end and the keyboard couldn't be checked without a device.
Re-renders. I ran React Compiler's Babel plugin with a logger, and it was skipping Ask, Crossfire and Sparring entirely. The causes were lint suppressions,
ref.currentwritten during render, and the ⌘↩ hook calling a function declared below it.lib/live-value.ts) that only the answer being written reads, and its saved messages are memoized. A token or a keystroke no longer redraws the conversation. Stop, the 30 ms batching and scroll-to-end work as before.iPad keyboard shortcuts. ⌘N opens a new debate, ⌘K the switcher, and ⌘↩ sends in Ask, Crossfire and Sparring. Each has a title in the ⌘ overlay. One registry decides which screen gets a press (tested), and screens listen only while in view. The native side is the app's own Expo module,
apps/mobile/modules/key-commands, about 100 lines of Swift that add UIKeyCommands to UIApplication near the end of the responder chain. No maintained package fit SDK 57, and Expensify's needs Objective-C AppDelegate edits. Autolinking finds the module. The Swift has not been compiled.Quick actions and Spotlight now work, with
expo-quick-actions6 andexpo-core-spotlight2 (3.0 dropped the config plugin). Spotlight indexes the Current Debate's cases, attacks, brainstorms and factoids, where it used to index cases only. The index empties when the Current Debate changes, on sign-out and after Delete all my data (spotlight-items.tsandspotlight.ts, tested).Share extension (
expo-share-intent8). Sharing a page from Safari opens Add link and starts the import. Sharing text opens New note filled in (share-intent-parse.ts, tested). The sheets get shared content only through a one-shot in-memory hand-off, so adebatelab://add-link?url=…link from anywhere else can't start a signed-in import. Only http and https addresses are accepted.VoiceOver. Every list row's long-press menu is also its accessibility actions. That covers saved work (Discuss, Share, Delete), History, Library sources (Copy and Share beside the switch and Delete) and recent sources.
Review
Five Codex rounds (gpt-6.1-sol, xhigh). I checked each finding against the code, and all were real. Round 5 had no findings on
eb0af2ce.debatelab://dataUrl=link without a#typecrashed the app, becauseexpo-share-intent's reader force-unwraps the fragment. Now only the extension's exact URL (our key, a UUID nonce,#textor#weburl) reaches the module. Any other share-shaped link opens the Debate tab.Two more fixes came from my own second look. The streamed Ask answer could vanish for one frame before the finished one replaced it. The phase was read through
useSyncExternalStore, whose renders run ahead of ordinary state updates, so it is plain state again. The share URL now waits inshare-inbox.ts, which imports nothing, so+native-intent.tsxstays as light asdeep-links.ts.What needs a development build or Adam's Apple account
npx expo config --jsonis byte-identical before and after.group.com.pistack.debateowlregistered on Adam's Apple account forcom.pistack.debateowl.share-extension, as well as for the app andExpoWidgetsTarget. Build day'scredentials.jsonnow lists three targets. Its main key used to beDebateLab, but prebuild names the targetDebateOwl, so docs/development.md now saysDebateOwl.Checks
npm run verifypassed locally ateb0af2ce. That run covers the docs check, eslint, knip and the repo rules, typecheck including the mobile app, 908 unit tests, 30 collaboration tests, 553 Edge Function tests, the web build, the service-worker check and 47 Playwright smoke tests. It is a local pass, not CI, because GitHub Actions is out of minutes.DEBATELAB_BUILD=dev DEBATELAB_APP_GROUP=… npx expo prebuild --platform ios --no-installproduced theDebateOwlSharetarget with its App Group entitlement, the Spotlight AppDelegate hook, the quick-action Info.plist items and no push entitlement. I deleted theios/folder afterwards.expo exportruns out of memory on this VPS. The list layouts, the ⌘ overlay and the share sheet still need to be seen in the app.Risks
keyCommandsoverride added through a config plugin.expo-share-intentdelivers a read's event before that read's promise resolves, which its Swift does. Two shares straddling an account switch inside that window remain a theoretical edge case.🤖 Generated with Claude Code