Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add content warning support to grouped notifications #2852

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions app/javascript/flavours/glitch/components/content_warning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* Significantly rewritten from upstream to keep the old design for now */

import { FormattedMessage } from 'react-intl';

export const ContentWarning: React.FC<{
text: string;
expanded?: boolean;
onClick?: () => void;
icons?: React.ReactNode[];
}> = ({ text, expanded, onClick, icons }) => (
<p>
<span dangerouslySetInnerHTML={{ __html: text }} className='translate' />{' '}
<button
type='button'
className='status__content__spoiler-link'
onClick={onClick}
aria-expanded={expanded}
>
{expanded ? (
<FormattedMessage id='status.show_less' defaultMessage='Show less' />
) : (
<FormattedMessage id='status.show_more' defaultMessage='Show more' />
)}
{icons}
</button>
</p>
);
23 changes: 23 additions & 0 deletions app/javascript/flavours/glitch/components/filter_warning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FormattedMessage } from 'react-intl';

import { StatusBanner, BannerVariant } from './status_banner';

export const FilterWarning: React.FC<{
title: string;
expanded?: boolean;
onClick?: () => void;
}> = ({ title, expanded, onClick }) => (
<StatusBanner
expanded={expanded}
onClick={onClick}
variant={BannerVariant.Blue}
>
<p>
<FormattedMessage
id='filter_warning.matches_filter'
defaultMessage='Matches filter “{title}”'
values={{ title }}
/>
</p>
</StatusBanner>
);
37 changes: 37 additions & 0 deletions app/javascript/flavours/glitch/components/status_banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { FormattedMessage } from 'react-intl';

export enum BannerVariant {
Yellow = 'yellow',
Blue = 'blue',
}

export const StatusBanner: React.FC<{
children: React.ReactNode;
variant: BannerVariant;
expanded?: boolean;
onClick?: () => void;
}> = ({ children, variant, expanded, onClick }) => (
<div
className={
variant === BannerVariant.Yellow
? 'content-warning'
: 'content-warning content-warning--filter'
}
>
{children}

<button className='link-button' onClick={onClick}>
{expanded ? (
<FormattedMessage
id='content_warning.hide'
defaultMessage='Hide post'
/>
) : (
<FormattedMessage
id='content_warning.show'
defaultMessage='Show anyway'
/>
)}
</button>
</div>
);
70 changes: 22 additions & 48 deletions app/javascript/flavours/glitch/components/status_content.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
import MovieIcon from '@/material-icons/400-24px/movie.svg?react';
import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
import { ContentWarning } from 'flavours/glitch/components/content_warning';
import { Icon } from 'flavours/glitch/components/icon';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
Expand Down Expand Up @@ -350,7 +351,7 @@ class StatusContent extends PureComponent {
const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);

const content = { __html: statusContent ?? getStatusContent(status) };
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const spoilerHtml = status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml');
const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', {
'status__content--with-action': parseClick && !disabled,
Expand All @@ -375,45 +376,26 @@ class StatusContent extends PureComponent {
</Permalink>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);

let toggleText = null;
if (hidden) {
toggleText = [
<FormattedMessage
id='status.show_more'
defaultMessage='Show more'
key='0'
/>,
];
if (mediaIcons) {
const mediaComponents = {
'link': LinkIcon,
'picture-o': ImageIcon,
'tasks': InsertChartIcon,
'video-camera': MovieIcon,
'music': MusicNoteIcon,
};

mediaIcons.forEach((mediaIcon, idx) => {
toggleText.push(
<Icon
fixedWidth
className='status__content__spoiler-icon'
id={mediaIcon}
icon={mediaComponents[mediaIcon]}
aria-hidden='true'
key={`icon-${idx}`}
/>,
);
});
}
} else {
toggleText = (
<FormattedMessage
id='status.show_less'
defaultMessage='Show less'
key='0'
let spoilerIcons = [];
if (hidden && mediaIcons) {
const mediaComponents = {
'link': LinkIcon,
'picture-o': ImageIcon,
'tasks': InsertChartIcon,
'video-camera': MovieIcon,
'music': MusicNoteIcon,
};

spoilerIcons = mediaIcons.map((mediaIcon) => (
<Icon
fixedWidth
className='status__content__spoiler-icon'
id={mediaIcon}
icon={mediaComponents[mediaIcon]}
aria-hidden='true'
key={`icon-${mediaIcon}`}
/>
);
));
}

if (hidden) {
Expand All @@ -422,15 +404,7 @@ class StatusContent extends PureComponent {

return (
<div className={classNames} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p
style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
>
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={language} />
{' '}
<button type='button' className='status__content__spoiler-link' onClick={this.handleSpoilerClick} aria-expanded={!hidden}>
{toggleText}
</button>
</p>
<ContentWarning text={spoilerHtml} expanded={!hidden} onClick={this.handleSpoilerClick} icons={spoilerIcons} />

{mentionsPlaceholder}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import type { List as ImmutableList, RecordOf } from 'immutable';

import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
import { toggleStatusSpoilers } from 'flavours/glitch/actions/statuses';
import { Avatar } from 'flavours/glitch/components/avatar';
import { ContentWarning } from 'flavours/glitch/components/content_warning';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { Icon } from 'flavours/glitch/components/icon';
import type { Status } from 'flavours/glitch/models/status';
import { useAppSelector } from 'flavours/glitch/store';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';

import { EmbeddedStatusContent } from './embedded_status_content';

Expand All @@ -23,6 +25,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
}) => {
const history = useHistory();
const clickCoordinatesRef = useRef<[number, number] | null>();
const dispatch = useAppDispatch();

const status = useAppSelector(
(state) => state.statuses.get(statusId) as Status | undefined,
Expand Down Expand Up @@ -96,15 +99,21 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
[],
);

const handleContentWarningClick = useCallback(() => {
dispatch(toggleStatusSpoilers(statusId));
}, [dispatch, statusId]);

if (!status) {
return null;
}

// Assign status attributes to variables with a forced type, as status is not yet properly typed
const contentHtml = status.get('contentHtml') as string;
const contentWarning = status.get('spoilerHtml') as string;
const poll = status.get('poll');
const language = status.get('language') as string;
const mentions = status.get('mentions') as ImmutableList<Mention>;
const expanded = !status.get('hidden') || !contentWarning;
const mediaAttachmentsSize = (
status.get('media_attachments') as ImmutableList<unknown>
).size;
Expand All @@ -124,14 +133,24 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
<DisplayName account={account} />
</div>

<EmbeddedStatusContent
className='notification-group__embedded-status__content reply-indicator__content translate'
content={contentHtml}
language={language}
mentions={mentions}
/>
{contentWarning && (
<ContentWarning
text={contentWarning}
onClick={handleContentWarningClick}
expanded={expanded}
/>
)}

{(!contentWarning || expanded) && (
<EmbeddedStatusContent
className='notification-group__embedded-status__content reply-indicator__content translate'
content={contentHtml}
language={language}
mentions={mentions}
/>
)}

{(poll || mediaAttachmentsSize > 0) && (
{expanded && (poll || mediaAttachmentsSize > 0) && (
<div className='notification-group__embedded-status__attachments reply-indicator__attachments'>
{!!poll && (
<>
Expand Down
Loading
Loading