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
41 changes: 19 additions & 22 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import {
import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";
import { useUiRuntimeMemoryWarningGc } from "./lib/useUiRuntimeMemoryWarningGc";
import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator";
import { VoiceInputProvider } from "./features/voice-input/VoiceInputProvider";
import { GlobalVoiceInputControl } from "./features/voice-input/GlobalVoiceInputControl";
import { restoreSettingsLinkState } from "./features/settings/settings-navigation";
import {
Expand Down Expand Up @@ -138,27 +137,25 @@ function AppContent() {
<GestureHandlerRootView className="flex-1">
<KeyboardProvider statusBarTranslucent>
<SafeAreaProvider>
<VoiceInputProvider>
<StatusBar barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"} />
{/* native-stack needs the theme's dark flag to select dark native headers. */}
<GlobalVoiceInputControl>
<NativeLayoutMetricsProvider>
<IncomingShareProvider>
<Navigation
ref={navigationRef}
linking={appLinking}
onReady={() => recordWorkspaceSelections(navigationRef.getRootState())}
theme={navigationTheme}
onStateChange={recordWorkspaceSelections}
/>
</IncomingShareProvider>
<Dialog.Host />
<ThreadArrangementHost />
<ProjectChipIconRasterizer />
</NativeLayoutMetricsProvider>
</GlobalVoiceInputControl>
<OverlayPortalHost />
</VoiceInputProvider>
<StatusBar barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"} />
{/* native-stack needs the theme's dark flag to select dark native headers. */}
<GlobalVoiceInputControl>
<NativeLayoutMetricsProvider>
<IncomingShareProvider>
<Navigation
ref={navigationRef}
linking={appLinking}
onReady={() => recordWorkspaceSelections(navigationRef.getRootState())}
theme={navigationTheme}
onStateChange={recordWorkspaceSelections}
/>
</IncomingShareProvider>
<Dialog.Host />
<ThreadArrangementHost />
<ProjectChipIconRasterizer />
</NativeLayoutMetricsProvider>
</GlobalVoiceInputControl>
<OverlayPortalHost />
</SafeAreaProvider>
</KeyboardProvider>
</GestureHandlerRootView>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export function ComposerDictationToolbar(props: {
}

const WaveformBar = memo(function WaveformBar(props: {
readonly audioLevels: SharedValue<number[]>;
readonly audioLevels: SharedValue<ReadonlyArray<number>>;
readonly sampleIndex: number;
readonly height?: number;
}) {
Expand Down Expand Up @@ -177,7 +177,7 @@ const COMPACT_WAVEFORM_BAR_COUNT = 8;
const COMPACT_WAVEFORM_BAR_HEIGHT = 14;

export const CompactVoiceWaveform = memo(function CompactVoiceWaveform(props: {
readonly audioLevels: SharedValue<number[]>;
readonly audioLevels: SharedValue<ReadonlyArray<number>>;
}) {
return (
<View
Expand All @@ -199,7 +199,7 @@ export const CompactVoiceWaveform = memo(function CompactVoiceWaveform(props: {
});

const VoiceWaveform = memo(function VoiceWaveform(props: {
readonly audioLevels: SharedValue<number[]>;
readonly audioLevels: SharedValue<ReadonlyArray<number>>;
}) {
const [barCount, setBarCount] = useState(0);
const handleLayout = useCallback((event: LayoutChangeEvent) => {
Expand Down Expand Up @@ -311,7 +311,7 @@ function VoiceActionButton(props: {
}

export function ComposerDictationStatus(props: {
readonly audioLevels: SharedValue<number[]>;
readonly audioLevels: SharedValue<ReadonlyArray<number>>;
readonly elapsedSeconds: number;
readonly phase: VoiceInputPhase;
readonly presentation: VoiceComposerPresentation;
Expand Down
34 changes: 15 additions & 19 deletions apps/mobile/src/features/voice-input/GlobalVoiceInputControl.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator";
import { useEffect, useRef, useState, type ReactNode, type RefObject } from "react";
import { useState, type ReactNode } from "react";
import { Linking, Platform, Pressable, useWindowDimensions, View } from "react-native";
import Animated, {
Easing,
Expand All @@ -8,7 +8,7 @@ import Animated, {
interpolate,
ReduceMotion,
useAnimatedStyle,
useSharedValue,
useDerivedValue,
withTiming,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
Expand All @@ -35,18 +35,22 @@ const ENTERING = FadeIn.duration(180).reduceMotion(ReduceMotion.System);

export function GlobalVoiceInputControl(props: { readonly children: ReactNode }) {
const voice = useGlobalVoiceInput();
const collapseRef = useRef<(() => void) | null>(null);
const [expanded, setExpanded] = useState(false);
const presentation = resolveVoiceComposerPresentation(voice.state, voice.elapsedSeconds);
const visible =
presentation.statusLabel !== null &&
!(voice.ownerKey && voice.focusedOwners.has(voice.ownerKey));
const pill = visible ? <EdgeDictationPill collapseRef={collapseRef} /> : null;
// A pill that reappears starts collapsed.
if (!visible && expanded) setExpanded(false);
const pill = visible ? (
<EdgeDictationPill expanded={expanded} onExpandedChange={setExpanded} />
) : null;
return (
<>
<View
className="flex-1"
onStartShouldSetResponderCapture={() => {
collapseRef.current?.();
if (expanded) setExpanded(false);
return false;
}}
>
Expand All @@ -57,24 +61,16 @@ export function GlobalVoiceInputControl(props: { readonly children: ReactNode })
);
}

function EdgeDictationPill(props: { readonly collapseRef: RefObject<(() => void) | null> }) {
function EdgeDictationPill(props: {
readonly expanded: boolean;
readonly onExpandedChange: (expanded: boolean) => void;
}) {
const voice = useGlobalVoiceInput();
const insets = useSafeAreaInsets();
const { width } = useWindowDimensions();
const { fabSize } = useAndroidControlSizing();
const [expanded, setExpanded] = useState(false);
const progress = useSharedValue(0);
useEffect(() => {
progress.set(withTiming(expanded ? 1 : 0, MORPH_TIMING));
}, [expanded, progress]);
const { collapseRef } = props;
useEffect(() => {
if (!expanded) return;
collapseRef.current = () => setExpanded(false);
return () => {
collapseRef.current = null;
};
}, [collapseRef, expanded]);
const { expanded, onExpandedChange: setExpanded } = props;
const progress = useDerivedValue(() => withTiming(expanded ? 1 : 0, MORPH_TIMING));

const expandedWidth = Math.min(width - insets.left - insets.right - 32, EXPANDED_MAX_WIDTH);
const pillStyle = useAnimatedStyle(() => ({
Expand Down
Loading
Loading