Desktop notifications

pull/149/head
Philipp Heckel 2022-02-26 10:14:43 -05:00
parent 530f55c234
commit aa79fe2861
5 changed files with 101 additions and 42 deletions

View File

@ -288,7 +288,7 @@ const formatTitle = (m) => {
if (m.title) {
return formatTitleA(m);
} else {
return `${location.host}/${m.topic}`;
return `${location.host}/${m.topic}`; // FIXME
}
};

View File

@ -32,13 +32,16 @@ class Connection {
console.log(`[Connection, ${this.shortUrl}] Message received from server: ${event.data}`);
try {
const data = JSON.parse(event.data);
if (data.event === 'open') {
return;
}
const relevantAndValid =
data.event === 'message' &&
'id' in data &&
'time' in data &&
'message' in data;
if (!relevantAndValid) {
console.log(`[Connection, ${this.shortUrl}] Message irrelevant or invalid. Ignoring.`);
console.log(`[Connection, ${this.shortUrl}] Unexpected message. Ignoring.`);
return;
}
this.since = data.time + 1; // Sigh. This works because on reconnect, we wait 5+ seconds anyway.

View File

@ -0,0 +1,33 @@
import {formatMessage, formatTitle} from "./utils";
class NotificationManager {
notify(subscription, notification, onClickFallback) {
const message = formatMessage(notification);
const title = formatTitle(notification);
const n = new Notification(title, {
body: message,
icon: '/static/img/favicon.png'
});
if (notification.click) {
n.onclick = (e) => window.open(notification.click);
} else {
n.onclick = onClickFallback;
}
}
granted() {
return Notification.permission === 'granted';
}
maybeRequestPermission(cb) {
if (!this.granted()) {
Notification.requestPermission().then((permission) => {
const granted = permission === 'granted';
cb(granted);
});
}
}
}
const notificationManager = new NotificationManager();
export default notificationManager;

View File

@ -13,6 +13,7 @@ import Subscriptions from "../app/Subscriptions";
import Navigation from "./Navigation";
import ActionBar from "./ActionBar";
import Users from "../app/Users";
import notificationManager from "../app/NotificationManager";
const App = () => {
console.log(`[App] Rendering main view`);
@ -21,9 +22,13 @@ const App = () => {
const [subscriptions, setSubscriptions] = useState(new Subscriptions());
const [users, setUsers] = useState(new Users());
const [selectedSubscription, setSelectedSubscription] = useState(null);
const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted());
const handleNotification = (subscriptionId, notification) => {
setSubscriptions(prev => {
const newSubscription = prev.get(subscriptionId).addNotification(notification);
notificationManager.notify(newSubscription, notification, () => {
setSelectedSubscription(newSubscription);
})
return prev.update(newSubscription).clone();
});
};
@ -41,6 +46,7 @@ const App = () => {
return prev.update(newSubscription).clone();
});
});
handleRequestPermission();
};
const handleDeleteNotification = (subscriptionId, notificationId) => {
console.log(`[App] Deleting notification ${notificationId} from ${subscriptionId}`);
@ -64,6 +70,11 @@ const App = () => {
return newSubscriptions;
});
};
const handleRequestPermission = () => {
notificationManager.maybeRequestPermission((granted) => {
setNotificationsGranted(granted);
})
};
useEffect(() => {
setSubscriptions(repository.loadSubscriptions());
setUsers(repository.loadUsers());
@ -90,9 +101,11 @@ const App = () => {
subscriptions={subscriptions}
selectedSubscription={selectedSubscription}
mobileDrawerOpen={mobileDrawerOpen}
notificationsGranted={notificationsGranted}
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
onSubscriptionClick={(subscriptionId) => setSelectedSubscription(subscriptions.get(subscriptionId))}
onSubscribeSubmit={handleSubscribeSubmit}
onRequestPermissionClick={handleRequestPermission}
/>
</Box>
<Box

View File

@ -1,27 +1,24 @@
import Drawer from "@mui/material/Drawer";
import * as React from "react";
import {useState} from "react";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
import ListItemText from "@mui/material/ListItemText";
import {useState} from "react";
import Toolbar from "@mui/material/Toolbar";
import Divider from "@mui/material/Divider";
import List from "@mui/material/List";
import SettingsIcon from "@mui/icons-material/Settings";
import AddIcon from "@mui/icons-material/Add";
import SubscribeDialog from "./SubscribeDialog";
import {Alert, AlertTitle} from "@mui/material";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
const navWidth = 240;
const Navigation = (props) => {
const navigationList =
<NavList
subscriptions={props.subscriptions}
selectedSubscription={props.selectedSubscription}
onSubscriptionClick={props.onSubscriptionClick}
onSubscribeSubmit={props.onSubscribeSubmit}
/>;
const navigationList = <NavList {...props}/>;
return (
<>
{/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */}
@ -64,17 +61,39 @@ const NavList = (props) => {
handleSubscribeReset();
props.onSubscribeSubmit(subscription, user);
}
const showSubscriptionsList = props.subscriptions.size() > 0;
const showGrantPermissionsBox = props.subscriptions.size() > 0 && !props.notificationsGranted;
return (
<>
<Toolbar/>
{props.subscriptions.size() > 0 &&
<Divider />}
<List component="nav">
<NavSubscriptionList
<List component="nav" sx={{paddingTop: 0}}>
{showGrantPermissionsBox &&
<>
<Divider/>
<Alert severity="warning" sx={{paddingTop: 2}}>
<AlertTitle>Notifications are disabled</AlertTitle>
<Typography gutterBottom>
Grant your browser permission to display desktop notifications.
</Typography>
<Button
sx={{float: 'right'}}
color="inherit"
size="small"
onClick={props.onRequestPermissionClick}
>
Grant now
</Button>
</Alert>
</>}
{showSubscriptionsList &&
<>
<Divider/>
<SubscriptionList
subscriptions={props.subscriptions}
selectedSubscription={props.selectedSubscription}
onSubscriptionClick={props.onSubscriptionClick}
/>
</>}
<Divider sx={{my: 1}}/>
<ListItemButton>
<ListItemIcon>
@ -99,30 +118,21 @@ const NavList = (props) => {
);
};
const NavSubscriptionList = (props) => {
const subscriptions = props.subscriptions;
const SubscriptionList = (props) => {
return (
<>
{subscriptions.map((id, subscription) =>
<NavSubscriptionItem
{props.subscriptions.map((id, subscription) =>
<ListItemButton
key={id}
subscription={subscription}
selected={props.selectedSubscription && props.selectedSubscription.id === id}
onClick={() => props.onSubscriptionClick(id)}
/>)
}
selected={props.selectedSubscription && props.selectedSubscription.id === id}
>
<ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon>
<ListItemText primary={subscription.shortUrl()}/>
</ListItemButton>
)}
</>
);
}
const NavSubscriptionItem = (props) => {
const subscription = props.subscription;
return (
<ListItemButton onClick={props.onClick} selected={props.selected}>
<ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon>
<ListItemText primary={subscription.shortUrl()}/>
</ListItemButton>
);
}
export default Navigation;