From e83d128f62344fb427a5e3fa3958c248cdef31b4 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 27 Jan 2023 11:47:30 +0800 Subject: [PATCH] Rewrite Notifications page + experimental fix on getting/showing updates --- src/app.jsx | 130 +++++---- src/pages/home.jsx | 22 +- src/pages/notifications.css | 1 + src/pages/notifications.jsx | 516 ++++++++++++++++++------------------ src/utils/states.js | 2 + 5 files changed, 346 insertions(+), 325 deletions(-) diff --git a/src/app.jsx b/src/app.jsx index 8dd1c783..1e93235e 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -165,7 +165,7 @@ function App() { if (isLoggedIn) { requestAnimationFrame(() => { - startStream(); + // startStream(); startVisibility(); // Collect instance info @@ -342,20 +342,35 @@ function App() { ); } +let ws; async function startStream() { + 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 handleNewStatus = debounce((status) => { console.log('UPDATE', status); const inHomeNew = states.homeNew.find((s) => s.id === status.id); - const inHome = states.home.find((s) => s.id === status.id); + const inHome = status.id === states.homeLast?.id; if (!inHomeNew && !inHome) { - states.homeNew.unshift({ - id: status.id, - reblog: status.reblog?.id, - reply: !!status.inReplyToAccountId, - }); + 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); @@ -377,9 +392,7 @@ async function startStream() { const inNotificationsNew = states.notificationsNew.find( (n) => n.id === notification.id, ); - const inNotifications = states.notifications.find( - (n) => n.id === notification.id, - ); + const inNotifications = notification.id === states.notificationLast?.id; if (!inNotificationsNew && !inNotifications) { states.notificationsNew.unshift(notification); } @@ -389,10 +402,9 @@ async function startStream() { stream.ws.onclose = () => { console.log('STREAM CLOSED!'); - - requestAnimationFrame(() => { + if (document.visibilityState !== 'hidden') { startStream(); - }); + } }; return { @@ -404,8 +416,8 @@ async function startStream() { } function startVisibility() { - const handleVisibilityChange = () => { - if (document.visibilityState === 'hidden') { + const handleVisible = (visible) => { + if (!visible) { const timestamp = Date.now(); store.session.set('lastHidden', timestamp); } else { @@ -415,26 +427,26 @@ function startVisibility() { const diffMins = Math.round(diff / 1000 / 60); if (diffMins > 1) { console.log('visible', { lastHidden, diffMins }); - setTimeout(() => { - // Buffer for WS reconnect - (async () => { - try { - const firstStatusID = states.home[0]?.id; - const firstNotificationID = states.notifications[0]?.id; - const fetchHome = masto.v1.timelines.listHome({ - limit: 1, - ...(firstStatusID && { sinceId: firstStatusID }), - }); - const fetchNotifications = masto.v1.notifications.list({ - limit: 1, - ...(firstNotificationID && { sinceId: firstNotificationID }), - }); + (async () => { + try { + const firstStatusID = states.home[0]?.id; + const firstNotificationID = states.notifications[0]?.id; + const fetchHome = masto.v1.timelines.listHome({ + limit: 1, + ...(firstStatusID && { sinceId: firstStatusID }), + }); + const fetchNotifications = masto.v1.notifications.list({ + limit: 1, + ...(firstNotificationID && { sinceId: firstNotificationID }), + }); - const newStatuses = await fetchHome; - if ( - newStatuses.length && - newStatuses[0].id !== states.home[0].id - ) { + const newStatuses = await fetchHome; + const newStatus = newStatuses?.[0]; + const inHome = newStatus?.id !== states.homeLast?.id; + if (newStatuses.length && !inHome) { + if (states.settings.boostsCarousel && newStatus.reblog) { + // do nothing + } else { states.homeNew = newStatuses.map((status) => { saveStatus(status); return { @@ -443,32 +455,40 @@ function startVisibility() { 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 = states.notifications.find( - (n) => n.id === notification.id, - ); - if (!inNotificationsNew && !inNotifications) { - states.notificationsNew.unshift(notification); - } - - saveStatus(notification.status, { override: false }); - } - } catch (e) { - // Silently fail - console.error(e); } - })(); - }, 100); + + 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.notificationLast?.id; + if (!inNotificationsNew && !inNotifications) { + states.notificationsNew.unshift(notification); + } + + saveStatus(notification.status, { 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); return { stop: () => { diff --git a/src/pages/home.jsx b/src/pages/home.jsx index 5fec0550..71060aa0 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -22,11 +22,7 @@ function Home({ hidden }) { console.debug('RENDER Home'); - const homeIterator = useRef( - masto.v1.timelines.listHome({ - limit: LIMIT, - }), - ); + const homeIterator = useRef(); async function fetchStatuses(firstLoad) { if (firstLoad) { // Reset iterator @@ -94,12 +90,14 @@ function Home({ hidden }) { specialHome, }); if (firstLoad) { + states.homeLast = specialHome[0]; states.home = specialHome; } else { states.home.push(...specialHome); } } else { if (firstLoad) { + states.homeLast = homeValues[0]; states.home = homeValues; } else { states.home.push(...homeValues); @@ -272,6 +270,13 @@ function Home({ hidden }) { })(); }, []); + // const showUpdatesButton = snapStates.homeNew.length > 0 && reachStart; + const [showUpdatesButton, setShowUpdatesButton] = useState(false); + useEffect(() => { + const isNewAndTop = snapStates.homeNew.length > 0 && reachStart; + setShowUpdatesButton(isNewAndTop); + }, [snapStates.homeNew.length]); + return ( <> + + ); +} function Notification({ notification }) { const { id, type, status, account, _accounts } = notification; @@ -61,7 +284,7 @@ function Notification({ notification }) { : contentText[type]; return ( - <> +
)}
- - ); -} - -function NotificationsList({ notifications, emptyCopy }) { - if (!notifications.length && emptyCopy) { - return

{emptyCopy}

; - } - - // Create new flat list of notifications - // Combine sibling notifications based on type and status id, ignore the id - // Concat all notification.account into an array of _accounts - const notificationsMap = {}; - const cleanNotifications = []; - for (let i = 0, j = 0; i < notifications.length; i++) { - const notification = notifications[i]; - // const cleanNotification = cleanNotifications[j]; - const { status, account, type, created_at } = notification; - const createdAt = new Date(created_at).toLocaleDateString(); - const key = `${status?.id}-${type}-${createdAt}`; - const mappedNotification = notificationsMap[key]; - if (mappedNotification?.account) { - mappedNotification._accounts.push(account); - } else { - let n = (notificationsMap[key] = { - ...notification, - _accounts: [account], - }); - cleanNotifications[j++] = n; - } - } - // console.log({ notifications, cleanNotifications }); - - return ( - - ); -} - -function Notifications() { - useTitle('Notifications'); - const snapStates = useSnapshot(states); - const [uiState, setUIState] = useState('default'); - const [showMore, setShowMore] = useState(false); - const [onlyMentions, setOnlyMentions] = useState(false); - - console.debug('RENDER Notifications'); - - const notificationsIterator = useRef( - masto.v1.notifications.list({ - limit: LIMIT, - }), - ); - async function fetchNotifications(firstLoad) { - if (firstLoad) { - // 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) => { - saveStatus(notification.status, { - skipThreading: true, - override: false, - }); - return notification; - }); - if (firstLoad) { - states.notifications = notificationsValues; - } else { - states.notifications.push(...notificationsValues); - } - } - states.notificationsLastFetchTime = Date.now(); - return allNotifications; - } - - const loadNotifications = (firstLoad) => { - setUIState('loading'); - (async () => { - try { - const { done } = await fetchNotifications(firstLoad); - setShowMore(!done); - setUIState('default'); - } catch (e) { - setUIState('error'); - } - })(); - }; - - useEffect(() => { - loadNotifications(true); - }, []); - - const scrollableRef = useRef(); - - // Group notifications by today, yesterday, and older - const groupedNotifications = snapStates.notifications.reduce( - (acc, notification) => { - const date = new Date(notification.createdAt); - const today = new Date(); - const yesterday = new Date(); - yesterday.setDate(today.getDate() - 1); - if ( - date.getDate() === today.getDate() && - date.getMonth() === today.getMonth() && - date.getFullYear() === today.getFullYear() - ) { - acc.today.push(notification); - } else if ( - date.getDate() === yesterday.getDate() && - date.getMonth() === yesterday.getMonth() && - date.getFullYear() === yesterday.getFullYear() - ) { - acc.yesterday.push(notification); - } else { - acc.older.push(notification); - } - return acc; - }, - { today: [], yesterday: [], older: [] }, - ); - // console.log(groupedNotifications); - return ( -
-
-
{ - scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); - }} - > -
- - - -
-

