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};