[Glitch] Add timeline of public posts about a trending link in web UI

Port 20fa9ce484 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
main-rebase-security-fix
Eugen Rochko 2024-07-01 16:45:48 +02:00 committed by Claire
parent a70b1141d6
commit 4c5067e3a4
8 changed files with 97 additions and 1 deletions

View File

@ -170,6 +170,7 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies, t
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
export const expandLinkTimeline = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done);
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId, max_id: maxId,

View File

@ -44,6 +44,7 @@ export interface ApiPreviewCardJSON {
type: string; type: string;
author_name: string; author_name: string;
author_url: string; author_url: string;
author_account?: ApiAccountJSON;
provider_name: string; provider_name: string;
provider_url: string; provider_url: string;
html: string; html: string;

View File

@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent {
withCounters: PropTypes.bool, withCounters: PropTypes.bool,
timelineId: PropTypes.string.isRequired, timelineId: PropTypes.string.isRequired,
lastId: PropTypes.string, lastId: PropTypes.string,
bindToDocument: PropTypes.bool,
regex: PropTypes.string, regex: PropTypes.string,
}; };

View File

@ -4,6 +4,8 @@ import { useState, useCallback } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { Blurhash } from 'flavours/glitch/components/blurhash'; import { Blurhash } from 'flavours/glitch/components/blurhash';
@ -57,7 +59,7 @@ export const Story = ({
<div className='story__details__shared'> <div className='story__details__shared'>
{author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />} {author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />}
{typeof sharedTimes === 'number' ? <span className='story__details__shared__pill'><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></span> : <Skeleton width='10ch' />} {typeof sharedTimes === 'number' ? <Link className='story__details__shared__pill' to={`/links/${encodeURIComponent(url)}`}><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></Link> : <Skeleton width='10ch' />}
</div> </div>
</div> </div>

View File

@ -0,0 +1,77 @@
import { useRef, useEffect, useCallback } from 'react';
import { Helmet } from 'react-helmet';
import { useParams } from 'react-router-dom';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import { expandLinkTimeline } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header';
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
import type { Card } from 'flavours/glitch/models/status';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
export const LinkTimeline: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
const { url } = useParams<{ url: string }>();
const decodedUrl = url ? decodeURIComponent(url) : undefined;
const dispatch = useAppDispatch();
const columnRef = useRef<Column>(null);
const firstStatusId = useAppSelector((state) =>
decodedUrl
? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
(state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string)
: undefined,
);
const story = useAppSelector((state) =>
firstStatusId
? (state.statuses.getIn([firstStatusId, 'card']) as Card)
: undefined,
);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, []);
const handleLoadMore = useCallback(
(maxId: string) => {
dispatch(expandLinkTimeline(decodedUrl, { maxId }));
},
[dispatch, decodedUrl],
);
useEffect(() => {
dispatch(expandLinkTimeline(decodedUrl));
}, [dispatch, decodedUrl]);
return (
<Column bindToDocument={!multiColumn} ref={columnRef} label={story?.title}>
<ColumnHeader
icon='explore'
iconComponent={ExploreIcon}
title={story?.title}
onClick={handleHeaderClick}
multiColumn={multiColumn}
showBackButton
/>
<StatusListContainer
timelineId={`link:${decodedUrl}`}
onLoadMore={handleLoadMore}
trackScroll
scrollKey={`link_timeline-${decodedUrl}`}
bindToDocument={!multiColumn}
regex={undefined}
/>
<Helmet>
<title>{story?.title}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default LinkTimeline;

View File

@ -58,6 +58,7 @@ import {
FavouritedStatuses, FavouritedStatuses,
BookmarkedStatuses, BookmarkedStatuses,
FollowedTags, FollowedTags,
LinkTimeline,
ListTimeline, ListTimeline,
Blocks, Blocks,
DomainBlocks, DomainBlocks,
@ -211,6 +212,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} /> <WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} /> <WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} /> <WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} /> <WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
<WrappedRoute path='/notifications' component={Notifications} content={children} exact /> <WrappedRoute path='/notifications' component={Notifications} content={children} exact />
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact /> <WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />

View File

@ -213,3 +213,7 @@ export function NotificationRequests () {
export function NotificationRequest () { export function NotificationRequest () {
return import(/*webpackChunkName: "features/glitch/notifications/request" */'../../notifications/request'); return import(/*webpackChunkName: "features/glitch/notifications/request" */'../../notifications/request');
} }
export function LinkTimeline () {
return import(/*webpackChunkName: "features/glitch/link_timeline" */'../../link_timeline');
}

View File

@ -1,4 +1,12 @@
import type { RecordOf } from 'immutable';
import type { ApiPreviewCardJSON } from 'flavours/glitch/api_types/statuses';
export type { StatusVisibility } from 'flavours/glitch/api_types/statuses'; export type { StatusVisibility } from 'flavours/glitch/api_types/statuses';
// Temporary until we type it correctly // Temporary until we type it correctly
export type Status = Immutable.Map<string, unknown>; export type Status = Immutable.Map<string, unknown>;
type CardShape = Required<ApiPreviewCardJSON>;
export type Card = RecordOf<CardShape>;