Move more stuff out of App.js
parent
acde2e5b6e
commit
09b128f27a
|
@ -4,11 +4,20 @@ import Toolbar from "@mui/material/Toolbar";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import SubscribeSettings from "./SubscribeSettings";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import {useEffect, useRef, useState} from "react";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import {topicShortUrl} from "../app/utils";
|
import {subscriptionRoute, topicShortUrl} from "../app/utils";
|
||||||
import {useLocation} from "react-router-dom";
|
import {useLocation, useNavigate} from "react-router-dom";
|
||||||
|
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||||
|
import Grow from '@mui/material/Grow';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Popper from '@mui/material/Popper';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import MenuList from '@mui/material/MenuList';
|
||||||
|
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||||
|
import api from "../app/Api";
|
||||||
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
|
|
||||||
const ActionBar = (props) => {
|
const ActionBar = (props) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -41,7 +50,7 @@ const ActionBar = (props) => {
|
||||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
{props.selectedSubscription && <SubscribeSettings
|
{props.selectedSubscription && <SettingsIcon
|
||||||
subscription={props.selectedSubscription}
|
subscription={props.selectedSubscription}
|
||||||
onUnsubscribe={props.onUnsubscribe}
|
onUnsubscribe={props.onUnsubscribe}
|
||||||
/>}
|
/>}
|
||||||
|
@ -50,4 +59,111 @@ const ActionBar = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Originally from https://mui.com/components/menus/#MenuListComposition.js
|
||||||
|
const SettingsIcon = (props) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const anchorRef = useRef(null);
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
setOpen((prevOpen) => !prevOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (event) => {
|
||||||
|
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAll = async (event) => {
|
||||||
|
handleClose(event);
|
||||||
|
console.log(`[ActionBar] Deleting all notifications from ${props.subscription.id}`);
|
||||||
|
await subscriptionManager.deleteNotifications(props.subscription.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnsubscribe = async (event) => {
|
||||||
|
console.log(`[ActionBar] Unsubscribing from ${props.subscription.id}`);
|
||||||
|
handleClose(event);
|
||||||
|
await subscriptionManager.remove(props.subscription.id);
|
||||||
|
const newSelected = await subscriptionManager.first(); // May be undefined
|
||||||
|
if (newSelected) {
|
||||||
|
navigate(subscriptionRoute(newSelected));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendTestMessage = () => {
|
||||||
|
const baseUrl = props.subscription.baseUrl;
|
||||||
|
const topic = props.subscription.topic;
|
||||||
|
api.publish(baseUrl, topic,
|
||||||
|
`This is a test notification sent by the ntfy Web UI at ${new Date().toString()}.`); // FIXME result ignored
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleListKeyDown = (event) => {
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
event.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return focus to the button when we transitioned from !open -> open
|
||||||
|
const prevOpen = useRef(open);
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevOpen.current === true && open === false) {
|
||||||
|
anchorRef.current.focus();
|
||||||
|
}
|
||||||
|
prevOpen.current = open;
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
size="large"
|
||||||
|
edge="end"
|
||||||
|
ref={anchorRef}
|
||||||
|
id="composition-button"
|
||||||
|
onClick={handleToggle}
|
||||||
|
>
|
||||||
|
<MoreVertIcon/>
|
||||||
|
</IconButton>
|
||||||
|
<Popper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorRef.current}
|
||||||
|
role={undefined}
|
||||||
|
placement="bottom-start"
|
||||||
|
transition
|
||||||
|
disablePortal
|
||||||
|
>
|
||||||
|
{({TransitionProps, placement}) => (
|
||||||
|
<Grow
|
||||||
|
{...TransitionProps}
|
||||||
|
style={{
|
||||||
|
transformOrigin:
|
||||||
|
placement === 'bottom-start' ? 'left top' : 'left bottom',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<MenuList
|
||||||
|
autoFocusItem={open}
|
||||||
|
id="composition-menu"
|
||||||
|
onKeyDown={handleListKeyDown}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleSendTestMessage}>Send test notification</MenuItem>
|
||||||
|
<MenuItem onClick={handleClearAll}>Clear all notifications</MenuItem>
|
||||||
|
<MenuItem onClick={handleUnsubscribe}>Unsubscribe</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Paper>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ActionBar;
|
export default ActionBar;
|
||||||
|
|
|
@ -47,31 +47,21 @@ const Root = () => {
|
||||||
const subscriptions = useLiveQuery(() => subscriptionManager.all());
|
const subscriptions = useLiveQuery(() => subscriptionManager.all());
|
||||||
const selectedSubscription = findSelected(location, subscriptions);
|
const selectedSubscription = findSelected(location, subscriptions);
|
||||||
|
|
||||||
const handleSubscriptionClick = async (subscriptionId) => {
|
|
||||||
const subscription = await subscriptionManager.get(subscriptionId);
|
|
||||||
navigate(subscriptionRoute(subscription));
|
|
||||||
}
|
|
||||||
const handleSubscribeSubmit = async (subscription) => {
|
const handleSubscribeSubmit = async (subscription) => {
|
||||||
console.log(`[App] New subscription: ${subscription.id}`, subscription);
|
console.log(`[App] New subscription: ${subscription.id}`, subscription);
|
||||||
navigate(subscriptionRoute(subscription));
|
navigate(subscriptionRoute(subscription));
|
||||||
handleRequestPermission();
|
handleRequestPermission();
|
||||||
};
|
};
|
||||||
const handleUnsubscribe = async (subscriptionId) => {
|
|
||||||
console.log(`[App] Unsubscribing from ${subscriptionId}`);
|
|
||||||
const newSelected = await subscriptionManager.first(); // May be undefined
|
|
||||||
if (newSelected) {
|
|
||||||
navigate(subscriptionRoute(newSelected));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleRequestPermission = () => {
|
const handleRequestPermission = () => {
|
||||||
notificationManager.maybeRequestPermission(granted => setNotificationsGranted(granted));
|
notificationManager.maybeRequestPermission(granted => setNotificationsGranted(granted));
|
||||||
};
|
};
|
||||||
// Define hooks: Note that the order of the hooks is important. The "loading" hooks
|
|
||||||
// must be before the "saving" hooks.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
poller.startWorker();
|
poller.startWorker();
|
||||||
pruner.startWorker();
|
pruner.startWorker();
|
||||||
}, [/* initial render */]);
|
}, [/* initial render */]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleNotification = async (subscriptionId, notification) => {
|
const handleNotification = async (subscriptionId, notification) => {
|
||||||
try {
|
try {
|
||||||
|
@ -93,14 +83,17 @@ const Root = () => {
|
||||||
// This is for the use of 'navigate' // FIXME
|
// This is for the use of 'navigate' // FIXME
|
||||||
//eslint-disable-next-line
|
//eslint-disable-next-line
|
||||||
}, [/* initial render */]);
|
}, [/* initial render */]);
|
||||||
useEffect(() => { connectionManager.refresh(subscriptions, users) }, [subscriptions, users]); // Dangle!
|
|
||||||
|
useEffect(() => {
|
||||||
|
connectionManager.refresh(subscriptions, users);
|
||||||
|
}, [subscriptions, users]); // Dangle!
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{display: 'flex'}}>
|
<Box sx={{display: 'flex'}}>
|
||||||
<CssBaseline/>
|
<CssBaseline/>
|
||||||
<ActionBar
|
<ActionBar
|
||||||
subscriptions={subscriptions}
|
subscriptions={subscriptions}
|
||||||
selectedSubscription={selectedSubscription}
|
selectedSubscription={selectedSubscription}
|
||||||
onUnsubscribe={handleUnsubscribe}
|
|
||||||
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
||||||
/>
|
/>
|
||||||
<Box component="nav" sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}>
|
<Box component="nav" sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}>
|
||||||
|
@ -110,7 +103,6 @@ const Root = () => {
|
||||||
mobileDrawerOpen={mobileDrawerOpen}
|
mobileDrawerOpen={mobileDrawerOpen}
|
||||||
notificationsGranted={notificationsGranted}
|
notificationsGranted={notificationsGranted}
|
||||||
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
||||||
onSubscriptionClick={handleSubscriptionClick}
|
|
||||||
onSubscribeSubmit={handleSubscribeSubmit}
|
onSubscribeSubmit={handleSubscribeSubmit}
|
||||||
onRequestPermissionClick={handleRequestPermission}
|
onRequestPermissionClick={handleRequestPermission}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -58,16 +58,20 @@ const NavList = (props) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [subscribeDialogKey, setSubscribeDialogKey] = useState(0);
|
const [subscribeDialogKey, setSubscribeDialogKey] = useState(0);
|
||||||
const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
|
const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
|
||||||
|
|
||||||
const handleSubscribeReset = () => {
|
const handleSubscribeReset = () => {
|
||||||
setSubscribeDialogOpen(false);
|
setSubscribeDialogOpen(false);
|
||||||
setSubscribeDialogKey(prev => prev+1);
|
setSubscribeDialogKey(prev => prev+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubscribeSubmit = (subscription) => {
|
const handleSubscribeSubmit = (subscription) => {
|
||||||
handleSubscribeReset();
|
handleSubscribeReset();
|
||||||
props.onSubscribeSubmit(subscription);
|
props.onSubscribeSubmit(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showSubscriptionsList = props.subscriptions?.length > 0;
|
const showSubscriptionsList = props.subscriptions?.length > 0;
|
||||||
const showGrantPermissionsBox = props.subscriptions?.length > 0 && !props.notificationsGranted;
|
const showGrantPermissionsBox = props.subscriptions?.length > 0 && !props.notificationsGranted;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar sx={{ display: { xs: 'none', sm: 'block' } }}/>
|
<Toolbar sx={{ display: { xs: 'none', sm: 'block' } }}/>
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import {useEffect, useRef, useState} from 'react';
|
|
||||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
|
||||||
import Grow from '@mui/material/Grow';
|
|
||||||
import Paper from '@mui/material/Paper';
|
|
||||||
import Popper from '@mui/material/Popper';
|
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
|
||||||
import MenuList from '@mui/material/MenuList';
|
|
||||||
import IconButton from "@mui/material/IconButton";
|
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
|
||||||
import api from "../app/Api";
|
|
||||||
import subscriptionManager from "../app/SubscriptionManager";
|
|
||||||
|
|
||||||
// Originally from https://mui.com/components/menus/#MenuListComposition.js
|
|
||||||
const SubscribeSettings = (props) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const anchorRef = useRef(null);
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
setOpen((prevOpen) => !prevOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = (event) => {
|
|
||||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearAll = async (event) => {
|
|
||||||
handleClose(event);
|
|
||||||
console.log(`[IconSubscribeSettings] Deleting all notifications from ${props.subscription.id}`);
|
|
||||||
await subscriptionManager.deleteNotifications(props.subscription.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnsubscribe = async (event) => {
|
|
||||||
handleClose(event);
|
|
||||||
await subscriptionManager.remove(props.subscription.id);
|
|
||||||
props.onUnsubscribe(props.subscription.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendTestMessage = () => {
|
|
||||||
const baseUrl = props.subscription.baseUrl;
|
|
||||||
const topic = props.subscription.topic;
|
|
||||||
api.publish(baseUrl, topic,
|
|
||||||
`This is a test notification sent by the ntfy Web UI at ${new Date().toString()}.`); // FIXME result ignored
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleListKeyDown = (event) => {
|
|
||||||
if (event.key === 'Tab') {
|
|
||||||
event.preventDefault();
|
|
||||||
setOpen(false);
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return focus to the button when we transitioned from !open -> open
|
|
||||||
const prevOpen = useRef(open);
|
|
||||||
useEffect(() => {
|
|
||||||
if (prevOpen.current === true && open === false) {
|
|
||||||
anchorRef.current.focus();
|
|
||||||
}
|
|
||||||
prevOpen.current = open;
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
color="inherit"
|
|
||||||
size="large"
|
|
||||||
edge="end"
|
|
||||||
ref={anchorRef}
|
|
||||||
id="composition-button"
|
|
||||||
onClick={handleToggle}
|
|
||||||
>
|
|
||||||
<MoreVertIcon/>
|
|
||||||
</IconButton>
|
|
||||||
<Popper
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorRef.current}
|
|
||||||
role={undefined}
|
|
||||||
placement="bottom-start"
|
|
||||||
transition
|
|
||||||
disablePortal
|
|
||||||
>
|
|
||||||
{({TransitionProps, placement}) => (
|
|
||||||
<Grow
|
|
||||||
{...TransitionProps}
|
|
||||||
style={{
|
|
||||||
transformOrigin:
|
|
||||||
placement === 'bottom-start' ? 'left top' : 'left bottom',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paper>
|
|
||||||
<ClickAwayListener onClickAway={handleClose}>
|
|
||||||
<MenuList
|
|
||||||
autoFocusItem={open}
|
|
||||||
id="composition-menu"
|
|
||||||
onKeyDown={handleListKeyDown}
|
|
||||||
>
|
|
||||||
<MenuItem onClick={handleSendTestMessage}>Send test notification</MenuItem>
|
|
||||||
<MenuItem onClick={handleClearAll}>Clear all notifications</MenuItem>
|
|
||||||
<MenuItem onClick={handleUnsubscribe}>Unsubscribe</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</ClickAwayListener>
|
|
||||||
</Paper>
|
|
||||||
</Grow>
|
|
||||||
)}
|
|
||||||
</Popper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SubscribeSettings;
|
|
Loading…
Reference in New Issue