Automatic account sync with react

pull/584/head
binwiederhier 2023-01-02 22:21:11 -05:00
parent d666cab77a
commit bb583eaa72
5 changed files with 81 additions and 57 deletions

View File

@ -1,22 +1,20 @@
import { import {
accountAccessSingleUrl,
accountAccessUrl,
accountPasswordUrl, accountPasswordUrl,
accountSettingsUrl, accountSettingsUrl,
accountSubscriptionSingleUrl, accountSubscriptionSingleUrl,
accountSubscriptionUrl, accountSubscriptionUrl,
accountTokenUrl, accountTokenUrl,
accountUrl, accountUrl,
fetchLinesIterator,
withBasicAuth, withBasicAuth,
withBearerAuth, withBearerAuth
topicShortUrl,
topicUrl,
topicUrlAuth,
topicUrlJsonPoll,
topicUrlJsonPollWithSince, accountAccessUrl, accountAccessSingleUrl
} from "./utils"; } from "./utils";
import userManager from "./UserManager";
import session from "./Session"; import session from "./Session";
import subscriptionManager from "./SubscriptionManager"; import subscriptionManager from "./SubscriptionManager";
import i18n from "i18next";
import prefs from "./Prefs";
import routes from "../components/routes";
const delayMillis = 45000; // 45 seconds const delayMillis = 45000; // 45 seconds
const intervalMillis = 900000; // 15 minutes const intervalMillis = 900000; // 15 minutes
@ -24,6 +22,15 @@ const intervalMillis = 900000; // 15 minutes
class AccountApi { class AccountApi {
constructor() { constructor() {
this.timer = null; 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) { async login(user) {
@ -92,6 +99,9 @@ class AccountApi {
} }
const account = await response.json(); const account = await response.json();
console.log(`[AccountApi] Account`, account); console.log(`[AccountApi] Account`, account);
if (this.listener) {
this.listener(account);
}
return account; return account;
} }
@ -240,8 +250,37 @@ class AccountApi {
} }
} }
sync() { async sync() {
// TODO 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() { startWorker() {

View File

@ -17,21 +17,17 @@ import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from
import {expandUrl} from "../app/utils"; import {expandUrl} from "../app/utils";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes"; import routes from "./routes";
import {useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks"; import {useAccountListener, useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks";
import PublishDialog from "./PublishDialog"; import PublishDialog from "./PublishDialog";
import Messaging from "./Messaging"; import Messaging from "./Messaging";
import "./i18n"; // Translations! import "./i18n"; // Translations!
import {Backdrop, CircularProgress} from "@mui/material"; import {Backdrop, CircularProgress} from "@mui/material";
import Home from "./Home"; import Home from "./Home";
import Login from "./Login"; import Login from "./Login";
import i18n from "i18next";
import prefs from "../app/Prefs";
import session from "../app/Session";
import Pricing from "./Pricing"; import Pricing from "./Pricing";
import Signup from "./Signup"; import Signup from "./Signup";
import Account from "./Account"; import Account from "./Account";
import ResetPassword from "./ResetPassword"; import ResetPassword from "./ResetPassword";
import accountApi, {UnauthorizedError} from "../app/AccountApi";
const App = () => { const App = () => {
return ( return (
@ -87,43 +83,10 @@ const Layout = () => {
}); });
useConnectionListeners(subscriptions, users); useConnectionListeners(subscriptions, users);
useAccountListener(setAccount)
useBackgroundProcesses(); useBackgroundProcesses();
useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]); 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 ( return (
<Box sx={{display: 'flex'}}> <Box sx={{display: 'flex'}}>
<CssBaseline/> <CssBaseline/>

View File

@ -27,6 +27,7 @@ import config from "../app/config";
import ArticleIcon from '@mui/icons-material/Article'; import ArticleIcon from '@mui/icons-material/Article';
import {Trans, useTranslation} from "react-i18next"; import {Trans, useTranslation} from "react-i18next";
import session from "../app/Session"; import session from "../app/Session";
import accountApi from "../app/AccountApi";
const navWidth = 280; const navWidth = 280;
@ -92,6 +93,11 @@ const NavList = (props) => {
notifier.maybeRequestPermission(granted => props.onNotificationGranted(granted)) notifier.maybeRequestPermission(granted => props.onNotificationGranted(granted))
}; };
const handleAccountClick = () => {
accountApi.sync(); // Dangle!
navigate(routes.account);
};
const showSubscriptionsList = props.subscriptions?.length > 0; const showSubscriptionsList = props.subscriptions?.length > 0;
const showNotificationBrowserNotSupportedBox = !notifier.browserSupported(); const showNotificationBrowserNotSupportedBox = !notifier.browserSupported();
const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
@ -124,7 +130,7 @@ const NavList = (props) => {
<Divider sx={{my: 1}}/> <Divider sx={{my: 1}}/>
</>} </>}
{session.exists() && {session.exists() &&
<ListItemButton onClick={() => navigate(routes.account)} selected={location.pathname === routes.account}> <ListItemButton onClick={handleAccountClick} selected={location.pathname === routes.account}>
<ListItemIcon><Person/></ListItemIcon> <ListItemIcon><Person/></ListItemIcon>
<ListItemText primary={t("nav_button_account")}/> <ListItemText primary={t("nav_button_account")}/>
</ListItemButton> </ListItemButton>

View File

@ -5,7 +5,7 @@ import {
CardContent, CardContent,
FormControl, FormControl,
Select, Select,
Stack, styled, Stack,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
@ -482,6 +482,11 @@ const Reservations = () => {
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
if (!session.exists() || !account) {
return <></>;
}
const reservations = account.reservations || [];
const handleAddClick = () => { const handleAddClick = () => {
setDialogKey(prev => prev+1); setDialogKey(prev => prev+1);
setDialogOpen(true); setDialogOpen(true);
@ -495,6 +500,7 @@ const Reservations = () => {
setDialogOpen(false); setDialogOpen(false);
try { try {
await accountApi.upsertAccess(reservation.topic, reservation.everyone); await accountApi.upsertAccess(reservation.topic, reservation.everyone);
await accountApi.sync();
console.debug(`[Preferences] Added topic reservation`, reservation); console.debug(`[Preferences] Added topic reservation`, reservation);
} catch (e) { } catch (e) {
console.log(`[Preferences] Error topic reservation.`, e); console.log(`[Preferences] Error topic reservation.`, e);
@ -502,10 +508,6 @@ const Reservations = () => {
// FIXME handle 401/403 // FIXME handle 401/403
}; };
if (!session.exists() || !account) {
return <></>;
}
return ( return (
<Card sx={{ padding: 1 }} aria-label={t("prefs_reservations_title")}> <Card sx={{ padding: 1 }} aria-label={t("prefs_reservations_title")}>
<CardContent sx={{ paddingBottom: 1 }}> <CardContent sx={{ paddingBottom: 1 }}>
@ -515,7 +517,7 @@ const Reservations = () => {
<Paragraph> <Paragraph>
{t("prefs_reservations_description")} {t("prefs_reservations_description")}
</Paragraph> </Paragraph>
{account.reservations.length > 0 && <ReservationsTable reservations={account.reservations}/>} {reservations.length > 0 && <ReservationsTable reservations={reservations}/>}
</CardContent> </CardContent>
<CardActions> <CardActions>
<Button onClick={handleAddClick}>{t("prefs_reservations_add_button")}</Button> <Button onClick={handleAddClick}>{t("prefs_reservations_add_button")}</Button>
@ -523,7 +525,7 @@ const Reservations = () => {
key={`reservationAddDialog${dialogKey}`} key={`reservationAddDialog${dialogKey}`}
open={dialogOpen} open={dialogOpen}
reservation={null} reservation={null}
reservations={account.reservations} reservations={reservations}
onCancel={handleDialogCancel} onCancel={handleDialogCancel}
onSubmit={handleDialogSubmit} onSubmit={handleDialogSubmit}
/> />
@ -552,6 +554,7 @@ const ReservationsTable = (props) => {
setDialogOpen(false); setDialogOpen(false);
try { try {
await accountApi.upsertAccess(reservation.topic, reservation.everyone); await accountApi.upsertAccess(reservation.topic, reservation.everyone);
await accountApi.sync();
console.debug(`[Preferences] Added topic reservation`, reservation); console.debug(`[Preferences] Added topic reservation`, reservation);
} catch (e) { } catch (e) {
console.log(`[Preferences] Error topic reservation.`, e); console.log(`[Preferences] Error topic reservation.`, e);
@ -562,6 +565,7 @@ const ReservationsTable = (props) => {
const handleDeleteClick = async (reservation) => { const handleDeleteClick = async (reservation) => {
try { try {
await accountApi.deleteAccess(reservation.topic); await accountApi.deleteAccess(reservation.topic);
await accountApi.sync();
console.debug(`[Preferences] Deleted topic reservation`, reservation); console.debug(`[Preferences] Deleted topic reservation`, reservation);
} catch (e) { } catch (e) {
console.log(`[Preferences] Error topic reservation.`, e); console.log(`[Preferences] Error topic reservation.`, e);

View File

@ -96,3 +96,15 @@ export const useBackgroundProcesses = () => {
accountApi.startWorker(); accountApi.startWorker();
}, []); }, []);
} }
export const useAccountListener = (setAccount) => {
useEffect(() => {
accountApi.registerListener(setAccount);
(async () => {
await accountApi.sync();
})();
return () => {
accountApi.registerListener();
}
}, []);
}