2022-12-10 12:14:48 +03:00
|
|
|
import { Link } from 'preact-router/match';
|
2023-01-07 09:45:04 +03:00
|
|
|
import { memo } from 'preact/compat';
|
2022-12-10 12:14:48 +03:00
|
|
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
2022-12-31 04:52:31 +03:00
|
|
|
import { useHotkeys } from 'react-hotkeys-hook';
|
2022-12-10 12:14:48 +03:00
|
|
|
import { useSnapshot } from 'valtio';
|
|
|
|
|
|
|
|
import Icon from '../components/icon';
|
|
|
|
import Loader from '../components/loader';
|
|
|
|
import Status from '../components/status';
|
2023-01-09 14:11:34 +03:00
|
|
|
import states, { saveStatus } from '../utils/states';
|
2023-01-02 16:36:24 +03:00
|
|
|
import useDebouncedCallback from '../utils/useDebouncedCallback';
|
|
|
|
import useScroll from '../utils/useScroll';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
|
|
const LIMIT = 20;
|
|
|
|
|
2022-12-16 08:27:04 +03:00
|
|
|
function Home({ hidden }) {
|
2022-12-10 12:14:48 +03:00
|
|
|
const snapStates = useSnapshot(states);
|
|
|
|
const [uiState, setUIState] = useState('default');
|
|
|
|
const [showMore, setShowMore] = useState(false);
|
|
|
|
|
2023-01-07 15:26:23 +03:00
|
|
|
console.debug('RENDER Home');
|
|
|
|
|
2022-12-18 06:52:53 +03:00
|
|
|
const homeIterator = useRef(
|
2022-12-25 18:28:55 +03:00
|
|
|
masto.v1.timelines.listHome({
|
2022-12-10 12:14:48 +03:00
|
|
|
limit: LIMIT,
|
|
|
|
}),
|
2022-12-25 18:28:55 +03:00
|
|
|
);
|
2022-12-10 12:14:48 +03:00
|
|
|
async function fetchStatuses(firstLoad) {
|
2022-12-25 18:28:55 +03:00
|
|
|
if (firstLoad) {
|
|
|
|
// Reset iterator
|
|
|
|
homeIterator.current = masto.v1.timelines.listHome({
|
|
|
|
limit: LIMIT,
|
|
|
|
});
|
2023-01-02 19:27:47 +03:00
|
|
|
states.homeNew = [];
|
2022-12-25 18:28:55 +03:00
|
|
|
}
|
|
|
|
const allStatuses = await homeIterator.current.next();
|
2022-12-10 12:14:48 +03:00
|
|
|
if (allStatuses.value <= 0) {
|
|
|
|
return { done: true };
|
|
|
|
}
|
|
|
|
const homeValues = allStatuses.value.map((status) => {
|
2023-01-09 14:11:34 +03:00
|
|
|
saveStatus(status);
|
2022-12-10 12:14:48 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-01-02 16:36:24 +03:00
|
|
|
const loadingStatuses = useRef(false);
|
|
|
|
const loadStatuses = useDebouncedCallback((firstLoad) => {
|
|
|
|
if (loadingStatuses.current) return;
|
|
|
|
loadingStatuses.current = true;
|
2022-12-10 12:14:48 +03:00
|
|
|
setUIState('loading');
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
const { done } = await fetchStatuses(firstLoad);
|
|
|
|
setShowMore(!done);
|
|
|
|
setUIState('default');
|
|
|
|
} catch (e) {
|
|
|
|
console.warn(e);
|
|
|
|
setUIState('error');
|
2023-01-02 16:36:24 +03:00
|
|
|
} finally {
|
|
|
|
loadingStatuses.current = false;
|
2022-12-10 12:14:48 +03:00
|
|
|
}
|
|
|
|
})();
|
2023-01-02 16:36:24 +03:00
|
|
|
}, 1000);
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
loadStatuses(true);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const scrollableRef = useRef();
|
|
|
|
|
2022-12-31 04:52:31 +03:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-01-02 16:36:24 +03:00
|
|
|
const { scrollDirection, reachTop, nearReachTop, nearReachBottom } =
|
|
|
|
useScroll({
|
|
|
|
scrollableElement: scrollableRef.current,
|
2023-01-05 10:29:11 +03:00
|
|
|
distanceFromTop: 0.1,
|
|
|
|
distanceFromBottom: 0.15,
|
2023-01-07 09:45:04 +03:00
|
|
|
scrollThresholdUp: 44,
|
2023-01-02 16:36:24 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (nearReachBottom && showMore) {
|
|
|
|
loadStatuses();
|
|
|
|
}
|
|
|
|
}, [nearReachBottom]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (reachTop) {
|
|
|
|
loadStatuses(true);
|
|
|
|
}
|
|
|
|
}, [reachTop]);
|
|
|
|
|
2022-12-10 12:14:48 +03:00
|
|
|
return (
|
2022-12-29 11:11:58 +03:00
|
|
|
<div
|
2022-12-30 15:37:57 +03:00
|
|
|
id="home-page"
|
2022-12-29 11:11:58 +03:00
|
|
|
class="deck-container"
|
|
|
|
hidden={hidden}
|
|
|
|
ref={scrollableRef}
|
|
|
|
tabIndex="-1"
|
|
|
|
>
|
2023-01-02 16:36:24 +03:00
|
|
|
<button
|
|
|
|
hidden={scrollDirection === 'down' && !nearReachTop}
|
|
|
|
type="button"
|
|
|
|
id="compose-button"
|
|
|
|
onClick={(e) => {
|
|
|
|
if (e.shiftKey) {
|
|
|
|
const newWin = openCompose();
|
|
|
|
if (!newWin) {
|
|
|
|
alert('Looks like your browser is blocking popups.');
|
|
|
|
states.showCompose = true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
states.showCompose = true;
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="quill" size="xxl" alt="Compose" />
|
|
|
|
</button>
|
2022-12-10 12:14:48 +03:00
|
|
|
<div class="timeline-deck deck">
|
|
|
|
<header
|
2023-01-02 16:36:24 +03:00
|
|
|
hidden={scrollDirection === 'down' && !nearReachTop}
|
2022-12-10 12:14:48 +03:00
|
|
|
onClick={() => {
|
|
|
|
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
}}
|
2022-12-25 18:53:18 +03:00
|
|
|
onDblClick={() => {
|
|
|
|
loadStatuses(true);
|
|
|
|
}}
|
2022-12-10 12:14:48 +03:00
|
|
|
>
|
|
|
|
<div class="header-side">
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="plain"
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
states.showSettings = true;
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="gear" size="l" alt="Settings" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<h1>Home</h1>
|
|
|
|
<div class="header-side">
|
|
|
|
<Loader hidden={uiState !== 'loading'} />{' '}
|
|
|
|
<a
|
|
|
|
href="#/notifications"
|
|
|
|
class={`button plain ${
|
|
|
|
snapStates.notificationsNew.length > 0 ? 'has-badge' : ''
|
|
|
|
}`}
|
2022-12-10 18:45:52 +03:00
|
|
|
onClick={(e) => {
|
|
|
|
e.stopPropagation();
|
|
|
|
}}
|
2022-12-10 12:14:48 +03:00
|
|
|
>
|
|
|
|
<Icon icon="notification" size="l" alt="Notifications" />
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
</header>
|
2023-01-02 19:52:16 +03:00
|
|
|
{snapStates.homeNew.length > 0 &&
|
2023-01-05 10:29:11 +03:00
|
|
|
scrollDirection === 'up' &&
|
|
|
|
!nearReachTop &&
|
|
|
|
!nearReachBottom && (
|
2023-01-02 19:52:16 +03:00
|
|
|
<button
|
|
|
|
class="updates-button"
|
|
|
|
type="button"
|
|
|
|
onClick={() => {
|
|
|
|
const uniqueHomeNew = snapStates.homeNew.filter(
|
|
|
|
(status) => !states.home.some((s) => s.id === status.id),
|
|
|
|
);
|
|
|
|
states.home.unshift(...uniqueHomeNew);
|
|
|
|
loadStatuses(true);
|
|
|
|
states.homeNew = [];
|
2023-01-01 14:24:08 +03:00
|
|
|
|
2023-01-02 19:52:16 +03:00
|
|
|
scrollableRef.current?.scrollTo({
|
|
|
|
top: 0,
|
|
|
|
behavior: 'smooth',
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="arrow-up" /> New posts
|
|
|
|
</button>
|
|
|
|
)}
|
2022-12-10 12:14:48 +03:00
|
|
|
{snapStates.home.length ? (
|
|
|
|
<>
|
|
|
|
<ul class="timeline">
|
|
|
|
{snapStates.home.map(({ id: statusID, reblog }) => {
|
|
|
|
const actualStatusID = reblog || statusID;
|
|
|
|
return (
|
|
|
|
<li key={statusID}>
|
|
|
|
<Link
|
|
|
|
activeClassName="active"
|
|
|
|
class="status-link"
|
|
|
|
href={`#/s/${actualStatusID}`}
|
|
|
|
>
|
|
|
|
<Status statusID={statusID} />
|
|
|
|
</Link>
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
{showMore && (
|
2022-12-12 17:42:58 +03:00
|
|
|
<>
|
2023-01-02 16:36:24 +03:00
|
|
|
{/* <InView
|
2022-12-12 17:42:58 +03:00
|
|
|
as="li"
|
2022-12-10 12:14:48 +03:00
|
|
|
style={{
|
2022-12-12 17:42:58 +03:00
|
|
|
height: '20vh',
|
|
|
|
}}
|
|
|
|
onChange={(inView) => {
|
|
|
|
if (inView) loadStatuses();
|
2022-12-10 12:14:48 +03:00
|
|
|
}}
|
2022-12-31 04:52:31 +03:00
|
|
|
root={scrollableRef.current}
|
|
|
|
rootMargin="100px 0px"
|
2023-01-02 16:36:24 +03:00
|
|
|
> */}
|
2023-01-02 19:56:11 +03:00
|
|
|
<li
|
|
|
|
style={{
|
|
|
|
height: '20vh',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Status skeleton />
|
|
|
|
</li>
|
2023-01-02 16:36:24 +03:00
|
|
|
{/* </InView> */}
|
2022-12-10 12:14:48 +03:00
|
|
|
<li
|
|
|
|
style={{
|
|
|
|
height: '25vh',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Status skeleton />
|
|
|
|
</li>
|
2022-12-12 17:42:58 +03:00
|
|
|
</>
|
2022-12-10 12:14:48 +03:00
|
|
|
)}
|
|
|
|
</ul>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
{uiState === 'loading' && (
|
|
|
|
<ul class="timeline">
|
|
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
|
|
<li key={i}>
|
|
|
|
<Status skeleton />
|
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
)}
|
|
|
|
{uiState === 'error' && (
|
2023-01-01 10:28:07 +03:00
|
|
|
<p class="ui-state">
|
|
|
|
Unable to load statuses
|
|
|
|
<br />
|
|
|
|
<br />
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
onClick={() => {
|
|
|
|
loadStatuses(true);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Try again
|
|
|
|
</button>
|
|
|
|
</p>
|
2022-12-10 12:14:48 +03:00
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
2022-12-16 08:27:04 +03:00
|
|
|
}
|
|
|
|
|
2023-01-07 09:45:04 +03:00
|
|
|
export default memo(Home);
|