frontend client for gemstone. decentralised workplace app
2
fork

Configure Feed

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

feat: lattice registration

serenity c81d9c4a 0e7d5ba2

+477 -17
+56
src/components/Settings/LatticeInfo.tsx
··· 1 + import { Loading } from "@/components/primitives/Loading"; 2 + import { Text } from "@/components/primitives/Text"; 3 + import type { AtUri } from "@/lib/types/atproto"; 4 + import type { SystemsGmstnDevelopmentLattice } from "@/lib/types/lexicon/systems.gmstn.development.lattice"; 5 + import { useCurrentPalette } from "@/providers/ThemeProvider"; 6 + import { getOwnerInfoFromLattice } from "@/queries/get-owner-info-from-lattice"; 7 + import { useQuery } from "@tanstack/react-query"; 8 + import { BadgeCheck, X } from "lucide-react-native"; 9 + import { View } from "react-native"; 10 + 11 + export const LatticeInfo = ({ 12 + shard, 13 + }: { 14 + shard: { 15 + uri: Required<AtUri>; 16 + value: SystemsGmstnDevelopmentLattice; 17 + }; 18 + }) => { 19 + const latticeDomain = shard.uri.rKey; 20 + const { isLoading, data: latticeInfo } = useQuery({ 21 + queryKey: ["shardInfo", latticeDomain], 22 + queryFn: async () => { 23 + return await getOwnerInfoFromLattice(latticeDomain); 24 + }, 25 + }); 26 + const { semantic } = useCurrentPalette(); 27 + 28 + return ( 29 + <View style={{ flexDirection: "row", gap: 6, alignItems: "center" }}> 30 + {isLoading ? ( 31 + <Loading size="small" /> 32 + ) : ( 33 + <> 34 + <Text>{latticeDomain}</Text> 35 + {latticeInfo ? ( 36 + latticeInfo.registered ? ( 37 + <BadgeCheck 38 + height={16} 39 + width={16} 40 + color={semantic.positive} 41 + /> 42 + ) : ( 43 + <X 44 + height={16} 45 + width={16} 46 + color={semantic.negative} 47 + /> 48 + ) 49 + ) : ( 50 + <X height={16} width={16} color={semantic.negative} /> 51 + )} 52 + </> 53 + )} 54 + </View> 55 + ); 56 + };
+183 -13
src/components/Settings/LatticeSettings.tsx
··· 1 + import { Loading } from "@/components/primitives/Loading"; 1 2 import { Text } from "@/components/primitives/Text"; 3 + import { LatticeInfo } from "@/components/Settings/LatticeInfo"; 4 + import { RegisterLatticeModalContent } from "@/components/Settings/RegisterLatticeModalContent"; 2 5 import { useFacet } from "@/lib/facet"; 6 + import { fade } from "@/lib/facet/src/lib/colors"; 7 + import type { AtUri } from "@/lib/types/atproto"; 8 + import { stringToAtUri } from "@/lib/utils/atproto"; 9 + import { useOAuthSessionGuaranteed } from "@/providers/OAuthProvider"; 3 10 import { useCurrentPalette } from "@/providers/ThemeProvider"; 4 - import { View } from "react-native"; 11 + import { getUserLattices } from "@/queries/get-lattices-from-pds"; 12 + import type { OAuthSession } from "@atproto/oauth-client"; 13 + import { useQuery } from "@tanstack/react-query"; 14 + import { Gem, Plus, Waypoints } from "lucide-react-native"; 15 + import { useState } from "react"; 16 + import { Modal, Pressable, View } from "react-native"; 5 17 6 18 export const LatticeSettings = () => { 7 19 const { semantic } = useCurrentPalette(); 8 20 const { atoms, typography } = useFacet(); 21 + const session = useOAuthSessionGuaranteed(); 22 + const [showRegisterModal, setShowRegisterModal] = useState(false); 9 23 10 - return ( 24 + const { data: lattices, isLoading } = useQuery({ 25 + queryKey: ["lattice", session.did], 26 + queryFn: async () => { 27 + return await latticeQueryFn(session); 28 + }, 29 + }); 30 + 31 + return isLoading ? ( 32 + <Loading /> 33 + ) : ( 11 34 <View 12 35 style={{ 13 36 borderWidth: 1, 14 37 borderColor: semantic.borderVariant, 15 38 borderRadius: atoms.radii.lg, 16 - padding: 8, 39 + padding: 12, 40 + paddingVertical: 16, 41 + gap: 16, 42 + width: "50%", 17 43 }} 18 44 > 19 - <Text 20 - style={[ 21 - typography.weights.byName.medium, 22 - typography.sizes.lg, 23 - { 24 - paddingLeft: 8, 25 - }, 26 - ]} 45 + <View 46 + style={{ 47 + flexDirection: "row", 48 + alignItems: "center", 49 + marginLeft: 6, 50 + gap: 6, 51 + }} 27 52 > 28 - Lattices 29 - </Text> 53 + <Waypoints height={20} width={20} color={semantic.text} /> 54 + <Text 55 + style={[ 56 + typography.weights.byName.medium, 57 + typography.sizes.xl, 58 + ]} 59 + > 60 + Lattices 61 + </Text> 62 + </View> 63 + {lattices && lattices.length > 0 && ( 64 + <View style={{ marginLeft: 10, gap: 8 }}> 65 + <View 66 + style={{ 67 + flexDirection: "row", 68 + alignItems: "center", 69 + gap: 4, 70 + }} 71 + > 72 + <Gem height={16} width={16} color={semantic.text} /> 73 + <Text style={[typography.weights.byName.normal]}> 74 + Your Lattices 75 + </Text> 76 + </View> 77 + <View 78 + style={{ 79 + gap: 4, 80 + marginLeft: 8, 81 + }} 82 + > 83 + {lattices.map((shard, idx) => ( 84 + <LatticeInfo key={idx} shard={shard} /> 85 + ))} 86 + </View> 87 + </View> 88 + )} 89 + <View> 90 + <Pressable 91 + style={{ 92 + flexDirection: "row", 93 + alignItems: "center", 94 + marginLeft: 10, 95 + gap: 4, 96 + backgroundColor: semantic.primary, 97 + alignSelf: "flex-start", 98 + padding: 8, 99 + paddingRight: 12, 100 + borderRadius: atoms.radii.md, 101 + }} 102 + onPress={() => { 103 + setShowRegisterModal(true); 104 + }} 105 + > 106 + <Plus height={16} width={16} color={semantic.textInverse} /> 107 + <Text 108 + style={[ 109 + typography.weights.byName.normal, 110 + { color: semantic.textInverse }, 111 + ]} 112 + > 113 + Register a Lattice 114 + </Text> 115 + </Pressable> 116 + <Modal 117 + visible={showRegisterModal} 118 + onRequestClose={() => { 119 + setShowRegisterModal(!showRegisterModal); 120 + }} 121 + animationType="fade" 122 + transparent={true} 123 + > 124 + <Pressable 125 + style={{ 126 + flex: 1, 127 + cursor: "auto", 128 + alignItems: "center", 129 + justifyContent: "center", 130 + backgroundColor: fade( 131 + semantic.backgroundDarker, 132 + 60, 133 + ), 134 + }} 135 + onPress={() => { 136 + setShowRegisterModal(false); 137 + }} 138 + > 139 + <Pressable 140 + style={{ 141 + flex: 0, 142 + cursor: "auto", 143 + alignItems: "center", 144 + }} 145 + onPress={(e) => { 146 + e.stopPropagation(); 147 + }} 148 + > 149 + <RegisterLatticeModalContent 150 + setShowRegisterModal={setShowRegisterModal} 151 + /> 152 + </Pressable> 153 + </Pressable> 154 + </Modal> 155 + </View> 30 156 </View> 31 157 ); 32 158 }; 159 + 160 + const latticeQueryFn = async (session: OAuthSession) => { 161 + const shards = await getUserLattices({ 162 + pdsEndpoint: session.serverMetadata.issuer, 163 + did: session.did, 164 + }); 165 + 166 + if (!shards.ok) { 167 + console.error("shardQueryFn error.", shards.error); 168 + throw new Error( 169 + `Something went wrong while getting the user's membership records.}`, 170 + ); 171 + } 172 + 173 + const results = shards.data 174 + .map((record) => { 175 + const convertResult = stringToAtUri(record.uri); 176 + if (!convertResult.ok) { 177 + console.error( 178 + "Could not convert", 179 + record, 180 + "into at:// URI object.", 181 + convertResult.error, 182 + ); 183 + return; 184 + } 185 + if (!convertResult.data.collection || !convertResult.data.rKey) { 186 + console.error( 187 + record, 188 + "did not convert to a full at:// URI with collection and rkey.", 189 + ); 190 + return; 191 + } 192 + const uri: Required<AtUri> = { 193 + authority: convertResult.data.authority, 194 + collection: convertResult.data.collection, 195 + rKey: convertResult.data.rKey, 196 + }; 197 + return { uri, value: record.value }; 198 + }) 199 + .filter((atUri) => atUri !== undefined); 200 + 201 + return results; 202 + };
+127
src/components/Settings/RegisterLatticeModalContent.tsx
··· 1 + import { Loading } from "@/components/primitives/Loading"; 2 + import { Text } from "@/components/primitives/Text"; 3 + import { useFacet } from "@/lib/facet"; 4 + import { registerNewLattice } from "@/lib/utils/gmstn"; 5 + import { 6 + useOAuthAgentGuaranteed, 7 + useOAuthSessionGuaranteed, 8 + } from "@/providers/OAuthProvider"; 9 + import { useCurrentPalette } from "@/providers/ThemeProvider"; 10 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 11 + import type { Dispatch, SetStateAction } from "react"; 12 + import { useState } from "react"; 13 + import { Pressable, TextInput, View } from "react-native"; 14 + 15 + export const RegisterLatticeModalContent = ({ 16 + setShowRegisterModal, 17 + }: { 18 + setShowRegisterModal: Dispatch<SetStateAction<boolean>>; 19 + }) => { 20 + const { semantic } = useCurrentPalette(); 21 + const { atoms, typography } = useFacet(); 22 + const [inputText, setInputText] = useState(""); 23 + const [registerError, setRegisterError] = useState<string | undefined>( 24 + undefined, 25 + ); 26 + const agent = useOAuthAgentGuaranteed(); 27 + const session = useOAuthSessionGuaranteed(); 28 + const queryClient = useQueryClient(); 29 + const { mutate: newShardMutation, isPending: mutationPending } = 30 + useMutation({ 31 + mutationFn: async () => { 32 + const registerResult = await registerNewLattice({ 33 + latticeDomain: inputText, 34 + agent, 35 + }); 36 + if (!registerResult.ok) { 37 + console.error( 38 + "Something went wrong when registering the lattice.", 39 + registerResult.error, 40 + ); 41 + throw new Error( 42 + `Something went wrong when registering the lattice. ${registerResult.error}`, 43 + ); 44 + } 45 + setShowRegisterModal(false); 46 + }, 47 + onSuccess: async () => { 48 + await queryClient.invalidateQueries({ 49 + queryKey: ["lattice", session.did], 50 + }); 51 + setShowRegisterModal(false); 52 + }, 53 + onError: (err) => { 54 + console.error( 55 + "Something went wrong when registering the lattice.", 56 + err, 57 + ); 58 + setRegisterError(err.message); 59 + }, 60 + }); 61 + 62 + return ( 63 + <View 64 + style={{ 65 + backgroundColor: semantic.surface, 66 + borderRadius: atoms.radii.lg, 67 + display: "flex", 68 + gap: 12, 69 + padding: 16, 70 + }} 71 + > 72 + <View style={{ gap: 4 }}> 73 + <Text>Lattice domain:</Text> 74 + <TextInput 75 + style={[ 76 + { 77 + flex: 1, 78 + borderWidth: 1, 79 + borderColor: semantic.borderVariant, 80 + borderRadius: 8, 81 + paddingHorizontal: 10, 82 + paddingVertical: 10, 83 + color: semantic.text, 84 + outline: "0", 85 + fontFamily: typography.families.primary, 86 + width: 256, 87 + }, 88 + typography.weights.byName.extralight, 89 + typography.sizes.sm, 90 + ]} 91 + value={inputText} 92 + onChangeText={setInputText} 93 + placeholder="lattice.gmstn.systems" 94 + placeholderTextColor={semantic.textPlaceholder} 95 + /> 96 + </View> 97 + <Pressable 98 + style={{ 99 + backgroundColor: inputText.trim() 100 + ? semantic.primary 101 + : registerError 102 + ? semantic.error 103 + : semantic.border, 104 + borderRadius: atoms.radii.lg, 105 + alignItems: "center", 106 + paddingVertical: 10, 107 + }} 108 + onPress={() => { 109 + newShardMutation(); 110 + }} 111 + > 112 + {mutationPending ? ( 113 + <Loading size="small" /> 114 + ) : ( 115 + <Text 116 + style={[ 117 + typography.weights.byName.normal, 118 + { color: semantic.textInverse }, 119 + ]} 120 + > 121 + Register 122 + </Text> 123 + )} 124 + </Pressable> 125 + </View> 126 + ); 127 + };
+39
src/lib/utils/gmstn.ts
··· 60 60 61 61 return { ok: true }; 62 62 }; 63 + export const registerNewLattice = async ({ 64 + latticeDomain: latticeDomain, 65 + agent, 66 + }: { 67 + latticeDomain: string; 68 + agent: Agent; 69 + }): Promise<Result<undefined, string>> => { 70 + if (!isDomain(latticeDomain)) 71 + return { ok: false, error: "Input was not a valid domain." }; 72 + 73 + const now = new Date().toISOString(); 74 + 75 + const record: Omit<SystemsGmstnDevelopmentShard, "$type"> = { 76 + // @ts-expect-error we want to explicitly use the ISO string variant 77 + createdAt: now, 78 + // TODO: actually figure out how to support the description 79 + description: "A Gemstone Systems Lattice.", 80 + }; 81 + console.log(record); 82 + 83 + const { success } = await agent.call( 84 + "com.atproto.repo.createRecord", 85 + {}, 86 + { 87 + repo: agent.did, 88 + collection: "systems.gmstn.development.lattice", 89 + rkey: latticeDomain, 90 + record, 91 + }, 92 + ); 93 + 94 + if (!success) 95 + return { 96 + ok: false, 97 + error: "Attempted to create lattice record failed. Check the domain inputs.", 98 + }; 99 + 100 + return { ok: true }; 101 + };
+27 -4
src/queries/get-lattices-from-pds.ts
··· 12 12 }: { 13 13 pdsEndpoint: string; 14 14 did: Did; 15 - }): Promise<Result<Array<SystemsGmstnDevelopmentLattice>, unknown>> => { 15 + }): Promise< 16 + Result< 17 + Array<{ 18 + uri: string; 19 + value: SystemsGmstnDevelopmentLattice; 20 + }>, 21 + unknown 22 + > 23 + > => { 16 24 const handler = simpleFetchHandler({ service: pdsEndpoint }); 17 25 const client = new Client({ handler }); 18 26 const shardRecordsResult = await fetchRecords({ ··· 30 38 }: { 31 39 client: Client; 32 40 did: Did; 33 - }): Promise<Result<Array<SystemsGmstnDevelopmentLattice>, unknown>> => { 34 - const allRecords: Array<SystemsGmstnDevelopmentLattice> = []; 41 + }): Promise< 42 + Result< 43 + Array<{ 44 + uri: string; 45 + value: SystemsGmstnDevelopmentLattice; 46 + }>, 47 + unknown 48 + > 49 + > => { 50 + const allRecords: Array<{ 51 + uri: string; 52 + value: SystemsGmstnDevelopmentLattice; 53 + }> = []; 35 54 let cursor: string | undefined; 36 55 37 56 let continueLoop = true; ··· 68 87 69 88 if (!success) return { ok: false, error: z.treeifyError(error) }; 70 89 71 - allRecords.push(...responses.map((data) => data.value)); 90 + allRecords.push( 91 + ...responses.map((data) => { 92 + return { uri: data.uri, value: data.value }; 93 + }), 94 + ); 72 95 73 96 if (records.length < 100) continueLoop = false; 74 97 cursor = nextCursor;
+45
src/queries/get-owner-info-from-lattice.ts
··· 1 + import { 2 + getOwnerDidResponseSchema, 3 + httpSuccessResponseSchema, 4 + } from "@/lib/types/http/responses"; 5 + import { z } from "zod"; 6 + 7 + export const getOwnerInfoFromLattice = async (latticeDomain: string) => { 8 + const reqUrl = new URL( 9 + (latticeDomain.startsWith("localhost") 10 + ? `http://${latticeDomain}` 11 + : `https://${latticeDomain}`) + 12 + "/xrpc/systems.gmstn.development.lattice.getOwner", 13 + ); 14 + const req = new Request(reqUrl); 15 + const res = await fetch(req); 16 + const data: unknown = await res.json(); 17 + 18 + const { 19 + success: httpResponseParseSuccess, 20 + error: httpResponseParseError, 21 + data: httpResponse, 22 + } = httpSuccessResponseSchema.safeParse(data); 23 + if (!httpResponseParseSuccess) { 24 + console.error( 25 + "Could not get lattice's owner info.", 26 + z.treeifyError(httpResponseParseError), 27 + ); 28 + return; 29 + } 30 + 31 + const { 32 + success, 33 + error, 34 + data: result, 35 + } = getOwnerDidResponseSchema.safeParse(httpResponse.data); 36 + 37 + if (!success) { 38 + console.error( 39 + "Could not get lattice's owner info.", 40 + z.treeifyError(error), 41 + ); 42 + return; 43 + } 44 + return result; 45 + };