From 21bdb6afc19261a7d109402e839ae30df1d54e43 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 19 Jun 2024 12:22:17 +0800 Subject: [PATCH] Posts timeline for trending links Timeline logic changed slightly, so might be buggy. --- public/sw.js | 19 +++ src/components/links-bar.css | 23 +++ src/components/timeline.jsx | 21 ++- src/data/features.json | 3 +- src/pages/trending.css | 55 ++++++++ src/pages/trending.jsx | 262 +++++++++++++++++++++++++---------- 6 files changed, 308 insertions(+), 75 deletions(-) create mode 100644 src/pages/trending.css diff --git a/public/sw.js b/public/sw.js index 9cbb8223..cf945218 100644 --- a/public/sw.js +++ b/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 diff --git a/src/components/links-bar.css b/src/components/links-bar.css index f9668c89..d2b5b66a 100644 --- a/src/components/links-bar.css +++ b/src/components/links-bar.css @@ -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; diff --git a/src/components/timeline.jsx b/src/components/timeline.jsx index f8cdda47..d4696502 100644 --- a/src/components/timeline.jsx +++ b/src/components/timeline.jsx @@ -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) { diff --git a/src/data/features.json b/src/data/features.json index 0aba1ba3..bb8c2d37 100644 --- a/src/data/features.json +++ b/src/data/features.json @@ -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" } diff --git a/src/pages/trending.css b/src/pages/trending.css new file mode 100644 index 00000000..45ec7ee4 --- /dev/null +++ b/src/pages/trending.css @@ -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; + } + } +} diff --git a/src/pages/trending.jsx b/src/pages/trending.jsx index 2a284817..1b85de77 100644 --- a/src/pages/trending.jsx +++ b/src/pages/trending.jsx @@ -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 ( - -
-
- {imageDescription} -
-
-
-
- {!!description && ( -

- {description} -

- )} -
-
- + + + + {supportsTrendingLinkPosts && ( + + )} + ); })} )} + {supportsTrendingLinkPosts && !!links.length && ( +
+ {hasCurrentLink ? ( + <> +
+ {currentLinkMentionsLoading ? ( + + ) : ( + + )} +
+

+ Showing posts mentioning{' '} + + {currentLink + .replace(/^https?:\/\/(www\.)?/i, '') + .replace(/\/$/, '')} + +

+ + ) : ( +

Trending posts

+ )} +
+ )} ); - }, [hashtags, links]); + }, [hashtags, links, currentLink, currentLinkMentionsLoading]); return ( } @@ -289,6 +408,9 @@ function Trending({ columnMode, ...props }) { // allowFilters filterContext="public" timelineStart={TimelineStart} + refresh={currentLink} + clearWhenRefresh + view={hasCurrentLink ? 'link-mentions' : undefined} headerEnd={