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 { View, type StyleProp, type ViewStyle } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
|
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";
|
import { useTauriDragHandlers } from "@/utils/tauri-window";
|
||||||
|
|
||||||
interface ScreenHeaderProps {
|
interface ScreenHeaderProps {
|
||||||
@@ -25,7 +29,7 @@ export function ScreenHeader({
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
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
|
// 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
|
// 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
|
// 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
|
// This ensures both headers have the same visual height
|
||||||
export const HEADER_INNER_HEIGHT = 48;
|
export const HEADER_INNER_HEIGHT = 48;
|
||||||
export const HEADER_INNER_HEIGHT_MOBILE = 56;
|
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)
|
// Max width for chat content (stream view, input area, new agent form)
|
||||||
export const MAX_CONTENT_WIDTH = 820;
|
export const MAX_CONTENT_WIDTH = 820;
|
||||||
|
|||||||
@@ -19,8 +19,13 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
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 { 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";
|
type ToastVariant = "default" | "success" | "error";
|
||||||
|
|
||||||
@@ -209,6 +214,11 @@ function ToastViewport({
|
|||||||
return null;
|
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 =
|
const icon =
|
||||||
toast.icon ?? (
|
toast.icon ?? (
|
||||||
toast.variant === "success" ? (
|
toast.variant === "success" ? (
|
||||||
@@ -227,7 +237,8 @@ function ToastViewport({
|
|||||||
toast.variant === "success" ? styles.toastSuccess : null,
|
toast.variant === "success" ? styles.toastSuccess : null,
|
||||||
toast.variant === "error" ? styles.toastError : null,
|
toast.variant === "error" ? styles.toastError : null,
|
||||||
{
|
{
|
||||||
marginTop: theme.spacing[2] + insets.top,
|
marginTop:
|
||||||
|
insets.top + headerTopPadding + headerHeight + theme.spacing[2],
|
||||||
opacity,
|
opacity,
|
||||||
transform: [{ translateY }],
|
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,
|
type AgentScreenMissingState,
|
||||||
} from "@/hooks/use-agent-screen-state-machine";
|
} from "@/hooks/use-agent-screen-state-machine";
|
||||||
import { useToast } from "@/contexts/toast-context";
|
import { useToast } from "@/contexts/toast-context";
|
||||||
|
import { useDelayedHistoryRefreshToast } from "@/hooks/use-delayed-history-refresh-toast";
|
||||||
import { getInitDeferred, getInitKey } from "@/utils/agent-initialization";
|
import { getInitDeferred, getInitKey } from "@/utils/agent-initialization";
|
||||||
import {
|
import {
|
||||||
derivePendingPermissionKey,
|
derivePendingPermissionKey,
|
||||||
@@ -954,24 +955,19 @@ function AgentScreenContent({
|
|||||||
[theme.colors.primary, toast]
|
[theme.colors.primary, toast]
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldEmitHistoryRefreshToast =
|
const isHistoryRefreshCatchingUp =
|
||||||
viewState.tag === "ready" &&
|
viewState.tag === "ready" &&
|
||||||
viewState.sync.status === "catching_up" &&
|
viewState.sync.status === "catching_up" &&
|
||||||
viewState.sync.shouldEmitHistoryRefreshToast;
|
viewState.sync.ui === "toast";
|
||||||
const shouldEmitSyncErrorToast =
|
const shouldEmitSyncErrorToast =
|
||||||
viewState.tag === "ready" &&
|
viewState.tag === "ready" &&
|
||||||
viewState.sync.status === "sync_error" &&
|
viewState.sync.status === "sync_error" &&
|
||||||
viewState.sync.shouldEmitSyncErrorToast;
|
viewState.sync.shouldEmitSyncErrorToast;
|
||||||
|
|
||||||
useEffect(() => {
|
useDelayedHistoryRefreshToast({
|
||||||
if (!shouldEmitHistoryRefreshToast) {
|
isCatchingUp: isHistoryRefreshCatchingUp,
|
||||||
return;
|
indicatorColor: theme.colors.primary,
|
||||||
}
|
});
|
||||||
toast.show("Refreshing agent history...", {
|
|
||||||
durationMs: 2200,
|
|
||||||
testID: "agent-history-refresh-toast",
|
|
||||||
});
|
|
||||||
}, [shouldEmitHistoryRefreshToast, toast]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldEmitSyncErrorToast) {
|
if (!shouldEmitSyncErrorToast) {
|
||||||
|
|||||||
Reference in New Issue
Block a user