Posts timeline for trending links

Timeline logic changed slightly, so might be buggy.
This commit is contained in:
Lim Chee Aun 2024-06-19 12:22:17 +08:00
parent 4be88da1d6
commit 21bdb6afc1
6 changed files with 308 additions and 75 deletions

View file

@ -96,6 +96,25 @@ const apiExtendedRoute = new RegExpRoute(
);
registerRoute(apiExtendedRoute);
const apiIntermediateRoute = new RegExpRoute(
// Matches:
// - trends/*
// - timelines/link
/^https?:\/\/[^\/]+\/api\/v\d+\/(trends|timelines\/link)/,
new StaleWhileRevalidate({
cacheName: 'api-intermediate',
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 10 * 60, // 10 minutes
}),
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
}),
);
registerRoute(apiIntermediateRoute);
const apiRoute = new RegExpRoute(
// Matches:
// - statuses/:id/context - some contexts are really huge

View file

@ -95,6 +95,29 @@
filter: brightness(0.8);
}
figure {
transition: 1s ease-out;
transition-property: opacity, mix-blend-mode;
}
&.inactive:not(:active, :hover) {
figure {
transition-duration: 0.3s;
opacity: 0.5;
mix-blend-mode: luminosity;
}
}
&.active {
border-color: var(--accent-color, var(--link-light-color));
height: 100%;
max-height: 100%;
+ button[disabled] {
display: none;
}
}
article {
width: 100%;
display: flex;

View file

@ -55,6 +55,7 @@ function Timeline({
filterContext,
showFollowedTags,
showReplyParent,
clearWhenRefresh,
}) {
const snapStates = useSnapshot(states);
const [items, setItems] = useState([]);
@ -69,14 +70,17 @@ function Timeline({
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
const allowGrouping = view !== 'media';
const loadItemsTS = useRef(0); // Ensures only one loadItems at a time
const loadItems = useDebouncedCallback(
(firstLoad) => {
setShowNew(false);
if (uiState === 'loading') return;
// if (uiState === 'loading') return;
setUIState('loading');
(async () => {
try {
const ts = (loadItemsTS.current = Date.now());
let { done, value } = await fetchItems(firstLoad);
if (ts !== loadItemsTS.current) return;
if (Array.isArray(value)) {
// Avoid grouping for pinned posts
const [pinnedPosts, otherPosts] = value.reduce(
@ -120,10 +124,10 @@ function Timeline({
}
})();
},
1500,
1_000,
{
leading: true,
trailing: false,
// trailing: false,
},
);
@ -273,9 +277,18 @@ function Timeline({
scrollableRef.current?.scrollTo({ top: 0 });
loadItems(true);
}, []);
const firstLoad = useRef(true);
useEffect(() => {
if (firstLoad.current) {
firstLoad.current = false;
return;
}
if (clearWhenRefresh && items?.length) {
loadItems.cancel?.();
setItems([]);
}
loadItems(true);
}, [refresh]);
}, [clearWhenRefresh, refresh]);
// useEffect(() => {
// if (reachStart) {

View file

@ -2,5 +2,6 @@
"@mastodon/edit-media-attributes": ">=4.1",
"@mastodon/list-exclusive": ">=4.2",
"@mastodon/filtered-notifications": "~4.3 || >=4.3",
"@mastodon/fetch-multiple-statuses": "~4.3 || >=4.3"
"@mastodon/fetch-multiple-statuses": "~4.3 || >=4.3",
"@mastodon/trending-link-posts": "~4.3 || >=4.3"
}

55
src/pages/trending.css Normal file
View file

@ -0,0 +1,55 @@
#trending-page {
.timeline-header-block {
display: flex;
gap: 12px;
align-items: center;
padding: 16px;
&.blended {
background-image: linear-gradient(
to bottom,
var(--bg-faded-color),
transparent
);
}
@media (min-width: 40em) {
padding: 0 16px;
}
&.loading {
color: var(--text-insignificant-color);
}
p {
margin: 0;
padding: 0;
flex-grow: 1;
min-width: 0;
}
.link-text {
color: var(--text-insignificant-color);
display: block;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.9em;
}
}
.timeline {
transition: opacity 0.3s ease-in-out;
}
.timeline.loading {
pointer-events: none;
opacity: 0.2;
}
.timeline-link-mentions {
.status .card {
display: none;
}
}
}

View file

@ -1,14 +1,16 @@
import '../components/links-bar.css';
import './trending.css';
import { MenuItem } from '@szhsin/react-menu';
import { getBlurHashAverageColor } from 'fast-blurhash';
import { useMemo, useRef, useState } from 'preact/hooks';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import punycode from 'punycode/';
import { useNavigate, useParams } from 'react-router-dom';
import { useSnapshot } from 'valtio';
import Icon from '../components/icon';
import Link from '../components/link';
import Loader from '../components/loader';
import Menu2 from '../components/menu2';
import RelativeTime from '../components/relative-time';
import Timeline from '../components/timeline';
@ -23,18 +25,27 @@ import supports from '../utils/supports';
import useTitle from '../utils/useTitle';
const LIMIT = 20;
const TREND_CACHE_TIME = 10 * 60 * 1000; // 10 minutes
const fetchLinks = pmem(
(masto) => {
return masto.v1.trends.links.list().next();
},
{
// News last much longer
maxAge: 10 * 60 * 1000, // 10 minutes
maxAge: TREND_CACHE_TIME,
},
);
function fetchTrends(masto) {
const fetchHashtags = pmem(
(masto) => {
return masto.v1.trends.tags.list().next();
},
{
maxAge: TREND_CACHE_TIME,
},
);
function fetchTrendsStatuses(masto) {
if (supports('@pixelfed/trending')) {
return masto.pixelfed.v2.discover.posts.trending.list({
range: 'daily',
@ -45,6 +56,10 @@ function fetchTrends(masto) {
});
}
function fetchLinkList(masto, params) {
return masto.v1.timelines.link.list(params);
}
function Trending({ columnMode, ...props }) {
const snapStates = useSnapshot(states);
const params = columnMode ? {} : useParams();
@ -61,15 +76,16 @@ function Trending({ columnMode, ...props }) {
const [links, setLinks] = useState([]);
const trendIterator = useRef();
async function fetchTrend(firstLoad) {
async function fetchTrends(firstLoad) {
console.log('fetchTrend', firstLoad);
if (firstLoad || !trendIterator.current) {
trendIterator.current = fetchTrends(masto);
trendIterator.current = fetchTrendsStatuses(masto);
// Get hashtags
if (supports('@mastodon/trending-hashtags')) {
try {
const iterator = masto.v1.trends.tags.list();
const { value: tags } = await iterator.next();
// const iterator = masto.v1.trends.tags.list();
const { value: tags } = await fetchHashtags(masto);
console.log('tags', tags);
if (tags?.length) {
setHashtags(tags);
@ -113,6 +129,52 @@ function Trending({ columnMode, ...props }) {
};
}
// Link mentions
// https://github.com/mastodon/mastodon/pull/30381
const [currentLinkMentionsLoading, setCurrentLinkMentionsLoading] =
useState(false);
const currentLinkMentionsIterator = useRef();
const [currentLink, setCurrentLink] = useState(null);
const hasCurrentLink = !!currentLink;
const currentLinkRef = useRef();
const supportsTrendingLinkPosts = supports('@mastodon/trending-hashtags');
useEffect(() => {
if (currentLink && currentLinkRef.current) {
currentLinkRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center',
});
}
}, [currentLink]);
const prevCurrentLink = useRef();
async function fetchLinkMentions(firstLoad) {
if (firstLoad || !currentLinkMentionsIterator.current) {
setCurrentLinkMentionsLoading(true);
currentLinkMentionsIterator.current = fetchLinkList(masto, {
url: currentLink,
});
}
prevCurrentLink.current = currentLink;
const results = await currentLinkMentionsIterator.current.next();
let { value } = results;
if (value?.length) {
value = filteredItems(value, 'public');
value.forEach((item) => {
saveStatus(item, instance);
});
}
if (prevCurrentLink.current === currentLink) {
setCurrentLinkMentionsLoading(false);
}
return {
...results,
value,
};
}
async function checkForUpdates() {
try {
const results = await masto.v1.trends.statuses
@ -194,77 +256,134 @@ function Trending({ columnMode, ...props }) {
}
return (
<a
key={url}
href={url}
target="_blank"
rel="noopener noreferrer"
style={
accentColor
? {
'--accent-color': `rgb(${accentColor.join(',')})`,
'--accent-alpha-color': `rgba(${accentColor.join(
',',
)}, 0.4)`,
}
: {}
}
>
<article>
<figure>
<img
src={image}
alt={imageDescription}
width={width}
height={height}
loading="lazy"
/>
</figure>
<div class="article-body">
<header>
<div class="article-meta">
<span class="domain">{domain}</span>{' '}
{!!publishedAt && <>&middot; </>}
{!!publishedAt && (
<>
<RelativeTime
datetime={publishedAt}
format="micro"
/>
</>
<div key={url}>
<a
ref={currentLink === url ? currentLinkRef : null}
href={url}
target="_blank"
rel="noopener noreferrer"
class={
hasCurrentLink
? currentLink === url
? 'active'
: 'inactive'
: ''
}
style={
accentColor
? {
'--accent-color': `rgb(${accentColor.join(',')})`,
'--accent-alpha-color': `rgba(${accentColor.join(
',',
)}, 0.4)`,
}
: {}
}
>
<article>
<figure>
<img
src={image}
alt={imageDescription}
width={width}
height={height}
loading="lazy"
/>
</figure>
<div class="article-body">
<header>
<div class="article-meta">
<span class="domain">{domain}</span>{' '}
{!!publishedAt && <>&middot; </>}
{!!publishedAt && (
<>
<RelativeTime
datetime={publishedAt}
format="micro"
/>
</>
)}
</div>
{!!title && (
<h1
class="title"
lang={language}
dir="auto"
title={title}
>
{title}
</h1>
)}
</div>
{!!title && (
<h1
class="title"
</header>
{!!description && (
<p
class="description"
lang={language}
dir="auto"
title={title}
title={description}
>
{title}
</h1>
{description}
</p>
)}
</header>
{!!description && (
<p
class="description"
lang={language}
dir="auto"
title={description}
>
{description}
</p>
)}
</div>
</article>
</a>
</div>
</article>
</a>
{supportsTrendingLinkPosts && (
<button
type="button"
class="small plain4 block"
onClick={() => {
setCurrentLink(url);
}}
disabled={url === currentLink}
>
<Icon icon="comment2" /> <span>Mentions</span>{' '}
<Icon icon="chevron-down" />
</button>
)}
</div>
);
})}
</div>
)}
{supportsTrendingLinkPosts && !!links.length && (
<div
class={`timeline-header-block ${hasCurrentLink ? 'blended' : ''}`}
>
{hasCurrentLink ? (
<>
<div style={{ width: 50, flexShrink: 0, textAlign: 'center' }}>
{currentLinkMentionsLoading ? (
<Loader abrupt />
) : (
<button
type="button"
class="light"
onClick={() => {
setCurrentLink(null);
}}
>
<Icon icon="x" />
</button>
)}
</div>
<p>
Showing posts mentioning{' '}
<span class="link-text">
{currentLink
.replace(/^https?:\/\/(www\.)?/i, '')
.replace(/\/$/, '')}
</span>
</p>
</>
) : (
<p class="insignificant">Trending posts</p>
)}
</div>
)}
</>
);
}, [hashtags, links]);
}, [hashtags, links, currentLink, currentLinkMentionsLoading]);
return (
<Timeline
@ -280,8 +399,8 @@ function Trending({ columnMode, ...props }) {
instance={instance}
emptyText="No trending posts."
errorText="Unable to load posts"
fetchItems={fetchTrend}
checkForUpdates={checkForUpdates}
fetchItems={hasCurrentLink ? fetchLinkMentions : fetchTrends}
checkForUpdates={hasCurrentLink ? undefined : checkForUpdates}
checkForUpdatesInterval={5 * 60 * 1000} // 5 minutes
useItemID
headerStart={<></>}
@ -289,6 +408,9 @@ function Trending({ columnMode, ...props }) {
// allowFilters
filterContext="public"
timelineStart={TimelineStart}
refresh={currentLink}
clearWhenRefresh
view={hasCurrentLink ? 'link-mentions' : undefined}
headerEnd={
<Menu2
portal