Navigation.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import Drawer from "@mui/material/Drawer";
  2. import * as React from "react";
  3. import {useState} from "react";
  4. import ListItemButton from "@mui/material/ListItemButton";
  5. import ListItemIcon from "@mui/material/ListItemIcon";
  6. import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
  7. import ListItemText from "@mui/material/ListItemText";
  8. import Toolbar from "@mui/material/Toolbar";
  9. import Divider from "@mui/material/Divider";
  10. import List from "@mui/material/List";
  11. import SettingsIcon from "@mui/icons-material/Settings";
  12. import AddIcon from "@mui/icons-material/Add";
  13. import SubscribeDialog from "./SubscribeDialog";
  14. import {Alert, AlertTitle, Badge, CircularProgress, ListSubheader} from "@mui/material";
  15. import Button from "@mui/material/Button";
  16. import Typography from "@mui/material/Typography";
  17. import {openUrl, topicShortUrl, topicUrl} from "../app/utils";
  18. import routes from "./routes";
  19. import {ConnectionState} from "../app/Connection";
  20. import {useLocation, useNavigate} from "react-router-dom";
  21. import subscriptionManager from "../app/SubscriptionManager";
  22. import {ChatBubble, NotificationsOffOutlined, Send} from "@mui/icons-material";
  23. import Box from "@mui/material/Box";
  24. import notifier from "../app/Notifier";
  25. import config from "../app/config";
  26. import ArticleIcon from '@mui/icons-material/Article';
  27. const navWidth = 280;
  28. const Navigation = (props) => {
  29. const navigationList = <NavList {...props}/>;
  30. return (
  31. <Box component="nav" sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}>
  32. {/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */}
  33. <Drawer
  34. variant="temporary"
  35. open={props.mobileDrawerOpen}
  36. onClose={props.onMobileDrawerToggle}
  37. ModalProps={{ keepMounted: true }} // Better open performance on mobile.
  38. sx={{
  39. display: { xs: 'block', sm: 'none' },
  40. '& .MuiDrawer-paper': { boxSizing: 'border-box', width: navWidth },
  41. }}
  42. >
  43. {navigationList}
  44. </Drawer>
  45. {/* Big screen drawer; persistent, shown if screen is big */}
  46. <Drawer
  47. open
  48. variant="permanent"
  49. sx={{
  50. display: { xs: 'none', sm: 'block' },
  51. '& .MuiDrawer-paper': { boxSizing: 'border-box', width: navWidth },
  52. }}
  53. >
  54. {navigationList}
  55. </Drawer>
  56. </Box>
  57. );
  58. };
  59. Navigation.width = navWidth;
  60. const NavList = (props) => {
  61. const navigate = useNavigate();
  62. const location = useLocation();
  63. const [subscribeDialogKey, setSubscribeDialogKey] = useState(0);
  64. const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
  65. const handleSubscribeReset = () => {
  66. setSubscribeDialogOpen(false);
  67. setSubscribeDialogKey(prev => prev+1);
  68. }
  69. const handleSubscribeSubmit = (subscription) => {
  70. console.log(`[Navigation] New subscription: ${subscription.id}`, subscription);
  71. handleSubscribeReset();
  72. navigate(routes.forSubscription(subscription));
  73. handleRequestNotificationPermission();
  74. }
  75. const handleRequestNotificationPermission = () => {
  76. notifier.maybeRequestPermission(granted => props.onNotificationGranted(granted))
  77. };
  78. const showSubscriptionsList = props.subscriptions?.length > 0;
  79. const showNotificationNotSupportedBox = !notifier.supported();
  80. const showNotificationGrantBox = notifier.supported() && props.subscriptions?.length > 0 && !props.notificationsGranted;
  81. return (
  82. <>
  83. <Toolbar sx={{ display: { xs: 'none', sm: 'block' } }}/>
  84. <List component="nav" sx={{ paddingTop: (showNotificationGrantBox || showNotificationNotSupportedBox) ? '0' : '' }}>
  85. {showNotificationNotSupportedBox && <NotificationNotSupportedAlert/>}
  86. {showNotificationGrantBox && <NotificationGrantAlert onRequestPermissionClick={handleRequestNotificationPermission}/>}
  87. {!showSubscriptionsList &&
  88. <ListItemButton onClick={() => navigate(routes.root)} selected={location.pathname === config.appRoot}>
  89. <ListItemIcon><ChatBubble/></ListItemIcon>
  90. <ListItemText primary="All notifications"/>
  91. </ListItemButton>}
  92. {showSubscriptionsList &&
  93. <>
  94. <ListSubheader>Subscribed topics</ListSubheader>
  95. <ListItemButton onClick={() => navigate(routes.root)} selected={location.pathname === config.appRoot}>
  96. <ListItemIcon><ChatBubble/></ListItemIcon>
  97. <ListItemText primary="All notifications"/>
  98. </ListItemButton>
  99. <SubscriptionList
  100. subscriptions={props.subscriptions}
  101. selectedSubscription={props.selectedSubscription}
  102. />
  103. <Divider sx={{my: 1}}/>
  104. </>}
  105. <ListItemButton onClick={() => navigate(routes.settings)} selected={location.pathname === routes.settings}>
  106. <ListItemIcon><SettingsIcon/></ListItemIcon>
  107. <ListItemText primary="Settings"/>
  108. </ListItemButton>
  109. <ListItemButton onClick={() => openUrl("/docs")}>
  110. <ListItemIcon><ArticleIcon/></ListItemIcon>
  111. <ListItemText primary="Documentation"/>
  112. </ListItemButton>
  113. <ListItemButton onClick={() => setSubscribeDialogOpen(true)}>
  114. <ListItemIcon><Send/></ListItemIcon>
  115. <ListItemText primary="Publish message"/>
  116. </ListItemButton>
  117. <ListItemButton onClick={() => setSubscribeDialogOpen(true)}>
  118. <ListItemIcon><AddIcon/></ListItemIcon>
  119. <ListItemText primary="Subscribe to topic"/>
  120. </ListItemButton>
  121. </List>
  122. <SubscribeDialog
  123. key={`subscribeDialog${subscribeDialogKey}`} // Resets dialog when canceled/closed
  124. open={subscribeDialogOpen}
  125. subscriptions={props.subscriptions}
  126. onCancel={handleSubscribeReset}
  127. onSuccess={handleSubscribeSubmit}
  128. />
  129. </>
  130. );
  131. };
  132. const SubscriptionList = (props) => {
  133. const sortedSubscriptions = props.subscriptions.sort( (a, b) => {
  134. return (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic)) ? -1 : 1;
  135. });
  136. return (
  137. <>
  138. {sortedSubscriptions.map(subscription =>
  139. <SubscriptionItem
  140. key={subscription.id}
  141. subscription={subscription}
  142. selected={props.selectedSubscription && props.selectedSubscription.id === subscription.id}
  143. />)}
  144. </>
  145. );
  146. }
  147. const SubscriptionItem = (props) => {
  148. const navigate = useNavigate();
  149. const subscription = props.subscription;
  150. const iconBadge = (subscription.new <= 99) ? subscription.new : "99+";
  151. const icon = (subscription.state === ConnectionState.Connecting)
  152. ? <CircularProgress size="24px"/>
  153. : <Badge badgeContent={iconBadge} invisible={subscription.new === 0} color="primary"><ChatBubbleOutlineIcon/></Badge>;
  154. const label = (subscription.baseUrl === window.location.origin)
  155. ? subscription.topic
  156. : topicShortUrl(subscription.baseUrl, subscription.topic);
  157. const handleClick = async () => {
  158. navigate(routes.forSubscription(subscription));
  159. await subscriptionManager.markNotificationsRead(subscription.id);
  160. };
  161. return (
  162. <ListItemButton onClick={handleClick} selected={props.selected}>
  163. <ListItemIcon>{icon}</ListItemIcon>
  164. <ListItemText primary={label}/>
  165. {subscription.mutedUntil > 0 &&
  166. <ListItemIcon edge="end"><NotificationsOffOutlined /></ListItemIcon>}
  167. </ListItemButton>
  168. );
  169. };
  170. const NotificationGrantAlert = (props) => {
  171. return (
  172. <>
  173. <Alert severity="warning" sx={{paddingTop: 2}}>
  174. <AlertTitle>Notifications are disabled</AlertTitle>
  175. <Typography gutterBottom>
  176. Grant your browser permission to display desktop notifications.
  177. </Typography>
  178. <Button
  179. sx={{float: 'right'}}
  180. color="inherit"
  181. size="small"
  182. onClick={props.onRequestPermissionClick}
  183. >
  184. Grant now
  185. </Button>
  186. </Alert>
  187. <Divider/>
  188. </>
  189. );
  190. };
  191. const NotificationNotSupportedAlert = () => {
  192. return (
  193. <>
  194. <Alert severity="warning" sx={{paddingTop: 2}}>
  195. <AlertTitle>Notifications not supported</AlertTitle>
  196. <Typography gutterBottom>
  197. Notifications are not supported in your browser.
  198. </Typography>
  199. </Alert>
  200. <Divider/>
  201. </>
  202. );
  203. };
  204. export default Navigation;