Change status content markup to match upstream (#2923)

* Remove option to have media outside of CWs

Upstream adopted the media-in-CW design glitch-soc originally had.

* Move poll to StatusContent

* Refactor status media icons

* Rename `forceFilter` to `showDespiteFilter` for consistency with upstream

* Change media and status content markup to match upstream's

* Add mention placeholders back
This commit is contained in:
Claire 2024-12-29 19:59:19 +01:00 committed by GitHub
parent 5e65586161
commit b7afca0f05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 231 additions and 329 deletions

View File

@ -1,17 +1,25 @@
import type { IconName } from './media_icon';
import { MediaIcon } from './media_icon';
import { StatusBanner, BannerVariant } from './status_banner';
export const ContentWarning: React.FC<{
text: string;
expanded?: boolean;
onClick?: () => void;
icons?: React.ReactNode[];
icons?: IconName[];
}> = ({ text, expanded, onClick, icons }) => (
<StatusBanner
expanded={expanded}
onClick={onClick}
variant={BannerVariant.Warning}
>
{icons}
{icons?.map((icon) => (
<MediaIcon
className='status__content__spoiler-icon'
icon={icon}
key={`icon-${icon}`}
/>
))}
<p dangerouslySetInnerHTML={{ __html: text }} />
</StatusBanner>
);

View File

@ -0,0 +1,55 @@
import { defineMessages, useIntl } from 'react-intl';
import ImageIcon from '@/material-icons/400-24px/image.svg?react';
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 { Icon } from 'flavours/glitch/components/icon';
const messages = defineMessages({
link: {
id: 'status.has_preview_card',
defaultMessage: 'Features an attached preview card',
},
'picture-o': {
id: 'status.has_pictures',
defaultMessage: 'Features attached pictures',
},
tasks: { id: 'status.is_poll', defaultMessage: 'This toot is a poll' },
'video-camera': {
id: 'status.has_video',
defaultMessage: 'Features attached videos',
},
music: {
id: 'status.has_audio',
defaultMessage: 'Features attached audio files',
},
});
const iconComponents = {
link: LinkIcon,
'picture-o': ImageIcon,
tasks: InsertChartIcon,
'video-camera': MovieIcon,
music: MusicNoteIcon,
};
export type IconName = keyof typeof iconComponents;
export const MediaIcon: React.FC<{
className?: string;
icon: IconName;
}> = ({ className, icon }) => {
const intl = useIntl();
return (
<Icon
className={className}
id={icon}
icon={iconComponents[icon]}
title={intl.formatMessage(messages[icon])}
aria-hidden='true'
/>
);
};

View File

@ -0,0 +1,29 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Permalink } from 'flavours/glitch/components/permalink';
export const MentionsPlaceholder = ({ status }) => {
if (status.get('spoiler_text').length === 0 || !status.get('mentions')) {
return null;
}
return (
<div className='status__content'>
{status.get('mentions').map(item => (
<Permalink
to={`/@${item.get('acct')}`}
href={item.get('url')}
key={item.get('id')}
className='mention'
>
@<span>{item.get('username')}</span>
</Permalink>
)).reduce((aggregate, item) => [...aggregate, item, ' '], [])}
</div>
);
};
MentionsPlaceholder.propTypes = {
status: ImmutablePropTypes.map.isRequired,
};

View File

