App.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import * as React from 'react';
  2. import { Suspense } from "react";
  3. import {useEffect, useState} from 'react';
  4. import Box from '@mui/material/Box';
  5. import {ThemeProvider} from '@mui/material/styles';
  6. import CssBaseline from '@mui/material/CssBaseline';
  7. import Toolbar from '@mui/material/Toolbar';
  8. import Notifications from "./Notifications";
  9. import theme from "./theme";
  10. import Navigation from "./Navigation";
  11. import ActionBar from "./ActionBar";
  12. import notifier from "../app/Notifier";
  13. import Preferences from "./Preferences";
  14. import {useLiveQuery} from "dexie-react-hooks";
  15. import subscriptionManager from "../app/SubscriptionManager";
  16. import userManager from "../app/UserManager";
  17. import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from "react-router-dom";
  18. import {expandUrl} from "../app/utils";
  19. import ErrorBoundary from "./ErrorBoundary";
  20. import routes from "./routes";
  21. import {useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks";
  22. import PublishDialog from "./PublishDialog";
  23. import Messaging from "./Messaging";
  24. import "./i18n"; // Translations!
  25. import {Backdrop, CircularProgress} from "@mui/material";
  26. // TODO races when two tabs are open
  27. // TODO investigate service workers
  28. const App = () => {
  29. return (
  30. <Suspense fallback={<Loader />}>
  31. <BrowserRouter>
  32. <ThemeProvider theme={theme}>
  33. <CssBaseline/>
  34. <ErrorBoundary>
  35. <Routes>
  36. <Route element={<Layout/>}>
  37. <Route path={routes.root} element={<AllSubscriptions/>}/>
  38. <Route path={routes.settings} element={<Preferences/>}/>
  39. <Route path={routes.subscription} element={<SingleSubscription/>}/>
  40. <Route path={routes.subscriptionExternal} element={<SingleSubscription/>}/>
  41. </Route>
  42. </Routes>
  43. </ErrorBoundary>
  44. </ThemeProvider>
  45. </BrowserRouter>
  46. </Suspense>
  47. );
  48. }
  49. const AllSubscriptions = () => {
  50. const { subscriptions } = useOutletContext();
  51. return <Notifications mode="all" subscriptions={subscriptions}/>;
  52. };
  53. const SingleSubscription = () => {
  54. const { subscriptions, selected } = useOutletContext();
  55. useAutoSubscribe(subscriptions, selected);
  56. return <Notifications mode="one" subscription={selected}/>;
  57. };
  58. const Layout = () => {
  59. const params = useParams();
  60. const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
  61. const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted());
  62. const [sendDialogOpenMode, setSendDialogOpenMode] = useState("");
  63. const users = useLiveQuery(() => userManager.all());
  64. const subscriptions = useLiveQuery(() => subscriptionManager.all());
  65. const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0;
  66. const [selected] = (subscriptions || []).filter(s => {
  67. return (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic)
  68. || (window.location.origin === s.baseUrl && params.topic === s.topic)
  69. });
  70. useConnectionListeners(subscriptions, users);
  71. useBackgroundProcesses();
  72. useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]);
  73. return (
  74. <Box sx={{display: 'flex'}}>
  75. <CssBaseline/>
  76. <ActionBar
  77. selected={selected}
  78. onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
  79. />
  80. <Navigation
  81. subscriptions={subscriptions}
  82. selectedSubscription={selected}
  83. notificationsGranted={notificationsGranted}
  84. mobileDrawerOpen={mobileDrawerOpen}
  85. onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
  86. onNotificationGranted={setNotificationsGranted}
  87. onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)}
  88. />
  89. <Main>
  90. <Toolbar/>
  91. <Outlet context={{ subscriptions, selected }}/>
  92. </Main>
  93. <Messaging
  94. selected={selected}
  95. dialogOpenMode={sendDialogOpenMode}
  96. onDialogOpenModeChange={setSendDialogOpenMode}
  97. />
  98. </Box>
  99. );
  100. }
  101. const Main = (props) => {
  102. return (
  103. <Box
  104. id="main"
  105. component="main"
  106. sx={{
  107. display: 'flex',
  108. flexGrow: 1,
  109. flexDirection: 'column',
  110. padding: 3,
  111. width: {sm: `calc(100% - ${Navigation.width}px)`},
  112. height: '100vh',
  113. overflow: 'auto',
  114. backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
  115. }}
  116. >
  117. {props.children}
  118. </Box>
  119. );
  120. };
  121. const Loader = () => (
  122. <Backdrop
  123. open={true}
  124. sx={{
  125. zIndex: 100000,
  126. backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
  127. }}
  128. >
  129. <CircularProgress color="success" disableShrink />
  130. </Backdrop>
  131. );
  132. const updateTitle = (newNotificationsCount) => {
  133. document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy` : "ntfy";
  134. }
  135. export default App;