Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b8524d0
fix(mobile): keep iOS Settings nested navigation visible
DoodleBears Oct 17, 2026
a10628c
docs(mobile): align Settings presentation contract
DoodleBears Oct 17, 2026
ca4eae0
docs(qa): preserve Settings native differential evidence
DoodleBears Oct 17, 2026
aa2de49
docs(mobile): define shared overlay contracts and acceptance gates
DoodleBears Oct 17, 2026
5ec2832
Merge remote-tracking branch 'origin/main' into codex/2107-ios-settin…
DoodleBears Oct 17, 2026
5794802
fix(mobile): give Settings a fixed header and functional dismissal ha…
DoodleBears Oct 17, 2026
c6f7916
fix(mobile): keep Settings close control clear at maximum text size
DoodleBears Oct 17, 2026
baa5777
docs(mobile): record Settings overlay implementation and regression path
DoodleBears Oct 17, 2026
6248306
fix(mobile): make statistics disclosure match Settings rows
DoodleBears Oct 17, 2026
db3c29b
fix(mobile): integrate Settings dismissal into the handle
DoodleBears Oct 17, 2026
79e306f
test(mobile): comply with source comment policy
DoodleBears Oct 17, 2026
c39cdd5
Merge main and retain Settings handle dismissal with chat backgrounds
DoodleBears Oct 18, 2026
0130e03
feat(mobile): glass title chip on Settings, pairing and Activity filt…
claude Oct 19, 2026
b0f9a98
docs(changes): link the mobile drawer title fragment to #2324
claude Oct 19, 2026
146cc59
docs(pr): iOS 26 Simulator before/after screenshots for #2324
DoodleBears Oct 19, 2026
9f4256c
fix(mobile): present Settings as a modal on iOS so it is not blank
DoodleBears Oct 19, 2026
c60573b
docs(pr): iOS 26 Settings after screenshots with the modal fix for #2324
DoodleBears Oct 19, 2026
aabc5c4
Merge main into #2112 and keep the overlay contract beside Android di…
claude Oct 19, 2026
d490682
fix(mobile): leave the Settings sheet to #2112
claude Oct 19, 2026
5a6975a
feat(mobile): one text field look, confirmed destructive actions and …
claude Oct 19, 2026
ee0e7bd
feat(mobile): float the workspace sidebar on Liquid Glass on iOS 26
claude Oct 19, 2026
22cce94
feat(mobile): float the Chats search field above the bottom navigation
claude Oct 19, 2026
68e7b56
docs(changes): note the iOS 26 HIG pass
claude Oct 19, 2026
0556392
feat(mobile): close the bottom search from a glass button beside it
claude Oct 19, 2026
249ba8e
Merge remote-tracking branch 'origin/codex/2107-ios-settings-sheet' i…
claude Oct 19, 2026
de22da3
feat(mobile): give the Settings sheet the glass title capsule
claude Oct 19, 2026
0146ed0
chore(mobile): drop unused Settings imports
claude Oct 19, 2026
a4a661f
feat(mobile): soft scroll edge and glass header in chats on iOS 26
claude Oct 19, 2026
273389c
feat(mobile): put the sheet close button on the leading side
claude Oct 19, 2026
1b1b4d0
feat(mobile): native Material dropdown for the Host menu on Android
claude Oct 19, 2026
986b8b7
feat(mobile): Material 3 system dialogs for Android confirmations and…
claude Oct 19, 2026
92b243f
feat(mobile): native ⋯ menu for group members; merge main
claude Oct 19, 2026
5a223b5
fix(mobile): Material menus as bottom sheets, dropdown divider and ri…
claude Oct 19, 2026
3415260
feat(mobile): system icons on Settings rows
claude Oct 19, 2026
50d4123
feat(mobile): centre sheet titles between close and confirm slots
claude Oct 19, 2026
14e27d4
feat(mobile): compact pairing sheet with scan and paste captions
claude Oct 19, 2026
6e31448
feat(mobile): Activity tab shows the bell or the count, never both
claude Oct 19, 2026
bd3cb3a
test(mobile): iOS 26 drawer-title screenshots on 50d41235b
DoodleBears Oct 19, 2026
b9f692a
Merge #2324 drawer titles; fix batch-49 findings
claude Oct 19, 2026
ad11c1a
test(mobile): iOS 26 HIG after-shots on b9f692a63
DoodleBears Oct 19, 2026
4b0f9c0
fix(mobile): chat edge blur reaches the status bar strip
claude Oct 19, 2026
af73961
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Oct 19, 2026
d60bff0
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Oct 19, 2026
b683744
test(mobile): re-shoot iOS 26 HIG step 7 on d60bff0a9
DoodleBears Oct 19, 2026
3d6d537
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Oct 19, 2026
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 15 additions & 13 deletions apps/mobile/DESIGN.md

