diff options
Diffstat (limited to 'app/javascript/flavours')
54 files changed, 1059 insertions, 192 deletions
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 0cf64e076..f5871beb3 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -88,6 +88,8 @@ export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET'; +export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL'; + export function fetchAccount(id) { return (dispatch, getState) => { dispatch(fetchRelationships([id])); @@ -798,6 +800,11 @@ export function unpinAccountFail(error) { }; }; +export const revealAccount = id => ({ + type: ACCOUNT_REVEAL, + id, +}); + export function fetchPinnedAccounts() { return (dispatch, getState) => { dispatch(fetchPinnedAccountsRequest()); diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index baa98e98f..ab74fb303 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -48,12 +48,13 @@ export const COMPOSE_MOUNT = 'COMPOSE_MOUNT'; export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE'; -export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; -export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; +export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; +export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; -export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; -export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; +export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; +export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE'; +export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE'; export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; @@ -189,6 +190,7 @@ export function submitCompose(routerHistory) { spoiler_text: spoilerText, visibility: getState().getIn(['compose', 'privacy']), poll: getState().getIn(['compose', 'poll'], null), + language: getState().getIn(['compose', 'language']), }, headers: { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), @@ -675,6 +677,11 @@ export function changeComposeSensitivity() { }; }; +export const changeComposeLanguage = language => ({ + type: COMPOSE_LANGUAGE_CHANGE, + language, +}); + export function changeComposeSpoilerness() { return { type: COMPOSE_SPOILERNESS_CHANGE, diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index bda15a9b0..c38af196a 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -1,7 +1,6 @@ import escapeTextContentForBrowser from 'escape-html'; import emojify from 'flavours/glitch/util/emoji'; import { unescapeHTML } from 'flavours/glitch/util/html'; -import { expandSpoilers } from 'flavours/glitch/util/initial_state'; const domParser = new DOMParser(); diff --git a/app/javascript/flavours/glitch/actions/languages.js b/app/javascript/flavours/glitch/actions/languages.js new file mode 100644 index 000000000..ad186ba0c --- /dev/null +++ b/app/javascript/flavours/glitch/actions/languages.js @@ -0,0 +1,12 @@ +import { saveSettings } from './settings'; + +export const LANGUAGE_USE = 'LANGUAGE_USE'; + +export const useLanguage = language => dispatch => { + dispatch({ + type: LANGUAGE_USE, + language, + }); + + dispatch(saveSettings()); +}; diff --git a/app/javascript/flavours/glitch/actions/local_settings.js b/app/javascript/flavours/glitch/actions/local_settings.js index 28660a4e8..856674eb3 100644 --- a/app/javascript/flavours/glitch/actions/local_settings.js +++ b/app/javascript/flavours/glitch/actions/local_settings.js @@ -1,4 +1,46 @@ +import { expandSpoilers, disableSwiping } from 'flavours/glitch/util/initial_state'; +import { openModal } from './modal'; + export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; +export const LOCAL_SETTING_DELETE = 'LOCAL_SETTING_DELETE'; + +export function checkDeprecatedLocalSettings() { + return (dispatch, getState) => { + const local_auto_unfold = getState().getIn(['local_settings', 'content_warnings', 'auto_unfold']); + const local_swipe_to_change_columns = getState().getIn(['local_settings', 'swipe_to_change_columns']); + let changed_settings = []; + + if (local_auto_unfold !== null && local_auto_unfold !== undefined) { + if (local_auto_unfold === expandSpoilers) { + dispatch(deleteLocalSetting(['content_warnings', 'auto_unfold'])); + } else { + changed_settings.push('user_setting_expand_spoilers'); + } + } + + if (local_swipe_to_change_columns !== null && local_swipe_to_change_columns !== undefined) { + if (local_swipe_to_change_columns === !disableSwiping) { + dispatch(deleteLocalSetting(['swipe_to_change_columns'])); + } else { + changed_settings.push('user_setting_disable_swiping'); + } + } + + if (changed_settings.length > 0) { + dispatch(openModal('DEPRECATED_SETTINGS', { + settings: changed_settings, + onConfirm: () => dispatch(clearDeprecatedLocalSettings()), + })); + } + }; +}; + +export function clearDeprecatedLocalSettings() { + return (dispatch) => { + dispatch(deleteLocalSetting(['content_warnings', 'auto_unfold'])); + dispatch(deleteLocalSetting(['swipe_to_change_columns'])); + }; +}; export function changeLocalSetting(key, value) { return dispatch => { @@ -12,6 +54,17 @@ export function changeLocalSetting(key, value) { }; }; +export function deleteLocalSetting(key) { + return dispatch => { + dispatch({ + type: LOCAL_SETTING_DELETE, + key, + }); + + dispatch(saveLocalSettings()); + }; +}; + // __TODO :__ // Right now `saveLocalSettings()` doesn't keep track of which user // is currently signed in, but it might be better to give each user diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 42ad39efa..85938867b 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -70,7 +70,8 @@ export const loadPending = () => ({ export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { - const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); + const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); + const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type; const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); const filters = getFiltersRegex(getState(), { contextType: 'notifications' }); diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js index 396a36ea0..489f60736 100644 --- a/app/javascript/flavours/glitch/components/account.js +++ b/app/javascript/flavours/glitch/components/account.js @@ -16,8 +16,10 @@ const messages = defineMessages({ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'You are not currently muting notifications from @{name}. Click to mute notifications' }, - unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' }, + mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, + unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, }); export default @injectIntl @@ -34,6 +36,7 @@ class Account extends ImmutablePureComponent { small: PropTypes.bool, actionIcon: PropTypes.string, actionTitle: PropTypes.string, + defaultAction: PropTypes.string, onActionClick: PropTypes.func, }; @@ -70,6 +73,7 @@ class Account extends ImmutablePureComponent { onActionClick, actionIcon, actionTitle, + defaultAction, } = this.props; if (!account) { @@ -114,6 +118,10 @@ class Account extends ImmutablePureComponent { {hidingNotificationsButton} </Fragment> ); + } else if (defaultAction === 'mute') { + buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />; + } else if (defaultAction === 'block') { + buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get('username') })} onClick={this.handleBlock} />; } else if (!account.get('moved') || following) { buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; } diff --git a/app/javascript/flavours/glitch/components/avatar.js b/app/javascript/flavours/glitch/components/avatar.js index c5e9072c4..6d53a5298 100644 --- a/app/javascript/flavours/glitch/components/avatar.js +++ b/app/javascript/flavours/glitch/components/avatar.js @@ -1,13 +1,13 @@ -import classNames from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import classNames from 'classnames'; export default class Avatar extends React.PureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.map, className: PropTypes.string, size: PropTypes.number.isRequired, style: PropTypes.object, @@ -45,11 +45,6 @@ export default class Avatar extends React.PureComponent { } = this.props; const { hovering } = this.state; - const src = account.get('avatar'); - const staticSrc = account.get('avatar_static'); - - const computedClass = classNames('account__avatar', { 'account__avatar-inline': inline }, className); - const style = { ...this.props.style, width: `${size}px`, @@ -57,19 +52,24 @@ export default class Avatar extends React.PureComponent { backgroundSize: `${size}px ${size}px`, }; - if (hovering || animate) { - style.backgroundImage = `url(${src})`; - } else { - style.backgroundImage = `url(${staticSrc})`; + if (account) { + const src = account.get('avatar'); + const staticSrc = account.get('avatar_static'); + + if (hovering || animate) { + style.backgroundImage = `url(${src})`; + } else { + style.backgroundImage = `url(${staticSrc})`; + } } return ( <div - className={computedClass} + className={classNames('account__avatar', { 'account__avatar-inline': inline }, className)} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={style} - data-avatar-of={`@${account.get('acct')}`} + data-avatar-of={account && `@${account.get('acct')}`} /> ); } diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index bd6bc4ffd..e04af8074 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -7,7 +7,7 @@ import Motion from 'flavours/glitch/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; -import { CircularProgress } from 'mastodon/components/loading_indicator'; +import { CircularProgress } from 'flavours/glitch/components/loading_indicator'; const listenerOptions = supportsPassiveEvents ? { passive: true } : false; let id = 0; diff --git a/app/javascript/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.js index 0595f6a0e..056277447 100644 --- a/app/javascript/flavours/glitch/components/modal_root.js +++ b/app/javascript/flavours/glitch/components/modal_root.js @@ -55,6 +55,10 @@ export default class ModalRoot extends React.PureComponent { window.addEventListener('keyup', this.handleKeyUp, false); window.addEventListener('keydown', this.handleKeyDown, false); this.history = this.context.router ? this.context.router.history : createBrowserHistory(); + + if (this.props.children) { + this._handleModalOpen(); + } } componentWillReceiveProps (nextProps) { diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js index de8ea8ee2..989e37024 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.js +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -8,6 +8,7 @@ import UI from 'flavours/glitch/features/ui'; import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; import { hydrateStore } from 'flavours/glitch/actions/store'; import { connectUserStream } from 'flavours/glitch/actions/streaming'; +import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from 'locales'; import initialState from 'flavours/glitch/util/initial_state'; @@ -20,6 +21,9 @@ export const store = configureStore(); const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); +// check for deprecated local settings +store.dispatch(checkDeprecatedLocalSettings()); + // load custom emojis store.dispatch(fetchCustomEmojis()); diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 68c6bae8e..45aba53f7 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -82,6 +82,7 @@ class Header extends ImmutablePureComponent { onEditAccountNote: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, domain: PropTypes.string.isRequired, + hidden: PropTypes.bool, }; openEditProfile = () => { @@ -115,7 +116,7 @@ class Header extends ImmutablePureComponent { } render () { - const { account, intl, domain, identity_proofs } = this.props; + const { account, hidden, intl, domain } = this.props; if (!account) { return null; @@ -270,23 +271,29 @@ class Header extends ImmutablePureComponent { {info} </div> - <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' /> + {!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />} </div> <div className='account__header__bar'> <div className='account__header__tabs'> <a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'> - <Avatar account={account} size={90} /> + <Avatar account={suspended || hidden ? undefined : account} size={90} /> </a> <div className='spacer' /> - <div className='account__header__tabs__buttons'> - {actionBtn} - {bellBtn} + {!suspended && ( + <div className='account__header__tabs__buttons'> + {!hidden && ( + <React.Fragment> + {actionBtn} + {bellBtn} + </React.Fragment> + )} - <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> - </div> + <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> + </div> + )} </div> <div className='account__header__tabs__name'> @@ -298,23 +305,11 @@ class Header extends ImmutablePureComponent { <AccountNoteContainer account={account} /> - {!suspended && ( + {!(suspended || hidden) && ( <div className='account__header__extra'> <div className='account__header__bio'> - { (fields.size > 0 || identity_proofs.size > 0) && ( + { fields.size > 0 && ( <div className='account__header__fields'> - {identity_proofs.map((proof, i) => ( - <dl key={i}> - <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} className='translate' /> - - <dd className='verified'> - <a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> - <Icon id='check' className='verified__mark' /> - </span></a> - <a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} className='translate' /></a> - </dd> - </dl> - ))} {fields.map((pair, i) => ( <dl key={i}> <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index d6e607a37..645ff29ea 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -12,7 +12,6 @@ export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, - identity_proofs: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, @@ -26,6 +25,7 @@ export default class Header extends ImmutablePureComponent { onAddToList: PropTypes.func.isRequired, hideTabs: PropTypes.bool, domain: PropTypes.string.isRequired, + hidden: PropTypes.bool, }; static contextTypes = { @@ -93,7 +93,7 @@ export default class Header extends ImmutablePureComponent { } render () { - const { account, hideTabs, identity_proofs } = this.props; + const { account, hidden, hideTabs } = this.props; if (account === null) { return null; @@ -101,11 +101,10 @@ export default class Header extends ImmutablePureComponent { return ( <div className='account-timeline__header'> - {account.get('moved') && <MovedNote from={account} to={account.get('moved')} />} + {(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />} <InnerHeader account={account} - identity_proofs={identity_proofs} onFollow={this.handleFollow} onBlock={this.handleBlock} onMention={this.handleMention} @@ -120,13 +119,14 @@ export default class Header extends ImmutablePureComponent { onAddToList={this.handleAddToList} onEditAccountNote={this.handleEditAccountNote} domain={this.props.domain} + hidden={hidden} /> <ActionBar account={account} /> - {!hideTabs && ( + {!(hideTabs || hidden) && ( <div className='account__section-headline'> <NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink> <NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts with replies' /></NavLink> diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js new file mode 100644 index 000000000..e465c83b4 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { revealAccount } from 'flavours/glitch/actions/accounts'; +import { FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/components/button'; + +const mapDispatchToProps = (dispatch, { accountId }) => ({ + + reveal () { + dispatch(revealAccount(accountId)); + }, + +}); + +export default @connect(() => {}, mapDispatchToProps) +class LimitedAccountHint extends React.PureComponent { + + static propTypes = { + accountId: PropTypes.string.isRequired, + reveal: PropTypes.func, + } + + render () { + const { reveal } = this.props; + + return ( + <div className='limited-account-hint'> + <p><FormattedMessage id='limited_account_hint.title' defaultMessage='This profile has been hidden by the moderators of your server.' /></p> + <Button onClick={reveal}><FormattedMessage id='limited_account_hint.action' defaultMessage='Show profile anyway' /></Button> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js index 90e746679..3fa7c1448 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { makeGetAccount } from 'flavours/glitch/selectors'; +import { makeGetAccount, getAccountHidden } from 'flavours/glitch/selectors'; import Header from '../components/header'; import { followAccount, @@ -22,7 +22,6 @@ import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_block import { initEditAccountNote } from 'flavours/glitch/actions/account_notes'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { unfollowModal } from 'flavours/glitch/util/initial_state'; -import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, @@ -35,7 +34,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, { accountId }) => ({ account: getAccount(state, accountId), domain: state.getIn(['meta', 'domain']), - identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()), + hidden: getAccountHidden(state, accountId), }); return mapStateToProps; diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 6d2df5c6f..68d558e66 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -13,9 +13,10 @@ import ColumnBackButton from 'flavours/glitch/components/column_back_button'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; -import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import TimelineHint from 'flavours/glitch/components/timeline_hint'; +import LimitedAccountHint from './components/limited_account_hint'; +import { getAccountHidden } from 'flavours/glitch/selectors'; const emptyList = ImmutableList(); @@ -40,6 +41,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) = isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), suspended: state.getIn(['accounts', accountId, 'suspended'], false), + hidden: getAccountHidden(state, accountId), }; }; @@ -68,6 +70,7 @@ class AccountTimeline extends ImmutablePureComponent { withReplies: PropTypes.bool, isAccount: PropTypes.bool, suspended: PropTypes.bool, + hidden: PropTypes.bool, remote: PropTypes.bool, remoteUrl: PropTypes.string, multiColumn: PropTypes.bool, @@ -77,7 +80,7 @@ class AccountTimeline extends ImmutablePureComponent { const { accountId, withReplies, dispatch } = this.props; dispatch(fetchAccount(accountId)); - dispatch(fetchAccountIdentityProofs(accountId)); + if (!withReplies) { dispatch(expandAccountFeaturedTimeline(accountId)); } @@ -109,10 +112,11 @@ class AccountTimeline extends ImmutablePureComponent { if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { dispatch(fetchAccount(nextProps.params.accountId)); - dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); + if (!nextProps.withReplies) { dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); } + dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies })); } } @@ -130,7 +134,7 @@ class AccountTimeline extends ImmutablePureComponent { } render () { - const { statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props; + const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props; if (!isAccount) { return ( @@ -151,8 +155,12 @@ class AccountTimeline extends ImmutablePureComponent { let emptyMessage; + const forceEmptyState = suspended || hidden; + if (suspended) { emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />; + } else if (hidden) { + emptyMessage = <LimitedAccountHint accountId={accountId} />; } else if (remote && statusIds.isEmpty()) { emptyMessage = <RemoteHint url={remoteUrl} />; } else { @@ -166,14 +174,14 @@ class AccountTimeline extends ImmutablePureComponent { <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> <StatusList - prepend={<HeaderContainer accountId={this.props.accountId} />} + prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} />} alwaysPrepend append={remoteMessage} scrollKey='account_timeline' - statusIds={suspended ? emptyList : statusIds} + statusIds={forceEmptyState ? emptyList : statusIds} featuredStatusIds={featuredStatusIds} isLoading={isLoading} - hasMore={hasMore} + hasMore={!forceEmptyState && hasMore} onLoadMore={this.handleLoadMore} emptyMessage={emptyMessage} bindToDocument={!multiColumn} diff --git a/app/javascript/flavours/glitch/features/blocks/index.js b/app/javascript/flavours/glitch/features/blocks/index.js index 4d0f58239..4461bd14d 100644 --- a/app/javascript/flavours/glitch/features/blocks/index.js +++ b/app/javascript/flavours/glitch/features/blocks/index.js @@ -69,7 +69,7 @@ class Blocks extends ImmutablePureComponent { bindToDocument={!multiColumn} > {accountIds.map(id => - <AccountContainer key={id} id={id} />, + <AccountContainer key={id} id={id} defaultAction='block' />, )} </ScrollableList> </Column> diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js new file mode 100644 index 000000000..c8c503e58 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js @@ -0,0 +1,332 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages } from 'react-intl'; +import TextIconButton from './text_icon_button'; +import Overlay from 'react-overlays/lib/Overlay'; +import Motion from 'flavours/glitch/util/optional_motion'; +import spring from 'react-motion/lib/spring'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import classNames from 'classnames'; +import { languages as preloadedLanguages } from 'flavours/glitch/util/initial_state'; +import fuzzysort from 'fuzzysort'; + +const messages = defineMessages({ + changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' }, + search: { id: 'compose.language.search', defaultMessage: 'Search languages...' }, + clear: { id: 'emoji_button.clear', defaultMessage: 'Clear' }, +}); + +// Copied from emoji-mart for consistency with emoji picker and since +// they don't export the icons in the package +const icons = { + loupe: ( + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'> + <path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' /> + </svg> + ), + + delete: ( + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'> + <path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' /> + </svg> + ), +}; + +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +class LanguageDropdownMenu extends React.PureComponent { + + static propTypes = { + style: PropTypes.object, + value: PropTypes.string.isRequired, + frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired, + placement: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), + intl: PropTypes.object, + }; + + static defaultProps = { + languages: preloadedLanguages, + }; + + state = { + mounted: false, + searchValue: '', + }; + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + } + + componentDidMount () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + this.setState({ mounted: true }); + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + } + + setListRef = c => { + this.listNode = c; + } + + handleSearchChange = ({ target }) => { + this.setState({ searchValue: target.value }); + } + + search () { + const { languages, value, frequentlyUsedLanguages } = this.props; + const { searchValue } = this.state; + + if (searchValue === '') { + return [...languages].sort((a, b) => { + // Push current selection to the top of the list + + if (a[0] === value) { + return -1; + } else if (b[0] === value) { + return 1; + } else { + // Sort according to frequently used languages + + const indexOfA = frequentlyUsedLanguages.indexOf(a[0]); + const indexOfB = frequentlyUsedLanguages.indexOf(b[0]); + + return ((indexOfA > -1 ? indexOfA : Infinity) - (indexOfB > -1 ? indexOfB : Infinity)); + } + }); + } + + return fuzzysort.go(searchValue, languages, { + keys: ['0', '1', '2'], + limit: 5, + threshold: -10000, + }).map(result => result.obj); + } + + frequentlyUsed () { + const { languages, value } = this.props; + const current = languages.find(lang => lang[0] === value); + const results = []; + + if (current) { + results.push(current); + } + + return results; + } + + handleClick = e => { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + this.props.onClose(); + this.props.onChange(value); + } + + handleKeyDown = e => { + const { onClose } = this.props; + const index = Array.from(this.listNode.childNodes).findIndex(node => node === e.currentTarget); + + let element = null; + + switch(e.key) { + case 'Escape': + onClose(); + break; + case 'Enter': + this.handleClick(e); + break; + case 'ArrowDown': + element = this.listNode.childNodes[index + 1] || this.listNode.firstChild; + break; + case 'ArrowUp': + element = this.listNode.childNodes[index - 1] || this.listNode.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = this.listNode.childNodes[index - 1] || this.listNode.lastChild; + } else { + element = this.listNode.childNodes[index + 1] || this.listNode.firstChild; + } + break; + case 'Home': + element = this.listNode.firstChild; + break; + case 'End': + element = this.listNode.lastChild; + break; + } + + if (element) { + element.focus(); + e.preventDefault(); + e.stopPropagation(); + } + } + + handleSearchKeyDown = e => { + const { onChange, onClose } = this.props; + const { searchValue } = this.state; + + let element = null; + + switch(e.key) { + case 'Tab': + case 'ArrowDown': + element = this.listNode.firstChild; + + if (element) { + element.focus(); + e.preventDefault(); + e.stopPropagation(); + } + + break; + case 'Enter': + element = this.listNode.firstChild; + + if (element) { + onChange(element.getAttribute('data-index')); + onClose(); + } + break; + case 'Escape': + if (searchValue !== '') { + e.preventDefault(); + this.handleClear(); + } + + break; + } + } + + handleClear = () => { + this.setState({ searchValue: '' }); + } + + renderItem = lang => { + const { value } = this.props; + + return ( + <div key={lang[0]} role='option' tabIndex='0' data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}> + <span className='language-dropdown__dropdown__results__item__native-name'>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span> + </div> + ); + } + + render () { + const { style, placement, intl } = this.props; + const { mounted, searchValue } = this.state; + const isSearching = searchValue !== ''; + const results = this.search(); + + return ( + <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> + {({ opacity, scaleX, scaleY }) => ( + // It should not be transformed when mounting because the resulting + // size will be used to determine the coordinate of the menu by + // react-overlays + <div className={`language-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> + <div className='emoji-mart-search'> + <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} autoFocus /> + <button className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? icons.loupe : icons.delete}</button> + </div> + + <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}> + {results.map(this.renderItem)} + </div> + </div> + )} + </Motion> + ); + } + +} + +export default @injectIntl +class LanguageDropdown extends React.PureComponent { + + static propTypes = { + value: PropTypes.string, + frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string), + intl: PropTypes.object.isRequired, + onChange: PropTypes.func, + onClose: PropTypes.func, + }; + + state = { + open: false, + placement: 'bottom', + }; + + handleToggle = ({ target }) => { + const { top } = target.getBoundingClientRect(); + + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + } + + this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); + this.setState({ open: !this.state.open }); + } + + handleClose = () => { + const { value, onClose } = this.props; + + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + } + + this.setState({ open: false }); + onClose(value); + } + + handleChange = value => { + const { onChange } = this.props; + onChange(value); + } + + render () { + const { value, intl, frequentlyUsedLanguages } = this.props; + const { open, placement } = this.state; + + return ( + <div className={classNames('privacy-dropdown', { active: open })}> + <div className='privacy-dropdown__value'> + <TextIconButton + className='privacy-dropdown__value-icon' + label={value && value.toUpperCase()} + title={intl.formatMessage(messages.changeLanguage)} + active={open} + onClick={this.handleToggle} + /> + </div> + + <Overlay show={open} placement={placement} target={this}> + <LanguageDropdownMenu + value={value} + frequentlyUsedLanguages={frequentlyUsedLanguages} + onClose={this.handleClose} + onChange={this.handleChange} + placement={placement} + intl={intl} + /> + </Overlay> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index 3a31e214d..f005dbdd1 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -12,6 +12,7 @@ import IconButton from 'flavours/glitch/components/icon_button'; import TextIconButton from './text_icon_button'; import Dropdown from './dropdown'; import PrivacyDropdown from './privacy_dropdown'; +import LanguageDropdown from '../containers/language_dropdown_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; // Utils. @@ -306,6 +307,7 @@ class ComposerOptions extends ImmutablePureComponent { title={formatMessage(messages.spoiler)} /> )} + <LanguageDropdown /> <Dropdown active={advancedOptions && advancedOptions.some(value => !!value)} disabled={disabled || isEditing} diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.js b/app/javascript/flavours/glitch/features/compose/components/search_results.js index d92a6bf6b..e82ee2ca2 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search_results.js +++ b/app/javascript/flavours/glitch/features/compose/components/search_results.js @@ -48,6 +48,9 @@ class SearchResults extends ImmutablePureComponent { render () { const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props; + let accounts, statuses, hashtags; + let count = 0; + if (searchTerm === '' && !suggestions.isEmpty()) { return ( <div className='drawer--results'> @@ -81,9 +84,6 @@ class SearchResults extends ImmutablePureComponent { ); } - let accounts, statuses, hashtags; - let count = 0; - if (results.get('accounts') && results.get('accounts').size > 0) { count += results.get('accounts').size; accounts = ( diff --git a/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js index 7f2005060..a35bd4ff5 100644 --- a/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js +++ b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js @@ -17,11 +17,6 @@ export default class TextIconButton extends React.PureComponent { ariaControls: PropTypes.string, }; - handleClick = (e) => { - e.preventDefault(); - this.props.onClick(); - } - render () { const { label, title, active, ariaControls } = this.props; @@ -31,7 +26,7 @@ export default class TextIconButton extends React.PureComponent { aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} - onClick={this.handleClick} + onClick={this.props.onClick} aria-controls={ariaControls} style={iconStyle} > diff --git a/app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js new file mode 100644 index 000000000..828d08cf5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js @@ -0,0 +1,34 @@ +import { connect } from 'react-redux'; +import LanguageDropdown from '../components/language_dropdown'; +import { changeComposeLanguage } from 'flavours/glitch/actions/compose'; +import { useLanguage } from 'flavours/glitch/actions/languages'; +import { createSelector } from 'reselect'; +import { Map as ImmutableMap } from 'immutable'; + +const getFrequentlyUsedLanguages = createSelector([ + state => state.getIn(['settings', 'frequentlyUsedLanguages'], ImmutableMap()), +], languageCounters => ( + languageCounters.keySeq() + .sort((a, b) => languageCounters.get(a) - languageCounters.get(b)) + .reverse() + .toArray() +)); + +const mapStateToProps = state => ({ + frequentlyUsedLanguages: getFrequentlyUsedLanguages(state), + value: state.getIn(['compose', 'language']), +}); + +const mapDispatchToProps = dispatch => ({ + + onChange (value) { + dispatch(changeComposeLanguage(value)); + }, + + onClose (value) { + dispatch(useLanguage(value)); + }, + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown); diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js index 978436dcc..27a63b3fd 100644 --- a/app/javascript/flavours/glitch/features/followers/index.js +++ b/app/javascript/flavours/glitch/features/followers/index.js @@ -19,6 +19,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import TimelineHint from 'flavours/glitch/components/timeline_hint'; +import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; +import { getAccountHidden } from 'flavours/glitch/selectors'; const mapStateToProps = (state, { params: { acct, id } }) => { const accountId = id || state.getIn(['accounts_map', acct]); @@ -37,6 +39,8 @@ const mapStateToProps = (state, { params: { acct, id } }) => { accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']), hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']), isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true), + suspended: state.getIn(['accounts', accountId, 'suspended'], false), + hidden: getAccountHidden(state, accountId), }; }; @@ -62,6 +66,8 @@ class Followers extends ImmutablePureComponent { hasMore: PropTypes.bool, isLoading: PropTypes.bool, isAccount: PropTypes.bool, + suspended: PropTypes.bool, + hidden: PropTypes.bool, remote: PropTypes.bool, remoteUrl: PropTypes.string, multiColumn: PropTypes.bool, @@ -107,7 +113,7 @@ class Followers extends ImmutablePureComponent { } render () { - const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props; + const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props; if (!isAccount) { return ( @@ -127,7 +133,13 @@ class Followers extends ImmutablePureComponent { let emptyMessage; - if (remote && accountIds.isEmpty()) { + const forceEmptyState = suspended || hidden; + + if (suspended) { + emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />; + } else if (hidden) { + emptyMessage = <LimitedAccountHint accountId={accountId} />; + } else if (remote && accountIds.isEmpty()) { emptyMessage = <RemoteHint url={remoteUrl} />; } else { emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />; @@ -141,7 +153,7 @@ class Followers extends ImmutablePureComponent { <ScrollableList scrollKey='followers' - hasMore={hasMore} + hasMore={!forceEmptyState && hasMore} isLoading={isLoading} onLoadMore={this.handleLoadMore} prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />} diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js index 446a19894..aa187bf95 100644 --- a/app/javascript/flavours/glitch/features/following/index.js +++ b/app/javascript/flavours/glitch/features/following/index.js @@ -19,6 +19,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import TimelineHint from 'flavours/glitch/components/timeline_hint'; +import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; +import { getAccountHidden } from 'flavours/glitch/selectors'; const mapStateToProps = (state, { params: { acct, id } }) => { const accountId = id || state.getIn(['accounts_map', acct]); @@ -37,6 +39,8 @@ const mapStateToProps = (state, { params: { acct, id } }) => { accountIds: state.getIn(['user_lists', 'following', accountId, 'items']), hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']), isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true), + suspended: state.getIn(['accounts', accountId, 'suspended'], false), + hidden: getAccountHidden(state, accountId), }; }; @@ -62,6 +66,8 @@ class Following extends ImmutablePureComponent { hasMore: PropTypes.bool, isLoading: PropTypes.bool, isAccount: PropTypes.bool, + suspended: PropTypes.bool, + hidden: PropTypes.bool, remote: PropTypes.bool, remoteUrl: PropTypes.string, multiColumn: PropTypes.bool, @@ -107,7 +113,7 @@ class Following extends ImmutablePureComponent { } render () { - const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props; + const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props; if (!isAccount) { return ( @@ -127,7 +133,13 @@ class Following extends ImmutablePureComponent { let emptyMessage; - if (remote && accountIds.isEmpty()) { + const forceEmptyState = suspended || hidden; + + if (suspended) { + emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />; + } else if (hidden) { + emptyMessage = <LimitedAccountHint accountId={accountId} />; + } else if (remote && accountIds.isEmpty()) { emptyMessage = <RemoteHint url={remoteUrl} />; } else { emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />; @@ -141,7 +153,7 @@ class Following extends ImmutablePureComponent { <ScrollableList scrollKey='following' - hasMore={hasMore} + hasMore={!forceEmptyState && hasMore} isLoading={isLoading} onLoadMore={this.handleLoadMore} prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />} diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js index 2f6d2de5c..f7097e2ec 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import IconButton from 'flavours/glitch/components/icon_button'; import Icon from 'flavours/glitch/components/icon'; import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl'; -import { autoPlayGif, reduceMotion } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, reduceMotion, disableSwiping } from 'flavours/glitch/util/initial_state'; import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg'; import { mascot } from 'flavours/glitch/util/initial_state'; import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light'; @@ -430,6 +430,7 @@ class Announcements extends ImmutablePureComponent { removeReaction={this.props.removeReaction} intl={intl} selected={index === idx} + disabled={disableSwiping} /> ))} </ReactSwipeableViews> diff --git a/app/javascript/flavours/glitch/features/local_settings/page/deprecated_item/index.js b/app/javascript/flavours/glitch/features/local_settings/page/deprecated_item/index.js new file mode 100644 index 000000000..362bd97c0 --- /dev/null +++ b/app/javascript/flavours/glitch/features/local_settings/page/deprecated_item/index.js @@ -0,0 +1,83 @@ +// Package imports +import React from 'react'; +import PropTypes from 'prop-types'; + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +export default class LocalSettingsPageItem extends React.PureComponent { + + static propTypes = { + children: PropTypes.node.isRequired, + id: PropTypes.string.isRequired, + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + hint: PropTypes.string, + })), + value: PropTypes.any, + placeholder: PropTypes.string, + }; + + render () { + const { id, options, children, placeholder, value } = this.props; + + if (options && options.length > 0) { + const currentValue = value; + const optionElems = options && options.length > 0 && options.map((opt) => { + let optionId = `${id}--${opt.value}`; + return ( + <label htmlFor={optionId}> + <input + type='radio' + name={id} + id={optionId} + value={opt.value} + checked={currentValue === opt.value} + disabled + /> + {opt.message} + {opt.hint && <span className='hint'>{opt.hint}</span>} + </label> + ); + }); + return ( + <div className='glitch local-settings__page__item radio_buttons'> + <fieldset> + <legend>{children}</legend> + {optionElems} + </fieldset> + </div> + ); + } else if (placeholder) { + return ( + <div className='glitch local-settings__page__item string'> + <label htmlFor={id}> + <p>{children}</p> + <p> + <input + id={id} + type='text' + value={value} + placeholder={placeholder} + disabled + /> + </p> + </label> + </div> + ); + } else return ( + <div className='glitch local-settings__page__item boolean'> + <label htmlFor={id}> + <input + id={id} + type='checkbox' + checked={value} + disabled + /> + {children} + </label> + </div> + ); + } + +} 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 45d10d154..4b86a8f6f 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -5,7 +5,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; // Our imports +import { expandSpoilers, disableSwiping } from 'flavours/glitch/util/initial_state'; +import { preferenceLink } from 'flavours/glitch/util/backend_links'; import LocalSettingsPageItem from './item'; +import DeprecatedLocalSettingsPageItem from './deprecated_item'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * @@ -146,14 +149,28 @@ class LocalSettingsPage extends React.PureComponent { > <FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' /> </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['swipe_to_change_columns']} + <DeprecatedLocalSettingsPageItem id='mastodon-settings--swipe_to_change_columns' - onChange={onChange} + value={!disableSwiping} > <FormattedMessage id='settings.swipe_to_change_columns' defaultMessage='Allow swiping to change columns (Mobile only)' /> - </LocalSettingsPageItem> + <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_disable_swiping')}> + <FormattedMessage + id='settings.shared_settings_link' + defaultMessage='user preferences' + /> + </a> + ) + }} + /> + </span> + </DeprecatedLocalSettingsPageItem> </section> </div> ), @@ -242,21 +259,35 @@ 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> - <LocalSettingsPageItem - settings={settings} - item={['content_warnings', 'auto_unfold']} + <DeprecatedLocalSettingsPageItem id='mastodon-settings--content_warnings-auto_unfold' - onChange={onChange} + value={expandSpoilers} > <FormattedMessage id='settings.enable_content_warnings_auto_unfold' defaultMessage='Automatically unfold content-warnings' /> - </LocalSettingsPageItem> + <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} - dependsOn={[['content_warnings', 'auto_unfold']]} placeholder={intl.formatMessage(messages.regexp)} + disabled={!expandSpoilers} > <FormattedMessage id='settings.content_warnings_filter' defaultMessage='Content warnings to not automatically unfold:' /> </LocalSettingsPageItem> diff --git a/app/javascript/flavours/glitch/features/local_settings/page/item/index.js b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js index 5a68523f6..6b24e4143 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/item/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js @@ -21,6 +21,7 @@ export default class LocalSettingsPageItem extends React.PureComponent { })), settings: ImmutablePropTypes.map.isRequired, placeholder: PropTypes.string, + disabled: PropTypes.bool, }; handleChange = e => { @@ -33,8 +34,8 @@ export default class LocalSettingsPageItem extends React.PureComponent { render () { const { handleChange } = this; - const { settings, item, id, options, children, dependsOn, dependsOnNot, placeholder } = this.props; - let enabled = true; + const { settings, item, id, options, children, dependsOn, dependsOnNot, placeholder, disabled } = this.props; + let enabled = !disabled; if (dependsOn) { for (let i = 0; i < dependsOn.length; i++) { diff --git a/app/javascript/flavours/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js index 9f0d5a43e..764cbef1a 100644 --- a/app/javascript/flavours/glitch/features/mutes/index.js +++ b/app/javascript/flavours/glitch/features/mutes/index.js @@ -69,7 +69,7 @@ class Mutes extends ImmutablePureComponent { bindToDocument={!multiColumn} > {accountIds.map(id => - <AccountContainer key={id} id={id} />, + <AccountContainer key={id} id={id} defaultAction='mute' />, )} </ScrollableList> </Column> diff --git a/app/javascript/flavours/glitch/features/report/thanks.js b/app/javascript/flavours/glitch/features/report/thanks.js index 9c41baa7f..454979f9f 100644 --- a/app/javascript/flavours/glitch/features/report/thanks.js +++ b/app/javascript/flavours/glitch/features/report/thanks.js @@ -8,7 +8,7 @@ import { unfollowAccount, muteAccount, blockAccount, -} from 'mastodon/actions/accounts'; +} from 'flavours/glitch/actions/accounts'; const mapStateToProps = () => ({}); diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index e39a31e5d..bfb1ae405 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -8,6 +8,8 @@ import ReactSwipeableViews from 'react-swipeable-views'; import TabsBar, { links, getIndex, getLink } from './tabs_bar'; import { Link } from 'react-router-dom'; +import { disableSwiping } from 'flavours/glitch/util/initial_state'; + import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; import DrawerLoading from './drawer_loading'; @@ -63,7 +65,6 @@ class ColumnsArea extends ImmutablePureComponent { static propTypes = { intl: PropTypes.object.isRequired, columns: ImmutablePropTypes.list.isRequired, - swipeToChangeColumns: PropTypes.bool, singleColumn: PropTypes.bool, children: PropTypes.node, navbarUnder: PropTypes.bool, @@ -210,7 +211,7 @@ class ColumnsArea extends ImmutablePureComponent { } render () { - const { columns, children, singleColumn, swipeToChangeColumns, intl, navbarUnder, openSettings } = this.props; + const { columns, children, singleColumn, intl, navbarUnder, openSettings } = this.props; const { shouldAnimate, renderComposePanel } = this.state; const columnIndex = getIndex(this.context.router.history.location.pathname); @@ -219,7 +220,7 @@ class ColumnsArea extends ImmutablePureComponent { const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; const content = columnIndex !== -1 ? ( - <ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={!swipeToChangeColumns}> + <ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={disableSwiping}> {links.map(this.renderView)} </ReactSwipeableViews> ) : ( @@ -234,7 +235,7 @@ class ColumnsArea extends ImmutablePureComponent { </div> </div> - <div className='columns-area__panels__main'> + <div className={`columns-area__panels__main ${floatingActionButton && 'with-fab'}`}> {!navbarUnder && <TabsBar key='tabs' />} {content} {navbarUnder && <TabsBar key='tabs' />} diff --git a/app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.js b/app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.js new file mode 100644 index 000000000..9cb5a30b9 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.js @@ -0,0 +1,86 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { preferenceLink } from 'flavours/glitch/util/backend_links'; +import Button from 'flavours/glitch/components/button'; +import Icon from 'flavours/glitch/components/icon'; +import illustration from 'flavours/glitch/images/logo_warn_glitch.svg'; + +const messages = defineMessages({ + discardChanges: { id: 'confirmations.deprecated_settings.confirm', defaultMessage: 'Use Mastodon preferences' }, + user_setting_expand_spoilers: { id: 'settings.enable_content_warnings_auto_unfold', defaultMessage: 'Automatically unfold content-warnings' }, + user_setting_disable_swiping: { id: 'settings.swipe_to_change_columns', defaultMessage: 'Allow swiping to change columns (Mobile only)' }, +}); + +export default @injectIntl +class DeprecatedSettingsModal extends React.PureComponent { + + static propTypes = { + settings: ImmutablePropTypes.list.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + componentDidMount() { + this.button.focus(); + } + + handleClick = () => { + this.props.onConfirm(); + this.props.onClose(); + } + + setRef = (c) => { + this.button = c; + } + + render () { + const { settings, intl } = this.props; + + return ( + <div className='modal-root__modal confirmation-modal'> + <div className='confirmation-modal__container'> + + <img src={illustration} className='modal-warning' alt='' /> + + <FormattedMessage + id='confirmations.deprecated_settings.message' + defaultMessage='Some of the glitch-soc device-specific {app_settings} you are using have been replaced by Mastodon {preferences} and will be overriden:' + values={{ + app_settings: ( + <strong className='deprecated-settings-label'> + <Icon id='cogs' /> <FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /> + </strong> + ), + preferences: ( + <strong className='deprecated-settings-label'> + <Icon id='cog' /> <FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /> + </strong> + ), + }} + /> + + <div className='deprecated-settings-info'> + <ul> + { settings.map((setting_name) => ( + <li> + <a href={preferenceLink(setting_name)}><FormattedMessage {...messages[setting_name]} /></a> + </li> + )) } + </ul> + </div> + </div> + + <div> + <div className='confirmation-modal__action-bar'> + <div /> + <Button text={intl.formatMessage(messages.discardChanges)} onClick={this.handleClick} ref={this.setRef} /> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index 6974aab26..baa5ff275 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -12,6 +12,7 @@ import Icon from 'flavours/glitch/components/icon'; import GIFV from 'flavours/glitch/components/gifv'; import Footer from 'flavours/glitch/features/picture_in_picture/components/footer'; import { getAverageFromBlurhash } from 'flavours/glitch/blurhash'; +import { disableSwiping } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -227,6 +228,7 @@ class MediaModal extends ImmutablePureComponent { onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleTransitionEnd} index={index} + disabled={disableSwiping} > {content} </ReactSwipeableViews> diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js index a975c4013..8f18d93b7 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js @@ -14,6 +14,7 @@ import AudioModal from './audio_modal'; import DoodleModal from './doodle_modal'; import ConfirmationModal from './confirmation_modal'; import FocalPointModal from './focal_point_modal'; +import DeprecatedSettingsModal from './deprecated_settings_modal'; import { OnboardingModal, MuteModal, @@ -40,6 +41,7 @@ const MODAL_COMPONENTS = { 'BLOCK': BlockModal, 'REPORT': ReportModal, 'SETTINGS': SettingsModal, + 'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }), 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'EMBED': EmbedModal, 'LIST_EDITOR': ListEditor, diff --git a/app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js b/app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js index b69842cd6..1107be740 100644 --- a/app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js +++ b/app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js @@ -4,7 +4,6 @@ import { openModal } from 'flavours/glitch/actions/modal'; const mapStateToProps = state => ({ columns: state.getIn(['settings', 'columns']), - swipeToChangeColumns: state.getIn(['local_settings', 'swipe_to_change_columns']), }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/flavours/glitch/images/logo_warn_glitch.svg b/app/javascript/flavours/glitch/images/logo_warn_glitch.svg new file mode 100644 index 000000000..32c5854ee --- /dev/null +++ b/app/javascript/flavours/glitch/images/logo_warn_glitch.svg @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 216.41507 232.00976" + version="1.1" + id="svg6" + sodipodi:docname="logo_warn_glitch.svg" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs10" /> + <sodipodi:namedview + id="namedview8" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="1.7951831" + inkscape:cx="-30.916067" + inkscape:cy="90.241493" + inkscape:window-width="1920" + inkscape:window-height="1011" + inkscape:window-x="0" + inkscape:window-y="32" + inkscape:window-maximized="1" + inkscape:current-layer="svg6" /> + <g + id="g2025"> + <path + d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" + fill="#3088d4" + id="path2" /> + <path + d="m 124.52893,137.75645 c 0,9.01375 -7.30875,16.32125 -16.3225,16.32125 -9.01375,0 -16.32125,-7.3075 -16.32125,-16.32125 0,-9.01375 7.3075,-16.3225 16.32125,-16.3225 9.01375,0 16.3225,7.30875 16.3225,16.3225" + fill="#ffffff" + id="path4" + sodipodi:nodetypes="csssc" /> + <path + id="path1121" + d="m 108.20703,25.453125 c -9.013749,0 -16.322264,7.308516 -16.322264,16.322266 0,5.31808 2.555126,37.386806 6.492187,67.763669 4.100497,4.20028 15.890147,3.77063 19.660157,-0.01 3.9367,-30.375272 6.49219,-62.4364 6.49219,-67.753909 0,-9.01375 -7.30852,-16.322266 -16.32227,-16.322266 z" + style="fill:#ffffff" + sodipodi:nodetypes="ssccsss" /> + </g> +</svg> diff --git a/app/javascript/flavours/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js index 530ed8e60..e02a5592e 100644 --- a/app/javascript/flavours/glitch/reducers/accounts.js +++ b/app/javascript/flavours/glitch/reducers/accounts.js @@ -1,4 +1,5 @@ -import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer'; +import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from 'flavours/glitch/actions/importer'; +import { ACCOUNT_REVEAL } from 'flavours/glitch/actions/accounts'; import { Map as ImmutableMap, fromJS } from 'immutable'; const initialState = ImmutableMap(); @@ -10,6 +11,8 @@ const normalizeAccount = (state, account) => { delete account.following_count; delete account.statuses_count; + account.hidden = state.getIn([account.id, 'hidden']) === false ? false : account.limited; + return state.set(account.id, fromJS(account)); }; @@ -27,6 +30,8 @@ export default function accounts(state = initialState, action) { return normalizeAccount(state, action.account); case ACCOUNTS_IMPORT: return normalizeAccounts(state, action.accounts); + case ACCOUNT_REVEAL: + return state.setIn([action.id, 'hidden'], false); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index f97c799e7..d0aeaa1f0 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -30,6 +30,7 @@ import { COMPOSE_SPOILERNESS_CHANGE, COMPOSE_SPOILER_TEXT_CHANGE, COMPOSE_VISIBILITY_CHANGE, + COMPOSE_LANGUAGE_CHANGE, COMPOSE_CONTENT_TYPE_CHANGE, COMPOSE_EMOJI_INSERT, COMPOSE_UPLOAD_CHANGE_REQUEST, @@ -100,6 +101,7 @@ const initialState = ImmutableMap({ }), default_privacy: 'public', default_sensitive: false, + default_language: 'en', resetFileKey: Math.floor((Math.random() * 0x10000)), idempotencyKey: null, tagHistory: ImmutableList(), @@ -175,7 +177,8 @@ function clearAll(state) { map => map.mergeWith(overwrite, state.get('default_advanced_options')) ); map.set('privacy', state.get('default_privacy')); - map.set('sensitive', false); + map.set('sensitive', state.get('default_sensitive')); + map.set('language', state.get('default_language')); map.update('media_attachments', list => list.clear()); map.set('poll', null); map.set('idempotencyKey', uuid()); @@ -557,6 +560,7 @@ export default function compose(state = initialState, action) { map.set('caretPosition', null); map.set('idempotencyKey', uuid()); map.set('sensitive', action.status.get('sensitive')); + map.set('language', action.status.get('language')); map.update( 'advanced_options', map => map.merge(new ImmutableMap({ do_not_federate })) @@ -589,6 +593,7 @@ export default function compose(state = initialState, action) { map.set('caretPosition', null); map.set('idempotencyKey', uuid()); map.set('sensitive', action.status.get('sensitive')); + map.set('language', action.status.get('language')); if (action.spoiler_text.length > 0) { map.set('spoiler', true); @@ -618,6 +623,8 @@ export default function compose(state = initialState, action) { return state.updateIn(['poll', 'options'], options => options.delete(action.index)); case COMPOSE_POLL_SETTINGS_CHANGE: return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple)); + case COMPOSE_LANGUAGE_CHANGE: + return state.set('language', action.language); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/identity_proofs.js b/app/javascript/flavours/glitch/reducers/identity_proofs.js deleted file mode 100644 index 58af0a5fa..000000000 --- a/app/javascript/flavours/glitch/reducers/identity_proofs.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; -import { - IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, - IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, - IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, -} from '../actions/identity_proofs'; - -const initialState = ImmutableMap(); - -export default function identityProofsReducer(state = initialState, action) { - switch(action.type) { - case IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST: - return state.set('isLoading', true); - case IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL: - return state.set('isLoading', false); - case IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS: - return state.update(identity_proofs => identity_proofs.withMutations(map => { - map.set('isLoading', false); - map.set('loaded', true); - map.set(action.accountId, fromJS(action.identity_proofs)); - })); - default: - return state; - } -}; diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index 92348c0c5..b8aad9fad 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -34,7 +34,6 @@ import conversations from './conversations'; import suggestions from './suggestions'; import pinnedAccountsEditor from './pinned_accounts_editor'; import polls from './polls'; -import identity_proofs from './identity_proofs'; import trends from './trends'; import announcements from './announcements'; import markers from './markers'; @@ -73,7 +72,6 @@ const reducers = { notifications, height_cache, custom_emojis, - identity_proofs, lists, listEditor, listAdder, diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index c115cad6b..a16c337fc 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -3,13 +3,12 @@ import { Map as ImmutableMap } from 'immutable'; // Our imports. import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; -import { LOCAL_SETTING_CHANGE } from 'flavours/glitch/actions/local_settings'; +import { LOCAL_SETTING_CHANGE, LOCAL_SETTING_DELETE } from 'flavours/glitch/actions/local_settings'; const initialState = ImmutableMap({ layout : 'auto', stretch : true, navbar_under : false, - swipe_to_change_columns: true, side_arm : 'none', side_arm_reply_mode : 'keep', show_reply_count : false, @@ -26,7 +25,6 @@ const initialState = ImmutableMap({ tag_misleading_links: true, rewrite_mentions: 'no', content_warnings : ImmutableMap({ - auto_unfold : false, filter : null, }), collapsed : ImmutableMap({ @@ -66,6 +64,8 @@ export default function localSettings(state = initialState, action) { return hydrate(state, action.state.get('local_settings')); case LOCAL_SETTING_CHANGE: return state.setIn(action.key, action.value); + case LOCAL_SETTING_DELETE: + return state.deleteIn(action.key); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js index 676a1ccc1..0c28b2959 100644 --- a/app/javascript/flavours/glitch/reducers/settings.js +++ b/app/javascript/flavours/glitch/reducers/settings.js @@ -3,6 +3,7 @@ import { NOTIFICATIONS_FILTER_SET } from 'flavours/glitch/actions/notifications' import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE, COLUMN_PARAMS_CHANGE } from 'flavours/glitch/actions/columns'; import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; import { EMOJI_USE } from 'flavours/glitch/actions/emojis'; +import { LANGUAGE_USE } from 'flavours/glitch/actions/languages'; import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists'; import { Map as ImmutableMap, fromJS } from 'immutable'; import uuid from 'flavours/glitch/util/uuid'; @@ -134,6 +135,8 @@ const changeColumnParams = (state, uuid, path, value) => { const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false); +const updateFrequentLanguages = (state, language) => state.update('frequentlyUsedLanguages', ImmutableMap(), map => map.update(language, 0, count => count + 1)).set('saved', false); + const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId)); export default function settings(state = initialState, action) { @@ -159,6 +162,8 @@ export default function settings(state = initialState, action) { return changeColumnParams(state, action.uuid, action.path, action.value); case EMOJI_USE: return updateFrequentEmojis(state, action.emoji); + case LANGUAGE_USE: + return updateFrequentLanguages(state, action.language); case SETTING_SAVE: return state.set('saved', true); case LIST_FETCH_FAIL: diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index bb9180d12..99afe5355 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -194,3 +194,11 @@ export const getAccountGallery = createSelector([ return medias; }); + +export const getAccountHidden = createSelector([ + (state, id) => state.getIn(['accounts', id, 'hidden']), + (state, id) => state.getIn(['relationships', id, 'following']) || state.getIn(['relationships', id, 'requested']), + (state, id) => id === me, +], (hidden, followingOrRequested, isSelf) => { + return hidden && !(isSelf || followingOrRequested); +}); diff --git a/app/javascript/flavours/glitch/styles/accessibility.scss b/app/javascript/flavours/glitch/styles/accessibility.scss index cb27497a4..96e20f839 100644 --- a/app/javascript/flavours/glitch/styles/accessibility.scss +++ b/app/javascript/flavours/glitch/styles/accessibility.scss @@ -12,6 +12,21 @@ $emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange' } } +// Display a checkmark on active UI elements otherwise differing only by color +.status__action-bar-button, +.detailed-status__button .icon-button { + position: relative; + + &.active::after { + position: absolute; + content: "\F00C"; + font-size: 50%; + font-family: FontAwesome; + right: -5px; + top: -4px; + } +} + .hicolor-privacy-icons { .status__visibility-icon.fa-globe, .composer--options--dropdown--content--item .fa-globe { diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 96e292d8b..d52ecf02c 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -560,6 +560,15 @@ } } +.limited-account-hint { + p { + color: $secondary-text-color; + font-size: 15px; + font-weight: 500; + margin-bottom: 20px; + } +} + .empty-column-indicator, .error-column, .follow_requests-unlocked_explanation { diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index 3137b2dea..6d45c110c 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -644,3 +644,68 @@ & > .count { color: $warning-red } } } + +.language-dropdown { + &__dropdown { + position: absolute; + background: $simple-background-color; + box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); + border-radius: 4px; + overflow: hidden; + z-index: 2; + + &.top { + transform-origin: 50% 100%; + } + + &.bottom { + transform-origin: 50% 0; + } + + .emoji-mart-search { + padding-right: 10px; + } + + .emoji-mart-search-icon { + right: 10px + 5px; + } + + .emoji-mart-scroll { + padding: 0 10px 10px; + } + + &__results { + &__item { + cursor: pointer; + color: $inverted-text-color; + font-weight: 500; + padding: 10px; + border-radius: 4px; + + &:focus, + &:active, + &:hover { + background: $ui-secondary-color; + } + + &__common-name { + color: $darker-text-color; + } + + &.active { + background: $ui-highlight-color; + color: $primary-text-color; + outline: 0; + + .language-dropdown__dropdown__results__item__common-name { + color: $secondary-text-color; + } + + &:hover { + background: lighten($ui-highlight-color, 4%); + } + } + } + } + } +} diff --git a/app/javascript/flavours/glitch/styles/components/local_settings.scss b/app/javascript/flavours/glitch/styles/components/local_settings.scss index 0b7a74575..db2b9f154 100644 --- a/app/javascript/flavours/glitch/styles/components/local_settings.scss +++ b/app/javascript/flavours/glitch/styles/components/local_settings.scss @@ -98,6 +98,18 @@ .glitch.local-settings__page__item { margin-bottom: 2px; + + .hint a { + color: $lighter-text-color; + font-weight: 500; + text-decoration: underline; + + &:active, + &:focus, + &:hover { + text-decoration: none; + } + } } .glitch.local-settings__page__item.string, @@ -120,3 +132,29 @@ } } } + +.deprecated-settings-label { + white-space: nowrap; +} + +.deprecated-settings-info { + text-align: start; + + ul { + padding: 10px; + margin-left: 12px; + list-style: disc inside; + } + + a { + color: $lighter-text-color; + font-weight: 500; + text-decoration: underline; + + &:active, + &:focus, + &:hover { + text-decoration: none; + } + } +} diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index 61c292b19..90e0da02a 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -1279,3 +1279,10 @@ pointer-events: auto; z-index: 9999; } + +img.modal-warning { + display: block; + margin: auto; + margin-bottom: 15px; + width: 60px; +} diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index db510f1f4..ba43e7f29 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -233,6 +233,10 @@ .columns-area__panels__pane--compositional { display: none; } + + .with-fab .scrollable .item-list:last-child { + padding-bottom: 5.25rem; + } } @media screen and (min-width: 600px + (285px * 1) + (10px * 1)) { diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index ea416a79f..e4105cba5 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -1016,68 +1016,6 @@ code { } } -.connection-prompt { - margin-bottom: 25px; - - .fa-link { - background-color: darken($ui-base-color, 4%); - border-radius: 100%; - font-size: 24px; - padding: 10px; - } - - &__column { - align-items: center; - display: flex; - flex: 1; - flex-direction: column; - flex-shrink: 1; - max-width: 50%; - - &-sep { - align-self: center; - flex-grow: 0; - overflow: visible; - position: relative; - z-index: 1; - } - - p { - word-break: break-word; - } - } - - .account__avatar { - margin-bottom: 20px; - } - - &__connection { - background-color: lighten($ui-base-color, 8%); - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - border-radius: 4px; - padding: 25px 10px; - position: relative; - text-align: center; - - &::after { - background-color: darken($ui-base-color, 4%); - content: ''; - display: block; - height: 100%; - left: 50%; - position: absolute; - top: 0; - width: 1px; - } - } - - &__row { - align-items: flex-start; - display: flex; - flex-direction: row; - } -} - .input.user_confirm_password, .input.user_website { &:not(.field_with_errors) { diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index bb91abdac..d16f23aed 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -165,6 +165,12 @@ } } +.language-dropdown__dropdown__results__item:hover, +.language-dropdown__dropdown__results__item:focus, +.language-dropdown__dropdown__results__item:active { + background-color: $ui-base-color; +} + .dropdown-menu__separator, .dropdown-menu__item.edited-timestamp__history__item, .dropdown-menu__container__header, diff --git a/app/javascript/flavours/glitch/util/backend_links.js b/app/javascript/flavours/glitch/util/backend_links.js index 2e5111a7f..5b2dd8dbf 100644 --- a/app/javascript/flavours/glitch/util/backend_links.js +++ b/app/javascript/flavours/glitch/util/backend_links.js @@ -7,3 +7,12 @@ export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${acc export const filterEditLink = (id) => `/filters/${id}/edit`; export const relationshipsLink = '/relationships'; export const securityLink = '/auth/edit'; +export const preferenceLink = (setting_name) => { + switch (setting_name) { + case 'user_setting_expand_spoilers': + case 'user_setting_disable_swiping': + return `/settings/preferences/appearance#${setting_name}`; + default: + return preferencesLink; + } +}; diff --git a/app/javascript/flavours/glitch/util/content_warning.js b/app/javascript/flavours/glitch/util/content_warning.js index 5e874a49c..baeb97881 100644 --- a/app/javascript/flavours/glitch/util/content_warning.js +++ b/app/javascript/flavours/glitch/util/content_warning.js @@ -1,5 +1,7 @@ +import { expandSpoilers } from 'flavours/glitch/util/initial_state'; + export function autoUnfoldCW (settings, status) { - if (!settings.getIn(['content_warnings', 'auto_unfold'])) { + if (!expandSpoilers) { return false; } diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index 7154e020b..b6eab0c87 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -13,8 +13,8 @@ const getMeta = (prop) => initialState && initialState.meta && initialState.meta export const reduceMotion = getMeta('reduce_motion'); export const autoPlayGif = getMeta('auto_play_gif'); -export const displaySensitiveMedia = getMeta('display_sensitive_media'); export const displayMedia = getMeta('display_media') || (getMeta('display_sensitive_media') ? 'show_all' : 'default'); +export const expandSpoilers = getMeta('expand_spoilers'); export const unfollowModal = getMeta('unfollow_modal'); export const boostModal = getMeta('boost_modal'); export const favouriteModal = getMeta('favourite_modal'); @@ -37,5 +37,7 @@ export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const useSystemEmojiFont = getMeta('system_emoji_font'); export const showTrends = getMeta('trends'); +export const disableSwiping = getMeta('disable_swiping'); +export const languages = initialState && initialState.languages; export default initialState; |