More refactoring work

pull/54/head
Lim Chee Aun 2023-02-03 21:08:08 +08:00
rodzic 016aea711b
commit e0bab6c70a
11 zmienionych plików z 281 dodań i 102 usunięć

Wyświetl plik

@ -26,6 +26,7 @@ import NotFound from './pages/404';
import AccountStatuses from './pages/account-statuses'; import AccountStatuses from './pages/account-statuses';
import Bookmarks from './pages/bookmarks'; import Bookmarks from './pages/bookmarks';
import Favourites from './pages/favourites'; import Favourites from './pages/favourites';
import Following from './pages/following';
import Hashtags from './pages/hashtags'; import Hashtags from './pages/hashtags';
import Home from './pages/home'; import Home from './pages/home';
import Lists from './pages/lists'; import Lists from './pages/lists';
@ -205,6 +206,7 @@ function App() {
{isLoggedIn && ( {isLoggedIn && (
<Route path="/notifications" element={<Notifications />} /> <Route path="/notifications" element={<Notifications />} />
)} )}
{isLoggedIn && <Route path="/l/f" element={<Following />} />}
{isLoggedIn && <Route path="/b" element={<Bookmarks />} />} {isLoggedIn && <Route path="/b" element={<Bookmarks />} />}
{isLoggedIn && <Route path="/f" element={<Favourites />} />} {isLoggedIn && <Route path="/f" element={<Favourites />} />}
{isLoggedIn && <Route path="/l/:id" element={<Lists />} />} {isLoggedIn && <Route path="/l/:id" element={<Lists />} />}

Wyświetl plik

@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import { useDebouncedCallback } from 'use-debounce';
import useScroll from '../utils/useScroll'; import useScroll from '../utils/useScroll';
import useTitle from '../utils/useTitle';
import Icon from './icon'; import Icon from './icon';
import Link from './link'; import Link from './link';
@ -11,45 +11,55 @@ import Status from './status';
function Timeline({ function Timeline({
title, title,
titleComponent, titleComponent,
path,
id, id,
emptyText, emptyText,
errorText, errorText,
boostsCarousel,
fetchItems = () => {}, fetchItems = () => {},
}) { }) {
if (title) {
useTitle(title, path);
}
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(null);
const { nearReachEnd, reachStart } = useScroll({ const { nearReachEnd, reachStart, reachEnd } = useScroll({
scrollableElement: scrollableRef.current, scrollableElement: scrollableRef.current,
distanceFromEnd: 1,
}); });
const loadItems = (firstLoad) => { const loadItems = useDebouncedCallback(
setUIState('loading'); (firstLoad) => {
(async () => { if (uiState === 'loading') return;
try { setUIState('loading');
const { done, value } = await fetchItems(firstLoad); (async () => {
if (value?.length) { try {
if (firstLoad) { let { done, value } = await fetchItems(firstLoad);
setItems(value); if (value?.length) {
if (boostsCarousel) {
value = groupBoosts(value);
}
console.log(value);
if (firstLoad) {
setItems(value);
} else {
setItems([...items, ...value]);
}
setShowMore(!done);
} else { } else {
setItems([...items, ...value]); setShowMore(false);
} }
setShowMore(!done); setUIState('default');
} else { } catch (e) {
setShowMore(false); console.error(e);
setUIState('error');
} }
setUIState('default'); })();
} catch (e) { },
console.error(e); 1500,
setUIState('error'); {
} leading: true,
})(); trailing: false,
}; },
);
useEffect(() => { useEffect(() => {
scrollableRef.current?.scrollTo({ top: 0 }); scrollableRef.current?.scrollTo({ top: 0 });
@ -63,7 +73,7 @@ function Timeline({
}, [reachStart]); }, [reachStart]);
useEffect(() => { useEffect(() => {
if (nearReachEnd && showMore) { if (nearReachEnd || (reachEnd && showMore)) {
loadItems(); loadItems();
} }
}, [nearReachEnd, showMore]); }, [nearReachEnd, showMore]);
@ -100,8 +110,15 @@ function Timeline({
<> <>
<ul class="timeline"> <ul class="timeline">
{items.map((status) => { {items.map((status) => {
const { id: statusID, reblog } = status; const { id: statusID, reblog, boosts } = status;
const actualStatusID = reblog?.id || statusID; const actualStatusID = reblog?.id || statusID;
if (boosts) {
return (
<li key={`timeline-${statusID}`}>
<BoostsCarousel boosts={boosts} />
</li>
);
}
return ( return (
<li key={`timeline-${statusID}`}> <li key={`timeline-${statusID}`}>
<Link class="status-link" to={`/s/${actualStatusID}`}> <Link class="status-link" to={`/s/${actualStatusID}`}>
@ -111,21 +128,19 @@ function Timeline({
); );
})} })}
</ul> </ul>
{showMore && ( {uiState === 'default' &&
<button (showMore ? (
type="button" <button
class="plain block" type="button"
disabled={uiState === 'loading'} class="plain block"
onClick={() => loadItems()} onClick={() => loadItems()}
style={{ marginBlockEnd: '6em' }} style={{ marginBlockEnd: '6em' }}
> >
{uiState === 'loading' ? ( Show more&hellip;
<Loader abrupt /> </button>
) : ( ) : (
<>Show more&hellip;</> <p class="ui-state insignificant">The end.</p>
)} ))}
</button>
)}
</> </>
) : uiState === 'loading' ? ( ) : uiState === 'loading' ? (
<ul class="timeline"> <ul class="timeline">
@ -136,9 +151,9 @@ function Timeline({
))} ))}
</ul> </ul>
) : ( ) : (
uiState !== 'loading' && <p class="ui-state">{emptyText}</p> uiState !== 'error' && <p class="ui-state">{emptyText}</p>
)} )}
{uiState === 'error' ? ( {uiState === 'error' && (
<p class="ui-state"> <p class="ui-state">
{errorText} {errorText}
<br /> <br />
@ -150,14 +165,112 @@ function Timeline({
Try again Try again
</button> </button>
</p> </p>
) : (
uiState !== 'loading' &&
!!items.length &&
!showMore && <p class="ui-state insignificant">The end.</p>
)} )}
</div> </div>
</div> </div>
); );
} }
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 (
<div class="boost-carousel">
<header>
<h3>{boosts.length} Boosts</h3>
<span>
<button
type="button"
class="small plain2"
disabled={reachStart}
onClick={() => {
carouselRef.current?.scrollBy({
left: -Math.min(320, carouselRef.current?.offsetWidth),
behavior: 'smooth',
});
}}
>
<Icon icon="chevron-left" />
</button>{' '}
<button
type="button"
class="small plain2"
disabled={reachEnd}
onClick={() => {
carouselRef.current?.scrollBy({
left: Math.min(320, carouselRef.current?.offsetWidth),
behavior: 'smooth',
});
}}
>
<Icon icon="chevron-right" />
</button>
</span>
</header>
<ul ref={carouselRef}>
{boosts.map((boost) => {
const { id: statusID, reblog } = boost;
const actualStatusID = reblog?.id || statusID;
return (
<li key={statusID}>
<Link class="status-boost-link" to={`/s/${actualStatusID}`}>
<Status status={boost} size="s" />
</Link>
</li>
);
})}
</ul>
</div>
);
}
export default Timeline; export default Timeline;

