Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions apps/mobile/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ The iOS app DebateOwl (`@debatelab/mobile`; bundle id `com.pistack.debateowl`, E

- `src/app/`: routes. `sign-in.tsx`; `(tabs)/` with one route group per tab: Debate, Prepare, Practice, Library, You; the form sheets `debates.tsx` (switcher), `new-debate.tsx`, `delete-account.tsx`, `delete-data.tsx`, `add-link.tsx`, `new-note.tsx`, `add-file.tsx` (Import file), `source/[id].tsx`, `edit-debate.tsx` (Edit settings), `debate-check.tsx` (the five-day check), `limit.tsx` (the website's limit-reached and model-restricted dialogs), `save-case.tsx` (Save case with a title), `context-review.tsx` (the debate context Review) and `welcome.tsx` (Ollie's welcome and the first-run tour); `share/[code].tsx`, the read-only viewer for a share link, outside the sign-in guard; `+native-intent.tsx`, which maps website links to app routes; and `ask.tsx`, `generate.tsx`, `case/[id].tsx`, `attack/`, `brainstorm/`, `factoids/` and `sparring/` (each a form `index.tsx` and a document `[id].tsx`), `crossfire.tsx`, `crossfire-round/[id].tsx` (a saved round, read only), `timer.tsx` (a full-screen modal) with its `timer-format.tsx` sheet (a custom format), `speech-time.tsx` and `motions.tsx` (the website's speech time calculator and motion generator), `flowchart/` (the screen `index.tsx` with its `card`, `claim` and `maps` form sheets), and `history.tsx`, full screen above the tabs. All are declared in the root `_layout.tsx`. Every Practice tool is in the app, so `(practice)/soon/[tool].tsx` is no longer reached from the Practice tab.
- `src/components/`: shared UI. `app-tabs.tsx` uses native tabs; `app-tabs.web.tsx` draws the mock's tab bar for the web preview.
- `modules/`: the app's own Expo modules, autolinked into development builds only. `key-commands` is the iPad's UIKeyCommand shortcuts (Swift, `DebateOwlKeyCommands`).
- Lists that grow without bound (History, the Library, saved chats) are `ListScreen` (`components/list-screen.tsx`): `Screen`'s column and offline notice around a Reanimated FlatList, so only rows near the screen are drawn, with pull to refresh. A list drawn as one card frames each row with `CardRowFrame`, and the list slides rows into a removed row's gap (`itemLayoutAnimation`). A row fades up (`ListItemIn`) only when it arrives in the data, not when it scrolls back into view (`useRowArrivals`, `lib/row-arrivals.ts`). Not FlashList: it recycles cells, and these rows hold swipe, press and link-preview state. Debates, a round's speeches and other lists bounded by their format stay plain views.
- React Compiler compiles each component it can; a component that writes `ref.current` during render, disables a react-hooks lint rule or uses a function before its declaration is skipped whole. To check, run `babel-plugin-react-compiler` with a `logger` over a file to list what it skips. Values that change many times a second (Ask's streamed answer) live in `lib/live-value.ts` and are read by the small component that shows them; `useLatest` hands effects the latest handler without a ref write in render. Crossfire's clock ticks in its own component.
- `src/lib/`: auth (`auth.tsx`), `native-capabilities.ts` (what the running binary has beyond Expo Go), the Supabase client, session storage (AES key in SecureStore, ciphertext in AsyncStorage) and i18n. `debates.ts`, `settings.tsx`, `subscription.ts` and `account.ts` read and write the website's tables the way its stores do (`src/lib/stores/debate-store.ts`, `settings-store.ts`, `src/hooks/use-subscription.ts`, `src/lib/account.ts`), so the app and the website share one Current Debate and one set of settings. Change them together with the website code they mirror.
- `sources.ts` mirrors the session store's source actions (`addSource` with Undo's restore, `updateSource`, `removeSource`, `toggleSource` and `setFolderActive` as `setSourcesActive`, the folder actions and `clearLibrary`) and `src/lib/source-import.ts`. Switch changes run in one serial queue, so they reach the server in the order the student made them; a screen shows a change at once and puts it back when the write fails (`source-actions.ts`, with the website's messages). The Library always shows the Current Debate, also in one-off mode, as on the website. Its folder list, search and eight sort orders are the website's, in `packages/core/src/library.ts`. Scan pages is not in the app yet.
- File import runs the website's reader (`packages/core/src/source-file-import.ts`, with its 8 MB and 120,000-character limits) inside an Expo DOM component, `components/source-file-reader.tsx`, because pdf.js and mammoth need a browser. `pick-source-file.ts` opens the Files picker, `source-file.tsx` checks size (the file's own when the picker gives none) and type before reading anything, then serves the picked copy in 768 KB slices that the reader pulls one at a time (`picked-file.ts` reads them natively as base64; the web preview slices the browser File); a read always ends, with a two-minute timeout for a reader that never loads, and `add-file.tsx` shows the draft. The DOM component imports pdf.js and mammoth statically, because Expo cannot export a DOM component that splits into chunks; its webview loads only while a file is read. It uses `@expo/dom-webview`, which Expo Go has. Never move the parsing into `import-source`: that would be an Edge Function deploy.
Expand Down Expand Up @@ -50,8 +53,9 @@ The iOS app DebateOwl (`@debatelab/mobile`; bundle id `com.pistack.debateowl`, E
- iPad layouts (iPad mock A, with mock C's docked claim) come from one hook, `useLayoutClass()` in `src/lib/layout.ts`, which also holds every width they use: `phone` on every iPhone and in iPad windows under 700 points, `regular` from 700, `wide` from 1100. Phone layouts must not change, so each screen keeps its phone JSX and branches before it; compare iPhone screenshots before and after. A full-screen iPad window from the regular width shows the app's own sidebar (`components/app-sidebar.tsx`, rows in `sidebar-items.ts`) beside the root stack, so it stays while a tool is open: the Current Debate switcher, then every tool under collapsible Debate, Prepare, Practice, Library and You sections (closed sections are kept on the device). NativeTabs hides its bar there (`hidden`), and `app-tabs.web.tsx` hides the preview's; Split View, Slide Over, Stage Manager windows and widths under 700 keep the tab bar (`sidebarWidthFor`). A sidebar row closes the screens above the tabs before it opens its own, so the sidebar never stacks tools. Screens lay out for the room beside the sidebar: `useLayoutClass()` and `useContentWidth()` subtract `ReservedWidth`, which sheets reset to 0; never size a screen from `useWindowDimensions()` directly. The Prepare and Practice tab pages are not in the sidebar: the Debate dashboard and History cover their lists. The Library opens a source beside its list (`SourceView`, shared with the source sheet), Ask lists its conversations beside the chat (`ask-chat-list.tsx`), the Debate tab is a two-column dashboard, saved documents get an "On this page" rail from 900 points (`DocumentScreen`'s `outline`, `components/outline.tsx`), and the Flowchart docks a card's claim beside the map (`ClaimPanel` in `claim-panel.tsx`, shared with the claim sheet). A docked claim or a source open beside its list is dropped when that map, debate, account or row is no longer the one on screen. All are plain views sized from the window, so they run in Expo Go.
- Share links (`share-links.ts`, `use-share-link.ts`): a saved case, attack, brainstorm or factoids document's menu calls the `share` Edge Function with the session and reuses a link made this session for the same unchanged result, because every call makes a new link and counts toward the daily limit. With no browser Origin the function builds debateowl.com links, so links made against staging open production's site. `share/[code].tsx` reads a link back with the public `get_shared_item` RPC. Universal links (`deep-links.ts`, a tested parser that honours only https debateowl.com and www) need the Associated Domains entitlement, which `app.config.ts` adds to development builds, and the website's apple-app-site-association file, which is not deployed.
- The first run (`first-run.ts`, `use-first-run.ts`, `welcome.tsx`) uses the website's keys: Ollie's welcome once per phone, the four-step tour once per account. The web preview opens it only with `?intro=show` (`first-run.web.ts`). `components/trial-banner.tsx` is the website's trial reminder (days left from `packages/core/src/trial.ts`); `auth.tsx`'s `sessionExpired` is set when the server ends a session (a refused refresh), never for a sign-out the student asked for, and the sign-in screen shows the website's notice.
- Accessibility: every text field is `AppTextInput` (`components/app-text-input.tsx`), with the one Done bar above the keyboard (`KeyboardDoneBar` in the root layout); `Screen`, `DocumentScreen` and sheets scroll fields clear of the keyboard. Text grows with Dynamic Type; only fixed chrome sets `maxFontSizeMultiplier` (floating action-bar buttons and segmented controls 1.6, the sidebar 1.35). The web preview draws a text size from `window.__debateowlFontScale` (`theme/font-scale.web.ts`, the script's `FONT_SCALE`). `a11y.ts` announces a finished Ask answer, Crossfire reply, Sparring speech or tool result once; never make streamed text a live region. iPad shortcuts (⌘N, ⌘K, ⌘↩) go through `key-commands.ts`, which works in the web preview only until a UIKeyCommand module is in the development build; the timer's space bar belongs to the timer work.
- The Current Debate widget (`widgets/current-debate.tsx`, published by `debate-widget.ts` from the debate context and the Debate tab) needs a development build with `DEBATELAB_APP_GROUP`. Quick actions (`quick-actions.ts`) and Spotlight (`spotlight.ts`) switch on only when `expo-quick-actions` or `expo-core-spotlight` is installed; neither is yet. `app.config.ts`'s round-4 section adds their native config for development builds only.
- Accessibility: every text field is `AppTextInput` (`components/app-text-input.tsx`), with the one Done bar above the keyboard (`KeyboardDoneBar` in the root layout); `Screen`, `DocumentScreen` and sheets scroll fields clear of the keyboard. Text grows with Dynamic Type; only fixed chrome sets `maxFontSizeMultiplier` (floating action-bar buttons and segmented controls 1.6, the sidebar 1.35). The web preview draws a text size from `window.__debateowlFontScale` (`theme/font-scale.web.ts`, the script's `FONT_SCALE`). `a11y.ts` announces a finished Ask answer, Crossfire reply, Sparring speech or tool result once; never make streamed text a live region. A list row's long-press menu is also its VoiceOver actions (`accessibilityActions`): saved work, History, Library sources, recent sources and the switcher. iPad shortcuts (⌘N new debate, ⌘K switcher, ⌘↩ send) go through `key-commands.ts`: one registry (`key-command-registry.ts`) gives a press to the newest screen listening, screens listen only while in view (`useFocusedKeyCommand`), and the development build delivers them from `modules/key-commands`, with titles in the ⌘ overlay; the web preview listens to `keydown` (`key-commands.web.ts`). The timer's space bar belongs to the timer work.
- The Current Debate widget (`widgets/current-debate.tsx`, published by `debate-widget.ts` from the debate context and the Debate tab) needs a development build with `DEBATELAB_APP_GROUP`. Quick actions (`quick-actions.ts`, `expo-quick-actions`) and Spotlight (`spotlight.ts`, `expo-core-spotlight` 2.x, whose config plugin 3.0 dropped) are in development builds; `app.config.ts`'s round-4 section adds their native config. Spotlight indexes the Current Debate's documents (`spotlight-items.ts`) as `debatelab://<kind>/<id>` links, and empties when the Current Debate changes, on sign-out and after Delete all my data.
- Sharing into the app (`share-intent.ts`, `expo-share-intent`): with the App Group, development builds get the `DebateOwlShare` extension. A shared page opens Add link and starts its import; other text opens New note filled in (`share-intent-parse.ts`). `+native-intent.tsx` hands the extension's `debatelab://dataUrl=…` to `share-intent.ts`, and the sheets take what was shared only from its one-shot hand-off, never from route parameters, so no other link can start an import.
- `src/theme/`: the website's DebateOwl design tokens (`src/styles/tokens.css` and the roles in `src/index.css`) in light and dark. `primary` is the blue that passes as text; `primaryFill` with `primaryLedge` is the button face and the ledge it sits on. Success is mint and always comes with a check mark. Fredoka (`weight="display"`) is for titles, the motion and button labels, Nunito for everything else, loaded with expo-font from `@expo-google-fonts`. Buttons are the website's tactile ones (`components/buttons.tsx`), which sink onto their ledge while pressed. Cards and fields have 2pt edges.
- `components/owl.tsx`: Ollie, from PNGs of the website's owl SVGs (`src/assets/owl`, `public/owl-mark.svg`), in the pose the website uses for each place: `point` for an empty start, `coach` beside Ask's answers, `arguing` for the Sparring opponent, `reading`, `surprised` and `sleepy` in empty states (`components/empty-state.tsx`), and a tool's pose while it works (`ToolProgress`). `scripts/render-owls.ts` renders them, the app icon, the splash image and the Android icons again after the website's owl changes.
- `scripts/screenshots.ts`: web-export screenshots. `scripts/render-tab-icons.ts` redraws the tab bar PNGs.
Expand All @@ -69,4 +73,4 @@ ONLY=offline PORT=18080 STAGING_DEMO_CODE=... npx tsx apps/mobile/scripts/screen

Screenshots go to `.verify/mobile-shots/`. For signed-in screens, see the header of `scripts/screenshots.ts`. To load the app on a phone, see [docs/development.md](../../docs/development.md#mobile-app).

knip ignores `expo-updates`: its Expo plugin expects the package whenever the app config does not disable updates, and EAS Update is not set up yet. It also ignores `expo-speech-recognition` and `expo-widgets`, which the app loads with `require` only in a build that has them, and `@expo/dom-webview`, which Expo's DOM components load by name.
knip ignores `expo-updates`: its Expo plugin expects the package whenever the app config does not disable updates, and EAS Update is not set up yet. It also ignores `expo-speech-recognition`, `expo-widgets`, `expo-quick-actions`, `expo-core-spotlight` and `expo-share-intent`, which the app reaches only by `require` or native module name in a build that has them, and `@expo/dom-webview`, which Expo's DOM components load by name.
26 changes: 23 additions & 3 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { type ConfigPlugin, withEntitlementsPlist } from 'expo/config-plugins';
// DEBATELAB_BUILD=dev (set by the `development` profile in eas.json) adds the
// native modules Expo Go lacks. Without it the config is the one Expo Go has
// always used. DEBATELAB_APP_GROUP adds the timer's widget extension (its Live
// Activity, Dynamic Island clock and lock-screen widget), which needs the App
// Group `group.com.pistack.debateowl`, registered for both
// `com.pistack.debateowl` and `com.pistack.debateowl.ExpoWidgetsTarget`. On a
// Activity, Dynamic Island clock and lock-screen widget) and the share
// extension, which need the App Group `group.com.pistack.debateowl`, registered
// for `com.pistack.debateowl`, `com.pistack.debateowl.ExpoWidgetsTarget` and
// `com.pistack.debateowl.share-extension`. On a
// device, set it only when the provisioning profile carries that group; a
// simulator build is not signed and always can.
// See docs/development.md#development-build.
Expand Down Expand Up @@ -256,6 +257,25 @@ export default ({ projectRoot }: ConfigContext): ExpoConfig => {
},
]);
}
if (appGroup) {
// The share extension: Safari and other apps share a web page or text into DebateOwl
// (src/lib/share-intent.ts). Its bundle id is `${bundleIdentifier}.share-extension`, which
// needs the same App Group registered on the Apple account. Text and pages only; files stay
// with Import file.
config.plugins!.push([
'expo-share-intent',
{
iosActivationRules: {
NSExtensionActivationSupportsWebURLWithMaxCount: 1,
NSExtensionActivationSupportsWebPageWithMaxCount: 1,
NSExtensionActivationSupportsText: true,
},
iosAppGroupIdentifier: APP_GROUP,
iosShareExtensionName: 'DebateOwl Share',
disableAndroid: true,
},
]);
}
// The Current Debate widget (with the App Group), quick actions and Spotlight; see withRound4 above.
withRound4(config, { appGroup, projectRoot });
// Registered before Expo applies the plugins list. Mods registered later act
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/modules/key-commands/expo-module.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"platforms": ["apple"],
"apple": {
"modules": ["KeyCommandsModule"]
}
}
19 changes: 19 additions & 0 deletions apps/mobile/modules/key-commands/ios/KeyCommands.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Pod::Spec.new do |s|
s.name = 'KeyCommands'
s.version = '1.0.0'
s.summary = "DebateOwl's iPad hardware-keyboard shortcuts"
s.description = 'UIKeyCommand shortcuts (⌘N, ⌘K, ⌘↩) for the DebateOwl app, set from JavaScript.'
s.author = ''
s.homepage = 'https://debateowl.com'
s.platforms = { :ios => '16.4' }
s.source = { git: '' }
s.static_framework = true

s.dependency 'ExpoModulesCore'

s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
}

s.source_files = "**/*.{h,m,mm,swift}"
end
Loading
Loading