Files
paseo/packages/app/src/components/realtime-voice-overlay.tsx
T
Mohamed BoudraandGitHub b97d6d13f3 Load complete chat history when reaching the top (#2481)
* fix(chat): load older timeline history consistently

Pagination depended on scroll events, so short or compacted initial pages could never request older history. Reevaluate edge visibility as layout and history change, and standardize loading indicators on the canonical app spinner.

* fix(chat): keep older history loading reliably

Use authoritative timeline cursor progress so merged rows cannot stall pagination. Wait for bottom anchoring before evaluating the history edge, and re-arm retries on a fresh upward gesture without automatic failure loops.
2026-07-27 18:21:07 +08:00

143 lines
4.1 KiB
TypeScript

import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Pressable, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { Mic, MicOff, Square } from "lucide-react-native";
import { FOOTER_HEIGHT } from "@/constants/layout";
import { useVoiceTelemetry } from "@/contexts/voice-context";
import { VolumeMeter } from "./volume-meter";
interface RealtimeVoiceOverlayProps {
isMuted: boolean;
isSwitching: boolean;
onToggleMute: () => void;
onStop: () => void;
}
const OVERLAY_BUTTON_SIZE = 44;
const OVERLAY_VERTICAL_PADDING = (FOOTER_HEIGHT - OVERLAY_BUTTON_SIZE) / 2;
export function RealtimeVoiceOverlay({
isMuted,
isSwitching,
onToggleMute,
onStop,
}: RealtimeVoiceOverlayProps) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const { volume, isSpeaking } = useVoiceTelemetry();
const muteButtonStyle = useMemo(
() => [
styles.actionButton,
styles.muteButton,
isMuted ? styles.muteButtonMuted : undefined,
isSwitching ? styles.buttonDisabled : undefined,
],
[isMuted, isSwitching],
);
const stopButtonStyle = useMemo(
() => [styles.actionButton, styles.stopButton, isSwitching ? styles.buttonDisabled : undefined],
[isSwitching],
);
return (
<View style={styles.container}>
<View style={styles.meterContainer}>
<VolumeMeter
volume={volume}
isMuted={isMuted}
isSpeaking={isSpeaking}
orientation="horizontal"
/>
</View>
<View style={styles.actionsContainer}>
<Pressable
onPress={onToggleMute}
disabled={isSwitching}
accessibilityRole="button"
accessibilityLabel={
isMuted ? t("realtimeVoice.actions.unmute") : t("realtimeVoice.actions.mute")
}
style={muteButtonStyle}
>
{isMuted ? (
<MicOff size={theme.iconSize.lg} color={theme.colors.palette.white} strokeWidth={2.5} />
) : (
<Mic size={theme.iconSize.lg} color={theme.colors.foreground} strokeWidth={2.5} />
)}
</Pressable>
<Pressable
onPress={onStop}
disabled={isSwitching}
accessibilityRole="button"
accessibilityLabel={t("realtimeVoice.actions.stop")}
style={stopButtonStyle}
>
{isSwitching ? (
<LoadingSpinner size="small" color={theme.colors.palette.white} />
) : (
<Square
size={theme.iconSize.lg}
color={theme.colors.palette.white}
fill={theme.colors.palette.white}
strokeWidth={2.5}
/>
)}
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
flexDirection: "row",
alignItems: "center",
width: "100%",
height: FOOTER_HEIGHT,
borderRadius: theme.borderRadius["2xl"],
justifyContent: "space-between",
paddingHorizontal: theme.spacing[4],
paddingVertical: OVERLAY_VERTICAL_PADDING,
backgroundColor: theme.colors.surface1,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
},
meterContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
actionsContainer: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
actionButton: {
width: OVERLAY_BUTTON_SIZE,
height: OVERLAY_BUTTON_SIZE,
borderRadius: theme.borderRadius.full,
alignItems: "center",
justifyContent: "center",
},
muteButton: {
backgroundColor: theme.colors.surface0,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
},
muteButtonMuted: {
backgroundColor: theme.colors.palette.red[600],
borderColor: theme.colors.palette.red[800],
},
stopButton: {
backgroundColor: theme.colors.palette.red[600],
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.palette.red[800],
},
buttonDisabled: {
opacity: 0.5,
},
}));