Wyświetl plik

@ -1,12 +1,15 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useSnapshot } from 'valtio';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import states from '../utils/states'; import states from '../utils/states';
import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
function AccountStatuses() { function AccountStatuses() {
const snapStates = useSnapshot(states);
const { id } = useParams(); const { id } = useParams();
const accountStatusesIterator = useRef(); const accountStatusesIterator = useRef();
async function fetchAccountStatuses(firstLoad) { async function fetchAccountStatuses(firstLoad) {
@ -19,6 +22,7 @@ function AccountStatuses() {
} }
const [account, setAccount] = useState({}); const [account, setAccount] = useState({});
useTitle(`${account?.acct ? '@' + account.acct : 'Posts'}`, '/a/:id');
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
@ -48,11 +52,11 @@ function AccountStatuses() {
</div> </div>
</h1> </h1>
} }
path="/a/:id"
id="account_statuses" id="account_statuses"
emptyText="Nothing to see here yet." emptyText="Nothing to see here yet."
errorText="Unable to load statuses" errorText="Unable to load statuses"
fetchItems={fetchAccountStatuses} fetchItems={fetchAccountStatuses}
boostsCarousel={snapStates.settings.boostsCarousel}
/> />
); );
} }

Wyświetl plik

@ -1,10 +1,12 @@
import { useRef } from 'preact/hooks'; import { useRef } from 'preact/hooks';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
function Bookmarks() { function Bookmarks() {
useTitle('Bookmarks', '/b');
const bookmarksIterator = useRef(); const bookmarksIterator = useRef();
async function fetchBookmarks(firstLoad) { async function fetchBookmarks(firstLoad) {
if (firstLoad || !bookmarksIterator.current) { if (firstLoad || !bookmarksIterator.current) {

Wyświetl plik

@ -1,10 +1,12 @@
import { useRef } from 'preact/hooks'; import { useRef } from 'preact/hooks';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
function Favourites() { function Favourites() {
useTitle('Favourites', '/f');
const favouritesIterator = useRef(); const favouritesIterator = useRef();
async function fetchFavourites(firstLoad) { async function fetchFavourites(firstLoad) {
if (firstLoad || !favouritesIterator.current) { if (firstLoad || !favouritesIterator.current) {

Wyświetl plik

@ -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 (
<Timeline
title="Following"
id="following"
emptyText="Nothing to see here."
errorText="Unable to load posts."
fetchItems={fetchHome}
boostsCarousel={snapStates.settings.boostsCarousel}
/>
);
}
export default Following;

Wyświetl plik

@ -2,11 +2,13 @@ import { useRef } from 'preact/hooks';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
function Hashtags() { function Hashtags() {
const { hashtag } = useParams(); const { hashtag } = useParams();
useTitle(`#${hashtag}`, `/t/${hashtag}`);
const hashtagsIterator = useRef(); const hashtagsIterator = useRef();
async function fetchHashtags(firstLoad) { async function fetchHashtags(firstLoad) {
if (firstLoad || !hashtagsIterator.current) { if (firstLoad || !hashtagsIterator.current) {

Wyświetl plik

@ -118,28 +118,28 @@ function Home({ hidden }) {
return allStatuses; return allStatuses;
} }
const loadingStatuses = useRef(false); const loadStatuses = useDebouncedCallback(
const loadStatuses = (firstLoad) => { (firstLoad) => {
if (loadingStatuses.current) return; if (uiState === 'loading') return;
loadingStatuses.current = true; setUIState('loading');
setUIState('loading'); (async () => {
(async () => { try {
try { const { done } = await fetchStatuses(firstLoad);
const { done } = await fetchStatuses(firstLoad); setShowMore(!done);
setShowMore(!done); setUIState('default');
setUIState('default'); } catch (e) {
} catch (e) { console.warn(e);
console.warn(e); setUIState('error');
setUIState('error'); } finally {
} finally { }
loadingStatuses.current = false; })();
} },
})(); 1500,
}; {
const debouncedLoadStatuses = useDebouncedCallback(loadStatuses, 3000, { leading: true,
leading: true, trailing: false,
trailing: false, },
}); );
useEffect(() => { useEffect(() => {
loadStatuses(true); loadStatuses(true);
@ -271,7 +271,6 @@ function Home({ hidden }) {
reachEnd, reachEnd,
} = useScroll({ } = useScroll({
scrollableElement: scrollableRef.current, scrollableElement: scrollableRef.current,
distanceFromStart: 1,
distanceFromEnd: 3, distanceFromEnd: 3,
scrollThresholdStart: 44, scrollThresholdStart: 44,
}); });
@ -284,7 +283,7 @@ function Home({ hidden }) {
useEffect(() => { useEffect(() => {
if (reachStart) { if (reachStart) {
debouncedLoadStatuses(true); loadStatuses(true);
} }
}, [reachStart]); }, [reachStart]);
@ -324,7 +323,7 @@ function Home({ hidden }) {
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
onDblClick={() => { onDblClick={() => {
debouncedLoadStatuses(true); loadStatuses(true);
}} }}
> >
<div class="header-side"> <div class="header-side">
@ -372,7 +371,7 @@ function Home({ hidden }) {
); );
states.home.unshift(...uniqueHomeNew); states.home.unshift(...uniqueHomeNew);
} }
debouncedLoadStatuses(true); loadStatuses(true);
states.homeNew = []; states.homeNew = [];
scrollableRef.current?.scrollTo({ scrollableRef.current?.scrollTo({
@ -404,7 +403,7 @@ function Home({ hidden }) {
</li> </li>
); );
})} })}
{showMore && ( {showMore && uiState === 'loading' && (
<> <>
<li <li
style={{ style={{
@ -423,34 +422,45 @@ function Home({ hidden }) {
</> </>
)} )}
</ul> </ul>
</> {uiState === 'default' &&
) : ( (showMore ? (
<>
{uiState === 'loading' && (
<ul class="timeline">
{Array.from({ length: 5 }).map((_, i) => (
<li key={i}>
<Status skeleton />
</li>
))}
</ul>
)}
{uiState === 'error' && (
<p class="ui-state">
Unable to load statuses
<br />
<br />
<button <button
type="button" type="button"
onClick={() => { class="plain block"
debouncedLoadStatuses(true); onClick={() => loadStatuses()}
}} style={{ marginBlockEnd: '6em' }}
> >
Try again Show more&hellip;
</button> </button>
</p> ) : (
)} <p class="ui-state insignificant">The end.</p>
))}
</> </>
) : uiState === 'loading' ? (
<ul class="timeline">
{Array.from({ length: 5 }).map((_, i) => (
<li key={i}>
<Status skeleton />
</li>
))}
</ul>
) : (
uiState !== 'error' && <p class="ui-state">Nothing to see here.</p>
)}
{uiState === 'error' && (
<p class="ui-state">
Unable to load statuses
<br />
<br />
<button
type="button"
onClick={() => {
loadStatuses(true);
}}
>
Try again
</button>
</p>
)} )}
</div> </div>
</div> </div>

Wyświetl plik

@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
@ -18,6 +19,7 @@ function Lists() {
} }
const [title, setTitle] = useState(`List ${id}`); const [title, setTitle] = useState(`List ${id}`);
useTitle(title, `/l/${id}`);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
@ -36,6 +38,7 @@ function Lists() {
emptyText="Nothing yet." emptyText="Nothing yet."
errorText="Unable to load posts." errorText="Unable to load posts."
fetchItems={fetchLists} fetchItems={fetchLists}
boostsCarousel
/> />
); );
} }

Wyświetl plik

@ -2,6 +2,7 @@
import { useMatch, useParams } from 'react-router-dom'; import { useMatch, useParams } from 'react-router-dom';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
@ -11,6 +12,8 @@ function Public() {
const isLocal = !!useMatch('/p/l/:instance'); const isLocal = !!useMatch('/p/l/:instance');
const params = useParams(); const params = useParams();
const { instance = '' } = params; const { instance = '' } = params;
const title = `${instance} (${isLocal ? 'local' : 'federated'})`;
useTitle(title, `/p/${instance}`);
async function fetchPublic(firstLoad) { async function fetchPublic(firstLoad) {
const url = firstLoad const url = firstLoad
? `https://${instance}/api/v1/timelines/public?limit=${LIMIT}&local=${isLocal}` ? `https://${instance}/api/v1/timelines/public?limit=${LIMIT}&local=${isLocal}`
@ -37,7 +40,7 @@ function Public() {
return ( return (
<Timeline <Timeline
key={instance + isLocal} key={instance + isLocal}
title={`${instance} (${isLocal ? 'local' : 'federated'})`} title={title}
id="public" id="public"
emptyText="No one has posted anything yet." emptyText="No one has posted anything yet."
errorText="Unable to load posts" errorText="Unable to load posts"

Wyświetl plik

@ -38,8 +38,14 @@ export default function useScroll({
const scrollDimension = isVertical ? scrollHeight : scrollWidth; const scrollDimension = isVertical ? scrollHeight : scrollWidth;
const clientDimension = isVertical ? clientHeight : clientWidth; const clientDimension = isVertical ? clientHeight : clientWidth;
const scrollDistance = Math.abs(scrollStart - previousScrollStart); const scrollDistance = Math.abs(scrollStart - previousScrollStart);
const distanceFromStartPx = clientDimension * distanceFromStart; const distanceFromStartPx = Math.min(
const distanceFromEndPx = clientDimension * distanceFromEnd; clientDimension * distanceFromStart,
scrollDimension,
);
const distanceFromEndPx = Math.min(
clientDimension * distanceFromEnd,
scrollDimension,
);
if ( if (
scrollDistance >= scrollDistance >=