mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 14:16:39 +01:00
More ports to reusable Timeline component
- use status id instead of status, for "auto-update" feature - hot keys!
This commit is contained in:
parent
db428c04d1
commit
9992299716
2 changed files with 138 additions and 14 deletions
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
import useScroll from '../utils/useScroll';
|
import useScroll from '../utils/useScroll';
|
||||||
|
@ -15,17 +16,14 @@ function Timeline({
|
||||||
instance,
|
instance,
|
||||||
emptyText,
|
emptyText,
|
||||||
errorText,
|
errorText,
|
||||||
|
useItemID, // use statusID instead of status object, assuming it's already in states
|
||||||
boostsCarousel,
|
boostsCarousel,
|
||||||
fetchItems = () => {},
|
fetchItems = () => {},
|
||||||
}) {
|
}) {
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
const scrollableRef = useRef(null);
|
const scrollableRef = useRef();
|
||||||
const { nearReachEnd, reachStart, reachEnd } = useScroll({
|
|
||||||
scrollableElement: scrollableRef.current,
|
|
||||||
distanceFromEnd: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadItems = useDebouncedCallback(
|
const loadItems = useDebouncedCallback(
|
||||||
(firstLoad) => {
|
(firstLoad) => {
|
||||||
|
@ -62,6 +60,99 @@ function Timeline({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const itemsSelector = '.timeline-item, .timeline-item-alt';
|
||||||
|
|
||||||
|
const jRef = useHotkeys('j, shift+j', (_, handler) => {
|
||||||
|
// focus on next status after active item
|
||||||
|
const activeItem = document.activeElement.closest(itemsSelector);
|
||||||
|
const activeItemRect = activeItem?.getBoundingClientRect();
|
||||||
|
const allItems = Array.from(
|
||||||
|
scrollableRef.current.querySelectorAll(itemsSelector),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
activeItem &&
|
||||||
|
activeItemRect.top < scrollableRef.current.clientHeight &&
|
||||||
|
activeItemRect.bottom > 0
|
||||||
|
) {
|
||||||
|
const activeItemIndex = allItems.indexOf(activeItem);
|
||||||
|
let nextItem = allItems[activeItemIndex + 1];
|
||||||
|
if (handler.shift) {
|
||||||
|
// get next status that's not .timeline-item-alt
|
||||||
|
nextItem = allItems.find(
|
||||||
|
(item, index) =>
|
||||||
|
index > activeItemIndex &&
|
||||||
|
!item.classList.contains('timeline-item-alt'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (nextItem) {
|
||||||
|
nextItem.focus();
|
||||||
|
nextItem.scrollIntoViewIfNeeded?.();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If active status is not in viewport, get the topmost status-link in viewport
|
||||||
|
const topmostItem = allItems.find((item) => {
|
||||||
|
const itemRect = item.getBoundingClientRect();
|
||||||
|
return itemRect.top >= 44 && itemRect.left >= 0; // 44 is the magic number for header height, not real
|
||||||
|
});
|
||||||
|
if (topmostItem) {
|
||||||
|
topmostItem.focus();
|
||||||
|
topmostItem.scrollIntoViewIfNeeded?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const kRef = useHotkeys('k, shift+k', (_, handler) => {
|
||||||
|
// focus on previous status after active item
|
||||||
|
const activeItem = document.activeElement.closest(itemsSelector);
|
||||||
|
const activeItemRect = activeItem?.getBoundingClientRect();
|
||||||
|
const allItems = Array.from(
|
||||||
|
scrollableRef.current.querySelectorAll(itemsSelector),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
activeItem &&
|
||||||
|
activeItemRect.top < scrollableRef.current.clientHeight &&
|
||||||
|
activeItemRect.bottom > 0
|
||||||
|
) {
|
||||||
|
const activeItemIndex = allItems.indexOf(activeItem);
|
||||||
|
let prevItem = allItems[activeItemIndex - 1];
|
||||||
|
if (handler.shift) {
|
||||||
|
// get prev status that's not .timeline-item-alt
|
||||||
|
prevItem = allItems.findLast(
|
||||||
|
(item, index) =>
|
||||||
|
index < activeItemIndex &&
|
||||||
|
!item.classList.contains('timeline-item-alt'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (prevItem) {
|
||||||
|
prevItem.focus();
|
||||||
|
prevItem.scrollIntoViewIfNeeded?.();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If active status is not in viewport, get the topmost status-link in viewport
|
||||||
|
const topmostItem = allItems.find((item) => {
|
||||||
|
const itemRect = item.getBoundingClientRect();
|
||||||
|
return itemRect.top >= 44 && itemRect.left >= 0; // 44 is the magic number for header height, not real
|
||||||
|
});
|
||||||
|
if (topmostItem) {
|
||||||
|
topmostItem.focus();
|
||||||
|
topmostItem.scrollIntoViewIfNeeded?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const oRef = useHotkeys(['enter', 'o'], () => {
|
||||||
|
// open active status
|
||||||
|
const activeItem = document.activeElement.closest(itemsSelector);
|
||||||
|
if (activeItem) {
|
||||||
|
activeItem.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { nearReachEnd, reachStart, reachEnd } = useScroll({
|
||||||
|
scrollableElement: scrollableRef.current,
|
||||||
|
distanceFromEnd: 1,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollableRef.current?.scrollTo({ top: 0 });
|
scrollableRef.current?.scrollTo({ top: 0 });
|
||||||
loadItems(true);
|
loadItems(true);
|
||||||
|
@ -83,7 +174,12 @@ function Timeline({
|
||||||
<div
|
<div
|
||||||
id={`${id}-page`}
|
id={`${id}-page`}
|
||||||
class="deck-container"
|
class="deck-container"
|
||||||
ref={scrollableRef}
|
ref={(node) => {
|
||||||
|
scrollableRef.current = node;
|
||||||
|
jRef.current = node;
|
||||||
|
kRef.current = node;
|
||||||
|
oRef.current = node;
|
||||||
|
}}
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
>
|
>
|
||||||
<div class="timeline-deck deck">
|
<div class="timeline-deck deck">
|
||||||
|
@ -119,14 +215,22 @@ function Timeline({
|
||||||
if (boosts) {
|
if (boosts) {
|
||||||
return (
|
return (
|
||||||
<li key={`timeline-${statusID}`}>
|
<li key={`timeline-${statusID}`}>
|
||||||
<BoostsCarousel boosts={boosts} instance={instance} />
|
<BoostsCarousel
|
||||||
|
boosts={boosts}
|
||||||
|
useItemID={useItemID}
|
||||||
|
instance={instance}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li key={`timeline-${statusID}`}>
|
<li key={`timeline-${statusID}`}>
|
||||||
<Link class="status-link" to={url}>
|
<Link class="status-link timeline-item" to={url}>
|
||||||
<Status status={status} instance={instance} />
|
{useItemID ? (
|
||||||
|
<Status statusID={statusID} instance={instance} />
|
||||||
|
) : (
|
||||||
|
<Status status={status} instance={instance} />
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -217,7 +321,7 @@ function groupBoosts(values) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function BoostsCarousel({ boosts, instance }) {
|
function BoostsCarousel({ boosts, useItemID, instance }) {
|
||||||
const carouselRef = useRef();
|
const carouselRef = useRef();
|
||||||
const { reachStart, reachEnd, init } = useScroll({
|
const { reachStart, reachEnd, init } = useScroll({
|
||||||
scrollableElement: carouselRef.current,
|
scrollableElement: carouselRef.current,
|
||||||
|
@ -269,8 +373,12 @@ function BoostsCarousel({ boosts, instance }) {
|
||||||
: `/s/${actualStatusID}`;
|
: `/s/${actualStatusID}`;
|
||||||
return (
|
return (
|
||||||
<li key={statusID}>
|
<li key={statusID}>
|
||||||
<Link class="status-boost-link" to={url}>
|
<Link class="status-boost-link timeline-item-alt" to={url}>
|
||||||
<Status status={boost} instance={instance} size="s" />
|
{useItemID ? (
|
||||||
|
<Status statusID={statusID} instance={instance} size="s" />
|
||||||
|
) : (
|
||||||
|
<Status status={boost} instance={instance} size="s" />
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,20 +4,35 @@ import { useSnapshot } from 'valtio';
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
import { saveStatus } from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
|
||||||
function Following() {
|
function Following() {
|
||||||
useTitle('Following', '/l/f');
|
useTitle('Following', '/l/f');
|
||||||
const { masto } = api();
|
const { masto, instance } = api();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const homeIterator = useRef();
|
const homeIterator = useRef();
|
||||||
async function fetchHome(firstLoad) {
|
async function fetchHome(firstLoad) {
|
||||||
if (firstLoad || !homeIterator.current) {
|
if (firstLoad || !homeIterator.current) {
|
||||||
homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT });
|
homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT });
|
||||||
}
|
}
|
||||||
return await homeIterator.current.next();
|
const results = await homeIterator.current.next();
|
||||||
|
const { value } = results;
|
||||||
|
if (value?.length) {
|
||||||
|
value.forEach((item) => {
|
||||||
|
saveStatus(item, instance);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ENFORCE sort by datetime (Latest first)
|
||||||
|
value.sort((a, b) => {
|
||||||
|
const aDate = new Date(a.createdAt);
|
||||||
|
const bDate = new Date(b.createdAt);
|
||||||
|
return bDate - aDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -27,6 +42,7 @@ function Following() {
|
||||||
emptyText="Nothing to see here."
|
emptyText="Nothing to see here."
|
||||||
errorText="Unable to load posts."
|
errorText="Unable to load posts."
|
||||||
fetchItems={fetchHome}
|
fetchItems={fetchHome}
|
||||||
|
useItemID
|
||||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue