diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ae1f8d5..9023fbd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,9 +8,11 @@ import { type Request } from "./api/types"; type AppProps = { socket: WebSocket | null; + token: string; + userName: string; }; -export function App({ socket }: AppProps) { +export function App({ socket, token, userName }: AppProps) { const [roomID, setRoomID] = useState(""); const [rooms, setRooms] = useState([]); const [joinedRooms, setJoinedRooms] = useState>(new Set()); @@ -27,10 +29,19 @@ export function App({ socket }: AppProps) { listRoomsRequest({ type: "list_rooms" }); }, [listRoomsRequest]); + const connectRequest = useWsRequest(socket, undefined); + useEffect(() => { + if (!token || !userName) { + return; + } + + connectRequest({ type: "connect", token, name: userName }); + }, [connectRequest, token, userName]); + const sendMessageRequest = useWsRequest(socket, undefined); const onMessageSent = (content: string) => { - const chat: Chat = { content, user: { name: "You" } }; + const chat: Chat = { content, user: { name: userName } }; if (!roomID) { return; diff --git a/frontend/src/TopBar.tsx b/frontend/src/TopBar.tsx index 4ba6a27..2f1cb9e 100644 --- a/frontend/src/TopBar.tsx +++ b/frontend/src/TopBar.tsx @@ -1,14 +1,103 @@ -export function TopBar() { +import type { FormEvent } from "react"; +import { useEffect, useRef, useState } from "react"; + +type TopBarProps = { + userName: string; + onRename: (nextName: string) => void; +}; + +export function TopBar({ userName, onRename }: TopBarProps) { + const dialogRef = useRef(null); + const [draftName, setDraftName] = useState(userName); + + useEffect(() => { + setDraftName(userName); + }, [userName]); + + const openDialog = () => { + setDraftName(userName); + dialogRef.current?.showModal(); + }; + + const closeDialog = () => { + dialogRef.current?.close(); + }; + + const submitRename = (event: FormEvent) => { + event.preventDefault(); + const nextName = draftName.trim(); + if (!nextName) { + return; + } + onRename(nextName); + closeDialog(); + }; + return ( -
-
- echx + <> +
+
+ echx +
+
+
+ Signed in as + {userName} +
+ +
-
- -
-
+ + +
+
+

Change your name

+
+ +
+
+

+ Pick a name that appears to everyone you chat with. This updates immediately for your + session. +

+
+ +
+ Max 40 characters. +
+ + +
+
+
+
+
+ +
+
+ ); } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 85f4fa0..fa2e4e4 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -16,6 +16,8 @@ const queryClient = new QueryClient(); export function Main() { const [socket, setSocket] = useState(null); + const [token, setToken] = useState(() => readStoredToken()); + const [userName, setUserName] = useState(() => readStoredName()); useEffect(() => { const nextSocket = new WebSocket(resolveWebSocketUrl()); @@ -33,12 +35,20 @@ export function Main() { }; }, []); + useEffect(() => { + storeToken(token); + }, [token]); + + useEffect(() => { + storeName(userName); + }, [userName]); + return (
- +
- +
@@ -60,3 +70,37 @@ function resolveWebSocketUrl(): string { return url.toString(); } + +function readStoredToken(): string { + if (typeof window === "undefined") { + return "guest"; + } + const stored = window.localStorage.getItem("echx_token"); + if (stored) { + return stored; + } + const nextToken = window.crypto?.randomUUID?.() ?? `guest-${Date.now()}`; + window.localStorage.setItem("echx_token", nextToken); + return nextToken; +} + +function readStoredName(): string { + if (typeof window === "undefined") { + return "Guest"; + } + return window.localStorage.getItem("echx_name") ?? "Guest"; +} + +function storeToken(token: string) { + if (typeof window === "undefined") { + return; + } + window.localStorage.setItem("echx_token", token); +} + +function storeName(name: string) { + if (typeof window === "undefined") { + return; + } + window.localStorage.setItem("echx_name", name); +}