Debug when user stays online at closing browser

This commit is contained in:
Alex Erdei 2025-05-07 22:46:45 +01:00
parent b2affa91c7
commit 545413d2ce
2 changed files with 172 additions and 132 deletions

View File

@ -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 --------------------------------------------------------------- // presence ---------------------------------------------------------------
const patchOnline = async (on) => { // patchOnline(on, keep = false) ← keep=true will add keepalive: true
const patchOnline = async (on, keep = false) => {
if (!authUser) return; if (!authUser) return;
try { try {
await $fetch(USERS_URL, { await fetch(USERS_URL, {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json", ...authHeader() },
body: JSON.stringify({ user_id: authUser, isOnline: on ? 1 : 0 }), body: JSON.stringify({ user_id: authUser, isOnline: on ? 1 : 0 }),
...(keep ? { keepalive: true } : {}),
}); });
} catch (e) { } catch (e) {
console.warn("[online] PUT failed:", e.message); console.warn("[online] PUT failed:", e.message);
@ -191,6 +179,77 @@ export const currentUserOnline = () => patchOnline(true);
export const currentUserOffline = () => patchOnline(false); 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 ----------------------------------------- // bootstrap after login/restore -----------------------------------------
const bootstrapSession = async (uid) => { const bootstrapSession = async (uid) => {

View File

@ -15,10 +15,10 @@ import FriendsListPage from "./FriendsListPage";
/* Router */ /* Router */
import { import {
BrowserRouter as Router, BrowserRouter as Router,
Switch, Switch,
Route, Route,
useLocation, useLocation,
} from "react-router-dom"; } from "react-router-dom";
/* Layout */ /* Layout */
@ -30,19 +30,19 @@ import Container from "react-bootstrap/Container";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
friendsListPageSet, friendsListPageSet,
profileLinkSet, profileLinkSet,
watchSet, watchSet,
} from "../features/accountPage/accountPageSlice"; } from "../features/accountPage/accountPageSlice";
/* Mock-backend helpers */ /* Mock-backend helpers */
import { import {
currentUserOffline, currentUserOffline,
currentUserOnline, currentUserOnline,
subscribeCurrentUser, subscribeCurrentUser,
subscribeUsers, subscribeUsers,
subscribePosts, subscribePosts,
} from "../backend/backend"; } from "../backend/backend";
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@ -52,158 +52,139 @@ import {
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
const RouteStateSync = () => { const RouteStateSync = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const location = useLocation(); const location = useLocation();
useEffect(() => { useEffect(() => {
const { pathname } = location; 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"))); dispatch(watchSet(pathname.startsWith("/fakebook/watch")));
}, [location, dispatch]); }, [location, dispatch]);
return null; // renders nothing return null; // renders nothing
}; };
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
const UserAccount = () => { 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(() => { useEffect(() => {
const unsubCurrentUser = subscribeCurrentUser(); 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 () => { useEffect(() => {
unsubCurrentUser(); 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 <div>Loading</div>;
}
/* -------------------------------------------------- */ /* -------------------------------------------------- */
useEffect(() => { /* Render */
if (!currentUser) return;
/* remove any existing trailing ".number" */ /* -------------------------------------------------- */
const base = profileLink.replace(/\.\d+$/, ""); return (
<div className="bg-200 vw-100 main-container overflow-hidden">
<Container className="w-100 p-0" fluid>
<Router>
<RouteStateSync />
const newLink = <TitleBar />
currentUser.index && currentUser.index > 0
? `${base}.${currentUser.index}`
: base;
dispatch(profileLinkSet(newLink)); <Switch>
}, [currentUser, profileLink, dispatch]); {/* Friends list ------------------------------------------------ */}
/* Loading guard */ <Route path="/fakebook/friends/list" component={FriendsListPage} />
if (!currentUser || users.length === 0) { {/* Single photo ----------------------------------------------- */}
return <div>Loading</div>;
}
/* -------------------------------------------------- */ <Route path="/fakebook/photo/:userID/:n" component={PhotoViewer} />
/* Render */ {/* Watch (video feed) ----------------------------------------- */}
/* -------------------------------------------------- */ <Route
path="/fakebook/watch"
render={(props) => <HomePage {...props} className="pt-5" />}
/>
return ( {/* User profile ----------------------------------------------- */}
<div className='bg-200 vw-100 main-container overflow-hidden'>
<Container className='w-100 p-0' fluid>
<Router>
<RouteStateSync />
<TitleBar /> <Route path="/fakebook/:userName" component={Profile} />
<Switch> {/* News-feed root --------------------------------------------- */}
{/* Friends list ------------------------------------------------ */}
<Route path='/fakebook/friends/list' component={FriendsListPage} /> <Route
path="/fakebook"
{/* Single photo ----------------------------------------------- */} exact
render={(props) => <HomePage {...props} className="pt-5" />}
<Route path='/fakebook/photo/:userID/:n' component={PhotoViewer} /> />
</Switch>
{/* Watch (video feed) ----------------------------------------- */} </Router>
</Container>
<Route </div>
path='/fakebook/watch' );
render={(props) => <HomePage {...props} className='pt-5' />}
/>
{/* User profile ----------------------------------------------- */}
<Route path='/fakebook/:userName' component={Profile} />
{/* News-feed root --------------------------------------------- */}
<Route
path='/fakebook'
exact
render={(props) => <HomePage {...props} className='pt-5' />}
/>
</Switch>
</Router>
</Container>
</div>
);
}; };
export default UserAccount; export default UserAccount;