import type { EnvironmentProject, EnvironmentThreadShell,} from "@t3tools/client-runtime/state/shell";import { LegendList } from "@legendapp/list/react-native";import type { EnvironmentId } from "@t3tools/contracts";import type { MenuAction } from "@react-native-menu/menu";import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";import { SymbolView } from "../../components/AppSymbol";import { useNavigation } from "@react-navigation/native";import { useCallback, useMemo, useRef, type ComponentProps } from "react";import { TextInput, ActivityIndicator, Platform, Pressable, RefreshControl, useWindowDimensions, View,} from "react-native";import { Gesture, GestureDetector } from "react-native-gesture-handler";import 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 { useSafeAreaInsets } from "react-native-safe-area-context";import { relativeTime } from "../../lib/time";import { useUniwindTheme } from "../../lib/useUniwindTheme";import { ThreadSwipeable } from "../home/thread-swipe-actions";import { createNativeMailSearchToolbarItem, NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,} from "../layout/native-mail-search-toolbar";import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList"; export interface ArchivedThreadsHeaderEnvironment { readonly environmentId: EnvironmentId; readonly label: string;} type ArchivedThreadListItem = | { readonly kind: "project"; readonly key: string; readonly environmentLabel: string | null; readonly project: EnvironmentProject; } | { readonly kind: "thread"; readonly key: string; readonly environmentLabel: string | null; readonly isFirst: boolean; readonly isLast: boolean; readonly thread: EnvironmentThreadShell; }; function ArchivedThreadsHeader(props: { readonly environments: ReadonlyArray<ArchivedThreadsHeaderEnvironment>; readonly searchQuery: string; readonly selectedEnvironmentId: EnvironmentId | null; readonly sortOrder: ArchivedThreadSortOrder; readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; readonly onRefresh: () => void; readonly onSearchQueryChange: (query: string) => void; readonly onSortOrderChange: (sortOrder: ArchivedThreadSortOrder) => void;}) { const { width } = useWindowDimensions(); const navigation = useNavigation(); const insets = useSafeAreaInsets(); const hasCustomFilter = props.selectedEnvironmentId !== null || props.sortOrder !== "newest"; const usesNativeChrome = Platform.OS === "ios"; const usesCompactMailToolbar = Platform.OS === "ios" && width < 700 && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; const androidFilterActions = useMemo<MenuAction[]>( () => [ { id: "environment", title: "Environment", subactions: [ { id: "environment:all", title: "All environments", state: props.selectedEnvironmentId === null ? ("on" as const) : undefined, }, ...props.environments.map((environment) => ({ id: `environment:${environment.environmentId}`, title: environment.label, state: props.selectedEnvironmentId === environment.environmentId ? ("on" as const) : undefined, })), ], }, { id: "sort", title: "Sort by archived date", subactions: [ { id: "sort:newest", title: "Newest first", state: props.sortOrder === "newest" ? ("on" as const) : undefined, }, { id: "sort:oldest", title: "Oldest first", state: props.sortOrder === "oldest" ? ("on" as const) : undefined, }, ], }, ], [props.environments, props.selectedEnvironmentId, props.sortOrder], ); const handleAndroidFilterAction = useCallback( (event: { nativeEvent: { event: string } }) => { const action = event.nativeEvent.event; if (action === "environment:all") { props.onEnvironmentChange(null); } else if (action.startsWith("environment:")) { props.onEnvironmentChange(action.slice("environment:".length) as EnvironmentId); } else if (action === "sort:newest") { props.onSortOrderChange("newest"); } else if (action === "sort:oldest") { props.onSortOrderChange("oldest"); } }, [props.onEnvironmentChange, props.onSortOrderChange], ); if (Platform.OS === "android") { // Single header row matching the app's Android chrome (AndroidScreenHeader // palette): back chevron, inline search, filter menu. return ( <> <NativeStackScreenOptions options={{ headerShown: false }} /> <View className="border-b border-header-border bg-header px-3 pb-2.5" style={{ paddingTop: Math.max(insets.top, 12), }} > <View className="min-h-12 flex-row items-center gap-2"> <Pressable accessibilityLabel="Navigate up" accessibilityRole="button" hitSlop={8} onPress={() => navigation.goBack()} className="size-11 items-center justify-center" > <SymbolView name="chevron.left" size={24} tintColorClassName={"accent-foreground"} type="monochrome" /> </Pressable> <View className="min-h-11 flex-1 flex-row items-center gap-2.5 rounded-2xl bg-input px-3.5"> <SymbolView name="magnifyingglass" size={17} tintColorClassName={"accent-icon"} type="monochrome" /> <TextInput accessibilityLabel="Search archived threads" autoCapitalize="none" onChangeText={props.onSearchQueryChange} value={props.searchQuery} placeholder="Search archived threads" placeholderTextColorClassName="accent-placeholder" className="flex-1 py-2 text-base font-sans text-foreground" /> </View> <ControlPillMenu actions={androidFilterActions} isAnchoredToRight onPressAction={handleAndroidFilterAction} > <Pressable accessibilityLabel="Filter and sort archived threads" accessibilityRole="button" className="size-11 items-center justify-center rounded-full bg-subtle" > <SymbolView name={ hasCustomFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle" } size={16} tintColorClassName={"accent-icon"} type="monochrome" /> </Pressable> </ControlPillMenu> </View> </View> </> ); } const archiveFilterMenu = { title: "Archived thread options", items: [ { type: "submenu" as const, title: "Environment", items: [ { type: "action" as const, title: "All environments", state: props.selectedEnvironmentId === null ? ("on" as const) : ("off" as const), onPress: () => props.onEnvironmentChange(null), }, ...props.environments.map((environment) => ({ type: "action" as const, title: environment.label, state: props.selectedEnvironmentId === environment.environmentId ? ("on" as const) : ("off" as const), onPress: () => props.onEnvironmentChange(environment.environmentId), })), ], }, { type: "submenu" as const, title: "Sort by archived date", items: [ { type: "action" as const, title: "Newest first", state: props.sortOrder === "newest" ? ("on" as const) : ("off" as const), onPress: () => props.onSortOrderChange("newest"), }, { type: "action" as const, title: "Oldest first", state: props.sortOrder === "oldest" ? ("on" as const) : ("off" as const), onPress: () => props.onSortOrderChange("oldest"), }, ], }, ], }; return ( <> {/* Static header config (glass preset + title) lives in Stack.tsx; only dynamic toolbar/search wiring is set here. */} <NativeStackScreenOptions options={{ unstable_headerToolbarItems: usesCompactMailToolbar ? () => [ createNativeMailSearchToolbarItem({ composeButtonId: "archived-refresh", composeSystemImageName: "arrow.clockwise", filterMenu: archiveFilterMenu, filterButtonId: "archived-filter", filterSystemImageName: hasCustomFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease", onComposePress: props.onRefresh, onSearchTextChange: props.onSearchQueryChange, placeholder: "Search", searchTextChangeId: "archived-search-text", }), ] : undefined, headerSearchBarOptions: usesCompactMailToolbar ? undefined : { ...(usesNativeChrome ? { allowToolbarIntegration: true, // "integratedButton" is an iOS 26 search-bar placement; // pre-glass iOS keeps the default pull-down placement. ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED ? { placement: "integratedButton" as const } : null), } : { placement: "stacked" as const, }), autoCapitalize: "none", hideNavigationBar: false, obscureBackground: false, placeholder: "Search archived threads", onChangeText: (event) => { props.onSearchQueryChange(event.nativeEvent.text); }, onCancelButtonPress: () => { props.onSearchQueryChange(""); }, }, }} /> {usesCompactMailToolbar ? null : ( <NativeHeaderToolbar placement="right"> {usesNativeChrome ? ( <NativeHeaderToolbar.Button accessibilityLabel="Refresh archived threads" icon="arrow.clockwise" onPress={props.onRefresh} separateBackground /> ) : null} <NativeHeaderToolbar.Menu accessibilityLabel="Filter and sort archived threads" icon={ hasCustomFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle" } separateBackground title="Archived thread options" > <NativeHeaderToolbar.Menu title="Environment"> <NativeHeaderToolbar.Label>Environment</NativeHeaderToolbar.Label> <NativeHeaderToolbar.MenuAction isOn={props.selectedEnvironmentId === null} onPress={() => props.onEnvironmentChange(null)} > <NativeHeaderToolbar.Label>All environments</NativeHeaderToolbar.Label> </NativeHeaderToolbar.MenuAction> {props.environments.map((environment) => ( <NativeHeaderToolbar.MenuAction key={environment.environmentId} isOn={props.selectedEnvironmentId === environment.environmentId} onPress={() => props.onEnvironmentChange(environment.environmentId)} > <NativeHeaderToolbar.Label>{environment.label}</NativeHeaderToolbar.Label> </NativeHeaderToolbar.MenuAction> ))} </NativeHeaderToolbar.Menu> <NativeHeaderToolbar.Menu title="Sort by archived date"> <NativeHeaderToolbar.Label>Sort by archived date</NativeHeaderToolbar.Label> <NativeHeaderToolbar.MenuAction isOn={props.sortOrder === "newest"} onPress={() => props.onSortOrderChange("newest")} > <NativeHeaderToolbar.Label>Newest first</NativeHeaderToolbar.Label> </NativeHeaderToolbar.MenuAction> <NativeHeaderToolbar.MenuAction isOn={props.sortOrder === "oldest"} onPress={() => props.onSortOrderChange("oldest")} > <NativeHeaderToolbar.Label>Oldest first</NativeHeaderToolbar.Label> </NativeHeaderToolbar.MenuAction> </NativeHeaderToolbar.Menu> </NativeHeaderToolbar.Menu> </NativeHeaderToolbar> )} </> );} function ProjectGroupLabel(props: { readonly environmentLabel: string | null; readonly project: EnvironmentProject;}) { return ( <View className="flex-row items-center gap-2.5 px-1 pb-2"> <ProjectFavicon environmentId={props.project.environmentId} faviconPath={props.project.faviconPath} projectTitle={props.project.title} size={18} workspaceRoot={props.project.workspaceRoot} /> <Text className="flex-1 text-xs font-t3-medium tracking-[0.5px] uppercase text-foreground-muted" numberOfLines={1} > {props.project.title} </Text> {props.environmentLabel ? ( <Text className="max-w-[42%] text-2xs text-foreground-tertiary" numberOfLines={1}> {props.environmentLabel} </Text> ) : null} </View> );} function ArchivedThreadRow(props: { readonly environmentLabel: string | null; readonly isFirst: boolean; readonly isLast: boolean; readonly onDelete: () => void; readonly onSwipeableClose: (methods: SwipeableMethods) => void; readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; readonly simultaneousSwipeGesture?: ComponentProps< typeof ThreadSwipeable >["simultaneousWithExternalGesture"]; readonly onUnarchive: () => void; readonly thread: EnvironmentThreadShell;}) { const { width: windowWidth } = useWindowDimensions(); const cardColor = useUniwindTheme()["--color-card"]; const timestamp = relativeTime(props.thread.archivedAt ?? props.thread.updatedAt); const subtitle = [props.environmentLabel, props.thread.branch].filter((part): part is string => Boolean(part), ); return ( <ThreadSwipeable backgroundColor={cardColor} // Round + clip the swipeable container so the group's corners stay // rounded while rows swipe; the row itself stays square inside. containerStyle={{ borderTopLeftRadius: props.isFirst ? 20 : 0, borderTopRightRadius: props.isFirst ? 20 : 0, borderBottomLeftRadius: props.isLast ? 20 : 0, borderBottomRightRadius: props.isLast ? 20 : 0, overflow: "hidden", }} fullSwipeWidth={windowWidth - 32} onDelete={props.onDelete} onSwipeableClose={props.onSwipeableClose} onSwipeableWillOpen={props.onSwipeableWillOpen} primaryAction={{ accessibilityLabel: `Unarchive ${props.thread.title}`, icon: "arrow.uturn.backward", label: "Unarchive", onPress: props.onUnarchive, }} simultaneousWithExternalGesture={props.simultaneousSwipeGesture} threadTitle={props.thread.title} > {() => ( <View className={`flex-row items-center gap-3 bg-card px-4 py-3 ${props.isLast ? "" : "border-b border-separator"}`} > <View className="h-[34px] w-[34px] items-center justify-center rounded-[11px] bg-subtle"> <SymbolView name="archivebox.fill" size={15} tintColorClassName={"accent-icon-subtle"} type="monochrome" /> </View> <View className="min-w-0 flex-1 gap-1"> <View className="flex-row items-center gap-2"> <Text className="min-w-0 flex-1 text-base font-t3-bold leading-snug text-foreground" numberOfLines={1} > {props.thread.title} </Text> <Text className="min-w-[30px] text-right text-xs tabular-nums text-foreground-tertiary"> {timestamp} </Text> </View> {subtitle.length > 0 ? ( <View className="flex-row items-center gap-1.5"> <SymbolView name="arrow.triangle.branch" size={10} tintColorClassName={"accent-icon-subtle"} type="monochrome" /> <Text className="min-w-0 flex-1 font-mono text-2xs text-foreground-tertiary" numberOfLines={1} > {subtitle.join(" · ")} </Text> </View> ) : null} </View> </View> )} </ThreadSwipeable> );} function ArchiveError(props: { readonly message: string; readonly onRetry: () => void }) { return ( <View className="rounded-[20px] border border-danger-border bg-danger p-4"> <Text className="text-base font-t3-bold text-danger-foreground"> Could not load every archive </Text> <Text className="mt-1 text-sm text-foreground-muted">{props.message}</Text> <Pressable className="mt-3 self-start active:opacity-60" onPress={props.onRetry}> <Text className="text-sm font-t3-bold text-danger-foreground">Try again</Text> </Pressable> </View> );} export function ArchivedThreadsScreen(props: { readonly environments: ReadonlyArray<ArchivedThreadsHeaderEnvironment>; readonly error: string | null; readonly groups: ReadonlyArray<ArchivedThreadGroup>; readonly isLoading: boolean; readonly searchQuery: string;Lines 1–500 of 657. Load more
import type { EnvironmentProject, EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; import { LegendList } from "@legendapp/list/react-native"; import type { EnvironmentId } from "@t3tools/contracts"; import type { MenuAction } from "@react-native-menu/menu"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { SymbolView } from "../../components/AppSymbol"; import { useNavigation } from "@react-navigation/native"; import { useCallback, useMemo, useRef, type ComponentProps } from "react"; import { TextInput, ActivityIndicator, Platform, Pressable, RefreshControl, useWindowDimensions, View, } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import 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 { useSafeAreaInsets } from "react-native-safe-area-context"; import { relativeTime } from "../../lib/time"; import { useUniwindTheme } from "../../lib/useUniwindTheme"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { createNativeMailSearchToolbarItem, NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, } from "../layout/native-mail-search-toolbar"; import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList"; export interface ArchivedThreadsHeaderEnvironment { readonly environmentId: EnvironmentId; readonly label: string; } type ArchivedThreadListItem = | { readonly kind: "project"; readonly key: string; readonly environmentLabel: string | null; readonly project: EnvironmentProject; } | { readonly kind: "thread"; readonly key: string; readonly environmentLabel: string | null; readonly isFirst: boolean; readonly isLast: boolean; readonly thread: EnvironmentThreadShell; }; function ArchivedThreadsHeader(props: { readonly environments: ReadonlyArray<ArchivedThreadsHeaderEnvironment>; readonly searchQuery: string; readonly selectedEnvironmentId: EnvironmentId | null; readonly sortOrder: ArchivedThreadSortOrder; readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; readonly onRefresh: () => void; readonly onSearchQueryChange: (query: string) => void; readonly onSortOrderChange: (sortOrder: ArchivedThreadSortOrder) => void; }) { const { width } = useWindowDimensions(); const navigation = useNavigation(); const insets = useSafeAreaInsets(); const hasCustomFilter = props.selectedEnvironmentId !== null || props.sortOrder !== "newest"; const usesNativeChrome = Platform.OS === "ios"; const usesCompactMailToolbar = Platform.OS === "ios" && width < 700 && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; const androidFilterActions = useMemo<MenuAction[]>( () => [ { id: "environment", title: "Environment", subactions: [ { id: "environment:all", title: "All environments", state: props.selectedEnvironmentId === null ? ("on" as const) : undefined, }, ...props.environments.map((environment) => ({ id: `environment:${environment.environmentId}`, title: environment.label, state: props.selectedEnvironmentId === environment.environmentId ? ("on" as const) : undefined, })), ], }, { id: "sort", title: "Sort by archived date", subactions: [ { id: "sort:newest", title: "Newest first", state: props.sortOrder === "newest" ? ("on" as const) : undefined, }, { id: "sort:oldest", title: "Oldest first", state: props.sortOrder === "oldest" ? ("on" as const) : undefined, }, ], }, ], [props.environments, props.selectedEnvironmentId, props.sortOrder], ); const handleAndroidFilterAction = useCallback( (event: { nativeEvent: { event: string } }) => { const action = event.nativeEvent.event; if (action === "environment:all") { props.onEnvironmentChange(null); } else if (action.startsWith("environment:")) { props.onEnvironmentChange(action.slice("environment:".length) as EnvironmentId); } else if (action === "sort:newest") { props.onSortOrderChange("newest"); } else if (action === "sort:oldest") { props.onSortOrderChange("oldest"); } }, [props.onEnvironmentChange, props.onSortOrderChange], ); if (Platform.OS === "android") { // Single header row matching the app's Android chrome (AndroidScreenHeader // palette): back chevron, inline search, filter menu. return ( <> <NativeStackScreenOptions options={{ headerShown: false }} /> <View className="border-b border-header-border bg-header px-3 pb-2.5" style={{ paddingTop: Math.max(insets.top, 12), }} > <View className="min-h-12 flex-row items-center gap-2"> <Pressable accessibilityLabel="Navigate up" accessibilityRole="button" hitSlop={8} onPress={() => navigation.goBack()} className="size-11 items-center justify-center" > <SymbolView name="chevron.left" size={24} tintColorClassName={"accent-foreground"} type="monochrome" /> </Pressable> <View className="min-h-11 flex-1 flex-row items-center gap-2.5 rounded-2xl bg-input px-3.5"> <SymbolView name="magnifyingglass" size={17} tintColorClassName={"accent-icon"} type="monochrome" /> <TextInput accessibilityLabel="Search archived threads" autoCapitalize="none" onChangeText={props.onSearchQueryChange} value={props.searchQuery} placeholder="Search archived threads" placeholderTextColorClassName="accent-placeholder" className="flex-1 py-2 text-base font-sans text-foreground" /> </View> <ControlPillMenu actions={androidFilterActions} isAnchoredToRight onPressAction={handleAndroidFilterAction} > <Pressable accessibilityLabel="Filter and sort archived threads" accessibilityRole="button" className="size-11 items-center justify-center rounded-full bg-subtle" > <SymbolView name={ hasCustomFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle" } size={16} tintColorClassName={"accent-icon"} type="monochrome" /> </Pressable> </ControlPillMenu> </View> </View> </> ); } const archiveFilterMenu = { title: "Archived thread options", items: [ { type: "submenu" as const, title: "Environment", items: [ { type: "action" as const, title: "All environments", state: props.selectedEnvironmentId === null ? ("on" as const) : ("off" as const), onPress: () => props.onEnvironmentChange(null), }, ...props.environments.map((environment) => ({ type: "action" as const, title: environment.label, state: props.selectedEnvironmentId === environment.environmentId ? ("on" as const) : ("off" as const), onPress: () => props.onEnvironmentChange(environment.environmentId), })), ], }, { type: "submenu" as const, title: "Sort by archived date", items: [ { type: "action" as const, title: "Newest first", state: props.sortOrder === "newest" ? ("on" as const) : ("off" as const), onPress: () => props.onSortOrderChange("newest"), }, { type: "action" as const, title: "Oldest first", state: props.sortOrder === "oldest" ? ("on" as const) : ("off" as const), onPress: () => props.onSortOrderChange("oldest"), }, ], }, ], }; return ( <> {/* Static header config (glass preset + title) lives in Stack.tsx; only dynamic toolbar/search wiring is set here. */} <NativeStackScreenOptions options={{ unstable_headerToolbarItems: usesCompactMailToolbar ? () => [ createNativeMailSearchToolbarItem({ composeButtonId: "archived-refresh", composeSystemImageName: "arrow.clockwise", filterMenu: archiveFilterMenu, filterButtonId: "archived-filter", filterSystemImageName: hasCustomFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease", onComposePress: props.onRefresh, onSearchTextChange: props.onSearchQueryChange, placeholder: "Search", searchTextChangeId: "archived-search-text", }), ] : undefined, headerSearchBarOptions: usesCompactMailToolbar ? undefined : { ...(usesNativeChrome ? { allowToolbarIntegration: true, // "integratedButton" is an iOS 26 search-bar placement; // pre-glass iOS keeps the default pull-down placement. ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED ? { placement: "integratedButton" as const } : null), } : { placement: "stacked" as const, }), autoCapitalize: "none", hideNavigationBar: false, obscureBackground: false, placeholder: "Search archived threads", onChangeText: (event) => { props.onSearchQueryChange(event.nativeEvent.text); }, onCancelButtonPress: () => { props.onSearchQueryChange(""); }, }, }} /> {usesCompactMailToolbar ? null : ( <NativeHeaderToolbar placement="right"> {usesNativeChrome ? ( <NativeHeaderToolbar.Button accessibilityLabel="Refresh archived threads" icon="arrow.clockwise" onPress={props.onRefresh} separateBackground /> ) : null} <NativeHeaderToolbar.Menu accessibilityLabel="Filter and sort archived threads" icon={ hasCustomFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle" } separateBackground title="Archived thread options" > <NativeHeaderToolbar.Menu title="Environment"> <NativeHeaderToolbar.Label>Environment</NativeHeaderToolbar.Label> <NativeHeaderToolbar.MenuAction isOn={props.selectedEnvironmentId === null} onPress={() => props.onEnvironmentChange(null)} > <NativeHeaderToolbar.Label>All environments</NativeHeaderToolbar.Label> </NativeHeaderToolbar.MenuAction> {props.environments.map((environment) => ( <NativeHeaderToolbar.MenuAction key={environment.environmentId} isOn={props.selectedEnvironmentId === environment.environmentId} onPress={() => props.onEnvironmentChange(environment.environmentId)} > <NativeHeaderToolbar.Label>{environment.label}</NativeHeaderToolbar.Label> </NativeHeaderToolbar.MenuAction> ))} </NativeHeaderToolbar.Menu> <NativeHeaderToolbar.Menu title="Sort by archived date"> <NativeHeaderToolbar.Label>Sort by archived date</NativeHeaderToolbar.Label> <NativeHeaderToolbar.MenuAction isOn={props.sortOrder === "newest"} onPress={() => props.onSortOrderChange("newest")} > <NativeHeaderToolbar.Label>Newest first</NativeHeaderToolbar.Label> </NativeHeaderToolbar.MenuAction> <NativeHeaderToolbar.MenuAction isOn={props.sortOrder === "oldest"} onPress={() => props.onSortOrderChange("oldest")} > <NativeHeaderToolbar.Label>Oldest first</NativeHeaderToolbar.Label> </NativeHeaderToolbar.MenuAction> </NativeHeaderToolbar.Menu> </NativeHeaderToolbar.Menu> </NativeHeaderToolbar> )} </> ); } function ProjectGroupLabel(props: { readonly environmentLabel: string | null; readonly project: EnvironmentProject; }) { return ( <View className="flex-row items-center gap-2.5 px-1 pb-2"> <ProjectFavicon environmentId={props.project.environmentId} faviconPath={props.project.faviconPath} projectTitle={props.project.title} size={18} workspaceRoot={props.project.workspaceRoot} /> <Text className="flex-1 text-xs font-t3-medium tracking-[0.5px] uppercase text-foreground-muted" numberOfLines={1} > {props.project.title} </Text> {props.environmentLabel ? ( <Text className="max-w-[42%] text-2xs text-foreground-tertiary" numberOfLines={1}> {props.environmentLabel} </Text> ) : null} </View> ); } function ArchivedThreadRow(props: { readonly environmentLabel: string | null; readonly isFirst: boolean; readonly isLast: boolean; readonly onDelete: () => void; readonly onSwipeableClose: (methods: SwipeableMethods) => void; readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; readonly simultaneousSwipeGesture?: ComponentProps< typeof ThreadSwipeable >["simultaneousWithExternalGesture"]; readonly onUnarchive: () => void; readonly thread: EnvironmentThreadShell; }) { const { width: windowWidth } = useWindowDimensions(); const cardColor = useUniwindTheme()["--color-card"]; const timestamp = relativeTime(props.thread.archivedAt ?? props.thread.updatedAt); const subtitle = [props.environmentLabel, props.thread.branch].filter((part): part is string => Boolean(part), ); return ( <ThreadSwipeable backgroundColor={cardColor} // Round + clip the swipeable container so the group's corners stay // rounded while rows swipe; the row itself stays square inside. containerStyle={{ borderTopLeftRadius: props.isFirst ? 20 : 0, borderTopRightRadius: props.isFirst ? 20 : 0, borderBottomLeftRadius: props.isLast ? 20 : 0, borderBottomRightRadius: props.isLast ? 20 : 0, overflow: "hidden", }} fullSwipeWidth={windowWidth - 32} onDelete={props.onDelete} onSwipeableClose={props.onSwipeableClose} onSwipeableWillOpen={props.onSwipeableWillOpen} primaryAction={{ accessibilityLabel: `Unarchive ${props.thread.title}`, icon: "arrow.uturn.backward", label: "Unarchive", onPress: props.onUnarchive, }} simultaneousWithExternalGesture={props.simultaneousSwipeGesture} threadTitle={props.thread.title} > {() => ( <View className={`flex-row items-center gap-3 bg-card px-4 py-3 ${props.isLast ? "" : "border-b border-separator"}`} > <View className="h-[34px] w-[34px] items-center justify-center rounded-[11px] bg-subtle"> <SymbolView name="archivebox.fill" size={15} tintColorClassName={"accent-icon-subtle"} type="monochrome" /> </View> <View className="min-w-0 flex-1 gap-1"> <View className="flex-row items-center gap-2"> <Text className="min-w-0 flex-1 text-base font-t3-bold leading-snug text-foreground" numberOfLines={1} > {props.thread.title} </Text> <Text className="min-w-[30px] text-right text-xs tabular-nums text-foreground-tertiary"> {timestamp} </Text> </View> {subtitle.length > 0 ? ( <View className="flex-row items-center gap-1.5"> <SymbolView name="arrow.triangle.branch" size={10} tintColorClassName={"accent-icon-subtle"} type="monochrome" /> <Text className="min-w-0 flex-1 font-mono text-2xs text-foreground-tertiary" numberOfLines={1} > {subtitle.join(" · ")} </Text> </View> ) : null} </View> </View> )} </ThreadSwipeable> ); } function ArchiveError(props: { readonly message: string; readonly onRetry: () => void }) { return ( <View className="rounded-[20px] border border-danger-border bg-danger p-4"> <Text className="text-base font-t3-bold text-danger-foreground"> Could not load every archive </Text> <Text className="mt-1 text-sm text-foreground-muted">{props.message}</Text> <Pressable className="mt-3 self-start active:opacity-60" onPress={props.onRetry}> <Text className="text-sm font-t3-bold text-danger-foreground">Try again</Text> </Pressable> </View> ); } export function ArchivedThreadsScreen(props: { readonly environments: ReadonlyArray<ArchivedThreadsHeaderEnvironment>; readonly error: string | null; readonly groups: ReadonlyArray<ArchivedThreadGroup>; readonly isLoading: boolean; readonly searchQuery: string;