diff --git a/client/app/layout.tsx b/client/app/layout.tsx index e12260b..9cb7152 100644 --- a/client/app/layout.tsx +++ b/client/app/layout.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @next/next/no-page-custom-font */ import "./globals.css"; import type { Metadata } from "next"; diff --git a/client/app/room/page.tsx b/client/app/room/page.tsx index 39bddc7..3974846 100644 --- a/client/app/room/page.tsx +++ b/client/app/room/page.tsx @@ -39,6 +39,7 @@ import { JetBrains_Mono } from "next/font/google"; import { ThemeProvider } from "next-themes"; import { ContentWarningModal } from "@/components/ContentWarningModal"; import { BetterHoverCard, HoverCardProvider } from "@/components/ui/BetterHoverCard"; +import { getCookie, setCookie } from "@/lib/cookies"; dotenv.config(); @@ -200,6 +201,33 @@ const generateUserColor = () => { return colors[Math.floor(Math.random() * colors.length)]; }; +// User persistence helpers using cookies +const restoreUser = (): User | null => { + if (typeof window === 'undefined') return null; + try { + const stored = getCookie("osborne-user"); + if (stored) { + const parsed = JSON.parse(stored); + return { + ...parsed, + lastSeen: new Date(parsed.lastSeen), + }; + } + } catch (error) { + console.error('Error loading user from storage:', error); + } + return null; +}; + +const saveUser = (user: User) => { + if (typeof window === 'undefined') return; + try { + setCookie("osborne-user", JSON.stringify(user), 30); // 30 days + } catch (error) { + console.error('Error saving user to storage:', error); + } +}; + const Room = () => { const router = useRouter(); const searchParams = useSearchParams(); @@ -232,6 +260,8 @@ const Room = () => { const [purgeError, setPurgeError] = useState(null); const [uploadProgress, setUploadProgress] = useState>([]); const [isRecordingOpen, setIsRecordingOpen] = useState(false); + const [reconnectAttempts, setReconnectAttempts] = useState(0); + const [isReconnecting, setIsReconnecting] = useState(false); // Detect mobile screen size useEffect(() => { @@ -325,6 +355,14 @@ const Room = () => { useEffect(() => { setIsClient(true); + // Load user from storage if available + if (roomCode) { + const storedUser = restoreUser(); + if (storedUser) { + setCurrentUser(storedUser); + } + } + // Set initial window width const handleResize = () => { const newWidth = window.innerWidth; @@ -347,7 +385,7 @@ const Room = () => { return () => { window.removeEventListener('resize', handleResize); }; - }, []); + }, [roomCode]); // Calculate panel visibility based on window width // Minimum width needed: 320px (left) + 640px (main content) + 320px (right) = 1280px @@ -440,16 +478,31 @@ const Room = () => { ws.onopen = () => { setStatus("Connected"); setError(""); + setIsReconnecting(false); + setReconnectAttempts(0); // Reset on successful connection - // Create user if not exists - const user: User = { - id: generateUserId(), - name: generateUserName(), - color: generateUserColor(), - lastSeen: new Date(), - isTyping: false, - }; + // Use existing user, check storage, or create new one + let user = currentUserRef.current; + if (!user && roomCode) { + user = restoreUser(); + } + if (!user) { + user = { + id: generateUserId(), + name: generateUserName(), + color: generateUserColor(), + lastSeen: new Date(), + isTyping: false, + }; + } + + // Update lastSeen for reconnection + user.lastSeen = new Date(); + setCurrentUser(user); + + // Save to storage for persistence across sessions + saveUser(user); const message: JoinRoom = { type: "join-room", @@ -585,6 +638,7 @@ const Room = () => { ws.onclose = () => { setStatus("Disconnected"); + setIsReconnecting(false); setTimeout(() => { if (socketRef.current === ws) { @@ -594,13 +648,17 @@ const Room = () => { }; ws.onerror = () => { + setIsReconnecting(true); + const backoffDelay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000); // Max 10 seconds + setTimeout(() => { if (socketRef.current === ws) { + setReconnectAttempts(prev => prev + 1); connectSocket(); } - }, 1000); + }, backoffDelay); }; - }, [roomCode]); + }, [roomCode, reconnectAttempts]); useEffect(() => { if (!isClient || !roomCode) return; @@ -850,7 +908,7 @@ const Room = () => { return ( -
+
{

- The connection to the server was lost. Please check your - internet connection and try to reconnect. + {isReconnecting + ? `Attempting to reconnect... (Attempt ${reconnectAttempts + 1})` + : "The connection to the server was lost. Please check your internet connection and try to reconnect." + }

+ {reconnectAttempts > 3 && ( + + )}
diff --git a/client/components/ContentWarningModal.tsx b/client/components/ContentWarningModal.tsx index 485745b..36219dd 100644 --- a/client/components/ContentWarningModal.tsx +++ b/client/components/ContentWarningModal.tsx @@ -4,20 +4,21 @@ import { useState, useEffect } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { TriangleAlert } from "lucide-react"; +import { getCookie, setCookie } from "@/lib/cookies"; export const ContentWarningModal = () => { const [showWarning, setShowWarning] = useState(false); useEffect(() => { // Check if user has already acknowledged the warning - const hasAcknowledged = localStorage.getItem('content-warning-acknowledged'); + const hasAcknowledged = getCookie('osborne-cwa') === 'true'; if (!hasAcknowledged) { setShowWarning(true); } }, []); const handleAcknowledge = () => { - localStorage.setItem('content-warning-acknowledged', 'true'); + setCookie('osborne-cwa', 'true', 365); // 1 year setShowWarning(false); }; diff --git a/client/lib/cookies.ts b/client/lib/cookies.ts new file mode 100644 index 0000000..8663990 --- /dev/null +++ b/client/lib/cookies.ts @@ -0,0 +1,50 @@ +/** + * Cookie utility functions for browser-based cookie management + */ + +/** + * Get a cookie value by name + * @param name - The name of the cookie + * @returns The cookie value or null if not found + */ +export const getCookie = (name: string): string | null => { + if (typeof window === 'undefined') return null; + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) { + const cookieValue = parts.pop()?.split(';').shift(); + return cookieValue ? decodeURIComponent(cookieValue) : null; + } + return null; +}; + +/** + * Set a cookie with an expiration date + * @param name - The name of the cookie + * @param value - The value to store + * @param days - Number of days until expiration (default: 30) + */ +export const setCookie = (name: string, value: string, days: number = 30) => { + if (typeof window === 'undefined') return; + const expires = new Date(); + expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000)); + document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires.toUTCString()};path=/;SameSite=Lax`; +}; + +/** + * Delete a cookie by setting its expiration to the past + * @param name - The name of the cookie to delete + */ +export const deleteCookie = (name: string) => { + if (typeof window === 'undefined') return; + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;SameSite=Lax`; +}; + +/** + * Check if a cookie exists + * @param name - The name of the cookie + * @returns True if the cookie exists, false otherwise + */ +export const cookieExists = (name: string): boolean => { + return getCookie(name) !== null; +}; \ No newline at end of file diff --git a/client/lib/themes.ts b/client/lib/themes.ts index a2fe37a..4ab05c9 100644 --- a/client/lib/themes.ts +++ b/client/lib/themes.ts @@ -377,11 +377,11 @@ export const getNextTheme = (currentThemeId: string): ThemeConfig => { // Cookie utilities export const saveThemeToCookie = (themeId: string): void => { - document.cookie = `theme=${themeId}; path=/; max-age=${60 * 60 * 24 * 365}`; // 1 year + document.cookie = `osborne-theme=${themeId}; path=/; max-age=${60 * 60 * 24 * 365}`; // 1 year }; export const getThemeFromCookie = (): string | null => { - const match = document.cookie.match(/(?:^|; )theme=([^;]*)/); + const match = document.cookie.match(/(?:^|; )osborne-theme=([^;]*)/); return match ? decodeURIComponent(match[1]) : null; };