const polls = [];
function processStatus(status) {
- pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
+ pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), getState().get('local_settings')));
pushUnique(accounts, status.account);
if (status.reblog && status.reblog.id) {
import escapeTextContentForBrowser from 'escape-html';
import emojify from 'flavours/glitch/util/emoji';
import { unescapeHTML } from 'flavours/glitch/util/html';
+import { autoHideCW } from 'flavours/glitch/util/content_warning';
const domParser = new DOMParser();
return account;
}
-export function normalizeStatus(status, normalOldStatus) {
+export function normalizeStatus(status, normalOldStatus, settings) {
const normalStatus = { ...status };
normalStatus.account = status.account.id;
normalStatus.search_index = normalOldStatus.get('search_index');
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
+ normalStatus.hidden = normalOldStatus.get('hidden');
} else {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
+ normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
}
return normalStatus;
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
+export const STATUS_REVEAL = 'STATUS_REVEAL';
+export const STATUS_HIDE = 'STATUS_HIDE';
+export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
+
export const REDRAFT = 'REDRAFT';
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
error,
};
};
+
+export function hideStatus(ids) {
+ if (!Array.isArray(ids)) {
+ ids = [ids];
+ }
+
+ return {
+ type: STATUS_HIDE,
+ ids,
+ };
+};
+
+export function revealStatus(ids) {
+ if (!Array.isArray(ids)) {
+ ids = [ids];
+ }
+
+ return {
+ type: STATUS_REVEAL,
+ ids,
+ };
+};
+
+export function toggleStatusCollapse(id, isCollapsed) {
+ return {
+ type: STATUS_COLLAPSE,
+ id,
+ isCollapsed,
+ };
+}
onBlock: PropTypes.func,
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func,
+ onToggleHidden: PropTypes.func,
muted: PropTypes.bool,
- collapse: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
prepend: PropTypes.string,
'settings',
'prepend',
'muted',
- 'collapse',
'notification',
'hidden',
'expanded',
let updated = false;
// Make sure the state mirrors props we track…
- if (nextProps.collapse !== prevState.collapseProp) {
- update.collapseProp = nextProps.collapse;
- updated = true;
- }
if (nextProps.expanded !== prevState.expandedProp) {
update.expandedProp = nextProps.expanded;
updated = true;
}
+ if (nextProps.status?.get('hidden') !== prevState.statusPropHidden) {
+ update.statusPropHidden = nextProps.status?.get('hidden');
+ updated = true;
+ }
// Update state based on new props
if (!nextProps.settings.getIn(['collapsed', 'enabled'])) {
update.isCollapsed = false;
updated = true;
}
- } else if (
- nextProps.collapse !== prevState.collapseProp &&
- nextProps.collapse !== undefined
+ }
+
+ // Handle uncollapsing toots when the shared CW state is expanded
+ if (nextProps.settings.getIn(['content_warnings', 'shared_state']) &&
+ nextProps.status?.get('spoiler_text')?.length && nextProps.status?.get('hidden') === false &&
+ prevState.statusPropHidden !== false && prevState.isCollapsed
) {
- update.isCollapsed = nextProps.collapse;
- if (nextProps.collapse) update.isExpanded = false;
+ update.isCollapsed = false;
updated = true;
}
+
+ // The “expanded” prop is used to one-off change the local state.
+ // It's used in the thread view when unfolding/re-folding all CWs at once.
if (nextProps.expanded !== prevState.expandedProp &&
nextProps.expanded !== undefined
) {
updated = true;
}
- if (nextProps.expanded === undefined &&
- prevState.isExpanded === undefined &&
- update.isExpanded === undefined
- ) {
- const isExpanded = autoUnfoldCW(nextProps.settings, nextProps.status);
- if (isExpanded !== undefined) {
- update.isExpanded = isExpanded;
- updated = true;
- }
+ if (prevState.isExpanded === undefined && update.isExpanded === undefined) {
+ update.isExpanded = autoUnfoldCW(nextProps.settings, nextProps.status);
+ updated = true;
}
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
- if (function () {
- switch (true) {
- case !!collapse:
- case !!autoCollapseSettings.get('all'):
- case autoCollapseSettings.get('notifications') && !!muted:
- case autoCollapseSettings.get('lengthy') && node.clientHeight > (
- status.get('media_attachments').size && !muted ? 650 : 400
- ):
- case autoCollapseSettings.get('reblogs') && prepend === 'reblogged_by':
- case autoCollapseSettings.get('replies') && status.get('in_reply_to_id', null) !== null:
- case autoCollapseSettings.get('media') && !(status.get('spoiler_text').length) && !!status.get('media_attachments').size:
- return true;
- default:
- return false;
- }
- }()) {
+ // Don't autocollapse if CW state is shared and status is explicitly revealed,
+ // as it could cause surprising changes when receiving notifications
+ if (settings.getIn(['content_warnings', 'shared_state']) && status.get('spoiler_text').length && !status.get('hidden')) return;
+
+ if (collapse ||
+ autoCollapseSettings.get('all') ||
+ (autoCollapseSettings.get('notifications') && muted) ||
+ (autoCollapseSettings.get('lengthy') && node.clientHeight > ((status.get('media_attachments').size && !muted) ? 650 : 400)) ||
+ (autoCollapseSettings.get('reblogs') && prepend === 'reblogged_by') ||
+ (autoCollapseSettings.get('replies') && status.get('in_reply_to_id', null) !== null) ||
+ (autoCollapseSettings.get('media') && !(status.get('spoiler_text').length) && status.get('media_attachments').size > 0)
+ ) {
this.setCollapsed(true);
// Hack to fix timeline jumps on second rendering when auto-collapsing
this.setState({ autoCollapsed: true });
// is enabled, so we don't have to.
setCollapsed = (value) => {
if (this.props.settings.getIn(['collapsed', 'enabled'])) {
- this.setState({ isCollapsed: value });
if (value) {
this.setExpansion(false);
}
+ this.setState({ isCollapsed: value });
} else {
this.setState({ isCollapsed: false });
}
}
setExpansion = (value) => {
+ if (this.props.settings.getIn(['content_warnings', 'shared_state']) && this.props.status.get('hidden') === value) {
+ this.props.onToggleHidden(this.props.status);
+ }
+
this.setState({ isExpanded: value });
if (value) {
this.setCollapsed(false);
}
handleExpandedToggle = () => {
- if (this.props.status.get('spoiler_text')) {
+ if (this.props.settings.getIn(['content_warnings', 'shared_state'])) {
+ this.props.onToggleHidden(this.props.status);
+ } else if (this.props.status.get('spoiler_text')) {
this.setExpansion(!this.state.isExpanded);
}
};
usingPiP,
...other
} = this.props;
- const { isExpanded, isCollapsed, forceFilter } = this.state;
+ const { isCollapsed, forceFilter } = this.state;
let background = null;
let attachments = null;
return null;
}
+ const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded;
+
const handlers = {
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
pin,
unpin,
} from 'flavours/glitch/actions/interactions';
-import { muteStatus, unmuteStatus, deleteStatus, editStatus } from 'flavours/glitch/actions/statuses';
+import {
+ muteStatus,
+ unmuteStatus,
+ deleteStatus,
+ hideStatus,
+ revealStatus,
+ editStatus
+} from 'flavours/glitch/actions/statuses';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initReport } from 'flavours/glitch/actions/reports';
}
},
+ onToggleHidden (status) {
+ if (status.get('hidden')) {
+ dispatch(revealStatus(status.get('id')));
+ } else {
+ dispatch(hideStatus(status.get('id')));
+ }
+ },
+
deployPictureInPicture (status, type, mediaProps) {
dispatch((_, getState) => {
if (getState().getIn(['local_settings', 'media', 'pop_in_player'])) {
}
handleShowMore = () => {
+ this.props.onToggleHidden(this.props.lastStatus);
+
if (this.props.lastStatus.get('spoiler_text')) {
this.setExpansion(!this.state.isExpanded);
}
render () {
const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
- const { isExpanded } = this.state;
if (lastStatus === null) {
return null;
}
+ const isExpanded = this.props.settings.getIn(['content_warnings', 'shared_state']) ? !lastStatus.get('hidden') : this.state.isExpanded;
+
const menu = [
{ text: intl.formatMessage(messages.open), action: this.handleClick },
null,
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
unread: conversation.get('unread'),
lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }),
+ settings: state.get('local_settings'),
};
};
};
({ intl, onChange, settings }) => (
<div className='glitch local-settings__page content_warnings'>
<h1><FormattedMessage id='settings.content_warnings' defaultMessage='Content warnings' /></h1>
+ <LocalSettingsPageItem
+ settings={settings}
+ item={['content_warnings', 'shared_state']}
+ id='mastodon-settings--content_warnings-shared_state'
+ onChange={onChange}
+ >
+ <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']}
directCompose,
} from 'flavours/glitch/actions/compose';
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
-import { muteStatus, unmuteStatus, deleteStatus, editStatus } from 'flavours/glitch/actions/statuses';
+import {
+ muteStatus,
+ unmuteStatus,
+ deleteStatus,
+ editStatus,
+ hideStatus,
+ revealStatus
+} from 'flavours/glitch/actions/statuses';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initReport } from 'flavours/glitch/actions/reports';
return updated ? update : null;
}
- handleExpandedToggle = () => {
- if (this.props.status.get('spoiler_text')) {
+ handleToggleHidden = () => {
+ const { status } = this.props;
+
+ if (this.props.settings.getIn(['content_warnings', 'shared_state'])) {
+ if (status.get('hidden')) {
+ this.props.dispatch(revealStatus(status.get('id')));
+ } else {
+ this.props.dispatch(hideStatus(status.get('id')));
+ }
+ } else if (this.props.status.get('spoiler_text')) {
this.setExpansion(!this.state.isExpanded);
}
- };
+ }
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
handleToggleAll = () => {
- const { isExpanded } = this.state;
+ const { status, ancestorsIds, descendantsIds, settings } = this.props;
+ const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
+ let { isExpanded } = this.state;
+
+ if (settings.getIn(['content_warnings', 'shared_state']))
+ isExpanded = !status.get('hidden');
+
+ if (!isExpanded) {
+ this.props.dispatch(revealStatus(statusIds));
+ } else {
+ this.props.dispatch(hideStatus(statusIds));
+ }
+
this.setState({ isExpanded: !isExpanded, threadExpanded: !isExpanded });
}
render () {
let ancestors, descendants;
- const { setExpansion } = this;
const { status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props;
- const { fullscreen, isExpanded } = this.state;
+ const { fullscreen } = this.state;
if (status === null) {
return (
);
}
+ const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded;
+
if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <div>{this.renderChildren(ancestorsIds)}</div>;
}
bookmark: this.handleHotkeyBookmark,
mention: this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile,
- toggleSpoiler: this.handleExpandedToggle,
+ toggleSpoiler: this.handleToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};
onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia}
expanded={isExpanded}
- onToggleHidden={this.handleExpandedToggle}
+ onToggleHidden={this.handleToggleHidden}
domain={domain}
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
content_warnings : ImmutableMap({
filter : null,
media_outside: false,
+ shared_state : false,
}),
collapsed : ImmutableMap({
enabled : true,
import {
STATUS_MUTE_SUCCESS,
STATUS_UNMUTE_SUCCESS,
+ STATUS_REVEAL,
+ STATUS_HIDE,
+ STATUS_COLLAPSE,
} from 'flavours/glitch/actions/statuses';
import {
TIMELINE_DELETE,
return state.setIn([action.id, 'muted'], true);
case STATUS_UNMUTE_SUCCESS:
return state.setIn([action.id, 'muted'], false);
+ case STATUS_REVEAL:
+ return state.withMutations(map => {
+ action.ids.forEach(id => {
+ if (!(state.get(id) === undefined)) {
+ map.setIn([id, 'hidden'], false);
+ }
+ });
+ });
+ case STATUS_HIDE:
+ return state.withMutations(map => {
+ action.ids.forEach(id => {
+ if (!(state.get(id) === undefined)) {
+ map.setIn([id, 'hidden'], true);
+ }
+ });
+ });
+ case STATUS_COLLAPSE:
+ return state.setIn([action.id, 'collapsed'], action.isCollapsed);
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references);
default:
import { expandSpoilers } from 'flavours/glitch/util/initial_state';
-export function autoUnfoldCW (settings, status) {
- if (!expandSpoilers) {
+function _autoUnfoldCW(spoiler_text, skip_unfold_regex) {
+ if (!expandSpoilers)
return false;
- }
-
- const rawRegex = settings.getIn(['content_warnings', 'filter']);
- if (!rawRegex) {
+ if (!skip_unfold_regex)
return true;
- }
- let regex = null;
+ let regex = null;
try {
- regex = rawRegex && new RegExp(rawRegex.trim(), 'i');
+ regex = new RegExp(skip_unfold_regex.trim(), 'i');
} catch (e) {
- // Bad regex, don't affect filters
+ // Bad regex, skip filters
+ return true;
}
- if (!(status && regex)) {
- return undefined;
- }
- return !regex.test(status.get('spoiler_text'));
+ return !regex.test(spoiler_text);
+}
+
+export function autoHideCW(settings, spoiler_text) {
+ return !_autoUnfoldCW(spoiler_text, settings.getIn(['content_warnings', 'filter']));
+}
+
+export function autoUnfoldCW(settings, status) {
+ if (!status)
+ return false;
+
+ return _autoUnfoldCW(status.get('spoiler_text'), settings.getIn(['content_warnings', 'filter']));
}