Skip to content
81 linesCodeBlameRaw
1import type { ChatSidebarEntry } from "@g1t/contracts";
2import { Stack, router } from "expo-router";
3import { Hash, Lock, MessagesSquare } from "lucide-react-native";
4import { RefreshControl, SectionList, View } from "react-native";
5
6import { Avatar, Badge, Empty, ErrorView, Loading, Row, Screen, SectionHeader } from "@/components/ui";
7import { useChatSidebar } from "@/lib/chat";
8import { useSignedIn } from "@/lib/session";
9import { ago } from "@/lib/time";
10import { useColors } from "@/theme";
11
12/** Chat: the workspace's channels you are in, then your direct messages. */
13export default function ChatList() {
14 const { workspace, server } = useSignedIn();
15 const sidebar = useChatSidebar();
16 const colors = useColors();
17
18 const title = workspace?.name ?? workspace?.slug ?? "Chat";
19 if (!workspace) {
20 return (
21 <Screen>
22 <Stack.Screen options={{ title: "Chat" }} />
23 <Empty icon={MessagesSquare} title="No workspace yet" body="Create or join a workspace on g1t, then come back." />
24 </Screen>
25 );
26 }
27
28 const entries = sidebar.data?.entries ?? [];
29 const sections = [
30 { title: "Channels", data: entries.filter((entry) => entry.channel.kind === "channel" && !entry.channel.archived_at) },
31 { title: "Direct messages", data: entries.filter((entry) => entry.channel.kind === "dm") },
32 ].filter((section) => section.data.length > 0);
33
34 return (
35 <Screen>
36 <Stack.Screen options={{ title }} />
37 {sidebar.isPending ? (
38 <Loading />
39 ) : sidebar.error ? (
40 <ErrorView error={sidebar.error} retry={() => sidebar.refetch()} />
41 ) : (
42 <SectionList
43 sections={sections}
44 keyExtractor={(entry) => entry.channel.id}
45 renderSectionHeader={({ section }) => <SectionHeader title={section.title} />}
46 refreshControl={<RefreshControl refreshing={sidebar.isRefetching} onRefresh={() => sidebar.refetch()} tintColor={colors.accent} />}
47 ListEmptyComponent={<Empty icon={MessagesSquare} title="No conversations yet" body="Join a channel or message someone on g1t." />}
48 renderItem={({ item }) => <Conversation entry={item} site={server.site} />}
49 stickySectionHeadersEnabled={false}
50 />
51 )}
52 </Screen>
53 );
54}
55
56function Conversation({ entry, site }: { entry: ChatSidebarEntry; site: string }) {
57 const colors = useColors();
58 const { channel } = entry;
59 const dm = channel.kind === "dm";
60 const other = entry.others[0];
61 const leading = dm && other ? (
62 <Avatar name={other.display_name} uri={other.avatar ? `${site}/avatars/${other.avatar}` : null} agent={other.kind === "agent"} />
63 ) : (
64 <View style={{ width: 32, alignItems: "center" }}>
65 {channel.private ? <Lock size={18} color={colors.muted} /> : <Hash size={18} color={colors.muted} />}
66 </View>
67 );
68 const unread = entry.unread > 0 && !entry.muted;
69 return (
70 <Row
71 leading={leading}
72 title={dm ? entry.title : channel.name ?? entry.title}
73 subtitle={channel.topic ?? (channel.last_message_at ? ago(channel.last_message_at) : undefined)}
74 trailing={
75 entry.mentions > 0 ? <Badge label={`@${entry.mentions}`} tone="accent" /> : unread ? <Badge label={String(entry.unread)} tone="neutral" /> : null
76 }
77 chevron={false}
78 onPress={() => router.push({ pathname: "/chat/[channel]", params: { channel: channel.id, title: dm ? entry.title : `#${channel.name}` } })}
79 />
80 );
81}