Refactor Status component (#39731)

This commit is contained in:
Echo
2026-07-06 17:16:50 +02:00
committed by GitHub
parent bcb47b814d
commit 5360faa899
10 changed files with 808 additions and 57 deletions

View File

@@ -35,6 +35,8 @@ import {
editStatus,
muteStatus,
setStatusQuotePolicy,
translateStatus,
undoStatusTranslation,
unmuteStatus,
} from './statuses';
@@ -53,7 +55,8 @@ export type StatusInteractionIntent =
| 'redraft'
| 'reply'
| 'report'
| 'revokeQuote';
| 'revokeQuote'
| 'translate';
const messages = defineMessages({
noEdits: {
@@ -77,12 +80,16 @@ export const statusInteraction = createAppThunk(
contextType,
intent,
}: {
statusId: string;
statusId?: string;
contextType?: StatusContextType;
intent: StatusInteractionIntent;
},
{ getState, dispatch },
) => {
if (!statusId) {
return;
}
const state = getState();
const statusImmutable = state.statuses.get(statusId);
if (!statusImmutable) {
@@ -255,6 +262,12 @@ export const statusInteraction = createAppThunk(
}),
);
return;
case 'translate':
if (status.translation) {
dispatch(undoStatusTranslation(statusId, status.poll));
} else {
dispatch(translateStatus(statusId));
}
}
},
);

View File

