about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features
diff options
context:
space:
mode:
authorStarfall <us@starfall.systems>2022-06-06 13:35:20 -0500
committerStarfall <us@starfall.systems>2022-06-06 13:35:20 -0500
commit78266a002b4735caaef07098ba66419fd71f47c1 (patch)
tree4ba2bc330d5ed4b6a3a926ab9a03a89f56bc8843 /app/javascript/flavours/glitch/features
parente9b2e11520056d0ec822ac0862923d00c6a1289c (diff)
parent434b08e95b1a440bf9ae563b72600d1590106260 (diff)
Merge remote-tracking branch 'glitch/main'
Diffstat (limited to 'app/javascript/flavours/glitch/features')
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js39
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/components/header.js10
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js35
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js5
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js22
-rw-r--r--app/javascript/flavours/glitch/features/blocks/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/language_dropdown.js332
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/options.js2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/search_results.js6
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/text_icon_button.js7
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js34
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js18
-rw-r--r--app/javascript/flavours/glitch/features/following/index.js18
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/components/announcements.js3
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/deprecated_item/index.js83
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/index.js53
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/item/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/mutes/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/report/thanks.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/columns_area.js9
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.js86
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/media_modal.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js1
24 files changed, 704 insertions, 74 deletions
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 => ({