Skip to content

Commit 518a4c5

Browse files
committed
feat: Implement and integrate a mobile thread list component with new and archive actions into the drawer.
1 parent 477de25 commit 518a4c5

File tree

6 files changed

+112
-12
lines changed

6 files changed

+112
-12
lines changed

apps/mobile/.expo/devices.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"devices": [
33
{
44
"installationId": "FF1011A1-7709-4E6F-AF64-252B30FCADD0",
5-
"lastUsed": 1770999837705
5+
"lastUsed": 1771013690224
66
}
77
]
88
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { View, ActivityIndicator, FlatList } from "react-native";
2+
import { Text } from "~/components/ui/text";
3+
import { Button } from "~/components/ui/button";
4+
import {
5+
ActionSheet,
6+
ActionSheetContent,
7+
ActionSheetTrigger,
8+
} from "~/components/ui/action-sheet";
9+
import * as ThreadListPrimitive from "@creatorem/ai-react-native/primitives/thread-list";
10+
import * as ThreadListItemPrimitive from "@creatorem/ai-react-native/primitives/thread-list-item";
11+
import { useThreads } from "@creatorem/ai-react-native/ai-provider";
12+
import { useCallback, type FC } from "react";
13+
import { useCSSVariable } from "uniwind";
14+
import { Icon } from "~/components/ui/icon";
15+
16+
export const ThreadList: FC = () => {
17+
const isLoading = useThreads((threads) => threads.isLoading);
18+
19+
return (
20+
<ThreadListPrimitive.Root className="flex flex-col gap-1">
21+
<ThreadListNew />
22+
{isLoading ? <ThreadListSkeleton /> : null}
23+
{!isLoading ? (
24+
<ThreadListPrimitive.Items components={{ ThreadListItem }} />
25+
) : null}
26+
</ThreadListPrimitive.Root>
27+
);
28+
};
29+
30+
const ThreadListNew: FC = () => {
31+
return (
32+
<ThreadListPrimitive.New
33+
variant="outline"
34+
className="mb-4 h-9 justify-start gap-2 rounded-lg px-3 text-sm hover:bg-muted data-active:bg-muted"
35+
>
36+
<Icon name="Plus" className="size-4" />
37+
<Text>New Thread</Text>
38+
</ThreadListPrimitive.New>
39+
);
40+
};
41+
42+
const ThreadListSkeleton: FC = () => {
43+
const foregroundColor = useCSSVariable("--color-muted-foreground");
44+
45+
return (
46+
<View className="mt-2 flex flex-col gap-1">
47+
{Array.from({ length: 5 }, (_, i) => (
48+
<View key={i} className="flex h-10 items-center justify-center px-3">
49+
<ActivityIndicator
50+
size="small"
51+
color={foregroundColor}
52+
style={i > 0 ? { opacity: 0 } : undefined}
53+
/>
54+
</View>
55+
))}
56+
</View>
57+
);
58+
};
59+
60+
const ThreadListItem: FC = () => {
61+
return (
62+
<ThreadListItemPrimitive.Root className="group flex h-9 flex-row items-center gap-2 rounded-lg transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none data-active:bg-muted">
63+
<ThreadListItemPrimitive.Trigger className="flex h-full min-w-0 flex-1 items-center justify-start truncate bg-transparent px-3 text-start text-sm">
64+
<ThreadListItemPrimitive.Title
65+
fallback="New Chat"
66+
className="text-foreground"
67+
/>
68+
</ThreadListItemPrimitive.Trigger>
69+
<ThreadListItemMore />
70+
</ThreadListItemPrimitive.Root>
71+
);
72+
};
73+
74+
const ThreadListItemMore: FC = () => {
75+
const textMutedForeground = useCSSVariable("--color-muted-foreground");
76+
return (
77+
<ActionSheet>
78+
<ActionSheetTrigger asChild>
79+
<Button size="icon" variant="ghost">
80+
<Icon name="MoreHorizontal" size={20} color={textMutedForeground} />
81+
</Button>
82+
</ActionSheetTrigger>
83+
<ActionSheetContent>
84+
<View className="p-6">
85+
<ThreadListItemPrimitive.Archive>
86+
<Icon name="Archive" className="size-4" />
87+
<Text>Archive</Text>
88+
</ThreadListItemPrimitive.Archive>
89+
</View>
90+
</ActionSheetContent>
91+
</ActionSheet>
92+
);
93+
};
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import { View, ScrollView } from "react-native";
1+
import { ScrollView } from "react-native";
22
import { useSafeAreaInsets } from "react-native-safe-area-context";
33
import { cn } from "~/utils/cn";
4+
import { ThreadList } from "./ai-chat/thread-list";
45

