From 485b43ed7e312ab2c3bad446132c57d6a0ce7de4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 27 Jun 2022 09:30:15 +0200 Subject: [Glitch] Add notifications for new reports Port 2936f42a14cfdca70d4a9653dab29382315945e7 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/components/icon_button.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'app/javascript/flavours/glitch/components') diff --git a/app/javascript/flavours/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.js index 9a05badd0..be2468d68 100644 --- a/app/javascript/flavours/glitch/components/icon_button.js +++ b/app/javascript/flavours/glitch/components/icon_button.js @@ -140,8 +140,16 @@ export default class IconButton extends React.PureComponent { ); if (href) { - contents = ( - + return ( + {contents} ); -- cgit From 30e7836a1961e8e70938b14232abd91238eb2be4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 5 Jul 2022 02:41:40 +0200 Subject: [Glitch] Add customizable user roles Port front-end changes from 44b2ee3485ba0845e5910cefcb4b1e2f84f34470 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/components/status_action_bar.js | 6 ++++-- app/javascript/flavours/glitch/containers/mastodon.js | 1 + .../flavours/glitch/features/account/components/header.js | 9 +++++++-- .../features/notifications/components/column_settings.js | 10 +++++++--- .../glitch/features/status/components/action_bar.js | 6 ++++-- .../flavours/glitch/features/ui/components/link_footer.js | 9 +++++++-- app/javascript/flavours/glitch/permissions.js | 3 +++ app/javascript/flavours/glitch/reducers/meta.js | 3 ++- app/javascript/flavours/glitch/styles/admin.scss | 15 +++++++++++++++ app/javascript/flavours/glitch/styles/forms.scss | 4 ++++ app/javascript/flavours/glitch/util/initial_state.js | 2 -- 11 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 app/javascript/flavours/glitch/permissions.js (limited to 'app/javascript/flavours/glitch/components') diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 0a5c5b69d..667afac5a 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -5,10 +5,11 @@ import IconButton from './icon_button'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me, isStaff } from 'flavours/glitch/util/initial_state'; +import { me } from 'flavours/glitch/util/initial_state'; import RelativeTimestamp from './relative_timestamp'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/util/backend_links'; import classNames from 'classnames'; +import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -47,6 +48,7 @@ class StatusActionBar extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -240,7 +242,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - if (isStaff && (accountAdminLink || statusAdminLink)) { + if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) { menu.push(null); if (accountAdminLink !== undefined) { menu.push({ diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js index 989e37024..d07b2b3d0 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.js +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -31,6 +31,7 @@ const createIdentityContext = state => ({ signedIn: !!state.meta.me, accountId: state.meta.me, accessToken: state.meta.access_token, + permissions: state.role.permissions, }); export default class Mastodon extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 45aba53f7..53170b7a6 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me, isStaff } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, me } from 'flavours/glitch/util/initial_state'; import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links'; import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; @@ -13,6 +13,7 @@ import Button from 'flavours/glitch/components/button'; import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; +import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -64,6 +65,10 @@ const dateFormatOptions = { export default @injectIntl class Header extends ImmutablePureComponent { + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { account: ImmutablePropTypes.map, identity_props: ImmutablePropTypes.list, @@ -244,7 +249,7 @@ class Header extends ImmutablePureComponent { } } - if (account.get('id') !== me && isStaff && accountAdminLink) { + if (account.get('id') !== me && (this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) { menu.push(null); menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) }); } diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js index a8502f563..42ab9de35 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js @@ -6,10 +6,14 @@ import ClearColumnButton from './clear_column_button'; import GrantPermissionButton from './grant_permission_button'; import SettingToggle from './setting_toggle'; import PillBarButton from './pill_bar_button'; -import { isStaff } from 'flavours/glitch/util/initial_state'; +import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions'; export default class ColumnSettings extends React.PureComponent { + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { settings: ImmutablePropTypes.map.isRequired, pushSettings: ImmutablePropTypes.map.isRequired, @@ -167,7 +171,7 @@ export default class ColumnSettings extends React.PureComponent { - {isStaff && ( + {(this.context.identity.permissions & PERMISSION_MANAGE_USERS === PERMISSION_MANAGE_USERS) && (
@@ -180,7 +184,7 @@ export default class ColumnSettings extends React.PureComponent {
)} - {isStaff && ( + {(this.context.identity.permissions & PERMISSION_MANAGE_REPORTS === PERMISSION_MANAGE_REPORTS) && (
diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index a67a045da..ef0f0f2b7 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -4,9 +4,10 @@ import IconButton from 'flavours/glitch/components/icon_button'; import ImmutablePropTypes from 'react-immutable-proptypes'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; -import { me, isStaff } from 'flavours/glitch/util/initial_state'; +import { me } from 'flavours/glitch/util/initial_state'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/util/backend_links'; import classNames from 'classnames'; +import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -41,6 +42,7 @@ class ActionBar extends React.PureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -182,7 +184,7 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - if (isStaff && (accountAdminLink || statusAdminLink)) { + if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) { menu.push(null); if (accountAdminLink !== undefined) { menu.push({ diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.js b/app/javascript/flavours/glitch/features/ui/components/link_footer.js index d9579e9c9..3abdaad4b 100644 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js @@ -3,10 +3,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { Link } from 'react-router-dom'; -import { invitesEnabled, limitedFederationMode, version, repository, source_url } from 'flavours/glitch/util/initial_state'; +import { limitedFederationMode, version, repository, source_url } from 'flavours/glitch/util/initial_state'; import { signOutLink, securityLink } from 'flavours/glitch/util/backend_links'; import { logOut } from 'flavours/glitch/util/log_out'; import { openModal } from 'flavours/glitch/actions/modal'; +import { PERMISSION_INVITE_USERS } from 'flavours/glitch/permissions'; const messages = defineMessages({ logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, @@ -28,6 +29,10 @@ export default @injectIntl @connect(null, mapDispatchToProps) class LinkFooter extends React.PureComponent { + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { onLogout: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -46,7 +51,7 @@ class LinkFooter extends React.PureComponent { return (
    - {invitesEnabled &&
  • ·
  • } + {((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) &&
  • ·
  • } {!!securityLink &&
  • ·
  • } {!limitedFederationMode &&
  • ·
  • }
  • ·
  • diff --git a/app/javascript/flavours/glitch/permissions.js b/app/javascript/flavours/glitch/permissions.js new file mode 100644 index 000000000..752ddd6c5 --- /dev/null +++ b/app/javascript/flavours/glitch/permissions.js @@ -0,0 +1,3 @@ +export const PERMISSION_INVITE_USERS = 0x0000000000010000; +export const PERMISSION_MANAGE_USERS = 0x0000000000000400; +export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010; diff --git a/app/javascript/flavours/glitch/reducers/meta.js b/app/javascript/flavours/glitch/reducers/meta.js index a98dc436a..0f3ab3b84 100644 --- a/app/javascript/flavours/glitch/reducers/meta.js +++ b/app/javascript/flavours/glitch/reducers/meta.js @@ -4,12 +4,13 @@ import { Map as ImmutableMap } from 'immutable'; const initialState = ImmutableMap({ streaming_api_base_url: null, access_token: null, + permissions: '0', }); export default function meta(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: - return state.merge(action.state.get('meta')); + return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions'])); default: return state; } diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 6ed67edc4..77890c467 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -943,6 +943,10 @@ a.name-tag, margin-top: 15px; } +.user-role { + color: var(--user-role-accent); +} + .announcements-list, .filters-list { border: 1px solid lighten($ui-base-color, 4%); @@ -979,6 +983,17 @@ a.name-tag, &__meta { padding: 0 15px; color: $dark-text-color; + + a { + color: inherit; + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } } &__action-bar { diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 1ce13b874..8ae2b5bd8 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -247,6 +247,10 @@ code { } } + .input.with_block_label.user_role_permissions_as_keys ul { + columns: unset; + } + .input.datetime .label_input select { display: inline-block; width: auto; diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index b6eab0c87..90dada4b3 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -23,14 +23,12 @@ export const me = getMeta('me'); export const searchEnabled = getMeta('search_enabled'); export const maxChars = (initialState && initialState.max_toot_chars) || 500; export const pollLimits = (initialState && initialState.poll_limits); -export const invitesEnabled = getMeta('invites_enabled'); export const limitedFederationMode = getMeta('limited_federation_mode'); export const repository = getMeta('repository'); export const source_url = getMeta('source_url'); export const version = getMeta('version'); export const mascot = getMeta('mascot'); export const profile_directory = getMeta('profile_directory'); -export const isStaff = getMeta('is_staff'); export const defaultContentType = getMeta('default_content_type'); export const forceSingleColumn = getMeta('advanced_layout') === false; export const useBlurhash = getMeta('use_blurhash'); -- cgit From 1ceebf2710794dc35211a16ad3af7f834641a2ab Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 22 Jul 2022 03:17:41 +0200 Subject: [Glitch] Change hashtag numbers to have clearer labels in web UI Port f5d8501138c2bb0058cca41fd76f2c8d9a7593ab to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/components/hashtag.js | 18 ++++++++++++------ .../flavours/glitch/styles/components/search.scss | 7 +++++++ 2 files changed, 19 insertions(+), 6 deletions(-) (limited to 'app/javascript/flavours/glitch/components') diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js index 769185a2b..5bbf32c87 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/components/hashtag.js @@ -1,7 +1,7 @@ // @ts-check import React from 'react'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from './permalink'; @@ -9,6 +9,10 @@ import ShortNumber from 'flavours/glitch/components/short_number'; import Skeleton from 'flavours/glitch/components/skeleton'; import classNames from 'classnames'; +const messages = defineMessages({ + totalVolume: { id: 'hashtag.total_volume', defaultMessage: 'Total volume in the last {days, plural, one {day} other {{days} days}}' }, +}); + class SilentErrorBoundary extends React.Component { static propTypes = { @@ -41,10 +45,11 @@ class SilentErrorBoundary extends React.Component { const accountsCountRenderer = (displayNumber, pluralReady) => ( {displayNumber}, + days: 2, }} /> ); @@ -64,7 +69,7 @@ ImmutableHashtag.propTypes = { hashtag: ImmutablePropTypes.map.isRequired, }; -const Hashtag = ({ name, href, to, people, uses, history, className }) => ( +const Hashtag = injectIntl(({ name, href, to, people, uses, history, className, intl }) => (
    @@ -74,9 +79,10 @@ const Hashtag = ({ name, href, to, people, uses, history, className }) => ( {typeof people !== 'undefined' ? : }
    -
    + {typeof uses !== 'undefined' ? : } -
    + * +
    @@ -86,7 +92,7 @@ const Hashtag = ({ name, href, to, people, uses, history, className }) => (
    -); +)); Hashtag.propTypes = { name: PropTypes.string, diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss index f7415368b..17a34db62 100644 --- a/app/javascript/flavours/glitch/styles/components/search.scss +++ b/app/javascript/flavours/glitch/styles/components/search.scss @@ -176,6 +176,13 @@ padding-right: 15px; margin-left: 5px; color: $secondary-text-color; + text-decoration: none; + + &__asterisk { + color: $darker-text-color; + font-size: 18px; + vertical-align: super; + } } &__sparkline { -- cgit From 3f4e27ab6f33ab1a18954a3046e6e9abdc2d5e12 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 24 Jul 2022 11:10:49 +0200 Subject: Add local setting toggle to move media attachments outside CWs Fixes #1812 --- .../flavours/glitch/components/status.js | 30 +++++++++++++++++----- .../glitch/features/local_settings/page/index.js | 9 +++++++ .../features/status/components/detailed_status.js | 27 ++++++++++++++----- .../flavours/glitch/reducers/local_settings.js | 3 ++- 4 files changed, 55 insertions(+), 14 deletions(-) (limited to 'app/javascript/flavours/glitch/components') diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 8a5fda676..d3bdcdc91 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -508,8 +508,21 @@ class Status extends ImmutablePureComponent { const { isExpanded, isCollapsed, forceFilter } = this.state; let background = null; let attachments = null; - let media = []; - let mediaIcons = []; + + // 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; + + if (settings.getIn(['content_warnings', 'media_outside'])) { + media = extraMedia; + mediaIcons = extraMediaIcons; + } if (status === null) { return null; @@ -681,8 +694,8 @@ class Status extends ImmutablePureComponent { } if (status.get('poll')) { - media.push(); - mediaIcons.push('tasks'); + contentMedia.push(); + contentMediaIcons.push('tasks'); } // Here we prepare extra data-* attributes for CSS selectors. @@ -748,7 +761,7 @@ class Status extends ImmutablePureComponent { + + {extraMedia} + {!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? ( (

    + + + + ); - mediaIcons.push('tasks'); + contentMedia.push(); + contentMediaIcons.push('tasks'); } if (status.get('application')) { @@ -282,8 +295,8 @@ class DetailedStatus extends ImmutablePureComponent { + {extraMedia} +
    diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index d4cdc124f..6d8feaf66 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -25,7 +25,8 @@ const initialState = ImmutableMap({ tag_misleading_links: true, rewrite_mentions: 'no', content_warnings : ImmutableMap({ - filter : null, + filter : null, + media_outside: false, }), collapsed : ImmutableMap({ enabled : true, -- cgit From a5f81e1abd82cb66e590b73af83c720d6410fafd Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 24 Jul 2022 13:16:03 +0200 Subject: Improve interaction between out-of-CW attachments and collapsed toots --- app/javascript/flavours/glitch/components/status.js | 3 +-- app/javascript/flavours/glitch/components/status_content.js | 6 ++++++ .../flavours/glitch/features/status/components/detailed_status.js | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) (limited to 'app/javascript/flavours/glitch/components') diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index d3bdcdc91..219f7de7a 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -771,6 +771,7 @@ class Status extends ImmutablePureComponent { - {extraMedia} - {!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? ( + {extraMedia} +
    ); } else if (parseClick) { @@ -372,6 +376,7 @@ export default class StatusContent extends React.PureComponent { lang={lang} /> {media} + {extraMedia}
    ); } else { @@ -391,6 +396,7 @@ export default class StatusContent extends React.PureComponent { lang={lang} /> {media} + {extraMedia}
); } diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index bfbfabcb6..301a2add6 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -296,6 +296,7 @@ class DetailedStatus extends ImmutablePureComponent { - {extraMedia} -
-- cgit From 18346f40443f1c01b45d94be1b0edce20b2c27be Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 24 Jul 2022 20:01:30 +0200 Subject: Add option to share CW toggle state across instances of a post --- .../flavours/glitch/actions/importer/index.js | 2 +- .../flavours/glitch/actions/importer/normalizer.js | 5 +- app/javascript/flavours/glitch/actions/statuses.js | 34 +++++++++ .../flavours/glitch/components/status.js | 80 +++++++++++----------- .../flavours/glitch/containers/status_container.js | 17 ++++- .../direct_timeline/components/conversation.js | 5 +- .../containers/conversation_container.js | 1 + .../glitch/features/local_settings/page/index.js | 9 +++ .../flavours/glitch/features/status/index.js | 46 ++++++++++--- .../flavours/glitch/reducers/local_settings.js | 1 + .../flavours/glitch/reducers/statuses.js | 21 ++++++ .../flavours/glitch/util/content_warning.js | 33 +++++---- 12 files changed, 188 insertions(+), 66 deletions(-) (limited to 'app/javascript/flavours/glitch/components') diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index f4372fb31..ec41fea6e 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 c38af196a..c6acdbdbb 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 6ffcf181d..1f223f22e 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 219f7de7a..11c81765b 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 358b89ab9..6c8f261e4 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 202d96676..7107c9db3 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 b15ce9f0f..f5e5946e3 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 94a87c484..ffa4e3409 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 }) => (

+ + + + { - 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 }); @@ -354,7 +369,19 @@ class Status extends ImmutablePureComponent { } 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 }); } @@ -513,9 +540,8 @@ class Status extends ImmutablePureComponent { 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 ( @@ -526,6 +552,8 @@ class Status extends ImmutablePureComponent { ); } + const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded; + if (ancestorsIds && ancestorsIds.size > 0) { ancestors =
{this.renderChildren(ancestorsIds)}
; } @@ -543,7 +571,7 @@ class Status extends ImmutablePureComponent { bookmark: this.handleHotkeyBookmark, mention: this.handleHotkeyMention, openProfile: this.handleHotkeyOpenProfile, - toggleSpoiler: this.handleExpandedToggle, + toggleSpoiler: this.handleToggleHidden, toggleSensitive: this.handleHotkeyToggleSensitive, openMedia: this.handleHotkeyOpenMedia, }; @@ -574,7 +602,7 @@ class Status extends ImmutablePureComponent { onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} expanded={isExpanded} - onToggleHidden={this.handleExpandedToggle} + onToggleHidden={this.handleToggleHidden} domain={domain} showMedia={this.state.showMedia} onToggleMediaVisibility={this.handleToggleMediaVisibility} diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index 6d8feaf66..62ce29f0c 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -27,6 +27,7 @@ const initialState = ImmutableMap({ content_warnings : ImmutableMap({ filter : null, media_outside: false, + shared_state : false, }), collapsed : ImmutableMap({ enabled : true, diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index 5db766b96..333e4b45c 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -10,6 +10,9 @@ import { import { STATUS_MUTE_SUCCESS, STATUS_UNMUTE_SUCCESS, + STATUS_REVEAL, + STATUS_HIDE, + STATUS_COLLAPSE, } from 'flavours/glitch/actions/statuses'; import { TIMELINE_DELETE, @@ -56,6 +59,24 @@ export default function statuses(state = initialState, action) { 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: diff --git a/app/javascript/flavours/glitch/util/content_warning.js b/app/javascript/flavours/glitch/util/content_warning.js index baeb97881..383a34226 100644 --- a/app/javascript/flavours/glitch/util/content_warning.js +++ b/app/javascript/flavours/glitch/util/content_warning.js @@ -1,26 +1,31 @@ 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'])); } -- cgit