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:
parent
5e65586161
commit
b7afca0f05
@ -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>
|
||||
);
|
||||
|
55
app/javascript/flavours/glitch/components/media_icon.tsx
Normal file
55
app/javascript/flavours/glitch/components/media_icon.tsx
Normal 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'
|
||||
/>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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'>
|
||||
|
@ -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",
|
||||
|
@ -23,7 +23,6 @@ const initialState = ImmutableMap({
|
||||
rewrite_mentions: 'no',
|
||||
content_warnings : ImmutableMap({
|
||||
filter : null,
|
||||
media_outside: false,
|
||||
shared_state : false,
|
||||
}),
|
||||
media : ImmutableMap({
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user