about summary refs log tree commit diff
path: root/app/javascript/flavours
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours')
-rw-r--r--app/javascript/flavours/glitch/actions/statuses.js6
-rw-r--r--app/javascript/flavours/glitch/components/scrollable_list.js6
-rw-r--r--app/javascript/flavours/glitch/components/status.js13
-rw-r--r--app/javascript/flavours/glitch/components/status_action_bar.js13
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js6
-rw-r--r--app/javascript/flavours/glitch/features/account/components/action_bar.js8
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/bookmarked_statuses/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/community_timeline/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/composer/index.js49
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/domain_blocks/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/drawer/index.js33
-rw-r--r--app/javascript/flavours/glitch/features/drawer/search/popout/index.js7
-rw-r--r--app/javascript/flavours/glitch/features/favourited_statuses/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/favourites/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/follow_requests/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/hashtag_timeline/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/navigation/index.js17
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/index.js32
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/item/index.js22
-rw-r--r--app/javascript/flavours/glitch/features/mutes/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/pinned_statuses/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/public_timeline/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/reblogs/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/standalone/community_timeline/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/status/components/action_bar.js13
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js16
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/columns_area.js25
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/tabs_bar.js16
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/local_settings.js5
-rw-r--r--app/javascript/flavours/glitch/styles/components/columns.scss24
-rw-r--r--app/javascript/flavours/glitch/styles/components/drawer.scss3
-rw-r--r--app/javascript/flavours/glitch/styles/containers.scss21
-rw-r--r--app/javascript/flavours/glitch/util/content_warning.js19
-rw-r--r--app/javascript/flavours/glitch/util/emoji/index.js4
-rw-r--r--app/javascript/flavours/glitch/util/initial_state.js2
41 files changed, 327 insertions, 125 deletions
diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index c6d8486f9..fa8845002 100644
--- a/app/javascript/flavours/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -79,7 +79,7 @@ export function redraft(status) {
   };
 };
 
