diff --git a/app/src/hooks/useChatRoom.ts b/app/src/hooks/useChatRoom.ts index 659920f..b36eaa2 100644 --- a/app/src/hooks/useChatRoom.ts +++ b/app/src/hooks/useChatRoom.ts @@ -28,6 +28,7 @@ export function useChatRoom( const expiresAtRef = useRef(0) const joinedMeetingRef = useRef(null) const hasJoinedRef = useRef(false) + const hasLeftRef = useRef(false) const expiryTimerRef = useRef | null>(null) const expiryFinalRef = useRef | null>(null) const countdownRef = useRef | null>(null) @@ -208,7 +209,26 @@ export function useChatRoom( setMessages([...mapped]) } + const doExpireLeave = () => { + if (hasLeftRef.current) return + hasLeftRef.current = true + setExpiryWarning( + "Room session expired. Please re-open the link to rejoin." + ) + meeting.leaveRoom().catch(() => {}) + if (typeof window !== "undefined") { + window.location.href = "/" + } + } + const onRoomLeft = ({ state }: { state: string }) => { + if (hasLeftRef.current) return + const isExpired = + expiresAtRef.current > 0 && Date.now() >= expiresAtRef.current + if (isExpired) { + doExpireLeave() + return + } if (state === "disconnected") { setConnectionStatus("reconnecting") } else if (state === "failed") { @@ -237,31 +257,51 @@ export function useChatRoom( }) hasJoinedRef.current = true - const joinedAt = Date.now() + const onVisibilityChange = () => { + if (document.visibilityState === "visible" && expiresAtRef.current > 0) { + const remaining = Math.max( + 0, + Math.floor((expiresAtRef.current - Date.now()) / 1000) + ) + setTimeLeft(remaining) + if (remaining === 0) doExpireLeave() + } + } + document.addEventListener("visibilitychange", onVisibilityChange) + countdownRef.current = setInterval(() => { - const base = expiresAtRef.current || joinedAt + 2 * 60 * 60 * 1000 + const base = expiresAtRef.current + if (!base) return const remaining = Math.max(0, Math.floor((base - Date.now()) / 1000)) setTimeLeft(remaining) - if (remaining === 0 && countdownRef.current) { - clearInterval(countdownRef.current) + if (remaining === 0) { + clearInterval(countdownRef.current!) + countdownRef.current = null + doExpireLeave() } }, 1000) - expiryTimerRef.current = setTimeout(() => { - setExpiryWarning( - "This room will expire in 10 minutes. Copy the link and re-open to continue." - ) - }, 6600 * 1000) - - expiryFinalRef.current = setTimeout(() => { - setExpiryWarning( - "Room session expired. Please re-open the link to rejoin." - ) - meeting.leaveRoom().catch(() => {}) - if (typeof window !== "undefined") { - window.location.href = "/" + const scheduleWarning = () => { + if (!expiresAtRef.current) return + const warnDelay = expiresAtRef.current - Date.now() - 10 * 60 * 1000 + if (warnDelay <= 0) { + setExpiryWarning( + "This room will expire in 10 minutes. Copy the link and re-open to continue." + ) + return } - }, 7200 * 1000) + expiryTimerRef.current = setTimeout(() => { + setExpiryWarning( + "This room will expire in 10 minutes. Copy the link and re-open to continue." + ) + }, warnDelay) + } + scheduleWarning() + + const finalDelay = expiresAtRef.current + ? Math.max(0, expiresAtRef.current - Date.now()) + : 7200 * 1000 + expiryFinalRef.current = setTimeout(doExpireLeave, finalDelay) meeting.self.on("roomLeft", onRoomLeft) meeting.self.on("roomJoined", onRoomJoined) @@ -277,6 +317,7 @@ export function useChatRoom( buildParticipants() return () => { + document.removeEventListener("visibilitychange", onVisibilityChange) meeting.self.off("roomLeft", onRoomLeft) meeting.self.off("roomJoined", onRoomJoined) ;(meeting.meta as any).off("socketConnectionUpdate", onSocketUpdate) @@ -287,8 +328,9 @@ export function useChatRoom( meeting.self.off("screenShareUpdate", buildParticipants) meeting.participants.joined.off("screenShareUpdate", buildParticipants) meeting.chat.off("chatUpdate", syncMessages) - if (hasJoinedRef.current) meeting.leaveRoom() + if (hasJoinedRef.current && !hasLeftRef.current) meeting.leaveRoom() hasJoinedRef.current = false + hasLeftRef.current = false if (expiryTimerRef.current) clearTimeout(expiryTimerRef.current) if (expiryFinalRef.current) clearTimeout(expiryFinalRef.current) if (countdownRef.current) clearInterval(countdownRef.current) diff --git a/app/src/pages/api/token.ts b/app/src/pages/api/token.ts index 94480a6..b11787d 100644 --- a/app/src/pages/api/token.ts +++ b/app/src/pages/api/token.ts @@ -32,6 +32,7 @@ const MAX_NAME_LENGTH = 32 const ROOM_MAX_AGE_MS = 2 * 60 * 60 * 1000 // 2 hours const RATE_LIMIT_WINDOW_S = 60 const RATE_LIMIT_MAX = 20 +const ROOM_KV_TTL_S = 4 * 3600 function rtkBase(env: Env) { return `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/realtime/kit/${env.RTK_APP_ID}` @@ -99,7 +100,7 @@ async function getOrCreateMeeting( botEnabled: record.botEnabled || enableBot, } await env.ROOMS_KV.put(key, JSON.stringify(upgraded), { - expirationTtl: 30 * 24 * 3600, + expirationTtl: ROOM_KV_TTL_S, }) return { meetingId: record.meetingId, @@ -139,7 +140,7 @@ async function getOrCreateMeeting( botEnabled: enableBot, } await env.ROOMS_KV.put(key, JSON.stringify(record), { - expirationTtl: 30 * 24 * 3600, + expirationTtl: ROOM_KV_TTL_S, }) return { meetingId,