diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 4ca3c3a151..f0ea46118e 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -158,6 +158,7 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies, t
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/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 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) => {
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId,
diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts
index db4e20506f..a934faeb7a 100644
--- a/app/javascript/mastodon/api_types/statuses.ts
+++ b/app/javascript/mastodon/api_types/statuses.ts
@@ -44,6 +44,7 @@ export interface ApiPreviewCardJSON {
type: string;
author_name: string;
author_url: string;
+ author_account?: ApiAccountJSON;
provider_name: string;
provider_url: string;
html: string;
diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx
index 3ed20f65eb..fee6675faa 100644
--- a/app/javascript/mastodon/components/status_list.jsx
+++ b/app/javascript/mastodon/components/status_list.jsx
@@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent {
withCounters: PropTypes.bool,
timelineId: PropTypes.string,
lastId: PropTypes.string,
+ bindToDocument: PropTypes.bool,
};
static defaultProps = {
diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx
index a2cae942d4..125df412a1 100644
--- a/app/javascript/mastodon/features/explore/components/story.jsx
+++ b/app/javascript/mastodon/features/explore/components/story.jsx
@@ -4,6 +4,8 @@ import { useState, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
import { Blurhash } from 'mastodon/components/blurhash';
@@ -57,7 +59,7 @@ export const Story = ({
{author ? : {author} }} /> : }
- {typeof sharedTimes === 'number' ? : }
+ {typeof sharedTimes === 'number' ? : }
diff --git a/app/javascript/mastodon/features/link_timeline/index.tsx b/app/javascript/mastodon/features/link_timeline/index.tsx
new file mode 100644
index 0000000000..dd726dac1a
--- /dev/null
+++ b/app/javascript/mastodon/features/link_timeline/index.tsx
@@ -0,0 +1,76 @@
+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 'mastodon/actions/timelines';
+import Column from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
+import type { Card } from 'mastodon/models/status';
+import { useAppDispatch, useAppSelector } from 'mastodon/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(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 (
+
+
+
+
+
+
+ {story?.title}
+
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default LinkTimeline;
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index b58e191ed8..d41132f9ca 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -56,6 +56,7 @@ import {
FavouritedStatuses,
BookmarkedStatuses,
FollowedTags,
+ LinkTimeline,
ListTimeline,
Blocks,
DomainBlocks,
@@ -202,6 +203,7 @@ class SwitchingColumnsArea extends PureComponent {
+
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index e1f5bfdaf6..b8a2359d92 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -201,3 +201,7 @@ export function NotificationRequests () {
export function NotificationRequest () {
return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request');
}
+
+export function LinkTimeline () {
+ return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline');
+}
diff --git a/app/javascript/mastodon/models/status.ts b/app/javascript/mastodon/models/status.ts
index 7907fc34f8..3900df4e38 100644
--- a/app/javascript/mastodon/models/status.ts
+++ b/app/javascript/mastodon/models/status.ts
@@ -1,4 +1,12 @@
+import type { RecordOf } from 'immutable';
+
+import type { ApiPreviewCardJSON } from 'mastodon/api_types/statuses';
+
export type { StatusVisibility } from 'mastodon/api_types/statuses';
// Temporary until we type it correctly
export type Status = Immutable.Map;
+
+type CardShape = Required;
+
+export type Card = RecordOf;
diff --git a/config/routes.rb b/config/routes.rb
index f4662dd5da..4b3bd4f187 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -27,6 +27,7 @@ Rails.application.routes.draw do
/public/remote
/conversations
/lists/(*any)
+ /links/(*any)
/notifications/(*any)
/favourites
/bookmarks