App.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import * as React from 'react';
  2. import {useEffect, useState} from 'react';
  3. import Typography from '@mui/material/Typography';
  4. import Box from '@mui/material/Box';
  5. import {styled, ThemeProvider} from '@mui/material/styles';
  6. import CssBaseline from '@mui/material/CssBaseline';
  7. import MuiDrawer from '@mui/material/Drawer';
  8. import MuiAppBar from '@mui/material/AppBar';
  9. import Toolbar from '@mui/material/Toolbar';
  10. import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
  11. import List from '@mui/material/List';
  12. import Divider from '@mui/material/Divider';
  13. import IconButton from '@mui/material/IconButton';
  14. import MenuIcon from '@mui/icons-material/Menu';
  15. import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
  16. import ListItemIcon from "@mui/material/ListItemIcon";
  17. import ListItemText from "@mui/material/ListItemText";
  18. import ListItemButton from "@mui/material/ListItemButton";
  19. import SettingsIcon from "@mui/icons-material/Settings";
  20. import AddIcon from "@mui/icons-material/Add";
  21. import AddDialog from "./AddDialog";
  22. import NotificationList from "./NotificationList";
  23. import DetailSettingsIcon from "./DetailSettingsIcon";
  24. import theme from "./theme";
  25. import api from "../app/Api";
  26. import repository from "../app/Repository";
  27. import connectionManager from "../app/ConnectionManager";
  28. import Subscriptions from "../app/Subscriptions";
  29. const drawerWidth = 240;
  30. const AppBar = styled(MuiAppBar, {
  31. shouldForwardProp: (prop) => prop !== 'open',
  32. })(({ theme, open }) => ({
  33. zIndex: theme.zIndex.drawer + 1,
  34. transition: theme.transitions.create(['width', 'margin'], {
  35. easing: theme.transitions.easing.sharp,
  36. duration: theme.transitions.duration.leavingScreen,
  37. }),
  38. ...(open && {
  39. marginLeft: drawerWidth,
  40. width: `calc(100% - ${drawerWidth}px)`,
  41. transition: theme.transitions.create(['width', 'margin'], {
  42. easing: theme.transitions.easing.sharp,
  43. duration: theme.transitions.duration.enteringScreen,
  44. }),
  45. }),
  46. }));
  47. const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(
  48. ({ theme, open }) => ({
  49. '& .MuiDrawer-paper': {
  50. position: 'relative',
  51. whiteSpace: 'nowrap',
  52. width: drawerWidth,
  53. transition: theme.transitions.create('width', {
  54. easing: theme.transitions.easing.sharp,
  55. duration: theme.transitions.duration.enteringScreen,
  56. }),
  57. boxSizing: 'border-box',
  58. ...(!open && {
  59. overflowX: 'hidden',
  60. transition: theme.transitions.create('width', {
  61. easing: theme.transitions.easing.sharp,
  62. duration: theme.transitions.duration.leavingScreen,
  63. }),
  64. width: theme.spacing(7),
  65. [theme.breakpoints.up('sm')]: {
  66. width: theme.spacing(9),
  67. },
  68. }),
  69. },
  70. }),
  71. );
  72. const SubscriptionNav = (props) => {
  73. const subscriptions = props.subscriptions;
  74. return (
  75. <>
  76. {subscriptions.map((id, subscription) =>
  77. <SubscriptionNavItem
  78. key={id}
  79. subscription={subscription}
  80. selected={props.selectedSubscription && props.selectedSubscription.id === id}
  81. onClick={() => props.handleSubscriptionClick(id)}
  82. />)
  83. }
  84. </>
  85. );
  86. }
  87. const SubscriptionNavItem = (props) => {
  88. const subscription = props.subscription;
  89. return (
  90. <ListItemButton onClick={props.onClick} selected={props.selected}>
  91. <ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon>
  92. <ListItemText primary={subscription.shortUrl()}/>
  93. </ListItemButton>
  94. );
  95. }
  96. const App = () => {
  97. console.log("Launching App component");
  98. const [drawerOpen, setDrawerOpen] = useState(true);
  99. const [subscriptions, setSubscriptions] = useState(new Subscriptions());
  100. const [selectedSubscription, setSelectedSubscription] = useState(null);
  101. const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
  102. const handleNotification = (subscriptionId, notification) => {
  103. setSubscriptions(prev => {
  104. const newSubscription = prev.get(subscriptionId).addNotification(notification);
  105. return prev.update(newSubscription).clone();
  106. });
  107. };
  108. const handleSubscribeSubmit = (subscription) => {
  109. setSubscribeDialogOpen(false);
  110. setSubscriptions(prev => prev.add(subscription).clone());
  111. setSelectedSubscription(subscription);
  112. api.poll(subscription.baseUrl, subscription.topic)
  113. .then(messages => {
  114. setSubscriptions(prev => {
  115. const newSubscription = prev.get(subscription.id).addNotifications(messages);
  116. return prev.update(newSubscription).clone();
  117. });
  118. });
  119. };
  120. const handleSubscribeCancel = () => {
  121. console.log(`Cancel clicked`);
  122. setSubscribeDialogOpen(false);
  123. };
  124. const handleUnsubscribe = (subscriptionId) => {
  125. setSubscriptions(prev => {
  126. const newSubscriptions = prev.remove(subscriptionId).clone();
  127. setSelectedSubscription(newSubscriptions.firstOrNull());
  128. return newSubscriptions;
  129. });
  130. };
  131. const handleSubscriptionClick = (subscriptionId) => {
  132. console.log(`Selected subscription ${subscriptionId}`);
  133. setSelectedSubscription(subscriptions.get(subscriptionId));
  134. };
  135. const notifications = (selectedSubscription !== null) ? selectedSubscription.notifications : [];
  136. const toggleDrawer = () => {
  137. setDrawerOpen(!drawerOpen);
  138. };
  139. useEffect(() => {
  140. connectionManager.refresh(subscriptions, handleNotification);
  141. repository.saveSubscriptions(subscriptions);
  142. }, [subscriptions]);
  143. return (
  144. <ThemeProvider theme={theme}>
  145. <CssBaseline />
  146. <Box sx={{ display: 'flex' }}>
  147. <AppBar position="absolute" open={drawerOpen}>
  148. <Toolbar sx={{pr: '24px'}} color="primary">
  149. <IconButton
  150. edge="start"
  151. color="inherit"
  152. aria-label="open drawer"
  153. onClick={toggleDrawer}
  154. sx={{
  155. marginRight: '36px',
  156. ...(drawerOpen && { display: 'none' }),
  157. }}
  158. >
  159. <MenuIcon />
  160. </IconButton>
  161. <Typography
  162. component="h1"
  163. variant="h6"
  164. color="inherit"
  165. noWrap
  166. sx={{ flexGrow: 1 }}
  167. >
  168. {(selectedSubscription !== null) ? selectedSubscription.shortUrl() : "ntfy"}
  169. </Typography>
  170. {selectedSubscription !== null && <DetailSettingsIcon
  171. subscription={selectedSubscription}
  172. onUnsubscribe={handleUnsubscribe}
  173. />}
  174. </Toolbar>
  175. </AppBar>
  176. <Drawer variant="permanent" open={drawerOpen}>
  177. <Toolbar
  178. sx={{
  179. display: 'flex',
  180. alignItems: 'center',
  181. justifyContent: 'flex-end',
  182. px: [1],
  183. }}
  184. >
  185. <IconButton onClick={toggleDrawer}>
  186. <ChevronLeftIcon />
  187. </IconButton>
  188. </Toolbar>
  189. <Divider />
  190. <List component="nav">
  191. <SubscriptionNav
  192. subscriptions={subscriptions}
  193. selectedSubscription={selectedSubscription}
  194. handleSubscriptionClick={handleSubscriptionClick}
  195. />
  196. <Divider sx={{ my: 1 }} />
  197. <ListItemButton>
  198. <ListItemIcon>
  199. <SettingsIcon />
  200. </ListItemIcon>
  201. <ListItemText primary="Settings" />
  202. </ListItemButton>
  203. <ListItemButton onClick={() => setSubscribeDialogOpen(true)}>
  204. <ListItemIcon>
  205. <AddIcon />
  206. </ListItemIcon>
  207. <ListItemText primary="Add subscription" />
  208. </ListItemButton>
  209. </List>
  210. </Drawer>
  211. <Box
  212. component="main"
  213. sx={{
  214. backgroundColor: (theme) =>
  215. theme.palette.mode === 'light'
  216. ? theme.palette.grey[100]
  217. : theme.palette.grey[900],
  218. flexGrow: 1,
  219. height: '100vh',
  220. overflow: 'auto',
  221. }}
  222. >
  223. <Toolbar />
  224. <NotificationList notifications={notifications}/>
  225. </Box>
  226. </Box>
  227. <AddDialog
  228. open={subscribeDialogOpen}
  229. onCancel={handleSubscribeCancel}
  230. onSubmit={handleSubscribeSubmit}
  231. />
  232. </ThemeProvider>
  233. );
  234. }
  235. export default App;