App.jsx 5.2 KB

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