From 0b6dd07eeec3a17ae725dedbeb98268ace964440 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sun, 12 Feb 2023 17:38:50 +0800 Subject: [PATCH] Rewrote notifications, again --- src/app.jsx | 342 +++++++++++++++++++++--------------- src/components/menu.jsx | 8 + src/pages/following.jsx | 13 +- src/pages/notifications.jsx | 20 +-- src/utils/states.js | 3 +- 5 files changed, 217 insertions(+), 169 deletions(-) diff --git a/src/app.jsx b/src/app.jsx index 7b0705fa..8bf20091 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -55,6 +55,7 @@ import { getAccessToken } from './utils/auth'; import states, { getStatus, saveStatus } from './utils/states'; import store from './utils/store'; import { getCurrentAccount } from './utils/store-utils'; +import usePageVisibility from './utils/usePageVisibility'; window.__STATES__ = states; @@ -110,6 +111,7 @@ function App() { if (account) { store.session.set('currentAccount', account.info.id); const { masto } = api({ account }); + console.log('masto', masto); initPreferences(masto); setUIState('loading'); (async () => { @@ -164,6 +166,54 @@ function App() { // } // }, [isLoggedIn]); + // Notifications service + // - WebSocket to receive notifications when page is visible + const [visible, setVisible] = useState(true); + usePageVisibility(setVisible); + const notificationStream = useRef(); + useEffect(() => { + if (isLoggedIn && visible) { + const { masto } = api(); + (async () => { + // 1. Get the latest notification + if (states.notificationsLast) { + const notificationsIterator = masto.v1.notifications.list({ + limit: 1, + since_id: states.notificationsLast.id, + }); + const { value: notifications } = await notificationsIterator.next(); + if (notifications?.length) { + states.notificationsShowNew = true; + } + } + + // 2. Start streaming + notificationStream.current = await masto.ws.stream( + '/api/v1/streaming', + { + stream: 'user:notification', + }, + ); + console.log('🎏 Streaming notification', notificationStream.current); + + notificationStream.current.on('notification', (notification) => { + console.log('🔔🔔 Notification', notification); + states.notificationsShowNew = true; + }); + + notificationStream.current.ws.onclose = () => { + console.log('🔔🔔 Notification stream closed'); + }; + })(); + } + return () => { + if (notificationStream.current) { + notificationStream.current.ws.close(); + notificationStream.current = null; + } + }; + }, [visible, isLoggedIn]); + const { prevLocation } = snapStates; const backgroundLocation = useRef(prevLocation || null); const isModalPage = @@ -360,164 +410,164 @@ function App() { ); } -let ws; -async function startStream() { - const { masto, instance } = api(); - if ( - ws && - (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) - ) { - return; - } +// let ws; +// async function startStream() { +// const { masto, instance } = api(); +// if ( +// ws && +// (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) +// ) { +// return; +// } - const stream = await masto.v1.stream.streamUser(); - console.log('STREAM START', { stream }); - ws = stream.ws; +// const stream = await masto.v1.stream.streamUser(); +// console.log('STREAM START', { stream }); +// ws = stream.ws; - const handleNewStatus = debounce((status) => { - console.log('UPDATE', status); - if (document.visibilityState === 'hidden') return; +// const handleNewStatus = debounce((status) => { +// console.log('UPDATE', status); +// if (document.visibilityState === 'hidden') return; - const inHomeNew = states.homeNew.find((s) => s.id === status.id); - const inHome = status.id === states.homeLast?.id; - if (!inHomeNew && !inHome) { - if (states.settings.boostsCarousel && status.reblog) { - // do nothing - } else { - states.homeNew.unshift({ - id: status.id, - reblog: status.reblog?.id, - reply: !!status.inReplyToAccountId, - }); - console.log('homeNew 1', [...states.homeNew]); - } - } +// const inHomeNew = states.homeNew.find((s) => s.id === status.id); +// const inHome = status.id === states.homeLast?.id; +// if (!inHomeNew && !inHome) { +// if (states.settings.boostsCarousel && status.reblog) { +// // do nothing +// } else { +// states.homeNew.unshift({ +// id: status.id, +// reblog: status.reblog?.id, +// reply: !!status.inReplyToAccountId, +// }); +// console.log('homeNew 1', [...states.homeNew]); +// } +// } - saveStatus(status, instance); - }, 5000); - stream.on('update', handleNewStatus); - stream.on('status.update', (status) => { - console.log('STATUS.UPDATE', status); - saveStatus(status, instance); - }); - stream.on('delete', (statusID) => { - console.log('DELETE', statusID); - // delete states.statuses[statusID]; - const s = getStatus(statusID); - if (s) s._deleted = true; - }); - stream.on('notification', (notification) => { - console.log('NOTIFICATION', notification); +// saveStatus(status, instance); +// }, 5000); +// stream.on('update', handleNewStatus); +// stream.on('status.update', (status) => { +// console.log('STATUS.UPDATE', status); +// saveStatus(status, instance); +// }); +// stream.on('delete', (statusID) => { +// console.log('DELETE', statusID); +// // delete states.statuses[statusID]; +// const s = getStatus(statusID); +// if (s) s._deleted = true; +// }); +// stream.on('notification', (notification) => { +// console.log('NOTIFICATION', notification); - const inNotificationsNew = states.notificationsNew.find( - (n) => n.id === notification.id, - ); - const inNotifications = notification.id === states.notificationsLast?.id; - if (!inNotificationsNew && !inNotifications) { - states.notificationsNew.unshift(notification); - } +// const inNotificationsNew = states.notificationsNew.find( +// (n) => n.id === notification.id, +// ); +// const inNotifications = notification.id === states.notificationsLast?.id; +// if (!inNotificationsNew && !inNotifications) { +// states.notificationsNew.unshift(notification); +// } - saveStatus(notification.status, instance, { override: false }); - }); +// saveStatus(notification.status, instance, { override: false }); +// }); - stream.ws.onclose = () => { - console.log('STREAM CLOSED!'); - if (document.visibilityState !== 'hidden') { - startStream(); - } - }; +// stream.ws.onclose = () => { +// console.log('STREAM CLOSED!'); +// if (document.visibilityState !== 'hidden') { +// startStream(); +// } +// }; - return { - stream, - stopStream: () => { - stream.ws.close(); - }, - }; -} +// return { +// stream, +// stopStream: () => { +// stream.ws.close(); +// }, +// }; +// } -let lastHidden; -function startVisibility() { - const { masto, instance } = api(); - const handleVisible = (visible) => { - if (!visible) { - const timestamp = Date.now(); - lastHidden = timestamp; - } else { - const timestamp = Date.now(); - const diff = timestamp - lastHidden; - const diffMins = Math.round(diff / 1000 / 60); - console.log(`visible: ${visible}`, { lastHidden, diffMins }); - if (!lastHidden || diffMins > 1) { - (async () => { - try { - const firstStatusID = states.homeLast?.id; - const firstNotificationID = states.notificationsLast?.id; - console.log({ states, firstNotificationID, firstStatusID }); - const fetchHome = masto.v1.timelines.listHome({ - limit: 5, - ...(firstStatusID && { sinceId: firstStatusID }), - }); - const fetchNotifications = masto.v1.notifications.list({ - limit: 1, - ...(firstNotificationID && { sinceId: firstNotificationID }), - }); +// let lastHidden; +// function startVisibility() { +// const { masto, instance } = api(); +// const handleVisible = (visible) => { +// if (!visible) { +// const timestamp = Date.now(); +// lastHidden = timestamp; +// } else { +// const timestamp = Date.now(); +// const diff = timestamp - lastHidden; +// const diffMins = Math.round(diff / 1000 / 60); +// console.log(`visible: ${visible}`, { lastHidden, diffMins }); +// if (!lastHidden || diffMins > 1) { +// (async () => { +// try { +// const firstStatusID = states.homeLast?.id; +// const firstNotificationID = states.notificationsLast?.id; +// console.log({ states, firstNotificationID, firstStatusID }); +// const fetchHome = masto.v1.timelines.listHome({ +// limit: 5, +// ...(firstStatusID && { sinceId: firstStatusID }), +// }); +// const fetchNotifications = masto.v1.notifications.list({ +// limit: 1, +// ...(firstNotificationID && { sinceId: firstNotificationID }), +// }); - const newStatuses = await fetchHome; - const hasOneAndReblog = - newStatuses.length === 1 && newStatuses?.[0]?.reblog; - if (newStatuses.length) { - if (states.settings.boostsCarousel && hasOneAndReblog) { - // do nothing - } else { - states.homeNew = newStatuses.map((status) => { - saveStatus(status, instance); - return { - id: status.id, - reblog: status.reblog?.id, - reply: !!status.inReplyToAccountId, - }; - }); - console.log('homeNew 2', [...states.homeNew]); - } - } +// const newStatuses = await fetchHome; +// const hasOneAndReblog = +// newStatuses.length === 1 && newStatuses?.[0]?.reblog; +// if (newStatuses.length) { +// if (states.settings.boostsCarousel && hasOneAndReblog) { +// // do nothing +// } else { +// states.homeNew = newStatuses.map((status) => { +// saveStatus(status, instance); +// return { +// id: status.id, +// reblog: status.reblog?.id, +// reply: !!status.inReplyToAccountId, +// }; +// }); +// console.log('homeNew 2', [...states.homeNew]); +// } +// } - const newNotifications = await fetchNotifications; - if (newNotifications.length) { - const notification = newNotifications[0]; - const inNotificationsNew = states.notificationsNew.find( - (n) => n.id === notification.id, - ); - const inNotifications = - notification.id === states.notificationsLast?.id; - if (!inNotificationsNew && !inNotifications) { - states.notificationsNew.unshift(notification); - } +// const newNotifications = await fetchNotifications; +// if (newNotifications.length) { +// const notification = newNotifications[0]; +// const inNotificationsNew = states.notificationsNew.find( +// (n) => n.id === notification.id, +// ); +// const inNotifications = +// notification.id === states.notificationsLast?.id; +// if (!inNotificationsNew && !inNotifications) { +// states.notificationsNew.unshift(notification); +// } - saveStatus(notification.status, instance, { override: false }); - } - } catch (e) { - // Silently fail - console.error(e); - } finally { - startStream(); - } - })(); - } - } - }; +// saveStatus(notification.status, instance, { override: false }); +// } +// } catch (e) { +// // Silently fail +// console.error(e); +// } finally { +// startStream(); +// } +// })(); +// } +// } +// }; - const handleVisibilityChange = () => { - const hidden = document.visibilityState === 'hidden'; - handleVisible(!hidden); - console.log('VISIBILITY: ' + (hidden ? 'hidden' : 'visible')); - }; - document.addEventListener('visibilitychange', handleVisibilityChange); - requestAnimationFrame(handleVisibilityChange); - return { - stop: () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - }, - }; -} +// const handleVisibilityChange = () => { +// const hidden = document.visibilityState === 'hidden'; +// handleVisible(!hidden); +// console.log('VISIBILITY: ' + (hidden ? 'hidden' : 'visible')); +// }; +// document.addEventListener('visibilitychange', handleVisibilityChange); +// requestAnimationFrame(handleVisibilityChange); +// return { +// stop: () => { +// document.removeEventListener('visibilitychange', handleVisibilityChange); +// }, +// }; +// } export { App }; diff --git a/src/components/menu.jsx b/src/components/menu.jsx index 6809be0a..adee356a 100644 --- a/src/components/menu.jsx +++ b/src/components/menu.jsx @@ -1,4 +1,5 @@ import { FocusableItem, Menu, MenuDivider, MenuItem } from '@szhsin/react-menu'; +import { useSnapshot } from 'valtio'; import { api } from '../utils/api'; import states from '../utils/states'; @@ -7,6 +8,7 @@ import Icon from './icon'; import Link from './link'; function NavMenu(props) { + const snapStates = useSnapshot(states); const { instance, authenticated } = api(); return ( Notifications + {snapStates.notificationsShowNew && ( + + {' '} + • + + )} diff --git a/src/pages/following.jsx b/src/pages/following.jsx index c9954e39..5cee334e 100644 --- a/src/pages/following.jsx +++ b/src/pages/following.jsx @@ -90,17 +90,6 @@ function Following({ title, path, id, headerStart }) { if (s) s._deleted = true; }); - stream.on('notification', (notification) => { - console.log('🔔 Notification', notification); - const inNotifications = - notification.id === snapStates.notificationsLast?.id; - if (inNotifications) return; - states.notificationsNew.unshift(notification); - saveStatus(notification.status, instance, { - override: false, - }); - }); - stream.ws.onclose = () => { console.log('🎏 Streaming user closed'); }; @@ -124,7 +113,7 @@ function Following({ title, path, id, headerStart }) { 0 ? 'has-badge' : '' + snapStates.notificationsShowNew ? 'has-badge' : '' }`} onClick={(e) => { e.stopPropagation(); diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index 4e27f943..ac490b30 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -64,32 +64,34 @@ function Notifications() { const notificationsIterator = useRef(); async function fetchNotifications(firstLoad) { - if (firstLoad) { + if (firstLoad || !notificationsIterator.current) { // Reset iterator notificationsIterator.current = masto.v1.notifications.list({ limit: LIMIT, }); - states.notificationsNew = []; } const allNotifications = await notificationsIterator.current.next(); - if (allNotifications.value?.length) { - const notificationsValues = allNotifications.value.map((notification) => { + const notifications = allNotifications.value; + + if (notifications?.length) { + notifications.forEach((notification) => { saveStatus(notification.status, { skipThreading: true, override: false, }); - return notification; }); - const groupedNotifications = groupNotifications(notificationsValues); + const groupedNotifications = groupNotifications(notifications); if (firstLoad) { - states.notificationsLast = notificationsValues[0]; + states.notificationsLast = notifications[0]; states.notifications = groupedNotifications; } else { states.notifications.push(...groupedNotifications); } } + + states.notificationsShowNew = false; states.notificationsLastFetchTime = Date.now(); return allNotifications; } @@ -161,14 +163,12 @@ function Notifications() { - {snapStates.notificationsNew.length > 0 && uiState !== 'loading' && ( + {snapStates.notificationsShowNew && uiState !== 'loading' && (