@ -9,8 +9,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import { ContentWarning } from 'flavours/glitch/components/content_warning';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
import PollContainer from 'flavours/glitch/containers/poll_container';
import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/glitch/utils/react_router';
@ -28,6 +28,7 @@ import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay';
import { DisplayName } from './display_name';
import { getHashtagBarForStatus } from './hashtag_bar';
import { MentionsPlaceholder } from './mentions_placeholder';
import { Permalink } from './permalink';
import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
@ -134,7 +135,7 @@ class Status extends ImmutablePureComponent {
showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
revealBehindCW: undefined,
showCard: false,
forceFilter: undefined,
showDespiteFilter: undefined,
};
// Avoid checking props that are functions (and whose equality will always
@ -158,7 +159,7 @@ class Status extends ImmutablePureComponent {
updateOnStates = [
'isExpanded',
'showMedia',
'forceFilter',
'showDespiteFilter',
];
static getDerivedStateFromProps(nextProps, prevState) {
@ -242,7 +243,7 @@ class Status extends ImmutablePureComponent {
if (this.props.status?.get('id') !== prevProps.status?.get('id')) {
this.setState({
showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
forceFilter: undefined,
showDespiteFilter: undefined,
});
}
}
@ -399,12 +400,12 @@ class Status extends ImmutablePureComponent {
};
handleUnfilterClick = e => {
this.setState({ forceFilter: false });
this.setState({ showDespiteFilter: false });
e.preventDefault();
};
handleFilterClick = () => {
this.setState({ forceFilter: true });
this.setState({ showDespiteFilter: true });
};
handleRef = c => {
@ -448,27 +449,16 @@ class Status extends ImmutablePureComponent {
} = this.props;
let attachments = null;
// Depending on user settings, some media are considered as parts of the
// contents (affected by CW) while other will be displayed outside of the
// CW.
let contentMedia = [];
let contentMediaIcons = [];
let extraMedia = [];
let extraMediaIcons = [];
let media = contentMedia;
let mediaIcons = contentMediaIcons;
let media = [];
let mediaIcons = [];
let statusAvatar;
if (settings.getIn(['content_warnings', 'media_outside'])) {
media = extraMedia;
mediaIcons = extraMediaIcons;
}
if (status === null) {
return null;
}
const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded;
const expanded = isExpanded || status.get('spoiler_text').length === 0;
const handlers = {
reply: this.handleHotkeyReply,
@ -498,13 +488,13 @@ class Status extends ImmutablePureComponent {
<div ref={this.handleRef} className='status focusable' tabIndex={unfocusable ? null : 0}>
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
{status.get('spoiler_text').length > 0 && (<span>{status.get('spoiler_text')}</span>)}
{isExpanded && <span>{status.get('content')}</span>}
{expanded && <span>{status.get('content')}</span>}
</div>
</HotKeys>
);
}
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
if (this.state.showDespiteFilter === undefined ? matchedFilters : this.state.showDespiteFilter) {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
@ -552,7 +542,7 @@ class Status extends ImmutablePureComponent {
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
hidden={!isExpanded}
hidden={!expanded}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
@ -609,7 +599,7 @@ class Status extends ImmutablePureComponent {
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
preventPlayback={!isExpanded}
preventPlayback={!expanded}
onOpenVideo={this.handleOpenVideo}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
visible={this.state.showMedia}
@ -631,9 +621,7 @@ class Status extends ImmutablePureComponent {
}
if (status.get('poll')) {
const language = status.getIn(['translation', 'language']) || status.get('language');
contentMedia.push(<PollContainer pollId={status.get('poll')} status={status} lang={language} />);
contentMediaIcons.push('tasks');
mediaIcons.push('tasks');
}
// Here we prepare extra data-* attributes for CSS selectors.
@ -672,7 +660,6 @@ class Status extends ImmutablePureComponent {
}
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
contentMedia.push(hashtagBar);
return (
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
@ -704,26 +691,35 @@ class Status extends ImmutablePureComponent {
</Permalink>
<StatusIcons
status={status}
mediaIcons={contentMediaIcons.concat(extraMediaIcons)}
mediaIcons={mediaIcons}
settings={settings.get('status_icons')}
/>
</header>
)}
<StatusContent
status={status}
onClick={this.handleClick}
onTranslate={this.handleTranslate}
collapsible
media={contentMedia}
extraMedia={extraMedia}
mediaIcons={contentMediaIcons}
expanded={isExpanded}
onExpandedToggle={this.handleExpandedToggle}
onCollapsedToggle={this.handleCollapsedToggle}
tagLinks={settings.get('tag_misleading_links')}
rewriteMentions={settings.get('rewrite_mentions')}
{...statusContentProps}
/>
{status.get('spoiler_text').length > 0 && <ContentWarning text={status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml')} expanded={expanded} onClick={this.handleExpandedToggle} icons={mediaIcons} />}
{expanded && (
<>
<StatusContent
status={status}
onClick={this.handleClick}
onTranslate={this.handleTranslate}
collapsible
media={media}
onCollapsedToggle={this.handleCollapsedToggle}
tagLinks={settings.get('tag_misleading_links')}
rewriteMentions={settings.get('rewrite_mentions')}
{...statusContentProps}
/>
{media}
{hashtagBar}
</>
)}
{/* This is a glitch-soc addition to have a placeholder */}
{!expanded && <MentionsPlaceholder status={status} />}
<StatusActionBar
status={status}

View File

@ -10,19 +10,12 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import ImageIcon from '@/material-icons/400-24px/image.svg?react';
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 PollContainer from 'flavours/glitch/containers/poll_container';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
import { Permalink } from './permalink';
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
const textMatchesTarget = (text, origin, host) => {
@ -135,16 +128,10 @@ class StatusContent extends PureComponent {
identity: identityContextPropShape,
status: ImmutablePropTypes.map.isRequired,
statusContent: PropTypes.string,
expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func,
onTranslate: PropTypes.func,
media: PropTypes.node,
extraMedia: PropTypes.node,
mediaIcons: PropTypes.arrayOf(PropTypes.string),
onClick: PropTypes.func,
collapsible: PropTypes.bool,
onCollapsedToggle: PropTypes.func,
onUpdate: PropTypes.func,
tagLinks: PropTypes.bool,
rewriteMentions: PropTypes.string,
languages: ImmutablePropTypes.map,
@ -160,12 +147,8 @@ class StatusContent extends PureComponent {
rewriteMentions: 'no',
};
state = {
hidden: true,
};
_updateStatusLinks () {
const node = this.contentsNode;
const node = this.node;
const { tagLinks, rewriteMentions } = this.props;
if (!node) {
@ -280,7 +263,6 @@ class StatusContent extends PureComponent {
componentDidUpdate () {
this._updateStatusLinks();
if (this.props.onUpdate) this.props.onUpdate();
}
onMentionClick = (mention, e) => {
@ -326,49 +308,27 @@ class StatusContent extends PureComponent {
this.startXY = null;
};
handleSpoilerClick = (e) => {
e.preventDefault();
if (this.props.onExpandedToggle) {
this.props.onExpandedToggle();
} else {
this.setState({ hidden: !this.state.hidden });
}
};
handleTranslate = () => {
this.props.onTranslate();
};
setContentsRef = (c) => {
this.contentsNode = c;
setRef = (c) => {
this.node = c;
};
render () {
const {
status,
media,
extraMedia,
mediaIcons,
tagLinks,
rewriteMentions,
intl,
statusContent,
} = this.props;
const { status, intl, statusContent } = this.props;
const renderReadMore = this.props.onClick && status.get('collapsed');
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const contentLocale = intl.locale.replace(/[_-].*/, '');
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
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 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': this.props.onClick && this.props.history,
'status__content--collapsed': renderReadMore,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
});
const readMoreButton = renderReadMore && (
@ -381,113 +341,30 @@ class StatusContent extends PureComponent {
<TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
);
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';
const mentionLinks = status.get('mentions').map(item => (
<Permalink
to={`/@${item.get('acct')}`}
href={item.get('url')}
key={item.get('id')}
className='mention'
>
@<span>{item.get('username')}</span>
</Permalink>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
let spoilerIcons = [];
if (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) {
mentionsPlaceholder = <div>{mentionLinks}</div>;
}
const poll = !!status.get('poll') && (
<PollContainer pollId={status.get('poll')} status={status} lang={language} />
);
if (this.props.onClick) {
return (
<div className={classNames} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<ContentWarning text={spoilerHtml} expanded={!hidden} onClick={this.handleSpoilerClick} icons={spoilerIcons} />
<>
<div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} tabIndex={0} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='status__content__text status__content__text--visible translate' lang={language} dangerouslySetInnerHTML={content} />
{mentionsPlaceholder}
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
<div
ref={this.setContentsRef}
key={`contents-${tagLinks}`}
tabIndex={!hidden ? 0 : null}
dangerouslySetInnerHTML={content}
className='status__content__text translate'
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
lang={language}
/>
{!hidden && translateButton}
{media}
{poll}
{translateButton}
</div>
{extraMedia}
</div>
);
} else if (this.props.onClick) {
return (
<div
className={classNames}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
tabIndex={0}
>
<div
ref={this.setContentsRef}
key={`contents-${tagLinks}-${rewriteMentions}`}
dangerouslySetInnerHTML={content}
className='status__content__text translate'
tabIndex={0}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
lang={language}
/>
{translateButton}
{readMoreButton}
{media}
{extraMedia}
</div>
</>
);
} else {
return (
<div
className='status__content'
tabIndex={0}
>
<div
ref={this.setContentsRef}
key={`contents-${tagLinks}`}
className='status__content__text translate'
dangerouslySetInnerHTML={content}
tabIndex={0}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
lang={language}
/>
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='status__content__text status__content__text--visible translate' lang={language} dangerouslySetInnerHTML={content} />
{poll}
{translateButton}
{media}
{extraMedia}
</div>
);
}

View File

@ -8,23 +8,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ForumIcon from '@/material-icons/400-24px/forum.svg?react';
import HomeIcon from '@/material-icons/400-24px/home.svg?react';
import ImageIcon from '@/material-icons/400-24px/image.svg?react';
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 { Icon } from 'flavours/glitch/components/icon';
import { MediaIcon } from 'flavours/glitch/components/media_icon';
import { languages } from 'flavours/glitch/initial_state';
import { VisibilityIcon } from './visibility_icon';
const messages = defineMessages({
inReplyTo: { id: 'status.in_reply_to', defaultMessage: 'This toot is a reply' },
previewCard: { id: 'status.has_preview_card', defaultMessage: 'Features an attached preview card' },
pictures: { id: 'status.has_pictures', defaultMessage: 'Features attached pictures' },
poll: { id: 'status.is_poll', defaultMessage: 'This toot is a poll' },
video: { id: 'status.has_video', defaultMessage: 'Features attached videos' },
audio: { id: 'status.has_audio', defaultMessage: 'Features attached audio files' },
localOnly: { id: 'status.local_only', defaultMessage: 'Only visible from your instance' },
});
@ -54,47 +45,6 @@ class StatusIcons extends PureComponent {
settings: ImmutablePropTypes.map.isRequired,
};
renderIcon (mediaIcon) {
const { intl } = this.props;
let title, iconComponent;
switch (mediaIcon) {
case 'link':
title = messages.previewCard;
iconComponent = LinkIcon;
break;
case 'picture-o':
title = messages.pictures;
iconComponent = ImageIcon;
break;
case 'tasks':
title = messages.poll;
iconComponent = InsertChartIcon;
break;
case 'video-camera':
title = messages.video;
iconComponent = MovieIcon;
break;
case 'music':
title = messages.audio;
iconComponent = MusicNoteIcon;
break;
}
return (
<Icon
fixedWidth
className='status__media-icon'
key={`media-icon--${mediaIcon}`}
id={mediaIcon}
icon={iconComponent}
aria-hidden='true'
title={title && intl.formatMessage(title)}
/>
);
}
render () {
const {
status,
@ -122,7 +72,7 @@ class StatusIcons extends PureComponent {
aria-hidden='true'
title={intl.formatMessage(messages.localOnly)}
/>}
{settings.get('media') && !!mediaIcons && mediaIcons.map(icon => this.renderIcon(icon))}
{settings.get('media') && !!mediaIcons && mediaIcons.map(icon => (<MediaIcon key={`media-icon--${icon}`} className='status__media-icon' icon={icon} />))}
{settings.get('visibility') && <VisibilityIcon visibility={status.get('visibility')} />}
</div>
);

View File

@ -274,15 +274,6 @@ class LocalSettingsPage extends PureComponent {
<FormattedMessage id='settings.content_warnings_shared_state' defaultMessage='Show/hide content of all copies at once' />
<span className='hint'><FormattedMessage id='settings.content_warnings_shared_state_hint' defaultMessage='Reproduce upstream Mastodon behavior by having the Content Warning button affect all copies of a post at once. This will prevent automatic collapsing of any copy of a toot with unfolded CW' /></span>
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['content_warnings', 'media_outside']}
id='mastodon-settings--content_warnings-media_outside'
onChange={onChange}
>
<FormattedMessage id='settings.content_warnings_media_outside' defaultMessage='Display media attachments outside content warnings' />
<span className='hint'><FormattedMessage id='settings.content_warnings_media_outside_hint' defaultMessage='Reproduce upstream Mastodon behavior by having the Content Warning toggle not affect media attachments' /></span>
</LocalSettingsPageItem>
<section>
<h2><FormattedMessage id='settings.content_warnings_unfold_opts' defaultMessage='Auto-unfolding options' /></h2>
<DeprecatedLocalSettingsPageItem

View File

@ -4,7 +4,7 @@
@typescript-eslint/no-unsafe-assignment */
import type { CSSProperties } from 'react';
import { useState, useRef, useCallback } from 'react';
import React, { useState, useRef, useCallback } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
@ -13,14 +13,15 @@ import { Link } from 'react-router-dom';
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
import AttachmentList from 'flavours/glitch/components/attachment_list';
import { ContentWarning } from 'flavours/glitch/components/content_warning';
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
import type { StatusLike } from 'flavours/glitch/components/hashtag_bar';
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
import { IconLogo } from 'flavours/glitch/components/logo';
import { MentionsPlaceholder } from 'flavours/glitch/components/mentions_placeholder';
import { Permalink } from 'flavours/glitch/components/permalink';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
import PollContainer from 'flavours/glitch/containers/poll_container';
import { useAppSelector } from 'flavours/glitch/store';
import { Avatar } from '../../../components/avatar';
@ -82,13 +83,6 @@ export const DetailedStatus: React.FC<{
(state) =>
state.local_settings.get('tag_misleading_links', false) as boolean,
);
const mediaOutsideCW = useAppSelector(
(state) =>
state.local_settings.getIn(
['content_warnings', 'media_outside'],
false,
) as boolean,
);
const letterboxMedia = useAppSelector(
(state) =>
state.local_settings.getIn(['media', 'letterbox'], false) as boolean,
@ -108,6 +102,10 @@ export const DetailedStatus: React.FC<{
[onOpenVideo, status],
);
const handleExpandedToggle = useCallback(() => {
if (onToggleHidden) onToggleHidden(status);
}, [onToggleHidden, status]);
const _measureHeight = useCallback(
(heightJustChanged?: boolean) => {
if (measureHeight && nodeRef.current) {
@ -132,10 +130,6 @@ export const DetailedStatus: React.FC<{
[_measureHeight],
);
const handleChildUpdate = useCallback(() => {
_measureHeight();
}, [_measureHeight]);
const handleTranslate = useCallback(() => {
if (onTranslate) onTranslate(status);
}, [onTranslate, status]);
@ -144,23 +138,11 @@ export const DetailedStatus: React.FC<{
return null;
}
let media;
let applicationLink;
let reblogLink;
// Depending on user settings, some media are considered as parts of the
// contents (affected by CW) while other will be displayed outside of the
// CW.
const contentMedia: React.ReactNode[] = [];
const contentMediaIcons: string[] = [];
const extraMedia: React.ReactNode[] = [];
const extraMediaIcons: string[] = [];
let media = contentMedia;
let mediaIcons: string[] = contentMediaIcons;
if (mediaOutsideCW) {
media = extraMedia;
mediaIcons = extraMediaIcons;
}
const mediaIcons: string[] = [];
const outerStyle = { boxSizing: 'border-box' } as CSSProperties;
@ -172,7 +154,7 @@ export const DetailedStatus: React.FC<{
status.getIn(['translation', 'language']) || status.get('language');
if (pictureInPicture.get('inUse')) {
media.push(<PictureInPicturePlaceholder />);
media = <PictureInPicturePlaceholder />;
mediaIcons.push('video-camera');
} else if (status.get('media_attachments').size > 0) {
if (
@ -182,14 +164,14 @@ export const DetailedStatus: React.FC<{
(item: Immutable.Map<string, any>) => item.get('type') === 'unknown',
)
) {
media.push(<AttachmentList media={status.get('media_attachments')} />);
media = <AttachmentList media={status.get('media_attachments')} />;
} else if (
['image', 'gifv', 'unknown'].includes(
status.getIn(['media_attachments', 0, 'type']) as string,
) ||
status.get('media_attachments').size > 1
) {
media.push(
media = (
<MediaGallery
standalone
sensitive={status.get('sensitive')}
@ -202,7 +184,7 @@ export const DetailedStatus: React.FC<{
onOpenMedia={onOpenMedia}
visible={showMedia}
onToggleVisibility={onToggleMediaVisibility}
/>,
/>
);
mediaIcons.push('picture-o');
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
@ -211,7 +193,7 @@ export const DetailedStatus: React.FC<{
attachment.getIn(['translation', 'description']) ||
attachment.get('description');
media.push(
media = (
<Audio
src={attachment.get('url')}
alt={description}
@ -229,7 +211,7 @@ export const DetailedStatus: React.FC<{
blurhash={attachment.get('blurhash')}
height={150}
onToggleVisibility={onToggleMediaVisibility}
/>,
/>
);
mediaIcons.push('music');
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
@ -238,7 +220,7 @@ export const DetailedStatus: React.FC<{
attachment.getIn(['translation', 'description']) ||
attachment.get('description');
media.push(
media = (
<Video
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
@ -257,31 +239,23 @@ export const DetailedStatus: React.FC<{
letterbox={letterboxMedia}
fullwidth={fullwidthMedia}
preventPlayback={!expanded}
/>,
/>
);
mediaIcons.push('video-camera');
}
} else if (status.get('spoiler_text').length === 0) {
media.push(
media = (
<Card
sensitive={status.get('sensitive')}
onOpenMedia={onOpenMedia}
card={status.get('card', null)}
/>,
/>
);
mediaIcons.push('link');
}
if (status.get('poll')) {
contentMedia.push(
<PollContainer
pollId={status.get('poll')}
// @ts-expect-error -- Poll/PollContainer is not typed yet
status={status}
lang={status.get('language')}
/>,
);
contentMediaIcons.push('tasks');
mediaIcons.push('tasks');
}
if (status.get('application')) {
@ -345,7 +319,8 @@ export const DetailedStatus: React.FC<{
const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
status as StatusLike,
);
contentMedia.push(hashtagBar);
expanded ||= status.get('spoiler_text').length === 0;
return (
<div style={outerStyle}>
@ -379,20 +354,34 @@ export const DetailedStatus: React.FC<{
)}
</Permalink>
<StatusContent
status={status}
media={contentMedia}
extraMedia={extraMedia}
mediaIcons={contentMediaIcons}
expanded={expanded}
collapsed={false}
onExpandedToggle={onToggleHidden}
onTranslate={handleTranslate}
onUpdate={handleChildUpdate}
tagLinks={tagMisleadingLinks}
rewriteMentions={rewriteMentions}
{...(statusContentProps as any)}
/>
{status.get('spoiler_text').length > 0 && (
<ContentWarning
text={
status.getIn(['translation', 'spoilerHtml']) ||
status.get('spoilerHtml')
}
expanded={expanded}
onClick={handleExpandedToggle}
/>
)}
{expanded && (
<>
<StatusContent
status={status}
onTranslate={handleTranslate}
tagLinks={tagMisleadingLinks}
rewriteMentions={rewriteMentions}
{...(statusContentProps as any)}
/>
{media}
{hashtagBar}
</>
)}
{/* This is a glitch-soc addition to have a placeholder */}
{!expanded && <MentionsPlaceholder status={status} />}
<div className='detailed-status__meta'>
<div className='detailed-status__meta__line'>

View File

@ -68,8 +68,6 @@
"settings.content_warnings": "Content Warnings",
"settings.content_warnings.regexp": "Regular expression",
"settings.content_warnings_filter": "Content warnings to not automatically unfold:",
"settings.content_warnings_media_outside": "Display media attachments outside content warnings",
"settings.content_warnings_media_outside_hint": "Reproduce upstream Mastodon behavior by having the Content Warning toggle not affect media attachments",
"settings.content_warnings_shared_state": "Show/hide content of all copies at once",
"settings.content_warnings_shared_state_hint": "Reproduce upstream Mastodon behavior by having the Content Warning button affect all copies of a post at once. This will prevent automatic collapsing of any copy of a toot with unfolded CW",
"settings.content_warnings_unfold_opts": "Auto-unfolding options",

View File

@ -23,7 +23,6 @@ const initialState = ImmutableMap({
rewrite_mentions: 'no',
content_warnings : ImmutableMap({
filter : null,
media_outside: false,
shared_state : false,
}),
media : ImmutableMap({

View File

@ -1133,11 +1133,6 @@ body > [data-popper-placement] {
}
}
.status__content {
// glitch: necessary for fullwidth media options
overflow: visible;
}
.reply-indicator {
display: grid;
grid-template-columns: 46px minmax(0, 1fr);
@ -1351,7 +1346,6 @@ body > [data-popper-placement] {
.status__content.status__content--collapsed .status__content__text {
max-height: 20px * 15; // 15 lines is roughly above 500 characters
overflow: hidden;
}
.status__content__read-more-button,
@ -1499,11 +1493,25 @@ body > [data-popper-placement] {
border-bottom: 0;
.status__content,
.status__action-bar {
.status__action-bar,
.media-gallery,
.video-player,
.audio-player,
.attachment-list,
.picture-in-picture-placeholder,
.more-from-author,
.status-card,
.hashtag-bar,
.content-warning,
.filter-warning {
margin-inline-start: $thread-margin;
width: calc(100% - $thread-margin);
}
.more-from-author {
width: calc(100% - $thread-margin + 2px);
}
.status__content__read-more-button {
margin-inline-start: $thread-margin;
}
@ -1659,14 +1667,6 @@ body > [data-popper-placement] {
.media-gallery__item-thumbnail {
cursor: default;
}
.content-warning {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
}
.status__prepend {
@ -10934,7 +10934,17 @@ noscript {
$icon-margin: 48px; // 40px avatar + 8px gap
.status__content,
.status__action-bar {
.status__action-bar,
.media-gallery,
.video-player,
.audio-player,
.attachment-list,
.picture-in-picture-placeholder,
.more-from-author,
.status-card,
.hashtag-bar,
.content-warning,
.filter-warning {
margin-inline-start: $icon-margin;
width: calc(100% - $icon-margin);
}