From 545413d2ce51ac4ecac201ea69a74fdecd891970 Mon Sep 17 00:00:00 2001 From: Alex Erdei Date: Wed, 7 May 2025 22:46:45 +0100 Subject: [PATCH] Debug when user stays online at closing browser --- src/backend/backend.js | 101 ++++++++++++---- src/components/UserAccount.jsx | 203 +++++++++++++++------------------ 2 files changed, 172 insertions(+), 132 deletions(-) diff --git a/src/backend/backend.js b/src/backend/backend.js index 8f7978f..4ac1d45 100644 --- a/src/backend/backend.js +++ b/src/backend/backend.js @@ -153,34 +153,22 @@ const openSocket = () => { }); }; -// auth helpers ----------------------------------------------------------- - -const setAuth = (t, u) => { - authToken = t; - authUser = u; - saveAuthToStorage(t, u); - openSocket(); -}; - -const clearAuth = () => { - authToken = null; - authUser = null; - clearAuthStorage(); - if (hub) { - hub.stop(); - hub = null; - } -}; - // presence --------------------------------------------------------------- -const patchOnline = async (on) => { +// patchOnline(on, keep = false) ← keep=true will add keepalive: true + +const patchOnline = async (on, keep = false) => { if (!authUser) return; try { - await $fetch(USERS_URL, { + await fetch(USERS_URL, { method: "PUT", + + headers: { "Content-Type": "application/json", ...authHeader() }, + body: JSON.stringify({ user_id: authUser, isOnline: on ? 1 : 0 }), + + ...(keep ? { keepalive: true } : {}), }); } catch (e) { console.warn("[online] PUT failed:", e.message); @@ -191,6 +179,77 @@ export const currentUserOnline = () => patchOnline(true); export const currentUserOffline = () => patchOnline(false); +// leave / background handlers ------------------------------------------- + +let leaveHandlerInstalled = false; + +const sendOfflineKeepalive = () => patchOnline(false, true); + +const handleVisibility = () => { + if (document.visibilityState === "hidden") sendOfflineKeepalive(); + + if (document.visibilityState === "visible") currentUserOnline(); +}; + +const installLeaveHandlers = () => { + if (leaveHandlerInstalled) return; + + window.addEventListener("pagehide", sendOfflineKeepalive); // tab / window close + + window.addEventListener("freeze", sendOfflineKeepalive); // page-lifecycle freeze + + window.addEventListener("resume", currentUserOnline); // page-lifecycle resume + + document.addEventListener("visibilitychange", handleVisibility); + + leaveHandlerInstalled = true; +}; + +const removeLeaveHandlers = () => { + if (!leaveHandlerInstalled) return; + + window.removeEventListener("pagehide", sendOfflineKeepalive); + + window.removeEventListener("freeze", sendOfflineKeepalive); + + window.removeEventListener("resume", currentUserOnline); + + document.removeEventListener("visibilitychange", handleVisibility); + + leaveHandlerInstalled = false; +}; + +// setAuth … add + +const setAuth = (t, u) => { + authToken = t; + + authUser = u; + + saveAuthToStorage(t, u); + + openSocket(); + + installLeaveHandlers(); // ← here +}; + +// clearAuth … add + +const clearAuth = () => { + authToken = null; + + authUser = null; + + clearAuthStorage(); + + if (hub) { + hub.stop(); + hub = null; + } + + removeLeaveHandlers(); // ← here +}; + // bootstrap after login/restore ----------------------------------------- const bootstrapSession = async (uid) => { diff --git a/src/components/UserAccount.jsx b/src/components/UserAccount.jsx index e05ea03..58d91bc 100644 --- a/src/components/UserAccount.jsx +++ b/src/components/UserAccount.jsx @@ -15,10 +15,10 @@ import FriendsListPage from "./FriendsListPage"; /* Router */ import { - BrowserRouter as Router, - Switch, - Route, - useLocation, + BrowserRouter as Router, + Switch, + Route, + useLocation, } from "react-router-dom"; /* Layout */ @@ -30,19 +30,19 @@ import Container from "react-bootstrap/Container"; import { useDispatch, useSelector } from "react-redux"; import { - friendsListPageSet, - profileLinkSet, - watchSet, + friendsListPageSet, + profileLinkSet, + watchSet, } from "../features/accountPage/accountPageSlice"; /* Mock-backend helpers */ import { - currentUserOffline, - currentUserOnline, - subscribeCurrentUser, - subscribeUsers, - subscribePosts, + currentUserOffline, + currentUserOnline, + subscribeCurrentUser, + subscribeUsers, + subscribePosts, } from "../backend/backend"; /* ------------------------------------------------------------------ */ @@ -52,158 +52,139 @@ import { /* ------------------------------------------------------------------ */ const RouteStateSync = () => { - const dispatch = useDispatch(); + const dispatch = useDispatch(); - const location = useLocation(); + const location = useLocation(); - useEffect(() => { - const { pathname } = location; + useEffect(() => { + const { pathname } = location; - /* friends list page */ + /* friends list page */ - dispatch(friendsListPageSet(pathname.startsWith("/fakebook/friends/list"))); + dispatch(friendsListPageSet(pathname.startsWith("/fakebook/friends/list"))); - /* watch page (videos feed) */ + /* watch page (videos feed) */ - dispatch(watchSet(pathname.startsWith("/fakebook/watch"))); - }, [location, dispatch]); + dispatch(watchSet(pathname.startsWith("/fakebook/watch"))); + }, [location, dispatch]); - return null; // renders nothing + return null; // renders nothing }; /* ------------------------------------------------------------------ */ const UserAccount = () => { - const dispatch = useDispatch(); + const dispatch = useDispatch(); - /* Redux selectors */ + /* Redux selectors */ - const profileLink = useSelector((state) => state.accountPage.profileLink); + const profileLink = useSelector((state) => state.accountPage.profileLink); - const currentUser = useSelector((state) => state.currentUser); + const currentUser = useSelector((state) => state.currentUser); - const users = useSelector((state) => state.users); + const users = useSelector((state) => state.users); - /* -------------------------------------------------- */ + /* -------------------------------------------------- */ - /* Firestore-like subscriptions & online/offline flag */ + /* Firestore-like subscriptions */ - /* -------------------------------------------------- */ + /* -------------------------------------------------- */ - useEffect(() => { - const unsubCurrentUser = subscribeCurrentUser(); + useEffect(() => { + const unsubCurrentUser = subscribeCurrentUser(); - const unsubUsers = subscribeUsers(); + const unsubUsers = subscribeUsers(); - const unsubPosts = subscribePosts(); + const unsubPosts = subscribePosts(); - /* mark user online */ + /* mark user online */ - currentUserOnline(); + currentUserOnline(); - /* window closed or refreshed */ + /* cleanup */ - const beforeUnload = () => currentUserOffline(); + return () => { + unsubCurrentUser(); - window.addEventListener("beforeunload", beforeUnload); + unsubUsers(); - /* tab visibility switch */ + unsubPosts(); + }; + }, []); - const visChange = () => - document.visibilityState === "visible" - ? currentUserOnline() - : currentUserOffline(); + /* -------------------------------------------------- */ - document.addEventListener("visibilitychange", visChange); + /* Build unique profile link (.index appended once) */ - /* cleanup */ + /* -------------------------------------------------- */ - return () => { - unsubCurrentUser(); + useEffect(() => { + if (!currentUser) return; - unsubUsers(); + /* remove any existing trailing ".number" */ - unsubPosts(); + const base = profileLink.replace(/\.\d+$/, ""); - window.removeEventListener("beforeunload", beforeUnload); + const newLink = + currentUser.index && currentUser.index > 0 + ? `${base}.${currentUser.index}` + : base; - document.removeEventListener("visibilitychange", visChange); - }; - }, []); + dispatch(profileLinkSet(newLink)); + }, [currentUser, profileLink, dispatch]); - /* -------------------------------------------------- */ + /* Loading guard */ - /* Build unique profile link (.index appended once) */ + if (!currentUser || users.length === 0) { + return
…Loading
; + } - /* -------------------------------------------------- */ + /* -------------------------------------------------- */ - useEffect(() => { - if (!currentUser) return; + /* Render */ - /* remove any existing trailing ".number" */ + /* -------------------------------------------------- */ - const base = profileLink.replace(/\.\d+$/, ""); + return ( +
+ + + - const newLink = - currentUser.index && currentUser.index > 0 - ? `${base}.${currentUser.index}` - : base; + - dispatch(profileLinkSet(newLink)); - }, [currentUser, profileLink, dispatch]); + + {/* Friends list ------------------------------------------------ */} - /* Loading guard */ + - if (!currentUser || users.length === 0) { - return
…Loading
; - } + {/* Single photo ----------------------------------------------- */} - /* -------------------------------------------------- */ + - /* Render */ + {/* Watch (video feed) ----------------------------------------- */} - /* -------------------------------------------------- */ + } + /> - return ( -
- - - + {/* User profile ----------------------------------------------- */} - + - - {/* Friends list ------------------------------------------------ */} + {/* News-feed root --------------------------------------------- */} - - - {/* Single photo ----------------------------------------------- */} - - - - {/* Watch (video feed) ----------------------------------------- */} - - } - /> - - {/* User profile ----------------------------------------------- */} - - - - {/* News-feed root --------------------------------------------- */} - - } - /> - - - -
- ); + } + /> +
+
+
+
+ ); }; export default UserAccount;