frontend client for gemstone. decentralised workplace app
1import type { AtUri } from "@/lib/types/atproto"; 2import type { RequestHistoryMessage, ShardMessage } from "@/lib/types/messages"; 3import { historyMessageSchema, shardMessageSchema } from "@/lib/types/messages"; 4import { atUriEquals, atUriToString, stringToAtUri } from "@/lib/utils/atproto"; 5import { sendShardMessage } from "@/lib/utils/messages"; 6import { 7 validateWsMessageString, 8 validateWsMessageType, 9} from "@/lib/validators"; 10import { useSessions } from "@/providers/authed/SessionsProvider"; 11import { useOAuthSession } from "@/providers/OAuthProvider"; 12import { useEffect, useState } from "react"; 13 14export const useChannel = (channel: AtUri) => { 15 const [messages, setMessages] = useState<Array<ShardMessage>>([]); 16 const [isConnected, setIsConnected] = useState(false); 17 const { findChannelSession } = useSessions(); 18 const oAuthSession = useOAuthSession(); 19 20 const { sessionInfo, socket } = findChannelSession(channel); 21 22 useEffect(() => { 23 if (!sessionInfo) 24 throw new Error( 25 "Channel did not resolve to a valid sessionInfo object.", 26 ); 27 if (!socket) 28 throw new Error( 29 "Session info did not resolve to a valid websocket connection. This should not happen and is likely a bug. Check the sessions map object.", 30 ); 31 32 // attach handlers here 33 34 const handleOpen = () => { 35 console.log("Connected to WebSocket"); 36 setIsConnected(true); 37 const requestHistoryMessage: RequestHistoryMessage = { 38 type: "shard/requestHistory", 39 channel: atUriToString(channel), 40 requestedBy: sessionInfo.clientDid, 41 }; 42 console.log( 43 "requested history from lattice", 44 requestHistoryMessage, 45 ); 46 socket.send(JSON.stringify(requestHistoryMessage)); 47 }; 48 49 socket.addEventListener("message", (event) => { 50 console.log("received message", event); 51 const validateEventResult = validateWsMessageString(event.data); 52 if (!validateEventResult.ok) return; 53 54 const data: unknown = JSON.parse(validateEventResult.data); 55 const validateTypeResult = validateWsMessageType(data); 56 if (!validateTypeResult.ok) return; 57 58 const { type: messageType } = validateTypeResult.data; 59 60 switch (messageType) { 61 case "shard/message": { 62 const { success, data: shardMessage } = 63 shardMessageSchema.safeParse(validateTypeResult.data); 64 if (!success) return; 65 66 const parseChannelResult = stringToAtUri( 67 shardMessage.channel, 68 ); 69 70 if (!parseChannelResult.ok) return; 71 const { data: channelAtUri } = parseChannelResult; 72 73 if (atUriEquals(channelAtUri, channel)) 74 setMessages((prev) => [...prev, shardMessage]); 75 break; 76 } 77 case "shard/history": { 78 console.log( 79 "received history from lattice", 80 validateTypeResult.data, 81 ); 82 const { success, data: historyMessage } = 83 historyMessageSchema.safeParse(validateTypeResult.data); 84 if (!success) return; 85 if (!historyMessage.messages) return; 86 87 const parseChannelResult = stringToAtUri( 88 historyMessage.channel, 89 ); 90 91 if (!parseChannelResult.ok) return; 92 const { data: channelAtUri } = parseChannelResult; 93 94 if (atUriEquals(channelAtUri, channel)) 95 setMessages([...historyMessage.messages]); 96 } 97 } 98 }); 99 100 socket.addEventListener("error", (error) => { 101 console.error("WebSocket error:", error); 102 }); 103 104 socket.addEventListener("close", () => { 105 console.log("Disconnected from WebSocket"); 106 setIsConnected(false); 107 }); 108 109 if (socket.readyState === WebSocket.OPEN) { 110 handleOpen(); 111 } 112 113 socket.addEventListener("open", handleOpen); 114 115 return () => { 116 socket.removeEventListener("open", handleOpen); 117 }; 118 }, [socket, sessionInfo, channel]); 119 120 if (!oAuthSession) throw new Error("No OAuth session"); 121 if (!sessionInfo) 122 throw new Error( 123 "Channel did not resolve to a valid sessionInfo object.", 124 ); 125 if (!socket) 126 throw new Error( 127 "Session info did not resolve to a valid websocket connection. This should not happen and is likely a bug. Check the sessions map object.", 128 ); 129 130 const channelStringified = atUriToString(channel); 131 132 const sendMessageToChannel = (content: string) => { 133 sendShardMessage( 134 { 135 content, 136 channel: channelStringified, 137 sentBy: oAuthSession.did, 138 routedThrough: sessionInfo.latticeDid, 139 }, 140 socket, 141 ); 142 }; 143 144 return { sessionInfo, socket, messages, isConnected, sendMessageToChannel }; 145};