Public Access
60 lines
2.6 KiB
React
60 lines
2.6 KiB
React
import {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
|
import {request} from '../utils/api.js';
|
|
import {hasSession} from '../utils/auth.js';
|
|
import useSession from '../hooks/useSession.js';
|
|
import {ME_URL, REQUEST_TIMEOUT_MS} from '../utils/settings.js';
|
|
|
|
const CurrentUserContext = createContext(null);
|
|
|
|
/**
|
|
* GET /api/users/me for the whole app. `user` is undefined until the first answer and null without a session.
|
|
* `reload()` loads it again and resolves to the `request()` result (not ok for a 200 without a body).
|
|
* `updateUser(fn)` changes it locally after a save, and skips it when the user is gone by then. It follows the
|
|
* session. A save that replaces settings still loads /me itself (see saveSettings), as it must be fresh.
|
|
*/
|
|
export function CurrentUserProvider({children}) {
|
|
const [user, setUser] = useState(() => (hasSession() ? undefined : null));
|
|
const latest = useRef(0);
|
|
|
|
const reload = useCallback(async () => {
|
|
const call = ++latest.current;
|
|
// After a logout, "no user" becomes "loading" (e.g. the import waits for the favorite platform).
|
|
setUser((current) => current ?? undefined);
|
|
const response = await request(ME_URL(), {signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)});
|
|
const result = {...response, ok: response.ok && response.data != null};
|
|
if (call === latest.current) {
|
|
// Only a 401 means "no user"; another failure keeps the user shown so far, or ends "loading".
|
|
if (result.ok) setUser(result.data);
|
|
else if (result.status === 401) setUser(null);
|
|
else setUser((current) => current ?? null);
|
|
}
|
|
return result;
|
|
}, []);
|
|
|
|
// A new stamp is a login, a "view as" or a settings save, also in another tab.
|
|
const {stamp} = useSession();
|
|
useEffect(() => {
|
|
if (stamp) {
|
|
reload();
|
|
} else {
|
|
// A /me answer still on the way must not bring the user back.
|
|
latest.current += 1;
|
|
setUser(null);
|
|
}
|
|
}, [stamp, reload]);
|
|
|
|
const updateUser = useCallback((change) => setUser((current) => current && change(current)), []);
|
|
const value = useMemo(() => ({user, reload, updateUser}), [user, reload, updateUser]);
|
|
|
|
return <CurrentUserContext.Provider value={value}>{children}</CurrentUserContext.Provider>;
|
|
}
|
|
|
|
/**
|
|
* `{user, reload, updateUser}`.
|
|
*/
|
|
export function useCurrentUserContext() {
|
|
const value = useContext(CurrentUserContext);
|
|
if (!value) throw new Error('useCurrentUserContext must be used inside <CurrentUserProvider>');
|
|
return value;
|
|
}
|