2023-10-22 14:40:46 +03:00
|
|
|
import './trending.css';
|
|
|
|
|
2023-10-03 10:07:47 +03:00
|
|
|
import { MenuItem } from '@szhsin/react-menu';
|
2023-10-22 14:40:46 +03:00
|
|
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
2023-06-27 14:39:33 +03:00
|
|
|
import { useMemo, useRef, useState } from 'preact/hooks';
|
2023-04-05 20:14:38 +03:00
|
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
|
|
import { useSnapshot } from 'valtio';
|
|
|
|
|
|
|
|
import Icon from '../components/icon';
|
2023-06-27 14:39:33 +03:00
|
|
|
import Link from '../components/link';
|
2023-06-13 12:46:37 +03:00
|
|
|
import Menu2 from '../components/menu2';
|
2023-10-22 14:40:46 +03:00
|
|
|
import RelativeTime from '../components/relative-time';
|
2023-04-05 20:14:38 +03:00
|
|
|
import Timeline from '../components/timeline';
|
|
|
|
import { api } from '../utils/api';
|
|
|
|
import { filteredItems } from '../utils/filters';
|
2023-10-22 14:40:46 +03:00
|
|
|
import pmem from '../utils/pmem';
|
2023-04-05 20:14:38 +03:00
|
|
|
import states from '../utils/states';
|
|
|
|
import { saveStatus } from '../utils/states';
|
|
|
|
import useTitle from '../utils/useTitle';
|
|
|
|
|
|
|
|
const LIMIT = 20;
|
|
|
|
|
2023-10-22 14:40:46 +03:00
|
|
|
const fetchLinks = pmem(
|
|
|
|
(masto) => {
|
|
|
|
return masto.v1.trends.links.list().next();
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// News last much longer
|
|
|
|
maxAge: 10 * 60 * 1000, // 10 minutes
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2023-07-05 11:59:28 +03:00
|
|
|
function Trending({ columnMode, ...props }) {
|
2023-04-05 20:14:38 +03:00
|
|
|
const snapStates = useSnapshot(states);
|
2023-07-05 11:59:28 +03:00
|
|
|
const params = columnMode ? {} : useParams();
|
2023-04-05 20:14:38 +03:00
|
|
|
const { masto, instance } = api({
|
|
|
|
instance: props?.instance || params.instance,
|
|
|
|
});
|
|
|
|
const title = `Trending (${instance})`;
|
|
|
|
useTitle(title, `/:instance?/trending`);
|
2023-07-05 11:59:28 +03:00
|
|
|
// const navigate = useNavigate();
|
2023-04-05 20:14:38 +03:00
|
|
|
const latestItem = useRef();
|
|
|
|
|
2023-06-27 14:39:33 +03:00
|
|
|
const [hashtags, setHashtags] = useState([]);
|
2023-10-22 14:40:46 +03:00
|
|
|
const [links, setLinks] = useState([]);
|
2023-04-05 20:14:38 +03:00
|
|
|
const trendIterator = useRef();
|
|
|
|
async function fetchTrend(firstLoad) {
|
|
|
|
if (firstLoad || !trendIterator.current) {
|
2023-10-12 07:48:09 +03:00
|
|
|
trendIterator.current = masto.v1.trends.statuses.list({
|
2023-04-05 20:14:38 +03:00
|
|
|
limit: LIMIT,
|
|
|
|
});
|
2023-06-27 14:39:33 +03:00
|
|
|
|
|
|
|
// Get hashtags
|
|
|
|
try {
|
2023-10-12 07:48:09 +03:00
|
|
|
const iterator = masto.v1.trends.tags.list();
|
2023-06-27 14:39:33 +03:00
|
|
|
const { value: tags } = await iterator.next();
|
2023-10-22 14:40:46 +03:00
|
|
|
console.log('tags', tags);
|
2023-10-22 20:36:07 +03:00
|
|
|
if (tags?.length) {
|
|
|
|
setHashtags(tags);
|
|
|
|
}
|
2023-06-27 14:39:33 +03:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
2023-10-22 14:40:46 +03:00
|
|
|
|
|
|
|
// Get links
|
|
|
|
try {
|
|
|
|
const { value: links } = await fetchLinks(masto);
|
|
|
|
console.log('links', links);
|
2023-10-22 20:36:07 +03:00
|
|
|
if (links?.length) {
|
|
|
|
setLinks(links);
|
|
|
|
}
|
2023-10-22 14:40:46 +03:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
2023-04-05 20:14:38 +03:00
|
|
|
}
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
}
|
2023-05-14 16:13:36 +03:00
|
|
|
return {
|
|
|
|
...results,
|
|
|
|
value,
|
|
|
|
};
|
2023-04-05 20:14:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async function checkForUpdates() {
|
|
|
|
try {
|
2023-10-12 07:48:09 +03:00
|
|
|
const results = await masto.v1.trends.statuses
|
|
|
|
.list({
|
2023-04-05 20:14:38 +03:00
|
|
|
limit: 1,
|
2023-04-06 05:24:52 +03:00
|
|
|
// NOT SUPPORTED
|
|
|
|
// since_id: latestItem.current,
|
2023-04-05 20:14:38 +03:00
|
|
|
})
|
|
|
|
.next();
|
|
|
|
let { value } = results;
|
|
|
|
value = filteredItems(value, 'public');
|
2023-04-06 05:24:52 +03:00
|
|
|
if (value?.length && value[0].id !== latestItem.current) {
|
|
|
|
latestItem.current = value[0].id;
|
2023-04-05 20:14:38 +03:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-27 14:39:33 +03:00
|
|
|
const TimelineStart = useMemo(() => {
|
|
|
|
return (
|
2023-10-22 14:40:46 +03:00
|
|
|
<>
|
|
|
|
{!!hashtags.length && (
|
|
|
|
<div class="filter-bar">
|
|
|
|
<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>
|
|
|
|
<span class="more-insignificant">#</span>
|
|
|
|
{name}
|
|
|
|
</span>
|
|
|
|
<span class="filter-count">{total.toLocaleString()}</span>
|
|
|
|
</Link>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{!!links.length && (
|
|
|
|
<div class="links-bar">
|
2023-10-22 20:36:07 +03:00
|
|
|
<header>
|
|
|
|
<h3>Trending News</h3>
|
|
|
|
</header>
|
2023-10-22 14:40:46 +03:00
|
|
|
{links.map((link) => {
|
|
|
|
const {
|
|
|
|
authorName,
|
|
|
|
authorUrl,
|
|
|
|
blurhash,
|
|
|
|
description,
|
|
|
|
height,
|
|
|
|
image,
|
|
|
|
imageDescription,
|
|
|
|
language,
|
|
|
|
providerName,
|
|
|
|
providerUrl,
|
|
|
|
publishedAt,
|
|
|
|
title,
|
|
|
|
url,
|
|
|
|
width,
|
|
|
|
} = link;
|
|
|
|
const domain = new URL(url).hostname
|
|
|
|
.replace(/^www\./, '')
|
|
|
|
.replace(/\/$/, '');
|
|
|
|
const averageColor = getBlurHashAverageColor(blurhash);
|
|
|
|
const labAverageColor = rgb2oklab(averageColor);
|
|
|
|
|
|
|
|
// const lightColor = averageColor.map((c) => {
|
|
|
|
// const v = c + 120;
|
|
|
|
// return v > 255 ? 255 : v;
|
|
|
|
// });
|
|
|
|
// const darkColor = averageColor.map((c) => {
|
|
|
|
// const v = c - 100;
|
|
|
|
// return v < 0 ? 0 : v;
|
|
|
|
// });
|
2023-10-22 20:36:07 +03:00
|
|
|
const accentColor = labAverageColor.map((c, i) => {
|
|
|
|
if (i === 0) {
|
|
|
|
return 0.65;
|
|
|
|
}
|
|
|
|
return c;
|
|
|
|
});
|
2023-10-22 14:40:46 +03:00
|
|
|
const lightColor = labAverageColor.map((c, i) => {
|
|
|
|
if (i === 0) {
|
|
|
|
return 0.9;
|
|
|
|
}
|
|
|
|
return c;
|
|
|
|
});
|
|
|
|
const darkColor = labAverageColor.map((c, i) => {
|
|
|
|
if (i === 0) {
|
|
|
|
return 0.4;
|
|
|
|
}
|
|
|
|
return c;
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<a
|
|
|
|
key={url}
|
|
|
|
href={url}
|
|
|
|
target="_blank"
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
style={{
|
|
|
|
'--average-color': `rgb(${averageColor?.join(',')})`,
|
|
|
|
// '--light-color': `rgb(${lightColor?.join(',')})`,
|
|
|
|
// '--dark-color': `rgb(${darkColor?.join(',')})`,
|
2023-10-22 20:36:07 +03:00
|
|
|
'--accent-color': `oklab(${accentColor?.join(' ')})`,
|
2023-10-22 14:40:46 +03:00
|
|
|
'--light-color': `oklab(${lightColor?.join(' ')})`,
|
|
|
|
'--dark-color': `oklab(${darkColor?.join(' ')})`,
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<article>
|
|
|
|
<figure>
|
|
|
|
<img
|
|
|
|
src={image}
|
|
|
|
alt={imageDescription}
|
|
|
|
width={width}
|
|
|
|
height={height}
|
2023-10-22 15:08:19 +03:00
|
|
|
loading="lazy"
|
2023-10-22 14:40:46 +03:00
|
|
|
/>
|
|
|
|
</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">{title}</h1>}
|
|
|
|
</header>
|
|
|
|
{!!description && (
|
|
|
|
<p class="description">{description}</p>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</article>
|
|
|
|
</a>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</>
|
2023-06-27 14:39:33 +03:00
|
|
|
);
|
2023-10-22 14:40:46 +03:00
|
|
|
}, [hashtags, links]);
|
2023-06-27 14:39:33 +03:00
|
|
|
|
2023-04-05 20:14:38 +03:00
|
|
|
return (
|
|
|
|
<Timeline
|
|
|
|
key={instance}
|
|
|
|
title={title}
|
|
|
|
titleComponent={
|
|
|
|
<h1 class="header-account">
|
|
|
|
<b>Trending</b>
|
|
|
|
<div>{instance}</div>
|
|
|
|
</h1>
|
|
|
|
}
|
|
|
|
id="trending"
|
|
|
|
instance={instance}
|
|
|
|
emptyText="No trending posts."
|
|
|
|
errorText="Unable to load posts"
|
|
|
|
fetchItems={fetchTrend}
|
|
|
|
checkForUpdates={checkForUpdates}
|
2023-04-06 05:24:52 +03:00
|
|
|
checkForUpdatesInterval={5 * 60 * 1000} // 5 minutes
|
2023-04-05 20:14:38 +03:00
|
|
|
useItemID
|
|
|
|
headerStart={<></>}
|
|
|
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
|
|
|
allowFilters
|
2023-06-27 14:39:33 +03:00
|
|
|
timelineStart={TimelineStart}
|
2023-04-05 20:14:38 +03:00
|
|
|
headerEnd={
|
2023-06-13 12:46:37 +03:00
|
|
|
<Menu2
|
|
|
|
portal
|
2023-04-05 20:14:38 +03:00
|
|
|
// setDownOverflow
|
|
|
|
overflow="auto"
|
|
|
|
viewScroll="close"
|
|
|
|
position="anchor"
|
|
|
|
menuButton={
|
|
|
|
<button type="button" class="plain">
|
|
|
|
<Icon icon="more" size="l" />
|
|
|
|
</button>
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
let newInstance = prompt(
|
|
|
|
'Enter a new instance e.g. "mastodon.social"',
|
|
|
|
);
|
|
|
|
if (!/\./.test(newInstance)) {
|
|
|
|
if (newInstance) alert('Invalid instance');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (newInstance) {
|
|
|
|
newInstance = newInstance.toLowerCase().trim();
|
2023-07-05 11:59:28 +03:00
|
|
|
// navigate(`/${newInstance}/trending`);
|
|
|
|
location.hash = `/${newInstance}/trending`;
|
2023-04-05 20:14:38 +03:00
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="bus" /> <span>Go to another instance…</span>
|
|
|
|
</MenuItem>
|
2023-06-13 12:46:37 +03:00
|
|
|
</Menu2>
|
2023-04-05 20:14:38 +03:00
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-10-22 20:36:07 +03:00
|
|
|
// https://gist.github.com/earthbound19/e7fe15fdf8ca3ef814750a61bc75b5ce
|
|
|
|
const gammaToLinear = (c) =>
|
|
|
|
c >= 0.04045 ? Math.pow((c + 0.055) / 1.055, 2.4) : c / 12.92;
|
|
|
|
function rgb2oklab([r, g, b]) {
|
|
|
|
r = gammaToLinear(r / 255);
|
|
|
|
g = gammaToLinear(g / 255);
|
|
|
|
b = gammaToLinear(b / 255);
|
|
|
|
var l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
|
|
var m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
|
|
var s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
|
|
l = Math.cbrt(l);
|
|
|
|
m = Math.cbrt(m);
|
|
|
|
s = Math.cbrt(s);
|
|
|
|
return [
|
|
|
|
l * +0.2104542553 + m * +0.793617785 + s * -0.0040720468,
|
|
|
|
l * +1.9779984951 + m * -2.428592205 + s * +0.4505937099,
|
|
|
|
l * +0.0259040371 + m * +0.7827717662 + s * -0.808675766,
|
|
|
|
];
|
2023-10-22 14:40:46 +03:00
|
|
|
}
|
|
|
|
|
2023-04-05 20:14:38 +03:00
|
|
|
export default Trending;
|