Notifications

-
-
-
- {snapStates.notificationsNew.length > 0 && ( - - )} -
- -
- {snapStates.notifications.length ? ( - <> -

Today

- - {groupedNotifications.yesterday.length > 0 && ( - <> -

Yesterday

- - - )} - {groupedNotifications.older.length > 0 && ( - <> -

Older

- - - )} - {showMore && ( - - )} - - ) : ( - <> - {uiState === 'loading' && ( - <> -

Today

-
    - {Array.from({ length: 5 }).map((_, i) => ( -
  • -
    - -
    -
    -

    ███████████ ████

    -
    -
  • - ))} -
- - )} - {uiState === 'error' && ( -

- Unable to load notifications -
-
- -

- )} - - )} -
); } @@ -470,4 +437,29 @@ function FollowRequestButtons({ accountID, onChange }) { ); } +function groupNotifications(notifications) { + // Create new flat list of notifications + // Combine sibling notifications based on type and status id + // Concat all notification.account into an array of _accounts + const notificationsMap = {}; + const cleanNotifications = []; + for (let i = 0, j = 0; i < notifications.length; i++) { + const notification = notifications[i]; + const { status, account, type, created_at } = notification; + const createdAt = new Date(created_at).toLocaleDateString(); + const key = `${status?.id}-${type}-${createdAt}`; + const mappedNotification = notificationsMap[key]; + if (mappedNotification?.account) { + mappedNotification._accounts.push(account); + } else { + let n = (notificationsMap[key] = { + ...notification, + _accounts: [account], + }); + cleanNotifications[j++] = n; + } + } + return cleanNotifications; +} + export default memo(Notifications); diff --git a/src/utils/states.js b/src/utils/states.js index faf1f87b..97a07501 100644 --- a/src/utils/states.js +++ b/src/utils/states.js @@ -9,8 +9,10 @@ const states = proxy({ home: [], specialHome: [], homeNew: [], + homeLast: null, // Last item in 'home' list homeLastFetchTime: null, notifications: [], + notificationLast: null, // Last item in 'notifications' list notificationsNew: [], notificationsLastFetchTime: null, accounts: {},