diff options
Diffstat (limited to 'app/javascript/flavours/glitch')
42 files changed, 722 insertions, 153 deletions
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(/<br\s*\/?>/g, '\n').replace(/<\/p><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/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 85938867b..3993b1ea5 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -103,6 +103,10 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch(importFetchedStatus(notification.status)); } + if (notification.report) { + dispatch(importFetchedAccount(notification.report.target_account)); + } + dispatch({ type: NOTIFICATIONS_UPDATE, notification, @@ -146,6 +150,7 @@ const excludeTypesFromFilter = filter => { 'status', 'update', 'admin.sign_up', + 'admin.report', ]); return allTypes.filterNot(item => item === filter).toJS(); @@ -191,6 +196,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); + dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); 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/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) => ( <FormattedMessage id='trends.counter_by_accounts' - defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking' + defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}' values={{ count: pluralReady, counter: <strong>{displayNumber}</strong>, + 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 }) => ( <div className={classNames('trends__item', className)}> <div className='trends__item__name'> <Permalink href={href} to={to}> @@ -74,9 +79,10 @@ const Hashtag = ({ name, href, to, people, uses, history, className }) => ( {typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />} </div> - <div className='trends__item__current'> + <abbr className='trends__item__current' title={intl.formatMessage(messages.totalVolume, { days: 2 })}> {typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />} - </div> + <span className='trends__item__current__asterisk'>*</span> + </abbr> <div className='trends__item__sparkline'> <SilentErrorBoundary> @@ -86,7 +92,7 @@ const Hashtag = ({ name, href, to, people, uses, history, className }) => ( </SilentErrorBoundary> </div> </div> -); +)); Hashtag.propTypes = { name: PropTypes.string, 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 = ( - <a href={href} target='_blank' rel='noopener noreferrer'> + return ( + <a + href={href} + aria-label={title} + title={title} + target='_blank' + rel='noopener noreferrer' + className={classes} + style={style} + > {contents} </a> ); diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 8a5fda676..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,16 +505,31 @@ 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; - 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; } + const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded; + const handlers = { reply: this.handleHotkeyReply, favourite: this.handleHotkeyFavourite, @@ -681,8 +696,8 @@ class Status extends ImmutablePureComponent { } if (status.get('poll')) { - media.push(<PollContainer pollId={status.get('poll')} />); - mediaIcons.push('tasks'); + contentMedia.push(<PollContainer pollId={status.get('poll')} />); + contentMediaIcons.push('tasks'); } // Here we prepare extra data-* attributes for CSS selectors. @@ -748,7 +763,7 @@ class Status extends ImmutablePureComponent { </span> <StatusIcons status={status} - mediaIcons={mediaIcons} + mediaIcons={contentMediaIcons.concat(extraMediaIcons)} collapsible={settings.getIn(['collapsed', 'enabled'])} collapsed={isCollapsed} setCollapsed={setCollapsed} @@ -757,8 +772,9 @@ class Status extends ImmutablePureComponent { </header> <StatusContent status={status} - media={media} - mediaIcons={mediaIcons} + media={contentMedia} + extraMedia={extraMedia} + mediaIcons={contentMediaIcons} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} parseClick={parseClick} @@ -766,6 +782,7 @@ class Status extends ImmutablePureComponent { tagLinks={settings.get('tag_misleading_links')} rewriteMentions={settings.get('rewrite_mentions')} /> + {!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? ( <StatusActionBar {...other} 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/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 6a027f8d2..39891da4f 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -70,6 +70,7 @@ export default class StatusContent extends React.PureComponent { collapsed: PropTypes.bool, onExpandedToggle: PropTypes.func, media: PropTypes.node, + extraMedia: PropTypes.node, mediaIcons: PropTypes.arrayOf(PropTypes.string), parseClick: PropTypes.func, disabled: PropTypes.bool, @@ -256,6 +257,7 @@ export default class StatusContent extends React.PureComponent { const { status, media, + extraMedia, mediaIcons, parseClick, disabled, @@ -351,6 +353,8 @@ export default class StatusContent extends React.PureComponent { {media} </div> + {extraMedia} + </div> ); } else if (parseClick) { @@ -372,6 +376,7 @@ export default class StatusContent extends React.PureComponent { lang={lang} /> {media} + {extraMedia} </div> ); } else { @@ -391,6 +396,7 @@ export default class StatusContent extends React.PureComponent { lang={lang} /> {media} + {extraMedia} </div> ); } 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/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/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/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 2490b6e2d..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,38 +303,59 @@ class LocalSettingsPage extends React.PureComponent { ({ intl, onChange, settings }) => ( <div className='glitch local-settings__page content_warnings'> <h1><FormattedMessage id='settings.content_warnings' defaultMessage='Content warnings' /></h1> - <DeprecatedLocalSettingsPageItem - id='mastodon-settings--content_warnings-auto_unfold' - value={expandSpoilers} + <LocalSettingsPageItem + settings={settings} + item={['content_warnings', 'shared_state']} + id='mastodon-settings--content_warnings-shared_state' + onChange={onChange} > - <FormattedMessage id='settings.enable_content_warnings_auto_unfold' defaultMessage='Automatically unfold content-warnings' /> - <span className='hint'> - <FormattedMessage - id='settings.deprecated_setting' - defaultMessage="This setting is now controlled from Mastodon's {settings_page_link}" - values={{ - settings_page_link: ( - <a href={preferenceLink('user_setting_expand_spoilers')}> - <FormattedMessage - id='settings.shared_settings_link' - defaultMessage='user preferences' - /> - </a> - ) - }} - /> - </span> - </DeprecatedLocalSettingsPageItem> + <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', 'filter']} - id='mastodon-settings--content_warnings-auto_unfold' + item={['content_warnings', 'media_outside']} + id='mastodon-settings--content_warnings-media_outside' onChange={onChange} - placeholder={intl.formatMessage(messages.regexp)} - disabled={!expandSpoilers} > - <FormattedMessage id='settings.content_warnings_filter' defaultMessage='Content warnings to not automatically unfold:' /> + <FormattedMessage id='settings.content_warnings_media_outside' defaultMessage='Display media attachments outside content warnings' /> + <span className='hint'><FormattedMessage id='settings.content_warnings_media_outside_hint' defaultMessage='Reproduce upstream Mastodon behavior by having the Content Warning toggle not affect media attachments' /></span> </LocalSettingsPageItem> + <section> + <h2><FormattedMessage id='settings.content_warnings_unfold_opts' defaultMessage='Auto-unfolding options' /></h2> + <DeprecatedLocalSettingsPageItem + id='mastodon-settings--content_warnings-auto_unfold' + value={expandSpoilers} + > + <FormattedMessage id='settings.enable_content_warnings_auto_unfold' defaultMessage='Automatically unfold content-warnings' /> + <span className='hint'> + <FormattedMessage + id='settings.deprecated_setting' + defaultMessage="This setting is now controlled from Mastodon's {settings_page_link}" + values={{ + settings_page_link: ( + <a href={preferenceLink('user_setting_expand_spoilers')}> + <FormattedMessage + id='settings.shared_settings_link' + defaultMessage='user preferences' + /> + </a> + ) + }} + /> + </span> + </DeprecatedLocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['content_warnings', 'filter']} + id='mastodon-settings--content_warnings-auto_unfold' + onChange={onChange} + placeholder={intl.formatMessage(messages.regexp)} + disabled={!expandSpoilers} + > + <FormattedMessage id='settings.content_warnings_filter' defaultMessage='Content warnings to not automatically unfold:' /> + </LocalSettingsPageItem> + </section> </div> ), ({ intl, onChange, settings }) => ( @@ -366,6 +387,7 @@ class LocalSettingsPage extends React.PureComponent { onChange={onChange} > <FormattedMessage id='settings.enable_collapsed' defaultMessage='Enable collapsed toots' /> + <span className='hint'><FormattedMessage id='settings.enable_collapsed_hint' defaultMessage='Collapsed posts have parts of their contents hidden to take up less screen space. This is distinct from the Content Warning feature' /></span> </LocalSettingsPageItem> <LocalSettingsPageItem settings={settings} @@ -457,6 +479,7 @@ class LocalSettingsPage extends React.PureComponent { dependsOn={[['collapsed', 'enabled']]} > <FormattedMessage id='settings.image_backgrounds_media' defaultMessage='Preview collapsed toot media' /> + <span className='hint'><FormattedMessage id='settings.image_backgrounds_media_hint' defaultMessage='If the post has any media attachment, use the first one as a background' /></span> </LocalSettingsPageItem> </section> </div> diff --git a/app/javascript/flavours/glitch/features/notifications/components/admin_report.js b/app/javascript/flavours/glitch/features/notifications/components/admin_report.js new file mode 100644 index 000000000..80beeb9da --- /dev/null +++ b/app/javascript/flavours/glitch/features/notifications/components/admin_report.js @@ -0,0 +1,108 @@ +// Package imports. +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { HotKeys } from 'react-hotkeys'; +import classNames from 'classnames'; + +// Our imports. +import Permalink from 'flavours/glitch/components/permalink'; +import AccountContainer from 'flavours/glitch/containers/account_container'; +import NotificationOverlayContainer from '../containers/overlay_container'; +import Icon from 'flavours/glitch/components/icon'; +import Report from './report'; + +const messages = defineMessages({ + adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' }, +}); + +export default class AdminReport extends ImmutablePureComponent { + + static propTypes = { + hidden: PropTypes.bool, + id: PropTypes.string.isRequired, + account: ImmutablePropTypes.map.isRequired, + notification: ImmutablePropTypes.map.isRequired, + unread: PropTypes.bool, + report: ImmutablePropTypes.map.isRequired, + }; + + handleMoveUp = () => { + const { notification, onMoveUp } = this.props; + onMoveUp(notification.get('id')); + } + + handleMoveDown = () => { + const { notification, onMoveDown } = this.props; + onMoveDown(notification.get('id')); + } + + handleOpen = () => { + this.handleOpenProfile(); + } + + handleOpenProfile = () => { + const { notification } = this.props; + this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`); + } + + handleMention = e => { + e.preventDefault(); + + const { notification, onMention } = this.props; + onMention(notification.get('account'), this.context.router.history); + } + + getHandlers () { + return { + moveUp: this.handleMoveUp, + moveDown: this.handleMoveDown, + open: this.handleOpen, + openProfile: this.handleOpenProfile, + mention: this.handleMention, + reply: this.handleMention, + }; + } + + render () { + const { intl, account, notification, unread, report } = this.props; + + // Links to the display name. + const displayName = account.get('display_name_html') || account.get('username'); + const link = ( + <bdi><Permalink + className='notification__display-name' + href={account.get('url')} + title={account.get('acct')} + to={`/@${account.get('acct')}`} + dangerouslySetInnerHTML={{ __html: displayName }} + /></bdi> + ); + + const targetAccount = report.get('target_account'); + const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') }; + const targetLink = <bdi><Permalink className='notification__display-name' href={targetAccount.get('url')} title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>; + + return ( + <HotKeys handlers={this.getHandlers()}> + <div className={classNames('notification notification-admin-report focusable', { unread })} tabIndex='0'> + <div className='notification__message'> + <div className='notification__favourite-icon-wrapper'> + <Icon id='flag' fixedWidth /> + </div> + + <span title={notification.get('created_at')}> + <FormattedMessage id='notification.admin.report' defaultMessage='{name} reported {target}' values={{ name: link, target: targetLink }} /> + </span> + </div> + + <Report account={account} report={notification.get('report')} hidden={this.props.hidden} /> + <NotificationOverlayContainer notification={notification} /> + </div> + </HotKeys> + ); + } + +} 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 0be2a7e13..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 { </div> </div> - {isStaff && ( + {(this.context.identity.permissions & PERMISSION_MANAGE_USERS === PERMISSION_MANAGE_USERS) && ( <div role='group' aria-labelledby='notifications-admin-sign-up'> <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span> @@ -179,6 +183,19 @@ export default class ColumnSettings extends React.PureComponent { </div> </div> )} + + {(this.context.identity.permissions & PERMISSION_MANAGE_REPORTS === PERMISSION_MANAGE_REPORTS) && ( + <div role='group' aria-labelledby='notifications-admin-report'> + <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span> + + <div className='column-settings__pillbar'> + <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.report']} onChange={onChange} label={alertStr} /> + {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'admin.report']} onChange={this.onPushChange} label={pushStr} />} + <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.report']} onChange={onChange} label={showStr} /> + <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.report']} onChange={onChange} label={soundStr} /> + </div> + </div> + )} </div> ); } diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js index e0cd3c7a6..d676a4207 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.js +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js @@ -9,6 +9,7 @@ import StatusContainer from 'flavours/glitch/containers/status_container'; import NotificationFollow from './follow'; import NotificationFollowRequestContainer from '../containers/follow_request_container'; import NotificationAdminSignup from './admin_signup'; +import NotificationAdminReportContainer from '../containers/admin_report_container'; export default class Notification extends ImmutablePureComponent { @@ -77,6 +78,19 @@ export default class Notification extends ImmutablePureComponent { unread={this.props.unread} /> ); + case 'admin.report': + return ( + <NotificationAdminReportContainer + hidden={hidden} + id={notification.get('id')} + account={notification.get('account')} + notification={notification} + onMoveDown={onMoveDown} + onMoveUp={onMoveUp} + onMention={onMention} + unread={this.props.unread} + /> + ); case 'mention': return ( <StatusContainer diff --git a/app/javascript/flavours/glitch/features/notifications/components/report.js b/app/javascript/flavours/glitch/features/notifications/components/report.js new file mode 100644 index 000000000..46a307250 --- /dev/null +++ b/app/javascript/flavours/glitch/features/notifications/components/report.js @@ -0,0 +1,62 @@ +import React, { Fragment } from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import AvatarOverlay from 'flavours/glitch/components/avatar_overlay'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; + +const messages = defineMessages({ + openReport: { id: 'report_notification.open', defaultMessage: 'Open report' }, + other: { id: 'report_notification.categories.other', defaultMessage: 'Other' }, + spam: { id: 'report_notification.categories.spam', defaultMessage: 'Spam' }, + violation: { id: 'report_notification.categories.violation', defaultMessage: 'Rule violation' }, +}); + +export default @injectIntl +class Report extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + report: ImmutablePropTypes.map.isRequired, + hidden: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + render () { + const { intl, hidden, report, account } = this.props; + + if (!report) { + return null; + } + + if (hidden) { + return ( + <Fragment> + {report.get('id')} + </Fragment> + ); + } + + return ( + <div className='notification__report'> + <div className='notification__report__avatar'> + <AvatarOverlay account={report.get('target_account')} friend={account} /> + </div> + + <div className='notification__report__details'> + <div> + <RelativeTimestamp timestamp={report.get('created_at')} short={false} /> · <FormattedMessage id='report_notification.attached_statuses' defaultMessage='{count, plural, one {{count} post} other {{count} posts}} attached' values={{ count: report.get('status_ids').size }} /> + <br /> + <strong>{intl.formatMessage(messages[report.get('category')])}</strong> + </div> + + <div className='notification__report__actions'> + <a href={`/admin/reports/${report.get('id')}`} className='button' target='_blank' rel='noopener noreferrer'>{intl.formatMessage(messages.openReport)}</a> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/notifications/containers/admin_report_container.js b/app/javascript/flavours/glitch/features/notifications/containers/admin_report_container.js new file mode 100644 index 000000000..4198afce8 --- /dev/null +++ b/app/javascript/flavours/glitch/features/notifications/containers/admin_report_container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { makeGetReport } from 'flavours/glitch/selectors'; +import AdminReport from '../components/admin_report'; + +const mapStateToProps = (state, { notification }) => { + const getReport = makeGetReport(); + + return { + report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null, + }; +}; + +export default connect(mapStateToProps)(AdminReport); 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/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index f4e6c24c5..301a2add6 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -122,14 +122,27 @@ class DetailedStatus extends ImmutablePureComponent { return null; } - let media = []; - let mediaIcons = []; let applicationLink = ''; let reblogLink = ''; let reblogIcon = 'retweet'; let favouriteLink = ''; let edited = ''; + // 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 (this.props.measureHeight) { outerStyle.height = `${this.state.height}px`; } @@ -199,8 +212,8 @@ class DetailedStatus extends ImmutablePureComponent { } if (status.get('poll')) { - media.push(<PollContainer pollId={status.get('poll')} />); - mediaIcons.push('tasks'); + contentMedia.push(<PollContainer pollId={status.get('poll')} />); + contentMediaIcons.push('tasks'); } if (status.get('application')) { @@ -282,8 +295,9 @@ class DetailedStatus extends ImmutablePureComponent { <StatusContent status={status} - media={media} - mediaIcons={mediaIcons} + media={contentMedia} + extraMedia={extraMedia} + mediaIcons={contentMediaIcons} expanded={expanded} collapsed={false} onExpandedToggle={onToggleHidden} diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 653fabeae..9c86d54db 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -26,7 +26,14 @@ import { 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'; @@ -215,11 +222,19 @@ class Status extends ImmutablePureComponent { 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 }); @@ -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 = <div>{this.renderChildren(ancestorsIds)}</div>; } @@ -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/features/ui/components/image_loader.js b/app/javascript/flavours/glitch/features/ui/components/image_loader.js index c6f16a792..dfa0efe49 100644 --- a/app/javascript/flavours/glitch/features/ui/components/image_loader.js +++ b/app/javascript/flavours/glitch/features/ui/components/image_loader.js @@ -1,10 +1,10 @@ -import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { PureComponent } from 'react'; import { LoadingBar } from 'react-redux-loading-bar'; import ZoomableImage from './zoomable_image'; -export default class ImageLoader extends React.PureComponent { +export default class ImageLoader extends PureComponent { static propTypes = { alt: PropTypes.string, @@ -43,7 +43,7 @@ export default class ImageLoader extends React.PureComponent { this.loadImage(this.props); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (this.props.src !== nextProps.src) { this.loadImage(nextProps); } @@ -139,14 +139,18 @@ export default class ImageLoader extends React.PureComponent { return ( <div className={className}> - <LoadingBar loading={loading ? 1 : 0} className='loading-bar' style={{ width: this.state.width || width }} /> {loading ? ( - <canvas - className='image-loader__preview-canvas' - ref={this.setCanvasRef} - width={width} - height={height} - /> + <> + <div className='loading-bar__container' style={{ width: this.state.width || width }}> + <LoadingBar className='loading-bar' loading={1} /> + </div> + <canvas + className='image-loader__preview-canvas' + ref={this.setCanvasRef} + width={width} + height={height} + /> + </> ) : ( <ZoomableImage alt={alt} 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 5d566e516..040e967f2 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 ( <div className='getting-started__footer'> <ul> - {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} + {((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} {!!securityLink && <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>} {!limitedFederationMode && <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>} <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> diff --git a/app/javascript/flavours/glitch/packs/settings.js b/app/javascript/flavours/glitch/packs/settings.js index 0a53e1c25..de88d4f52 100644 --- a/app/javascript/flavours/glitch/packs/settings.js +++ b/app/javascript/flavours/glitch/packs/settings.js @@ -2,6 +2,7 @@ import 'packs/public-path'; import loadPolyfills from 'flavours/glitch/util/load_polyfills'; import ready from 'flavours/glitch/util/ready'; import loadKeyboardExtensions from 'flavours/glitch/util/load_keyboard_extensions'; +import 'cocoon-js-vanilla'; function main() { const { delegate } = require('@rails/ujs'); 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/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index d4cdc124f..62ce29f0c 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -25,7 +25,9 @@ const initialState = ImmutableMap({ tag_misleading_links: true, rewrite_mentions: 'no', content_warnings : ImmutableMap({ - filter : null, + filter : null, + media_outside: false, + shared_state : false, }), collapsed : ImmutableMap({ enabled : true, 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/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index 859a8b3a1..f538af7fa 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -31,7 +31,7 @@ import { } from 'flavours/glitch/actions/markers'; import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks'; import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from 'flavours/glitch/util/compare_id'; const initialState = ImmutableMap({ @@ -58,6 +58,7 @@ const notificationToMap = (state, notification) => ImmutableMap({ account: notification.account.id, markedForDelete: state.get('markNewForDelete'), status: notification.status ? notification.status.id : null, + report: notification.report ? fromJS(notification.report) : null, }); const normalizeNotification = (state, notification, usePendingItems) => { diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js index 0c28b2959..1d99441a1 100644 --- a/app/javascript/flavours/glitch/reducers/settings.js +++ b/app/javascript/flavours/glitch/reducers/settings.js @@ -43,6 +43,7 @@ const initialState = ImmutableMap({ status: false, update: false, 'admin.sign_up': false, + 'admin.report': false, }), quickFilter: ImmutableMap({ @@ -64,6 +65,7 @@ const initialState = ImmutableMap({ status: true, update: true, 'admin.sign_up': true, + 'admin.report': true, }), sounds: ImmutableMap({ @@ -76,6 +78,7 @@ const initialState = ImmutableMap({ status: true, update: true, 'admin.sign_up': true, + 'admin.report': 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/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index 99afe5355..d9aa8f140 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -171,14 +171,15 @@ export const getAlerts = createSelector([getAlertsBase], (base) => { return arr; }); -export const makeGetNotification = () => { - return createSelector([ - (_, base) => base, - (state, _, accountId) => state.getIn(['accounts', accountId]), - ], (base, account) => { - return base.set('account', account); - }); -}; +export const makeGetNotification = () => createSelector([ + (_, base) => base, + (state, _, accountId) => state.getIn(['accounts', accountId]), +], (base, account) => base.set('account', account)); + +export const makeGetReport = () => createSelector([ + (_, base) => base, + (state, _, targetAccountId) => state.getIn(['accounts', targetAccountId]), +], (base, targetAccount) => base.set('target_account', targetAccount)); export const getAccountGallery = createSelector([ (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()), diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index 87e35236c..ac1be6ad8 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -211,9 +211,9 @@ font-size: 12px; line-height: 12px; font-weight: 500; - color: $ui-secondary-color; - background-color: rgba($ui-secondary-color, 0.1); - border: 1px solid rgba($ui-secondary-color, 0.5); + color: var(--user-role-accent, $ui-secondary-color); + background-color: var(--user-role-background, rgba($ui-secondary-color, 0.1)); + border: 1px solid var(--user-role-border, rgba($ui-secondary-color, 0.5)); &.moderator { color: $success-green; diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 9553aa4ae..77890c467 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -75,6 +75,13 @@ $content-width: 840px; height: 100px; } + .logo--wordmark { + display: inherit; + margin: inherit; + width: inherit; + height: 20px; + } + @media screen and (max-width: $no-columns-breakpoint) { & > a:first-child { display: none; @@ -927,7 +934,8 @@ a.name-tag, text-align: center; } -.applications-list__item { +.applications-list__item, +.filters-list__item { padding: 15px 0; background: $ui-base-color; border: 1px solid lighten($ui-base-color, 4%); @@ -935,7 +943,12 @@ a.name-tag, margin-top: 15px; } -.announcements-list { +.user-role { + color: var(--user-role-accent); +} + +.announcements-list, +.filters-list { border: 1px solid lighten($ui-base-color, 4%); border-radius: 4px; @@ -970,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 { @@ -988,6 +1012,33 @@ a.name-tag, } } +.filters-list__item { + &__title { + display: flex; + justify-content: space-between; + margin-bottom: 0; + } + + &__permissions { + margin-top: 0; + margin-bottom: 10px; + } + + .expiration { + font-size: 13px; + } + + &.expired { + .expiration { + color: lighten($error-red, 12%); + } + + .permissions-list__item__icon { + color: $dark-text-color; + } + } +} + .dashboard__counters.admin-account-counters { margin-top: 10px; } diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 377cdd91f..4e912b18b 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -105,6 +105,8 @@ position: relative; @include avatar-size(48px); + position: relative; + &-base { @include avatar-radius(); @include avatar-size(36px); @@ -243,6 +245,33 @@ margin-right: 10px; } +.notification__report { + padding: 8px 10px; + padding-left: 68px; + position: relative; + border-bottom: 1px solid lighten($ui-base-color, 8%); + min-height: 54px; + + &__details { + display: flex; + justify-content: space-between; + align-items: center; + color: $darker-text-color; + font-size: 15px; + line-height: 22px; + + strong { + font-weight: 500; + } + } + + &__avatar { + position: absolute; + left: 10px; + top: 10px; + } +} + .notification__message { margin-left: 42px; padding: 8px 0 0 26px; diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 7f9ed2186..b54c3f696 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -442,10 +442,14 @@ object-fit: contain; } - .loading-bar { + .loading-bar__container { position: relative; } + .loading-bar { + position: absolute; + } + &.image-loader--amorphous .image-loader__preview-canvas { display: none; } 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 { diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index d10fc1d3e..3843bcd68 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -154,6 +154,16 @@ padding-top: 15px; } + .notification__report { + padding: 15px 15px 15px (48px + 15px * 2); + min-height: 48px + 2px; + + &__avatar { + left: 15px; + top: 17px; + } + } + .status { padding: 15px; min-height: 48px + 2px; diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index de8bd2d45..9ed656e13 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -241,6 +241,10 @@ code { } } + .input.with_block_label.user_role_permissions_as_keys ul { + columns: unset; + } + .input.datetime .label_input select { display: inline-block; width: auto; @@ -1059,3 +1063,34 @@ code { } } } + +.keywords-table { + thead { + th { + white-space: nowrap; + } + + th:first-child { + width: 100%; + } + } + + tfoot { + td { + border: 0; + } + } + + .input.string { + margin-bottom: 0; + } + + .label_input__wrapper { + margin-top: 10px; + } + + .table-action-link { + margin-top: 10px; + white-space: nowrap; + } +} 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'])); } 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'); |