56
export function DrawerContent() {
67
const insets = useSafeAreaInsets();
8+
79
return (
810
<ScrollView
9-
showsVerticalScrollIndicator={false}
10-
bounces={false}
1111
overScrollMode="never"
12-
className={cn(`w-full flex-1 bg-background px-6`)}
13-
contentContainerStyle={[{ paddingTop: insets.top }]}
12+
className={cn("w-full flex-1 bg-background px-4")}
13+
contentContainerStyle={[
14+
{ paddingTop: insets.top + 8, paddingBottom: 24 },
15+
]}
1416
>
15-
<View className="h-20 w-full" />
17+
<ThreadList />
1618
</ScrollView>
1719
);
1820
}

apps/mobile/components/ui/icon.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
Share2Icon,
4444
ArrowDownIcon,
4545
ChevronDownIcon,
46+
ArchiveIcon,
4647
LockIcon,
4748
UnlockIcon,
4849
EyeIcon,
@@ -94,6 +95,7 @@ const lucideIcons = {
9495
File: FileIcon,
9596
Globe: GlobeIcon,
9697
Telescope: TelescopeIcon,
98+
Archive: ArchiveIcon,
9799
MoreHorizontal: MoreHorizontalIcon,
98100
Download: DownloadIcon,
99101
Mic: MicIcon,
@@ -181,7 +183,6 @@ interface IconProps extends VariantProps<typeof iconVariants> {
181183
onPress?: () => void;
182184
disabled?: boolean;
183185
className?: string;
184-
colorClassName?: string;
185186
strokeWidth?: number;
186187
fill?: string;
187188
}
@@ -196,7 +197,6 @@ export const Icon: React.FC<IconProps> = ({
196197
onPress,
197198
disabled = false,
198199
className,
199-
colorClassName,
200200
strokeWidth = 2,
201201
fill = "none",
202202
}) => {

packages-test-3/ai-chat/src/primitives/thread-list-item/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ export { ThreadListItemPrimitiveTrigger as Trigger } from "./thread-list-item-tr
44
export { ThreadListItemPrimitiveArchive as Archive } from "./thread-list-item-archive";
55
export { ThreadListItemPrimitiveUnarchive as Unarchive } from "./thread-list-item-unarchive";
66
export { ThreadListItemPrimitiveDelete as Delete } from "./thread-list-item-delete";
7+
export { useThreadListItem, useThreadListItemStore } from "./thread-list-item-by-index-provider";
8+
export type { ThreadListItemState, ThreadListItemMethods } from "./thread-list-item-by-index-provider";

packages-test-3/ai-chat/src/primitives/thread-list-item/thread-list-item-title.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22

33
import type { FC, ReactNode } from "react";
44
import { useThreadListItem } from "./thread-list-item-by-index-provider";
5+
import { useRuntime } from "@creatorem/ai-chat/runtime";
6+
import { RuntimeComponents } from "@creatorem/ai-chat/component-types";
57

68
export namespace ThreadListItemPrimitiveTitle {
7-
export type Props = {
9+
export type Props = React.ComponentPropsWithoutRef<RuntimeComponents['Text']> & {
810
fallback?: ReactNode;
911
};
1012
}
1113

1214
export const ThreadListItemPrimitiveTitle: FC<
1315
ThreadListItemPrimitiveTitle.Props
14-
> = ({ fallback }) => {
16+
> = ({ fallback, ...props }) => {
1517
const title = useThreadListItem((thread) => thread.title);
16-
return <>{title || fallback}</>;
18+
const {Text} = useRuntime().components
19+
return <Text {...props}>{title ? title + '...' : fallback}</Text>;
1720
};
1821

1922
ThreadListItemPrimitiveTitle.displayName = "ThreadListItemPrimitive.Title";

0 commit comments

Comments
 (0)