Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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