ntfy/web/src/components/ActionBar.js

329 lines
13 KiB
JavaScript
Raw Normal View History

2022-02-25 18:46:22 +01:00
import AppBar from "@mui/material/AppBar";
import Navigation from "./Navigation";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
import Typography from "@mui/material/Typography";
import * as React from "react";
2022-03-06 04:33:34 +01:00
import {useEffect, useRef, useState} from "react";
2022-02-28 22:56:38 +01:00
import Box from "@mui/material/Box";
2022-12-15 05:43:43 +01:00
import {formatShortDateTime, shuffle, topicDisplayName} from "../app/utils";
2022-12-28 21:51:09 +01:00
import db from "../app/db";
2022-03-06 04:33:34 +01:00
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";
2022-03-08 22:56:41 +01:00
import NotificationsIcon from '@mui/icons-material/Notifications';
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
2022-12-25 17:59:44 +01:00
import api from "../app/Api";
import routes from "./routes";
2022-03-06 04:33:34 +01:00
import subscriptionManager from "../app/SubscriptionManager";
2022-03-10 21:37:50 +01:00
import logo from "../img/ntfy.svg";
2022-04-08 16:44:35 +02:00
import {useTranslation} from "react-i18next";
2022-12-15 05:43:43 +01:00
import {Menu, Portal, Snackbar} from "@mui/material";
2022-06-29 21:57:56 +02:00
import SubscriptionSettingsDialog from "./SubscriptionSettingsDialog";
2022-12-02 21:37:48 +01:00
import session from "../app/Session";
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import Button from "@mui/material/Button";
2022-12-15 05:43:43 +01:00
import Divider from "@mui/material/Divider";
import {Logout, Person, Settings} from "@mui/icons-material";
import ListItemIcon from "@mui/material/ListItemIcon";
2022-12-25 17:59:44 +01:00
import accountApi, {UnauthorizedError} from "../app/AccountApi";
2022-02-25 18:46:22 +01:00
const ActionBar = (props) => {
2022-04-08 16:44:35 +02:00
const { t } = useTranslation();
2022-03-04 22:10:04 +01:00
const location = useLocation();
let title = "ntfy";
if (props.selected) {
2022-06-29 21:57:56 +02:00
title = topicDisplayName(props.selected);
2022-03-04 22:10:04 +01:00
} else if (location.pathname === "/settings") {
2022-04-08 16:44:35 +02:00
title = t("action_bar_settings");
2022-03-04 22:10:04 +01:00
}
2022-02-25 18:46:22 +01:00
return (
2022-02-26 20:22:21 +01:00
<AppBar position="fixed" sx={{
width: '100%',
2022-02-26 20:36:23 +01:00
zIndex: { sm: 1250 }, // > Navigation (1200), but < Dialog (1300)
2022-02-26 20:22:21 +01:00
ml: { sm: `${Navigation.width}px` }
}}>
2022-02-25 18:46:22 +01:00
<Toolbar sx={{pr: '24px'}}>
<IconButton
color="inherit"
edge="start"
2022-05-03 01:30:29 +02:00
aria-label={t("action_bar_show_menu")}
2022-02-25 18:46:22 +01:00
onClick={props.onMobileDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
2022-05-03 01:30:29 +02:00
<Box
component="img"
src={logo}
alt={t("action_bar_logo_alt")}
sx={{
display: { xs: 'none', sm: 'block' },
marginRight: '10px',
height: '28px'
}}
/>
2022-02-26 20:22:21 +01:00
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
{title}
</Typography>
2022-03-08 22:56:41 +01:00
{props.selected &&
<SettingsIcons
subscription={props.selected}
onUnsubscribe={props.onUnsubscribe}
/>}
2022-12-02 21:37:48 +01:00
<ProfileIcon/>
2022-02-25 18:46:22 +01:00
</Toolbar>
</AppBar>
);
};
2022-03-08 22:56:41 +01:00
const SettingsIcons = (props) => {
2022-04-08 16:44:35 +02:00
const { t } = useTranslation();
2022-03-06 04:33:34 +01:00
const navigate = useNavigate();
2022-12-29 08:32:05 +01:00
const [anchorEl, setAnchorEl] = useState(null);
const [snackOpen, setSnackOpen] = useState(false);
2022-06-29 21:57:56 +02:00
const [subscriptionSettingsOpen, setSubscriptionSettingsOpen] = useState(false);
2022-03-08 22:56:41 +01:00
const subscription = props.subscription;
2022-12-29 08:32:05 +01:00
const open = Boolean(anchorEl);
2022-03-06 04:33:34 +01:00
2022-12-29 08:32:05 +01:00
const handleToggleOpen = (event) => {
setAnchorEl(event.currentTarget);
2022-03-06 04:33:34 +01:00
};
2022-03-08 22:56:41 +01:00
const handleToggleMute = async () => {
const mutedUntil = (subscription.mutedUntil) ? 0 : 1; // Make this a timestamp in the future
await subscriptionManager.setMutedUntil(subscription.id, mutedUntil);
}
2022-12-29 08:32:05 +01:00
const handleClose = () => {
setAnchorEl(null);
2022-03-06 04:33:34 +01:00
};
const handleClearAll = async (event) => {
console.log(`[ActionBar] Deleting all notifications from ${props.subscription.id}`);
await subscriptionManager.deleteNotifications(props.subscription.id);
};
const handleUnsubscribe = async (event) => {
2022-12-09 02:50:48 +01:00
console.log(`[ActionBar] Unsubscribing from ${props.subscription.id}`, props.subscription);
2022-03-06 04:33:34 +01:00
await subscriptionManager.remove(props.subscription.id);
2022-12-09 02:50:48 +01:00
if (session.exists() && props.subscription.remoteId) {
try {
2022-12-25 17:59:44 +01:00
await accountApi.deleteSubscription(props.subscription.remoteId);
} catch (e) {
console.log(`[ActionBar] Error unsubscribing`, e);
if ((e instanceof UnauthorizedError)) {
session.resetAndRedirect(routes.login);
}
}
2022-12-09 02:50:48 +01:00
}
2022-03-06 04:33:34 +01:00
const newSelected = await subscriptionManager.first(); // May be undefined
if (newSelected) {
navigate(routes.forSubscription(newSelected));
2022-03-06 22:35:31 +01:00
} else {
2022-12-02 21:37:48 +01:00
navigate(routes.app);
2022-03-06 04:33:34 +01:00
}
};
2022-06-29 21:57:56 +02:00
const handleSubscriptionSettings = async () => {
setSubscriptionSettingsOpen(true);
}
const handleSendTestMessage = async () => {
2022-03-06 04:33:34 +01:00
const baseUrl = props.subscription.baseUrl;
const topic = props.subscription.topic;
2022-03-11 04:58:24 +01:00
const tags = shuffle([
"grinning", "octopus", "upside_down_face", "palm_tree", "maple_leaf", "apple", "skull", "warning", "jack_o_lantern",
"de-server-1", "backups", "cron-script", "script-error", "phils-automation", "mouse", "go-rocks", "hi-ben"])
.slice(0, Math.round(Math.random() * 4));
const priority = shuffle([1, 2, 3, 4, 5])[0];
const title = shuffle([
"",
"",
"", // Higher chance of no title
"Oh my, another test message?",
"Titles are optional, did you know that?",
"ntfy is open source, and will always be free. Cool, right?",
"I don't really like apples",
2022-03-24 18:17:04 +01:00
"My favorite TV show is The Wire. You should watch it!",
"You can attach files and URLs to messages too",
"You can delay messages up to 3 days"
2022-03-11 04:58:24 +01:00
])[0];
2022-03-24 18:17:04 +01:00
const nowSeconds = Math.round(Date.now()/1000);
2022-03-11 04:58:24 +01:00
const message = shuffle([
2022-03-24 18:17:04 +01:00
`Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(nowSeconds)} right now. Is that early or late?`,
2022-03-11 04:58:24 +01:00
`So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`,
`It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`,
2022-03-24 18:17:04 +01:00
`Alright then, it's ${formatShortDateTime(nowSeconds)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`,
2022-03-11 04:58:24 +01:00
`There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`,
2022-03-15 16:09:20 +01:00
`I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`,
2022-03-11 04:58:24 +01:00
`It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?`
])[0];
try {
await api.publish(baseUrl, topic, message, {
title: title,
priority: priority,
tags: tags
});
} catch (e) {
console.log(`[ActionBar] Error publishing message`, e);
setSnackOpen(true);
}
2022-03-06 04:33:34 +01:00
}
return (
<>
2022-12-26 04:29:55 +01:00
<IconButton color="inherit" size="large" edge="end" onClick={handleToggleMute} aria-label={t("action_bar_toggle_mute")}>
2022-03-08 22:56:41 +01:00
{subscription.mutedUntil ? <NotificationsOffIcon/> : <NotificationsIcon/>}
</IconButton>
2022-12-29 08:32:05 +01:00
<IconButton color="inherit" size="large" edge="end" onClick={handleToggleOpen} aria-label={t("action_bar_toggle_action_menu")}>
2022-03-06 04:33:34 +01:00
<MoreVertIcon/>
</IconButton>
2022-12-29 08:32:05 +01:00
<PopupMenu
anchorEl={anchorEl}
2022-03-06 04:33:34 +01:00
open={open}
2022-12-29 08:32:05 +01:00
onClose={handleClose}
2022-03-06 04:33:34 +01:00
>
2022-12-29 08:32:05 +01:00
<MenuItem onClick={handleSubscriptionSettings}>{t("action_bar_subscription_settings")}</MenuItem>
<MenuItem onClick={handleSendTestMessage}>{t("action_bar_send_test_notification")}</MenuItem>
<MenuItem onClick={handleClearAll}>{t("action_bar_clear_notifications")}</MenuItem>
<MenuItem onClick={handleUnsubscribe}>{t("action_bar_unsubscribe")}</MenuItem>
</PopupMenu>
<Portal>
<Snackbar
open={snackOpen}
autoHideDuration={3000}
onClose={() => setSnackOpen(false)}
message={t("message_bar_error_publishing")}
/>
</Portal>
2022-06-29 21:57:56 +02:00
<Portal>
<SubscriptionSettingsDialog
key={`subscriptionSettingsDialog${subscription.id}`}
open={subscriptionSettingsOpen}
subscription={subscription}
onClose={() => setSubscriptionSettingsOpen(false)}
/>
</Portal>
2022-03-06 04:33:34 +01:00
</>
);
};
2022-12-29 08:32:05 +01:00
const ProfileIcon = () => {
2022-12-02 21:37:48 +01:00
const { t } = useTranslation();
2022-12-15 05:43:43 +01:00
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
2022-12-08 02:44:20 +01:00
const navigate = useNavigate();
2022-12-02 21:37:48 +01:00
2022-12-15 05:43:43 +01:00
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
2022-12-02 21:37:48 +01:00
};
2022-12-29 08:32:05 +01:00
2022-12-15 05:43:43 +01:00
const handleClose = () => {
setAnchorEl(null);
2022-12-02 21:37:48 +01:00
};
2022-12-29 08:32:05 +01:00
2022-12-08 02:44:20 +01:00
const handleLogout = async () => {
2022-12-25 17:59:44 +01:00
try {
await accountApi.logout();
2022-12-28 21:51:09 +01:00
await db.delete();
2022-12-25 17:59:44 +01:00
} finally {
session.resetAndRedirect(routes.app);
2022-12-25 17:59:44 +01:00
}
2022-12-02 21:37:48 +01:00
};
2022-12-29 08:32:05 +01:00
2022-12-02 21:37:48 +01:00
return (
<>
2022-12-08 02:44:20 +01:00
{session.exists() &&
2022-12-29 08:32:05 +01:00
<IconButton color="inherit" size="large" edge="end" onClick={handleClick} aria-label={t("action_bar_profile_title")}>
2022-12-02 21:37:48 +01:00
<AccountCircleIcon/>
</IconButton>
}
2022-12-21 19:19:07 +01:00
{!session.exists() && config.enableLogin &&
2022-12-29 08:32:05 +01:00
<Button color="inherit" variant="text" onClick={() => navigate(routes.login)} sx={{m: 1}} aria-label={t("action_bar_sign_in")}>
{t("action_bar_sign_in")}
</Button>
2022-12-21 19:19:07 +01:00
}
{!session.exists() && config.enableSignup &&
2022-12-29 08:32:05 +01:00
<Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)} aria-label={t("action_bar_sign_up")}>
{t("action_bar_sign_up")}
</Button>
2022-12-02 21:37:48 +01:00
}
2022-12-29 08:32:05 +01:00
<PopupMenu
2022-12-15 05:43:43 +01:00
anchorEl={anchorEl}
2022-12-02 21:37:48 +01:00
open={open}
2022-12-15 05:43:43 +01:00
onClose={handleClose}
2022-12-02 21:37:48 +01:00
>
2022-12-16 04:07:04 +01:00
<MenuItem onClick={() => navigate(routes.account)}>
2022-12-15 05:43:43 +01:00
<ListItemIcon>
<Person />
</ListItemIcon>
<b>{session.username()}</b>
</MenuItem>
<Divider />
2022-12-16 04:07:04 +01:00
<MenuItem onClick={() => navigate(routes.settings)}>
2022-12-15 05:43:43 +01:00
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
2022-12-29 08:32:05 +01:00
{t("action_bar_profile_settings")}
2022-12-15 05:43:43 +01:00
</MenuItem>
<MenuItem onClick={handleLogout}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
2022-12-29 08:32:05 +01:00
{t("action_bar_profile_logout")}
2022-12-15 05:43:43 +01:00
</MenuItem>
2022-12-29 08:32:05 +01:00
</PopupMenu>
2022-12-02 21:37:48 +01:00
</>
);
};
2022-12-29 08:32:05 +01:00
const PopupMenu = (props) => {
return (
<Menu
anchorEl={props.anchorEl}
open={props.open}
onClose={props.onClose}
onClick={props.onClose}
PaperProps={{
elevation: 0,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 19,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
{props.children}
</Menu>
);
};
2022-12-02 21:37:48 +01:00
2022-02-25 18:46:22 +01:00
export default ActionBar;