import { Link } from 'preact-router/match'; import { memo } from 'preact/compat'; import { useEffect, useRef, useState } from 'preact/hooks'; import { useHotkeys } from 'react-hotkeys-hook'; import { useSnapshot } from 'valtio'; import Icon from '../components/icon'; import Loader from '../components/loader'; import Status from '../components/status'; import states from '../utils/states'; import useDebouncedCallback from '../utils/useDebouncedCallback'; import useScroll from '../utils/useScroll'; const LIMIT = 20; function Home({ hidden }) { const snapStates = useSnapshot(states); const [uiState, setUIState] = useState('default'); const [showMore, setShowMore] = useState(false); console.debug('RENDER Home'); const homeIterator = useRef( masto.v1.timelines.listHome({ limit: LIMIT, }), ); 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 <= 0) { return { done: true }; } const homeValues = allStatuses.value.map((status) => { states.statuses[status.id] = status; if (status.reblog) { states.statuses[status.reblog.id] = status.reblog; } return { id: status.id, reblog: status.reblog?.id, reply: !!status.inReplyToAccountId, }; }); if (firstLoad) { states.home = homeValues; } else { states.home.push(...homeValues); } states.homeLastFetchTime = Date.now(); return allStatuses; } const loadingStatuses = useRef(false); const loadStatuses = useDebouncedCallback((firstLoad) => { if (loadingStatuses.current) return; loadingStatuses.current = true; setUIState('loading'); (async () => { try { const { done } = await fetchStatuses(firstLoad); setShowMore(!done); setUIState('default'); } catch (e) { console.warn(e); setUIState('error'); } finally { loadingStatuses.current = false; } })(); }, 1000); useEffect(() => { loadStatuses(true); }, []); const scrollableRef = useRef(); useHotkeys('j', () => { // focus on next status after active status // Traverses .timeline li .status-link, focus on .status-link const activeStatus = document.activeElement.closest('.status-link'); const activeStatusRect = activeStatus?.getBoundingClientRect(); if ( activeStatus && activeStatusRect.top < scrollableRef.current.clientHeight && activeStatusRect.bottom > 0 ) { const nextStatus = activeStatus.parentElement.nextElementSibling; if (nextStatus) { const statusLink = nextStatus.querySelector('.status-link'); if (statusLink) { statusLink.focus(); } } } else { // If active status is not in viewport, get the topmost status-link in viewport const statusLinks = document.querySelectorAll( '.timeline li .status-link', ); let topmostStatusLink; for (const statusLink of statusLinks) { const statusLinkRect = statusLink.getBoundingClientRect(); if (statusLinkRect.top >= 44) { // 44 is the magic number for header height, not real topmostStatusLink = statusLink; break; } } if (topmostStatusLink) { topmostStatusLink.focus(); } } }); useHotkeys('k', () => { // focus on previous status after active status // Traverses .timeline li .status-link, focus on .status-link const activeStatus = document.activeElement.closest('.status-link'); const activeStatusRect = activeStatus?.getBoundingClientRect(); if ( activeStatus && activeStatusRect.top < scrollableRef.current.clientHeight && activeStatusRect.bottom > 0 ) { const prevStatus = activeStatus.parentElement.previousElementSibling; if (prevStatus) { const statusLink = prevStatus.querySelector('.status-link'); if (statusLink) { statusLink.focus(); } } } else { // If active status is not in viewport, get the topmost status-link in viewport const statusLinks = document.querySelectorAll( '.timeline li .status-link', ); let topmostStatusLink; for (const statusLink of statusLinks) { const statusLinkRect = statusLink.getBoundingClientRect(); if (statusLinkRect.top >= 44) { // 44 is the magic number for header height, not real topmostStatusLink = statusLink; break; } } if (topmostStatusLink) { topmostStatusLink.focus(); } } }); useHotkeys(['enter', 'o'], () => { // open active status const activeStatus = document.activeElement.closest('.status-link'); if (activeStatus) { activeStatus.click(); } }); const { scrollDirection, reachTop, nearReachTop, nearReachBottom } = useScroll({ scrollableElement: scrollableRef.current, distanceFromTop: 0.1, distanceFromBottom: 0.15, scrollThresholdUp: 44, }); useEffect(() => { if (nearReachBottom && showMore) { loadStatuses(); } }, [nearReachBottom]); useEffect(() => { if (reachTop) { loadStatuses(true); } }, [reachTop]); return ( ); } export default memo(Home);