Profile redesign: Timeline filters (#37626)

This commit is contained in:
Echo authored and GitHub committed 2026-01-28 10:17:32 +00:00
1 parent 3f46034039
commit ec76288dff
13 files changed
+704 -5

No files matched your search

@@ -0,0 +1,60 @@
import { parseTimelineKey, timelineKey } from './timelines_typed';
describe('timelineKey', () => {
test('returns expected key for account timeline with filters', () => {
const key = timelineKey({
type: 'account',
userId: '123',
replies: true,
boosts: false,
media: true,
});
expect(key).toBe('account:123:0110');
});
test('returns expected key for account timeline with tag', () => {
const key = timelineKey({
type: 'account',
userId: '456',
tagged: 'nature',
replies: true,
});
expect(key).toBe('account:456:0100:nature');
});
test('returns expected key for account timeline with pins', () => {
const key = timelineKey({
type: 'account',
userId: '789',
pinned: true,
});
expect(key).toBe('account:789:0001');
});
});
describe('parseTimelineKey', () => {
test('parses account timeline key with filters correctly', () => {
const params = parseTimelineKey('account:123:1010');
expect(params).toEqual({
type: 'account',
userId: '123',
boosts: true,
replies: false,
media: true,
pinned: false,
});
});
test('parses account timeline key with tag correctly', () => {
const params = parseTimelineKey('account:456:0100:nature');
expect(params).toEqual({
type: 'account',
userId: '456',
replies: true,
boosts: false,
media: false,
pinned: false,
tagged: 'nature',
});
});
});
@@ -2,7 +2,153 @@ import { createAction } from '@reduxjs/toolkit';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import { TIMELINE_NON_STATUS_MARKERS } from './timelines';
import { createAppThunk } from '../store/typed_functions';
import { expandTimeline, TIMELINE_NON_STATUS_MARKERS } from './timelines';
export const expandTimelineByKey = createAppThunk(
(args: { key: string; maxId?: number }, { dispatch }) => {
const params = parseTimelineKey(args.key);
if (!params) {
return;
}
void dispatch(expandTimelineByParams({ ...params, maxId: args.maxId }));
},
);
export const expandTimelineByParams = createAppThunk(
(params: TimelineParams & { maxId?: number }, { dispatch }) => {
let url = '';
const extra: Record<string, string | boolean> = {};
if (params.type === 'account') {
url = `/api/v1/accounts/${params.userId}/statuses`;
if (!params.replies) {
extra.exclude_replies = true;
}
if (!params.boosts) {
extra.exclude_reblogs = true;
}
if (params.pinned) {
extra.pinned = true;
}
if (params.media) {
extra.only_media = true;
}
if (params.tagged) {
extra.tagged = params.tagged;
}
} else if (params.type === 'public') {
url = '/api/v1/timelines/public';
}
if (params.maxId) {
extra.max_id = params.maxId.toString();
}
return dispatch(expandTimeline(timelineKey(params), url, extra));
},
);
export interface AccountTimelineParams {
type: 'account';
userId: string;
tagged?: string;
media?: boolean;
pinned?: boolean;
boosts?: boolean;
replies?: boolean;
}
export type PublicTimelineServer = 'local' | 'remote' | 'all';
export interface PublicTimelineParams {
type: 'public';
tagged?: string;
server?: PublicTimelineServer; // Defaults to 'all'
media?: boolean;
}
export interface HomeTimelineParams {
type: 'home';
}
export type TimelineParams =
| AccountTimelineParams
| PublicTimelineParams
| HomeTimelineParams;
const ACCOUNT_FILTERS = ['boosts', 'replies', 'media', 'pinned'] as const;
export function timelineKey(params: TimelineParams): string {
const { type } = params;
const key: string[] = [type];
if (type === 'account') {
key.push(params.userId);
const view = ACCOUNT_FILTERS.reduce(
(prev, curr) => prev + (params[curr] ? '1' : '0'),
'',
);
key.push(view);
} else if (type === 'public') {
key.push(params.server ?? 'all');
if (params.media) {
key.push('media');
}
}
if (type !== 'home' && params.tagged) {
key.push(params.tagged);
}
return key.filter(Boolean).join(':');
}
export function parseTimelineKey(key: string): TimelineParams | null {
const segments = key.split(':');
const type = segments[0];
if (type === 'account') {
const userId = segments[1];
if (!userId) {
return null;
}
const parsed: TimelineParams = {
type: 'account',
userId,
tagged: segments[3],
};
const view = segments[2]?.split('') ?? [];
for (let i = 0; i < view.length; i++) {
const flagName = ACCOUNT_FILTERS[i];
if (flagName) {
parsed[flagName] = view[i] === '1';
}
}
return parsed;
}
if (type === 'public') {
return {
type: 'public',
server:
segments[1] === 'remote' || segments[1] === 'local'
? segments[1]
: 'all',
tagged: segments[2],
media: segments[3] === 'media',
};
}
if (type === 'home') {
return { type: 'home' };
}
return null;
}
export function isNonStatusId(value: unknown) {
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);