Skip to content
107 linesCodeBlameRaw
1import * as Notifications from "expo-notifications";
2import { Tabs, router, useGlobalSearchParams } from "expo-router";
3import { Bell, Bot, Code2, House, Menu, MessagesSquare } from "lucide-react-native";
4import { useEffect, useRef } from "react";
5
6import { Banner } from "@/components/chat/banner";
7import { FeedProvider, useFeed } from "@/lib/feed";
8import { routeFor } from "@/lib/links";
9import { type PushData, registerForPush } from "@/lib/push";
10import { useSignedIn } from "@/lib/session";
11import { hasCodeAccess } from "@g1t/contracts";
12import { useColors } from "@/theme";
13
14/**
15 * The signed-in app. Its bar is the site's phone bar
16 * (apps/web/app/components/mobile.tsx): Home, Chat, Notifications, Agents,
17 * Code, and More. Chat and Code have their screens; the others say so and
18 * open the site. Customize and your account are under More.
19 */
20export default function AppLayout() {
21 return (
22 <FeedProvider>
23 <PushSetup />
24 <AppTabs />
25 <Banner />
26 </FeedProvider>
27 );
28}
29
30function AppTabs() {
31 const colors = useColors();
32 const { workspace, selectWorkspace } = useSignedIn();
33 const { unread, mentions } = useFeed();
34 // A link names its workspace (`/chat?workspace=acme`, lib/links.ts): switch to it.
35 const { workspace: linked } = useGlobalSearchParams<{ workspace?: string }>();
36 useEffect(() => {
37 if (linked && linked.toLowerCase() !== workspace?.slug.toLowerCase()) void selectWorkspace(linked);
38 }, [linked, workspace?.slug, selectWorkspace]);
39 const code = !workspace || hasCodeAccess(workspace);
40 const badge = mentions > 0 ? mentions : unread > 0 ? unread : undefined;
41
42 return (
43 <Tabs
44 initialRouteName="chat"
45 screenOptions={{
46 headerShown: false,
47 tabBarActiveTintColor: colors.accent,
48 tabBarInactiveTintColor: colors.muted,
49 tabBarStyle: { backgroundColor: colors.surface, borderTopColor: colors.line },
50 tabBarLabelStyle: { fontSize: 11, fontWeight: "600" },
51 }}
52 >
53 <Tabs.Screen name="index" options={{ href: null }} />
54 <Tabs.Screen name="home" options={{ title: "Home", tabBarIcon: ({ color, size }) => <House color={color} size={size} /> }} />
55 <Tabs.Screen
56 name="chat"
57 options={{
58 title: "Chat",
59 tabBarIcon: ({ color, size }) => <MessagesSquare color={color} size={size} />,
60 tabBarBadge: badge,
61 tabBarBadgeStyle: { backgroundColor: mentions > 0 ? colors.accent : colors.lineStrong, color: mentions > 0 ? colors.onAccent : colors.fg, fontSize: 11 },
62 }}
63 />
64 <Tabs.Screen name="notifications" options={{ title: "Notifications", tabBarIcon: ({ color, size }) => <Bell color={color} size={size} /> }} />
65 <Tabs.Screen name="agents" options={{ title: "Agents", tabBarIcon: ({ color, size }) => <Bot color={color} size={size} /> }} />
66 <Tabs.Screen name="code" options={{ title: "Code", href: code ? undefined : null, tabBarIcon: ({ color, size }) => <Code2 color={color} size={size} /> }} />
67 <Tabs.Screen name="customize" options={{ href: null }} />
68 <Tabs.Screen name="more" options={{ title: "More", tabBarIcon: ({ color, size }) => <Menu color={color} size={size} /> }} />
69 </Tabs>
70 );
71}
72
73/**
74 * Registers the phone for notifications once signed in (asking the first
75 * time), and opens what a tapped notification is about: the thread or
76 * conversation, in its workspace.
77 */
78function PushSetup() {
79 const { client, selectWorkspace } = useSignedIn();
80 const handled = useRef<string | null>(null);
81 const response = Notifications.useLastNotificationResponse();
82
83 useEffect(() => {
84 void registerForPush(client, true).catch((error: unknown) => console.warn("g1t: registering for notifications failed", error));
85 }, [client]);
86
87 useEffect(() => {
88 if (!response || response.actionIdentifier !== Notifications.DEFAULT_ACTION_IDENTIFIER) return;
89 const id = response.notification.request.identifier;
90 if (handled.current === id) return;
91 handled.current = id;
92 const data = (response.notification.request.content.data ?? {}) as PushData;
93 void (async () => {
94 if (data.workspace) await selectWorkspace(data.workspace);
95 if (data.channel_id && data.thread_root) {
96 router.push({ pathname: "/chat/thread", params: { channel: data.channel_id, root: data.thread_root } });
97 } else if (data.channel_id) {
98 router.push({ pathname: "/chat/[channel]", params: { channel: data.channel_id } });
99 } else if (data.href) {
100 const route = routeFor(data.href);
101 router.push((route ?? `/open?path=${encodeURIComponent(data.href)}`) as never);
102 }
103 })();
104 }, [response, selectWorkspace]);
105
106 return null;
107}