2023-05-24 12:25:20 +02:00
import {
Drawer ,
ListItemButton ,
ListItemIcon ,
ListItemText ,
Toolbar ,
Divider ,
List ,
Alert ,
AlertTitle ,
Badge ,
CircularProgress ,
Link ,
ListSubheader ,
Portal ,
Tooltip ,
Typography ,
Box ,
2023-06-14 14:31:31 +02:00
IconButton ,
Button ,
2023-05-24 12:25:20 +02:00
} from "@mui/material" ;
2022-02-25 18:46:22 +01:00
import * as React from "react" ;
2023-05-23 21:13:01 +02:00
import { useContext , useState } from "react" ;
2022-02-25 18:46:22 +01:00
import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline" ;
2022-12-16 04:07:04 +01:00
import Person from "@mui/icons-material/Person" ;
2022-02-25 18:46:22 +01:00
import SettingsIcon from "@mui/icons-material/Settings" ;
import AddIcon from "@mui/icons-material/Add" ;
2023-05-24 09:03:28 +02:00
import { useLocation , useNavigate } from "react-router-dom" ;
import { ChatBubble , MoreVert , NotificationsOffOutlined , Send } from "@mui/icons-material" ;
import ArticleIcon from "@mui/icons-material/Article" ;
import { Trans , useTranslation } from "react-i18next" ;
import CelebrationIcon from "@mui/icons-material/Celebration" ;
import SubscribeDialog from "./SubscribeDialog" ;
2023-05-23 21:13:01 +02:00
import { openUrl , topicDisplayName , topicUrl } from "../app/utils" ;
2022-03-10 05:28:55 +01:00
import routes from "./routes" ;
2023-05-23 21:13:01 +02:00
import { ConnectionState } from "../app/Connection" ;
2022-03-07 04:37:13 +01:00
import subscriptionManager from "../app/SubscriptionManager" ;
2022-03-08 17:33:17 +01:00
import notifier from "../app/Notifier" ;
2022-03-10 05:28:55 +01:00
import config from "../app/config" ;
2022-12-16 04:07:04 +01:00
import session from "../app/Session" ;
2023-05-23 21:13:01 +02:00
import accountApi , { Permission , Role } from "../app/AccountApi" ;
2023-01-09 21:40:46 +01:00
import UpgradeDialog from "./UpgradeDialog" ;
2023-05-23 21:13:01 +02:00
import { AccountContext } from "./App" ;
2023-05-24 01:29:47 +02:00
import { PermissionDenyAll , PermissionRead , PermissionReadWrite , PermissionWrite } from "./ReserveIcons" ;
2023-02-11 03:19:44 +01:00
import { SubscriptionPopup } from "./SubscriptionPopup" ;
2022-02-25 18:46:22 +01:00
2022-03-08 22:56:41 +01:00
const navWidth = 280 ;
2022-02-25 18:46:22 +01:00
const Navigation = ( props ) => {
2023-05-23 21:13:01 +02:00
const navigationList = < NavList { ...props } / > ;
return (
2023-05-24 01:29:47 +02:00
< Box component = "nav" role = "navigation" sx = { { width : { sm : Navigation . width } , flexShrink : { sm : 0 } } } >
2023-05-23 21:13:01 +02:00
{ /* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */ }
< Drawer
variant = "temporary"
role = "menubar"
open = { props . mobileDrawerOpen }
onClose = { props . onMobileDrawerToggle }
ModalProps = { { keepMounted : true } } // Better open performance on mobile.
sx = { {
display : { xs : "block" , sm : "none" } ,
"& .MuiDrawer-paper" : { boxSizing : "border-box" , width : navWidth } ,
} }
>
{ navigationList }
< / Drawer >
{ /* Big screen drawer; persistent, shown if screen is big */ }
< Drawer
open
variant = "permanent"
role = "menubar"
sx = { {
display : { xs : "none" , sm : "block" } ,
"& .MuiDrawer-paper" : { boxSizing : "border-box" , width : navWidth } ,
} }
>
{ navigationList }
< / Drawer >
< / Box >
) ;
2022-02-25 18:46:22 +01:00
} ;
Navigation . width = navWidth ;
const NavList = ( props ) => {
2023-05-23 21:13:01 +02:00
const { t } = useTranslation ( ) ;
const navigate = useNavigate ( ) ;
const location = useLocation ( ) ;
const { account } = useContext ( AccountContext ) ;
const [ subscribeDialogKey , setSubscribeDialogKey ] = useState ( 0 ) ;
const [ subscribeDialogOpen , setSubscribeDialogOpen ] = useState ( false ) ;
2022-03-06 04:33:34 +01:00
2023-05-23 21:13:01 +02:00
const handleSubscribeReset = ( ) => {
setSubscribeDialogOpen ( false ) ;
setSubscribeDialogKey ( ( prev ) => prev + 1 ) ;
} ;
2022-03-06 04:33:34 +01:00
2023-05-23 21:13:01 +02:00
const handleSubscribeSubmit = ( subscription ) => {
2023-05-24 01:29:47 +02:00
console . log ( ` [Navigation] New subscription: ${ subscription . id } ` , subscription ) ;
2023-05-23 21:13:01 +02:00
handleSubscribeReset ( ) ;
navigate ( routes . forSubscription ( subscription ) ) ;
} ;
2022-03-06 04:33:34 +01:00
2023-05-23 21:13:01 +02:00
const handleAccountClick = ( ) => {
accountApi . sync ( ) ; // Dangle!
navigate ( routes . account ) ;
} ;
2023-01-03 04:21:11 +01:00
2023-05-23 21:13:01 +02:00
const isAdmin = account ? . role === Role . ADMIN ;
const isPaid = account ? . billing ? . subscription ;
const showUpgradeBanner = config . enable _payments && ! isAdmin && ! isPaid ;
const showSubscriptionsList = props . subscriptions ? . length > 0 ;
2023-06-02 13:22:54 +02:00
const [ showNotificationPermissionRequired , setShowNotificationPermissionRequired ] = useState ( notifier . notRequested ( ) ) ;
const [ showNotificationPermissionDenied , setShowNotificationPermissionDenied ] = useState ( notifier . denied ( ) ) ;
2023-05-24 21:36:01 +02:00
const showNotificationIOSInstallRequired = notifier . iosSupportedButInstallRequired ( ) ;
const showNotificationBrowserNotSupportedBox = ! showNotificationIOSInstallRequired && ! notifier . browserSupported ( ) ;
2023-05-24 01:29:47 +02:00
const showNotificationContextNotSupportedBox = notifier . browserSupported ( ) && ! notifier . contextSupported ( ) ; // Only show if notifications are generally supported in the browser
2023-05-24 21:36:01 +02:00
2023-06-02 13:22:54 +02:00
const refreshPermissions = ( ) => {
setShowNotificationPermissionRequired ( notifier . notRequested ( ) ) ;
setShowNotificationPermissionDenied ( notifier . denied ( ) ) ;
} ;
const alertVisible =
showNotificationPermissionRequired ||
2023-05-24 21:36:01 +02:00
showNotificationPermissionDenied ||
showNotificationIOSInstallRequired ||
showNotificationBrowserNotSupportedBox ||
2023-06-02 13:22:54 +02:00
showNotificationContextNotSupportedBox ;
2022-03-06 04:33:34 +01:00
2023-05-23 21:13:01 +02:00
return (
< >
< Toolbar sx = { { display : { xs : "none" , sm : "block" } } } / >
2023-06-02 13:22:54 +02:00
< List component = "nav" sx = { { paddingTop : alertVisible ? "0" : "" } } >
{ showNotificationPermissionRequired && < NotificationPermissionRequired refreshPermissions = { refreshPermissions } / > }
2023-05-24 21:36:01 +02:00
{ showNotificationPermissionDenied && < NotificationPermissionDeniedAlert / > }
2023-05-24 01:29:47 +02:00
{ showNotificationBrowserNotSupportedBox && < NotificationBrowserNotSupportedAlert / > }
{ showNotificationContextNotSupportedBox && < NotificationContextNotSupportedAlert / > }
2023-05-24 21:36:01 +02:00
{ showNotificationIOSInstallRequired && < NotificationIOSInstallRequiredAlert / > }
2023-06-02 13:22:54 +02:00
{ alertVisible && < Divider / > }
2023-05-23 21:13:01 +02:00
{ ! showSubscriptionsList && (
2023-05-24 01:29:47 +02:00
< ListItemButton onClick = { ( ) => navigate ( routes . app ) } selected = { location . pathname === config . app _root } >
2023-05-23 21:13:01 +02:00
< ListItemIcon >
< ChatBubble / >
< / ListItemIcon >
< ListItemText primary = { t ( "nav_button_all_notifications" ) } / >
< / ListItemButton >
) }
{ showSubscriptionsList && (
< >
< ListSubheader > { t ( "nav_topics_title" ) } < / ListSubheader >
2023-05-24 01:29:47 +02:00
< ListItemButton onClick = { ( ) => navigate ( routes . app ) } selected = { location . pathname === config . app _root } >
2023-05-23 21:13:01 +02:00
< ListItemIcon >
< ChatBubble / >
< / ListItemIcon >
< ListItemText primary = { t ( "nav_button_all_notifications" ) } / >
< / ListItemButton >
2023-05-24 01:29:47 +02:00
< SubscriptionList subscriptions = { props . subscriptions } selectedSubscription = { props . selectedSubscription } / >
2023-05-23 21:13:01 +02:00
< Divider sx = { { my : 1 } } / >
< / >
) }
{ session . exists ( ) && (
2023-05-24 01:29:47 +02:00
< ListItemButton onClick = { handleAccountClick } selected = { location . pathname === routes . account } >
2023-05-23 21:13:01 +02:00
< ListItemIcon >
< Person / >
< / ListItemIcon >
< ListItemText primary = { t ( "nav_button_account" ) } / >
< / ListItemButton >
) }
2023-05-24 01:29:47 +02:00
< ListItemButton onClick = { ( ) => navigate ( routes . settings ) } selected = { location . pathname === routes . settings } >
2023-05-23 21:13:01 +02:00
< ListItemIcon >
< SettingsIcon / >
< / ListItemIcon >
< ListItemText primary = { t ( "nav_button_settings" ) } / >
< / ListItemButton >
< ListItemButton onClick = { ( ) => openUrl ( "/docs" ) } >
< ListItemIcon >
< ArticleIcon / >
< / ListItemIcon >
< ListItemText primary = { t ( "nav_button_documentation" ) } / >
< / ListItemButton >
< ListItemButton onClick = { ( ) => props . onPublishMessageClick ( ) } >
< ListItemIcon >
< Send / >
< / ListItemIcon >
< ListItemText primary = { t ( "nav_button_publish_message" ) } / >
< / ListItemButton >
< ListItemButton onClick = { ( ) => setSubscribeDialogOpen ( true ) } >
< ListItemIcon >
< AddIcon / >
< / ListItemIcon >
< ListItemText primary = { t ( "nav_button_subscribe" ) } / >
< / ListItemButton >
{ showUpgradeBanner && < UpgradeBanner / > }
< / List >
< SubscribeDialog
key = { ` subscribeDialog ${ subscribeDialogKey } ` } // Resets dialog when canceled/closed
open = { subscribeDialogOpen }
subscriptions = { props . subscriptions }
onCancel = { handleSubscribeReset }
onSuccess = { handleSubscribeSubmit }
/ >
< / >
) ;
2022-02-25 18:46:22 +01:00
} ;
2022-02-26 05:25:04 +01:00
2023-01-09 21:40:46 +01:00
const UpgradeBanner = ( ) => {
2023-05-23 21:13:01 +02:00
const { t } = useTranslation ( ) ;
const [ dialogKey , setDialogKey ] = useState ( 0 ) ;
const [ dialogOpen , setDialogOpen ] = useState ( false ) ;
2023-01-16 22:35:37 +01:00
2023-05-23 21:13:01 +02:00
const handleClick = ( ) => {
setDialogKey ( ( k ) => k + 1 ) ;
setDialogOpen ( true ) ;
} ;
2023-01-16 22:35:37 +01:00
2023-05-23 21:13:01 +02:00
return (
< Box
sx = { {
position : "fixed" ,
width : ` ${ Navigation . width - 1 } px ` ,
bottom : 0 ,
mt : "auto" ,
2023-05-24 01:29:47 +02:00
background : "linear-gradient(150deg, rgba(196, 228, 221, 0.46) 0%, rgb(255, 255, 255) 100%)" ,
2023-05-23 21:13:01 +02:00
} }
>
< Divider / >
< ListItemButton onClick = { handleClick } sx = { { pt : 2 , pb : 2 } } >
< ListItemIcon >
< CelebrationIcon sx = { { color : "#55b86e" } } fontSize = "large" / >
< / ListItemIcon >
< ListItemText
sx = { { ml : 1 } }
primary = { t ( "nav_upgrade_banner_label" ) }
secondary = { t ( "nav_upgrade_banner_description" ) }
primaryTypographyProps = { {
style : {
fontWeight : 500 ,
fontSize : "1.1rem" ,
2023-05-24 01:29:47 +02:00
background : "-webkit-linear-gradient(45deg, #09009f, #00ff95 80%)" ,
2023-05-23 21:13:01 +02:00
WebkitBackgroundClip : "text" ,
WebkitTextFillColor : "transparent" ,
} ,
} }
secondaryTypographyProps = { {
style : {
fontSize : "1rem" ,
} ,
} }
/ >
< / ListItemButton >
2023-05-24 01:29:47 +02:00
< UpgradeDialog key = { ` upgradeDialog ${ dialogKey } ` } open = { dialogOpen } onCancel = { ( ) => setDialogOpen ( false ) } / >
2023-05-23 21:13:01 +02:00
< / Box >
) ;
2023-01-09 21:40:46 +01:00
} ;
2022-02-26 16:14:43 +01:00
const SubscriptionList = ( props ) => {
2023-05-23 21:13:01 +02:00
const sortedSubscriptions = props . subscriptions
. filter ( ( s ) => ! s . internal )
2023-05-24 09:03:28 +02:00
. sort ( ( a , b ) => ( topicUrl ( a . baseUrl , a . topic ) < topicUrl ( b . baseUrl , b . topic ) ? - 1 : 1 ) ) ;
2023-05-23 21:13:01 +02:00
return (
< >
{ sortedSubscriptions . map ( ( subscription ) => (
< SubscriptionItem
key = { subscription . id }
subscription = { subscription }
2023-05-24 01:29:47 +02:00
selected = { props . selectedSubscription && props . selectedSubscription . id === subscription . id }
2023-05-23 21:13:01 +02:00
/ >
) ) }
< / >
) ;
} ;
2022-02-25 18:46:22 +01:00
2022-03-04 17:08:32 +01:00
const SubscriptionItem = ( props ) => {
2023-05-23 21:13:01 +02:00
const { t } = useTranslation ( ) ;
const navigate = useNavigate ( ) ;
const [ menuAnchorEl , setMenuAnchorEl ] = useState ( null ) ;
2023-02-01 03:39:30 +01:00
2023-05-24 09:03:28 +02:00
const { subscription } = props ;
2023-05-23 21:13:01 +02:00
const iconBadge = subscription . new <= 99 ? subscription . new : "99+" ;
const displayName = topicDisplayName ( subscription ) ;
2023-05-24 01:29:47 +02:00
const ariaLabel = subscription . state === ConnectionState . Connecting ? ` ${ displayName } ( ${ t ( "nav_button_connecting" ) } ) ` : displayName ;
2023-05-23 21:13:01 +02:00
const icon =
subscription . state === ConnectionState . Connecting ? (
< CircularProgress size = "24px" / >
) : (
2023-05-24 01:29:47 +02:00
< Badge badgeContent = { iconBadge } invisible = { subscription . new === 0 } color = "primary" >
2023-05-23 21:13:01 +02:00
< ChatBubbleOutlineIcon / >
< / Badge >
) ;
2023-02-01 03:39:30 +01:00
2023-05-23 21:13:01 +02:00
const handleClick = async ( ) => {
navigate ( routes . forSubscription ( subscription ) ) ;
await subscriptionManager . markNotificationsRead ( subscription . id ) ;
} ;
2023-02-01 03:39:30 +01:00
2023-05-23 21:13:01 +02:00
return (
< >
2023-05-24 01:29:47 +02:00
< ListItemButton onClick = { handleClick } selected = { props . selected } aria - label = { ariaLabel } aria - live = "polite" >
2023-05-23 21:13:01 +02:00
< ListItemIcon > { icon } < / ListItemIcon >
< ListItemText
primary = { displayName }
primaryTypographyProps = { {
style : { overflow : "hidden" , textOverflow : "ellipsis" } ,
} }
/ >
{ subscription . reservation ? . everyone && (
< ListItemIcon edge = "end" sx = { { minWidth : "26px" } } >
{ subscription . reservation ? . everyone === Permission . READ _WRITE && (
2023-05-24 01:29:47 +02:00
< Tooltip title = { t ( "prefs_reservations_table_everyone_read_write" ) } >
2023-05-23 21:13:01 +02:00
< PermissionReadWrite size = "small" / >
< / Tooltip >
) }
{ subscription . reservation ? . everyone === Permission . READ _ONLY && (
< Tooltip title = { t ( "prefs_reservations_table_everyone_read_only" ) } >
< PermissionRead size = "small" / >
< / Tooltip >
) }
{ subscription . reservation ? . everyone === Permission . WRITE _ONLY && (
2023-05-24 01:29:47 +02:00
< Tooltip title = { t ( "prefs_reservations_table_everyone_write_only" ) } >
2023-05-23 21:13:01 +02:00
< PermissionWrite size = "small" / >
< / Tooltip >
) }
{ subscription . reservation ? . everyone === Permission . DENY _ALL && (
< Tooltip title = { t ( "prefs_reservations_table_everyone_deny_all" ) } >
< PermissionDenyAll size = "small" / >
< / Tooltip >
) }
< / ListItemIcon >
) }
{ subscription . mutedUntil > 0 && (
2023-05-24 01:29:47 +02:00
< ListItemIcon edge = "end" sx = { { minWidth : "26px" } } aria - label = { t ( "nav_button_muted" ) } >
2023-05-23 21:13:01 +02:00
< Tooltip title = { t ( "nav_button_muted" ) } >
< NotificationsOffOutlined / >
< / Tooltip >
< / ListItemIcon >
) }
< ListItemIcon edge = "end" sx = { { minWidth : "26px" } } >
< IconButton
size = "small"
onMouseDown = { ( e ) => e . stopPropagation ( ) }
onClick = { ( e ) => {
e . stopPropagation ( ) ;
setMenuAnchorEl ( e . currentTarget ) ;
} }
>
< MoreVert fontSize = "small" / >
< / IconButton >
< / ListItemIcon >
< / ListItemButton >
< Portal >
2023-05-24 01:29:47 +02:00
< SubscriptionPopup subscription = { subscription } anchor = { menuAnchorEl } onClose = { ( ) => setMenuAnchorEl ( null ) } / >
2023-05-23 21:13:01 +02:00
< / Portal >
< / >
) ;
2022-03-04 17:08:32 +01:00
} ;
2023-06-02 13:22:54 +02:00
const NotificationPermissionRequired = ( { refreshPermissions } ) => {
const { t } = useTranslation ( ) ;
2023-06-14 04:03:00 +02:00
const requestPermission = async ( ) => {
await notifier . maybeRequestPermission ( ) ;
refreshPermissions ( ) ;
} ;
2023-06-02 13:22:54 +02:00
return (
2023-06-14 04:03:00 +02:00
< Alert severity = "warning" sx = { { paddingTop : 2 } } >
2023-06-02 13:22:54 +02:00
< AlertTitle > { t ( "alert_notification_permission_required_title" ) } < / AlertTitle >
2023-06-14 04:03:00 +02:00
< Typography gutterBottom > { t ( "alert_notification_permission_required_description" ) } < / Typography >
< Button sx = { { float : "right" } } color = "inherit" size = "small" onClick = { requestPermission } >
{ t ( "alert_notification_permission_required_button" ) }
< / Button >
2023-06-02 13:22:54 +02:00
< / Alert >
) ;
} ;
2023-05-24 21:36:01 +02:00
const NotificationPermissionDeniedAlert = ( ) => {
const { t } = useTranslation ( ) ;
return (
2023-06-02 13:22:54 +02:00
< Alert severity = "warning" sx = { { paddingTop : 2 } } >
< AlertTitle > { t ( "alert_notification_permission_denied_title" ) } < / AlertTitle >
< Typography gutterBottom > { t ( "alert_notification_permission_denied_description" ) } < / Typography >
< / Alert >
2023-05-24 21:36:01 +02:00
) ;
} ;
const NotificationIOSInstallRequiredAlert = ( ) => {
2023-05-23 21:13:01 +02:00
const { t } = useTranslation ( ) ;
return (
2023-06-14 04:03:00 +02:00
< Alert severity = "warning" sx = { { paddingTop : 2 } } >
< AlertTitle > { t ( "alert_notification_ios_install_required_title" ) } < / AlertTitle >
< Typography gutterBottom > { t ( "alert_notification_ios_install_required_description" ) } < / Typography >
< / Alert >
2023-05-23 21:13:01 +02:00
) ;
2022-03-01 22:22:47 +01:00
} ;
2022-06-12 22:38:33 +02:00
const NotificationBrowserNotSupportedAlert = ( ) => {
2023-05-23 21:13:01 +02:00
const { t } = useTranslation ( ) ;
return (
2023-06-02 13:22:54 +02:00
< Alert severity = "warning" sx = { { paddingTop : 2 } } >
< AlertTitle > { t ( "alert_not_supported_title" ) } < / AlertTitle >
< Typography gutterBottom > { t ( "alert_not_supported_description" ) } < / Typography >
< / Alert >
2023-05-23 21:13:01 +02:00
) ;
2022-03-11 00:11:12 +01:00
} ;
2022-06-12 22:38:33 +02:00
const NotificationContextNotSupportedAlert = ( ) => {
2023-05-23 21:13:01 +02:00
const { t } = useTranslation ( ) ;
return (
2023-06-02 13:22:54 +02:00
< Alert severity = "warning" sx = { { paddingTop : 2 } } >
< AlertTitle > { t ( "alert_not_supported_title" ) } < / AlertTitle >
< Typography gutterBottom >
< Trans
i18nKey = "alert_not_supported_context_description"
components = { {
mdnLink : < Link href = "https://developer.mozilla.org/en-US/docs/Web/API/notification" target = "_blank" rel = "noopener" / > ,
} }
/ >
< / Typography >
< / Alert >
2023-05-23 21:13:01 +02:00
) ;
2022-06-12 22:38:33 +02:00
} ;
2022-02-25 18:46:22 +01:00
export default Navigation ;