From a1a2939f3c94ae04a41a09b1f77d8393a0532315 Mon Sep 17 00:00:00 2001 From: German Viescas Date: Tue, 22 Nov 2022 14:36:01 -0300 Subject: [PATCH 1/5] Create notifications page --- src/pointsdk/index.d.ts | 13 +++++ src/pointsdk/sdk.ts | 7 ++- src/popup/App.tsx | 13 ++++- src/popup/components/BackArrow.tsx | 15 +++++ src/popup/components/Layout.tsx | 67 ++++++++++----------- src/popup/components/Links.tsx | 17 +++++- src/popup/components/Notification.tsx | 43 ++++++++++++++ src/popup/components/Notifications.tsx | 81 ++++++++++++++++++++++++++ 8 files changed, 218 insertions(+), 38 deletions(-) create mode 100644 src/popup/components/BackArrow.tsx create mode 100644 src/popup/components/Notification.tsx create mode 100644 src/popup/components/Notifications.tsx diff --git a/src/pointsdk/index.d.ts b/src/pointsdk/index.d.ts index 8cd89cf..53714d2 100644 --- a/src/pointsdk/index.d.ts +++ b/src/pointsdk/index.d.ts @@ -187,3 +187,16 @@ export type Network = { currency_code: string; tokens?: Token[]; }; + +export type PointNotification = { + id: number; + block_number: string; + timestamp: number; + identity: string; + address: string; + contract: string; + event: string; + arguments: Record; + viewed: boolean; + log: Record; +}; diff --git a/src/pointsdk/sdk.ts b/src/pointsdk/sdk.ts index 5398273..5ff3bfa 100644 --- a/src/pointsdk/sdk.ts +++ b/src/pointsdk/sdk.ts @@ -22,7 +22,8 @@ import { SubscriptionMessages, SubscriptionEvent, SubscriptionParams, - IdentityData + IdentityData, + PointNotification } from './index.d'; const getSdk = (host: string, version: string, swal: any): PointType => { @@ -781,6 +782,10 @@ const getSdk = (host: string, version: string, swal: any): PointType => { api.get(`identity/ownerToIdentity/${owner}`, args), me: () => api.get('identity/isIdentityRegistered/') }, + notifications: { + unread: () => api.get('notifications/unread'), + markRead: (id: number) => api.get(`notifications/read/${id}`) + }, ...(host === 'https://point' && !window.top.IS_GATEWAY ? { point: { diff --git a/src/popup/App.tsx b/src/popup/App.tsx index 20d59f5..824c37e 100644 --- a/src/popup/App.tsx +++ b/src/popup/App.tsx @@ -1,14 +1,25 @@ import React, {FunctionComponent} from 'react'; +import {MemoryRouter as Router, Routes, Route} from 'react-router-dom'; +import UIThemeProvider from 'pointsdk/theme/UIThemeProvider'; import {BlockchainContext, useBlockchain} from './context/blockchain'; import Layout from './components/Layout'; +import Notifications from './components/Notifications'; const App: FunctionComponent = () => { const blockchainContext = useBlockchain(); return ( - + + + + } /> + } /> + + + ); }; + export default App; diff --git a/src/popup/components/BackArrow.tsx b/src/popup/components/BackArrow.tsx new file mode 100644 index 0000000..396022a --- /dev/null +++ b/src/popup/components/BackArrow.tsx @@ -0,0 +1,15 @@ +import React, {FunctionComponent} from 'react'; +import {Link} from 'react-router-dom'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; + +type Props = { + to: string; +}; + +const BackArrow: FunctionComponent = ({to}: Props) => ( + + + +); + +export default BackArrow; diff --git a/src/popup/components/Layout.tsx b/src/popup/components/Layout.tsx index e40c1b3..f263774 100644 --- a/src/popup/components/Layout.tsx +++ b/src/popup/components/Layout.tsx @@ -5,7 +5,6 @@ import Typography from '@mui/material/Typography'; import CircularProgress from '@mui/material/CircularProgress'; import Divider from '@mui/material/Divider'; import {BlockchainContext} from '../context/blockchain'; -import UIThemeProvider from 'pointsdk/theme/UIThemeProvider'; import NetworkSwitcher from './NetworkSwitcher'; import UserData from './UserData'; import Links from './Links'; @@ -17,41 +16,39 @@ const Layout: FunctionComponent = () => { const {loading} = useContext(BlockchainContext); return ( - - - {' '} - - point-logo - - PointSDK - - - {loading ? ( - - - Loading... - - ) : ( - - - - - - - - - - - - )} + + {' '} + + point-logo + + PointSDK + - + {loading ? ( + + + Loading... + + ) : ( + + + + + + + + + + + + )} + ); }; diff --git a/src/popup/components/Links.tsx b/src/popup/components/Links.tsx index d0fc874..2faaa52 100644 --- a/src/popup/components/Links.tsx +++ b/src/popup/components/Links.tsx @@ -1,11 +1,15 @@ import React, {FunctionComponent} from 'react'; +import {useNavigate} from 'react-router-dom'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; import HomeIcon from '@mui/icons-material/Home'; import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; +import NotificationsIcon from '@mui/icons-material/Notifications'; import browser from 'webextension-polyfill'; const Links: FunctionComponent = () => { + const navigate = useNavigate(); + const go = async (url: string) => { // Open new tab with the desired URL. await browser.tabs.create({url}); @@ -19,7 +23,7 @@ const Links: FunctionComponent = () => { Quick Links - + { My Wallet + navigate('/notifications')} + > + + + Notifications + + ); diff --git a/src/popup/components/Notification.tsx b/src/popup/components/Notification.tsx new file mode 100644 index 0000000..a3e86b7 --- /dev/null +++ b/src/popup/components/Notification.tsx @@ -0,0 +1,43 @@ +import React, {FunctionComponent} from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import CloseIcon from '@mui/icons-material/Close'; +import {PointNotification} from 'pointsdk/pointsdk/index.d'; + +type Props = { + n: PointNotification; + onMarkRead: (id: number) => void; +}; + +const Notification: FunctionComponent = ({n, onMarkRead}: Props) => { + const markRead = () => { + onMarkRead(n.id); + }; + + return ( + + + + {n.event} ({n.contract}) + + + +
{JSON.stringify(n.arguments, null, 2)}
+
+ ); +}; + +export default Notification; diff --git a/src/popup/components/Notifications.tsx b/src/popup/components/Notifications.tsx new file mode 100644 index 0000000..bdb8921 --- /dev/null +++ b/src/popup/components/Notifications.tsx @@ -0,0 +1,81 @@ +import React, {FunctionComponent, useEffect, useState, useCallback} from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import CircularProgress from '@mui/material/CircularProgress'; +import Alert from '@mui/material/Alert'; +import {PointNotification} from 'pointsdk/pointsdk/index.d'; +import BackArrow from './BackArrow'; +import Notification from './Notification'; + +const Notifications: FunctionComponent = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [notifications, setNotifications] = useState([]); + + useEffect(() => { + async function fetchUnread() { + setLoading(true); + try { + const {data} = (await window.point.notifications.unread()) as { + data: PointNotification[]; + }; + setNotifications(data); + } catch (err) { + console.error(err); + setError('Unable to fetch notifications.'); + } finally { + setLoading(false); + } + } + void fetchUnread(); + }, []); + + const handleMarkRead = useCallback(async (id: number) => { + try { + await window.point.notifications.markRead(id); + setNotifications(prev => prev.filter(n => n.id !== id)); + } catch (err) { + console.error(err); + setError('Unable to mark notification as read.'); + } + }, []); + + const handleCloseError = () => { + setError(''); + }; + + return ( + + + + + Notifications{' '} + {notifications.length > 0 ? ({notifications.length}) : ''} + + + + {loading || error ? ( + + {loading ? : null} + {error ? ( + + {error} + + ) : null} + + ) : null} + + {!loading && !error && notifications.length === 0 ? ( + + You have no notifications. + + ) : null} + + {notifications.map(n => ( + + ))} + + ); +}; + +export default Notifications; From 1e4ff95eb98eeedf2d838d550fd5452bc96a6604 Mon Sep 17 00:00:00 2001 From: German Viescas Date: Wed, 23 Nov 2022 09:38:51 -0300 Subject: [PATCH 2/5] Create notifications ctx and show badge in homepage --- src/popup/App.tsx | 19 +++--- src/popup/components/Links.tsx | 12 +++- src/popup/components/Notifications.tsx | 44 ++------------ src/popup/context/notifications.tsx | 80 ++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 48 deletions(-) create mode 100644 src/popup/context/notifications.tsx diff --git a/src/popup/App.tsx b/src/popup/App.tsx index 824c37e..3d4eac6 100644 --- a/src/popup/App.tsx +++ b/src/popup/App.tsx @@ -2,6 +2,7 @@ import React, {FunctionComponent} from 'react'; import {MemoryRouter as Router, Routes, Route} from 'react-router-dom'; import UIThemeProvider from 'pointsdk/theme/UIThemeProvider'; import {BlockchainContext, useBlockchain} from './context/blockchain'; +import {NotificationsProvider} from './context/notifications'; import Layout from './components/Layout'; import Notifications from './components/Notifications'; @@ -10,14 +11,16 @@ const App: FunctionComponent = () => { return ( - - - - } /> - } /> - - - + + + + + } /> + } /> + + + + ); }; diff --git a/src/popup/components/Links.tsx b/src/popup/components/Links.tsx index 2faaa52..52a73af 100644 --- a/src/popup/components/Links.tsx +++ b/src/popup/components/Links.tsx @@ -5,10 +5,13 @@ import Box from '@mui/material/Box'; import HomeIcon from '@mui/icons-material/Home'; import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; import NotificationsIcon from '@mui/icons-material/Notifications'; +import Badge from '@mui/material/Badge'; import browser from 'webextension-polyfill'; +import {useNotifications} from '../context/notifications'; const Links: FunctionComponent = () => { const navigate = useNavigate(); + const {notifications} = useNotifications(); const go = async (url: string) => { // Open new tab with the desired URL. @@ -53,7 +56,14 @@ const Links: FunctionComponent = () => { sx={{cursor: 'pointer'}} onClick={() => navigate('/notifications')} > - + + + Notifications diff --git a/src/popup/components/Notifications.tsx b/src/popup/components/Notifications.tsx index bdb8921..e62d863 100644 --- a/src/popup/components/Notifications.tsx +++ b/src/popup/components/Notifications.tsx @@ -1,48 +1,14 @@ -import React, {FunctionComponent, useEffect, useState, useCallback} from 'react'; +import React, {FunctionComponent} from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import CircularProgress from '@mui/material/CircularProgress'; import Alert from '@mui/material/Alert'; -import {PointNotification} from 'pointsdk/pointsdk/index.d'; +import {useNotifications} from '../context/notifications'; import BackArrow from './BackArrow'; import Notification from './Notification'; const Notifications: FunctionComponent = () => { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [notifications, setNotifications] = useState([]); - - useEffect(() => { - async function fetchUnread() { - setLoading(true); - try { - const {data} = (await window.point.notifications.unread()) as { - data: PointNotification[]; - }; - setNotifications(data); - } catch (err) { - console.error(err); - setError('Unable to fetch notifications.'); - } finally { - setLoading(false); - } - } - void fetchUnread(); - }, []); - - const handleMarkRead = useCallback(async (id: number) => { - try { - await window.point.notifications.markRead(id); - setNotifications(prev => prev.filter(n => n.id !== id)); - } catch (err) { - console.error(err); - setError('Unable to mark notification as read.'); - } - }, []); - - const handleCloseError = () => { - setError(''); - }; + const {notifications, loading, error, markRead, dismissError} = useNotifications(); return ( @@ -58,7 +24,7 @@ const Notifications: FunctionComponent = () => { {loading ? : null} {error ? ( - + {error} ) : null} @@ -72,7 +38,7 @@ const Notifications: FunctionComponent = () => { ) : null} {notifications.map(n => ( - + ))} ); diff --git a/src/popup/context/notifications.tsx b/src/popup/context/notifications.tsx new file mode 100644 index 0000000..827b58d --- /dev/null +++ b/src/popup/context/notifications.tsx @@ -0,0 +1,80 @@ +import React, { + createContext, + useContext, + useEffect, + useState, + useMemo, + useCallback, + ReactNode +} from 'react'; +import {PointNotification} from 'pointsdk/pointsdk/index.d'; + +type NotificationsCtxType = { + notifications: PointNotification[]; + loading: boolean; + error: string; + markRead: (id: number) => void; + dismissError: () => void; +}; + +const NotificationsCtx = createContext({ + notifications: [], + loading: false, + error: '', + markRead: () => {}, + dismissError: () => {} +}); +NotificationsCtx.displayName = 'NotificationsCtx'; + +export const NotificationsProvider = ({children}: {children: ReactNode}) => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [notifications, setNotifications] = useState([]); + + useEffect(() => { + async function fetchUnread() { + setLoading(true); + try { + const {data} = (await window.point.notifications.unread()) as { + data: PointNotification[]; + }; + setNotifications(data); + } catch (err) { + console.error(err); + setError('Unable to fetch notifications.'); + } finally { + setLoading(false); + } + } + void fetchUnread(); + }, []); + + const markRead = useCallback(async (id: number) => { + try { + await window.point.notifications.markRead(id); + setNotifications(prev => prev.filter(n => n.id !== id)); + } catch (err) { + console.error(err); + setError('Unable to mark notification as read.'); + } + }, []); + + const dismissError = useCallback(() => { + setError(''); + }, []); + + const value = useMemo( + () => ({notifications, loading, error, markRead, dismissError}), + [notifications, loading, error, markRead, dismissError] + ); + + return {children}; +}; + +export function useNotifications() { + const ctx = useContext(NotificationsCtx); + if (!ctx) { + throw new Error('useNotifications must be used within a '); + } + return ctx; +} From 1188d746fe9e47b3a7c47529b6dee909c446157a Mon Sep 17 00:00:00 2001 From: German Viescas Date: Wed, 23 Nov 2022 11:26:55 -0300 Subject: [PATCH 3/5] Improve notification UI --- src/popup/components/Notification.tsx | 97 +++++++++++++++++++++++--- src/popup/components/Notifications.tsx | 2 +- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/popup/components/Notification.tsx b/src/popup/components/Notification.tsx index a3e86b7..0f7df82 100644 --- a/src/popup/components/Notification.tsx +++ b/src/popup/components/Notification.tsx @@ -1,41 +1,118 @@ import React, {FunctionComponent} from 'react'; +import browser from 'webextension-polyfill'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Button from '@mui/material/Button'; import CloseIcon from '@mui/icons-material/Close'; import {PointNotification} from 'pointsdk/pointsdk/index.d'; +type ParsedNotification = { + title: string; + from: string; + date: string; + link: string; +}; + +const socialActionToTile: Record string> = { + POST_LIKE: (id: string | number) => `Post #${id} got a new like`, + POST_LIKE_DELETE: (id: string | number) => `Post #${id} got an unlike`, + POST_DISLIKE: (id: string | number) => `Post #${id} got a new dislike`, + POST_CREATE: (id: string | number) => `New post #${id}`, + POST_EDIT: (id: string | number) => `Post #${id} has been edited`, + POST_DELETE: (id: string | number) => `Post #${id} has been deleted`, + POST_FLAG: (id: string | number) => `Post #${id} has been flagged`, + POST_COMMENT_CREATE: (id: string | number) => `New comment on post #${id}`, + POST_COMMENT_DELETE: (id: string | number) => `Comment deleted on post #${id}`, + COMMENT_EDIT: (id: string | number) => `Comment edited on post #${id}`, + COMMENT_DELETE: (id: string | number) => `Comment deleted on post #${id}` +}; + +function parseNotification(n: PointNotification): ParsedNotification { + const data = n.arguments; + if (n.identity === 'social.point' && n.event === 'StateChange') { + return { + from: n.identity, + title: socialActionToTile[data.changeAction as string] + ? socialActionToTile[data.changeAction as string]!(data.id as string) + : `New ${n.event}`, + date: new Date(n.timestamp).toLocaleDateString(), + link: String(data.changeAction).includes('DELETE') + ? '' + : `https://social.point/post/${data.id as string}` + }; + } + + if (n.identity === 'email.point' && n.event === 'RecipientAdded') { + return { + from: n.identity, + title: 'You got email', + date: new Date(n.timestamp).toLocaleDateString(), + link: `https://email.point/show?id=${data.id as string}` + }; + } + + return { + from: n.identity, + title: `New ${n.event}`, + date: new Date(n.timestamp).toLocaleDateString(), + link: '' + }; +} + type Props = { n: PointNotification; onMarkRead: (id: number) => void; }; const Notification: FunctionComponent = ({n, onMarkRead}: Props) => { - const markRead = () => { - onMarkRead(n.id); + const {title, from, date, link} = parseNotification(n); + + const go = async (url: string) => { + // Open new tab with the desired URL. + await browser.tabs.create({url}); + // Close the extension popup window. + window.close(); }; return ( - - {n.event} ({n.contract}) + + {title} - -
{JSON.stringify(n.arguments, null, 2)}
+ + + App: {from} + + + On: {date} + + + {link ? ( + + ) : null}
); }; diff --git a/src/popup/components/Notifications.tsx b/src/popup/components/Notifications.tsx index e62d863..834719d 100644 --- a/src/popup/components/Notifications.tsx +++ b/src/popup/components/Notifications.tsx @@ -16,7 +16,7 @@ const Notifications: FunctionComponent = () => { Notifications{' '} - {notifications.length > 0 ? ({notifications.length}) : ''} + {notifications.length > 0 ? ({notifications.length}) : ''}
From 8cd4f59bddd401ad318db7c7fd149b09335d2577 Mon Sep 17 00:00:00 2001 From: German Viescas Date: Wed, 23 Nov 2022 14:04:02 -0300 Subject: [PATCH 4/5] Ask Engine for past events --- src/pointsdk/index.d.ts | 6 ++++++ src/pointsdk/sdk.ts | 13 ++++++++++++- src/popup/context/notifications.tsx | 26 ++++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/pointsdk/index.d.ts b/src/pointsdk/index.d.ts index 53714d2..6468bf0 100644 --- a/src/pointsdk/index.d.ts +++ b/src/pointsdk/index.d.ts @@ -200,3 +200,9 @@ export type PointNotification = { viewed: boolean; log: Record; }; + +export type BlockRange = { + from: number; + to: number; + latest?: number; +}; diff --git a/src/pointsdk/sdk.ts b/src/pointsdk/sdk.ts index 5ff3bfa..52a2b38 100644 --- a/src/pointsdk/sdk.ts +++ b/src/pointsdk/sdk.ts @@ -23,7 +23,8 @@ import { SubscriptionEvent, SubscriptionParams, IdentityData, - PointNotification + PointNotification, + BlockRange } from './index.d'; const getSdk = (host: string, version: string, swal: any): PointType => { @@ -784,6 +785,16 @@ const getSdk = (host: string, version: string, swal: any): PointType => { }, notifications: { unread: () => api.get('notifications/unread'), + scan: ({from, to, latest}: Partial) => { + const endpoint = 'notifications/scan'; + const query = new URLSearchParams(); + if (from) query.append('from', String(from)); + if (to) query.append('to', String(to)); + if (latest) query.append('latest', String(latest)); + const queryStr = query.toString(); + const url = queryStr ? `${endpoint}?${queryStr}` : endpoint; + return api.get(url); + }, markRead: (id: number) => api.get(`notifications/read/${id}`) }, ...(host === 'https://point' && !window.top.IS_GATEWAY diff --git a/src/popup/context/notifications.tsx b/src/popup/context/notifications.tsx index 827b58d..d326fd5 100644 --- a/src/popup/context/notifications.tsx +++ b/src/popup/context/notifications.tsx @@ -7,7 +7,7 @@ import React, { useCallback, ReactNode } from 'react'; -import {PointNotification} from 'pointsdk/pointsdk/index.d'; +import {PointNotification, BlockRange} from 'pointsdk/pointsdk/index.d'; type NotificationsCtxType = { notifications: PointNotification[]; @@ -31,6 +31,28 @@ export const NotificationsProvider = ({children}: {children: ReactNode}) => { const [error, setError] = useState(''); const [notifications, setNotifications] = useState([]); + useEffect(() => { + async function fetchPastEvents(opts: Partial = {}) { + console.log('Request to fetch past events', opts); + try { + const {data} = (await window.point.notifications.scan(opts)) as { + data: {from: number; to: number; latest: number; logs: PointNotification[]}; + }; + console.log({from: data.from, to: data.to, latest: data.latest}); + if (data.logs && data.logs.length > 0) { + setNotifications(prev => [...prev, ...data.logs]); + } + if (data.latest > data.to) { + await fetchPastEvents({from: data.to + 1, latest: data.latest}); + } + } catch (err) { + console.error(err); + } + } + // TODO: remove the hardcoded `latest` value to scan up to the actual latest block. + void fetchPastEvents({latest: 4_053_000}); + }, []); + useEffect(() => { async function fetchUnread() { setLoading(true); @@ -38,7 +60,7 @@ export const NotificationsProvider = ({children}: {children: ReactNode}) => { const {data} = (await window.point.notifications.unread()) as { data: PointNotification[]; }; - setNotifications(data); + setNotifications(prev => [...prev, ...data]); } catch (err) { console.error(err); setError('Unable to fetch notifications.'); From 629bc168a83e9bef19cd5d4028737da465cd420a Mon Sep 17 00:00:00 2001 From: German Viescas Date: Thu, 24 Nov 2022 14:39:57 -0300 Subject: [PATCH 5/5] Show system notification after fetching past events --- src/manifest.json | 3 ++- src/popup/context/notifications.tsx | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/manifest.json b/src/manifest.json index e8211dd..3c765d3 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -24,7 +24,8 @@ "tabs", "activeTab", "storage", - "webRequest" + "webRequest", + "notifications" ], "content_scripts": [ { diff --git a/src/popup/context/notifications.tsx b/src/popup/context/notifications.tsx index d326fd5..5e0103f 100644 --- a/src/popup/context/notifications.tsx +++ b/src/popup/context/notifications.tsx @@ -7,6 +7,7 @@ import React, { useCallback, ReactNode } from 'react'; +import browser from 'webextension-polyfill'; import {PointNotification, BlockRange} from 'pointsdk/pointsdk/index.d'; type NotificationsCtxType = { @@ -30,6 +31,7 @@ export const NotificationsProvider = ({children}: {children: ReactNode}) => { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [notifications, setNotifications] = useState([]); + const [finishFetching, setFinishFetching] = useState(false); useEffect(() => { async function fetchPastEvents(opts: Partial = {}) { @@ -38,12 +40,13 @@ export const NotificationsProvider = ({children}: {children: ReactNode}) => { const {data} = (await window.point.notifications.scan(opts)) as { data: {from: number; to: number; latest: number; logs: PointNotification[]}; }; - console.log({from: data.from, to: data.to, latest: data.latest}); if (data.logs && data.logs.length > 0) { setNotifications(prev => [...prev, ...data.logs]); } if (data.latest > data.to) { await fetchPastEvents({from: data.to + 1, latest: data.latest}); + } else { + setFinishFetching(true); } } catch (err) { console.error(err); @@ -71,6 +74,18 @@ export const NotificationsProvider = ({children}: {children: ReactNode}) => { void fetchUnread(); }, []); + useEffect(() => { + const count = notifications.length; + if (finishFetching && count > 0) { + void browser.notifications.create({ + type: 'basic', + iconUrl: '../../assets/icons/icon-48.png', + title: 'Point Notifications', + message: `You have ${count} new notification${count > 1 ? 's' : ''}.` + }); + } + }, [notifications, finishFetching]); + const markRead = useCallback(async (id: number) => { try { await window.point.notifications.markRead(id);