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
6 changes: 4 additions & 2 deletions .agents/skills/verify-deepseekbot/features/phone-composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ Open a paired computer, then a Human/Bot DM or a group conversation. Use the mes

## Driving it with verify-app

Use `docs/agents/device-qa.md` and its designated runner, qualified native client and isolated QA Host. Drive the actual native app by accessible labels or `composer-input`, `composer-attach` and `composer-send`. Source tests in `apps/mobile/test/dictation.test.ts` and the style/system-text guards do not prove native geometry.
Use `docs/agents/device-qa.md` and its designated runner, qualified native client and isolated QA Host. Drive the actual native app by accessible labels or `composer-input`, `composer-attach`, `composer-send`, `composer-expand`, `composer-dictation`, `composer-dictation-cancel` and `jump-to-latest`. Source tests in `apps/mobile/test/dictation.test.ts` and the style/system-text guards do not prove native geometry.

1. Capture matched base/candidate empty and short-draft states: iOS has one continuous rounded surface around +, field and trailing control; default compact height is 56pt and the controls are at least 44pt. Check opaque Reduce Transparency fallback too.
2. Open the real soft keyboard, type several lines and move the caret. Open/close the attachment tray, pick and remove an owned fixture, expand/return from editing and preserve the draft and selection. Required controls and the current editing line remain reachable.
3. Repeat light/dark at default, intermediate and maximum system text. Change default → maximum → default live with a draft open; retain the editable field, draft and focus. Record CJK IME, VoiceOver and orientation separately if unavailable.
4. Send a uniquely marked message and read the QA Host Channel timeline: exact text, attachments and mention metadata agree. A Bot reply requires a usable model. Exercise dictation start, cancel and stop with microphone permission; audio/transcription availability is a separate prerequisite.
5. Check the unaffected Android separate-button path when its queue/device is available; never infer that result from an iOS capture.
5. For iOS native controls, tap the entire 44pt target (including its edge), open/close attachments, send exactly once and expand/return without losing input focus or draft. Scroll to history, receive unseen messages, verify the count badge, then tap the native return button and confirm it follows the latest message. Repeat with Reduce Transparency.
6. With working microphone/recognition, tap the mic then Stop; hold ≥300ms and release; hold and drag left ≥80pt to cancel or up ≥60pt to lock; interrupt an unlocked hold or add a second finger. Confirm recording cleanup, exact draft preservation/discard semantics and no message sent automatically. VoiceOver activation is a separate physical/assistive check, not proven by a mocked callback.
7. Check the unaffected Android separate-button path when its queue/device is available; never infer that result from an iOS capture.

## Observable proof

Expand Down
5 changes: 3 additions & 2 deletions apps/mobile/DESIGN.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions apps/mobile/src/ui/chat/composer-action.ios.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ReactNode } from 'react';
import { useDesignTheme } from '../design/theme';
import { composerControl } from './composer-chrome';
import { NativeComposerButton } from './native-composer-button.ios';
import type { ComposerSymbol } from './composer-symbol';

export function ComposerActionButton(props: {
label: string;
filled: boolean;
symbol: ComposerSymbol;
onPress(): void;
testID?: string;
children: ReactNode;
}) {
const theme = useDesignTheme();
return <NativeComposerButton {...props} size={composerControl(theme)} />;
}
57 changes: 57 additions & 0 deletions apps/mobile/src/ui/chat/composer-action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ReactNode } from 'react';
import { Pressable } from 'react-native';
import { pressTint, useDesignTheme } from '../design/theme';
import { radius, space } from '../design/tokens';
import { composerControl, composerPrimary, rippleFeedback } from './composer-chrome';
import type { ComposerSymbol } from './composer-symbol';

export function ComposerActionButton({
label,
filled,
onPress,
testID,
children,
}: {
label: string;
filled: boolean;
onPress(): void;
testID?: string;
symbol: ComposerSymbol;
children: ReactNode;
}) {
const theme = useDesignTheme();
const size = composerControl(theme);
return (
<Pressable
testID={testID}
accessibilityRole="button"
accessibilityLabel={label}
onPress={onPress}
hitSlop={space.x2}
android_ripple={
rippleFeedback
? {
color: pressTint(filled ? composerPrimary(theme).ink : theme.color.fg),
borderless: true,
radius: size / 2,
}
: undefined
}
style={({ pressed }) => ({
width: size,
height: size,
borderRadius: radius.round,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: filled
? composerPrimary(theme).fill
: pressed && !rippleFeedback
? pressTint(theme.color.fg)
: undefined,
opacity: filled && pressed && !rippleFeedback ? 0.7 : 1,
})}
>
{children}
</Pressable>
);
}
43 changes: 14 additions & 29 deletions apps/mobile/src/ui/chat/composer-chrome.ios.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Circle, Host, RoundedRectangle } from '@expo/ui/swift-ui';
import { accessibilityHidden, foregroundStyle, glassEffect } from '@expo/ui/swift-ui/modifiers';
import { NativeComposerButton } from './native-composer-button.ios';
import type { ComposerSymbol } from './composer-symbol';
import type { ReactNode } from 'react';
import { Pressable, StyleSheet, View } from 'react-native';
import { StyleSheet, View } from 'react-native';

import { pressTint, useDesignTheme, type DesignTheme } from '../design/theme';
import { edge, radius, space } from '../design/tokens';
import { useDesignTheme, type DesignTheme } from '../design/theme';
import { edge, space } from '../design/tokens';
import { useLiquidGlass } from '../primitives/drawer-material';

export function composerPrimary(theme: DesignTheme): { fill: string; ink: string } {
Expand Down Expand Up @@ -67,42 +69,25 @@ export function ComposerCircleButton({
size,
embedded = false,
onPress,
children,
symbol,
}: {
label: string;
testID?: string;
size: number;
embedded?: boolean;
onPress(): void;
symbol: ComposerSymbol;
children: ReactNode;
}) {
const theme = useDesignTheme();
const glass = useLiquidGlass();
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={label}
testID={testID}
<NativeComposerButton
label={label}
{...(testID === undefined ? {} : { testID })}
size={size}
embedded={embedded}
onPress={onPress}
hitSlop={Math.max(0, (theme.touch - size) / 2)}
style={({ pressed }) => ({
width: size,
height: size,
borderRadius: radius.round,
alignItems: 'center',
justifyContent: 'center',
...(embedded || glass
? { backgroundColor: pressed ? pressTint(theme.color.fg) : 'transparent' }
: {
borderWidth: edge.hairline,
borderColor: theme.color.borderStrong,
backgroundColor: pressed ? theme.layer.raised.pressed : theme.color.surface,
}),
})}
>
{glass && !embedded ? <Glass /> : null}
{children}
</Pressable>
symbol={symbol}
/>
);
}

Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/ui/chat/composer-chrome.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ComposerSymbol } from './composer-symbol';
import type { ReactNode } from 'react';
import { Pressable, StyleSheet, View } from 'react-native';

Expand Down Expand Up @@ -32,6 +33,7 @@ export function ComposerCircleButton({
size: number;
embedded?: boolean;
onPress(): void;
symbol: ComposerSymbol;
children: ReactNode;
}) {
const theme = useDesignTheme();
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/ui/chat/composer-symbol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ComposerSymbol = 'plus' | 'xmark' | 'arrow.up' | 'chevron.up' | 'chevron.down';
Loading
Loading