Update content & placement of "sensitive content" warning on collection page (#39069)

This commit is contained in:
diondiondion
2026-05-18 15:08:41 +02:00
committed by GitHub
parent 99b72f60ad
commit dcb6dbbc86
5 changed files with 128 additions and 90 deletions

View File

@@ -67,9 +67,14 @@ interface LinkProps
extends React.ComponentPropsWithoutRef<typeof Link>, ContentProps {}
export const ListItemLink = polymorphicForwardRef<'h3', LinkProps>(
({ as, subtitle, children, className, ...otherProps }, ref) => {
({ as, subtitle, subtitleId, children, className, ...otherProps }, ref) => {
return (
<ListItemContent ref={ref} as={as} subtitle={subtitle}>
<ListItemContent
ref={ref}
as={as}
subtitle={subtitle}
subtitleId={subtitleId}
>
<Link className={classNames(className, 'focusable')} {...otherProps}>
{children}
</Link>
@@ -82,9 +87,14 @@ interface ButtonProps
extends React.ComponentPropsWithoutRef<'button'>, ContentProps {}
export const ListItemButton = polymorphicForwardRef<'h3', ButtonProps>(
({ as, subtitle, children, className, ...otherProps }, ref) => {
({ as, subtitle, subtitleId, children, className, ...otherProps }, ref) => {
return (
<ListItemContent as={as} ref={ref} subtitle={subtitle}>
<ListItemContent
as={as}
ref={ref}
subtitle={subtitle}
subtitleId={subtitleId}
>
<button
type='button'
className={classNames(className, 'focusable')}

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@@ -14,7 +14,6 @@ import {
AccountListItemFollowButton,
} from 'mastodon/components/account_list_item';
import { Button } from 'mastodon/components/button';
import { Callout } from 'mastodon/components/callout';
import {
Article,
ItemList,
@@ -35,50 +34,6 @@ const messages = defineMessages({
},
});
const SensitiveScreen: React.FC<{
sensitive: boolean | undefined;
focusTargetRef: React.RefObject<HTMLHeadingElement>;
children: React.ReactNode;
}> = ({ sensitive, focusTargetRef, children }) => {
const [isVisible, setIsVisible] = useState(!sensitive);
const showAnyway = useCallback(() => {
setIsVisible(true);
setTimeout(() => {
focusTargetRef.current?.focus();
}, 0);
}, [focusTargetRef]);
if (isVisible) {
return children;
}
return (
<Callout
variant='warning'
title={
<FormattedMessage
id='collections.detail.sensitive_content'
defaultMessage='Sensitive content'
/>
}
primaryLabel={
<FormattedMessage
id='content_warning.show_short'
defaultMessage='Show'
/>
}
onPrimary={showAnyway}
className={classes.sensitiveScreen}
>
<FormattedMessage
id='collections.detail.sensitive_note'
defaultMessage='The description and accounts may not be suitable for all viewers.'
/>
</Callout>
);
};
type CollectionItemWithAccount = CollectionAccountItem & {
account?: Account | null;
};
@@ -203,35 +158,30 @@ export const CollectionAccountsList: React.FC<{
values={{ count: collection.item_count }}
/>
</h3>
<SensitiveScreen
sensitive={!isOwnCollection && collection.sensitive}
focusTargetRef={listHeadingRef}
>
<ItemList emptyMessage={intl.formatMessage(messages.empty)}>
<TruncatedListItems
visibleItems={visibleAccounts}
truncatedItems={hiddenAccounts}
toggleButton={{
icon: VisibilityOffIcon,
title: (
<FormattedMessage
id='collections.hidden_accounts_link'
defaultMessage='{count, plural, one {# hidden account} other {# hidden accounts}}'
values={{ count: hiddenAccounts.length }}
/>
),
subtitle: (
<FormattedMessage
id='collections.hidden_accounts_description'
defaultMessage='Youve blocked or muted {count, plural, one {this user} other {these users}}'
values={{ count: hiddenAccounts.length }}
/>
),
}}
renderListItem={renderListItem}
/>
</ItemList>
</SensitiveScreen>
<ItemList emptyMessage={intl.formatMessage(messages.empty)}>
<TruncatedListItems
visibleItems={visibleAccounts}
truncatedItems={hiddenAccounts}
toggleButton={{
icon: VisibilityOffIcon,
title: (
<FormattedMessage
id='collections.hidden_accounts_link'
defaultMessage='{count, plural, one {# hidden account} other {# hidden accounts}}'
values={{ count: hiddenAccounts.length }}
/>
),
subtitle: (
<FormattedMessage
id='collections.hidden_accounts_description'
defaultMessage='Youve blocked or muted {count, plural, one {this user} other {these users}}'
values={{ count: hiddenAccounts.length }}
/>
),
}}
renderListItem={renderListItem}
/>
</ItemList>
</>
);
};

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@@ -138,9 +138,40 @@ export const PendingNote: React.FC = () => {
);
};
const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
collection,
const SensitiveContentNote: React.FC<{ onReveal: () => void }> = ({
onReveal,
}) => {
return (
<Callout
variant='warning'
title={
<FormattedMessage
id='collections.detail.sensitive_content'
defaultMessage='Sensitive content'
/>
}
primaryLabel={
<FormattedMessage
id='content_warning.show_short'
defaultMessage='Show'
/>
}
onPrimary={onReveal}
className={classes.sensitiveScreen}
>
<FormattedMessage
id='collections.detail.sensitive_note'
defaultMessage='The description and accounts may not be suitable for all viewers.'
/>
</Callout>
);
};
const CollectionHeader: React.FC<{
collection: ApiCollectionJSON;
withDescription: boolean;
headingRef: React.RefObject<HTMLHeadingElement>;
}> = ({ collection, withDescription, headingRef }) => {
const intl = useIntl();
const { name, description, tag, account_id, items } = collection;
const dispatch = useAppDispatch();
@@ -181,7 +212,9 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
<div className={classes.titleWithMenu}>
<div className={classes.titleWrapper}>
{tag && <Badge label={`#${tag.name}`} icon={null} />}
<h2 className={classes.name}>{name}</h2>
<h2 className={classes.name} ref={headingRef} tabIndex={-1}>
{name}
</h2>
<AuthorNote id={account_id} />
</div>
<div className={classes.headerButtonWrapper}>
@@ -199,7 +232,9 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
/>
</div>
</div>
{description && <p className={classes.description}>{description}</p>}
{withDescription && description && (
<p className={classes.description}>{description}</p>
)}
{hasPendingAccounts && <PendingNote />}
{isCurrentUserInCollection && (
<RevokeControls
@@ -211,6 +246,52 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
);
};
function useRevealSensitiveContent({
sensitive,
}: {
sensitive: boolean | undefined;
}) {
const postRevealFocusTargetRef = useRef<HTMLHeadingElement>(null);
const [isContentVisible, setIsContentVisible] = useState(!sensitive);
const revealContent = useCallback(() => {
setIsContentVisible(true);
setTimeout(() => {
postRevealFocusTargetRef.current?.focus();
}, 0);
}, [postRevealFocusTargetRef]);
return {
isContentVisible,
revealContent,
postRevealFocusTargetRef,
};
}
const ColumnContent: React.FC<{
collection: ApiCollectionJSON;
}> = ({ collection }) => {
const { isContentVisible, revealContent, postRevealFocusTargetRef } =
useRevealSensitiveContent({
sensitive: collection.sensitive && collection.account_id !== me,
});
return (
<>
<CollectionHeader
collection={collection}
headingRef={postRevealFocusTargetRef}
withDescription={isContentVisible}
/>
{isContentVisible ? (
<CollectionAccountsList collection={collection} />
) : (
<SensitiveContentNote onReveal={revealContent} />
)}
</>
);
};
export const CollectionDetailPage: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
@@ -241,10 +322,7 @@ export const CollectionDetailPage: React.FC<{
<Scrollable>
{collection ? (
<>
<CollectionHeader collection={collection} />
<CollectionAccountsList collection={collection} />
</>
<ColumnContent collection={collection} />
) : (
<LoadingIndicator />
)}

View File

@@ -64,7 +64,7 @@
}
.sensitiveScreen {
margin: 16px;
margin-inline: 16px;
}
.columnSubheading {

View File

@@ -394,7 +394,7 @@
"collections.detail.loading": "Loading collection…",
"collections.detail.revoke_inclusion": "Remove me",
"collections.detail.sensitive_content": "Sensitive content",
"collections.detail.sensitive_note": "This collection contains accounts and content that may be sensitive to some users.",
"collections.detail.sensitive_note": "The description and accounts may not be suitable for all viewers.",
"collections.detail.share": "Share this collection",
"collections.detail.you_are_in_this_collection": "You're featured in this collection",
"collections.edit_details": "Edit details",