From 1d2f3f72e43287fbf36213d3baa106eef2b64ead Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 6 Mar 2022 22:37:13 -0500 Subject: [PATCH] Add "new" badge and title --- web/src/app/SubscriptionManager.js | 29 +++++++++++++++++++++++------ web/src/app/db.js | 2 +- web/src/components/App.js | 6 ++++++ web/src/components/Navigation.js | 11 ++++++++--- web/src/components/Notifications.js | 4 ++++ 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index a64c3bdd..ca9fd340 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -2,7 +2,14 @@ import db from "./db"; class SubscriptionManager { 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) { @@ -14,7 +21,6 @@ class SubscriptionManager { } async updateState(subscriptionId, state) { - console.log(`Update state: ${subscriptionId} ${state}`) db.subscriptions.update(subscriptionId, { state: state }); } @@ -41,10 +47,15 @@ class SubscriptionManager { if (exists) { return false; } - await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab - await db.subscriptions.update(subscriptionId, { - last: notification.id - }); + try { + notification.new = 1; // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + 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; } @@ -69,6 +80,12 @@ class SubscriptionManager { .delete(); } + async markNotificationsRead(subscriptionId) { + await db.notifications + .where({subscriptionId: subscriptionId, new: 1}) + .modify({new: 0}); + } + async pruneNotifications(thresholdTimestamp) { await db.notifications .where("time").below(thresholdTimestamp) diff --git a/web/src/app/db.js b/web/src/app/db.js index aec043ad..7c82be31 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -10,7 +10,7 @@ const db = new Dexie('ntfy'); db.version(1).stores({ subscriptions: '&id,baseUrl', - notifications: '&id,subscriptionId,time', + notifications: '&id,subscriptionId,time,new,[subscriptionId+new]', // compound key for query performance users: '&baseUrl,username', prefs: '&key' }); diff --git a/web/src/components/App.js b/web/src/components/App.js index 48654d06..113ef952 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -46,6 +46,7 @@ const Root = () => { const users = useLiveQuery(() => userManager.all()); const subscriptions = useLiveQuery(() => subscriptionManager.all()); const selectedSubscription = findSelected(location, subscriptions); + const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0; useWorkers(); useConnectionListeners(); @@ -54,6 +55,11 @@ const Root = () => { connectionManager.refresh(subscriptions, users); }, [subscriptions, users]); // Dangle! + useEffect(() => { + console.log(`hello ${newNotificationsCount}`) + document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy web` : "ntfy web"; + }, [newNotificationsCount]); + return ( diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js index f559eed4..551cd6e1 100644 --- a/web/src/components/Navigation.js +++ b/web/src/components/Navigation.js @@ -12,12 +12,13 @@ import SettingsIcon from "@mui/icons-material/Settings"; import HomeIcon from '@mui/icons-material/Home'; import AddIcon from "@mui/icons-material/Add"; 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 Typography from "@mui/material/Typography"; import {subscriptionRoute, topicShortUrl, topicUrl} from "../app/utils"; import {ConnectionState} from "../app/Connection"; import {useLocation, useNavigate} from "react-router-dom"; +import subscriptionManager from "../app/SubscriptionManager"; const navWidth = 240; @@ -134,12 +135,16 @@ const SubscriptionItem = (props) => { const subscription = props.subscription; const icon = (subscription.state === ConnectionState.Connecting) ? - : ; + : ; const label = (subscription.baseUrl === window.location.origin) ? subscription.topic : topicShortUrl(subscription.baseUrl, subscription.topic); + const handleClick = async () => { + navigate(subscriptionRoute(subscription)); + await subscriptionManager.markNotificationsRead(subscription.id); + }; return ( - navigate(subscriptionRoute(subscription))} selected={props.selected}> + {icon} diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js index cb015729..1e53f5b4 100644 --- a/web/src/components/Notifications.js +++ b/web/src/components/Notifications.js @@ -80,6 +80,10 @@ const NotificationItem = (props) => { alt={`Priority ${notification.priority}`} style={{ verticalAlign: 'bottom' }} />} + {notification.new === 1 && + + + } {notification.title && {formatTitle(notification)}} {formatMessage(notification)}