-export function deleteStatus(id, withRedraft = false) {
+export function deleteStatus(id, router, withRedraft = false) {
   return (dispatch, getState) => {
     const status = getState().getIn(['statuses', id]);
 
@@ -91,6 +91,10 @@ export function deleteStatus(id, withRedraft = false) {
 
       if (withRedraft) {
         dispatch(redraft(status));
+
+        if (!getState().getIn(['compose', 'mounted'])) {
+          router.push('/statuses/new');
+        }
       }
     }).catch(error => {
       dispatch(deleteStatusFail(id, error));
diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js
index b96b4dd98..a677cbf5b 100644
--- a/app/javascript/flavours/glitch/components/scrollable_list.js
+++ b/app/javascript/flavours/glitch/components/scrollable_list.js
@@ -149,6 +149,10 @@ export default class ScrollableList extends PureComponent {
     this.props.onLoadMore();
   }
 
+  defaultShouldUpdateScroll = (prevRouterProps, { location }) => {
+    return !(location.state && location.state.mastodonModalOpen);
+  }
+
   render () {
     const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props;
     const { fullscreen } = this.state;
@@ -190,7 +194,7 @@ export default class ScrollableList extends PureComponent {
 
     if (trackScroll) {
       return (
-        <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
+        <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll || this.defaultShouldUpdateScroll}>
           {scrollableArea}
         </ScrollContainer>
       );
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 1ac5a4b3e..9f47abfef 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -13,6 +13,7 @@ import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
 import { HotKeys } from 'react-hotkeys';
 import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
 import classNames from 'classnames';
+import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
 
 // We use the component (and not the container) since we do not want
 // to use the progress bar to show download progress
@@ -56,6 +57,7 @@ export default class Status extends ImmutablePureComponent {
   state = {
     isCollapsed: false,
     autoCollapsed: false,
+    isExpanded: undefined,
   }
 
   // Avoid checking props that are functions (and whose equality will always
@@ -123,6 +125,17 @@ export default class Status extends ImmutablePureComponent {
       updated = true;
     }
 
+    if (nextProps.expanded === undefined &&
+      prevState.isExpanded === undefined &&
+      update.isExpanded === undefined
+    ) {
+      const isExpanded = autoUnfoldCW(nextProps.settings, nextProps.status);
+      if (isExpanded !== undefined) {
+        update.isExpanded = isExpanded;
+        updated = true;
+      }
+    }
+
     return updated ? update : null;
   }
 
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index 8a840030a..f7e741d2d 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar.js
+++ b/app/javascript/flavours/glitch/components/status_action_bar.js
@@ -5,7 +5,7 @@ import IconButton from './icon_button';
 import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me } from 'flavours/glitch/util/initial_state';
+import { me, isStaff } from 'flavours/glitch/util/initial_state';
 import RelativeTimestamp from './relative_timestamp';
 
 const messages = defineMessages({
@@ -31,6 +31,8 @@ const messages = defineMessages({
   pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
   unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
   embed: { id: 'status.embed', defaultMessage: 'Embed' },
+  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
+  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
 });
 
 const obfuscatedCount = count => {
@@ -102,11 +104,11 @@ export default class StatusActionBar extends ImmutablePureComponent {
   }
 
   handleDeleteClick = () => {
-    this.props.onDelete(this.props.status);
+    this.props.onDelete(this.props.status, this.context.router.history);
   }
 
   handleRedraftClick = () => {
-    this.props.onDelete(this.props.status, true);
+    this.props.onDelete(this.props.status, this.context.router.history, true);
   }
 
   handlePinClick = () => {
@@ -186,6 +188,11 @@ export default class StatusActionBar extends ImmutablePureComponent {
       menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
       menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
       menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
+      if (isStaff) {
+        menu.push(null);
+        menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
+        menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+      }
     }
 
     if (status.get('in_reply_to_id', null) === null) {
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index 48cb76f86..5ac92ea39 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -122,14 +122,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(openModal('EMBED', { url: status.get('url') }));
   },
 
-  onDelete (status, withRedraft = false) {
+  onDelete (status, history, withRedraft = false) {
     if (!deleteModal) {
-      dispatch(deleteStatus(status.get('id'), withRedraft));
+      dispatch(deleteStatus(status.get('id'), history, withRedraft));
     } else {
       dispatch(openModal('CONFIRM', {
         message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
         confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
-        onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
+        onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
       }));
     }
   },
diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js
index 9c80a470b..26717ee49 100644
--- a/app/javascript/flavours/glitch/features/account/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 import { Link } from 'react-router-dom';
 import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
-import { me } from 'flavours/glitch/util/initial_state';
+import { me, isStaff } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
@@ -25,6 +25,7 @@ const messages = defineMessages({
   showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
   endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
   unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
+  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
 });
 
 @injectIntl
@@ -120,6 +121,11 @@ export default class ActionBar extends React.PureComponent {
       }
     }
 
+    if (account.get('id') !== me && isStaff) {
+      menu.push(null);
+      menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
+    }
+
     return (
       <div>
         {extraInfo}
diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js
index 20ba0a1b1..2216f9153 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/index.js
@@ -60,10 +60,6 @@ export default class AccountTimeline extends ImmutablePureComponent {
     this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies }));
   }
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { statusIds, featuredStatusIds, isLoading, hasMore } = this.props;
 
@@ -87,7 +83,6 @@ export default class AccountTimeline extends ImmutablePureComponent {
           isLoading={isLoading}
           hasMore={hasMore}
           onLoadMore={this.handleLoadMore}
-          shouldUpdateScroll={this.shouldUpdateScroll}
         />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js
index f1b4f947e..9468ad81d 100644
--- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js
@@ -66,10 +66,6 @@ export default class Bookmarks extends ImmutablePureComponent {
     this.props.dispatch(expandBookmarkedStatuses());
   }, 300, { leading: true })
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
     const pinned = !!columnId;
@@ -91,7 +87,6 @@ export default class Bookmarks extends ImmutablePureComponent {
           trackScroll={!pinned}
           statusIds={statusIds}
           scrollKey={`bookmarked_statuses-${columnId}`}
-          shouldUpdateScroll={this.shouldUpdateScroll}
           hasMore={hasMore}
           isLoading={isLoading}
           onLoadMore={this.handleLoadMore}
diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js
index e5006b4d3..b5843ca16 100644
--- a/app/javascript/flavours/glitch/features/community_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/community_timeline/index.js
@@ -71,10 +71,6 @@ export default class CommunityTimeline extends React.PureComponent {
     this.props.dispatch(expandCommunityTimeline({ maxId }));
   }
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { intl, hasUnread, columnId, multiColumn } = this.props;
     const pinned = !!columnId;
@@ -97,7 +93,6 @@ export default class CommunityTimeline extends React.PureComponent {
         <StatusListContainer
           trackScroll={!pinned}
           scrollKey={`community_timeline-${columnId}`}
-          shouldUpdateScroll={this.shouldUpdateScroll}
           timelineId='community'
           onLoadMore={this.handleLoadMore}
           emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js
index cf6f45b34..bc409f0a3 100644
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ b/app/javascript/flavours/glitch/features/composer/index.js
@@ -2,6 +2,7 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages } from 'react-intl';
 
 const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\S+)/i;
 
@@ -49,6 +50,13 @@ import { assignHandlers } from 'flavours/glitch/util/react_helpers';
 import { wrap } from 'flavours/glitch/util/redux_helpers';
 import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
 
+const messages = defineMessages({
+  missingDescriptionMessage: {  id: 'confirmations.missing_media_description.message',
+                                defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
+  missingDescriptionConfirm: {  id: 'confirmations.missing_media_description.confirm',
+                                defaultMessage: 'Send anyway' },
+});
+
 //  State mapping.
 function mapStateToProps (state) {
   const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']);
@@ -93,11 +101,12 @@ function mapStateToProps (state) {
     text: state.getIn(['compose', 'text']),
     anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
     spoilersAlwaysOn: spoilersAlwaysOn,
+    mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
   };
 };
 
 //  Dispatch mapping.
-const mapDispatchToProps = (dispatch) => ({
+const mapDispatchToProps = (dispatch, { intl }) => ({
   onCancelReply() {
     dispatch(cancelReplyCompose());
   },
@@ -149,6 +158,13 @@ const mapDispatchToProps = (dispatch) => ({
   onSelectSuggestion(position, token, suggestion) {
     dispatch(selectComposeSuggestion(position, token, suggestion));
   },
+  onMediaDescriptionConfirm() {
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.missingDescriptionMessage),
+      confirm: intl.formatMessage(messages.missingDescriptionConfirm),
+      onConfirm: () => dispatch(submitCompose()),
+    }));
+  },
   onSubmit() {
     dispatch(submitCompose());
   },
@@ -206,14 +222,17 @@ const handlers = {
 
   //  Submits the status.
   handleSubmit () {
-    const { textarea: { value } } = this;
+    const { textarea: { value }, uploadForm } = this;
     const {
       onChangeText,
       onSubmit,
       isSubmitting,
       isUploading,
+      media,
       anyMedia,
       text,
+      mediaDescriptionConfirmation,
+      onMediaDescriptionConfirm,
     } = this.props;
 
     //  If something changes inside the textarea, then we update the
@@ -227,12 +246,26 @@ const handlers = {
       return;
     }
 
-    //  Submits the status.
-    if (onSubmit) {
+    // Submit unless there are media with missing descriptions
+    if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) {
+      const firstWithoutDescription = media.findIndex(item => !item.get('description'));
+      if (uploadForm) {
+        const inputs = uploadForm.querySelectorAll('.composer--upload_form--item input');
+        if (inputs.length == media.size && firstWithoutDescription !== -1) {
+          inputs[firstWithoutDescription].focus();
+        }
+      }
+      onMediaDescriptionConfirm();
+    } else if (onSubmit) {
       onSubmit();
     }
   },
 
+  //  Sets a reference to the upload form.
+  handleRefUploadForm (uploadFormComponent) {
+    this.uploadForm = uploadFormComponent;
+  },
+
   //  Sets a reference to the textarea.
   handleRefTextarea (textareaComponent) {
     if (textareaComponent) {
@@ -339,6 +372,7 @@ class Composer extends React.Component {
       handleSecondarySubmit,
       handleSelect,
       handleSubmit,
+      handleRefUploadForm,
       handleRefTextarea,
       handleRefSpoilerText,
     } = this.handlers;
@@ -429,6 +463,7 @@ class Composer extends React.Component {
             onRemove={onUndoUpload}
             progress={progress}
             uploading={isUploading}
+            handleRef={handleRefUploadForm}
           />
         ) : null}
         <ComposerOptions
@@ -495,6 +530,9 @@ Composer.propTypes = {
   suggestionToken: PropTypes.string,
   suggestions: ImmutablePropTypes.list,
   text: PropTypes.string,
+  anyMedia: PropTypes.bool,
+  spoilersAlwaysOn: PropTypes.bool,
+  mediaDescriptionConfirmation: PropTypes.bool,
 
   //  Dispatch props.
   onCancelReply: PropTypes.func,
@@ -517,8 +555,7 @@ Composer.propTypes = {
   onUndoUpload: PropTypes.func,
   onUnmount: PropTypes.func,
   onUpload: PropTypes.func,
-  anyMedia: PropTypes.bool,
-  spoilersAlwaysOn: PropTypes.bool,
+  onMediaDescriptionConfirm: PropTypes.func,
 };
 
 //  Connecting and export.
diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/index.js
index f3cadc2f5..c2ff66623 100644
--- a/app/javascript/flavours/glitch/features/composer/upload_form/index.js
+++ b/app/javascript/flavours/glitch/features/composer/upload_form/index.js
@@ -17,12 +17,13 @@ export default function ComposerUploadForm ({
   onRemove,
   progress,
   uploading,
+  handleRef,
 }) {
   const computedClass = classNames('composer--upload_form', { uploading });
 
   //  The result.
   return (
-    <div className={computedClass}>
+    <div className={computedClass} ref={handleRef}>
       {uploading ? <ComposerUploadFormProgress progress={progress} /> : null}
       {media ? (
         <div className='content'>
@@ -55,4 +56,5 @@ ComposerUploadForm.propTypes = {
   onRemove: PropTypes.func.isRequired,
   progress: PropTypes.number,
   uploading: PropTypes.bool,
+  handleRef: PropTypes.func,
 };
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js
index 25af49342..418db7c79 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js
@@ -71,10 +71,6 @@ export default class DirectTimeline extends React.PureComponent {
     this.props.dispatch(expandDirectTimeline({ maxId }));
   }
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { intl, hasUnread, columnId, multiColumn } = this.props;
     const pinned = !!columnId;
@@ -97,7 +93,6 @@ export default class DirectTimeline extends React.PureComponent {
         <StatusListContainer
           trackScroll={!pinned}
           scrollKey={`direct_timeline-${columnId}`}
-          shouldUpdateScroll={this.shouldUpdateScroll}
           timelineId='direct'
           onLoadMore={this.handleLoadMore}
           emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js
index 8b023e0bc..3b29e2a26 100644
--- a/app/javascript/flavours/glitch/features/domain_blocks/index.js
+++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js
@@ -40,10 +40,6 @@ export default class Blocks extends ImmutablePureComponent {
     this.props.dispatch(expandDomainBlocks());
   }, 300, { leading: true });
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { intl, domains } = this.props;
 
@@ -58,7 +54,7 @@ export default class Blocks extends ImmutablePureComponent {
     return (
       <Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
         <ColumnBackButtonSlim />
-        <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore} shouldUpdateScroll={this.shouldUpdateScroll}>
+        <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore}>
           {domains.map(domain =>
             <DomainContainer key={domain} domain={domain} />
           )}
diff --git a/app/javascript/flavours/glitch/features/drawer/index.js b/app/javascript/flavours/glitch/features/drawer/index.js
index 1679e9a4b..4649e404f 100644
--- a/app/javascript/flavours/glitch/features/drawer/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/index.js
@@ -86,6 +86,7 @@ class Drawer extends React.Component {
       searchHidden,
       searchValue,
       submitted,
+      isSearchPage,
     } = this.props;
     const computedClass = classNames('drawer', `mbstobon-${elefriend}`);
 
@@ -99,23 +100,24 @@ class Drawer extends React.Component {
             onSettingsClick={onOpenSettings}
           />
         ) : null}
-        <DrawerSearch
-          intl={intl}
-          onChange={onChange}
-          onClear={onClear}
-          onShow={onShow}
-          onSubmit={onSubmit}
-          submitted={submitted}
-          value={searchValue}
-        />
+        {(multiColumn || isSearchPage) && <DrawerSearch
+            intl={intl}
+            onChange={onChange}
+            onClear={onClear}
+            onShow={onShow}
+            onSubmit={onSubmit}
+            submitted={submitted}
+            value={searchValue}
+          /> }
         <div className='contents'>
-          <DrawerAccount account={account} />
-          <Composer />
+          {!isSearchPage && <DrawerAccount account={account} />}
+          {!isSearchPage && <Composer />}
           {multiColumn && <button className='mastodon' onClick={onClickElefriend} />}
-          <DrawerResults
-            results={results}
-            visible={submitted && !searchHidden}
-          />
+          {(multiColumn || isSearchPage) &&
+            <DrawerResults
+              results={results}
+              visible={submitted && !searchHidden}
+            />}
         </div>
       </div>
     );
@@ -126,6 +128,7 @@ class Drawer extends React.Component {
 //  Props.
 Drawer.propTypes = {
   intl: PropTypes.object.isRequired,
+  isSearchPage: PropTypes.bool,
   multiColumn: PropTypes.bool,
 
   //  State props.
diff --git a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
index 6219f46ca..fec090b64 100644
--- a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
@@ -9,6 +9,7 @@ import spring from 'react-motion/lib/spring';
 
 //  Utils.
 import Motion from 'flavours/glitch/util/optional_motion';
+import { searchEnabled } from 'flavours/glitch/util/initial_state';
 
 //  Messages.
 const messages = defineMessages({
@@ -28,6 +29,10 @@ const messages = defineMessages({
     defaultMessage: 'Simple text returns matching display names, usernames and hashtags',
     id: 'search_popout.tips.text',
   },
+  full_text: {
+    defaultMessage: 'Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.',
+    id: 'search_popout.tips.full_text',
+  },
   user: {
     defaultMessage: 'user',
     id: 'search_popout.tips.user',
@@ -92,7 +97,7 @@ export default function DrawerSearchPopout ({ style }) {
                 <FormattedMessage {...messages.status} />
               </li>
             </ul>
-            <FormattedMessage {...messages.text} />
+            { searchEnabled ? <FormattedMessage {...messages.full_text} /> : <FormattedMessage {...messages.text} /> }
           </div>
         )}
       </Motion>
diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.js b/app/javascript/flavours/glitch/features/favourited_statuses/index.js
index 644493183..d8fa1b84e 100644
--- a/app/javascript/flavours/glitch/features/favourited_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.js
@@ -66,10 +66,6 @@ export default class Favourites extends ImmutablePureComponent {
     this.props.dispatch(expandFavouritedStatuses());
   }, 300, { leading: true })
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
     const pinned = !!columnId;
@@ -91,7 +87,6 @@ export default class Favourites extends ImmutablePureComponent {
           trackScroll={!pinned}
           statusIds={statusIds}
           scrollKey={`favourited_statuses-${columnId}`}
-          shouldUpdateScroll={this.shouldUpdateScroll}
           hasMore={hasMore}
           isLoading={isLoading}
           onLoadMore={this.handleLoadMore}
diff --git a/app/javascript/flavours/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js
index 055a15ccb..cf8b31eb3 100644
--- a/app/javascript/flavours/glitch/features/favourites/index.js
+++ b/app/javascript/flavours/glitch/features/favourites/index.js
@@ -33,6 +33,10 @@ export default class Favourites extends ImmutablePureComponent {
     }
   }
 
+  shouldUpdateScroll = (prevRouterProps, { location }) => {
+    return !(location.state && location.state.mastodonModalOpen);
+  }
+
   render () {
     const { accountIds } = this.props;
 
@@ -48,7 +52,7 @@ export default class Favourites extends ImmutablePureComponent {
       <Column>
         <ColumnBackButton />
 
-        <ScrollContainer scrollKey='favourites'>
+        <ScrollContainer scrollKey='favourites' shouldUpdateScroll={this.shouldUpdateScroll}>
           <div className='scrollable'>
             {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
           </div>
diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.js b/app/javascript/flavours/glitch/features/follow_requests/index.js
index 04ff3f111..1e4633984 100644
--- a/app/javascript/flavours/glitch/features/follow_requests/index.js
+++ b/app/javascript/flavours/glitch/features/follow_requests/index.js
@@ -42,6 +42,10 @@ export default class FollowRequests extends ImmutablePureComponent {
     }
   }
 
+  shouldUpdateScroll = (prevRouterProps, { location }) => {
+    return !(location.state && location.state.mastodonModalOpen);
+  }
+
   render () {
     const { intl, accountIds } = this.props;
 
@@ -57,7 +61,7 @@ export default class FollowRequests extends ImmutablePureComponent {
       <Column name='follow-requests' icon='users' heading={intl.formatMessage(messages.heading)}>
         <ColumnBackButtonSlim />
 
-        <ScrollContainer scrollKey='follow_requests'>
+        <ScrollContainer scrollKey='follow_requests' shouldUpdateScroll={this.shouldUpdateScroll}>
           <div className='scrollable' onScroll={this.handleScroll}>
             {accountIds.map(id =>
               <AccountAuthorizeContainer key={id} id={id} />
diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js
index c42e0386c..cdde1775c 100644
--- a/app/javascript/flavours/glitch/features/followers/index.js
+++ b/app/javascript/flavours/glitch/features/followers/index.js
@@ -56,6 +56,10 @@ export default class Followers extends ImmutablePureComponent {
     this.props.dispatch(expandFollowers(this.props.params.accountId));
   }
 
+  shouldUpdateScroll = (prevRouterProps, { location }) => {
+    return !(location.state && location.state.mastodonModalOpen);
+  }
+
   render () {
     const { accountIds, hasMore } = this.props;
 
@@ -77,7 +81,7 @@ export default class Followers extends ImmutablePureComponent {
       <Column>
         <ColumnBackButton />
 
-        <ScrollContainer scrollKey='followers'>
+        <ScrollContainer scrollKey='followers' shouldUpdateScroll={this.shouldUpdateScroll}>
           <div className='scrollable' onScroll={this.handleScroll}>
             <div className='followers'>
               <HeaderContainer accountId={this.props.params.accountId} hideTabs />
diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
index b3e8b7a6e..8f77ed42b 100644
--- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
@@ -82,10 +82,6 @@ export default class HashtagTimeline extends React.PureComponent {
     this.props.dispatch(expandHashtagTimeline(this.props.params.id, { maxId }));
   }
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { hasUnread, columnId, multiColumn } = this.props;
     const { id } = this.props.params;
@@ -110,7 +106,6 @@ export default class HashtagTimeline extends React.PureComponent {
           scrollKey={`hashtag_timeline-${columnId}`}
           timelineId={`hashtag:${id}`}
           onLoadMore={this.handleLoadMore}
-          shouldUpdateScroll={this.shouldUpdateScroll}
           emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
         />
       </Column>
diff --git a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js
index fc2167c0c..0c1040290 100644
--- a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js
@@ -10,6 +10,7 @@ import LocalSettingsNavigationItem from './item';
 
 const messages = defineMessages({
   general: {  id: 'settings.general', defaultMessage: 'General' },
+  content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' },
   collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' },
   media: { id: 'settings.media', defaultMessage: 'Media' },
   preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' },
@@ -42,25 +43,31 @@ export default class LocalSettingsNavigation extends React.PureComponent {
           active={index === 1}
           index={1}
           onNavigate={onNavigate}
-          title={intl.formatMessage(messages.collapsed)}
+          title={intl.formatMessage(messages.content_warnings)}
         />
         <LocalSettingsNavigationItem
           active={index === 2}
           index={2}
           onNavigate={onNavigate}
-          title={intl.formatMessage(messages.media)}
+          title={intl.formatMessage(messages.collapsed)}
         />
         <LocalSettingsNavigationItem
           active={index === 3}
-          href='/settings/preferences'
           index={3}
+          onNavigate={onNavigate}
+          title={intl.formatMessage(messages.media)}
+        />
+        <LocalSettingsNavigationItem
+          active={index === 4}
+          href='/settings/preferences'
+          index={4}
           icon='cog'
           title={intl.formatMessage(messages.preferences)}
         />
         <LocalSettingsNavigationItem
-          active={index === 4}
+          active={index === 5}
           className='close'
-          index={4}
+          index={5}
           onNavigate={onClose}
           title={intl.formatMessage(messages.close)}
         />
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 ad5c11979..0db49ba5d 100644
--- a/app/javascript/flavours/glitch/features/local_settings/page/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js
@@ -17,6 +17,7 @@ const messages = defineMessages({
   side_arm_keep: { id: 'settings.side_arm_reply_mode.keep', defaultMessage: 'Keep secondary toot button to set privacy' },
   side_arm_copy: { id: 'settings.side_arm_reply_mode.copy', defaultMessage: 'Copy privacy setting of the toot being replied to' },
   side_arm_restrict: { id: 'settings.side_arm_reply_mode.restrict', defaultMessage: 'Restrict privacy setting to that of the toot being replied to' },
+  regexp: { id: 'settings.content_warnings.regexp', defaultMessage: 'Regular expression' },
 });
 
 @injectIntl
@@ -85,6 +86,14 @@ export default class LocalSettingsPage extends React.PureComponent {
           </LocalSettingsPageItem>
           <LocalSettingsPageItem
             settings={settings}
+            item={['confirm_missing_media_description']}
+            id='mastodon-settings--confirm_missing_media_description'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.confirm_missing_media_description' defaultMessage='Show confirmation dialog before sending toots lacking media descriptions' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
             item={['side_arm']}
             id='mastodon-settings--side_arm'
             options={[
@@ -114,6 +123,29 @@ export default class LocalSettingsPage extends React.PureComponent {
         </section>
       </div>
     ),
+    ({ 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']}
+          id='mastodon-settings--content_warnings-auto_unfold'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.enable_content_warnings_auto_unfold' defaultMessage='Automatically unfold content-warnings' />
+        </LocalSettingsPageItem>
+        <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)}
+        >
+          <FormattedMessage id='settings.content_warnings_filter' defaultMessage='Content warnings to not automatically unfold:' />
+        </LocalSettingsPageItem>
+      </div>
+    ),
     ({ onChange, settings }) => (
       <div className='glitch local-settings__page collapsed'>
         <h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1>
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 66e84dfe1..fe237f11e 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
@@ -19,18 +19,20 @@ export default class LocalSettingsPageItem extends React.PureComponent {
       message: PropTypes.string.isRequired,
     })),
     settings: ImmutablePropTypes.map.isRequired,
+    placeholder: PropTypes.string,
   };
 
   handleChange = e => {
     const { target } = e;
-    const { item, onChange, options } = this.props;
+    const { item, onChange, options, placeholder } = this.props;
     if (options && options.length > 0) onChange(item, target.value);
+    else if (placeholder) onChange(item, target.value);
     else onChange(item, target.checked);
   }
 
   render () {
     const { handleChange } = this;
-    const { settings, item, id, options, children, dependsOn, dependsOnNot } = this.props;
+    const { settings, item, id, options, children, dependsOn, dependsOnNot, placeholder } = this.props;
     let enabled = true;
 
     if (dependsOn) {
@@ -70,6 +72,22 @@ export default class LocalSettingsPageItem extends React.PureComponent {
           </p>
         </label>
       );
+    } else if (placeholder) {
+      return (
+        <label className='glitch local-settings__page__item' htmlFor={id}>
+          <p>{children}</p>
+          <p>
+            <input
+              id={id}
+              type='text'
+              value={settings.getIn(item)}
+              placeholder={placeholder}
+              onChange={handleChange}
+              disabled={!enabled}
+            />
+          </p>
+        </label>
+      );
     } else return (
       <label className='glitch local-settings__page__item' htmlFor={id}>
         <input
diff --git a/app/javascript/flavours/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js
index 87517eec9..d94c1d8ad 100644
--- a/app/javascript/flavours/glitch/features/mutes/index.js
+++ b/app/javascript/flavours/glitch/features/mutes/index.js
@@ -42,6 +42,10 @@ export default class Mutes extends ImmutablePureComponent {
     }
   }
 
+  shouldUpdateScroll = (prevRouterProps, { location }) => {
+    return !(location.state && location.state.mastodonModalOpen);
+  }
+
   render () {
     const { intl, accountIds } = this.props;
 
@@ -56,7 +60,7 @@ export default class Mutes extends ImmutablePureComponent {
     return (
       <Column name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}>
         <ColumnBackButtonSlim />
-        <ScrollContainer scrollKey='mutes'>
+        <ScrollContainer scrollKey='mutes' shouldUpdateScroll={this.shouldUpdateScroll}>
           <div className='scrollable mutes' onScroll={this.handleScroll}>
             {accountIds.map(id =>
               <AccountContainer key={id} id={id} />
diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.js b/app/javascript/flavours/glitch/features/pinned_statuses/index.js
index e7fa7ac0d..f56d70176 100644
--- a/app/javascript/flavours/glitch/features/pinned_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.js
@@ -41,10 +41,6 @@ export default class PinnedStatuses extends ImmutablePureComponent {
     this.column = c;
   }
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { intl, statusIds, hasMore } = this.props;
 
@@ -54,7 +50,6 @@ export default class PinnedStatuses extends ImmutablePureComponent {
         <StatusList
           statusIds={statusIds}
           scrollKey='pinned_statuses'
-          shouldUpdateScroll={this.shouldUpdateScroll}
           hasMore={hasMore}
         />
       </Column>
diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js
index 3eb92cafa..a6c0b1688 100644
--- a/app/javascript/flavours/glitch/features/public_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/public_timeline/index.js
@@ -71,10 +71,6 @@ export default class PublicTimeline extends React.PureComponent {
     this.props.dispatch(expandPublicTimeline({ maxId }));
   }
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { intl, columnId, hasUnread, multiColumn } = this.props;
     const pinned = !!columnId;
@@ -99,7 +95,6 @@ export default class PublicTimeline extends React.PureComponent {
           onLoadMore={this.handleLoadMore}
           trackScroll={!pinned}
           scrollKey={`public_timeline-${columnId}`}
-          shouldUpdateScroll={this.shouldUpdateScroll}
           emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />}
         />
       </Column>
diff --git a/app/javascript/flavours/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js
index 25b792b39..c0a65d1de 100644
--- a/app/javascript/flavours/glitch/features/reblogs/index.js
+++ b/app/javascript/flavours/glitch/features/reblogs/index.js
@@ -33,6 +33,10 @@ export default class Reblogs extends ImmutablePureComponent {
     }
   }
 
+  shouldUpdateScroll = (prevRouterProps, { location }) => {
+    return !(location.state && location.state.mastodonModalOpen);
+  }
+
   render () {
     const { accountIds } = this.props;
 
@@ -48,7 +52,7 @@ export default class Reblogs extends ImmutablePureComponent {
       <Column>
         <ColumnBackButton />
 
-        <ScrollContainer scrollKey='reblogs'>
+        <ScrollContainer scrollKey='reblogs' shouldUpdateScroll={this.shouldUpdateScroll}>
           <div className='scrollable reblogs'>
             {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
           </div>
diff --git a/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js
index 08b9e9e57..c488f9541 100644
--- a/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js
@@ -47,10 +47,6 @@ export default class CommunityTimeline extends React.PureComponent {
     this.props.dispatch(expandCommunityTimeline({ maxId }));
   }
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { intl } = this.props;
 
@@ -66,7 +62,6 @@ export default class CommunityTimeline extends React.PureComponent {
           timelineId='community'
           onLoadMore={this.handleLoadMore}
           scrollKey='standalone_public_timeline'
-          shouldUpdateScroll={this.shouldUpdateScroll}
           trackScroll={false}
         />
       </Column>
diff --git a/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js
index d2b1971ec..dc02f1c91 100644
--- a/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js
@@ -41,10 +41,6 @@ export default class HashtagTimeline extends React.PureComponent {
     this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId }));
   }
 
-  shouldUpdateScroll = (prevRouterProps, { location }) => {
-    return !(location.state && location.state.mastodonModalOpen)
-  }
-
   render () {
     const { hashtag } = this.props;
 
@@ -59,7 +55,6 @@ export default class HashtagTimeline extends React.PureComponent {
         <StatusListContainer
           trackScroll={false}
           scrollKey='standalone_hashtag_timeline'
-          shouldUpdateScroll={this.shouldUpdateScroll}
           timelineId={`hashtag:${hashtag}`}
           onLoadMore={this.handleLoadMore}
         />
diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js
index 99e2c594b..009aa49eb 100644
--- a/app/javascript/flavours/glitch/features/status/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js
@@ -4,7 +4,7 @@ import IconButton from 'flavours/glitch/components/icon_button';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 import { defineMessages, injectIntl } from 'react-intl';
-import { me } from 'flavours/glitch/util/initial_state';
+import { me, isStaff } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -26,6 +26,8 @@ const messages = defineMessages({
   pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
   unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
   embed: { id: 'status.embed', defaultMessage: 'Embed' },
+  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
+  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
 });
 
 @injectIntl
@@ -70,11 +72,11 @@ export default class ActionBar extends React.PureComponent {
   }
 
   handleDeleteClick = () => {
-    this.props.onDelete(this.props.status);
+    this.props.onDelete(this.props.status, this.context.router.history);
   }
 
   handleRedraftClick = () => {
-    this.props.onDelete(this.props.status, true);
+    this.props.onDelete(this.props.status, this.context.router.history, true);
   }
 
   handleDirectClick = () => {
@@ -146,6 +148,11 @@ export default class ActionBar extends React.PureComponent {
       menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
       menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
       menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
+      if (isStaff) {
+        menu.push(null);
+        menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
+        menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+      }
     }
 
     const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index ddc2f820a..3d309976a 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -38,6 +38,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import { HotKeys } from 'react-hotkeys';
 import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
 import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
+import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
 
 const messages = defineMessages({
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@@ -82,8 +83,8 @@ export default class Status extends ImmutablePureComponent {
 
   state = {
     fullscreen: false,
-    isExpanded: false,
-    threadExpanded: null,
+    isExpanded: undefined,
+    threadExpanded: undefined,
   };
 
   componentWillMount () {
@@ -95,9 +96,14 @@ export default class Status extends ImmutablePureComponent {
   }
 
   componentWillReceiveProps (nextProps) {
+    if (this.state.isExpanded === undefined) {
+      const isExpanded = autoUnfoldCW(nextProps.settings, nextProps.status);
+      if (isExpanded !== undefined) this.setState({ isExpanded: isExpanded });
+    }
     if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
       this._scrolledIntoView = false;
       this.props.dispatch(fetchStatus(nextProps.params.statusId));
+      this.setState({ isExpanded: autoUnfoldCW(nextProps.settings, nextProps.status) });
     }
   }
 
@@ -159,16 +165,16 @@ export default class Status extends ImmutablePureComponent {
     }
   }
 
-  handleDeleteClick = (status, withRedraft = false) => {
+  handleDeleteClick = (status, history, withRedraft = false) => {
     const { dispatch, intl } = this.props;
 
     if (!deleteModal) {
-      dispatch(deleteStatus(status.get('id'), withRedraft));
+      dispatch(deleteStatus(status.get('id'), history, withRedraft));
     } else {
       dispatch(openModal('CONFIRM', {
         message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
         confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
-        onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
+        onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
       }));
     }
   }
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 ee71e514a..f87c078ec 100644
--- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
@@ -1,11 +1,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { injectIntl } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 import ReactSwipeableViews from 'react-swipeable-views';
 import { links, getIndex, getLink } from './tabs_bar';
+import { Link } from 'react-router-dom';
 
 import BundleContainer from '../containers/bundle_container';
 import ColumnLoading from './column_loading';
@@ -29,6 +30,10 @@ const componentMap = {
   'LIST': ListTimeline,
 };
 
+const messages = defineMessages({
+  publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
+});
+
 @component => injectIntl(component, { withRef: true })
 export default class ColumnsArea extends ImmutablePureComponent {
 
@@ -146,18 +151,26 @@ export default class ColumnsArea extends ImmutablePureComponent {
   }
 
   render () {
-    const { columns, children, singleColumn } = this.props;
+    const { columns, children, singleColumn, intl } = this.props;
     const { shouldAnimate } = this.state;
 
     const columnIndex = getIndex(this.context.router.history.location.pathname);
     this.pendingIndex = null;
 
     if (singleColumn) {
-      return columnIndex !== -1 ? (
-        <ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
+      const floatingActionButton = this.context.router.history.location.pathname === '/statuses/new' ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><i className='fa fa-pencil' /></Link>;
+
+      return columnIndex !== -1 ? [
+        <ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
           {links.map(this.renderView)}
-        </ReactSwipeableViews>
-      ) : <div className='columns-area'>{children}</div>;
+        </ReactSwipeableViews>,
+
+        floatingActionButton,
+      ] : [
+        <div className='columns-area'>{children}</div>,
+
+        floatingActionButton,
+      ];
     }
 
     return (
diff --git a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
index 89b455dd8..b2fee21e1 100644
--- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
+++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
@@ -1,19 +1,19 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { NavLink } from 'react-router-dom';
+import { NavLink, withRouter } from 'react-router-dom';
 import { FormattedMessage, injectIntl } from 'react-intl';
 import { debounce } from 'lodash';
 import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 
 export const links = [
-  <NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
   <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
   <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
 
   <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
   <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
+  <NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><i className='fa fa-fw fa-search' /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
 
-  <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='asterisk' ><i className='fa fa-fw fa-asterisk' /></NavLink>,
+  <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><i className='fa fa-fw fa-bars' /></NavLink>,
 ];
 
 export function getIndex (path) {
@@ -25,14 +25,12 @@ export function getLink (index) {
 }
 
 @injectIntl
-export default class TabsBar extends React.Component {
-
-  static contextTypes = {
-    router: PropTypes.object.isRequired,
-  }
+@withRouter
+export default class TabsBar extends React.PureComponent {
 
   static propTypes = {
     intl: PropTypes.object.isRequired,
+    history: PropTypes.object.isRequired,
   }
 
   setRef = ref => {
@@ -60,7 +58,7 @@ export default class TabsBar extends React.Component {
 
           const listener = debounce(() => {
             nextTab.removeEventListener('transitionend', listener);
-            this.context.router.history.push(to);
+            this.props.history.push(to);
           }, 50);
 
           nextTab.addEventListener('transitionend', listener);
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index d58e11b55..1cff94321 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -437,6 +437,8 @@ export default class UI extends React.Component {
               <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
               <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
 
+              <WrappedRoute path='/search' component={Drawer} content={children} componentParams={{ isSearchPage: true }} />
+
               <WrappedRoute path='/statuses/new' component={Drawer} content={children} />
               <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
               <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js
index 1d24f0e9a..063ae3943 100644
--- a/app/javascript/flavours/glitch/reducers/local_settings.js
+++ b/app/javascript/flavours/glitch/reducers/local_settings.js
@@ -13,6 +13,11 @@ const initialState = ImmutableMap({
   side_arm_reply_mode : 'keep',
   show_reply_count : false,
   always_show_spoilers_field: false,
+  confirm_missing_media_description: false,
+  content_warnings : ImmutableMap({
+    auto_unfold : false,
+    filter      : null,
+  }),
   collapsed : ImmutableMap({
     enabled     : true,
     auto        : ImmutableMap({
diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index c61cd038f..86c77f980 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -503,3 +503,27 @@
     margin-left: 5px;
   }
 }
+
+.floating-action-button {
+  position: fixed;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 3.9375rem;
+  height: 3.9375rem;
+  bottom: 1.3125rem;
+  right: 1.3125rem;
+  background: darken($ui-highlight-color, 3%);
+  color: $white;
+  border-radius: 50%;
+  font-size: 21px;
+  line-height: 21px;
+  text-decoration: none;
+  box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4);
+
+  &:hover,
+  &:focus,
+  &:active {
+    background: lighten($ui-highlight-color, 7%);
+  }
+}
diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss
index 0432b233a..6a9af4490 100644
--- a/app/javascript/flavours/glitch/styles/components/drawer.scss
+++ b/app/javascript/flavours/glitch/styles/components/drawer.scss
@@ -5,7 +5,6 @@
   padding: 10px 5px;
   width: 300px;
   flex: none;
-  contain: strict;
 
   &:first-child {
     padding-left: 10px;
@@ -49,7 +48,6 @@
     background: lighten($ui-base-color, 13%);
     overflow-x: hidden;
     overflow-y: auto;
-    contain: strict;
 
     & > .mastodon {
       flex: 1;
@@ -253,7 +251,6 @@
   background: $ui-base-color;
   overflow-x: hidden;
   overflow-y: auto;
-  contain: strict;
 
   & > header {
     border-bottom: 1px solid darken($ui-base-color, 4%);
diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss
index b5d79f4d7..17901f233 100644
--- a/app/javascript/flavours/glitch/styles/containers.scss
+++ b/app/javascript/flavours/glitch/styles/containers.scss
@@ -347,6 +347,23 @@
     margin-bottom: 10px;
     box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
 
+    &.inactive {
+      opacity: 0.5;
+
+      .public-account-header__image,
+      .avatar {
+        filter: grayscale(100%);
+      }
+
+      .logo-button {
+        background-color: $secondary-text-color;
+
+        svg path:last-child {
+          fill: $secondary-text-color;
+        }
+      }
+    }
+
     &__image {
       border-radius: 4px 4px 0 0;
       overflow: hidden;
@@ -588,6 +605,10 @@
               border-bottom: 4px solid $highlight-text-color;
               opacity: 1;
             }
+
+            &.inactive::after {
+              border-bottom-color: $secondary-text-color;
+            }
           }
 
           &:hover {
diff --git a/app/javascript/flavours/glitch/util/content_warning.js b/app/javascript/flavours/glitch/util/content_warning.js
new file mode 100644
index 000000000..29e221c8e
--- /dev/null
+++ b/app/javascript/flavours/glitch/util/content_warning.js
@@ -0,0 +1,19 @@
+export function autoUnfoldCW (settings, status) {
+  if (!settings.getIn(['content_warnings', 'auto_unfold'])) {
+    return false;
+  }
+
+  const rawRegex = settings.getIn(['content_warnings', 'filter']);
+  let regex      = null;
+
+  try {
+    regex = rawRegex && new RegExp(rawRegex.trim(), 'i');
+  } catch (e) {
+    // Bad regex, don't affect filters
+  }
+
+  if (!(status && regex)) {
+    return undefined;
+  }
+  return !regex.test(status.get('spoiler_text'));
+}
diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js
index c6416db2d..82a1ef89c 100644
--- a/app/javascript/flavours/glitch/util/emoji/index.js
+++ b/app/javascript/flavours/glitch/util/emoji/index.js
@@ -62,6 +62,10 @@ const emojify = (str, customEmojis = {}) => {
       const title = shortCode ? `:${shortCode}:` : '';
       replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`;
       rend = i + match.length;
+      // If the matched character was followed by VS15 (for selecting text presentation), skip it.
+      if (str.codePointAt(rend) === 65038) {
+        rend += 1;
+      }
     }
     rtn += str.slice(0, i) + replacement;
     str = str.slice(rend);
diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js
index 2c4ab9091..fdf004527 100644
--- a/app/javascript/flavours/glitch/util/initial_state.js
+++ b/app/javascript/flavours/glitch/util/initial_state.js
@@ -19,6 +19,8 @@ export const boostModal = getMeta('boost_modal');
 export const favouriteModal = getMeta('favourite_modal');
 export const deleteModal = getMeta('delete_modal');
 export const me = getMeta('me');
+export const searchEnabled = getMeta('search_enabled');
 export const maxChars = (initialState && initialState.max_toot_chars) || 500;
+export const isStaff = getMeta('is_staff');
 
 export default initialState;