frontend client for gemstone. decentralised workplace app
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add channel modal

serenity 952c41f3 6def438f

+385 -49
+304
src/components/Settings/AddChannelModalContent.tsx
··· 1 + import { Loading } from "@/components/primitives/Loading"; 2 + import { Text } from "@/components/primitives/Text"; 3 + import { useFacet } from "@/lib/facet"; 4 + import { lighten } from "@/lib/facet/src/lib/colors"; 5 + import type { ComAtprotoRepoStrongRef } from "@/lib/types/atproto"; 6 + import { 7 + useOAuthAgentGuaranteed, 8 + useOAuthSessionGuaranteed, 9 + } from "@/providers/OAuthProvider"; 10 + import { useCurrentPalette } from "@/providers/ThemeProvider"; 11 + import { useChannelsQuery } from "@/queries/hooks/useChannelsQuery"; 12 + import { useLatticesQuery } from "@/queries/hooks/useLatticesQuery"; 13 + import { useShardsQuery } from "@/queries/hooks/useShardsQuery"; 14 + import { Picker } from "@react-native-picker/picker"; 15 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 16 + import type { Dispatch, SetStateAction } from "react"; 17 + import { useState } from "react"; 18 + import { Pressable, TextInput, View } from "react-native"; 19 + 20 + export const AddChannelModalContent = ({ 21 + setShowAddModal, 22 + }: { 23 + setShowAddModal: Dispatch<SetStateAction<boolean>>; 24 + }) => { 25 + const { semantic } = useCurrentPalette(); 26 + const { atoms, typography } = useFacet(); 27 + const [name, setName] = useState(""); 28 + const [topic, setTopic] = useState(""); 29 + const [mutationError, setMutationError] = useState<string | undefined>( 30 + undefined, 31 + ); 32 + 33 + const agent = useOAuthAgentGuaranteed(); 34 + const session = useOAuthSessionGuaranteed(); 35 + const queryClient = useQueryClient(); 36 + const { useQuery: useLatticesQueryActual } = useLatticesQuery(session); 37 + const { useQuery: useShardsQueryActual } = useShardsQuery(session); 38 + const { queryKey: channelsQueryKey } = useChannelsQuery(session); 39 + 40 + const { data: lattices, isLoading: latticesLoading } = 41 + useLatticesQueryActual(); 42 + const { data: shards, isLoading: shardsLoading } = useShardsQueryActual(); 43 + 44 + const { mutate: newChannelMutation, isPending: mutationPending } = 45 + useMutation({ 46 + mutationFn: async () => { 47 + // const registerResult = await registerNewChannel({ 48 + // channelDomain: inputText, 49 + // agent, 50 + // }); 51 + // if (!registerResult.ok) { 52 + // console.error( 53 + // "Something went wrong when registering the channel.", 54 + // registerResult.error, 55 + // ); 56 + // throw new Error( 57 + // `Something went wrong when registering the channel. ${registerResult.error}`, 58 + // ); 59 + // } 60 + // setShowAddModal(false); 61 + }, 62 + onSuccess: async () => { 63 + await queryClient.invalidateQueries({ 64 + queryKey: channelsQueryKey, 65 + }); 66 + setShowAddModal(false); 67 + }, 68 + onError: (err) => { 69 + console.error( 70 + "Something went wrong when registering the channel.", 71 + err, 72 + ); 73 + setMutationError(err.message); 74 + }, 75 + }); 76 + 77 + const selectableShards = shards 78 + ? shards.map((shard) => ({ 79 + domain: shard.uri.rKey, 80 + ref: { 81 + cid: shard.cid, 82 + uri: shard.uriStr, 83 + }, 84 + })) 85 + : []; 86 + const selectableLattices = lattices 87 + ? lattices.map((lattice) => ({ 88 + domain: lattice.uri.rKey, 89 + ref: { 90 + cid: lattice.cid, 91 + uri: lattice.uriStr, 92 + }, 93 + })) 94 + : []; 95 + 96 + const [selectedShard, setSelectedShard] = useState< 97 + Omit<ComAtprotoRepoStrongRef, "$type"> 98 + >(selectableShards[0].ref); 99 + const [selectedLattice, setSelectedLattice] = useState< 100 + Omit<ComAtprotoRepoStrongRef, "$type"> 101 + >(selectableLattices[0].ref); 102 + 103 + const isLoading = latticesLoading && shardsLoading; 104 + console.log({ 105 + selectedShard: JSON.stringify(selectedShard), 106 + selectedLattice: JSON.stringify(selectedLattice), 107 + name: name.trim(), 108 + }); 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 + 260 + const 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 + 283 + const 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 + };
+2 -5
src/components/Settings/ChannelInfo.tsx
··· 2 2 import { InviteUserModalContent } from "@/components/Settings/InviteUserModalContent"; 3 3 import { useFacet } from "@/lib/facet"; 4 4 import { fade } from "@/lib/facet/src/lib/colors"; 5 - import type { AtUri } from "@/lib/types/atproto"; 6 5 import type { SystemsGmstnDevelopmentChannel } from "@/lib/types/lexicon/systems.gmstn.development.channel"; 7 - import { atUriToString } from "@/lib/utils/atproto"; 8 6 import { useCurrentPalette } from "@/providers/ThemeProvider"; 9 7 import { Hash, UserRoundPlus } from "lucide-react-native"; 10 8 import { useState } from "react"; ··· 15 13 }: { 16 14 channel: { 17 15 value: SystemsGmstnDevelopmentChannel; 18 - uri: Required<AtUri>; 16 + uriStr: string; 19 17 cid: string; 20 18 }; 21 19 }) => { 22 20 const { semantic } = useCurrentPalette(); 23 21 const { atoms } = useFacet(); 24 22 const [showInviteModal, setShowInviteModal] = useState(false); 25 - const channelAtUri = atUriToString(channel.uri); 26 23 27 24 return ( 28 25 <View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}> ··· 80 77 > 81 78 <InviteUserModalContent 82 79 setShowInviteModal={setShowInviteModal} 83 - channelAtUri={channelAtUri} 80 + channelAtUri={channel.uriStr} 84 81 channelCid={channel.cid} 85 82 /> 86 83 </Pressable>
+79 -44
src/components/Settings/ChannelSettings.tsx
··· 1 1 import { Loading } from "@/components/primitives/Loading"; 2 2 import { Text } from "@/components/primitives/Text"; 3 + import { AddChannelModalContent } from "@/components/Settings/AddChannelModalContent"; 3 4 import { ChannelInfo } from "@/components/Settings/ChannelInfo"; 4 5 import { useFacet } from "@/lib/facet"; 5 6 import { fade, lighten } from "@/lib/facet/src/lib/colors"; ··· 63 64 ))} 64 65 </View> 65 66 )} 67 + <View> 68 + <Pressable 69 + style={{ alignSelf: "flex-start", marginLeft: 10 }} 70 + onPress={() => { 71 + setShowAddModal(true); 72 + }} 73 + > 74 + {({ hovered }) => ( 75 + <View 76 + style={{ 77 + flexDirection: "row", 78 + alignItems: "center", 79 + 80 + gap: 4, 81 + backgroundColor: hovered 82 + ? lighten(semantic.primary, 7) 83 + : semantic.primary, 84 + alignSelf: "flex-start", 85 + padding: 8, 86 + paddingRight: 12, 87 + borderRadius: atoms.radii.md, 88 + }} 89 + > 90 + <Plus 91 + height={16} 92 + width={16} 93 + color={semantic.textInverse} 94 + /> 95 + <Text 96 + style={[ 97 + typography.weights.byName.normal, 98 + { color: semantic.textInverse }, 99 + ]} 100 + > 101 + Add 102 + </Text> 103 + </View> 104 + )} 105 + </Pressable> 106 + <Modal 107 + visible={showAddModal} 108 + onRequestClose={() => { 109 + setShowAddModal(!showAddModal); 110 + }} 111 + animationType="fade" 112 + transparent={true} 113 + > 114 + <Pressable 115 + style={{ 116 + flex: 1, 117 + cursor: "auto", 118 + alignItems: "center", 119 + justifyContent: "center", 120 + backgroundColor: fade( 121 + semantic.backgroundDarker, 122 + 60, 123 + ), 124 + }} 125 + onPress={() => { 126 + setShowAddModal(false); 127 + }} 128 + > 129 + <Pressable 130 + style={{ 131 + alignSelf: "center", 132 + cursor: "auto", 133 + }} 134 + onPress={(e) => { 135 + e.stopPropagation(); 136 + }} 137 + > 138 + <AddChannelModalContent 139 + setShowAddModal={setShowAddModal} 140 + /> 141 + </Pressable> 142 + </Pressable> 143 + </Modal> 144 + </View> 66 145 </View> 67 146 ); 68 147 }; 69 - 70 - const channelsQueryFn = async (session: OAuthSession) => { 71 - const channels = await getChannelRecordsFromPds({ 72 - pdsEndpoint: session.serverMetadata.issuer, 73 - did: session.did, 74 - }); 75 - 76 - if (!channels.ok) { 77 - console.error("channelsQueryFn error.", channels.error); 78 - throw new Error( 79 - `Something went wrong while getting the user's channel records.}`, 80 - ); 81 - } 82 - 83 - const results = channels.data 84 - .map((record) => { 85 - const convertResult = stringToAtUri(record.uri); 86 - if (!convertResult.ok) { 87 - console.error( 88 - "Could not convert", 89 - record, 90 - "into at:// URI object.", 91 - convertResult.error, 92 - ); 93 - return; 94 - } 95 - if (!convertResult.data.collection || !convertResult.data.rKey) { 96 - console.error( 97 - record, 98 - "did not convert to a full at:// URI with collection and rkey.", 99 - ); 100 - return; 101 - } 102 - const uri: Required<AtUri> = { 103 - authority: convertResult.data.authority, 104 - collection: convertResult.data.collection, 105 - rKey: convertResult.data.rKey, 106 - }; 107 - return { cid: record.cid, uri, value: record.value }; 108 - }) 109 - .filter((atUri) => atUri !== undefined); 110 - 111 - return results; 112 - };