| 1 | import type { ChatSidebarEntry } from "@g1t/contracts"; |
| 2 | import { Stack, router } from "expo-router"; |
| 3 | import { Hash, Lock, MessagesSquare } from "lucide-react-native"; |
| 4 | import { RefreshControl, SectionList, View } from "react-native"; |
| 5 | |
| 6 | import { Avatar, Badge, Empty, ErrorView, Loading, Row, Screen, SectionHeader } from "@/components/ui"; |
| 7 | import { useChatSidebar } from "@/lib/chat"; |
| 8 | import { useSignedIn } from "@/lib/session"; |
| 9 | import { ago } from "@/lib/time"; |
| 10 | import { useColors } from "@/theme"; |
| 11 | |
| 12 | /** Chat: the workspace's channels you are in, then your direct messages. */ |
| 13 | export 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 | |
| 56 | function 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 | } |