From e0bab6c70a18a320008faf654074474ab510a67c Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 3 Feb 2023 21:08:08 +0800 Subject: [PATCH] More refactoring work --- src/app.jsx | 2 + src/components/timeline.jsx | 209 +++++++++++++++++++++++++-------- src/pages/account-statuses.jsx | 6 +- src/pages/bookmarks.jsx | 2 + src/pages/favourites.jsx | 2 + src/pages/following.jsx | 32 +++++ src/pages/hashtags.jsx | 2 + src/pages/home.jsx | 110 +++++++++-------- src/pages/lists.jsx | 3 + src/pages/public.jsx | 5 +- src/utils/useScroll.js | 10 +- 11 files changed, 281 insertions(+), 102 deletions(-) create mode 100644 src/pages/following.jsx diff --git a/src/app.jsx b/src/app.jsx index 9240841..99d5165 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -26,6 +26,7 @@ import NotFound from './pages/404'; import AccountStatuses from './pages/account-statuses'; import Bookmarks from './pages/bookmarks'; import Favourites from './pages/favourites'; +import Following from './pages/following'; import Hashtags from './pages/hashtags'; import Home from './pages/home'; import Lists from './pages/lists'; @@ -205,6 +206,7 @@ function App() { {isLoggedIn && ( } /> )} + {isLoggedIn && } />} {isLoggedIn && } />} {isLoggedIn && } />} {isLoggedIn && } />} diff --git a/src/components/timeline.jsx b/src/components/timeline.jsx index baaca09..c4c1ca7 100644 --- a/src/components/timeline.jsx +++ b/src/components/timeline.jsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'preact/hooks'; +import { useDebouncedCallback } from 'use-debounce'; import useScroll from '../utils/useScroll'; -import useTitle from '../utils/useTitle'; import Icon from './icon'; import Link from './link'; @@ -11,45 +11,55 @@ import Status from './status'; function Timeline({ title, titleComponent, - path, id, emptyText, errorText, + boostsCarousel, fetchItems = () => {}, }) { - if (title) { - useTitle(title, path); - } const [items, setItems] = useState([]); const [uiState, setUIState] = useState('default'); const [showMore, setShowMore] = useState(false); const scrollableRef = useRef(null); - const { nearReachEnd, reachStart } = useScroll({ + const { nearReachEnd, reachStart, reachEnd } = useScroll({ scrollableElement: scrollableRef.current, + distanceFromEnd: 1, }); - const loadItems = (firstLoad) => { - setUIState('loading'); - (async () => { - try { - const { done, value } = await fetchItems(firstLoad); - if (value?.length) { - if (firstLoad) { - setItems(value); + const loadItems = useDebouncedCallback( + (firstLoad) => { + if (uiState === 'loading') return; + setUIState('loading'); + (async () => { + try { + let { done, value } = await fetchItems(firstLoad); + if (value?.length) { + if (boostsCarousel) { + value = groupBoosts(value); + } + console.log(value); + if (firstLoad) { + setItems(value); + } else { + setItems([...items, ...value]); + } + setShowMore(!done); } else { - setItems([...items, ...value]); + setShowMore(false); } - setShowMore(!done); - } else { - setShowMore(false); + setUIState('default'); + } catch (e) { + console.error(e); + setUIState('error'); } - setUIState('default'); - } catch (e) { - console.error(e); - setUIState('error'); - } - })(); - }; + })(); + }, + 1500, + { + leading: true, + trailing: false, + }, + ); useEffect(() => { scrollableRef.current?.scrollTo({ top: 0 }); @@ -63,7 +73,7 @@ function Timeline({ }, [reachStart]); useEffect(() => { - if (nearReachEnd && showMore) { + if (nearReachEnd || (reachEnd && showMore)) { loadItems(); } }, [nearReachEnd, showMore]); @@ -100,8 +110,15 @@ function Timeline({ <>
    {items.map((status) => { - const { id: statusID, reblog } = status; + const { id: statusID, reblog, boosts } = status; const actualStatusID = reblog?.id || statusID; + if (boosts) { + return ( +
  • + +
  • + ); + } return (
  • @@ -111,21 +128,19 @@ function Timeline({ ); })}
- {showMore && ( - - )} + {uiState === 'default' && + (showMore ? ( + + ) : ( +

The end.

+ ))} ) : uiState === 'loading' ? (
    @@ -136,9 +151,9 @@ function Timeline({ ))}
) : ( - uiState !== 'loading' &&

{emptyText}

+ uiState !== 'error' &&

{emptyText}

)} - {uiState === 'error' ? ( + {uiState === 'error' && (

{errorText}
@@ -150,14 +165,112 @@ function Timeline({ Try again

- ) : ( - uiState !== 'loading' && - !!items.length && - !showMore &&

The end.

)} ); } +function groupBoosts(values) { + let newValues = []; + let boostStash = []; + let serialBoosts = 0; + for (let i = 0; i < values.length; i++) { + const item = values[i]; + if (item.reblog) { + boostStash.push(item); + serialBoosts++; + } else { + newValues.push(item); + if (serialBoosts < 3) { + serialBoosts = 0; + } + } + } + // if boostStash is more than quarter of values + // or if there are 3 or more boosts in a row + if (boostStash.length > values.length / 4 || serialBoosts >= 3) { + // if boostStash is more than 3 quarter of values + const boostStashID = boostStash.map((status) => status.id); + if (boostStash.length > (values.length * 3) / 4) { + // insert boost array at the end of specialHome list + newValues = [...newValues, { id: boostStashID, boosts: boostStash }]; + } else { + // insert boosts array in the middle of specialHome list + const half = Math.floor(newValues.length / 2); + newValues = [ + ...newValues.slice(0, half), + { + id: boostStashID, + boosts: boostStash, + }, + ...newValues.slice(half), + ]; + } + return newValues; + } else { + return values; + } +} + +function BoostsCarousel({ boosts }) { + const carouselRef = useRef(); + const { reachStart, reachEnd, init } = useScroll({ + scrollableElement: carouselRef.current, + direction: 'horizontal', + }); + useEffect(() => { + init?.(); + }, []); + + return ( + + ); +} + export default Timeline; diff --git a/src/pages/account-statuses.jsx b/src/pages/account-statuses.jsx index 69a8aca..0b144a5 100644 --- a/src/pages/account-statuses.jsx +++ b/src/pages/account-statuses.jsx @@ -1,12 +1,15 @@ import { useEffect, useRef, useState } from 'preact/hooks'; import { useParams } from 'react-router-dom'; +import { useSnapshot } from 'valtio'; import Timeline from '../components/timeline'; import states from '../utils/states'; +import useTitle from '../utils/useTitle'; const LIMIT = 20; function AccountStatuses() { + const snapStates = useSnapshot(states); const { id } = useParams(); const accountStatusesIterator = useRef(); async function fetchAccountStatuses(firstLoad) { @@ -19,6 +22,7 @@ function AccountStatuses() { } const [account, setAccount] = useState({}); + useTitle(`${account?.acct ? '@' + account.acct : 'Posts'}`, '/a/:id'); useEffect(() => { (async () => { try { @@ -48,11 +52,11 @@ function AccountStatuses() { } - path="/a/:id" id="account_statuses" emptyText="Nothing to see here yet." errorText="Unable to load statuses" fetchItems={fetchAccountStatuses} + boostsCarousel={snapStates.settings.boostsCarousel} /> ); } diff --git a/src/pages/bookmarks.jsx b/src/pages/bookmarks.jsx index fb25f9d..8a5b1af 100644 --- a/src/pages/bookmarks.jsx +++ b/src/pages/bookmarks.jsx @@ -1,10 +1,12 @@ import { useRef } from 'preact/hooks'; import Timeline from '../components/timeline'; +import useTitle from '../utils/useTitle'; const LIMIT = 20; function Bookmarks() { + useTitle('Bookmarks', '/b'); const bookmarksIterator = useRef(); async function fetchBookmarks(firstLoad) { if (firstLoad || !bookmarksIterator.current) { diff --git a/src/pages/favourites.jsx b/src/pages/favourites.jsx index 6143283..4080c8b 100644 --- a/src/pages/favourites.jsx +++ b/src/pages/favourites.jsx @@ -1,10 +1,12 @@ import { useRef } from 'preact/hooks'; import Timeline from '../components/timeline'; +import useTitle from '../utils/useTitle'; const LIMIT = 20; function Favourites() { + useTitle('Favourites', '/f'); const favouritesIterator = useRef(); async function fetchFavourites(firstLoad) { if (firstLoad || !favouritesIterator.current) { diff --git a/src/pages/following.jsx b/src/pages/following.jsx new file mode 100644 index 0000000..015fae6 --- /dev/null +++ b/src/pages/following.jsx @@ -0,0 +1,32 @@ +import { useRef } from 'preact/hooks'; +import { useSnapshot } from 'valtio'; + +import Timeline from '../components/timeline'; +import useTitle from '../utils/useTitle'; + +const LIMIT = 20; + +function Following() { + useTitle('Following', '/l/f'); + const snapStates = useSnapshot(states); + const homeIterator = useRef(); + async function fetchHome(firstLoad) { + if (firstLoad || !homeIterator.current) { + homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT }); + } + return await homeIterator.current.next(); + } + + return ( + + ); +} + +export default Following; diff --git a/src/pages/hashtags.jsx b/src/pages/hashtags.jsx index efd145b..61bab69 100644 --- a/src/pages/hashtags.jsx +++ b/src/pages/hashtags.jsx @@ -2,11 +2,13 @@ import { useRef } from 'preact/hooks'; import { useParams } from 'react-router-dom'; import Timeline from '../components/timeline'; +import useTitle from '../utils/useTitle'; const LIMIT = 20; function Hashtags() { const { hashtag } = useParams(); + useTitle(`#${hashtag}`, `/t/${hashtag}`); const hashtagsIterator = useRef(); async function fetchHashtags(firstLoad) { if (firstLoad || !hashtagsIterator.current) { diff --git a/src/pages/home.jsx b/src/pages/home.jsx index 741e782..3facbb0 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -118,28 +118,28 @@ function Home({ hidden }) { return allStatuses; } - const loadingStatuses = useRef(false); - const loadStatuses = (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; - } - })(); - }; - const debouncedLoadStatuses = useDebouncedCallback(loadStatuses, 3000, { - leading: true, - trailing: false, - }); + 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); @@ -271,7 +271,6 @@ function Home({ hidden }) { reachEnd, } = useScroll({ scrollableElement: scrollableRef.current, - distanceFromStart: 1, distanceFromEnd: 3, scrollThresholdStart: 44, }); @@ -284,7 +283,7 @@ function Home({ hidden }) { useEffect(() => { if (reachStart) { - debouncedLoadStatuses(true); + loadStatuses(true); } }, [reachStart]); @@ -324,7 +323,7 @@ function Home({ hidden }) { scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); }} onDblClick={() => { - debouncedLoadStatuses(true); + loadStatuses(true); }} >
@@ -372,7 +371,7 @@ function Home({ hidden }) { ); states.home.unshift(...uniqueHomeNew); } - debouncedLoadStatuses(true); + loadStatuses(true); states.homeNew = []; scrollableRef.current?.scrollTo({ @@ -404,7 +403,7 @@ function Home({ hidden }) { ); })} - {showMore && ( + {showMore && uiState === 'loading' && ( <>
diff --git a/src/pages/lists.jsx b/src/pages/lists.jsx index b5b0ce4..ee24bc1 100644 --- a/src/pages/lists.jsx +++ b/src/pages/lists.jsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'preact/hooks'; import { useParams } from 'react-router-dom'; import Timeline from '../components/timeline'; +import useTitle from '../utils/useTitle'; const LIMIT = 20; @@ -18,6 +19,7 @@ function Lists() { } const [title, setTitle] = useState(`List ${id}`); + useTitle(title, `/l/${id}`); useEffect(() => { (async () => { try { @@ -36,6 +38,7 @@ function Lists() { emptyText="Nothing yet." errorText="Unable to load posts." fetchItems={fetchLists} + boostsCarousel /> ); } diff --git a/src/pages/public.jsx b/src/pages/public.jsx index 0696326..50e2deb 100644 --- a/src/pages/public.jsx +++ b/src/pages/public.jsx @@ -2,6 +2,7 @@ import { useMatch, useParams } from 'react-router-dom'; import Timeline from '../components/timeline'; +import useTitle from '../utils/useTitle'; const LIMIT = 20; @@ -11,6 +12,8 @@ function Public() { const isLocal = !!useMatch('/p/l/:instance'); const params = useParams(); const { instance = '' } = params; + const title = `${instance} (${isLocal ? 'local' : 'federated'})`; + useTitle(title, `/p/${instance}`); async function fetchPublic(firstLoad) { const url = firstLoad ? `https://${instance}/api/v1/timelines/public?limit=${LIMIT}&local=${isLocal}` @@ -37,7 +40,7 @@ function Public() { return ( =