From 325bccf010b561cb3bb8c1f32edb9a426620971d Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 27 Mar 2023 15:05:50 +0800 Subject: [PATCH] Prevent re-render when page visibility changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also time to remove legacy homeV1 🙏 --- src/app.jsx | 179 +++++++------- src/pages/home-v1.jsx | 546 ------------------------------------------ 2 files changed, 87 insertions(+), 638 deletions(-) delete mode 100644 src/pages/home-v1.jsx diff --git a/src/app.jsx b/src/app.jsx index 77d2ce17..a94a9e76 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -33,7 +33,6 @@ import FollowedHashtags from './pages/followed-hashtags'; import Following from './pages/following'; import Hashtag from './pages/hashtag'; import Home from './pages/home'; -import HomeV1 from './pages/home-v1'; import List from './pages/list'; import Lists from './pages/lists'; import Login from './pages/login'; @@ -140,10 +139,6 @@ function App() { let location = useLocation(); states.currentLocation = location.pathname; - const locationDeckMap = { - '/': 'home-page', - '/notifications': 'notifications-page', - }; const focusDeck = () => { let timer = setTimeout(() => { const columns = document.getElementById('columns'); @@ -161,11 +156,6 @@ function App() { page.focus(); } } - // const page = document.getElementById(locationDeckMap[location.pathname]); - // console.debug('FOCUS', location.pathname, page); - // if (page) { - // page.focus(); - // } }, 100); return () => clearTimeout(timer); }; @@ -182,59 +172,6 @@ function App() { if (!showModal) focusDeck(); }, [showModal]); - // 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, instance } = 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); - if (notification.status) { - saveStatus(notification.status, instance, { - skipThreading: true, - }); - } - 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 = @@ -255,34 +192,6 @@ function App() { return !/^\/(login|welcome)/.test(pathname); }, [location]); - const lastCheckDate = useRef(); - const checkForUpdates = () => { - lastCheckDate.current = Date.now(); - console.log('✨ Check app update'); - fetch('./version.json') - .then((r) => r.json()) - .then((info) => { - if (info) states.appVersion = info; - }) - .catch((e) => { - console.error(e); - }); - }; - useInterval(() => checkForUpdates, visible && 1000 * 60 * 30); // 30 minutes - usePageVisibility((visible) => { - if (visible) { - if (!lastCheckDate.current) { - checkForUpdates(); - } else { - const diff = Date.now() - lastCheckDate.current; - if (diff > 1000 * 60 * 60) { - // 1 hour - checkForUpdates(); - } - } - } - }); - return ( <> @@ -306,7 +215,6 @@ function App() { } /> )} {isLoggedIn && } />} - {isLoggedIn && } />} {isLoggedIn && } />} {isLoggedIn && } />} {isLoggedIn && ( @@ -472,8 +380,95 @@ function App() { )} + ); } +function BackgroundService({ 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, instance } = 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); + if (notification.status) { + saveStatus(notification.status, instance, { + skipThreading: true, + }); + } + 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]); + + // Check for updates service + const lastCheckDate = useRef(); + const checkForUpdates = () => { + lastCheckDate.current = Date.now(); + console.log('✨ Check app update'); + fetch('./version.json') + .then((r) => r.json()) + .then((info) => { + if (info) states.appVersion = info; + }) + .catch((e) => { + console.error(e); + }); + }; + useInterval(() => checkForUpdates, visible && 1000 * 60 * 30); // 30 minutes + usePageVisibility((visible) => { + if (visible) { + if (!lastCheckDate.current) { + checkForUpdates(); + } else { + const diff = Date.now() - lastCheckDate.current; + if (diff > 1000 * 60 * 60) { + // 1 hour + checkForUpdates(); + } + } + } + }); + + return null; +} + export { App }; diff --git a/src/pages/home-v1.jsx b/src/pages/home-v1.jsx deleted file mode 100644 index 17fee040..00000000 --- a/src/pages/home-v1.jsx +++ /dev/null @@ -1,546 +0,0 @@ -import { memo } from 'preact/compat'; -import { useEffect, useRef, useState } from 'preact/hooks'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useDebouncedCallback } from 'use-debounce'; -import { useSnapshot } from 'valtio'; - -import Icon from '../components/icon'; -import Link from '../components/link'; -import Loader from '../components/loader'; -import Status from '../components/status'; -import { api } from '../utils/api'; -import db from '../utils/db'; -import states, { saveStatus } from '../utils/states'; -import { getCurrentAccountNS } from '../utils/store-utils'; -import useScroll from '../utils/useScroll'; -import useTitle from '../utils/useTitle'; - -const LIMIT = 20; - -function Home({ hidden }) { - useTitle('Home', '/'); - const { masto, instance } = api(); - const snapStates = useSnapshot(states); - const [uiState, setUIState] = useState('default'); - const [showMore, setShowMore] = useState(false); - - console.debug('RENDER Home'); - - const homeIterator = useRef(); - async function fetchStatuses(firstLoad) { - if (firstLoad) { - // Reset iterator - homeIterator.current = masto.v1.timelines.listHome({ - limit: LIMIT, - }); - states.homeNew = []; - } - const allStatuses = await homeIterator.current.next(); - if (allStatuses.value?.length) { - // ENFORCE sort by datetime (Latest first) - allStatuses.value.sort((a, b) => { - const aDate = new Date(a.createdAt); - const bDate = new Date(b.createdAt); - return bDate - aDate; - }); - const homeValues = allStatuses.value.map((status) => { - saveStatus(status, instance); - return { - id: status.id, - reblog: status.reblog?.id, - reply: !!status.inReplyToAccountId, - }; - }); - - // BOOSTS CAROUSEL - if (snapStates.settings.boostsCarousel) { - let specialHome = []; - let boostStash = []; - let serialBoosts = 0; - for (let i = 0; i < homeValues.length; i++) { - const status = homeValues[i]; - if (status.reblog) { - boostStash.push(status); - serialBoosts++; - } else { - specialHome.push(status); - if (serialBoosts < 3) { - serialBoosts = 0; - } - } - } - // if boostStash is more than quarter of homeValues - // or if there are 3 or more boosts in a row - if (boostStash.length > homeValues.length / 4 || serialBoosts >= 3) { - // if boostStash is more than 3 quarter of homeValues - const boostStashID = boostStash.map((status) => status.id); - if (boostStash.length > (homeValues.length * 3) / 4) { - // insert boost array at the end of specialHome list - specialHome = [ - ...specialHome, - { id: boostStashID, boosts: boostStash }, - ]; - } else { - // insert boosts array in the middle of specialHome list - const half = Math.floor(specialHome.length / 2); - specialHome = [ - ...specialHome.slice(0, half), - { - id: boostStashID, - boosts: boostStash, - }, - ...specialHome.slice(half), - ]; - } - } else { - // Untouched, this is fine - specialHome = homeValues; - } - console.log({ - 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); - } - } - } - - states.homeLastFetchTime = Date.now(); - return allStatuses; - } - - const loadStatuses = useDebouncedCallback( - (firstLoad) => { - if (uiState === 'loading') return; - setUIState('loading'); - (async () => { - try { - const { done } = await fetchStatuses(firstLoad); - setShowMore(!done); - setUIState('default'); - } catch (e) { - console.warn(e); - setUIState('error'); - } finally { - } - })(); - }, - 1500, - { - leading: true, - trailing: false, - }, - ); - - useEffect(() => { - loadStatuses(true); - }, []); - - const scrollableRef = useRef(); - - const jRef = useHotkeys('j, shift+j', (_, handler) => { - // focus on next status after active status - // Traverses .timeline li .status-link, focus on .status-link - const activeStatus = document.activeElement.closest( - '.status-link, .status-boost-link', - ); - const activeStatusRect = activeStatus?.getBoundingClientRect(); - const allStatusLinks = Array.from( - scrollableRef.current.querySelectorAll( - '.status-link, .status-boost-link', - ), - ); - if ( - activeStatus && - activeStatusRect.top < scrollableRef.current.clientHeight && - activeStatusRect.bottom > 0 - ) { - const activeStatusIndex = allStatusLinks.indexOf(activeStatus); - let nextStatus = allStatusLinks[activeStatusIndex + 1]; - if (handler.shift) { - // get next status that's not .status-boost-link - nextStatus = allStatusLinks.find( - (statusLink, index) => - index > activeStatusIndex && - !statusLink.classList.contains('status-boost-link'), - ); - } - if (nextStatus) { - nextStatus.focus(); - nextStatus.scrollIntoViewIfNeeded?.(); - } - } else { - // If active status is not in viewport, get the topmost status-link in viewport - const topmostStatusLink = allStatusLinks.find((statusLink) => { - const statusLinkRect = statusLink.getBoundingClientRect(); - return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real - }); - if (topmostStatusLink) { - topmostStatusLink.focus(); - topmostStatusLink.scrollIntoViewIfNeeded?.(); - } - } - }); - - const kRef = useHotkeys('k, shift+k', (_, handler) => { - // focus on previous status after active status - // Traverses .timeline li .status-link, focus on .status-link - const activeStatus = document.activeElement.closest( - '.status-link, .status-boost-link', - ); - const activeStatusRect = activeStatus?.getBoundingClientRect(); - const allStatusLinks = Array.from( - scrollableRef.current.querySelectorAll( - '.status-link, .status-boost-link', - ), - ); - if ( - activeStatus && - activeStatusRect.top < scrollableRef.current.clientHeight && - activeStatusRect.bottom > 0 - ) { - const activeStatusIndex = allStatusLinks.indexOf(activeStatus); - let prevStatus = allStatusLinks[activeStatusIndex - 1]; - if (handler.shift) { - // get prev status that's not .status-boost-link - prevStatus = allStatusLinks.findLast( - (statusLink, index) => - index < activeStatusIndex && - !statusLink.classList.contains('status-boost-link'), - ); - } - if (prevStatus) { - prevStatus.focus(); - prevStatus.scrollIntoViewIfNeeded?.(); - } - } else { - // If active status is not in viewport, get the topmost status-link in viewport - const topmostStatusLink = allStatusLinks.find((statusLink) => { - const statusLinkRect = statusLink.getBoundingClientRect(); - return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real - }); - if (topmostStatusLink) { - topmostStatusLink.focus(); - topmostStatusLink.scrollIntoViewIfNeeded?.(); - } - } - }); - - const oRef = useHotkeys(['enter', 'o'], () => { - // open active status - const activeStatus = document.activeElement.closest( - '.status-link, .status-boost-link', - ); - if (activeStatus) { - activeStatus.click(); - } - }); - - const { - scrollDirection, - reachStart, - nearReachStart, - nearReachEnd, - reachEnd, - } = useScroll({ - scrollableElement: scrollableRef.current, - distanceFromEnd: 3, - scrollThresholdStart: 44, - }); - - useEffect(() => { - if (nearReachEnd || (reachEnd && showMore)) { - loadStatuses(); - } - }, [nearReachEnd, reachEnd]); - - useEffect(() => { - if (reachStart) { - loadStatuses(true); - } - }, [reachStart]); - - useEffect(() => { - (async () => { - const keys = await db.drafts.keys(); - if (keys.length) { - const ns = getCurrentAccountNS(); - const ownKeys = keys.filter((key) => key.startsWith(ns)); - if (ownKeys.length) { - states.showDrafts = true; - } - } - })(); - }, []); - - // const showUpdatesButton = snapStates.homeNew.length > 0 && reachStart; - const [showUpdatesButton, setShowUpdatesButton] = useState(false); - useEffect(() => { - const isNewAndTop = snapStates.homeNew.length > 0 && reachStart; - console.log( - 'isNewAndTop', - isNewAndTop, - snapStates.homeNew.length, - reachStart, - ); - setShowUpdatesButton(isNewAndTop); - }, [snapStates.homeNew.length, reachStart]); - - return ( - <> - - - - ); -} - -function BoostsCarousel({ boosts }) { - const carouselRef = useRef(); - const { reachStart, reachEnd, init } = useScroll({ - scrollableElement: carouselRef.current, - direction: 'horizontal', - }); - useEffect(() => { - init?.(); - }, []); - - return ( - - ); -} - -export default memo(Home);