mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 14:16:39 +01:00
More refactoring work
This commit is contained in:
parent
016aea711b
commit
e0bab6c70a
11 changed files with 281 additions and 102 deletions
|
@ -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 />} />}
|
||||
|
|
|
@ -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…</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{uiState === 'default' &&
|
||||
(showMore ? (
|
||||
<button
|
||||
type="button"
|
||||
class="plain block"
|
||||
onClick={() => loadItems()}
|
||||
style={{ marginBlockEnd: '6em' }}
|
||||
>
|
||||
Show more…
|
||||
</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;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
32
src/pages/following.jsx
Normal file
32
src/pages/following.jsx
Normal file
|
@ -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;
|
|
@ -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) {
|
||||
|
|
|
@ -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…
|
||||
</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>
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 >=
|
||||
|
|
Loading…
Reference in a new issue