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 ---------------------------------------------------------------
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) => {

View File

@ -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 <div>Loading</div>;
}
/* -------------------------------------------------- */
/* -------------------------------------------------- */
useEffect(() => {
if (!currentUser) return;
/* Render */
/* 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 =
currentUser.index && currentUser.index > 0
? `${base}.${currentUser.index}`
: base;
<TitleBar />
dispatch(profileLinkSet(newLink));
}, [currentUser, profileLink, dispatch]);
<Switch>
{/* Friends list ------------------------------------------------ */}
/* Loading guard */
<Route path="/fakebook/friends/list" component={FriendsListPage} />
if (!currentUser || users.length === 0) {
return <div>Loading</div>;
}
{/* Single photo ----------------------------------------------- */}
/* -------------------------------------------------- */
<Route path="/fakebook/photo/:userID/:n" component={PhotoViewer} />
/* Render */
{/* Watch (video feed) ----------------------------------------- */}
/* -------------------------------------------------- */
<Route
path="/fakebook/watch"
render={(props) => <HomePage {...props} className="pt-5" />}
/>
return (
<div className='bg-200 vw-100 main-container overflow-hidden'>
<Container className='w-100 p-0' fluid>
<Router>
<RouteStateSync />
{/* User profile ----------------------------------------------- */}
<TitleBar />
<Route path="/fakebook/:userName" component={Profile} />
<Switch>
{/* Friends list ------------------------------------------------ */}
{/* News-feed root --------------------------------------------- */}
<Route path='/fakebook/friends/list' component={FriendsListPage} />
{/* Single photo ----------------------------------------------- */}
<Route path='/fakebook/photo/:userID/:n' component={PhotoViewer} />
{/* Watch (video feed) ----------------------------------------- */}
<Route
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>
);
<Route
path="/fakebook"
exact
render={(props) => <HomePage {...props} className="pt-5" />}
/>
</Switch>
</Router>
</Container>
</div>
);
};
export default UserAccount;