frontend client for gemstone. decentralised workplace app

refactor: working sessions and channels

serenity a5409c07 50b1a6e3

Changed files
+167 -125
src
+14 -1
src/app/(protected)/index.tsx
···
import ChatComponentProfiled from "@/components/ChatComponentProfiled";
+
import { Loading } from "@/components/Loading";
+
import { useChannelRecords } from "@/providers/authed/ChannelsProvider";
+
import { useSessions } from "@/providers/authed/SessionsProvider";
import { useOAuthSession } from "@/providers/OAuthProvider";
import { Redirect } from "expo-router";
import { View } from "react-native";
export default function Index() {
const oAuthSession = useOAuthSession();
+
const { channels } = useChannelRecords();
+
const { isInitialising } = useSessions();
+
const isAppReady = channels.length > 0 && !isInitialising;
return (
<View
style={{
···
}}
>
{oAuthSession ? (
-
<ChatComponentProfiled did={oAuthSession.did} />
+
isAppReady ? (
+
<ChatComponentProfiled
+
did={oAuthSession.did}
+
channelAtUri={channels[0].channelAtUri}
+
/>
+
) : (
+
<Loading />
+
)
) : (
<Redirect href={"/login"} />
)}
+8 -4
src/components/ChatComponentProfiled.tsx
···
import { Loading } from "@/components/Loading";
import { Message } from "@/components/Message";
-
import { useWebSocket } from "@/lib/hooks/useWebSocket";
-
import type { DidPlc, DidWeb } from "@/lib/types/atproto";
+
import { useChannel } from "@/lib/hooks/useChannel";
+
import type { AtUri, DidPlc, DidWeb } from "@/lib/types/atproto";
import { getBskyProfile } from "@/queries/get-profile";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
···
export default function ChatComponentProfiled({
did,
+
channelAtUri,
}: {
did: DidPlc | DidWeb;
+
channelAtUri: AtUri
}) {
const [inputText, setInputText] = useState("");
-
const { messages, isConnected, sendMessage } = useWebSocket();
+
const { messages, sendMessageToChannel, isConnected } = useChannel(
+
channelAtUri,
+
);
const handleSend = () => {
if (inputText.trim()) {
-
sendMessage({ text: inputText, did });
+
sendMessageToChannel(inputText);
setInputText("");
}
};
+91
src/lib/hooks/useChannel.ts
···
+
import type { AtUri } from "@/lib/types/atproto";
+
import type { ShardMessage } from "@/lib/types/messages";
+
import { atUriToString } from "@/lib/utils/atproto";
+
import { sendShardMessage } from "@/lib/utils/messages";
+
import {
+
validateWsMessageString,
+
validateWsMessageType,
+
} from "@/lib/validators";
+
import { useSessions } from "@/providers/authed/SessionsProvider";
+
import { useOAuthSession } from "@/providers/OAuthProvider";
+
import { useEffect, useState } from "react";
+
+
export const useChannel = (channel: AtUri) => {
+
const [messages, setMessages] = useState<Array<ShardMessage>>([]);
+
const [isConnected, setIsConnected] = useState(false);
+
const { findChannelSession } = useSessions();
+
const oAuthSession = useOAuthSession();
+
+
const { sessionInfo, socket } = findChannelSession(channel);
+
+
useEffect(() => {
+
if (!sessionInfo)
+
throw new Error(
+
"Channel did not resolve to a valid sessionInfo object.",
+
);
+
if (!socket)
+
throw new Error(
+
"Session info did not resolve to a valid websocket connection. This should not happen and is likely a bug. Check the sessions map object.",
+
);
+
+
// attach handlers here
+
+
socket.addEventListener("open", () => {
+
console.log("Connected to WebSocket");
+
setIsConnected(true);
+
});
+
+
socket.addEventListener("message", (event) => {
+
const validateEventResult = validateWsMessageString(event.data);
+
if (!validateEventResult.ok) return;
+
+
const data: unknown = JSON.parse(validateEventResult.data);
+
const validateTypeResult = validateWsMessageType(data);
+
if (!validateTypeResult.ok) return;
+
+
const { type: messageType } = validateTypeResult.data;
+
+
switch (messageType) {
+
case "shard/message":
+
setMessages((prev) => [
+
...prev,
+
validateTypeResult.data as ShardMessage,
+
]);
+
}
+
});
+
+
socket.addEventListener("error", (error) => {
+
console.error("WebSocket error:", error);
+
});
+
+
socket.addEventListener("close", () => {
+
console.log("Disconnected from WebSocket");
+
setIsConnected(false);
+
});
+
}, [socket, sessionInfo]);
+
+
if (!oAuthSession) throw new Error("No OAuth session");
+
if (!sessionInfo)
+
throw new Error(
+
"Channel did not resolve to a valid sessionInfo object.",
+
);
+
if (!socket)
+
throw new Error(
+
"Session info did not resolve to a valid websocket connection. This should not happen and is likely a bug. Check the sessions map object.",
+
);
+
+
const channelStringified = atUriToString(channel);
+
+
const sendMessageToChannel = (content: string) => {
+
sendShardMessage(
+
{
+
content,
+
channel: channelStringified,
+
sentBy: oAuthSession.did,
+
},
+
socket,
+
);
+
};
+
+
return { sessionInfo, socket, messages, isConnected, sendMessageToChannel };
+
};
-79
src/lib/hooks/useWebSocket.ts
···
-
import type { DidPlc, DidWeb } from "@/lib/types/atproto";
-
import type { ShardMessage } from "@/lib/types/messages";
-
import {
-
validateHistoryMessage,
-
validateNewMessage,
-
validateWsMessageString,
-
validateWsMessageType,
-
} from "@/lib/validators";
-
import { useFirstSessionWsTemp } from "@/providers/authed/SessionsProvider";
-
import { useEffect, useState } from "react";
-
-
export function useWebSocket() {
-
const [messages, setMessages] = useState<Array<ShardMessage>>([]);
-
const [isConnected, setIsConnected] = useState(false);
-
const ws = useFirstSessionWsTemp();
-
-
useEffect(() => {
-
if (!ws) return;
-
ws.onopen = () => {
-
console.log("Connected to WebSocket");
-
setIsConnected(true);
-
};
-
-
ws.onmessage = (event) => {
-
const eventData = validateWsMessageString(event.data);
-
if (!eventData) return;
-
-
const data: unknown = JSON.parse(eventData);
-
const validateResult = validateWsMessageType(data);
-
if (!validateResult.ok) return;
-
-
const { data: wsMessage } = validateResult;
-
if (wsMessage.type === "shard/history") {
-
const history = validateHistoryMessage(wsMessage);
-
if (!history) return;
-
if (history.messages) setMessages(history.messages);
-
} else {
-
const message = validateNewMessage(wsMessage);
-
if (!message) return;
-
setMessages((prev) => [...prev, message]);
-
}
-
};
-
-
ws.onerror = (error) => {
-
console.error("WebSocket error:", error);
-
};
-
-
ws.onclose = () => {
-
console.log("Disconnected from WebSocket");
-
setIsConnected(false);
-
};
-
-
// Cleanup on unmount
-
return () => {
-
ws.close();
-
};
-
}, [ws]);
-
-
const sendMessage = ({ text, did }: SendMessageOpts) => {
-
if (!ws) return;
-
if (ws.readyState === WebSocket.OPEN) {
-
ws.send(
-
JSON.stringify({
-
type: "shard/message",
-
text,
-
did,
-
timestamp: new Date(),
-
}),
-
);
-
}
-
};
-
-
return { messages, isConnected, sendMessage };
-
}
-
-
export interface SendMessageOpts {
-
text: string;
-
did: DidPlc | DidWeb;
-
}
+36
src/lib/utils/messages.ts
···
+
import type { Did } from "@/lib/types/atproto";
+
import type { ShardMessage } from "@/lib/types/messages";
+
import type { Dispatch, SetStateAction } from "react";
+
+
export const sendShardMessage = (
+
{
+
content,
+
channel,
+
sentBy,
+
}: {
+
content: string;
+
channel: string;
+
sentBy: Did;
+
},
+
latticeSocket: WebSocket,
+
) => {
+
const message: ShardMessage = {
+
type: "shard/message",
+
content,
+
channel,
+
sentBy,
+
sentAt: new Date(),
+
};
+
if (latticeSocket.readyState === WebSocket.OPEN)
+
latticeSocket.send(JSON.stringify(message));
+
};
+
+
export const shardMessageHandler = ({
+
incomingMessage,
+
setMessages,
+
}: {
+
incomingMessage: ShardMessage;
+
setMessages: Dispatch<SetStateAction<Array<ShardMessage>>>;
+
}) => {
+
setMessages((prev) => [...prev, incomingMessage]);
+
};
-34
src/lib/validators.ts
···
import type { WebsocketMessage } from "@/lib/types/messages";
import {
-
historyMessageSchema,
-
shardMessageSchema,
websocketMessageSchema,
} from "@/lib/types/messages";
import type { Result } from "@/lib/utils/result";
···
}
return { ok: true, data: wsMessage };
};
-
-
export const validateHistoryMessage = (data: unknown) => {
-
const {
-
success: historySuccess,
-
error: historyError,
-
data: history,
-
} = historyMessageSchema.safeParse(data);
-
if (!historySuccess) {
-
console.error(
-
"History message schema parsing failed. Did your type drift?",
-
);
-
console.error(historyError);
-
return;
-
}
-
return history;
-
};
-
-
export const validateNewMessage = (data: unknown) => {
-
const {
-
success: messageSuccess,
-
error: messageError,
-
data: message,
-
} = shardMessageSchema.safeParse(data);
-
if (!messageSuccess) {
-
console.error(
-
"New message schema parsing failed. Did your type drift?",
-
);
-
console.error(messageError);
-
return;
-
}
-
return message;
-
};
+2 -1
src/providers/authed/HandshakesProvider.tsx
···
const handshakeQueries = useQueries({
queries: channels.map((channelObj) => ({
+
enabled: !channelsInitialising && !membershipsInitialising,
queryKey: ["handshakes", channelObj.channel.name],
queryFn: () =>
handshakesQueryFn({
···
});
const isInitialising =
-
isOAuthReady ||
+
!isOAuthReady ||
membershipsInitialising ||
channelsInitialising ||
handshakeQueries.some((q) => q.isLoading);
+16 -6
src/providers/authed/SessionsProvider.tsx
···
};
export const SessionsProvider = ({ children }: { children: ReactNode }) => {
-
const { handshakesMap } = useHandshakes();
+
const { handshakesMap, isInitialising: handshakesInitialising } =
+
useHandshakes();
const handshakes = handshakesMap.entries().toArray();
const endpointQueries = useQueries({
queries: handshakes.map((handshake) => ({
+
enabled: !handshakesInitialising,
queryKey: ["lattice-endpoints", handshake[0].rKey],
queryFn: async () => {
return await endpointQueryFn(handshake);
···
})),
});
-
const isInitialising = endpointQueries.some((q) => q.isLoading);
+
console.log(endpointQueries);
+
+
const isInitialising =
+
handshakesInitialising || endpointQueries.some((q) => q.isLoading);
const error = endpointQueries.find((q) => q.error)?.error ?? null;
const sessionsMap = new Map<LatticeSessionInfo, WebSocket>();
···
getSession: (sessionInfo: LatticeSessionInfo) =>
sessionsMap.get(sessionInfo),
findChannelSession: (channel: AtUri) => {
-
const sessionInfo = sessionsMap
-
.keys()
-
.find((sessionInfo) =>
-
sessionInfo.allowedChannels.includes(channel),
+
console.log("sessionsMap", sessionsMap);
+
const sessionInfo = sessionsMap.keys().find((sessionInfo) => {
+
const foundInfo = sessionInfo.allowedChannels.some(
+
(allowedChannel) => allowedChannel.rKey === channel.rKey,
);
+
if (!foundInfo) return;
+
return sessionInfo;
+
});
+
+
console.log("tried to find", channel);
if (!sessionInfo)
throw new Error(
"Provided channel at:// URI (object) could not be found in any existing lattice sessions",