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 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 && (
<Route path="/notifications" element={<Notifications />} />
)}
{isLoggedIn && <Route path="/l/f" element={<Following />} />}
{isLoggedIn && <Route path="/b" element={<Bookmarks />} />}
{isLoggedIn && <Route path="/f" element={<Favourites />} />}
{isLoggedIn && <Route path="/l/:id" element={<Lists />} />}

Wyświetl plik

@ -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({
<>
<ul class="timeline">
{items.map((status) => {
const { id: statusID, reblog } = status;
const { id: statusID, reblog, boosts } = status;
const actualStatusID = reblog?.id || statusID;
if (boosts) {
return (
<li key={`timeline-${statusID}`}>
<BoostsCarousel boosts={boosts} />
</li>
);
}
return (
<li key={`timeline-${statusID}`}>
<Link class="status-link" to={`/s/${actualStatusID}`}>
@ -111,21 +128,19 @@ function Timeline({
);
})}
</ul>
{showMore && (
<button
type="button"
class="plain block"
disabled={uiState === 'loading'}
onClick={() => loadItems()}
style={{ marginBlockEnd: '6em' }}
>
{uiState === 'loading' ? (
<Loader abrupt />
) : (
<>Show more&hellip;</>
)}
</button>
)}
{uiState === 'default' &&
(showMore ? (
<button
type="button"
class="plain block"
onClick={() => loadItems()}
style={{ marginBlockEnd: '6em' }}
>
Show more&hellip;
</button>
) : (
<p class="ui-state insignificant">The end.</p>
))}
</>
) : uiState === 'loading' ? (
<ul class="timeline">
@ -136,9 +151,9 @@ function Timeline({
))}
</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">
{errorText}
<br />
@ -150,14 +165,112 @@ function Timeline({
Try again
</button>
</p>
) : (
uiState !== 'loading' &&
!!items.length &&
!showMore && <p class="ui-state insignificant">The end.</p>
)}
</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;

Wyświetl plik

@ -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() {
</div>
</h1>
}
path="/a/:id"
id="account_statuses"
emptyText="Nothing to see here yet."
errorText="Unable to load statuses"
fetchItems={fetchAccountStatuses}
boostsCarousel={snapStates.settings.boostsCarousel}
/>
);
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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) {

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 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) {

Wyświetl plik

@ -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);
}}
>
<div class="header-side">
@ -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 }) {
</li>
);
})}
{showMore && (
{showMore && uiState === 'loading' && (
<>
<li
style={{
@ -423,34 +422,45 @@ function Home({ hidden }) {
</>
)}
</ul>
</>
) : (
<>
{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 />
{uiState === 'default' &&
(showMore ? (
<button
type="button"
onClick={() => {
debouncedLoadStatuses(true);
}}
class="plain block"
onClick={() => loadStatuses()}
style={{ marginBlockEnd: '6em' }}
>
Try again
Show more&hellip;
</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>

Wyświetl plik

@ -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
/>
);
}

Wyświetl plik

@ -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 (
<Timeline
key={instance + isLocal}
title={`${instance} (${isLocal ? 'local' : 'federated'})`}
title={title}
id="public"
emptyText="No one has posted anything yet."
errorText="Unable to load posts"

Wyświetl plik

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