about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/features')
-rw-r--r--app/javascript/flavours/glitch/features/account/components/action_bar.js29
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js25
-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.js16
-rw-r--r--app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js2
-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/header/index.js11
-rw-r--r--app/javascript/flavours/glitch/features/drawer/index.js8
-rw-r--r--app/javascript/flavours/glitch/features/emoji_picker/index.js1
-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.js19
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/index.js123
-rw-r--r--app/javascript/flavours/glitch/features/mutes/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/notifications/index.js24
-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.js9
-rw-r--r--app/javascript/flavours/glitch/features/status/components/card.js177
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js1
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/columns_area.js12
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/media_modal.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/report_modal.js1
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/tabs_bar.js26
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/video_modal.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/zoomable_image.js1
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js47
-rw-r--r--app/javascript/flavours/glitch/features/video/index.js17
38 files changed, 433 insertions, 208 deletions
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..3d6eeb06a 100644
--- a/app/javascript/flavours/glitch/features/account/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js
@@ -2,9 +2,9 @@ import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
-import { Link } from 'react-router-dom';
+import { NavLink } 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
@@ -51,6 +52,13 @@ export default class ActionBar extends React.PureComponent {
     });
   }
 
+  isStatusesPageActive = (match, location) => {
+    if (!match) {
+      return false;
+    }
+    return !location.pathname.match(/\/(followers|following)\/?$/);
+  }
+
   render () {
     const { account, intl } = this.props;
 
@@ -120,6 +128,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}
@@ -130,20 +143,20 @@ export default class ActionBar extends React.PureComponent {
           </div>
 
           <div className='account__action-bar-links'>
-            <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
+            <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
               <FormattedMessage id='account.posts' defaultMessage='Posts' />
               <strong><FormattedNumber value={account.get('statuses_count')} /></strong>
-            </Link>
+            </NavLink>
 
-            <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}>
+            <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}>
               <FormattedMessage id='account.follows' defaultMessage='Follows' />
               <strong><FormattedNumber value={account.get('following_count')} /></strong>
-            </Link>
+            </NavLink>
 
-            <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
+            <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
               <FormattedMessage id='account.followers' defaultMessage='Followers' />
               <strong><FormattedNumber value={account.get('followers_count')} /></strong>
-            </Link>
+            </NavLink>
           </div>
         </div>
       </div>
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index eda0d637e..f0d36947d 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -15,8 +15,19 @@ const messages = defineMessages({
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
   requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
   unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
+  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+  link_verified_on: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
 });
 
+const dateFormatOptions = {
+  month: 'short',
+  day: 'numeric',
+  year: 'numeric',
+  hour12: false,
+  hour: '2-digit',
+  minute: '2-digit',
+};
+
 @injectIntl
 export default class Header extends ImmutablePureComponent {
 
@@ -27,6 +38,10 @@ export default class Header extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
   };
 
+  openEditProfile = () => {
+    window.open('/settings/profile', '_blank');
+  }
+
   render () {
     const { account, intl } = this.props;
 
@@ -77,6 +92,12 @@ export default class Header extends ImmutablePureComponent {
           </div>
         );
       }
