diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js
index f4372fb31d..ec41fea6e2 100644
--- a/app/javascript/flavours/glitch/actions/importer/index.js
+++ b/app/javascript/flavours/glitch/actions/importer/index.js
@@ -63,7 +63,7 @@ export function importFetchedStatuses(statuses) {
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) {
diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js
index c38af196af..c6acdbdbb3 100644
--- a/app/javascript/flavours/glitch/actions/importer/normalizer.js
+++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js
@@ -1,6 +1,7 @@
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();
@@ -41,7 +42,7 @@ export function normalizeAccount(account) {
return account;
}
-export function normalizeStatus(status, normalOldStatus) {
+export function normalizeStatus(status, normalOldStatus, settings) {
const normalStatus = { ...status };
normalStatus.account = status.account.id;
@@ -60,6 +61,7 @@ export function normalizeStatus(status, normalOldStatus) {
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(/
/g, '\n').replace(/<\/p>
/g, '\n\n'); @@ -68,6 +70,7 @@ export function normalizeStatus(status, normalOldStatus) { 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; diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 6ffcf181de..1f223f22ef 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -24,6 +24,10 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST'; 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'; @@ -277,3 +281,33 @@ export function unmuteStatusFail(id, error) { 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, + }; +} diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 219f7de7ab..11c81765b0 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -81,8 +81,8 @@ class Status extends ImmutablePureComponent { 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, @@ -121,7 +121,6 @@ class Status extends ImmutablePureComponent { 'settings', 'prepend', 'muted', - 'collapse', 'notification', 'hidden', 'expanded', @@ -149,14 +148,14 @@ class Status extends ImmutablePureComponent { 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'])) { @@ -164,14 +163,19 @@ class Status extends ImmutablePureComponent { 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 ) { @@ -180,15 +184,9 @@ class Status extends ImmutablePureComponent { 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) { @@ -243,22 +241,18 @@ class Status extends ImmutablePureComponent { 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 }); @@ -309,16 +303,20 @@ class Status extends ImmutablePureComponent { // 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); @@ -365,7 +363,9 @@ class Status extends ImmutablePureComponent { } 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); } }; @@ -505,7 +505,7 @@ class Status extends ImmutablePureComponent { usingPiP, ...other } = this.props; - const { isExpanded, isCollapsed, forceFilter } = this.state; + const { isCollapsed, forceFilter } = this.state; let background = null; let attachments = null; @@ -528,6 +528,8 @@ class Status extends ImmutablePureComponent { return null; } + const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded; + const handlers = { reply: this.handleHotkeyReply, favourite: this.handleHotkeyFavourite, diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 358b89ab9f..6c8f261e46 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -17,7 +17,14 @@ import { 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'; @@ -252,6 +259,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, + 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'])) { diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js index 202d966761..7107c9db3b 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js @@ -132,6 +132,8 @@ class Conversation extends ImmutablePureComponent { } handleShowMore = () => { + this.props.onToggleHidden(this.props.lastStatus); + if (this.props.lastStatus.get('spoiler_text')) { this.setExpansion(!this.state.isExpanded); } @@ -143,12 +145,13 @@ class Conversation extends ImmutablePureComponent { 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, diff --git a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js index b15ce9f0f6..f5e5946e3b 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js @@ -23,6 +23,7 @@ const mapStateToProps = () => { 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'), }; }; }; diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index 94a87c484b..ffa4e34091 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -303,6 +303,15 @@ class LocalSettingsPage extends React.PureComponent { ({ intl, onChange, settings }) => (