Skip to content
116 linesCodeBlameRaw
1import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2import Constants from "expo-constants";
3import { Stack, router } from "expo-router";
4import * as WebBrowser from "expo-web-browser";
5import { Bell, BellRing, Blocks, Check, ExternalLink, LogOut } from "lucide-react-native";
6import { Linking, View } from "react-native";
7
8import { Avatar, Badge, Button, Row, Screen, SectionHeader, Text } from "@/components/ui";
9import { pushState, registerForPush, sendTestPush } from "@/lib/push";
10import { isHosted, siteUrl } from "@/lib/server";
11import { useSignedIn } from "@/lib/session";
12import { space, useColors } from "@/theme";
13
14/** More: Customize, your workspaces, notifications on this phone, how you signed in, and signing out. */
15export default function More() {
16 const { me, workspace, server, client, selectWorkspace, signOut } = useSignedIn();
17 const colors = useColors();
18 const queries = useQueryClient();
19 const name = me.display_username ?? me.username;
20 const oauth = client.credentials.kind === "oauth";
21 const push = useQuery({ queryKey: ["push-state", server.site], queryFn: () => pushState(server) });
22 const test = useMutation({ mutationFn: () => sendTestPush(client) });
23
24 const pushRow = {
25 unsupported: { label: "Not here", detail: "Notifications need a build of the app on a phone: not Expo Go on Android, nor a simulator." },
26 off: { label: "Off", detail: `${new URL(server.site).host} sends no notifications to phones. Chat still updates while the app is open.` },
27 denied: { label: "Off", detail: "Turned off for g1t in your phone's settings. Tap to open them." },
28 undetermined: { label: "Off", detail: "Tap to get notified of messages to you, mentions and agents waiting on you." },
29 granted: { label: "On", detail: "Messages to you, mentions, replies and agents waiting on you, when you're not at g1t elsewhere." },
30 }[push.data ?? "undetermined"];
31
32 return (
33 <Screen scroll style={{ paddingHorizontal: 0 }}>
34 <Stack.Screen options={{ title: "More" }} />
35 <View style={{ flexDirection: "row", gap: space.md, alignItems: "center", paddingHorizontal: space.lg }}>
36 <Avatar name={name} uri={me.avatar ? `${server.site}/avatars/${me.avatar}` : null} size={48} />
37 <View style={{ flex: 1 }}>
38 <Text size="heading">{name}</Text>
39 <Text size="small" tone="muted">
40 @{me.username} · {isHosted(server) ? "g1t.sh" : new URL(server.site).host}
41 </Text>
42 </View>
43 </View>
44
45 <View>
46 <Row leading={<Blocks size={22} color={colors.accent} />} title="Customize" subtitle="Skills, connectors and plugins for your agents" onPress={() => router.push("/customize")} />
47 <Row
48 leading={<Bell size={22} color={colors.accent} />}
49 title="Notifications on this phone"
50 subtitle={pushRow.detail}
51 trailing={<Badge label={pushRow.label} tone={push.data === "granted" ? "success" : "neutral"} />}
52 chevron={false}
53 onPress={
54 push.data === "denied"
55 ? () => void Linking.openSettings()
56 : push.data === "undetermined"
57 ? () => void registerForPush(client, true).finally(() => queries.invalidateQueries({ queryKey: ["push-state", server.site] }))
58 : undefined
59 }
60 />
61 {push.data === "granted" ? (
62 <Row
63 leading={<BellRing size={22} color={colors.accent} />}
64 title="Send a test notification"
65 subtitle={
66 test.isPending
67 ? "Sending…"
68 : test.error
69 ? test.error.message
70 : test.data !== undefined
71 ? test.data > 0
72 ? `Sent to ${test.data} ${test.data === 1 ? "place" : "places"}: this phone, and any browser with notifications on.`
73 : "Nothing was sent: g1t has no phone or browser for you. Sign out and in again to register this phone."
74 : "To check this phone gets them. It arrives in a few seconds, even with the app open; if it never does, see the setup guide's troubleshooting."
75 }
76 chevron={false}
77 onPress={test.isPending ? undefined : () => test.mutate()}
78 />
79 ) : null}
80 </View>
81
82 <View>
83 <SectionHeader title="Workspaces" />
84 {(me.workspaces ?? []).map((one) => (
85 <Row
86 key={one.slug}
87 leading={<Avatar name={one.name ?? one.slug} uri={one.avatar ? `${server.site}/avatars/${one.avatar}` : null} />}
88 title={one.name ?? one.slug}
89 subtitle={`${one.slug} · ${one.role}`}
90 trailing={one.slug === workspace?.slug ? <Check size={18} color={colors.accent} /> : null}
91 chevron={false}
92 onPress={() => selectWorkspace(one.slug)}
93 />
94 ))}
95 </View>
96
97 <View>
98 <SectionHeader title="Signed in" />
99 <Row
100 title={oauth ? "With g1t" : "With an access token"}
101 subtitle={oauth ? "End or narrow the app's access in Settings → Connected applications." : "The token you pasted. Delete it in Settings → Access tokens to end it."}
102 trailing={<ExternalLink size={16} color={colors.faint} />}
103 chevron={false}
104 onPress={() => WebBrowser.openBrowserAsync(siteUrl(server, oauth ? "/settings/applications" : "/settings/tokens"))}
105 />
106 </View>
107
108 <View style={{ paddingHorizontal: space.lg, gap: space.md }}>
109 <Button title="Sign out" variant="danger" icon={LogOut} onPress={signOut} />
110 <Text size="small" tone="faint" style={{ textAlign: "center" }}>
111 g1t {Constants.expoConfig?.version ?? ""}
112 </Text>
113 </View>
114 </Screen>
115 );
116}