Settings icon
This commit is contained in:
parent
dd1a85e733
commit
4ba23390b5
3 changed files with 143 additions and 54 deletions
|
@ -1,6 +1,5 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import Container from '@mui/material/Container';
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import WsConnection from '../app/WsConnection';
|
import WsConnection from '../app/WsConnection';
|
||||||
|
@ -13,19 +12,16 @@ import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
|
||||||
import List from '@mui/material/List';
|
import List from '@mui/material/List';
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Badge from '@mui/material/Badge';
|
|
||||||
import Grid from '@mui/material/Grid';
|
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||||
import NotificationsIcon from '@mui/icons-material/Notifications';
|
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
import ListItemText from "@mui/material/ListItemText";
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
import ListItemButton from "@mui/material/ListItemButton";
|
import ListItemButton from "@mui/material/ListItemButton";
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import Card from "@mui/material/Card";
|
|
||||||
import {CardContent, Stack} from "@mui/material";
|
|
||||||
import AddDialog from "./AddDialog";
|
import AddDialog from "./AddDialog";
|
||||||
|
import NotificationList from "./NotificationList";
|
||||||
|
import DetailSettingsIcon from "./DetailSettingsIcon";
|
||||||
import theme from "./theme";
|
import theme from "./theme";
|
||||||
import LocalStorage from "../app/Storage";
|
import LocalStorage from "../app/Storage";
|
||||||
|
|
||||||
|
@ -102,34 +98,6 @@ const SubscriptionNavItem = (props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const NotificationList = (props) => {
|
|
||||||
return (
|
|
||||||
<Stack spacing={3} className="notificationList">
|
|
||||||
{props.notifications.map(notification =>
|
|
||||||
<NotificationItem key={notification.id} notification={notification}/>)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const NotificationItem = (props) => {
|
|
||||||
const notification = props.notification;
|
|
||||||
return (
|
|
||||||
<Card sx={{ minWidth: 275 }}>
|
|
||||||
<CardContent>
|
|
||||||
<Typography sx={{ fontSize: 14 }} color="text.secondary" gutterBottom>
|
|
||||||
{notification.time}
|
|
||||||
</Typography>
|
|
||||||
{notification.title && <Typography variant="h5" component="div">
|
|
||||||
{notification.title}
|
|
||||||
</Typography>}
|
|
||||||
<Typography variant="body1">
|
|
||||||
{notification.message}
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
console.log("Launching App component");
|
console.log("Launching App component");
|
||||||
|
|
||||||
|
@ -149,11 +117,11 @@ const App = () => {
|
||||||
connection.start();
|
connection.start();
|
||||||
};
|
};
|
||||||
const handleAddCancel = () => {
|
const handleAddCancel = () => {
|
||||||
console.log(`Cancel clicked`)
|
console.log(`Cancel clicked`);
|
||||||
setAddDialogOpen(false);
|
setAddDialogOpen(false);
|
||||||
}
|
}
|
||||||
const handleSubscriptionClick = (subscriptionId) => {
|
const handleSubscriptionClick = (subscriptionId) => {
|
||||||
console.log(`Selected subscription ${subscriptionId}`)
|
console.log(`Selected subscription ${subscriptionId}`);
|
||||||
setSelectedSubscription(subscriptions[subscriptionId]);
|
setSelectedSubscription(subscriptions[subscriptionId]);
|
||||||
};
|
};
|
||||||
const notifications = (selectedSubscription !== null) ? selectedSubscription.notifications : [];
|
const notifications = (selectedSubscription !== null) ? selectedSubscription.notifications : [];
|
||||||
|
@ -183,15 +151,10 @@ const App = () => {
|
||||||
}, [subscriptions]);
|
}, [subscriptions]);
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<CssBaseline />
|
|
||||||
<AppBar position="absolute" open={drawerOpen}>
|
<AppBar position="absolute" open={drawerOpen}>
|
||||||
<Toolbar
|
<Toolbar sx={{pr: '24px'}} color="primary">
|
||||||
sx={{
|
|
||||||
pr: '24px', // keep right padding when drawer closed
|
|
||||||
}}
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
@ -211,13 +174,9 @@ const App = () => {
|
||||||
noWrap
|
noWrap
|
||||||
sx={{ flexGrow: 1 }}
|
sx={{ flexGrow: 1 }}
|
||||||
>
|
>
|
||||||
ntfy
|
{(selectedSubscription != null) ? selectedSubscription.shortUrl() : "ntfy.sh"}
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton color="inherit">
|
<DetailSettingsIcon/>
|
||||||
<Badge badgeContent={4} color="secondary">
|
|
||||||
<NotificationsIcon />
|
|
||||||
</Badge>
|
|
||||||
</IconButton>
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<Drawer variant="permanent" open={drawerOpen}>
|
<Drawer variant="permanent" open={drawerOpen}>
|
||||||
|
@ -268,11 +227,7 @@ const App = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
<NotificationList notifications={notifications}/>
|
||||||
<Grid container spacing={3}>
|
|
||||||
<NotificationList notifications={notifications}/>
|
|
||||||
</Grid>
|
|
||||||
</Container>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<AddDialog
|
<AddDialog
|
||||||
|
|
98
web/src/components/DetailSettingsIcon.js
Normal file
98
web/src/components/DetailSettingsIcon.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
// Originally from https://mui.com/components/menus/#MenuListComposition.js
|
||||||
|
const DetailSettingsIcon = () => {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
function 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"
|
||||||
|
aria-controls={open ? 'composition-menu' : undefined}
|
||||||
|
aria-expanded={open ? 'true' : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
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"
|
||||||
|
aria-labelledby="composition-button"
|
||||||
|
onKeyDown={handleListKeyDown}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleClose}>Send test notification</MenuItem>
|
||||||
|
<MenuItem onClick={handleClose}>Unsubscribe</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Paper>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailSettingsIcon;
|
36
web/src/components/NotificationList.js
Normal file
36
web/src/components/NotificationList.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import Container from "@mui/material/Container";
|
||||||
|
import {CardContent, Stack} from "@mui/material";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const NotificationList = (props) => {
|
||||||
|
const sortedNotifications = props.notifications.sort((a, b) => a.time < b.time);
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ marginTop: 3 }}>
|
||||||
|
<Stack container spacing={3}>
|
||||||
|
{sortedNotifications.map(notification =>
|
||||||
|
<NotificationItem key={notification.id} notification={notification}/>)}
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotificationItem = (props) => {
|
||||||
|
const notification = props.notification;
|
||||||
|
const date = new Intl.DateTimeFormat('default', {dateStyle: 'short', timeStyle: 'short'})
|
||||||
|
.format(new Date(notification.time * 1000));
|
||||||
|
const tags = (notification.tags && notification.tags.length > 0) ? notification.tags.join(', ') : null;
|
||||||
|
return (
|
||||||
|
<Card sx={{ minWidth: 275 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography sx={{ fontSize: 14 }} color="text.secondary">{date}</Typography>
|
||||||
|
{notification.title && <Typography variant="h5" component="div">{notification.title}</Typography>}
|
||||||
|
<Typography variant="body1" gutterBottom>{notification.message}</Typography>
|
||||||
|
{tags && <Typography sx={{ fontSize: 14 }} color="text.secondary">Tags: {tags}</Typography>}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotificationList;
|
Loading…
Reference in a new issue