import { SymbolView } from "expo-symbols"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Modal, Pressable, ScrollView, useWindowDimensions, View } from "react-native"; import * as Arr from "effect/Array"; import * as Order from "effect/Order"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; import { useThemeColor } from "../../lib/useThemeColor"; import { AppText as Text } from "../../components/AppText"; import { StatusPill } from "../../components/StatusPill"; import { groupProjectsByRepository } from "../../lib/repositoryGroups"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; import { threadStatusTone } from "./threadPresentation"; import { EnvironmentScopedProjectShell, EnvironmentScopedThreadShell, } from "@t3tools/client-runtime"; const threadActivityOrder = Order.mapInput( Order.Struct({ activityAt: Order.flip(Order.Number), title: Order.String, }), (thread: EnvironmentScopedThreadShell) => ({ activityAt: new Date(thread.updatedAt ?? thread.createdAt).getTime(), title: thread.title, }), ); export function ThreadNavigationDrawer(props: { readonly visible: boolean; readonly projects: ReadonlyArray; readonly threads: ReadonlyArray; readonly selectedThreadKey: string | null; readonly onClose: () => void; readonly onSelectThread: (thread: EnvironmentScopedThreadShell) => void; readonly onStartNewTask: () => void; }) { const insets = useSafeAreaInsets(); const { width } = useWindowDimensions(); const drawerWidth = Math.min(width * 0.84, 360); const [mounted, setMounted] = useState(props.visible); const translateX = useSharedValue(-drawerWidth); const overlayOpacity = useSharedValue(0); const backdropColor = useThemeColor("--color-backdrop"); const drawerBg = useThemeColor("--color-drawer"); const drawerShadow = useThemeColor("--color-drawer-shadow"); const primaryForeground = useThemeColor("--color-primary-foreground"); const borderSubtleColor = useThemeColor("--color-border-subtle"); const repositoryGroups = useMemo( () => groupProjectsByRepository({ projects: props.projects, threads: props.threads }), [props.projects, props.threads], ); const groupedThreads = useMemo( () => repositoryGroups.map((group) => { const threads: EnvironmentScopedThreadShell[] = []; for (const projectGroup of group.projects) { threads.push(...projectGroup.threads); } return { key: group.key, title: group.projects[0]?.project.title ?? group.title, threads: Arr.sort(threads, threadActivityOrder), }; }), [repositoryGroups], ); useEffect(() => { if (props.visible) { setMounted(true); translateX.value = withTiming(0, { duration: 240 }); overlayOpacity.value = withTiming(1, { duration: 220 }); return; } overlayOpacity.value = withTiming(0, { duration: 180 }); translateX.value = withTiming(-drawerWidth, { duration: 220 }, (finished) => { if (finished) { runOnJS(setMounted)(false); } }); }, [drawerWidth, overlayOpacity, props.visible, translateX]); const closeDrawer = useCallback(() => { props.onClose(); }, [props]); const panGesture = useMemo( () => Gesture.Pan() .activeOffsetX([-12, 12]) .failOffsetY([-24, 24]) .onUpdate((event) => { translateX.value = Math.min(0, event.translationX); }) .onEnd((event) => { const shouldClose = event.translationX < -drawerWidth * 0.2 || event.velocityX < -500; if (shouldClose) { runOnJS(closeDrawer)(); return; } translateX.value = withTiming(0, { duration: 180 }); }), [closeDrawer, drawerWidth, translateX], ); const drawerStyle = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.value }], })); const backdropStyle = useAnimatedStyle(() => ({ opacity: overlayOpacity.value, })); if (!mounted) { return null; } return ( Threads { props.onClose(); props.onStartNewTask(); }} className="h-11 w-11 items-center justify-center rounded-full bg-primary" > {groupedThreads.map((group) => ( {group.title} {group.threads.length === 0 ? ( No threads yet ) : ( group.threads.map((thread, index) => { const threadKey = scopedThreadKey(thread.environmentId, thread.id); const selected = props.selectedThreadKey === threadKey; return ( { props.onSelectThread(thread); props.onClose(); }} style={{ paddingHorizontal: 16, paddingVertical: 15, borderTopWidth: index === 0 ? 0 : 1, borderTopColor: borderSubtleColor, backgroundColor: selected ? undefined : "transparent", }} className={selected ? "bg-subtle" : undefined} > {thread.title} {relativeTime(thread.updatedAt ?? thread.createdAt)} ); }) )} ))} ); }