From bb583eaa72ef1d724050063b078881a1c22848b7 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 2 Jan 2023 22:21:11 -0500 Subject: [PATCH] Automatic account sync with react --- web/src/app/AccountApi.js | 59 +++++++++++++++++++++++++------ web/src/components/App.js | 41 ++------------------- web/src/components/Navigation.js | 8 ++++- web/src/components/Preferences.js | 18 ++++++---- web/src/components/hooks.js | 12 +++++++ 5 files changed, 81 insertions(+), 57 deletions(-) diff --git a/web/src/app/AccountApi.js b/web/src/app/AccountApi.js index cc13452..4a143dd 100644 --- a/web/src/app/AccountApi.js +++ b/web/src/app/AccountApi.js @@ -1,22 +1,20 @@ import { + accountAccessSingleUrl, + accountAccessUrl, accountPasswordUrl, accountSettingsUrl, accountSubscriptionSingleUrl, accountSubscriptionUrl, accountTokenUrl, accountUrl, - fetchLinesIterator, withBasicAuth, - withBearerAuth, - topicShortUrl, - topicUrl, - topicUrlAuth, - topicUrlJsonPoll, - topicUrlJsonPollWithSince, accountAccessUrl, accountAccessSingleUrl + withBearerAuth } from "./utils"; -import userManager from "./UserManager"; import session from "./Session"; import subscriptionManager from "./SubscriptionManager"; +import i18n from "i18next"; +import prefs from "./Prefs"; +import routes from "../components/routes"; const delayMillis = 45000; // 45 seconds const intervalMillis = 900000; // 15 minutes @@ -24,6 +22,15 @@ const intervalMillis = 900000; // 15 minutes class AccountApi { constructor() { this.timer = null; + this.listener = null; // Fired when account is fetched from remote + } + + registerListener(listener) { + this.listener = listener; + } + + resetListener() { + this.listener = null; } async login(user) { @@ -92,6 +99,9 @@ class AccountApi { } const account = await response.json(); console.log(`[AccountApi] Account`, account); + if (this.listener) { + this.listener(account); + } return account; } @@ -240,8 +250,37 @@ class AccountApi { } } - sync() { - // TODO + async sync() { + try { + if (!session.token()) { + return null; + } + console.log(`[AccountApi] Syncing account`); + const remoteAccount = await this.get(); + if (remoteAccount.language) { + await i18n.changeLanguage(remoteAccount.language); + } + if (remoteAccount.notification) { + if (remoteAccount.notification.sound) { + await prefs.setSound(remoteAccount.notification.sound); + } + if (remoteAccount.notification.delete_after) { + await prefs.setDeleteAfter(remoteAccount.notification.delete_after); + } + if (remoteAccount.notification.min_priority) { + await prefs.setMinPriority(remoteAccount.notification.min_priority); + } + } + if (remoteAccount.subscriptions) { + await subscriptionManager.syncFromRemote(remoteAccount.subscriptions); + } + return remoteAccount; + } catch (e) { + console.log(`[AccountApi] Error fetching account`, e); + if ((e instanceof UnauthorizedError)) { + session.resetAndRedirect(routes.login); + } + } } startWorker() { diff --git a/web/src/components/App.js b/web/src/components/App.js index 1374d8d..f9b9dd2 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -17,21 +17,17 @@ import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from import {expandUrl} from "../app/utils"; import ErrorBoundary from "./ErrorBoundary"; import routes from "./routes"; -import {useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks"; +import {useAccountListener, useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks"; import PublishDialog from "./PublishDialog"; import Messaging from "./Messaging"; import "./i18n"; // Translations! import {Backdrop, CircularProgress} from "@mui/material"; import Home from "./Home"; import Login from "./Login"; -import i18n from "i18next"; -import prefs from "../app/Prefs"; -import session from "../app/Session"; import Pricing from "./Pricing"; import Signup from "./Signup"; import Account from "./Account"; import ResetPassword from "./ResetPassword"; -import accountApi, {UnauthorizedError} from "../app/AccountApi"; const App = () => { return ( @@ -87,43 +83,10 @@ const Layout = () => { }); useConnectionListeners(subscriptions, users); + useAccountListener(setAccount) useBackgroundProcesses(); useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]); - useEffect(() => { - (async () => { - // TODO this should not live here - try { - if (!session.token()) { - return; - } - const remoteAccount = await accountApi.get(); - setAccount(remoteAccount); - if (remoteAccount.language) { - await i18n.changeLanguage(remoteAccount.language); - } - if (remoteAccount.notification) { - if (remoteAccount.notification.sound) { - await prefs.setSound(remoteAccount.notification.sound); - } - if (remoteAccount.notification.delete_after) { - await prefs.setDeleteAfter(remoteAccount.notification.delete_after); - } - if (remoteAccount.notification.min_priority) { - await prefs.setMinPriority(remoteAccount.notification.min_priority); - } - } - if (remoteAccount.subscriptions) { - await subscriptionManager.syncFromRemote(remoteAccount.subscriptions); - } - } catch (e) { - console.log(`[App] Error fetching account`, e); - if ((e instanceof UnauthorizedError)) { - session.resetAndRedirect(routes.login); - } - } - })(); - }, []); return ( diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js index 988893c..e7c2a63 100644 --- a/web/src/components/Navigation.js +++ b/web/src/components/Navigation.js @@ -27,6 +27,7 @@ import config from "../app/config"; import ArticleIcon from '@mui/icons-material/Article'; import {Trans, useTranslation} from "react-i18next"; import session from "../app/Session"; +import accountApi from "../app/AccountApi"; const navWidth = 280; @@ -92,6 +93,11 @@ const NavList = (props) => { notifier.maybeRequestPermission(granted => props.onNotificationGranted(granted)) }; + const handleAccountClick = () => { + accountApi.sync(); // Dangle! + navigate(routes.account); + }; + const showSubscriptionsList = props.subscriptions?.length > 0; const showNotificationBrowserNotSupportedBox = !notifier.browserSupported(); const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser @@ -124,7 +130,7 @@ const NavList = (props) => { } {session.exists() && - navigate(routes.account)} selected={location.pathname === routes.account}> + diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index 32c48d2..6b7162d 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -5,7 +5,7 @@ import { CardContent, FormControl, Select, - Stack, styled, + Stack, Table, TableBody, TableCell, @@ -482,6 +482,11 @@ const Reservations = () => { const [dialogKey, setDialogKey] = useState(0); const [dialogOpen, setDialogOpen] = useState(false); + if (!session.exists() || !account) { + return <>; + } + const reservations = account.reservations || []; + const handleAddClick = () => { setDialogKey(prev => prev+1); setDialogOpen(true); @@ -495,6 +500,7 @@ const Reservations = () => { setDialogOpen(false); try { await accountApi.upsertAccess(reservation.topic, reservation.everyone); + await accountApi.sync(); console.debug(`[Preferences] Added topic reservation`, reservation); } catch (e) { console.log(`[Preferences] Error topic reservation.`, e); @@ -502,10 +508,6 @@ const Reservations = () => { // FIXME handle 401/403 }; - if (!session.exists() || !account) { - return <>; - } - return ( @@ -515,7 +517,7 @@ const Reservations = () => { {t("prefs_reservations_description")} - {account.reservations.length > 0 && } + {reservations.length > 0 && } @@ -523,7 +525,7 @@ const Reservations = () => { key={`reservationAddDialog${dialogKey}`} open={dialogOpen} reservation={null} - reservations={account.reservations} + reservations={reservations} onCancel={handleDialogCancel} onSubmit={handleDialogSubmit} /> @@ -552,6 +554,7 @@ const ReservationsTable = (props) => { setDialogOpen(false); try { await accountApi.upsertAccess(reservation.topic, reservation.everyone); + await accountApi.sync(); console.debug(`[Preferences] Added topic reservation`, reservation); } catch (e) { console.log(`[Preferences] Error topic reservation.`, e); @@ -562,6 +565,7 @@ const ReservationsTable = (props) => { const handleDeleteClick = async (reservation) => { try { await accountApi.deleteAccess(reservation.topic); + await accountApi.sync(); console.debug(`[Preferences] Deleted topic reservation`, reservation); } catch (e) { console.log(`[Preferences] Error topic reservation.`, e); diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 1c4e872..ce80c95 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -96,3 +96,15 @@ export const useBackgroundProcesses = () => { accountApi.startWorker(); }, []); } + +export const useAccountListener = (setAccount) => { + useEffect(() => { + accountApi.registerListener(setAccount); + (async () => { + await accountApi.sync(); + })(); + return () => { + accountApi.registerListener(); + } + }, []); +}