Make pinning and unpinning directly modify state (#37831)

This commit is contained in:
Echo 2026-02-11 17:58:00 +01:00 committed by GitHub
parent 9129f98776
commit ca9966ce2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 105 additions and 59 deletions

View File

@ -6,7 +6,10 @@ import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatus } from './importer';
import { unreblog, reblog } from './interactions_typed';
import { openModal } from './modal';
import { timelineExpandPinnedFromStatus } from './timelines_typed';
import {
insertPinnedStatusIntoTimelines,
removePinnedStatusFromTimelines,
} from './timelines_typed';
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
@ -369,7 +372,7 @@ export function pin(status) {
api().post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(pinSuccess(status));
dispatch(timelineExpandPinnedFromStatus(status));
dispatch(insertPinnedStatusIntoTimelines(status));
}).catch(error => {
dispatch(pinFail(status, error));
});
@ -408,7 +411,7 @@ export function unpin (status) {
api().post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unpinSuccess(status));
dispatch(timelineExpandPinnedFromStatus(status));
dispatch(removePinnedStatusFromTimelines(status));
}).catch(error => {
dispatch(unpinFail(status, error));
});

View File

@ -7,8 +7,8 @@ import type { Status } from '../models/status';
import { createAppThunk } from '../store/typed_functions';
import {
expandAccountFeaturedTimeline,
expandTimeline,
insertIntoTimeline,
TIMELINE_NON_STATUS_MARKERS,
} from './timelines';
@ -173,9 +173,13 @@ export function parseTimelineKey(key: string): TimelineParams | null {
return null;
}
export function isTimelineKeyPinned(key: string) {
export function isTimelineKeyPinned(key: string, accountId?: string) {
const parsedKey = parseTimelineKey(key);
return parsedKey?.type === 'account' && parsedKey.pinned;
const isPinned = parsedKey?.type === 'account' && parsedKey.pinned;
if (!accountId || !isPinned) {
return isPinned;
}
return parsedKey.userId === accountId;
}
export function isNonStatusId(value: unknown) {
@ -199,52 +203,71 @@ export const timelineDelete = createAction<{
reblogOf: string | null;
}>('timelines/delete');
export const timelineExpandPinnedFromStatus = createAppThunk(
export const timelineDeleteStatus = createAction<{
statusId: string;
timelineKey: string;
}>('timelines/deleteStatus');
export const insertPinnedStatusIntoTimelines = createAppThunk(
(status: Status, { dispatch, getState }) => {
const accountId = status.getIn(['account', 'id']) as string;
if (!accountId) {
const currentAccountId = getState().meta.get('me', null) as string | null;
if (!currentAccountId) {
return;
}
// Verify that any of the relevant timelines are actually expanded before dispatching, to avoid unnecessary API calls.
const tags =
(
status.get('tags') as
| ImmutableList<ImmutableMap<'name', string>> // We only care about the tag name.
| undefined
)
?.map((tag) => tag.get('name') as string)
.toArray() ?? [];
const timelines = getState().timelines as ImmutableMap<string, unknown>;
if (!timelines.some((_, key) => key.startsWith(`account:${accountId}:`))) {
return;
}
void dispatch(
expandTimelineByParams({
type: 'account',
userId: accountId,
pinned: true,
}),
);
void dispatch(expandAccountFeaturedTimeline(accountId));
// Iterate over tags and clear those too.
const tags = status.get('tags') as
| ImmutableList<ImmutableMap<'name', string>> // We only care about the tag name.
| undefined;
if (!tags) {
return;
}
tags.forEach((tag) => {
const tagName = tag.get('name');
if (!tagName) {
return;
const accountTimelines = timelines.filter((_, key) => {
if (!key.startsWith(`account:${currentAccountId}:`)) {
return false;
}
const parsed = parseTimelineKey(key);
const isPinned = parsed?.type === 'account' && parsed.pinned;
if (!isPinned) {
return false;
}
void dispatch(
expandTimelineByParams({
type: 'account',
userId: accountId,
pinned: true,
tagged: tagName,
}),
);
void dispatch(
expandAccountFeaturedTimeline(accountId, { tagged: tagName }),
);
return !parsed.tagged || tags.includes(parsed.tagged);
});
accountTimelines.forEach((_, key) => {
dispatch(insertIntoTimeline(key, status.get('id') as string, 0));
});
},
);
export const removePinnedStatusFromTimelines = createAppThunk(
(status: Status, { dispatch, getState }) => {
const currentAccountId = getState().meta.get('me', null) as string | null;
if (!currentAccountId) {
return;
}
const statusId = status.get('id') as string;
const timelines = getState().timelines as ImmutableMap<
string,
ImmutableMap<'items' | 'pendingItems', ImmutableList<string>>
>;
timelines.forEach((timeline, key) => {
if (!isTimelineKeyPinned(key, currentAccountId)) {
return;
}
if (
timeline.get('items')?.includes(statusId) ||
timeline.get('pendingItems')?.includes(statusId)
) {
dispatch(timelineDeleteStatus({ statusId, timelineKey: key }));
}
});
},
);

View File

@ -20,7 +20,12 @@ import {
TIMELINE_GAP,
disconnectTimeline,
} from '../actions/timelines';
import { timelineDelete, isTimelineKeyPinned, isNonStatusId } from '../actions/timelines_typed';
import {
timelineDelete,
timelineDeleteStatus,
isTimelineKeyPinned,
isNonStatusId,
} from '../actions/timelines_typed';
import { compareId } from '../compare_id';
const initialState = ImmutableMap();
@ -145,6 +150,11 @@ const deleteStatus = (state, id, references, exclude_account = null) => {
return state;
};
const deleteStatusFromTimeline = (state, statusId, timelineKey) => {
const helper = list => list.filterNot((status) => status === statusId);
return state.updateIn([timelineKey, 'items'], helper).updateIn([timelineKey, 'pendingItems'], helper);
}
const clearTimeline = (state, timeline) => {
return state.set(timeline, initialTimeline);
};
@ -200,25 +210,12 @@ export default function timelines(state = initialState, action) {
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems);
case TIMELINE_UPDATE:
return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
case timelineDelete.type:
return deleteStatus(state, action.payload.statusId, action.payload.references, action.payload.reblogOf);
case TIMELINE_CLEAR:
return clearTimeline(state, action.timeline);
case blockAccountSuccess.type:
case muteAccountSuccess.type:
return filterTimelines(state, action.payload.relationship, action.payload.statuses);
case unfollowAccountSuccess.type:
return filterTimeline('home', state, action.payload.relationship, action.payload.statuses);
case TIMELINE_SCROLL_TOP:
return updateTop(state, action.timeline, action.top);
case TIMELINE_CONNECT:
return state.update(action.timeline, initialTimeline, map => reconnectTimeline(map, action.usePendingItems));
case disconnectTimeline.type:
return state.update(
action.payload.timeline,
initialTimeline,
map => map.set('online', false).update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(TIMELINE_GAP) : items),
);
case TIMELINE_MARK_AS_PARTIAL:
return state.update(
action.timeline,
@ -238,6 +235,29 @@ export default function timelines(state = initialState, action) {
})
);
default:
if (timelineDelete.match(action)) {
return deleteStatus(state, action.payload.statusId, action.payload.references, action.payload.reblogOf);
} else if (timelineDeleteStatus.match(action)) {
return deleteStatusFromTimeline(state, action.payload.statusId, action.payload.timelineKey);
} else if (blockAccountSuccess.match(action) || muteAccountSuccess.match(action)) {
return filterTimelines(state, action.payload.relationship, action.payload.statuses);
} else if (unfollowAccountSuccess.match(action)) {
return filterTimeline('home', state, action.payload.relationship, action.payload.statuses);
} else if (disconnectTimeline.match(action)) {
return state.update(
action.payload.timeline,
initialTimeline,
(map) => map.set('online', false).update(
action.payload.usePendingItems
? 'pendingItems'
: 'items',
items => items.first()
? items.unshift(TIMELINE_GAP)
: items
),
);
}
return state;
}
}