+    } else {
+      actionBtn = (
+        <div className='account--action-button'>
+          <IconButton size={26} icon='pencil' title={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />
+        </div>
+      );
     }
 
     if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
@@ -111,7 +132,9 @@ export default class Header extends ImmutablePureComponent {
                 {fields.map((pair, i) => (
                   <dl key={i}>
                     <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
-                    <dd dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} title={pair.get('value_plain')} />
+                    <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
+                      {pair.get('verified_at') && <span title={intl.formatMessage(messages.link_verified_on, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><i className='fa fa-check verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
+                    </dd>
                  </dl>
                 ))}
               </div>
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 5bd9d037c..ddcca2dc0 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 bc409f0a3..257797047 100644
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ b/app/javascript/flavours/glitch/features/composer/index.js
@@ -102,6 +102,7 @@ function mapStateToProps (state) {
     anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
     spoilersAlwaysOn: spoilersAlwaysOn,
     mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
+    preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
   };
 };
 
@@ -146,7 +147,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   onMount() {
     dispatch(mountCompose());
   },
-  onOpenActionModal(props) {
+  onOpenActionsModal(props) {
     dispatch(openModal('ACTIONS', props));
   },
   onOpenDoodleModal() {
@@ -242,7 +243,7 @@ const handlers = {
     }
 
     // Submit disabled:
-    if (isSubmitting || isUploading || (!!text.length && !text.trim().length && !anyMedia)) {
+    if (isSubmitting || isUploading || (!text.trim().length && !anyMedia)) {
       return;
     }
 
@@ -328,13 +329,14 @@ class Composer extends React.Component {
       isSubmitting,
       preselectDate,
       text,
+      preselectOnReply,
     } = this.props;
     let selectionEnd, selectionStart;
 
     //  Caret/selection handling.
     if (focusDate !== prevProps.focusDate) {
       switch (true) {
-      case preselectDate !== prevProps.preselectDate:
+      case preselectDate !== prevProps.preselectDate && preselectOnReply:
         selectionStart = text.search(/\s/) + 1;
         selectionEnd = text.length;
         break;
@@ -347,6 +349,7 @@ class Composer extends React.Component {
       if (textarea) {
         textarea.setSelectionRange(selectionStart, selectionEnd);
         textarea.focus();
+        textarea.scrollIntoView();
       }
 
     //  Refocuses the textarea after submitting.
@@ -415,7 +418,7 @@ class Composer extends React.Component {
       spoilersAlwaysOn,
     } = this.props;
 
-    let disabledButton = isSubmitting || isUploading || (!!text.length && !text.trim().length && !anyMedia);
+    let disabledButton = isSubmitting || isUploading || (!text.trim().length && !anyMedia);
 
     return (
       <div className='composer'>
@@ -485,8 +488,8 @@ class Composer extends React.Component {
           onUpload={onUpload}
           privacy={privacy}
           resetFileKey={resetFileKey}
-          sensitive={sensitive}
-          spoiler={spoiler}
+          sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)}
+          spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler}
         />
         <ComposerPublisher
           countText={`${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`}
@@ -533,6 +536,7 @@ Composer.propTypes = {
   anyMedia: PropTypes.bool,
   spoilersAlwaysOn: PropTypes.bool,
   mediaDescriptionConfirmation: PropTypes.bool,
+  preselectOnReply: PropTypes.bool,
 
   //  Dispatch props.
   onCancelReply: PropTypes.func,
diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js
index 331692398..1b7ae8904 100644
--- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js
+++ b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js
@@ -11,7 +11,7 @@ import { unicodeMapping } from 'flavours/glitch/util/emoji';
 import { assignHandlers } from 'flavours/glitch/util/react_helpers';
 
 //  Gets our asset host from the environment, if available.
-const assetHost = ((process || {}).env || {}).CDN_HOST || '';
+const assetHost = process.env.CDN_HOST || '';
 
 //  Handlers.
 const handlers = {
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js
index 949c85dce..dc7e0534d 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/header/index.js b/app/javascript/flavours/glitch/features/drawer/header/index.js
index deec42435..7fefd32c9 100644
--- a/app/javascript/flavours/glitch/features/drawer/header/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/header/index.js
@@ -46,6 +46,8 @@ const messages = defineMessages({
 //  The component.
 export default function DrawerHeader ({
   columns,
+  unreadNotifications,
+  showNotificationsBadge,
   intl,
   onSettingsClick,
 }) {
@@ -77,7 +79,12 @@ export default function DrawerHeader ({
           aria-label={intl.formatMessage(messages.notifications)}
           title={intl.formatMessage(messages.notifications)}
           to='/notifications'
-        ><Icon icon='bell' /></Link>
+        >
+          <span className='icon-badge-wrapper'>
+            <Icon icon='bell' />
+            { showNotificationsBadge && unreadNotifications > 0 && <div className='icon-badge' />}
+          </span>
+        </Link>
       ))}
       {renderForColumn('COMMUNITY', (
         <Link
@@ -112,6 +119,8 @@ export default function DrawerHeader ({
 //  Props.
 DrawerHeader.propTypes = {
   columns: ImmutablePropTypes.list,
+  unreadNotifications: PropTypes.number,
+  showNotificationsBadge: PropTypes.bool,
   intl: PropTypes.object,
   onSettingsClick: PropTypes.func,
 };
diff --git a/app/javascript/flavours/glitch/features/drawer/index.js b/app/javascript/flavours/glitch/features/drawer/index.js
index 497b7d0e4..038a2513e 100644
--- a/app/javascript/flavours/glitch/features/drawer/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/index.js
@@ -40,6 +40,8 @@ const mapStateToProps = state => ({
   searchHidden: state.getIn(['search', 'hidden']),
   searchValue: state.getIn(['search', 'value']),
   submitted: state.getIn(['search', 'submitted']),
+  unreadNotifications: state.getIn(['notifications', 'unread']),
+  showNotificationsBadge: state.getIn(['local_settings', 'notifications', 'tab_badge']),
 });
 
 //  Dispatch mapping.
@@ -93,6 +95,8 @@ class Drawer extends React.Component {
       searchValue,
       submitted,
       isSearchPage,
+      unreadNotifications,
+      showNotificationsBadge,
     } = this.props;
     const computedClass = classNames('drawer', `mbstobon-${elefriend}`);
 
@@ -102,6 +106,8 @@ class Drawer extends React.Component {
         {multiColumn ? (
           <DrawerHeader
             columns={columns}
+            unreadNotifications={unreadNotifications}
+            showNotificationsBadge={showNotificationsBadge}
             intl={intl}
             onSettingsClick={onOpenSettings}
           />
@@ -145,6 +151,8 @@ Drawer.propTypes = {
   searchHidden: PropTypes.bool,
   searchValue: PropTypes.string,
   submitted: PropTypes.bool,
+  unreadNotifications: PropTypes.number,
+  showNotificationsBadge: PropTypes.bool,
 
   //  Dispatch props.
   onChange: PropTypes.func,
diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js
index d22a50848..a78117971 100644
--- a/app/javascript/flavours/glitch/features/emoji_picker/index.js
+++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js
@@ -340,6 +340,7 @@ class EmojiPickerMenu extends React.PureComponent {
           skin={skinTone}
           showPreview={false}
           backgroundImageFn={backgroundImageFn}
+          autoFocus
           emojiTooltip
         />
 
diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.js b/app/javascript/flavours/glitch/features/favourited_statuses/index.js
index 923d4b24f..32bf4e71a 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 f710456d5..311fabb63 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 0c1040290..a992b1ffc 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' },
+  compose: {  id: 'settings.compose_box_opts', defaultMessage: 'Compose box options' },
   content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' },
   collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' },
   media: { id: 'settings.media', defaultMessage: 'Media' },
@@ -43,31 +44,37 @@ export default class LocalSettingsNavigation extends React.PureComponent {
           active={index === 1}
           index={1}
           onNavigate={onNavigate}
-          title={intl.formatMessage(messages.content_warnings)}
+          title={intl.formatMessage(messages.compose)}
         />
         <LocalSettingsNavigationItem
           active={index === 2}
           index={2}
           onNavigate={onNavigate}
-          title={intl.formatMessage(messages.collapsed)}
+          title={intl.formatMessage(messages.content_warnings)}
         />
         <LocalSettingsNavigationItem
           active={index === 3}
           index={3}
           onNavigate={onNavigate}
-          title={intl.formatMessage(messages.media)}
+          title={intl.formatMessage(messages.collapsed)}
         />
         <LocalSettingsNavigationItem
           active={index === 4}
-          href='/settings/preferences'
           index={4}
+          onNavigate={onNavigate}
+          title={intl.formatMessage(messages.media)}
+        />
+        <LocalSettingsNavigationItem
+          active={index === 5}
+          href='/settings/preferences'
+          index={5}
           icon='cog'
           title={intl.formatMessage(messages.preferences)}
         />
         <LocalSettingsNavigationItem
-          active={index === 5}
+          active={index === 6}
           className='close'
-          index={5}
+          index={6}
           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 0db49ba5d..5b4e0cb01 100644
--- a/app/javascript/flavours/glitch/features/local_settings/page/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js
@@ -43,6 +43,25 @@ export default class LocalSettingsPage extends React.PureComponent {
           <FormattedMessage id='settings.show_reply_counter' defaultMessage='Display an estimate of the reply count' />
         </LocalSettingsPageItem>
         <section>
+          <h2><FormattedMessage id='settings.notifications_opts' defaultMessage='Notifications options' /></h2>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['notifications', 'tab_badge']}
+            id='mastodon-settings--notifications-tab_badge'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.notifications.tab_badge' defaultMessage="Display a badge for unread notifications if the notifications column isn't open" />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['notifications', 'favicon_badge']}
+            id='mastodon-settings--notifications-favicon_badge'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.notifications.favicon_badge' defaultMessage='Display unread notifications count in the favicon' />
+          </LocalSettingsPageItem>
+        </section>
+        <section>
           <h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2>
           <LocalSettingsPageItem
             settings={settings}
@@ -74,53 +93,63 @@ export default class LocalSettingsPage extends React.PureComponent {
             <FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' />
           </LocalSettingsPageItem>
         </section>
-        <section>
-          <h2><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h2>
-          <LocalSettingsPageItem
-            settings={settings}
-            item={['always_show_spoilers_field']}
-            id='mastodon-settings--always_show_spoilers_field'
-            onChange={onChange}
-          >
-            <FormattedMessage id='settings.always_show_spoilers_field' defaultMessage='Always enable the Content Warning field' />
-          </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={[
-              { value: 'none', message: intl.formatMessage(messages.side_arm_none) },
-              { value: 'direct', message: intl.formatMessage({ id: 'privacy.direct.short' }) },
-              { value: 'private', message: intl.formatMessage({ id: 'privacy.private.short' }) },
-              { value: 'unlisted', message: intl.formatMessage({ id: 'privacy.unlisted.short' }) },
-              { value: 'public', message: intl.formatMessage({ id: 'privacy.public.short' }) },
-            ]}
-            onChange={onChange}
-          >
-            <FormattedMessage id='settings.side_arm' defaultMessage='Secondary toot button:' />
-          </LocalSettingsPageItem>
-          <LocalSettingsPageItem
-            settings={settings}
-            item={['side_arm_reply_mode']}
-            id='mastodon-settings--side_arm_reply_mode'
-            options={[
-              { value: 'keep', message: intl.formatMessage(messages.side_arm_keep) },
-              { value: 'copy', message: intl.formatMessage(messages.side_arm_copy) },
-              { value: 'restrict', message: intl.formatMessage(messages.side_arm_restrict) },
-            ]}
-            onChange={onChange}
-          >
-            <FormattedMessage id='settings.side_arm_reply_mode' defaultMessage='When replying to a toot:' />
-          </LocalSettingsPageItem>
-        </section>
+      </div>
+    ),
+    ({ intl, onChange, settings }) => (
+      <div className='glitch local-settings__page compose_box_opts'>
+        <h1><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h1>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['always_show_spoilers_field']}
+          id='mastodon-settings--always_show_spoilers_field'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.always_show_spoilers_field' defaultMessage='Always enable the Content Warning field' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['preselect_on_reply']}
+          id='mastodon-settings--preselect_on_reply'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.preselect_on_reply' defaultMessage='Pre-select usernames past the first when replying to a toot with multiple participants' />
+        </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={[
+            { value: 'none', message: intl.formatMessage(messages.side_arm_none) },
+            { value: 'direct', message: intl.formatMessage({ id: 'privacy.direct.short' }) },
+            { value: 'private', message: intl.formatMessage({ id: 'privacy.private.short' }) },
+            { value: 'unlisted', message: intl.formatMessage({ id: 'privacy.unlisted.short' }) },
+            { value: 'public', message: intl.formatMessage({ id: 'privacy.public.short' }) },
+          ]}
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.side_arm' defaultMessage='Secondary toot button:' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['side_arm_reply_mode']}
+          id='mastodon-settings--side_arm_reply_mode'
+          options={[
+            { value: 'keep', message: intl.formatMessage(messages.side_arm_keep) },
+            { value: 'copy', message: intl.formatMessage(messages.side_arm_copy) },
+            { value: 'restrict', message: intl.formatMessage(messages.side_arm_restrict) },
+          ]}
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.side_arm_reply_mode' defaultMessage='When replying to a toot:' />
+        </LocalSettingsPageItem>
       </div>
     ),
     ({ intl, onChange, settings }) => (
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/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js
index 4c05b24fe..13ed26865 100644
--- a/app/javascript/flavours/glitch/features/notifications/index.js
+++ b/app/javascript/flavours/glitch/features/notifications/index.js
@@ -8,6 +8,8 @@ import {
   enterNotificationClearingMode,
   expandNotifications,
   scrollTopNotifications,
+  mountNotifications,
+  unmountNotifications,
 } from 'flavours/glitch/actions/notifications';
 import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import NotificationContainer from './containers/notification_container';
@@ -42,6 +44,12 @@ const mapDispatchToProps = dispatch => ({
   onEnterCleaningMode(yes) {
     dispatch(enterNotificationClearingMode(yes));
   },
+  onMount() {
+    dispatch(mountNotifications());
+  },
+  onUnmount() {
+    dispatch(unmountNotifications());
+  },
   dispatch,
 });
 
@@ -62,6 +70,8 @@ export default class Notifications extends React.PureComponent {
     localSettings: ImmutablePropTypes.map,
     notifCleaningActive: PropTypes.bool,
     onEnterCleaningMode: PropTypes.func,
+    onMount: PropTypes.func,
+    onUnmount: PropTypes.func,
   };
 
   static defaultProps = {
@@ -126,6 +136,20 @@ export default class Notifications extends React.PureComponent {
     }
   }
 
+  componentDidMount () {
+    const { onMount } = this.props;
+    if (onMount) {
+      onMount();
+    }
+  }
+
+  componentWillUnmount () {
+    const { onUnmount } = this.props;
+    if (onUnmount) {
+      onUnmount();
+    }
+  }
+
   render () {
     const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props;
     const pinned = !!columnId;
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 af86b22ac..53f2836f1 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 d4ef3ce48..2b67e836a 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 413833a79..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
@@ -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/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js
index 680bf63ab..b52f3c4fa 100644
--- a/app/javascript/flavours/glitch/features/status/components/card.js
+++ b/app/javascript/flavours/glitch/features/status/components/card.js
@@ -20,6 +20,39 @@ const getHostname = url => {
   return parser.hostname;
 };
 
+const trim = (text, len) => {
+  const cut = text.indexOf(' ', len);
+
+  if (cut === -1) {
+    return text;
+  }
+
+  return text.substring(0, cut) + (text.length > len ? '…' : '');
+};
+
+const domParser = new DOMParser();
+
+const addAutoPlay = html => {
+  const document = domParser.parseFromString(html, 'text/html').documentElement;
+  const iframe = document.querySelector('iframe');
+
+  if (iframe) {
+    if (iframe.src.indexOf('?') !== -1) {
+      iframe.src += '&';
+    } else {
+      iframe.src += '?';
+    }
+
+    iframe.src += 'autoplay=1&auto_play=1';
+
+    // DOM parser creates html/body elements around original HTML fragment,
+    // so we need to get innerHTML out of the body and not the entire document
+    return document.querySelector('body').innerHTML;
+  }
+
+  return html;
+};
+
 export default class Card extends React.PureComponent {
 
   static propTypes = {
@@ -33,9 +66,16 @@ export default class Card extends React.PureComponent {
   };
 
   state = {
-    width: 0,
+    width: 280,
+    embedded: false,
   };
 
+  componentWillReceiveProps (nextProps) {
+    if (this.props.card !== nextProps.card) {
+      this.setState({ embedded: false });
+    }
+  }
+
   handlePhotoClick = () => {
     const { card, onOpenMedia } = this.props;
 
@@ -43,7 +83,7 @@ export default class Card extends React.PureComponent {
       Immutable.fromJS([
         {
           type: 'image',
-          url: card.get('url'),
+          url: card.get('embed_url'),
           description: card.get('title'),
           meta: {
             original: {
@@ -57,56 +97,14 @@ export default class Card extends React.PureComponent {
     );
   };
 
-  renderLink () {
-    const { card, maxDescription } = this.props;
-
-    let image    = '';
-    let provider = card.get('provider_name');
-
-    if (card.get('image')) {
-      image = (
-        <div className='status-card__image'>
-          <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} />
-        </div>
-      );
-    }
-
-    if (provider.length < 1) {
-      provider = decodeIDNA(getHostname(card.get('url')));
-    }
-
-    const className = classnames('status-card', {
-      'horizontal': card.get('width') > card.get('height'),
-    });
-
-    return (
-      <a href={card.get('url')} className={className} target='_blank' rel='noopener'>
-        {image}
-
-        <div className='status-card__content'>
-          <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
-          <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>
-          <span className='status-card__host'>{provider}</span>
-        </div>
-      </a>
-    );
-  }
-
-  renderPhoto () {
+  handleEmbedClick = () => {
     const { card } = this.props;
 
-    return (
-      <img
-        className='status-card-photo'
-        onClick={this.handlePhotoClick}
-        role='button'
-        tabIndex='0'
-        src={card.get('url')}
-        alt={card.get('title')}
-        width={card.get('width')}
-        height={card.get('height')}
-      />
-    );
+    if (card.get('type') === 'photo') {
+      this.handlePhotoClick();
+    } else {
+      this.setState({ embedded: true });
+    }
   }
 
   setRef = c => {
@@ -117,7 +115,7 @@ export default class Card extends React.PureComponent {
 
   renderVideo () {
     const { card }  = this.props;
-    const content   = { __html: card.get('html') };
+    const content   = { __html: addAutoPlay(card.get('html')) };
     const { width } = this.state;
     const ratio     = card.get('width') / card.get('height');
     const height    = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
@@ -125,7 +123,7 @@ export default class Card extends React.PureComponent {
     return (
       <div
         ref={this.setRef}
-        className='status-card-video'
+        className='status-card__image status-card-video'
         dangerouslySetInnerHTML={content}
         style={{ height }}
       />
@@ -133,23 +131,76 @@ export default class Card extends React.PureComponent {
   }
 
   render () {
-    const { card } = this.props;
+    const { card, maxDescription } = this.props;
+    const { width, embedded }      = this.state;
 
     if (card === null) {
       return null;
     }
 
-    switch(card.get('type')) {
-    case 'link':
-      return this.renderLink();
-    case 'photo':
-      return this.renderPhoto();
-    case 'video':
-      return this.renderVideo();
-    case 'rich':
-    default:
-      return null;
+    const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
+    const horizontal  = card.get('width') > card.get('height') && (card.get('width') + 100 >= width) || card.get('type') !== 'link';
+    const className   = classnames('status-card', { horizontal });
+    const interactive = card.get('type') !== 'link';
+    const title       = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
+    const ratio       = card.get('width') / card.get('height');
+    const height      = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
+
+    const description = (
+      <div className='status-card__content'>
+        {title}
+        {!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
+        <span className='status-card__host'>{provider}</span>
+      </div>
+    );
+
+    let embed     = '';
+    let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />;
+
+    if (interactive) {
+      if (embedded) {
+        embed = this.renderVideo();
+      } else {
+        let iconVariant = 'play';
+
+        if (card.get('type') === 'photo') {
+          iconVariant = 'search-plus';
+        }
+
+        embed = (
+          <div className='status-card__image'>
+            {thumbnail}
+
+            <div className='status-card__actions'>
+              <div>
+                <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button>
+                <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>
+              </div>
+            </div>
+          </div>
+        );
+      }
+
+      return (
+        <div className={className} ref={this.setRef}>
+          {embed}
+          {description}
+        </div>
+      );
+    } else if (card.get('image')) {
+      embed = (
+        <div className='status-card__image'>
+          {thumbnail}
+        </div>
+      );
     }
+
+    return (
+      <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
+        {embed}
+        {description}
+      </a>
+    );
   }
 
 }
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index ee9eb02c6..2e61e6d8c 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -77,6 +77,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
             sensitive={status.get('sensitive')}
             media={status.get('media_attachments')}
             letterbox={settings.getIn(['media', 'letterbox'])}
+            fullwidth={settings.getIn(['media', 'fullwidth'])}
             onOpenMedia={this.props.onOpenMedia}
           />
         );
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index 0697b49ad..5759a575c 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -105,7 +105,7 @@ export default class Status extends ImmutablePureComponent {
     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) });
+      this.setState({ isExpanded: autoUnfoldCW(nextProps.settings, nextProps.status), threadExpanded: undefined });
     }
   }
 
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 8fde279c7..71cb7e8c9 100644
--- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
@@ -1,6 +1,6 @@
 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';
 
@@ -30,6 +30,12 @@ const componentMap = {
   'LIST': ListTimeline,
 };
 
+const shouldHideFAB = path => path.match(/^\/statuses\//);
+
+const messages = defineMessages({
+  publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
+});
+
 @component => injectIntl(component, { withRef: true })
 export default class ColumnsArea extends ImmutablePureComponent {
 
@@ -147,14 +153,14 @@ 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) {
-      const floatingActionButton = this.context.router.history.location.pathname === '/statuses/new' ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button'><i className='fa fa-pencil' /></Link>;
+      const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? 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%' }}>
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 d4fd45d4d..1f3ac18ea 100644
--- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js
@@ -129,7 +129,7 @@ export default class MediaModal extends ImmutablePureComponent {
             startTime={time || 0}
             onCloseVideo={onClose}
             detailed
-            description={image.get('description')}
+            alt={image.get('description')}
             key={image.get('url')}
           />
         );
diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
index 81643b6c2..a139394ac 100644
--- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
@@ -106,6 +106,7 @@ export default class ReportModal extends ImmutablePureComponent {
               onChange={this.handleCommentChange}
               onKeyDown={this.handleKeyDown}
               disabled={isSubmitting}
+              autoFocus
             />
 
             {domain && (
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 b2fee21e1..b44a21a42 100644
--- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
+++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
@@ -4,10 +4,34 @@ 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';
+import { connect } from 'react-redux';
+
+const mapStateToProps = state => ({
+  unreadNotifications: state.getIn(['notifications', 'unread']),
+  showBadge: state.getIn(['local_settings', 'notifications', 'tab_badge']),
+});
+
+@connect(mapStateToProps)
+class NotificationsIcon extends React.PureComponent {
+  static propTypes = {
+    unreadNotifications: PropTypes.number,
+    showBadge: PropTypes.bool,
+  };
+
+  render() {
+    const { unreadNotifications, showBadge } = this.props;
+    return (
+      <span className='icon-badge-wrapper'>
+        <i className='fa fa-fw fa-bell' />
+        { showBadge && unreadNotifications > 0 && <div className='icon-badge' />}
+      </span>
+    );
+  }
+}
 
 export const links = [
   <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 primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsIcon /><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>,
diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
index e0cb7fc09..69e0ee46e 100644
--- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
@@ -24,7 +24,7 @@ export default class VideoModal extends ImmutablePureComponent {
             startTime={time}
             onCloseVideo={onClose}
             detailed
-            description={media.get('description')}
+            alt={media.get('description')}
           />
         </div>
       </div>
diff --git a/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js b/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js
index 0a0a4d41a..3f6562bc9 100644
--- a/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js
+++ b/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js
@@ -137,6 +137,7 @@ export default class ZoomableImage extends React.PureComponent {
           role='presentation'
           ref={this.setImageRef}
           alt={alt}
+          title={alt}
           src={src}
           style={{
             transform: `scale(${scale})`,
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index 1cff94321..ecbac1f8f 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -10,13 +10,14 @@ import { isMobile } from 'flavours/glitch/util/is_mobile';
 import { debounce } from 'lodash';
 import { uploadCompose, resetCompose } from 'flavours/glitch/actions/compose';
 import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
-import { expandNotifications } from 'flavours/glitch/actions/notifications';
+import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications';
 import { fetchFilters } from 'flavours/glitch/actions/filters';
 import { clearHeight } from 'flavours/glitch/actions/height_cache';
 import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers';
 import UploadArea from './components/upload_area';
 import ColumnsAreaContainer from './containers/columns_area_container';
 import classNames from 'classnames';
+import Favico from 'favico.js';
 import {
   Drawer,
   Status,
@@ -59,11 +60,14 @@ const messages = defineMessages({
 });
 
 const mapStateToProps = state => ({
-  hasComposingText: state.getIn(['compose', 'text']) !== '',
+  hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
+  hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
   layout: state.getIn(['local_settings', 'layout']),
   isWide: state.getIn(['local_settings', 'stretch']),
   navbarUnder: state.getIn(['local_settings', 'navbar_under']),
   dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
+  unreadNotifications: state.getIn(['notifications', 'unread']),
+  showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
 });
 
 const keyMap = {
@@ -110,11 +114,14 @@ export default class UI extends React.Component {
     navbarUnder: PropTypes.bool,
     isComposing: PropTypes.bool,
     hasComposingText: PropTypes.bool,
+    hasMediaAttachments: PropTypes.bool,
     match: PropTypes.object.isRequired,
     location: PropTypes.object.isRequired,
     history: PropTypes.object.isRequired,
     intl: PropTypes.object.isRequired,
     dropdownMenuIsOpen: PropTypes.bool,
+    unreadNotifications: PropTypes.number,
+    showFaviconBadge: PropTypes.bool,
   };
 
   state = {
@@ -123,9 +130,9 @@ export default class UI extends React.Component {
   };
 
   handleBeforeUnload = (e) => {
-    const { intl, hasComposingText } = this.props;
+    const { intl, hasComposingText, hasMediaAttachments } = this.props;
 
-    if (hasComposingText) {
+    if (hasComposingText || hasMediaAttachments) {
       // Setting returnValue to any string causes confirmation dialog.
       // Many browsers no longer display this text to users,
       // but we set user-friendly message for other browsers, e.g. Edge.
@@ -206,7 +213,27 @@ export default class UI extends React.Component {
     }
   }
 
+  handleVisibilityChange = () => {
+    const visibility = !document[this.visibilityHiddenProp];
+    this.props.dispatch(notificationsSetVisibility(visibility));
+  }
+
   componentWillMount () {
+    if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
+      this.visibilityHiddenProp = 'hidden';
+      this.visibilityChange = 'visibilitychange';
+    } else if (typeof document.msHidden !== 'undefined') {
+      this.visibilityHiddenProp = 'msHidden';
+      this.visibilityChange = 'msvisibilitychange';
+    } else if (typeof document.webkitHidden !== 'undefined') {
+      this.visibilityHiddenProp = 'webkitHidden';
+      this.visibilityChange = 'webkitvisibilitychange';
+    }
+    if (this.visibilityChange !== undefined) {
+      document.addEventListener(this.visibilityChange, this.handleVisibilityChange, false);
+      this.handleVisibilityChange();
+    }
+
     window.addEventListener('beforeunload', this.handleBeforeUnload, false);
     window.addEventListener('resize', this.handleResize, { passive: true });
     document.addEventListener('dragenter', this.handleDragEnter, false);
@@ -219,6 +246,8 @@ export default class UI extends React.Component {
       navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
     }
 
+    this.favicon = new Favico({ animation:"none" });
+
     this.props.dispatch(expandHomeTimeline());
     this.props.dispatch(expandNotifications());
     setTimeout(() => this.props.dispatch(fetchFilters()), 500);
@@ -247,9 +276,19 @@ export default class UI extends React.Component {
     if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
       this.columnsAreaNode.handleChildrenContentChange();
     }
+    if (this.props.unreadNotifications != prevProps.unreadNotifications ||
+        this.props.showFaviconBadge != prevProps.showFaviconBadge) {
+      if (this.favicon) {
+        this.favicon.badge(this.props.showFaviconBadge ? this.props.unreadNotifications : 0);
+      }
+    }
   }
 
   componentWillUnmount () {
+    if (this.visibilityChange !== undefined) {
+      document.removeEventListener(this.visibilityChange, this.handleVisibilityChange);
+    }
+
     window.removeEventListener('beforeunload', this.handleBeforeUnload);
     window.removeEventListener('resize', this.handleResize);
     document.removeEventListener('dragenter', this.handleDragEnter);
diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js
index 7e284a0bc..5cbe01f26 100644
--- a/app/javascript/flavours/glitch/features/video/index.js
+++ b/app/javascript/flavours/glitch/features/video/index.js
@@ -101,6 +101,7 @@ export default class Video extends React.PureComponent {
     fullwidth: PropTypes.bool,
     detailed: PropTypes.bool,
     inline: PropTypes.bool,
+    preventPlayback: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
@@ -134,7 +135,10 @@ export default class Video extends React.PureComponent {
     this.seek = c;
   }
 
-  handleClickRoot = e => e.stopPropagation();
+  handleMouseDownRoot = e => {
+    e.preventDefault();
+    e.stopPropagation();
+  }
 
   handlePlay = () => {
     this.setState({ paused: false });
@@ -215,6 +219,12 @@ export default class Video extends React.PureComponent {
     document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
   }
 
+  componentDidUpdate (prevProps) {
+    if (this.video && this.state.revealed && this.props.preventPlayback && !prevProps.preventPlayback) {
+      this.video.pause();
+    }
+  }
+
   handleFullscreenChange = () => {
     this.setState({ fullscreen: isFullscreen() });
   }
@@ -254,11 +264,12 @@ export default class Video extends React.PureComponent {
   }
 
   handleOpenVideo = () => {
-    const { src, preview, width, height } = this.props;
+    const { src, preview, width, height, alt } = this.props;
     const media = fromJS({
       type: 'video',
       url: src,
       preview_url: preview,
+      description: alt,
       width,
       height,
     });
@@ -311,7 +322,7 @@ export default class Video extends React.PureComponent {
         ref={this.setPlayerRef}
         onMouseEnter={this.handleMouseEnter}
         onMouseLeave={this.handleMouseLeave}
-        onClick={this.handleClickRoot}
+        onMouseDown={this.handleMouseDownRoot}
         tabIndex={0}
       >
         <video