import type { EnvironmentProject, EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; import type { EnvironmentId } from "@t3tools/contracts"; import type { MenuAction } from "@react-native-menu/menu"; import * as Haptics from "expo-haptics"; import { Stack } from "expo-router"; import { SymbolView } from "expo-symbols"; import { useCallback, useRef } from "react"; import { ActivityIndicator, Pressable, RefreshControl, ScrollView, useWindowDimensions, View, } from "react-native"; import ReanimatedSwipeable, { type SwipeableMethods, } from "react-native-gesture-handler/ReanimatedSwipeable"; import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; import { EmptyState } from "../../components/EmptyState"; import { ProjectFavicon } from "../../components/ProjectFavicon"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import { THREAD_SWIPE_ACTIONS_WIDTH, THREAD_SWIPE_SPRING, ThreadSwipeActions, } from "../home/thread-swipe-actions"; import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList"; export interface ArchivedThreadsHeaderEnvironment { readonly environmentId: EnvironmentId; readonly label: string; } const THREAD_ACTIONS: MenuAction[] = [ { id: "unarchive", title: "Unarchive", image: "arrow.uturn.backward", }, { id: "delete", title: "Delete", image: "trash", attributes: { destructive: true }, }, ]; function ArchivedThreadsHeader(props: { readonly environments: ReadonlyArray; readonly selectedEnvironmentId: EnvironmentId | null; readonly sortOrder: ArchivedThreadSortOrder; readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; readonly onSearchQueryChange: (query: string) => void; readonly onSortOrderChange: (sortOrder: ArchivedThreadSortOrder) => void; }) { const hasCustomFilter = props.selectedEnvironmentId !== null || props.sortOrder !== "newest"; return ( <> { props.onSearchQueryChange(event.nativeEvent.text); }, onCancelButtonPress: () => { props.onSearchQueryChange(""); }, }, }} /> Environment props.onEnvironmentChange(null)} > All environments {props.environments.map((environment) => ( props.onEnvironmentChange(environment.environmentId)} > {environment.label} ))} Sort by archived date props.onSortOrderChange("newest")} > Newest first props.onSortOrderChange("oldest")} > Oldest first ); } function ProjectGroupLabel(props: { readonly environmentLabel: string | null; readonly project: EnvironmentProject; }) { return ( {props.project.title} {props.environmentLabel ? ( {props.environmentLabel} ) : null} ); } function ArchivedThreadRow(props: { readonly environmentLabel: string | null; readonly isLast: boolean; readonly onDelete: () => void; readonly onSwipeableClose: (methods: SwipeableMethods) => void; readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; readonly onUnarchive: () => void; readonly thread: EnvironmentThreadShell; }) { const swipeableRef = useRef(null); const fullSwipeArmedRef = useRef(false); const { width: windowWidth } = useWindowDimensions(); const cardColor = useThemeColor("--color-card"); const iconColor = useThemeColor("--color-icon-subtle"); const separatorColor = useThemeColor("--color-separator"); const fullSwipeThreshold = Math.max(THREAD_SWIPE_ACTIONS_WIDTH + 44, (windowWidth - 32) * 0.58); const timestamp = relativeTime(props.thread.archivedAt ?? props.thread.updatedAt); const subtitle = [props.environmentLabel, props.thread.branch].filter((part): part is string => Boolean(part), ); const handleFullSwipeArmedChange = useCallback((armed: boolean) => { if (armed && !fullSwipeArmedRef.current && process.env.EXPO_OS === "ios") { void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); } fullSwipeArmedRef.current = armed; }, []); const handleMenuAction = useCallback( (event: { nativeEvent: { event: string } }) => { if (event.nativeEvent.event === "unarchive") { props.onUnarchive(); } else if (event.nativeEvent.event === "delete") { props.onDelete(); } }, [props.onDelete, props.onUnarchive], ); return ( { fullSwipeArmedRef.current = false; if (swipeableRef.current) { props.onSwipeableClose(swipeableRef.current); } }} onSwipeableOpenStartDrag={() => { if (swipeableRef.current) { props.onSwipeableWillOpen(swipeableRef.current); } }} onSwipeableWillOpen={() => { const methods = swipeableRef.current; if (!methods) return; props.onSwipeableWillOpen(methods); if (fullSwipeArmedRef.current) { fullSwipeArmedRef.current = false; methods.close(); props.onDelete(); } }} overshootFriction={1} overshootRight renderRightActions={(_progress, translation, methods) => ( )} rightThreshold={THREAD_SWIPE_ACTIONS_WIDTH * 0.42} > {props.thread.title} {timestamp} {subtitle.length > 0 ? ( {subtitle.join(" · ")} ) : null} ); } function ArchiveError(props: { readonly message: string; readonly onRetry: () => void }) { return ( Could not load every archive {props.message} Try again ); } export function ArchivedThreadsScreen(props: { readonly environments: ReadonlyArray; readonly error: string | null; readonly groups: ReadonlyArray; readonly isLoading: boolean; readonly searchQuery: string; readonly selectedEnvironmentId: EnvironmentId | null; readonly sortOrder: ArchivedThreadSortOrder; readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; readonly onRefresh: () => void; readonly onSearchQueryChange: (query: string) => void; readonly onSortOrderChange: (sortOrder: ArchivedThreadSortOrder) => void; readonly onUnarchiveThread: (thread: EnvironmentThreadShell) => void; }) { const openSwipeableRef = useRef(null); const refreshTint = useThemeColor("--color-icon"); const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { if (openSwipeableRef.current && openSwipeableRef.current !== methods) { openSwipeableRef.current.close(); } openSwipeableRef.current = methods; }, []); const handleSwipeableClose = useCallback((methods: SwipeableMethods) => { if (openSwipeableRef.current === methods) { openSwipeableRef.current = null; } }, []); const isInitialLoad = props.isLoading && props.groups.length === 0 && props.error === null; const isFiltered = props.searchQuery.trim().length > 0 || props.selectedEnvironmentId !== null; return ( openSwipeableRef.current?.close()} refreshControl={ } showsVerticalScrollIndicator={false} > {props.error ? : null} {isInitialLoad ? ( Loading archive… ) : props.groups.length === 0 ? ( ) : ( props.groups.map((group) => { const environmentLabel = props.environments.find( (environment) => environment.environmentId === group.project.environmentId, )?.label ?? null; return ( {group.threads.map((thread, index) => ( props.onDeleteThread(thread)} onSwipeableClose={handleSwipeableClose} onSwipeableWillOpen={handleSwipeableWillOpen} onUnarchive={() => props.onUnarchiveThread(thread)} thread={thread} /> ))} ); }) )} ); }