diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js
index d3ce4c575c..eb5050f152 100644
--- a/app/javascript/flavours/glitch/actions/timelines.js
+++ b/app/javascript/flavours/glitch/actions/timelines.js
@@ -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 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/flavours/glitch/api_types/statuses.ts b/app/javascript/flavours/glitch/api_types/statuses.ts
index 261d600305..9de86e7fa6 100644
--- a/app/javascript/flavours/glitch/api_types/statuses.ts
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/components/status_list.jsx b/app/javascript/flavours/glitch/components/status_list.jsx
index dde8bd9663..374d14a56a 100644
--- a/app/javascript/flavours/glitch/components/status_list.jsx
+++ b/app/javascript/flavours/glitch/components/status_list.jsx
@@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent {
withCounters: PropTypes.bool,
timelineId: PropTypes.string.isRequired,
lastId: PropTypes.string,
+ bindToDocument: PropTypes.bool,
regex: PropTypes.string,
};
diff --git a/app/javascript/flavours/glitch/features/explore/components/story.jsx b/app/javascript/flavours/glitch/features/explore/components/story.jsx
index 28a1d69f8a..b07425b277 100644
--- a/app/javascript/flavours/glitch/features/explore/components/story.jsx
+++ b/app/javascript/flavours/glitch/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 'flavours/glitch/components/blurhash';
@@ -57,7 +59,7 @@ export const Story = ({
{author ? : {author} }} /> : }
- {typeof sharedTimes === 'number' ? : }
+ {typeof sharedTimes === 'number' ? : }
diff --git a/app/javascript/flavours/glitch/features/link_timeline/index.tsx b/app/javascript/flavours/glitch/features/link_timeline/index.tsx
new file mode 100644
index 0000000000..bbe295d474
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/link_timeline/index.tsx
@@ -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(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/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx
index 9fb4a784b3..514536a836 100644
--- a/app/javascript/flavours/glitch/features/ui/index.jsx
+++ b/app/javascript/flavours/glitch/features/ui/index.jsx
@@ -58,6 +58,7 @@ import {
FavouritedStatuses,
BookmarkedStatuses,
FollowedTags,
+ LinkTimeline,
ListTimeline,
Blocks,
DomainBlocks,
@@ -211,6 +212,7 @@ class SwitchingColumnsArea extends PureComponent {
+
diff --git a/app/javascript/flavours/glitch/features/ui/util/async-components.js b/app/javascript/flavours/glitch/features/ui/util/async-components.js
index 6a140e3fd7..a312cefff7 100644
--- a/app/javascript/flavours/glitch/features/ui/util/async-components.js
+++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js
@@ -213,3 +213,7 @@ export function NotificationRequests () {
export function NotificationRequest () {
return import(/*webpackChunkName: "features/glitch/notifications/request" */'../../notifications/request');
}
+
+export function LinkTimeline () {
+ return import(/*webpackChunkName: "features/glitch/link_timeline" */'../../link_timeline');
+}
diff --git a/app/javascript/flavours/glitch/models/status.ts b/app/javascript/flavours/glitch/models/status.ts
index d9566daf39..bf1784bc61 100644
--- a/app/javascript/flavours/glitch/models/status.ts
+++ b/app/javascript/flavours/glitch/models/status.ts
@@ -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';
// Temporary until we type it correctly
export type Status = Immutable.Map;
+
+type CardShape = Required;
+
+export type Card = RecordOf;