frontend client for gemstone. decentralised workplace app
1import { Loading } from "@/components/primitives/Loading";
2import { Text } from "@/components/primitives/Text";
3import { useFacet } from "@/lib/facet";
4import { lighten } from "@/lib/facet/src/lib/colors";
5import type { ComAtprotoRepoStrongRef } from "@/lib/types/atproto";
6import { addChannel } from "@/lib/utils/gmstn";
7import {
8 useOAuthAgentGuaranteed,
9 useOAuthSessionGuaranteed,
10} from "@/providers/OAuthProvider";
11import { useCurrentPalette } from "@/providers/ThemeProvider";
12import { useChannelsQuery } from "@/queries/hooks/useChannelsQuery";
13import { useLatticesQuery } from "@/queries/hooks/useLatticesQuery";
14import { useShardsQuery } from "@/queries/hooks/useShardsQuery";
15import { Picker } from "@react-native-picker/picker";
16import { useMutation, useQueryClient } from "@tanstack/react-query";
17import type { Dispatch, SetStateAction } from "react";
18import { useState } from "react";
19import { Pressable, TextInput, View } from "react-native";
20
21export const AddChannelModalContent = ({
22 setShowAddModal,
23}: {
24 setShowAddModal: Dispatch<SetStateAction<boolean>>;
25}) => {
26 const { semantic } = useCurrentPalette();
27 const { atoms, typography } = useFacet();
28 const [name, setName] = useState("");
29 const [topic, setTopic] = useState("");
30 const [mutationError, setMutationError] = useState<string | undefined>(
31 undefined,
32 );
33
34 const agent = useOAuthAgentGuaranteed();
35 const session = useOAuthSessionGuaranteed();
36 const queryClient = useQueryClient();
37 const { useQuery: useLatticesQueryActual } = useLatticesQuery(session);
38 const { useQuery: useShardsQueryActual } = useShardsQuery(session);
39 const { queryKey: channelsQueryKey } = useChannelsQuery(session);
40
41 const { data: lattices, isLoading: latticesLoading } =
42 useLatticesQueryActual();
43 const { data: shards, isLoading: shardsLoading } = useShardsQueryActual();
44 const selectableShards = shards
45 ? shards.map((shard) => ({
46 domain: shard.uri.rKey,
47 ref: {
48 cid: shard.cid,
49 uri: shard.uriStr,
50 },
51 }))
52 : [];
53 const selectableLattices = lattices
54 ? lattices.map((lattice) => ({
55 domain: lattice.uri.rKey,
56 ref: {
57 cid: lattice.cid,
58 uri: lattice.uriStr,
59 },
60 }))
61 : [];
62
63 const [selectedShard, setSelectedShard] = useState<
64 Omit<ComAtprotoRepoStrongRef, "$type">
65 >(selectableShards[0].ref);
66 const [selectedLattice, setSelectedLattice] = useState<
67 Omit<ComAtprotoRepoStrongRef, "$type">
68 >(selectableLattices[0].ref);
69
70 const { mutate: newChannelMutation, isPending: mutationPending } =
71 useMutation({
72 mutationFn: async () => {
73 const registerResult = await addChannel({
74 channelInfo: {
75 name,
76 topic,
77 storeAt: selectedShard,
78 routeThrough: selectedLattice,
79 },
80 agent,
81 });
82 if (!registerResult.ok) {
83 console.error(
84 "Something went wrong when registering the channel.",
85 registerResult.error,
86 );
87 throw new Error(
88 `Something went wrong when registering the channel. ${registerResult.error}`,
89 );
90 }
91 setShowAddModal(false);
92 },
93 onSuccess: async () => {
94 await queryClient.invalidateQueries({
95 queryKey: channelsQueryKey,
96 });
97 setShowAddModal(false);
98 },
99 onError: (err) => {
100 console.error(
101 "Something went wrong when registering the channel.",
102 err,
103 );
104 setMutationError(err.message);
105 },
106 });
107
108 const isLoading = latticesLoading && shardsLoading;
109
110 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- must explicitly check because we are deriving from an array.
111 const readyToSubmit = !!(selectedShard && selectedLattice && name.trim());
112
113 return (
114 <View
115 style={{
116 backgroundColor: semantic.surface,
117 borderRadius: atoms.radii.lg,
118 display: "flex",
119 gap: 12,
120 padding: 16,
121 }}
122 >
123 {isLoading ? (
124 <Loading />
125 ) : (
126 <>
127 <View style={{ gap: 4 }}>
128 <Text>Name:</Text>
129 <TextInput
130 style={[
131 {
132 flex: 1,
133 borderWidth: 1,
134 borderColor: semantic.borderVariant,
135 borderRadius: 8,
136 paddingHorizontal: 10,
137 paddingVertical: 10,
138 color: semantic.text,
139 outline: "0",
140 fontFamily: typography.families.primary,
141 minWidth: 256,
142 },
143 typography.weights.byName.extralight,
144 typography.sizes.sm,
145 ]}
146 value={name}
147 onChangeText={(newName) => {
148 const coerced = newName
149 .toLowerCase()
150 .replace(" ", "-");
151 setName(coerced);
152 }}
153 placeholder="general"
154 placeholderTextColor={semantic.textPlaceholder}
155 />
156 </View>
157 <View style={{ gap: 4 }}>
158 <Text>(optional) Topic:</Text>
159 <TextInput
160 style={[
161 {
162 flex: 1,
163 borderWidth: 1,
164 borderColor: semantic.borderVariant,
165 borderRadius: 8,
166 paddingHorizontal: 10,
167 paddingVertical: 10,
168 color: semantic.text,
169 outline: "0",
170 fontFamily: typography.families.primary,
171 minWidth: 256,
172 },
173 typography.weights.byName.extralight,
174 typography.sizes.sm,
175 ]}
176 value={topic}
177 onChangeText={setTopic}
178 placeholder="General discussion channel"
179 placeholderTextColor={semantic.textPlaceholder}
180 />
181 </View>
182 <View style={{ gap: 4 }}>
183 <Text>Shard (store at):</Text>
184 {/* TODO: for native, we want to render this with a bottom sheet instead*/}
185 <SelectShard
186 shards={
187 shards
188 ? shards.map((shard) => ({
189 domain: shard.uri.rKey,
190 ref: {
191 cid: shard.cid,
192 uri: shard.uriStr,
193 $type: "com.atproto.repo.strongRef",
194 },
195 }))
196 : []
197 }
198 setSelectedShard={setSelectedShard}
199 />
200 </View>
201 <View style={{ gap: 4 }}>
202 <Text>Lattice (route through):</Text>
203 {/* TODO: for native, we want to render this with a bottom sheet instead*/}
204 <SelectLattices
205 lattices={
206 lattices
207 ? lattices.map((lattice) => ({
208 domain: lattice.uri.rKey,
209 ref: {
210 cid: lattice.cid,
211 uri: lattice.uriStr,
212 $type: "com.atproto.repo.strongRef",
213 },
214 }))
215 : []
216 }
217 setSelectedLattice={setSelectedLattice}
218 />
219 </View>
220 <Pressable
221 disabled={!readyToSubmit}
222 onPress={() => {
223 newChannelMutation();
224 }}
225 >
226 {({ hovered }) =>
227 mutationPending ? (
228 <Loading size="small" />
229 ) : (
230 <View
231 style={{
232 backgroundColor: readyToSubmit
233 ? hovered
234 ? lighten(semantic.primary, 7)
235 : semantic.primary
236 : semantic.textPlaceholder,
237 borderRadius: atoms.radii.lg,
238 alignItems: "center",
239 paddingVertical: 10,
240 }}
241 >
242 <Text
243 style={[
244 typography.weights.byName.normal,
245 { color: semantic.textInverse },
246 ]}
247 >
248 Add
249 </Text>
250 </View>
251 )
252 }
253 </Pressable>
254 </>
255 )}
256 </View>
257 );
258};
259
260const SelectShard = ({
261 shards,
262 setSelectedShard,
263}: {
264 shards: Array<{
265 domain: string;
266 ref: ComAtprotoRepoStrongRef;
267 }>;
268 setSelectedShard: Dispatch<SetStateAction<ComAtprotoRepoStrongRef>>;
269}) => {
270 return (
271 <Picker
272 onValueChange={(_, idx) => {
273 setSelectedShard(shards[idx].ref);
274 }}
275 >
276 {shards.map((shard) => (
277 <Picker.Item label={shard.domain} key={shard.domain} />
278 ))}
279 </Picker>
280 );
281};
282
283const SelectLattices = ({
284 lattices,
285 setSelectedLattice,
286}: {
287 lattices: Array<{
288 domain: string;
289 ref: ComAtprotoRepoStrongRef;
290 }>;
291 setSelectedLattice: Dispatch<SetStateAction<ComAtprotoRepoStrongRef>>;
292}) => {
293 return (
294 <Picker
295 onValueChange={(_, idx) => {
296 setSelectedLattice(lattices[idx].ref);
297 }}
298 >
299 {lattices.map((lattice) => (
300 <Picker.Item label={lattice.domain} key={lattice.domain} />
301 ))}
302 </Picker>
303 );
304};