mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-03-07 08:37:31 +03:00
516 lines
16 KiB
JavaScript
516 lines
16 KiB
JavaScript
import '../components/links-bar.css';
|
|
import './trending.css';
|
|
|
|
import { t, Trans } from '@lingui/macro';
|
|
import { MenuItem } from '@szhsin/react-menu';
|
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
|
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 NameText from '../components/name-text';
|
|
import RelativeTime from '../components/relative-time';
|
|
import Timeline from '../components/timeline';
|
|
import { api } from '../utils/api';
|
|
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
|
|
import { filteredItems } from '../utils/filters';
|
|
import pmem from '../utils/pmem';
|
|
import shortenNumber from '../utils/shorten-number';
|
|
import states, { saveStatus } from '../utils/states';
|
|
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();
|
|
},
|
|
{
|
|
maxAge: TREND_CACHE_TIME,
|
|
},
|
|
);
|
|
|
|
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',
|
|
});
|
|
}
|
|
return masto.v1.trends.statuses.list({
|
|
limit: LIMIT,
|
|
});
|
|
}
|
|
|
|
function fetchLinkList(masto, params) {
|
|
return masto.v1.timelines.link.list(params);
|
|
}
|
|
|
|
function Trending({ columnMode, ...props }) {
|
|
const snapStates = useSnapshot(states);
|
|
const params = columnMode ? {} : useParams();
|
|
const { masto, instance } = api({
|
|
instance: props?.instance || params.instance,
|
|
});
|
|
const { masto: currentMasto, instance: currentInstance } = api();
|
|
const title = t`Trending (${instance})`;
|
|
useTitle(title, `/:instance?/trending`);
|
|
// const navigate = useNavigate();
|
|
const latestItem = useRef();
|
|
|
|
const sameCurrentInstance = instance === currentInstance;
|
|
|
|
const [hashtags, setHashtags] = useState([]);
|
|
const [links, setLinks] = useState([]);
|
|
const trendIterator = useRef();
|
|
|
|
async function fetchTrends(firstLoad) {
|
|
console.log('fetchTrend', firstLoad);
|
|
if (firstLoad || !trendIterator.current) {
|
|
trendIterator.current = fetchTrendsStatuses(masto);
|
|
|
|
// Get hashtags
|
|
if (supports('@mastodon/trending-hashtags')) {
|
|
try {
|
|
// const iterator = masto.v1.trends.tags.list();
|
|
const { value: tags } = await fetchHashtags(masto);
|
|
console.log('tags', tags);
|
|
if (tags?.length) {
|
|
setHashtags(tags);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
// Get links
|
|
if (supports('@mastodon/trending-links')) {
|
|
try {
|
|
const { value } = await fetchLinks(masto, instance);
|
|
// 4 types available: link, photo, video, rich
|
|
// Only want links for now
|
|
const links = value?.filter?.((link) => link.type === 'link');
|
|
console.log('links', links);
|
|
if (links?.length) {
|
|
setLinks(links);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
const results = await trendIterator.current.next();
|
|
let { value } = results;
|
|
if (value?.length) {
|
|
if (firstLoad) {
|
|
latestItem.current = value[0].id;
|
|
}
|
|
|
|
// value = filteredItems(value, 'public'); // Might not work here
|
|
value.forEach((item) => {
|
|
saveStatus(item, instance);
|
|
});
|
|
}
|
|
return {
|
|
...results,
|
|
value,
|
|
};
|
|
}
|
|
|
|
// 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 =
|
|
sameCurrentInstance && supports('@mastodon/trending-link-posts');
|
|
|
|
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
|
|
.list({
|
|
limit: 1,
|
|
// NOT SUPPORTED
|
|
// since_id: latestItem.current,
|
|
})
|
|
.next();
|
|
let { value } = results;
|
|
value = filteredItems(value, 'public');
|
|
if (value?.length && value[0].id !== latestItem.current) {
|
|
latestItem.current = value[0].id;
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const TimelineStart = useMemo(() => {
|
|
return (
|
|
<>
|
|
{!!hashtags.length && (
|
|
<div class="filter-bar expandable">
|
|
<Icon icon="chart" class="insignificant" size="l" />
|
|
{hashtags.map((tag, i) => {
|
|
const { name, history } = tag;
|
|
const total = history.reduce((acc, cur) => acc + +cur.uses, 0);
|
|
return (
|
|
<Link to={`/${instance}/t/${name}`} key={name}>
|
|
<span dir="auto">
|
|
<span class="more-insignificant">#</span>
|
|
{name}
|
|
</span>
|
|
<span class="filter-count">{shortenNumber(total)}</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
{!!links.length && (
|
|
<div class="links-bar">
|
|
<header>
|
|
<h3>
|
|
<Trans>Trending News</Trans>
|
|
</h3>
|
|
</header>
|
|
{links.map((link) => {
|
|
const {
|
|
authors,
|
|
authorName,
|
|
authorUrl,
|
|
blurhash,
|
|
description,
|
|
height,
|
|
image,
|
|
imageDescription,
|
|
language,
|
|
providerName,
|
|
providerUrl,
|
|
publishedAt,
|
|
title,
|
|
url,
|
|
width,
|
|
} = link;
|
|
const author = authors?.[0]?.account?.id
|
|
? authors[0].account
|
|
: null;
|
|
const isShortTitle = title.length < 30;
|
|
const hasAuthor = !!(authorName || author);
|
|
const domain = punycode.toUnicode(
|
|
URL.parse(url)
|
|
.hostname.replace(/^www\./, '')
|
|
.replace(/\/$/, ''),
|
|
);
|
|
let accentColor;
|
|
if (blurhash) {
|
|
const averageColor = getBlurHashAverageColor(blurhash);
|
|
const labAverageColor = rgb2oklab(averageColor);
|
|
accentColor = oklab2rgb([
|
|
0.6,
|
|
labAverageColor[1],
|
|
labAverageColor[2],
|
|
]);
|
|
}
|
|
|
|
return (
|
|
<div key={url}>
|
|
<a
|
|
ref={currentLink === url ? currentLinkRef : null}
|
|
href={url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class={`link-block ${
|
|
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>
|
|
)}
|
|
</header>
|
|
{!!description && (
|
|
<p
|
|
class={`description ${
|
|
hasAuthor && !isShortTitle ? '' : 'more-lines'
|
|
}`}
|
|
lang={language}
|
|
dir="auto"
|
|
title={description}
|
|
>
|
|
{description}
|
|
</p>
|
|
)}
|
|
{hasAuthor && (
|
|
<>
|
|
<hr />
|
|
<p class="byline">
|
|
<small>
|
|
<Trans comment="By [Author]">
|
|
By{' '}
|
|
{author ? (
|
|
<NameText account={author} showAvatar />
|
|
) : authorUrl ? (
|
|
<a
|
|
href={authorUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{authorName}
|
|
</a>
|
|
) : (
|
|
authorName
|
|
)}
|
|
</Trans>
|
|
</small>
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</article>
|
|
</a>
|
|
{supportsTrendingLinkPosts && (
|
|
<button
|
|
type="button"
|
|
class="small plain4 block"
|
|
onClick={() => {
|
|
setCurrentLink(url);
|
|
}}
|
|
disabled={url === currentLink}
|
|
>
|
|
<Icon icon="comment2" />{' '}
|
|
<span>
|
|
<Trans>Mentions</Trans>
|
|
</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" alt={t`Back to showing trending posts`} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<p>
|
|
<Trans>
|
|
Showing posts mentioning{' '}
|
|
<span class="link-text">
|
|
{currentLink
|
|
.replace(/^https?:\/\/(www\.)?/i, '')
|
|
.replace(/\/$/, '')}
|
|
</span>
|
|
</Trans>
|
|
</p>
|
|
</>
|
|
) : (
|
|
<p class="insignificant">
|
|
<Trans>Trending posts</Trans>
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}, [hashtags, links, currentLink, currentLinkMentionsLoading]);
|
|
|
|
return (
|
|
<Timeline
|
|
key={instance}
|
|
title={title}
|
|
titleComponent={
|
|
<h1 class="header-double-lines">
|
|
<b>
|
|
<Trans>Trending</Trans>
|
|
</b>
|
|
<div>{instance}</div>
|
|
</h1>
|
|
}
|
|
id="trending"
|
|
instance={instance}
|
|
emptyText={t`No trending posts.`}
|
|
errorText={t`Unable to load posts`}
|
|
fetchItems={hasCurrentLink ? fetchLinkMentions : fetchTrends}
|
|
checkForUpdates={hasCurrentLink ? undefined : checkForUpdates}
|
|
checkForUpdatesInterval={5 * 60 * 1000} // 5 minutes
|
|
useItemID
|
|
headerStart={<></>}
|
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
|
// allowFilters
|
|
filterContext="public"
|
|
timelineStart={TimelineStart}
|
|
refresh={currentLink}
|
|
clearWhenRefresh
|
|
view={hasCurrentLink ? 'link-mentions' : undefined}
|
|
headerEnd={
|
|
<Menu2
|
|
portal
|
|
// setDownOverflow
|
|
overflow="auto"
|
|
viewScroll="close"
|
|
position="anchor"
|
|
menuButton={
|
|
<button type="button" class="plain">
|
|
<Icon icon="more" size="l" alt={t`More`} />
|
|
</button>
|
|
}
|
|
>
|
|
<MenuItem
|
|
onClick={() => {
|
|
let newInstance = prompt(
|
|
t`Enter a new instance e.g. "mastodon.social"`,
|
|
);
|
|
if (!/\./.test(newInstance)) {
|
|
if (newInstance) alert(t`Invalid instance`);
|
|
return;
|
|
}
|
|
if (newInstance) {
|
|
newInstance = newInstance.toLowerCase().trim();
|
|
// navigate(`/${newInstance}/trending`);
|
|
location.hash = `/${newInstance}/trending`;
|
|
}
|
|
}}
|
|
>
|
|
<Icon icon="bus" />{' '}
|
|
<span>
|
|
<Trans>Go to another instance…</Trans>
|
|
</span>
|
|
</MenuItem>
|
|
{currentInstance !== instance && (
|
|
<MenuItem
|
|
onClick={() => {
|
|
location.hash = `/${currentInstance}/trending`;
|
|
}}
|
|
>
|
|
<Icon icon="bus" />{' '}
|
|
<small class="menu-double-lines">
|
|
<Trans>
|
|
Go to my instance (<b>{currentInstance}</b>)
|
|
</Trans>
|
|
</small>
|
|
</MenuItem>
|
|
)}
|
|
</Menu2>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default Trending;
|