From bcc30bfdd7b2bfc0840efc9c7846ca5775f9b8e4 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 16 Sep 2026 12:33:10 +0000 Subject: [PATCH] Status redesign: Filters and spoilers (#40550) --- .../mastodon/components/status/content.tsx | 7 +- .../mastodon/components/status/hooks.ts | 20 +--- .../mastodon/components/status/status.tsx | 82 ++++++-------- .../components/status/styles.module.scss | 76 ++++++++++++- .../mastodon/components/status/warning.tsx | 105 ++++++++++++++++++ app/javascript/mastodon/locales/en.json | 1 + app/javascript/mastodon/selectors/filters.ts | 26 +++-- app/javascript/mastodon/selectors/statuses.ts | 9 +- 8 files changed, 241 insertions(+), 85 deletions(-) create mode 100644 app/javascript/mastodon/components/status/warning.tsx diff --git a/app/javascript/mastodon/components/status/content.tsx b/app/javascript/mastodon/components/status/content.tsx index 0a1038e1823..4181302d419 100644 --- a/app/javascript/mastodon/components/status/content.tsx +++ b/app/javascript/mastodon/components/status/content.tsx @@ -1,4 +1,3 @@ -import type React from 'react'; import { useCallback, useState } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -51,12 +50,10 @@ export const StatusContent: React.FC< const maxHeight = lineHeightPx * MAX_LINES; setCollapsed( - (node.clientHeight > maxHeight || - node.scrollWidth > node.clientWidth) && - !status.spoiler_text, + node.clientHeight > maxHeight || node.scrollWidth > node.clientWidth, ); }, - [collapsed, status.spoiler_text], + [collapsed], ); const htmlHandlers = useHandlersForStatus(status); diff --git a/app/javascript/mastodon/components/status/hooks.ts b/app/javascript/mastodon/components/status/hooks.ts index 6d372ecc3b2..d82b5c33e44 100644 --- a/app/javascript/mastodon/components/status/hooks.ts +++ b/app/javascript/mastodon/components/status/hooks.ts @@ -56,7 +56,7 @@ export function useStatusHandlers({ contextType?: StatusContextType; onOpen?: () => void; }) { - const matchedFilters = useAppSelector((state) => + const { filterAction } = useAppSelector((state) => selectStatusFilters(state, { contextType, statusId: status?.id }), ); const [showDespiteFilter, { onToggle: onFilterToggle }] = useToggle(false); @@ -65,28 +65,18 @@ export function useStatusHandlers({ const statusId = status?.id; // Display handlers - const onExpandedToggle = useCallback(() => { - dispatch(toggleStatusSpoilers(statusId)); - }, [dispatch, statusId]); - const onToggleHidden = useCallback(() => { if (!status) { return; } - if (!matchedFilters.length || showDespiteFilter) { + if (!filterAction || showDespiteFilter) { dispatch(toggleStatusSpoilers(status.id)); } if (!status.hidden || !status.spoiler_text) { onFilterToggle(); } - }, [ - dispatch, - matchedFilters.length, - onFilterToggle, - showDespiteFilter, - status, - ]); + }, [dispatch, filterAction, onFilterToggle, showDespiteFilter, status]); // Interaction handlers const handlerFactory = useCallback( @@ -185,9 +175,9 @@ export function useStatusHandlers({ return useMemo( () => ({ + isFiltered: !!filterAction, showDespiteFilter, onOpenClick, - onExpandedToggle, onFilterToggle, onMention, onOpen: () => { @@ -203,8 +193,8 @@ export function useStatusHandlers({ onTranslate: handlerFactory('translate'), }), [ + filterAction, handlerFactory, - onExpandedToggle, onFilterToggle, onMention, onOpenCallback, diff --git a/app/javascript/mastodon/components/status/status.tsx b/app/javascript/mastodon/components/status/status.tsx index 1add81732d1..b3b7af29151 100644 --- a/app/javascript/mastodon/components/status/status.tsx +++ b/app/javascript/mastodon/components/status/status.tsx @@ -1,15 +1,12 @@ -import { useMemo } from 'react'; +import { useId, useMemo } from 'react'; import classNames from 'classnames'; import type { Merge } from 'type-fest'; -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 } from '../hashtag_bar'; import { Hotkeys } from '../hotkeys'; import { Poll } from '../poll'; @@ -30,6 +27,7 @@ import { StatusRedesignHeader } from './redesign/header'; import classes from './styles.module.scss'; import { TranslateButton } from './translate'; import type { StatusContainerProps, StatusContextType } from './types'; +import { StatusWarning } from './warning'; type StatusRedesignProps = Merge< Omit, @@ -83,9 +81,6 @@ export const StatusRedesign: React.FC = ({ const { status, parent } = useAppSelector((state) => selectStatusReblog(state, id), ); - const matchedFilters = useAppSelector((state) => - selectStatusFilters(state, { contextType, statusId: parent?.id ?? id }), - ); const statusId = status?.id; // Display @@ -99,15 +94,20 @@ export const StatusRedesign: React.FC = ({ status ? computeHashtagBarForStatus(status) : {}, [status], ); + const contentWrapperId = useId(); // Handlers const { + isFiltered, showDespiteFilter, - onExpandedToggle, onFilterToggle, onTranslate, ...handlers - } = useStatusHandlers({ status, contextType, onOpen }); + } = useStatusHandlers({ + status, + contextType, + onOpen, + }); if (!status) { return null; // loading state @@ -115,10 +115,6 @@ export const StatusRedesign: React.FC = ({ const actualStatus = parent ?? status; - const expanded = - (matchedFilters.length === 0 || showDespiteFilter) && - (!status.hidden || !status.spoiler_text); - const hotkeysProps = { handlers: { ...handlers, @@ -129,21 +125,20 @@ export const StatusRedesign: React.FC = ({ 'data-id': id, }; + const isHidden = + (!showDespiteFilter && isFiltered) || + (!!status.spoiler_text && status.hidden); + if (hidden) { return ( {status.account.display_name || status.account.username} {status.spoiler_text && {status.spoiler_text}} - {expanded && {status.content}} + {!isHidden && {status.content}} ); } - const showFooter = - (expanded && hashtagsInBar.length > 0) || - variant === 'page' || - (showActions && !isQuotedPost); - return ( = ({ {headerContents} - {matchedFilters.length > 0 && ( - filter.title).join(', ')} - expanded={showDespiteFilter} - onClick={onFilterToggle} - /> - )} - - {(matchedFilters.length === 0 || showDespiteFilter) && ( - - )} - - {expanded && ( + + +
= ({ {children} - )} - {showFooter && ( + +
+ + {(variant === 'page' || (showActions && !isQuotedPost)) && (