Large diffs are not rendered by default.

23 changes: 13 additions & 10 deletions apps/mobile/app/(tabs)/(home)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import { conversations, searchKey, type Conversation } from '../../../src/ui/cha
import { CloudConversationRow } from '../../../src/ui/chat/cloud-conversation-row';
import { useConversationFilter } from '../../../src/host/native-conversation-filter';
import { ConnectionBanner } from '../../../src/ui/connection-banner';
import { useDesignTheme } from '../../../src/ui/design/theme';
import { space } from '../../../src/ui/design/tokens';
import { SearchField } from '../../../src/ui/primitives/search-field';
import { BottomSearch, bottomSearchHeight } from '../../../src/ui/shell/bottom-search';
import { EnvironmentPager } from '../../../src/ui/shell/environment-pager';
import {
conversationEnvironments,
Expand Down Expand Up @@ -98,13 +99,15 @@ const HomeContent = memo(function HomeContent({
};
}, [router, client]),
);
const onScroll = useChromeScroll();
const [query, setQuery] = useState('');
const [searching, setSearching] = useState(false);
const onScroll = useChromeScroll(true, searching || query !== '');
const chrome = useScrollChrome();
const bottom = useFloatingTabInset();
const theme = useDesignTheme();
const bottom = useFloatingTabInset() + bottomSearchHeight(theme.touch);
const look = useHostLook(saved?.hostId);
const cloud = useCloudSnapshot();
const directory = useCloudConversations();
const [query, setQuery] = useState('');
const [filter, setFilter] = useConversationFilter();
const [refreshing, setRefreshing] = useState(false);
const [opening, setOpening] = useState(false);
Expand Down Expand Up @@ -300,12 +303,6 @@ const HomeContent = memo(function HomeContent({
onPairAgain={() => router.push('/pair')}
/>
)}
<SearchField
inset={space.x5}
value={query}
placeholder={t.searchPlaceholder}
onChangeText={setQuery}
/>
{(directory.errors.length > 0 || cloud.error || error) && (
<Text tone="muted" style={{ paddingHorizontal: space.x5 }}>
{t.cloudListUnavailable}
Expand Down Expand Up @@ -347,6 +344,12 @@ const HomeContent = memo(function HomeContent({
)}
/>
</HomeHeader>
<BottomSearch
value={query}
placeholder={t.searchPlaceholder}
onChangeText={setQuery}
onFocusChange={setSearching}
/>
</HostDrawerEdge>
</>
);
Expand Down
156 changes: 84 additions & 72 deletions apps/mobile/app/pair.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Text, TextInput } from '../src/ui/primitives/system-text';
import { TextField } from '../src/ui/primitives/text-field';
import { CameraView, useCameraPermissions } from 'expo-camera';
import Constants from 'expo-constants';
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router';
import { useCallback, useRef, useState } from 'react';
import { Platform, Pressable, View } from 'react-native';
import { Platform, Pressable, View, useWindowDimensions } from 'react-native';

import { hostSession } from '../src/host/host-session';
import { hostLooks } from '../src/host/native-host-look';
Expand All @@ -16,19 +16,21 @@ import { mobileTelemetry } from '../src/telemetry/native';
import type { PairingMethod } from '../src/telemetry/schema';
import { Button } from '../src/ui/primitives/button';
import { DrawerHeader } from '../src/ui/primitives/drawer-header';
import { useDesignTheme } from '../src/ui/design/theme';
import { KeyboardFormScroll } from '../src/ui/primitives/keyboard-form';
import { cloudAvailable } from '../src/cloud/available';
import { t } from '../src/ui/strings';
import { useTheme } from '../src/ui/theme';
import { useDesignTheme } from '../src/ui/design/theme';
import { pairingScan, radius, space } from '../src/ui/design/tokens';
import { Text } from '../src/ui/primitives/text';

function deviceName(): string {
return Constants.deviceName ?? (Platform.OS === 'ios' ? 'iPhone' : 'Android phone');
}

export default function PairScreen() {
const theme = useTheme();
const design = useDesignTheme();
const theme = useDesignTheme();
const { width } = useWindowDimensions();
const scan = Math.min(width * pairingScan.widthRatio, pairingScan.maxSize);
const router = useRouter();
const params = useLocalSearchParams<{ p?: string }>();
const [permission, requestPermission] = useCameraPermissions();
Expand Down Expand Up @@ -92,60 +94,60 @@ export default function PairScreen() {
<KeyboardFormScroll
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
style={{ flex: 1, backgroundColor: design.overlay.fill }}
contentContainerStyle={{ padding: 16, gap: 12 }}
style={{ flex: 1, backgroundColor: theme.overlay.fill }}
contentContainerStyle={{ padding: space.x5, gap: space.x4 }}
>
<DrawerHeader
title={t.pairTitle}
{...(router.canGoBack() ? { onClose: () => router.back() } : {})}
/>
{cloudAvailable && (
<Button label={t.cloud.title} onPress={() => router.dismissTo('/cloud-services')} />
)}
<Text style={{ color: theme.inkSoft, fontSize: 14 }}>{t.pairHint}</Text>
<ConnectionNameField
value={connectionName}
onChange={setConnectionName}
maxLength={HOST_NAME_MAX}
disabled={busy}
/>
<View
style={{
aspectRatio: 1,
borderRadius: 16,
overflow: 'hidden',
backgroundColor: design.layer.raised.fill,
alignItems: 'center',
justifyContent: 'center',
gap: space.x4,
padding: space.x5,
borderRadius: radius.lg,
backgroundColor: theme.layer.raised.fill,
}}
>
{permission?.granted ? (
focused && (
<CameraView
style={{ width: '100%', height: '100%' }}
facing="back"
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
onBarcodeScanned={busy ? undefined : ({ data }) => void pair(data, 'qr')}
/>
)
) : (
<Pressable
onPress={() => void requestPermission()}
accessibilityRole="button"
style={{ padding: 16 }}
>
<Text style={{ color: theme.accent, fontSize: 16, fontWeight: '600' }}>
{t.allowCamera}
</Text>
</Pressable>
)}
<View
style={{
width: scan,
height: scan,
borderRadius: radius.lg,
overflow: 'hidden',
backgroundColor: theme.layer.well.fill,
alignItems: 'center',
justifyContent: 'center',
}}
>
{permission?.granted ? (
focused && (
<CameraView
style={{ width: '100%', height: '100%' }}
facing="back"
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
onBarcodeScanned={busy ? undefined : ({ data }) => void pair(data, 'qr')}
/>
)
) : (
<Pressable
onPress={() => void requestPermission()}
accessibilityRole="button"
style={{ padding: space.x5, minHeight: theme.touch, justifyContent: 'center' }}
>
<Text tone="accent" strong style={{ textAlign: 'center' }}>
{t.allowCamera}
</Text>
</Pressable>
)}
</View>
<Text variant="meta" tone="muted" style={{ textAlign: 'center' }}>
{t.pairHint}
</Text>
</View>
{error !== undefined ? (
<Text
accessibilityRole="alert"
accessibilityLiveRegion="polite"
style={{ color: theme.danger }}
>
<Text accessibilityRole="alert" accessibilityLiveRegion="polite" tone="danger">
{pairingFailureText(error, lastMethod.current)}
</Text>
) : null}
Expand All @@ -155,30 +157,33 @@ export default function PairScreen() {
onPress={() => void pair(handled.current!, lastMethod.current, true)}
/>
) : null}
<Text style={{ color: theme.ink, fontWeight: '600' }}>{t.pasteLink}</Text>
<TextInput
accessibilityLabel={t.pasteLink}
value={link}
onChangeText={(value) => {
setLink(value);
handled.current = undefined;
}}
placeholder="deepseekbot://pair?p=…"
placeholderTextColor={theme.inkSoft}
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
returnKeyType="go"
onSubmitEditing={() => void pair(link, 'paste')}
style={{
borderWidth: 1,
borderColor: theme.border,
borderRadius: 12,
padding: 12,
fontSize: 16,
color: theme.ink,
backgroundColor: theme.surface,
}}
<View style={{ gap: space.x2 }}>
<Text variant="meta" tone="muted" accessibilityRole="header">
{t.pasteLink}
</Text>
<TextField
accessibilityLabel={t.pasteLink}
value={link}
onChangeText={(value) => {
setLink(value);
handled.current = undefined;
}}
placeholder="deepseekbot://pair?p=…"
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
returnKeyType="go"
onSubmitEditing={() => void pair(link, 'paste')}
/>
<Text variant="meta" tone="muted">
{t.pasteLinkHint}
</Text>
</View>
<ConnectionNameField
value={connectionName}
onChange={setConnectionName}
maxLength={HOST_NAME_MAX}
disabled={busy}
/>
<Button
label={t.pair}
Expand All @@ -188,6 +193,13 @@ export default function PairScreen() {
disabled={busy || link.trim() === ''}
onPress={() => void pair(link, 'paste')}
/>
{cloudAvailable && (
<Button
label={t.cloud.title}
variant="text"
onPress={() => router.dismissTo('/cloud-services')}
/>
)}
</KeyboardFormScroll>
);
}
2 changes: 2 additions & 0 deletions apps/mobile/app/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { StatisticsSettings } from '../../src/ui/shell/statistics-settings';
import { MessageDisplaySettings } from '../../src/ui/shell/message-display-settings';
import { ActivityCountLink } from '../../src/ui/shell/activity-count-settings';
import { SHEET_GROUP } from '../../src/ui/chat/sheet';
import { RowIcon } from '../../src/ui/primitives/row-icon';
import { SettingsRow } from '../../src/ui/primitives/settings-row';
import { t } from '../../src/ui/strings';
import { SettingsView } from '../../src/ui/settings-view';
Expand All @@ -33,6 +34,7 @@ export default function SettingsScreen() {
<SettingsRow
on={SHEET_GROUP}
title={t.chatBackground.global}
leading={<RowIcon name="background" />}
disclosure
onPress={() => router.push('/settings/chat-background')}
/>
Expand Down
12 changes: 7 additions & 5 deletions apps/mobile/src/ui/chat/chat-title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Pressable, View } from 'react-native';

import type { RosterBot } from '../../host/client';
import { BotAvatar } from '../bot-avatar';
import { frosted, useDesignTheme } from '../design/theme';
import { edge, radius, space } from '../design/tokens';
import { useDesignTheme } from '../design/theme';
import { radius, space } from '../design/tokens';
import { DrawerMaterial, useLiquidGlass } from '../primitives/drawer-material';
import { t } from '../strings';
import { botDotTone } from './bot-tone';
import { headerGlassFill } from './header-chrome';

const AVATAR = 28;
const DOT = space.x3;
Expand All @@ -21,6 +23,7 @@ export function ChatTitle({
onPress(): void;
}) {
const theme = useDesignTheme();
const glass = useLiquidGlass();
const status = bot === undefined ? undefined : bot.paused ? t.paused : t.state[bot.state];
const name = bot?.displayName ?? fallback;
return (
Expand All @@ -39,11 +42,10 @@ export function ChatTitle({
paddingLeft: bot === undefined ? theme.touch / 2 : (theme.touch - AVATAR) / 2,
paddingRight: bot === undefined ? theme.touch / 2 : (theme.touch - DOT) / 2,
borderRadius: radius.round,
borderWidth: edge.hairline,
borderColor: theme.layer.raised.edge,
backgroundColor: pressed ? theme.layer.raised.pressed : frosted(theme.layer.raised.fill),
...headerGlassFill(theme, glass, pressed),
})}
>
{glass ? <DrawerMaterial /> : null}
{bot === undefined ? null : <BotAvatar bot={bot} size={AVATAR} ring={false} />}
<SourceName name={name} source="computer" />
{bot === undefined ? null : (
Expand Down
8 changes: 6 additions & 2 deletions apps/mobile/src/ui/chat/conversation-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,11 @@ function ChatConversation({
/>
</SpeakerStageContext.Provider>
</MarkdownViewportContext.Provider>
<EdgeFade edge="bottom" height={edgeFade.bottom} />
<EdgeFade
edge="bottom"
height={composerHeight + edgeFade.bottom}
fade={edgeFade.bottom}
/>
{latest.away ? (
<View
pointerEvents="box-none"
Expand All @@ -844,7 +848,7 @@ function ChatConversation({
) : null}
</Animated.View>
</KeyboardChatFrame>
<EdgeFade edge="top" height={headerHeight + edgeFade.top} />
<EdgeFade edge="top" height={headerHeight + edgeFade.top} fade={edgeFade.top} />
<ChatHeader
left={<HeaderBackButton onPress={leave} />}
center={
Expand Down
Loading
Loading