Files
Mastodon/app/javascript/mastodon/selectors/statuses.ts
T
2026-07-06 10:00:41 +00:00

228 lines
6.6 KiB
TypeScript

import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
import type { StatusInteractionIntent } from '@/mastodon/actions/interactions_typed';
import type {
ExpandedStatusShape,
StatusShape,
} from '@/mastodon/models/status';
import { createAppSelector } from '@/mastodon/store/typed_functions';
import { selectIsAccountLocal, selectPlainAccount } from './accounts';
import type { FilterShape } from './filters';
import { getFilters } from './filters';
export const getStatusList = createAppSelector(
[
(state, type: 'favourites' | 'bookmarks' | 'pins' | 'trending') =>
state.status_lists.getIn([type, 'items']) as ImmutableOrderedSet<string>,
],
(items) => items.toList(),
);
export const selectPlainStatus = createAppSelector(
[(state, statusId: string) => state.statuses.get(statusId)],
(status) => {
if (!status) {
return null;
}
return status.toJS() as unknown as StatusShape;
},
);
export const selectAccountStatus = createAppSelector(
[
selectPlainStatus,
(state, statusId: string) => {
const accountId = state.statuses.getIn([statusId, 'account']);
if (typeof accountId !== 'string') {
return null;
}
return selectPlainAccount(state, accountId);
},
],
(status, account) => {
if (!status || !account) {
return null;
}
return {
...status,
account,
};
},
);
export const selectExpandedStatus = createAppSelector(
[
selectAccountStatus,
(state, statusId: string) => {
const reblogId = state.statuses.getIn([statusId, 'reblog']);
if (typeof reblogId !== 'string') {
return null;
}
return selectAccountStatus(state, reblogId);
},
],
(status, reblog): ExpandedStatusShape | null => {
if (!status) {
return null;
}
return {
...status,
reblog: reblog ?? undefined,
};
},
);
export const selectStatusConditions = createAppSelector(
[
selectPlainStatus,
(state) => state.meta.get('me', null) as string | null,
(state, statusId: string) =>
state.statuses.getIn([
state.statuses.getIn([statusId, 'quote', 'quoted_status']),
'account',
]) as string | undefined | null,
(state, statusId: string) =>
selectIsAccountLocal(
state,
state.statuses.getIn([statusId, 'account']) as string,
),
],
(status, userId, quotedAccountId, isLocal) => {
const isPublic =
!!status && ['public', 'unlisted'].includes(status.visibility);
const isMine = status?.account === userId;
const isMineAndPrivate =
userId === status?.account && status.visibility === 'private';
const quoteApproval = status?.quote_approval?.current_user;
return {
isPublic,
isLocal,
isLoggedIn: !!userId,
isMine,
isNotMine: !isMine,
isQuoted: !!status && !!userId && quotedAccountId === userId,
isNotDirect: !!status && status.visibility !== 'direct',
isPrivateReblog:
userId === status?.account && status.visibility === 'private',
isBoosted: status?.reblogged ?? false,
isBoostingAllowed: isPublic || isMineAndPrivate,
isQuoteAutomaticallyAccepted:
quoteApproval === 'automatic' && (isPublic || isMineAndPrivate),
isQuoteManuallyAccepted:
quoteApproval === 'manual' && (isPublic || isMineAndPrivate),
isQuoteFollowersOnly:
status?.quote_approval?.automatic[0] === 'followers' ||
status?.quote_approval?.manual[0] === 'followers',
};
},
);
export type StatusConditions = ReturnType<typeof selectStatusConditions>;
export const selectStatusInteractions = createAppSelector(
[(_, statusId: string) => statusId, selectStatusConditions],
(statusId, conditionals) => {
function addAllowed(conditions: Partial<typeof conditionals>) {
return {
...conditions,
allowed: Object.values(conditions).every(Boolean),
};
}
const {
isLoggedIn,
isMine,
isNotMine,
isPublic,
isLocal,
isQuoted,
isNotDirect,
} = conditionals;
const interactions: Record<
StatusInteractionIntent,
Partial<typeof conditionals> & { allowed: boolean }
> = {
bookmark: addAllowed({ isLoggedIn }),
delete: addAllowed({ isMine }),
edit: addAllowed({ isMine }),
editQuotePolicy: addAllowed({ isMine, isPublic }),
embed: addAllowed({ isPublic, isLocal }),
favourite: addAllowed({ isLoggedIn }),
filter: addAllowed({ isLoggedIn, isNotMine }),
mute: addAllowed({ isMine }),
pin: addAllowed({ isMine, isNotDirect }),
quote: addAllowed({ isLoggedIn }),
reblog: addAllowed({ isLoggedIn }),
redraft: addAllowed({ isMine }),
reply: addAllowed({ isLoggedIn }),
report: addAllowed({ isLoggedIn, isNotMine }),
revokeQuote: addAllowed({ isQuoted, isNotMine }),
};
return {
statusId,
...interactions,
};
},
);
export type StatusInteractions = ReturnType<typeof selectStatusInteractions>;
export const selectStatusInteractionsAllowed = createAppSelector(
[selectStatusInteractions],
({ statusId, ...interactions }) => ({
statusId,
...(Object.fromEntries(
Object.entries(interactions).map(([key, { allowed }]) => [key, allowed]),
) as Record<StatusInteractionIntent, boolean>),
}),
);
export type StatusInteractionsAllowed = ReturnType<
typeof selectStatusInteractionsAllowed
>;
export const selectStatusIntentAllowed = createAppSelector(
[
selectStatusInteractions,
(_state, _statusId: string, intent: StatusInteractionIntent) => intent,
],
(allowedInteractions, intent) => allowedInteractions[intent].allowed,
);
export const selectPictureInPicture = createAppSelector(
[
(state, statusId: string) =>
state.picture_in_picture.type !== null &&
state.picture_in_picture.statusId === statusId,
(state) => state.meta.get('layout') !== 'mobile',
],
(inUse, available) => ({
inUse: inUse && available,
available,
}),
);
export const selectMediaMatchFilters = createAppSelector(
[
(state, { statusId }: { statusId: string }) =>
selectPlainStatus(state, statusId),
getFilters,
],
(status, immutableFilters) => {
const filters = immutableFilters
? (immutableFilters.toJS() as unknown as Record<string, FilterShape>)
: null;
const mediaFilters: string[] = [];
if (status?.filtered && filters) {
for (const { filter } of status.filtered) {
if (filters[filter]?.filter_action === 'blur') {
mediaFilters.push(filters[filter].title);
}
}
}
return mediaFilters;
},
);