@@ -13,10 +13,8 @@ import type {
MediaAttachmentShape,
} from '@/mastodon/models/status';
import { isMediaAttachmentOfType } from '@/mastodon/models/status';
import {
selectMediaMatchFilters,
selectPictureInPicture,
} from '@/mastodon/selectors/statuses';
import { selectMediaFilters } from '@/mastodon/selectors/filters';
import { selectPictureInPicture } from '@/mastodon/selectors/statuses';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { compareUrls } from '@/mastodon/utils/compare_urls';
@@ -125,7 +123,7 @@ const MediaAttachments: React.FC<{
]) as Immutable.List<MediaAttachment>;
});
const mediaFilters = useAppSelector((state) =>
selectMediaMatchFilters(state, { statusId, contextType }),
selectMediaFilters(state, { statusId, contextType }),
);
const pictureInPicture = useAppSelector((state) =>
selectPictureInPicture(state, statusId),

View File

@@ -0,0 +1,262 @@
import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router';
import { mentionComposeById } from '@/mastodon/actions/compose';
import type { StatusInteractionIntent } from '@/mastodon/actions/interactions_typed';
import { statusInteraction } from '@/mastodon/actions/interactions_typed';
import { openModal } from '@/mastodon/actions/modal';
import { toggleStatusSpoilers } from '@/mastodon/actions/statuses';
import { useExpandedStatus } from '@/mastodon/hooks/useStatus';
import { useToggle } from '@/mastodon/hooks/useToggle';
import type { ExpandedStatusShape } from '@/mastodon/models/status';
import { selectStatusFilters } from '@/mastodon/selectors/filters';
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
import { FOCUS_TARGET } from '../navigation_focus_target';
import type { StatusContextType } from './types';
const messages = defineMessages({
quote_noun: {
id: 'status.quote_noun',
defaultMessage: 'Quote',
description: 'Quote as a noun',
},
contains_quote: {
id: 'status.contains_quote',
defaultMessage: 'Contains quote',
},
boosted: { id: 'status.reblogged_by', defaultMessage: '{name} boosted' },
});
export function useStatusHandlers({
status,
contextType,
onClick,
}: {
status?: ExpandedStatusShape;
contextType?: StatusContextType;
onClick?: () => void;
}) {
const matchedFilters = useAppSelector((state) =>
selectStatusFilters(state, {}),
);
const [showDespiteFilter, { onToggle: onFilterToggle }] = useToggle(false);
const dispatch = useAppDispatch();
const statusId = status?.id;
// Display handlers
const onExpandedToggle = useCallback(() => {
dispatch(toggleStatusSpoilers(statusId));
}, [dispatch, statusId]);
const onToggleHidden = useCallback(() => {
if (!status) {
return;
}
if (!matchedFilters.length || showDespiteFilter) {
dispatch(toggleStatusSpoilers(status.id));
}
if (!status.hidden || !status.spoiler_text) {
onFilterToggle();
}
}, [
dispatch,
matchedFilters.length,
onFilterToggle,
showDespiteFilter,
status,
]);
// Interaction handlers
const handlerFactory = useCallback(
(intent: StatusInteractionIntent) => {
return () => {
dispatch(statusInteraction({ statusId, intent, contextType }));
};
},
[contextType, dispatch, statusId],
);
const accountId = status?.account.id;
const onMention = useCallback(() => {
dispatch(mentionComposeById(accountId));
}, [dispatch, accountId]);
// Navigation handlers
const history = useHistory();
const onOpen = useCallback(
(newTab = false) => {
if (onClick || !status) {
onClick?.();
return;
}
const path = `/@${status.account.acct}/${status.id}`;
if (newTab) {
window.open(path, '_blank', 'noopener');
} else if (history.location.pathname.replace('/deck/', '/') === path) {
history.replace(path, { focusTarget: FOCUS_TARGET.POST });
} else {
history.push(path, { focusTarget: FOCUS_TARGET.POST });
}
},
[history, onClick, status],
);
const onOpenClick: React.MouseEventHandler = useCallback(
(event) => {
event.preventDefault();
if (event.button === 0 && !(event.ctrlKey || event.metaKey)) {
onOpen();
} else if (
event.button === 1 ||
(event.button === 0 && (event.ctrlKey || event.metaKey))
) {
onOpen(true);
}
},
[onOpen],
);
const onHeaderClick: React.MouseEventHandler = useCallback(
(event) => {
// Only handle clicks on the empty space above the content
if (event.target !== event.currentTarget && event.detail >= 1) {
return;
}
onOpenClick(event);
},
[onOpenClick],
);
const acct = status?.account.acct;
const onOpenProfile = useCallback(() => {
if (acct) {
history.push(`/@${acct}`);
}
}, [history, acct]);
const onOpenMedia = useCallback(() => {
const attachment = status?.media_attachments[0];
if (!attachment) {
return;
}
const lang = status.translation?.language ?? status.language;
if (attachment.type === 'video') {
dispatch(
openModal({
modalType: 'VIDEO',
modalProps: {
statusId: status.id,
media: attachment,
lang,
options: { startTime: 0 },
},
}),
);
} else {
dispatch(
openModal({
modalType: 'MEDIA',
modalProps: {
statusId: status.id,
media: status.media_attachments,
lang,
index: 0,
},
}),
);
}
}, [dispatch, status]);
return useMemo(
() => ({
showDespiteFilter,
onOpenClick,
onExpandedToggle,
onFilterToggle,
onHeaderClick,
onMention,
onOpen,
onOpenMedia,
onOpenProfile,
onToggleHidden,
onReply: handlerFactory('reply'),
onFavourite: handlerFactory('favourite'),
onBoost: handlerFactory('reblog'),
onQuote: handlerFactory('quote'),
onTranslate: handlerFactory('translate'),
}),
[
handlerFactory,
onExpandedToggle,
onFilterToggle,
onHeaderClick,
onMention,
onOpen,
onOpenClick,
onOpenMedia,
onOpenProfile,
onToggleHidden,
showDespiteFilter,
],
);
}
export type StatusHandlers = ReturnType<typeof useStatusHandlers>;
const domParser = new DOMParser();
export function useTextForScreenReader({
statusId,
reblogAcct,
isQuote = false,
}: {
statusId?: string | null;
reblogAcct?: string;
isQuote?: boolean;
}) {
const intl = useIntl();
const status = useExpandedStatus(statusId);
return useMemo(() => {
if (!status) {
return '';
}
const displayName = status.account.display_name;
const spoilerText = status.translation?.spoiler_text ?? status.spoiler_text;
const contentHtml = status.translation?.contentHtml ?? status.contentHtml;
const contentText = domParser.parseFromString(contentHtml, 'text/html')
.documentElement.textContent;
const values = [
isQuote ? intl.formatMessage(messages.quote_noun) : undefined,
displayName.length === 0
? status.account.acct.split('@')[0]
: displayName,
spoilerText && status.hidden ? spoilerText : contentText,
status.quote ? intl.formatMessage(messages.contains_quote) : undefined,
intl.formatDate(status.created_at, {
hour: '2-digit',
minute: '2-digit',
month: 'short',
day: 'numeric',
}),
status.account.acct,
reblogAcct
? intl.formatMessage(messages.boosted, { name: reblogAcct })
: false,
].filter((val) => !!val);
return values.join(', ');
}, [intl, isQuote, reblogAcct, status]);
}

View File

@@ -0,0 +1,67 @@
import { FormattedMessage } from 'react-intl';
import type { ExpandedStatusShape } from '@/mastodon/models/status';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import { LinkedDisplayName } from '../display_name';
import { Icon } from '../icon';
import { StatusThreadLabel } from '../status_thread_label';
export const StatusPrepend: React.FC<{
status: ExpandedStatusShape;
showThread?: boolean;
isReblog?: boolean;
}> = ({ status, showThread, isReblog }) => {
if (isReblog) {
return (
<div className='status__prepend'>
<div className='status__prepend__icon'>
<Icon id='retweet' icon={RepeatIcon} />
</div>
<FormattedMessage
id='status.reblogged_by'
defaultMessage='{name} boosted'
values={{
name: (
<LinkedDisplayName
displayProps={{
account: status.account,
variant: 'simple',
}}
className='status__display-name muted'
/>
),
}}
tagName='span'
/>
</div>
);
}
if (status.visibility === 'direct') {
return (
<div className='status__prepend'>
<div className='status__prepend__icon'>
<Icon id='at' icon={AlternateEmailIcon} />
</div>
<FormattedMessage
id='status.direct_indicator'
defaultMessage='Private mention'
tagName='span'
/>
</div>
);
}
if (showThread && status.in_reply_to_account_id) {
return (
<StatusThreadLabel
accountId={status.account.id}
inReplyToAccountId={status.in_reply_to_account_id}
/>
);
}
return null;
};

View File

@@ -0,0 +1,353 @@
import type React from 'react';
import { useCallback, useMemo } from 'react';
import classNames from 'classnames';
import type { Merge } from 'type-fest';
import { selectPlainAccount } from '@/mastodon/selectors/accounts';
import { selectStatusFilters } from '@/mastodon/selectors/filters';
import { selectExpandedStatus } from '@/mastodon/selectors/statuses';
import { createAppSelector, useAppSelector } from '@/mastodon/store';
import { ContentWarning } from '../content_warning';
import { FilterWarning } from '../filter_warning';
import { computeHashtagBarForStatus, HashtagBar } from '../hashtag_bar';
import { Hotkeys } from '../hotkeys';
import { StatusActionBar } from './action_bar';
import { StatusAttachments } from './attachments';
import { StatusContent } from './content';
import { StatusHeader } from './header';
import type { StatusHandlers } from './hooks';
import { useStatusHandlers, useTextForScreenReader } from './hooks';
import { StatusPrepend } from './prepend';
import type { StatusContainerProps, StatusContextType } from './types';
type StatusRedesignProps = Merge<
Omit<StatusContainerProps, 'account'>,
{
accountId?: string;
contextType?: StatusContextType;
onClick?: () => void;
}
>;
const selectStatusReblog = createAppSelector(
[(state, id?: string | null) => selectExpandedStatus(state, id ?? undefined)],
(status) => {
if (!status) {
return {};
}
if (!status.reblog) {
return { status };
}
const { reblog, ...statusRest } = status;
return {
status: reblog,
parent: statusRest,
};
},
);
export const StatusRedesign: React.FC<StatusRedesignProps> = ({
id,
muted,
rootId,
previousId,
nextId,
unread,
skipPrepend,
unfocusable,
contextType,
featured,
isQuotedPost,
accountId,
hidden,
shouldHighlightOnMount,
showActions,
scrollKey,
children,
headerRenderFn,
avatarSize,
withCounters,
withDismiss,
onClick,
showThread,
}) => {
// Select data from store
const { status, parent } = useAppSelector((state) =>
selectStatusReblog(state, id),
);
const account = useAppSelector(
(state) =>
parent?.account ?? selectPlainAccount(state, accountId) ?? undefined,
);
const matchedFilters = useAppSelector((state) =>
selectStatusFilters(state, { contextType, statusId: parent?.id ?? id }),
);
const statusId = status?.id;
// Display
const screenReaderText = useTextForScreenReader({
statusId,
reblogAcct: parent?.account.acct,
isQuote: isQuotedPost,
});
const { statusContent, hashtagsInBar } = useMemo(
(): Partial<ReturnType<typeof computeHashtagBarForStatus>> =>
status ? computeHashtagBarForStatus(status) : {},
[status],
);
// Handlers
const {
showDespiteFilter,
onHeaderClick,
onExpandedToggle,
onFilterToggle,
onOpenClick,
onTranslate,
...handlers
} = useStatusHandlers({ status, contextType, onClick });
if (!status) {
return null; // loading state
}
const actualStatus = parent ?? status;
const expanded =
(matchedFilters.length === 0 || showDespiteFilter) &&
(!status.hidden || !status.spoiler_text);
const hotkeysProps = {
...handlers,
onTranslate,
muted,
unfocusable,
} satisfies Omit<React.ComponentProps<typeof StatusHotkeys>, 'children'>;
if (hidden) {
return (
<StatusHotkeys {...hotkeysProps}>
<div
className={classNames('status__wrapper', { focusable: !muted })}
tabIndex={unfocusable ? undefined : 0}
>
<span>{status.account.display_name || status.account.username}</span>
{status.spoiler_text && <span>{status.spoiler_text}</span>}
{expanded && <span>{status.content}</span>}
</div>
</StatusHotkeys>
);
}
const header = headerRenderFn ? (
headerRenderFn({
statusId: status.id,
account,
avatarSize,
onHeaderClick,
featured,
})
) : (
<StatusHeader
statusId={status.id}
account={account}
avatarSize={avatarSize}
onHeaderClick={onHeaderClick}
/>
);
return (
<StatusHotkeys {...hotkeysProps}>
<div
className={classNames(
'status__wrapper',
`status__wrapper-${status.visibility}`,
{
'status__wrapper-reply': !!status.in_reply_to_id,
'status__wrapper--in-thread': !!rootId,
unread,
focusable: !muted,
},
)}
tabIndex={muted || unfocusable ? undefined : 0}
data-featured={featured ? 'true' : null}
aria-label={screenReaderText}
data-nosnippet={status.account.noindex || undefined}
>
{!skipPrepend && (
<StatusPrepend
status={actualStatus}
isReblog={!!parent}
showThread={showThread}
/>
)}
<StatusContentWrapper
statusId={status.id}
inReplyToId={actualStatus.in_reply_to_id}
rootId={rootId}
nextId={nextId}
previousId={previousId}
className={classNames(`status-${status.visibility}`, {
muted,
'status--is-quote': isQuotedPost,
'status--has-quote': !!status.quote,
'status--highlighted-entry': shouldHighlightOnMount,
})}
>
{header}
{matchedFilters.length > 0 && (
<FilterWarning
title={matchedFilters.map((filter) => filter.title).join(', ')}
expanded={showDespiteFilter}
onClick={onFilterToggle}
/>
)}
{(matchedFilters.length === 0 || showDespiteFilter) && (
<ContentWarning
statusId={status.id}
expanded={expanded}
onClick={onExpandedToggle}
/>
)}
{expanded && (
<>
<StatusContent
statusId={status.id}
statusContent={statusContent}
onClick={onOpenClick}
onTranslate={onTranslate}
collapsible
/>
<StatusAttachments
statusId={status.id}
contextType={contextType}
/>
{hashtagsInBar && (
<HashtagBar
hashtags={hashtagsInBar}
accountId={status.account.id}
/>
)}
{children}
</>
)}
{showActions && !isQuotedPost && (
<StatusActionBar
scrollKey={scrollKey}
statusId={status.id}
contextType={contextType}
withDismiss={withDismiss}
withCounters={withCounters}
/>
)}
</StatusContentWrapper>
</div>
</StatusHotkeys>
);
};
const StatusHotkeys: React.FC<
{
muted?: boolean;
unfocusable?: boolean;
children: React.ReactNode;
} & Omit<
StatusHandlers,
| 'showDespiteFilter'
| 'onOpenClick'
| 'onHeaderClick'
| 'onExpandedToggle'
| 'onFilterToggle'
>
> = ({ muted, unfocusable, children, ...handlers }) => {
const onOpen = useCallback(() => {
handlers.onOpen();
}, [handlers]);
if (muted) {
return children;
}
return (
<Hotkeys
handlers={{
reply: handlers.onReply,
favourite: handlers.onFavourite,
boost: handlers.onBoost,
quote: handlers.onQuote,
mention: handlers.onMention,
open: onOpen,
openProfile: handlers.onOpenProfile,
toggleHidden: handlers.onToggleHidden,
// TODO: This is handled in a child component, so needs to be fixed.
// toggleSensitive: onMediaShowToggle,
openMedia: handlers.onOpenMedia,
onTranslate: handlers.onTranslate,
}}
focusable={!unfocusable}
>
{children}
</Hotkeys>
);
};
const StatusContentWrapper: React.FC<
Pick<StatusRedesignProps, 'rootId' | 'previousId' | 'nextId' | 'children'> & {
statusId: string;
inReplyToId?: string;
className?: string;
}
> = ({
statusId,
inReplyToId,
rootId,
previousId,
nextId,
className,
children,
}) => {
const nextInReplyToId = useAppSelector((state) =>
nextId ? state.statuses.getIn([nextId, 'in_reply_to_id']) : null,
);
const connectUp = !!previousId && previousId === inReplyToId;
const connectToRoot = !!rootId && rootId === inReplyToId;
const connectReply = !!nextInReplyToId && nextInReplyToId === statusId;
return (
<div
className={classNames(
'status',
{
'status-reply': !!inReplyToId,
'status--in-thread': !!rootId,
'status--first-in-thread':
previousId && (!connectUp || connectToRoot),
},
className,
)}
data-id={statusId}
>
{(connectReply || connectUp || connectToRoot) && (
<div
className={classNames('status__line', {
'status__line--full': connectReply,
'status__line--first': !inReplyToId && !connectToRoot,
})}
/>
)}
{children}
</div>
);
};

View File

@@ -25,8 +25,9 @@ export interface StatusContainerProps {
id?: string | null;
account?: TAccount;
children?: ReactNode;
previousId?: string;
rootId?: string;
previousId?: string;
nextId?: string;
onClick?: MouseEventHandler<HTMLDivElement>;
muted?: boolean;
hidden?: boolean;
@@ -54,7 +55,7 @@ export const TypedStatusContainer =
StatusContainer as ComponentType<StatusContainerProps>;
// Taken from the Status component.
export interface StatusProps extends StatusContainerProps {
export interface StatusProps extends Omit<StatusContainerProps, 'nextId'> {
status: TStatus;
nextInReplyToId?: string;
onReply: (status: TStatus) => void;

View File

@@ -1,10 +1,13 @@
import { selectExpandedStatus, selectPlainStatus } from '../selectors/statuses';
import { useAppSelector } from '../store';
export function useStatus(id: string) {
export function useStatus(id?: string | null) {
return useAppSelector((state) => selectPlainStatus(state, id));
}
export function useExpandedStatus(id: string) {
return useAppSelector((state) => selectExpandedStatus(state, id));
/** Adds reblog status and account information to standard Status */
export function useExpandedStatus(id?: string | null) {
return useAppSelector((state) =>
selectExpandedStatus(state, id ?? undefined),
);
}

View File

@@ -50,6 +50,7 @@ export interface StatusShape {
collapsed: boolean | null;
uri: string;
url: string | null;
isLoading?: boolean;
// Content
content: string;

View File

@@ -1,8 +1,10 @@
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'mastodon/store';
import { createAppSelector } from 'mastodon/store';
import { toServerSideType } from 'mastodon/utils/filters';
import type { StatusContextType } from '../components/status/types';
import { selectExpandedStatus } from './statuses';
export interface FilterShape {
id: string;
title: string;
@@ -17,9 +19,9 @@ type Filter = Immutable.Map<string, unknown>;
// TODO: move to `app/javascript/mastodon/models` and use more globally
type FilterResult = Immutable.Map<string, unknown>;
export const getFilters = createSelector(
export const getFilters = createAppSelector(
[
(state: RootState) => state.filters as Immutable.Map<string, Filter>,
(state) => state.filters as Immutable.Map<string, Filter>,
(_, { contextType }: { contextType?: string }) => contextType,
],
(filters, contextType) => {
@@ -41,18 +43,93 @@ export const getFilters = createSelector(
},
);
export const getStatusHidden = (
state: RootState,
{ id, contextType }: { id: string; contextType: string },
) => {
const filters = getFilters(state, { contextType });
if (filters === null) return false;
export const selectPlainFilters = createAppSelector([getFilters], (filters) => {
if (!filters) {
return null;
}
return filters.toJS() as unknown as Record<string, FilterShape>;
});
const filtered = state.statuses.getIn([id, 'filtered']) as
| Immutable.List<FilterResult>
| undefined;
return filtered?.some(
(result) =>
filters.getIn([result.get('filter'), 'filter_action']) === 'hide',
);
};
export const selectStatusFilters = createAppSelector(
[
(state, { statusId }: { statusId?: string | null }) =>
selectExpandedStatus(state, statusId ?? undefined),
selectPlainFilters,
(_, { warnInsteadOfHide }: { warnInsteadOfHide?: boolean }) =>
warnInsteadOfHide,
],
(status, filters) => {
const results: FilterShape[] = [];
if (!status || !filters) {
return results;
}
const filtered = status.reblog?.filtered ?? status.filtered;
for (const result of filtered) {
const filter = filters[result.filter];
if (!filter) {
continue;
}
results.push(filter);
}
return results;
},
);
export const selectStatusLoadingState = createAppSelector(
[
(state, { statusId }: { statusId?: string | null }) =>
selectExpandedStatus(state, statusId ?? undefined),
selectStatusFilters,
(_, { warnInsteadOfHide }: { warnInsteadOfHide?: boolean }) =>
warnInsteadOfHide,
],
(status, filters, warnInsteadOfHide) => {
if (!status) {
return { state: 'not-found', status: null };
}
if (status.isLoading) {
return { state: 'loading', status: null };
}
if (
!warnInsteadOfHide &&
filters.some((filter) => filter.filter_action === 'hide')
) {
return { state: 'filtered', status: null };
}
return { state: 'loaded', status };
},
);
export const selectMediaFilters = createAppSelector(
[selectStatusFilters],
(filters) =>
filters
.filter((filter) => filter.filter_action === 'blur')
.map((filter) => filter.title),
);
export const getStatusHidden = createAppSelector(
[
(state, { contextType }: { contextType: StatusContextType }) =>
getFilters(state, { contextType }),
(state, { id }: { id: string }) =>
state.statuses.getIn([id, 'filtered']) as
| Immutable.List<FilterResult>
| undefined,
],
(filters, filtered) => {
if (!filters) {
return false;
}
return filtered?.some(
(result) =>
filters.getIn([result.get('filter'), 'filter_action']) === 'hide',
);
},
);

View File

@@ -8,8 +8,6 @@ import type {
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(
[
@@ -20,7 +18,7 @@ export const getStatusList = createAppSelector(
);
export const selectPlainStatus = createAppSelector(
[(state, statusId: string) => state.statuses.get(statusId)],
[(state, statusId?: string | null) => state.statuses.get(statusId ?? '')],
(status) => {
if (!status) {
return null;
@@ -159,6 +157,7 @@ export const selectStatusInteractions = createAppSelector(
reply: addAllowed({ isLoggedIn }),
report: addAllowed({ isLoggedIn, isNotMine }),
revokeQuote: addAllowed({ isQuoted, isNotMine }),
translate: addAllowed({ isLoggedIn }),
};
return {
@@ -202,26 +201,3 @@ export const selectPictureInPicture = createAppSelector(
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;
},
);