Add "new" badge and title

pull/149/head
Philipp Heckel 2022-03-06 22:37:13 -05:00
parent 3a76e4733c
commit 1d2f3f72e4
5 changed files with 42 additions and 10 deletions

View File

@ -2,7 +2,14 @@ import db from "./db";
class SubscriptionManager { class SubscriptionManager {
async all() { async all() {
return db.subscriptions.toArray(); // All subscriptions, including "new count"; this is a JOIN, see https://dexie.org/docs/API-Reference#joining
const subscriptions = await db.subscriptions.toArray();
await Promise.all(subscriptions.map(async s => {
s.new = await db.notifications
.where({ subscriptionId: s.id, new: 1 })
.count();
}));
return subscriptions;
} }
async get(subscriptionId) { async get(subscriptionId) {
@ -14,7 +21,6 @@ class SubscriptionManager {
} }
async updateState(subscriptionId, state) { async updateState(subscriptionId, state) {
console.log(`Update state: ${subscriptionId} ${state}`)
db.subscriptions.update(subscriptionId, { state: state }); db.subscriptions.update(subscriptionId, { state: state });
} }
@ -41,10 +47,15 @@ class SubscriptionManager {
if (exists) { if (exists) {
return false; return false;
} }
await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab try {
await db.subscriptions.update(subscriptionId, { notification.new = 1; // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
last: notification.id await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab
}); await db.subscriptions.update(subscriptionId, {
last: notification.id
});
} catch (e) {
console.error(`[SubscriptionManager] Error adding notification`, e);
}
return true; return true;
} }
@ -69,6 +80,12 @@ class SubscriptionManager {
.delete(); .delete();
} }
async markNotificationsRead(subscriptionId) {
await db.notifications
.where({subscriptionId: subscriptionId, new: 1})
.modify({new: 0});
}
async pruneNotifications(thresholdTimestamp) { async pruneNotifications(thresholdTimestamp) {
await db.notifications await db.notifications
.where("time").below(thresholdTimestamp) .where("time").below(thresholdTimestamp)

View File

@ -10,7 +10,7 @@ const db = new Dexie('ntfy');
db.version(1).stores({ db.version(1).stores({
subscriptions: '&id,baseUrl', subscriptions: '&id,baseUrl',
notifications: '&id,subscriptionId,time', notifications: '&id,subscriptionId,time,new,[subscriptionId+new]', // compound key for query performance
users: '&baseUrl,username', users: '&baseUrl,username',
prefs: '&key' prefs: '&key'
}); });

View File

@ -46,6 +46,7 @@ const Root = () => {
const users = useLiveQuery(() => userManager.all()); const users = useLiveQuery(() => userManager.all());
const subscriptions = useLiveQuery(() => subscriptionManager.all()); const subscriptions = useLiveQuery(() => subscriptionManager.all());
const selectedSubscription = findSelected(location, subscriptions); const selectedSubscription = findSelected(location, subscriptions);
const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0;
useWorkers(); useWorkers();
useConnectionListeners(); useConnectionListeners();
@ -54,6 +55,11 @@ const Root = () => {
connectionManager.refresh(subscriptions, users); connectionManager.refresh(subscriptions, users);
}, [subscriptions, users]); // Dangle! }, [subscriptions, users]); // Dangle!
useEffect(() => {
console.log(`hello ${newNotificationsCount}`)
document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy web` : "ntfy web";
}, [newNotificationsCount]);
return ( return (
<Box sx={{display: 'flex'}}> <Box sx={{display: 'flex'}}>
<CssBaseline/> <CssBaseline/>

View File

@ -12,12 +12,13 @@ import SettingsIcon from "@mui/icons-material/Settings";
import HomeIcon from '@mui/icons-material/Home'; import HomeIcon from '@mui/icons-material/Home';
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import SubscribeDialog from "./SubscribeDialog"; import SubscribeDialog from "./SubscribeDialog";
import {Alert, AlertTitle, CircularProgress, ListSubheader} from "@mui/material"; import {Alert, AlertTitle, Badge, CircularProgress, ListSubheader} from "@mui/material";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import {subscriptionRoute, topicShortUrl, topicUrl} from "../app/utils"; import {subscriptionRoute, topicShortUrl, topicUrl} from "../app/utils";
import {ConnectionState} from "../app/Connection"; import {ConnectionState} from "../app/Connection";
import {useLocation, useNavigate} from "react-router-dom"; import {useLocation, useNavigate} from "react-router-dom";
import subscriptionManager from "../app/SubscriptionManager";
const navWidth = 240; const navWidth = 240;
@ -134,12 +135,16 @@ const SubscriptionItem = (props) => {
const subscription = props.subscription; const subscription = props.subscription;
const icon = (subscription.state === ConnectionState.Connecting) const icon = (subscription.state === ConnectionState.Connecting)
? <CircularProgress size="24px"/> ? <CircularProgress size="24px"/>
: <ChatBubbleOutlineIcon/>; : <Badge badgeContent={subscription.new} invisible={subscription.new === 0} color="primary"><ChatBubbleOutlineIcon/></Badge>;
const label = (subscription.baseUrl === window.location.origin) const label = (subscription.baseUrl === window.location.origin)
? subscription.topic ? subscription.topic
: topicShortUrl(subscription.baseUrl, subscription.topic); : topicShortUrl(subscription.baseUrl, subscription.topic);
const handleClick = async () => {
navigate(subscriptionRoute(subscription));
await subscriptionManager.markNotificationsRead(subscription.id);
};
return ( return (
<ListItemButton onClick={() => navigate(subscriptionRoute(subscription))} selected={props.selected}> <ListItemButton onClick={handleClick} selected={props.selected}>
<ListItemIcon>{icon}</ListItemIcon> <ListItemIcon>{icon}</ListItemIcon>
<ListItemText primary={label}/> <ListItemText primary={label}/>
</ListItemButton> </ListItemButton>

View File

@ -80,6 +80,10 @@ const NotificationItem = (props) => {
alt={`Priority ${notification.priority}`} alt={`Priority ${notification.priority}`}
style={{ verticalAlign: 'bottom' }} style={{ verticalAlign: 'bottom' }}
/>} />}
{notification.new === 1 &&
<svg style={{ width: '8px', height: '8px', marginLeft: '4px' }} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" fill="#338574"/>
</svg>}
</Typography> </Typography>
{notification.title && <Typography variant="h5" component="div">{formatTitle(notification)}</Typography>} {notification.title && <Typography variant="h5" component="div">{formatTitle(notification)}</Typography>}
<Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>{formatMessage(notification)}</Typography> <Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>{formatMessage(notification)}</Typography>