mirror of
https://github.com/glitch-soc/mastodon
synced 2025-04-24 21:14:51 +00:00
Merge pull request #3012 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to c93b2c6809
This commit is contained in:
commit
21c05f838d
21 changed files with 182 additions and 111 deletions
|
@ -231,6 +231,7 @@ class MediaGallery extends PureComponent {
|
||||||
visible: PropTypes.bool,
|
visible: PropTypes.bool,
|
||||||
autoplay: PropTypes.bool,
|
autoplay: PropTypes.bool,
|
||||||
onToggleVisibility: PropTypes.func,
|
onToggleVisibility: PropTypes.func,
|
||||||
|
matchedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -314,7 +315,7 @@ class MediaGallery extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, lang, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props;
|
const { media, lang, sensitive, letterbox, fullwidth, defaultWidth, autoplay, matchedFilters } = this.props;
|
||||||
const { visible } = this.state;
|
const { visible } = this.state;
|
||||||
const size = media.size;
|
const size = media.size;
|
||||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||||
|
@ -343,7 +344,7 @@ class MediaGallery extends PureComponent {
|
||||||
<div className={computedClass} style={style} ref={this.handleRef}>
|
<div className={computedClass} style={style} ref={this.handleRef}>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{(!visible || uncached) && <SpoilerButton uncached={uncached} sensitive={sensitive} onClick={this.handleOpen} />}
|
{(!visible || uncached) && <SpoilerButton uncached={uncached} sensitive={sensitive} onClick={this.handleOpen} matchedFilters={matchedFilters} />}
|
||||||
|
|
||||||
{(visible && !uncached) && (
|
{(visible && !uncached) && (
|
||||||
<div className='media-gallery__actions'>
|
<div className='media-gallery__actions'>
|
||||||
|
|
|
@ -6,6 +6,7 @@ interface Props {
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
sensitive: boolean;
|
sensitive: boolean;
|
||||||
uncached?: boolean;
|
uncached?: boolean;
|
||||||
|
matchedFilters?: string[];
|
||||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ export const SpoilerButton: React.FC<Props> = ({
|
||||||
hidden = false,
|
hidden = false,
|
||||||
sensitive,
|
sensitive,
|
||||||
uncached = false,
|
uncached = false,
|
||||||
|
matchedFilters,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
let warning;
|
let warning;
|
||||||
|
@ -28,6 +30,20 @@ export const SpoilerButton: React.FC<Props> = ({
|
||||||
action = (
|
action = (
|
||||||
<FormattedMessage id='status.media.open' defaultMessage='Click to open' />
|
<FormattedMessage id='status.media.open' defaultMessage='Click to open' />
|
||||||
);
|
);
|
||||||
|
} else if (matchedFilters) {
|
||||||
|
warning = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='filter_warning.matches_filter'
|
||||||
|
defaultMessage='Matches filter “<span>{title}</span>”'
|
||||||
|
values={{
|
||||||
|
title: matchedFilters.join(', '),
|
||||||
|
span: (chunks) => <span className='filter-name'>{chunks}</span>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
action = (
|
||||||
|
<FormattedMessage id='status.media.show' defaultMessage='Click to show' />
|
||||||
|
);
|
||||||
} else if (sensitive) {
|
} else if (sensitive) {
|
||||||
warning = (
|
warning = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|
|
@ -70,7 +70,7 @@ export const defaultMediaVisibility = (status, settings) => {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
|
return !status.get('matched_media_filters') && (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
|
||||||
};
|
};
|
||||||
|
|
||||||
class Status extends ImmutablePureComponent {
|
class Status extends ImmutablePureComponent {
|
||||||
|
@ -557,6 +557,7 @@ class Status extends ImmutablePureComponent {
|
||||||
defaultWidth={this.props.cachedMediaWidth}
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
visible={this.state.showMedia}
|
visible={this.state.showMedia}
|
||||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Bundle>,
|
</Bundle>,
|
||||||
|
@ -586,6 +587,7 @@ class Status extends ImmutablePureComponent {
|
||||||
blurhash={attachment.get('blurhash')}
|
blurhash={attachment.get('blurhash')}
|
||||||
visible={this.state.showMedia}
|
visible={this.state.showMedia}
|
||||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Bundle>,
|
</Bundle>,
|
||||||
|
@ -613,6 +615,7 @@ class Status extends ImmutablePureComponent {
|
||||||
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
||||||
visible={this.state.showMedia}
|
visible={this.state.showMedia}
|
||||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>)}
|
/>)}
|
||||||
</Bundle>,
|
</Bundle>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -62,6 +62,7 @@ class Audio extends PureComponent {
|
||||||
volume: PropTypes.number,
|
volume: PropTypes.number,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
deployPictureInPicture: PropTypes.func,
|
deployPictureInPicture: PropTypes.func,
|
||||||
|
matchedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -478,7 +479,7 @@ class Audio extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
|
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash, matchedFilters } = this.props;
|
||||||
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
||||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||||
const muted = this.state.muted || volume === 0;
|
const muted = this.state.muted || volume === 0;
|
||||||
|
@ -520,7 +521,7 @@ class Audio extends PureComponent {
|
||||||
lang={lang}
|
lang={lang}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} />
|
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} />
|
||||||
|
|
||||||
{(revealed || editable) && <img
|
{(revealed || editable) && <img
|
||||||
src={this.props.poster}
|
src={this.props.poster}
|
||||||
|
|
|
@ -190,6 +190,7 @@ export const DetailedStatus: React.FC<{
|
||||||
onOpenMedia={onOpenMedia}
|
onOpenMedia={onOpenMedia}
|
||||||
visible={showMedia}
|
visible={showMedia}
|
||||||
onToggleVisibility={onToggleMediaVisibility}
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
mediaIcons.push('picture-o');
|
mediaIcons.push('picture-o');
|
||||||
|
@ -217,6 +218,7 @@ export const DetailedStatus: React.FC<{
|
||||||
blurhash={attachment.get('blurhash')}
|
blurhash={attachment.get('blurhash')}
|
||||||
height={150}
|
height={150}
|
||||||
onToggleVisibility={onToggleMediaVisibility}
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
mediaIcons.push('music');
|
mediaIcons.push('music');
|
||||||
|
@ -242,6 +244,7 @@ export const DetailedStatus: React.FC<{
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
visible={showMedia}
|
visible={showMedia}
|
||||||
onToggleVisibility={onToggleMediaVisibility}
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
letterbox={letterboxMedia}
|
letterbox={letterboxMedia}
|
||||||
fullwidth={fullwidthMedia}
|
fullwidth={fullwidthMedia}
|
||||||
preventPlayback={!expanded}
|
preventPlayback={!expanded}
|
||||||
|
|
|
@ -139,6 +139,7 @@ class Video extends PureComponent {
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
componentIndex: PropTypes.number,
|
componentIndex: PropTypes.number,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
|
matchedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -542,7 +543,7 @@ class Video extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus, matchedFilters } = this.props;
|
||||||
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state;
|
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state;
|
||||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||||
const muted = this.state.muted || volume === 0;
|
const muted = this.state.muted || volume === 0;
|
||||||
|
@ -603,7 +604,7 @@ class Video extends PureComponent {
|
||||||
style={{ ...playerStyle, width: '100%' }}
|
style={{ ...playerStyle, width: '100%' }}
|
||||||
/>}
|
/>}
|
||||||
|
|
||||||
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} />
|
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} />
|
||||||
|
|
||||||
<div className={classNames('video-player__controls', { active: paused || hovered })}>
|
<div className={classNames('video-player__controls', { active: paused || hovered })}>
|
||||||
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
|
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
|
||||||
|
|
|
@ -24,12 +24,19 @@ export const makeGetStatus = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
let mediaFiltered = false;
|
||||||
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||||
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||||
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
filterResults = filterResults.filter(result => filters.has(result.get('filter')));
|
|
||||||
|
let mediaFilters = filterResults.filter(result => filters.getIn([result.get('filter'), 'filter_action']) === 'blur');
|
||||||
|
if (!mediaFilters.isEmpty()) {
|
||||||
|
mediaFiltered = mediaFilters.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||||
|
}
|
||||||
|
|
||||||
|
filterResults = filterResults.filter(result => filters.has(result.get('filter')) && filters.getIn([result.get('filter'), 'filter_action']) !== 'blur');
|
||||||
if (!filterResults.isEmpty()) {
|
if (!filterResults.isEmpty()) {
|
||||||
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||||
}
|
}
|
||||||
|
@ -46,6 +53,7 @@ export const makeGetStatus = () => {
|
||||||
map.set('reblog', statusReblog);
|
map.set('reblog', statusReblog);
|
||||||
map.set('account', accountBase);
|
map.set('account', accountBase);
|
||||||
map.set('matched_filters', filtered);
|
map.set('matched_filters', filtered);
|
||||||
|
map.set('matched_media_filters', mediaFiltered);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -226,6 +226,7 @@ class MediaGallery extends PureComponent {
|
||||||
visible: PropTypes.bool,
|
visible: PropTypes.bool,
|
||||||
autoplay: PropTypes.bool,
|
autoplay: PropTypes.bool,
|
||||||
onToggleVisibility: PropTypes.func,
|
onToggleVisibility: PropTypes.func,
|
||||||
|
matchedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -296,7 +297,7 @@ class MediaGallery extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, lang, sensitive, defaultWidth, autoplay } = this.props;
|
const { media, lang, sensitive, defaultWidth, autoplay, matchedFilters } = this.props;
|
||||||
const { visible } = this.state;
|
const { visible } = this.state;
|
||||||
const width = this.state.width || defaultWidth;
|
const width = this.state.width || defaultWidth;
|
||||||
|
|
||||||
|
@ -323,7 +324,7 @@ class MediaGallery extends PureComponent {
|
||||||
<div className={`media-gallery media-gallery--layout-${size}`} style={style} ref={this.handleRef}>
|
<div className={`media-gallery media-gallery--layout-${size}`} style={style} ref={this.handleRef}>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{(!visible || uncached) && <SpoilerButton uncached={uncached} sensitive={sensitive} onClick={this.handleOpen} />}
|
{(!visible || uncached) && <SpoilerButton uncached={uncached} sensitive={sensitive} onClick={this.handleOpen} matchedFilters={matchedFilters} />}
|
||||||
|
|
||||||
{(visible && !uncached) && (
|
{(visible && !uncached) && (
|
||||||
<div className='media-gallery__actions'>
|
<div className='media-gallery__actions'>
|
||||||
|
|
|
@ -6,6 +6,7 @@ interface Props {
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
sensitive: boolean;
|
sensitive: boolean;
|
||||||
uncached?: boolean;
|
uncached?: boolean;
|
||||||
|
matchedFilters?: string[];
|
||||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ export const SpoilerButton: React.FC<Props> = ({
|
||||||
hidden = false,
|
hidden = false,
|
||||||
sensitive,
|
sensitive,
|
||||||
uncached = false,
|
uncached = false,
|
||||||
|
matchedFilters,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
let warning;
|
let warning;
|
||||||
|
@ -28,6 +30,20 @@ export const SpoilerButton: React.FC<Props> = ({
|
||||||
action = (
|
action = (
|
||||||
<FormattedMessage id='status.media.open' defaultMessage='Click to open' />
|
<FormattedMessage id='status.media.open' defaultMessage='Click to open' />
|
||||||
);
|
);
|
||||||
|
} else if (matchedFilters) {
|
||||||
|
warning = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='filter_warning.matches_filter'
|
||||||
|
defaultMessage='Matches filter “<span>{title}</span>”'
|
||||||
|
values={{
|
||||||
|
title: matchedFilters.join(', '),
|
||||||
|
span: (chunks) => <span className='filter-name'>{chunks}</span>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
action = (
|
||||||
|
<FormattedMessage id='status.media.show' defaultMessage='Click to show' />
|
||||||
|
);
|
||||||
} else if (sensitive) {
|
} else if (sensitive) {
|
||||||
warning = (
|
warning = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|
|
@ -70,7 +70,7 @@ export const defaultMediaVisibility = (status) => {
|
||||||
status = status.get('reblog');
|
status = status.get('reblog');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
|
return !status.get('matched_media_filters') && (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
|
||||||
};
|
};
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -470,6 +470,7 @@ class Status extends ImmutablePureComponent {
|
||||||
defaultWidth={this.props.cachedMediaWidth}
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
visible={this.state.showMedia}
|
visible={this.state.showMedia}
|
||||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
|
@ -498,6 +499,7 @@ class Status extends ImmutablePureComponent {
|
||||||
blurhash={attachment.get('blurhash')}
|
blurhash={attachment.get('blurhash')}
|
||||||
visible={this.state.showMedia}
|
visible={this.state.showMedia}
|
||||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
|
@ -522,6 +524,7 @@ class Status extends ImmutablePureComponent {
|
||||||
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
||||||
visible={this.state.showMedia}
|
visible={this.state.showMedia}
|
||||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
|
|
|
@ -62,6 +62,7 @@ class Audio extends PureComponent {
|
||||||
volume: PropTypes.number,
|
volume: PropTypes.number,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
deployPictureInPicture: PropTypes.func,
|
deployPictureInPicture: PropTypes.func,
|
||||||
|
matchedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -472,7 +473,7 @@ class Audio extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
|
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash, matchedFilters } = this.props;
|
||||||
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
||||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||||
const muted = this.state.muted || volume === 0;
|
const muted = this.state.muted || volume === 0;
|
||||||
|
@ -514,7 +515,7 @@ class Audio extends PureComponent {
|
||||||
lang={lang}
|
lang={lang}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} />
|
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} />
|
||||||
|
|
||||||
{(revealed || editable) && <img
|
{(revealed || editable) && <img
|
||||||
src={this.props.poster}
|
src={this.props.poster}
|
||||||
|
|
|
@ -175,6 +175,7 @@ export const DetailedStatus: React.FC<{
|
||||||
onOpenMedia={onOpenMedia}
|
onOpenMedia={onOpenMedia}
|
||||||
visible={showMedia}
|
visible={showMedia}
|
||||||
onToggleVisibility={onToggleMediaVisibility}
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||||
|
@ -201,6 +202,7 @@ export const DetailedStatus: React.FC<{
|
||||||
blurhash={attachment.get('blurhash')}
|
blurhash={attachment.get('blurhash')}
|
||||||
height={150}
|
height={150}
|
||||||
onToggleVisibility={onToggleMediaVisibility}
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
|
@ -224,6 +226,7 @@ export const DetailedStatus: React.FC<{
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
visible={showMedia}
|
visible={showMedia}
|
||||||
onToggleVisibility={onToggleMediaVisibility}
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
matchedFilters={status.get('matched_media_filters')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,6 +136,7 @@ class Video extends PureComponent {
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
componentIndex: PropTypes.number,
|
componentIndex: PropTypes.number,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
|
matchedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -535,7 +536,7 @@ class Video extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { preview, src, aspectRatio, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
const { preview, src, aspectRatio, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus, matchedFilters } = this.props;
|
||||||
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state;
|
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state;
|
||||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||||
const muted = this.state.muted || volume === 0;
|
const muted = this.state.muted || volume === 0;
|
||||||
|
@ -592,7 +593,7 @@ class Video extends PureComponent {
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>}
|
/>}
|
||||||
|
|
||||||
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} />
|
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} />
|
||||||
|
|
||||||
<div className={classNames('video-player__controls', { active: paused || hovered })}>
|
<div className={classNames('video-player__controls', { active: paused || hovered })}>
|
||||||
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
|
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
|
||||||
|
|
|
@ -30,12 +30,19 @@ export const makeGetStatus = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
let mediaFiltered = false;
|
||||||
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||||
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||||
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
filterResults = filterResults.filter(result => filters.has(result.get('filter')));
|
|
||||||
|
let mediaFilters = filterResults.filter(result => filters.getIn([result.get('filter'), 'filter_action']) === 'blur');
|
||||||
|
if (!mediaFilters.isEmpty()) {
|
||||||
|
mediaFiltered = mediaFilters.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||||
|
}
|
||||||
|
|
||||||
|
filterResults = filterResults.filter(result => filters.has(result.get('filter')) && filters.getIn([result.get('filter'), 'filter_action']) !== 'blur');
|
||||||
if (!filterResults.isEmpty()) {
|
if (!filterResults.isEmpty()) {
|
||||||
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||||
}
|
}
|
||||||
|
@ -45,6 +52,7 @@ export const makeGetStatus = () => {
|
||||||
map.set('reblog', statusReblog);
|
map.set('reblog', statusReblog);
|
||||||
map.set('account', accountBase);
|
map.set('account', accountBase);
|
||||||
map.set('matched_filters', filtered);
|
map.set('matched_filters', filtered);
|
||||||
|
map.set('matched_media_filters', mediaFiltered);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -33,7 +33,7 @@ class CustomFilter < ApplicationRecord
|
||||||
include Expireable
|
include Expireable
|
||||||
include Redisable
|
include Redisable
|
||||||
|
|
||||||
enum :action, { warn: 0, hide: 1 }, suffix: :action
|
enum :action, { warn: 0, hide: 1, blur: 2 }, suffix: :action
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy
|
has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :filter_action,
|
= f.input :filter_action,
|
||||||
as: :radio_buttons,
|
as: :radio_buttons,
|
||||||
collection: %i(warn hide),
|
collection: %i(warn blur hide),
|
||||||
hint: t('simple_form.hints.filters.action'),
|
hint: t('simple_form.hints.filters.action'),
|
||||||
include_blank: false,
|
include_blank: false,
|
||||||
label_method: ->(action) { filter_action_label(action) },
|
label_method: ->(action) { filter_action_label(action) },
|
||||||
|
|
|
@ -75,6 +75,7 @@ en:
|
||||||
filters:
|
filters:
|
||||||
action: Chose which action to perform when a post matches the filter
|
action: Chose which action to perform when a post matches the filter
|
||||||
actions:
|
actions:
|
||||||
|
blur: Hide media behind a warning, without hiding the text itself
|
||||||
hide: Completely hide the filtered content, behaving as if it did not exist
|
hide: Completely hide the filtered content, behaving as if it did not exist
|
||||||
warn: Hide the filtered content behind a warning mentioning the filter's title
|
warn: Hide the filtered content behind a warning mentioning the filter's title
|
||||||
form_admin_settings:
|
form_admin_settings:
|
||||||
|
@ -260,6 +261,7 @@ en:
|
||||||
name: Hashtag
|
name: Hashtag
|
||||||
filters:
|
filters:
|
||||||
actions:
|
actions:
|
||||||
|
blur: Hide media with a warning
|
||||||
hide: Hide completely
|
hide: Hide completely
|
||||||
warn: Hide with a warning
|
warn: Hide with a warning
|
||||||
form_admin_settings:
|
form_admin_settings:
|
||||||
|
|
|
@ -45,7 +45,7 @@ module Mastodon
|
||||||
|
|
||||||
def api_versions
|
def api_versions
|
||||||
{
|
{
|
||||||
mastodon: 4,
|
mastodon: 5,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe Settings::DeletesController do
|
|
||||||
render_views
|
|
||||||
|
|
||||||
describe 'GET #show' do
|
|
||||||
context 'when signed in' do
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
sign_in user, scope: :user
|
|
||||||
get :show
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders confirmation page with private cache control headers', :aggregate_failures do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
expect(response.headers['Cache-Control']).to include('private, no-store')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when suspended' do
|
|
||||||
let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
|
|
||||||
|
|
||||||
it 'returns http forbidden with private cache control headers', :aggregate_failures do
|
|
||||||
expect(response).to have_http_status(403)
|
|
||||||
expect(response.headers['Cache-Control']).to include('private, no-store')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when not signed in' do
|
|
||||||
it 'redirects' do
|
|
||||||
get :show
|
|
||||||
expect(response).to redirect_to '/auth/sign_in'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'DELETE #destroy' do
|
|
||||||
context 'when signed in' do
|
|
||||||
let(:user) { Fabricate(:user, password: 'petsmoldoggos') }
|
|
||||||
|
|
||||||
before do
|
|
||||||
sign_in user, scope: :user
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with correct password' do
|
|
||||||
before do
|
|
||||||
delete :destroy, params: { form_delete_confirmation: { password: 'petsmoldoggos' } }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'removes user record and redirects', :aggregate_failures, :inline_jobs do
|
|
||||||
expect(response).to redirect_to '/auth/sign_in'
|
|
||||||
expect(User.find_by(id: user.id)).to be_nil
|
|
||||||
expect(user.account.reload).to be_suspended
|
|
||||||
expect(CanonicalEmailBlock.block?(user.email)).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when suspended' do
|
|
||||||
let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
|
|
||||||
|
|
||||||
it 'returns http forbidden' do
|
|
||||||
expect(response).to have_http_status(403)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with incorrect password' do
|
|
||||||
before do
|
|
||||||
delete :destroy, params: { form_delete_confirmation: { password: 'blaze420' } }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'redirects back to confirmation page' do
|
|
||||||
expect(response).to redirect_to settings_delete_path
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when not signed in' do
|
|
||||||
it 'redirects' do
|
|
||||||
delete :destroy
|
|
||||||
expect(response).to redirect_to '/auth/sign_in'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -4,13 +4,65 @@ require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Settings Deletes' do
|
RSpec.describe 'Settings Deletes' do
|
||||||
describe 'DELETE /settings/delete' do
|
describe 'DELETE /settings/delete' do
|
||||||
before { sign_in Fabricate(:user) }
|
context 'when signed in' do
|
||||||
|
before { sign_in(user) }
|
||||||
|
|
||||||
it 'gracefully handles invalid nested params' do
|
let(:user) { Fabricate(:user) }
|
||||||
delete settings_delete_path(form_delete_confirmation: 'invalid')
|
|
||||||
|
|
||||||
expect(response)
|
it 'gracefully handles invalid nested params' do
|
||||||
.to have_http_status(400)
|
delete settings_delete_path(form_delete_confirmation: 'invalid')
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(400)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when suspended' do
|
||||||
|
let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
|
||||||
|
|
||||||
|
it 'returns http forbidden' do
|
||||||
|
delete settings_delete_path
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when not signed in' do
|
||||||
|
it 'redirects to sign in' do
|
||||||
|
delete settings_delete_path
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to redirect_to(new_user_session_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /settings/delete' do
|
||||||
|
context 'when signed in' do
|
||||||
|
before { sign_in(user) }
|
||||||
|
|
||||||
|
context 'when suspended' do
|
||||||
|
let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
|
||||||
|
|
||||||
|
it 'returns http forbidden with private cache control headers' do
|
||||||
|
get settings_delete_path
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(403)
|
||||||
|
expect(response.headers['Cache-Control'])
|
||||||
|
.to include('private, no-store')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when not signed in' do
|
||||||
|
it 'redirects to sign in' do
|
||||||
|
get settings_delete_path
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to redirect_to(new_user_session_path)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
38
spec/system/settings/deletes_spec.rb
Normal file
38
spec/system/settings/deletes_spec.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Settings Deletes' do
|
||||||
|
describe 'Deleting user from settings area' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before { sign_in(user) }
|
||||||
|
|
||||||
|
it 'requires password and deletes user record', :inline_jobs do
|
||||||
|
visit settings_delete_path
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('settings.delete'))
|
||||||
|
.and have_private_cache_control
|
||||||
|
|
||||||
|
# Wrong confirmation value
|
||||||
|
fill_in 'form_delete_confirmation_password', with: 'wrongvalue'
|
||||||
|
click_on I18n.t('deletes.proceed')
|
||||||
|
expect(page)
|
||||||
|
.to have_content(I18n.t('deletes.challenge_not_passed'))
|
||||||
|
|
||||||
|
# Correct confirmation value
|
||||||
|
fill_in 'form_delete_confirmation_password', with: user.password
|
||||||
|
click_on I18n.t('deletes.proceed')
|
||||||
|
expect(page)
|
||||||
|
.to have_content(I18n.t('deletes.success_msg'))
|
||||||
|
expect(page)
|
||||||
|
.to have_title(I18n.t('auth.login'))
|
||||||
|
expect(User.find_by(id: user.id))
|
||||||
|
.to be_nil
|
||||||
|
expect(user.account.reload)
|
||||||
|
.to be_suspended
|
||||||
|
expect(CanonicalEmailBlock.block?(user.email))
|
||||||
|
.to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Reference in a new issue