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 db from '../utils/db'; import states, { saveStatus } from '../utils/states'; import { getCurrentAccountNS } from '../utils/store-utils'; 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) => { saveStatus(status); 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]); 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; } } })(); }, []); return ( ); } export default memo(Home);