kopia lustrzana https://github.com/cheeaun/phanpy
Posts timeline for trending links
Timeline logic changed slightly, so might be buggy.pull/466/merge
rodzic
4be88da1d6
commit
21bdb6afc1
19
public/sw.js
19
public/sw.js
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 && <>· </>}
|
||||
{!!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 && <>· </>}
|
||||
{!!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
|
||||
|
|
Ładowanie…
Reference in New Issue