import { SymbolView } from "expo-symbols";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
type ColorValue,
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 { useProjects, useThreadShells } from "../../state/entities";
import { groupProjectsByRepository } from "../../lib/repositoryGroups";
import { scopedThreadKey } from "../../lib/scopedEntities";
import { relativeTime } from "../../lib/time";
import { threadStatusTone } from "./threadPresentation";
import { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
const threadActivityOrder = Order.mapInput(
Order.Struct({
activityAt: Order.flip(Order.Number),
title: Order.String,
}),
(thread: EnvironmentThreadShell) => ({
activityAt: new Date(thread.updatedAt ?? thread.createdAt).getTime(),
title: thread.title,
}),
);
export function ThreadNavigationDrawer(props: {
readonly visible: boolean;
readonly selectedThreadKey: string | null;
readonly onClose: () => void;
readonly onSelectThread: (thread: EnvironmentThreadShell) => 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");
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"
>
);
}
function ThreadNavigationDrawerContent(props: {
readonly bottomInset: number;
readonly borderSubtleColor: ColorValue;
readonly selectedThreadKey: string | null;
readonly onClose: () => void;
readonly onSelectThread: (thread: EnvironmentThreadShell) => void;
}) {
const projects = useProjects();
const threads = useThreadShells();
const repositoryGroups = useMemo(
() => groupProjectsByRepository({ projects, threads }),
[projects, threads],
);
const groupedThreads = useMemo(
() =>
repositoryGroups.map((group) => {
const threads: EnvironmentThreadShell[] = [];
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],
);
return (
{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: props.borderSubtleColor,
backgroundColor: selected ? undefined : "transparent",
}}
className={selected ? "bg-subtle" : undefined}
>
{thread.title}
{relativeTime(thread.updatedAt ?? thread.createdAt)}
);
})
)}
))}
);
}