authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-03 22:45:53-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-03 22:45:53-07:00
log930aec13da56f83c404bc00d92486b2409a5cac0
treec11c30c894f4ba95fa047237a811e37781027567
parentfe93a5dbdd3c4496f402f71d232597c391c4973b
signature Signed by PGP key B5690EEEBB952194

fix(mobile): keep the dictation timer from shifting width (#15504)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

2 files changed, 33 insertions(+), 22 deletions(-)

apps/mobile/src/features/voice-input/ComposerDictationControl.tsx+24-8
...@@ -5,6 +5,7 @@ import {...@@ -5,6 +5,7 @@ import {
5 Linking,5 Linking,
6 Platform,6 Platform,
7 Pressable,7 Pressable,
8 Text as SystemText,
8 View,9 View,
9 type LayoutChangeEvent,10 type LayoutChangeEvent,
10} from "react-native";11} from "react-native";
...@@ -245,6 +246,25 @@ const VoiceWaveform = memo(function VoiceWaveform(props: {...@@ -245,6 +246,25 @@ const VoiceWaveform = memo(function VoiceWaveform(props: {
245 );246 );
246});247});
247248
249/**
250 * Renders the recording time in the system font: DM Sans has no tabular
251 * figures, so `tabular-nums` alone would still let the width jitter.
252 */
253export function DictationElapsedTime(props: {
254 readonly seconds: number;
255 readonly className?: string;
256}) {
257 return (
258 <SystemText
259 className={cn("text-xs", props.className)}
260 numberOfLines={1}
261 style={{ fontVariant: ["tabular-nums"] }}
262 >
263 {Math.floor(props.seconds / 60)}:{String(props.seconds % 60).padStart(2, "0")}
264 </SystemText>
265 );
266}
267
248function VoiceActionButton(props: {268function VoiceActionButton(props: {
249 readonly accessibilityLabel: string;269 readonly accessibilityLabel: string;
250 readonly disabled?: boolean;270 readonly disabled?: boolean;
...@@ -322,7 +342,6 @@ export function ComposerDictationStatus(props: {...@@ -322,7 +342,6 @@ export function ComposerDictationStatus(props: {
322342
323 if (!props.presentation.statusLabel) return null;343 if (!props.presentation.statusLabel) return null;
324 const isError = props.presentation.statusKind === "error";344 const isError = props.presentation.statusKind === "error";
325 const elapsedLabel = `${Math.floor(props.elapsedSeconds / 60)}:${String(props.elapsedSeconds % 60).padStart(2, "0")}`;
326 return (345 return (
327 <View className="relative h-11 min-w-0 flex-1 justify-center">346 <View className="relative h-11 min-w-0 flex-1 justify-center">
328 {isError ? (347 {isError ? (
...@@ -357,13 +376,10 @@ export function ComposerDictationStatus(props: {...@@ -357,13 +376,10 @@ export function ComposerDictationStatus(props: {
357 style={waveformStyle}376 style={waveformStyle}
358 >377 >
359 <VoiceWaveform audioLevels={props.audioLevels} />378 <VoiceWaveform audioLevels={props.audioLevels} />
360 <Text379 <DictationElapsedTime
361 className="text-xs text-foreground-muted"380 className="text-foreground-muted"
362 numberOfLines={1}381 seconds={props.elapsedSeconds}
363 style={{ fontVariant: ["tabular-nums"] }}382 />
364 >
365 {elapsedLabel}
366 </Text>
367 </Animated.View>383 </Animated.View>
368 <Animated.View className="absolute inset-0 justify-center px-2" style={labelStyle}>384 <Animated.View className="absolute inset-0 justify-center px-2" style={labelStyle}>
369 <Text className="text-center text-sm text-foreground-muted" numberOfLines={1}>385 <Text className="text-center text-sm text-foreground-muted" numberOfLines={1}>
apps/mobile/src/features/voice-input/GlobalVoiceInputControl.tsx+9-14
...@@ -24,7 +24,7 @@ import { AppText as Text } from "../../components/AppText";...@@ -24,7 +24,7 @@ import { AppText as Text } from "../../components/AppText";
24import { SymbolView } from "../../components/AppSymbol";24import { SymbolView } from "../../components/AppSymbol";
25import { useAndroidControlSizing } from "../../components/useAndroidControlSizing";25import { useAndroidControlSizing } from "../../components/useAndroidControlSizing";
26import { cn } from "../../lib/cn";26import { cn } from "../../lib/cn";
27import { CompactVoiceWaveform } from "./ComposerDictationControl";27import { CompactVoiceWaveform, DictationElapsedTime } from "./ComposerDictationControl";
28import { useGlobalVoiceInput } from "./VoiceInputProvider";28import { useGlobalVoiceInput } from "./VoiceInputProvider";
29import { resolveVoiceComposerPresentation } from "./voiceInputPresentation";29import { resolveVoiceComposerPresentation } from "./voiceInputPresentation";
3030
...@@ -114,7 +114,6 @@ function EdgeDictationPill(props: { readonly collapseRef: RefObject<(() => void)...@@ -114,7 +114,6 @@ function EdgeDictationPill(props: { readonly collapseRef: RefObject<(() => void)
114 const isError = phase === "error";114 const isError = phase === "error";
115 const openSettings = isError && voice.state.errorAction === "settings";115 const openSettings = isError && voice.state.errorAction === "settings";
116 const label = voice.label ?? "Draft";116 const label = voice.label ?? "Draft";
117 const elapsedLabel = `${Math.floor(voice.elapsedSeconds / 60)}:${String(voice.elapsedSeconds % 60).padStart(2, "0")}`;
118117
119 return (118 return (
120 <View pointerEvents="box-none" className="absolute inset-0">119 <View pointerEvents="box-none" className="absolute inset-0">
...@@ -149,12 +148,10 @@ function EdgeDictationPill(props: { readonly collapseRef: RefObject<(() => void)...@@ -149,12 +148,10 @@ function EdgeDictationPill(props: { readonly collapseRef: RefObject<(() => void)
149 ) : phase === "recording" ? (148 ) : phase === "recording" ? (
150 <>149 <>
151 <View className="size-2 rounded-full bg-danger-foreground" />150 <View className="size-2 rounded-full bg-danger-foreground" />
152 <Text151 <DictationElapsedTime
153 className="text-xs text-foreground"152 className="text-foreground"
154 style={{ fontVariant: ["tabular-nums"] }}153 seconds={voice.elapsedSeconds}
155 >154 />
156 {elapsedLabel}
157 </Text>
158 </>155 </>
159 ) : (156 ) : (
160 <ActivityIndicator size="small" colorClassName="accent-icon-muted" />157 <ActivityIndicator size="small" colorClassName="accent-icon-muted" />
...@@ -202,12 +199,10 @@ function EdgeDictationPill(props: { readonly collapseRef: RefObject<(() => void)...@@ -202,12 +199,10 @@ function EdgeDictationPill(props: { readonly collapseRef: RefObject<(() => void)
202 {phase === "recording" ? (199 {phase === "recording" ? (
203 <>200 <>
204 <CompactVoiceWaveform audioLevels={voice.audioLevels} />201 <CompactVoiceWaveform audioLevels={voice.audioLevels} />
205 <Text202 <DictationElapsedTime
206 className="text-xs text-foreground-muted"203 className="text-foreground-muted"
207 style={{ fontVariant: ["tabular-nums"] }}204 seconds={voice.elapsedSeconds}
208 >205 />
209 {elapsedLabel}
210 </Text>
211 <Pressable206 <Pressable
212 accessibilityLabel="Finish dictation"207 accessibilityLabel="Finish dictation"
213 accessibilityRole="button"208 accessibilityRole="button"