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 console.warn(
25 "Channel did not resolve to a valid sessionInfo object.",
26 );
27 return;
28 }
29 if (!socket) {
30 console.warn(
31 "Session info did not resolve to a valid websocket connection. This should not happen and is likely a bug. Check the sessions map object.",
32 );
33 return;
34 }
35
36 // attach handlers here
37
38 const handleOpen = () => {
39 console.log("Connected to WebSocket");
40 setIsConnected(true);
41 const requestHistoryMessage: RequestHistoryMessage = {
42 type: "shard/requestHistory",
43 channel: atUriToString(channel),
44 requestedBy: sessionInfo.clientDid,
45 };
46 console.log(
47 "requested history from lattice",
48 requestHistoryMessage,
49 );
50 socket.send(JSON.stringify(requestHistoryMessage));
51 };
52
53 socket.addEventListener("message", (event) => {
54 console.log("received message", event);
55 const validateEventResult = validateWsMessageString(event.data);
56 if (!validateEventResult.ok) return;
57
58 const data: unknown = JSON.parse(validateEventResult.data);
59 const validateTypeResult = validateWsMessageType(data);
60 if (!validateTypeResult.ok) return;
61
62 const { type: messageType } = validateTypeResult.data;
63
64 switch (messageType) {
65 case "shard/message": {
66 const { success, data: shardMessage } =
67 shardMessageSchema.safeParse(validateTypeResult.data);
68 if (!success) return;
69
70 const parseChannelResult = stringToAtUri(
71 shardMessage.channel,
72 );
73
74 if (!parseChannelResult.ok) return;
75 const { data: channelAtUri } = parseChannelResult;
76
77 if (atUriEquals(channelAtUri, channel))
78 setMessages((prev) => [...prev, shardMessage]);
79 break;
80 }
81 case "shard/history": {
82 console.log(
83 "received history from lattice",
84 validateTypeResult.data,
85 );
86 const { success, data: historyMessage } =
87 historyMessageSchema.safeParse(validateTypeResult.data);
88 if (!success) return;
89 if (!historyMessage.messages) return;
90
91 const parseChannelResult = stringToAtUri(
92 historyMessage.channel,
93 );
94
95 if (!parseChannelResult.ok) return;
96 const { data: channelAtUri } = parseChannelResult;
97
98 if (atUriEquals(channelAtUri, channel))
99 setMessages([...historyMessage.messages]);
100 }
101 }
102 });
103
104 socket.addEventListener("error", (error) => {
105 console.error("WebSocket error:", error);
106 });
107
108 socket.addEventListener("close", () => {
109 console.log("Disconnected from WebSocket");
110 setIsConnected(false);
111 });
112
113 if (socket.readyState === WebSocket.OPEN) {
114 handleOpen();
115 }
116
117 socket.addEventListener("open", handleOpen);
118
119 return () => {
120 socket.removeEventListener("open", handleOpen);
121 };
122 }, [socket, sessionInfo, channel]);
123
124 if (!oAuthSession) {console.warn("No OAuth session"); return }
125 if (!sessionInfo) {
126 console.warn(
127 "Channel did not resolve to a valid sessionInfo object.",
128 );
129 return;
130 }
131 if (!socket) {
132 console.warn(
133 "Session info did not resolve to a valid websocket connection. This should not happen and is likely a bug. Check the sessions map object.",
134 );
135 return;
136 }
137
138 const channelStringified = atUriToString(channel);
139
140 const sendMessageToChannel = (content: string) => {
141 sendShardMessage(
142 {
143 content,
144 channel: channelStringified,
145 sentBy: oAuthSession.did,
146 routedThrough: sessionInfo.latticeDid,
147 },
148 socket,
149 );
150 };
151
152 return { sessionInfo, socket, messages, isConnected, sendMessageToChannel };
153};