feat: add delayed toast for history refresh with proper header spacing
This commit is contained in:
@@ -2,7 +2,11 @@ import type { ReactNode } from "react";
|
||||
import { View, type StyleProp, type ViewStyle } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
|
||||
import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from "@/constants/layout";
|
||||
import {
|
||||
HEADER_INNER_HEIGHT,
|
||||
HEADER_INNER_HEIGHT_MOBILE,
|
||||
HEADER_TOP_PADDING_MOBILE,
|
||||
} from "@/constants/layout";
|
||||
import { useTauriDragHandlers } from "@/utils/tauri-window";
|
||||
|
||||
interface ScreenHeaderProps {
|
||||
@@ -25,7 +29,7 @@ export function ScreenHeader({
|
||||
const insets = useSafeAreaInsets();
|
||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
|
||||
const topPadding = isMobile ? 8 : 0;
|
||||
const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
|
||||
|
||||
// On Tauri macOS, enable window dragging and double-click to maximize
|
||||
// Left padding for traffic lights is handled by _layout.tsx when sidebar is collapsed
|
||||
|
||||
@@ -8,6 +8,7 @@ export const FOOTER_HEIGHT = 75;
|
||||
// This ensures both headers have the same visual height
|
||||
export const HEADER_INNER_HEIGHT = 48;
|
||||
export const HEADER_INNER_HEIGHT_MOBILE = 56;
|
||||
export const HEADER_TOP_PADDING_MOBILE = 8;
|
||||
|
||||
// Max width for chat content (stream view, input area, new agent form)
|
||||
export const MAX_CONTENT_WIDTH = 820;
|
||||
|
||||
@@ -19,8 +19,13 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import { CheckCircle2, AlertTriangle } from "lucide-react-native";
|
||||
import {
|
||||
HEADER_INNER_HEIGHT,
|
||||
HEADER_INNER_HEIGHT_MOBILE,
|
||||
HEADER_TOP_PADDING_MOBILE,
|
||||
} from "@/constants/layout";
|
||||
|
||||
type ToastVariant = "default" | "success" | "error";
|
||||
|
||||
@@ -209,6 +214,11 @@ function ToastViewport({
|
||||
return null;
|
||||
}
|
||||
|
||||
const isMobile =
|
||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
const headerHeight = isMobile ? HEADER_INNER_HEIGHT_MOBILE : HEADER_INNER_HEIGHT;
|
||||
const headerTopPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
|
||||
|
||||
const icon =
|
||||
toast.icon ?? (
|
||||
toast.variant === "success" ? (
|
||||
@@ -227,7 +237,8 @@ function ToastViewport({
|
||||
toast.variant === "success" ? styles.toastSuccess : null,
|
||||
toast.variant === "error" ? styles.toastError : null,
|
||||
{
|
||||
marginTop: theme.spacing[2] + insets.top,
|
||||
marginTop:
|
||||
insets.top + headerTopPadding + headerHeight + theme.spacing[2],
|
||||
opacity,
|
||||
transform: [{ translateY }],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ActivityIndicator } from "react-native";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
|
||||
const HISTORY_REFRESH_TOAST_DELAY_MS = 1000;
|
||||
const HISTORY_REFRESH_TOAST_DURATION_MS = 2200;
|
||||
|
||||
interface UseDelayedHistoryRefreshToastParams {
|
||||
isCatchingUp: boolean;
|
||||
indicatorColor: string;
|
||||
}
|
||||
|
||||
export function useDelayedHistoryRefreshToast({
|
||||
isCatchingUp,
|
||||
indicatorColor,
|
||||
}: UseDelayedHistoryRefreshToastParams): void {
|
||||
const toast = useToast();
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const wasCatchingUpRef = useRef(false);
|
||||
const isCatchingUpRef = useRef(false);
|
||||
const toastRef = useRef(toast);
|
||||
const indicatorColorRef = useRef(indicatorColor);
|
||||
|
||||
useEffect(() => {
|
||||
toastRef.current = toast;
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
indicatorColorRef.current = indicatorColor;
|
||||
}, [indicatorColor]);
|
||||
|
||||
useEffect(() => {
|
||||
isCatchingUpRef.current = isCatchingUp;
|
||||
|
||||
const enteredCatchUp = !wasCatchingUpRef.current && isCatchingUp;
|
||||
const exitedCatchUp = wasCatchingUpRef.current && !isCatchingUp;
|
||||
|
||||
if (enteredCatchUp) {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
timerRef.current = setTimeout(() => {
|
||||
timerRef.current = null;
|
||||
if (!isCatchingUpRef.current) {
|
||||
return;
|
||||
}
|
||||
toastRef.current.show("Refreshing", {
|
||||
icon: (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={indicatorColorRef.current}
|
||||
/>
|
||||
),
|
||||
durationMs: HISTORY_REFRESH_TOAST_DURATION_MS,
|
||||
testID: "agent-history-refresh-toast",
|
||||
});
|
||||
}, HISTORY_REFRESH_TOAST_DELAY_MS);
|
||||
} else if (exitedCatchUp && timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
|
||||
wasCatchingUpRef.current = isCatchingUp;
|
||||
}, [isCatchingUp]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
type AgentScreenMissingState,
|
||||
} from "@/hooks/use-agent-screen-state-machine";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { useDelayedHistoryRefreshToast } from "@/hooks/use-delayed-history-refresh-toast";
|
||||
import { getInitDeferred, getInitKey } from "@/utils/agent-initialization";
|
||||
import {
|
||||
derivePendingPermissionKey,
|
||||
@@ -954,24 +955,19 @@ function AgentScreenContent({
|
||||
[theme.colors.primary, toast]
|
||||
);
|
||||
|
||||
const shouldEmitHistoryRefreshToast =
|
||||
const isHistoryRefreshCatchingUp =
|
||||
viewState.tag === "ready" &&
|
||||
viewState.sync.status === "catching_up" &&
|
||||
viewState.sync.shouldEmitHistoryRefreshToast;
|
||||
viewState.sync.ui === "toast";
|
||||
const shouldEmitSyncErrorToast =
|
||||
viewState.tag === "ready" &&
|
||||
viewState.sync.status === "sync_error" &&
|
||||
viewState.sync.shouldEmitSyncErrorToast;
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldEmitHistoryRefreshToast) {
|
||||
return;
|
||||
}
|
||||
toast.show("Refreshing agent history...", {
|
||||
durationMs: 2200,
|
||||
testID: "agent-history-refresh-toast",
|
||||
});
|
||||
}, [shouldEmitHistoryRefreshToast, toast]);
|
||||
useDelayedHistoryRefreshToast({
|
||||
isCatchingUp: isHistoryRefreshCatchingUp,
|
||||
indicatorColor: theme.colors.primary,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldEmitSyncErrorToast) {
|
||||
|
||||
Reference in New Issue
Block a user