Skip to content
115 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"], queryFn: pushState });
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 denied: { label: "Off", detail: "Turned off for g1t in your phone's settings. Tap to open them." },
27 undetermined: { label: "Off", detail: "Tap to get notified of messages to you, mentions and agents waiting on you." },
28 granted: { label: "On", detail: "Messages to you, mentions, replies and agents waiting on you, when you're not at g1t elsewhere." },
29 }[push.data ?? "undetermined"];
30
31 return (
32 <Screen scroll style={{ paddingHorizontal: 0 }}>
33 <Stack.Screen options={{ title: "More" }} />
34 <View style={{ flexDirection: "row", gap: space.md, alignItems: "center", paddingHorizontal: space.lg }}>
35 <Avatar name={name} uri={me.avatar ? `${server.site}/avatars/${me.avatar}` : null} size={48} />
36 <View style={{ flex: 1 }}>
37 <Text size="heading">{name}</Text>
38 <Text size="small" tone="muted">
39 @{me.username} · {isHosted(server) ? "g1t.sh" : new URL(server.site).host}
40 </Text>
41 </View>
42 </View>
43
44 <View>
45 <Row leading={<Blocks size={22} color={colors.accent} />} title="Customize" subtitle="Skills, connectors and plugins for your agents" onPress={() => router.push("/customize")} />
46 <Row
47 leading={<Bell size={22} color={colors.accent} />}
48 title="Notifications on this phone"
49 subtitle={pushRow.detail}
50 trailing={<Badge label={pushRow.label} tone={push.data === "granted" ? "success" : "neutral"} />}
51 chevron={false}
52 onPress={
53 push.data === "denied"
54 ? () => void Linking.openSettings()
55 : push.data === "undetermined"
56 ? () => void registerForPush(client, true).finally(() => queries.invalidateQueries({ queryKey: ["push-state"] }))
57 : undefined
58 }
59 />
60 {push.data === "granted" ? (
61 <Row
62 leading={<BellRing size={22} color={colors.accent} />}
63 title="Send a test notification"
64 subtitle={
65 test.isPending
66 ? "Sending…"
67 : test.error
68 ? test.error.message
69 : test.data !== undefined
70 ? test.data > 0
71 ? `Sent to ${test.data} ${test.data === 1 ? "place" : "places"}: this phone, and any browser with notifications on.`
72 : "Nothing was sent: g1t has no phone or browser for you. Sign out and in again to register this phone."
73 : "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."
74 }
75 chevron={false}
76 onPress={test.isPending ? undefined : () => test.mutate()}
77 />
78 ) : null}
79 </View>
80
81 <View>
82 <SectionHeader title="Workspaces" />
83 {(me.workspaces ?? []).map((one) => (
84 <Row
85 key={one.slug}
86 leading={<Avatar name={one.name ?? one.slug} uri={one.avatar ? `${server.site}/avatars/${one.avatar}` : null} />}
87 title={one.name ?? one.slug}
88 subtitle={`${one.slug} · ${one.role}`}
89 trailing={one.slug === workspace?.slug ? <Check size={18} color={colors.accent} /> : null}
90 chevron={false}
91 onPress={() => selectWorkspace(one.slug)}
92 />
93 ))}
94 </View>
95
96 <View>
97 <SectionHeader title="Signed in" />
98 <Row
99 title={oauth ? "With g1t" : "With an access token"}
100 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."}
101 trailing={<ExternalLink size={16} color={colors.faint} />}
102 chevron={false}
103 onPress={() => WebBrowser.openBrowserAsync(siteUrl(server, oauth ? "/settings/applications" : "/settings/tokens"))}
104 />
105 </View>
106
107 <View style={{ paddingHorizontal: space.lg, gap: space.md }}>
108 <Button title="Sign out" variant="danger" icon={LogOut} onPress={signOut} />
109 <Text size="small" tone="faint" style={{ textAlign: "center" }}>
110 g1t {Constants.expoConfig?.version ?? ""}
111 </Text>
112 </View>
113 </Screen>
114 );
115}