frontend client for gemstone. decentralised workplace app
at main 12 kB view raw
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};