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.js25
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/components/header.js8
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js22
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/compose_form.js24
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/publisher.js24
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js9
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/list_adder/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/lists/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/follow.js12
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js7
-rw-r--r--app/javascript/flavours/glitch/features/status/components/action_bar.js17
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js53
-rw-r--r--app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js16
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js20
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/link_footer.js1
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/list_panel.js5
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/mute_modal.js27
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/report_modal.js6
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js6
20 files changed, 224 insertions, 73 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 2d4cc7f49..0f1b83b2d 100644
--- a/app/javascript/flavours/glitch/features/account/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js
@@ -44,14 +44,20 @@ class ActionBar extends React.PureComponent {
     if (account.get('acct') !== account.get('username')) {
       extraInfo = (
         <div className='account__disclaimer'>
-          <Icon id='info-circle' fixedWidth /> <FormattedMessage
-            id='account.disclaimer_full'
-            defaultMessage="Information below may reflect the user's profile incompletely."
-          />
-          {' '}
-          <a target='_blank' rel='noopener' href={account.get('url')}>
-            <FormattedMessage id='account.view_full_profile' defaultMessage='View full profile' />
-          </a>
+          <p>
+            <Icon id='info-circle' fixedWidth /> <FormattedMessage
+              id='account.disclaimer_full'
+              defaultMessage="Information below may reflect the user's profile incompletely."
+            />
+          </p>
+          <p>
+            <Icon id='link' fixedWidth /> <a target='_blank' rel='noopener' href={account.get('url')}>
+              <FormattedMessage
+                id='account.view_full_profile'
+                defaultMessage='View full profile'
+              />
+            </a>
+          </p>
         </div>
       );
     }
@@ -64,17 +70,14 @@ class ActionBar extends React.PureComponent {
           <div className='account__action-bar-links'>
             <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>
             </NavLink>
 
             <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>
             </NavLink>
 
             <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
               <FormattedMessage id='account.followers' defaultMessage='Followers' />
-              <strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong>
             </NavLink>
           </div>
         </div>
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
index a6b57d331..d399d4aa9 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
@@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import { FormattedMessage } from 'react-intl';
 import { NavLink } from 'react-router-dom';
 import MovedNote from './moved_note';
+import { me } from 'flavours/glitch/util/initial_state';
 
 export default class Header extends ImmutablePureComponent {
 
@@ -128,9 +129,12 @@ export default class Header extends ImmutablePureComponent {
 
         {!hideTabs && (
           <div className='account__section-headline'>
-            <NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
-            <NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
+            <NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.threads' defaultMessage='Threads' /></NavLink>
+            { (account.get('id') === me || account.get('show_replies')) &&
+                (<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>) }
+            { (account.get('id') !== me) && (<NavLink exact to={`/accounts/${account.get('id')}/mentions`}><FormattedMessage id='account.mentions' defaultMessage='Mentions' /></NavLink>) }
             <NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
+            <NavLink exact to={`/accounts/${account.get('id')}/reblogs`}><FormattedMessage id='account.reblogs' defaultMessage='Boosts' /></NavLink>
           </div>
         )}
       </div>
diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js
index c56cc9b8e..c88f6ac89 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/index.js
@@ -19,15 +19,15 @@ import TimelineHint from 'flavours/glitch/components/timeline_hint';
 
 const emptyList = ImmutableList();
 
-const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
-  const path = withReplies ? `${accountId}:with_replies` : accountId;
+const mapStateToProps = (state, { params: { accountId }, filter = '' }) => {
+  const path = `${accountId}${filter}`;
 
   return {
     remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
     remoteUrl: state.getIn(['accounts', accountId, 'url']),
     isAccount: !!state.getIn(['accounts', accountId]),
     statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
-    featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
+    featuredStatusIds: !filter ? state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()) : ImmutableList(),
     isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
     hasMore:   state.getIn(['timelines', `account:${path}`, 'hasMore']),
     suspended: state.getIn(['accounts', accountId, 'suspended'], false),
@@ -52,7 +52,7 @@ class AccountTimeline extends ImmutablePureComponent {
     featuredStatusIds: ImmutablePropTypes.list,
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
-    withReplies: PropTypes.bool,
+    filter: PropTypes.string,
     isAccount: PropTypes.bool,
     suspended: PropTypes.bool,
     remote: PropTypes.bool,
@@ -61,24 +61,24 @@ class AccountTimeline extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    const { params: { accountId }, withReplies } = this.props;
+    const { params: { accountId }, filter } = this.props;
 
     this.props.dispatch(fetchAccount(accountId));
     this.props.dispatch(fetchAccountIdentityProofs(accountId));
-    if (!withReplies) {
+    if (!filter) {
       this.props.dispatch(expandAccountFeaturedTimeline(accountId));
     }
-    this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
+    this.props.dispatch(expandAccountTimeline(accountId, { filter }));
   }
 
   componentWillReceiveProps (nextProps) {
-    if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
+    if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.filter !== this.props.filter) {
       this.props.dispatch(fetchAccount(nextProps.params.accountId));
       this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
-      if (!nextProps.withReplies) {
+      if (!nextProps.filter) {
         this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
       }
-      this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
+      this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { filter: nextProps.params.filter }));
     }
   }
 
@@ -87,7 +87,7 @@ class AccountTimeline extends ImmutablePureComponent {
   }
 
   handleLoadMore = maxId => {
-    this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies }));
+    this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, filter: this.props.filter }));
   }
 
   setRef = c => {
diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
index a7cb95222..e812ba982 100644
--- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
@@ -63,6 +63,8 @@ class ComposeForm extends ImmutablePureComponent {
     layout: PropTypes.string,
     media: ImmutablePropTypes.list,
     sideArm: PropTypes.string,
+    sideArmWarning: PropTypes.bool,
+    privacyWarning: PropTypes.bool,
     sensitive: PropTypes.bool,
     spoilersAlwaysOn: PropTypes.bool,
     mediaDescriptionConfirmation: PropTypes.bool,
@@ -71,10 +73,12 @@ class ComposeForm extends ImmutablePureComponent {
     onChangeVisibility: PropTypes.func,
     onPaste: PropTypes.func,
     onMediaDescriptionConfirm: PropTypes.func,
+    clearTimeout: PropTypes.bool,
   };
 
   static defaultProps = {
     showSearch: false,
+    clearTimeout: null,
   };
 
   handleChange = (e) => {
@@ -149,6 +153,17 @@ class ComposeForm extends ImmutablePureComponent {
     this.handleSubmit(sideArm === 'none' ? null : sideArm);
   }
 
+  handleClearAll = () => {
+    if(!this.clearTimeout || this.clearTimeout === null) {
+      this.clearTimeout = window.setTimeout(() => {
+        this.clearTimeout = null;
+      }, 500);
+    } else {
+      this.clearTimeout = null;
+      this.props.onClearAll();
+    }
+  }
+
   //  Selects a suggestion from the autofill.
   onSuggestionSelected = (tokenStart, token, value) => {
     this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
@@ -256,6 +271,7 @@ class ComposeForm extends ImmutablePureComponent {
       handleSecondarySubmit,
       handleSelect,
       handleSubmit,
+      handleClearAll,
       handleRefTextarea,
     } = this;
     const {
@@ -273,19 +289,22 @@ class ComposeForm extends ImmutablePureComponent {
       onFetchSuggestions,
       onPaste,
       privacy,
+      privacyWarning,
       sensitive,
       showSearch,
       sideArm,
+      sideArmWarning,
       spoiler,
       spoilerText,
       suggestions,
       text,
       spoilersAlwaysOn,
+      clearTimeout,
     } = this.props;
 
     let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);
 
-    const countText = `${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`;
+    const countText = `${spoilerText}${countableText(text)}`;
 
     return (
       <div className='composer'>
@@ -356,8 +375,11 @@ class ComposeForm extends ImmutablePureComponent {
           disabled={disabledButton}
           onSecondarySubmit={handleSecondarySubmit}
           onSubmit={handleSubmit}
+          onClearAll={handleClearAll}
           privacy={privacy}
+          privacyWarning={privacyWarning}
           sideArm={sideArm}
+          sideArmWarning={sideArmWarning}
         />
       </div>
     );
diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js
index 97890f40d..d42c578aa 100644
--- a/app/javascript/flavours/glitch/features/compose/components/publisher.js
+++ b/app/javascript/flavours/glitch/features/compose/components/publisher.js
@@ -23,6 +23,10 @@ const messages = defineMessages({
     defaultMessage: '{publish}!',
     id: 'compose_form.publish_loud',
   },
+  clear: {
+    defaultMessage: 'Double-click to clear',
+    id: 'compose_form.clear',
+  },
 });
 
 export default @injectIntl
@@ -34,8 +38,11 @@ class Publisher extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
     onSecondarySubmit: PropTypes.func,
     onSubmit: PropTypes.func,
+    onClearAll: PropTypes.func,
     privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
+    privacyWarning: PropTypes.bool,
     sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
+    sideArmWarning: PropTypes.bool,
   };
 
   handleSubmit = () => {
@@ -43,7 +50,7 @@ class Publisher extends ImmutablePureComponent {
   };
 
   render () {
-    const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props;
+    const { countText, disabled, intl, onClearAll, onSecondarySubmit, privacy, privacyWarning, sideArm, sideArmWarning } = this.props;
 
     const diff = maxChars - length(countText || '');
     const computedClass = classNames('composer--publisher', {
@@ -53,9 +60,20 @@ class Publisher extends ImmutablePureComponent {
 
     return (
       <div className={computedClass}>
+        <Button
+          className='clear'
+          onClick={onClearAll}
+          style={{ padding: null }}
+          title={intl.formatMessage(messages.clear)}
+          text={
+            <span>
+              <Icon id='trash-o' />
+            </span>
+          }
+        />
         {sideArm && sideArm !== 'none' ? (
           <Button
-            className='side_arm'
+            className={classNames('side_arm', {privacy_warning: sideArmWarning})}
             disabled={disabled || diff < 0}
             onClick={onSecondarySubmit}
             style={{ padding: null }}
@@ -75,7 +93,7 @@ class Publisher extends ImmutablePureComponent {
           />
         ) : null}
         <Button
-          className='primary'
+          className={classNames('primary', {privacy_warning: privacyWarning})}
           text={function () {
             switch (true) {
             case !!sideArm && sideArm !== 'none':
diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
index fcd2caf1b..cf953ec3d 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
@@ -12,13 +12,14 @@ import {
   selectComposeSuggestion,
   submitCompose,
   uploadCompose,
+  resetCompose,
 } from 'flavours/glitch/actions/compose';
 import {
   openModal,
 } from 'flavours/glitch/actions/modal';
 import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
 
-import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
+import { privacyPreference, order as privacyOrder } from 'flavours/glitch/util/privacy_preference';
 
 const messages = defineMessages({
   missingDescriptionMessage: {  id: 'confirmations.missing_media_description.message',
@@ -57,7 +58,9 @@ function mapStateToProps (state) {
     media: state.getIn(['compose', 'media_attachments']),
     preselectDate: state.getIn(['compose', 'preselectDate']),
     privacy: state.getIn(['compose', 'privacy']),
+    privacyWarning: replyPrivacy && privacyOrder.indexOf(state.getIn(['compose', 'privacy'])) < privacyOrder.indexOf(replyPrivacy),
     sideArm: sideArmPrivacy,
+    sideArmWarning: sideArmPrivacy && sideArmRestrictedPrivacy && privacyOrder.indexOf(sideArmPrivacy) < privacyOrder.indexOf(sideArmRestrictedPrivacy),
     sensitive: state.getIn(['compose', 'sensitive']),
     showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
     spoiler: spoilersAlwaysOn || state.getIn(['compose', 'spoiler']),
@@ -82,6 +85,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(submitCompose(routerHistory));
   },
 
+  onClearAll() {
+    dispatch(resetCompose());
+  },
+
   onClearSuggestions() {
     dispatch(clearComposeSuggestions());
   },
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js
index b4549fdf8..43d535ac5 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started/index.js
@@ -46,7 +46,10 @@ const makeMapStateToProps = () => {
       return lists;
     }
 
-    return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+    return lists.toList().filter(item => !!item).sort((a, b) => {
+      const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0);
+      return r === 0 ? a.get('title').localeCompare(b.get('title')) : r;
+    });
   });
 
   const mapStateToProps = state => ({
diff --git a/app/javascript/flavours/glitch/features/list_adder/index.js b/app/javascript/flavours/glitch/features/list_adder/index.js
index cb8a15e8c..b7f3d1ef7 100644
--- a/app/javascript/flavours/glitch/features/list_adder/index.js
+++ b/app/javascript/flavours/glitch/features/list_adder/index.js
@@ -16,7 +16,10 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
     return lists;
   }
 
-  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+  return lists.toList().filter(item => !!item).sort((a, b) => {
+    const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0);
+    return r === 0 ? a.get('title').localeCompare(b.get('title')) : r;
+  });
 });
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/flavours/glitch/features/lists/index.js b/app/javascript/flavours/glitch/features/lists/index.js
index e384f301b..3863b8e25 100644
--- a/app/javascript/flavours/glitch/features/lists/index.js
+++ b/app/javascript/flavours/glitch/features/lists/index.js
@@ -24,7 +24,10 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
     return lists;
   }
 
-  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+  return lists.toList().filter(item => !!item).sort((a, b) => {
+    const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0);
+    return r === 0 ? a.get('title').localeCompare(b.get('title')) : r;
+  });
 });
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js
index 0d3162fc9..7b47f411b 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/follow.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js
@@ -84,11 +84,13 @@ export default class NotificationFollow extends ImmutablePureComponent {
               <Icon fixedWidth id='user-plus' />
             </div>
 
-            <FormattedMessage
-              id='notification.follow'
-              defaultMessage='{name} followed you'
-              values={{ name: link }}
-            />
+            <span title={notification.get('created_at')}>
+              <FormattedMessage
+                id='notification.follow'
+                defaultMessage='{name} followed you'
+                values={{ name: link }}
+              />
+            </span>
           </div>
 
           <AccountContainer hidden={hidden} id={account.get('id')} withNote={false} />
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js
index 73fc05dea..4cac53266 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js
@@ -20,6 +20,10 @@ class NotificationsPermissionBanner extends React.PureComponent {
     intl: PropTypes.object.isRequired,
   };
 
+  handleClickDisable = () => {
+    window.location = '/settings/preferences/notifications';
+  }
+
   handleClick = () => {
     this.props.dispatch(requestBrowserPermission());
   }
@@ -39,7 +43,8 @@ class NotificationsPermissionBanner extends React.PureComponent {
 
         <h2><FormattedMessage id='notifications_permission_banner.title' defaultMessage='Never miss a thing' /></h2>
         <p><FormattedMessage id='notifications_permission_banner.how_to_control' defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled." values={{ icon: <Icon id='sliders' /> }} /></p>
-        <Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Enable desktop notifications' /></Button>
+        <Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Request' /></Button>
+        <Button onClick={this.handleClickDisable}><FormattedMessage id='notifications_permission_banner.disable' defaultMessage='Disable' /></Button>
       </div>
     );
   }
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 0f16d93fe..b2c8ac87f 100644
--- a/app/javascript/flavours/glitch/features/status/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js
@@ -11,6 +11,8 @@ import classNames from 'classnames';
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
   redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
+  edit: { id: 'status.edit', defaultMessage: 'Edit' },
+  publish: { id: 'status.publish', defaultMessage: 'Publish' },
   direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
   mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
   reply: { id: 'status.reply', defaultMessage: 'Reply' },
@@ -52,6 +54,8 @@ class ActionBar extends React.PureComponent {
     onMuteConversation: PropTypes.func,
     onBlock: PropTypes.func,
     onDelete: PropTypes.func.isRequired,
+    onEdit: PropTypes.func.isRequired,
+    onPublish: PropTypes.func.isRequired,
     onDirect: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
     onReport: PropTypes.func,
@@ -84,6 +88,14 @@ class ActionBar extends React.PureComponent {
     this.props.onDelete(this.props.status, this.context.router.history, true);
   }
 
+  handleEditClick = () => {
+    this.props.onEdit(this.props.status, this.context.router.history);
+  }
+
+  handlePublishClick = () => {
+    this.props.onPublish(this.props.status);
+  }
+
   handleDirectClick = () => {
     this.props.onDirect(this.props.status.get('account'), this.context.router.history);
   }
@@ -166,6 +178,11 @@ class ActionBar extends React.PureComponent {
       menu.push(null);
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
       menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
+      menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
+
+      if (status.get('published') === false) {
+        menu.push({ text: intl.formatMessage(messages.publish), action: this.handlePublishClick });
+      }
     } else {
       menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
       menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
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 04d350bcb..d03a425eb 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -17,7 +17,7 @@ import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
 import classNames from 'classnames';
 import PollContainer from 'flavours/glitch/containers/poll_container';
 import Icon from 'flavours/glitch/components/icon';
-import AnimatedNumber from 'flavours/glitch/components/animated_number';
+import { me } from 'flavours/glitch/util/initial_state';
 import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
 
 export default class DetailedStatus extends ImmutablePureComponent {
@@ -200,7 +200,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
       applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
     }
 
-    const visibilityLink = <React.Fragment> · <VisibilityIcon visibility={status.get('visibility')} /></React.Fragment>;
+    const visibilityLink = <React.Fragment><VisibilityIcon visibility={status.get('visibility')} /> · </React.Fragment>;
 
     if (status.get('visibility') === 'direct') {
       reblogIcon = 'envelope';
@@ -208,7 +208,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
       reblogIcon = 'lock';
     }
 
-    if (!['unlisted', 'public'].includes(status.get('visibility'))) {
+    if (status.getIn(['account', 'id']) !== me || !['unlisted', 'public'].includes(status.get('visibility'))) {
       reblogLink = null;
     } else if (this.context.router) {
       reblogLink = (
@@ -216,9 +216,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
           <React.Fragment> · </React.Fragment>
           <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
             <Icon id={reblogIcon} />
-            <span className='detailed-status__reblogs'>
-              <AnimatedNumber value={status.get('reblogs_count')} />
-            </span>
           </Link>
         </React.Fragment>
       );
@@ -228,37 +225,43 @@ export default class DetailedStatus extends ImmutablePureComponent {
           <React.Fragment> · </React.Fragment>
           <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
             <Icon id={reblogIcon} />
-            <span className='detailed-status__reblogs'>
-              <AnimatedNumber value={status.get('reblogs_count')} />
-            </span>
           </a>
         </React.Fragment>
       );
     }
 
-    if (this.context.router) {
+    if (status.getIn(['account', 'id']) !== me) {
+      favouriteLink = null;
+    } else if (this.context.router) {
       favouriteLink = (
-        <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
-          <Icon id='star' />
-          <span className='detailed-status__favorites'>
-            <AnimatedNumber value={status.get('favourites_count')} />
-          </span>
-        </Link>
+        <React.Fragment>
+          <React.Fragment> · </React.Fragment>
+          <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
+            <Icon id='star' />
+          </Link>
+        </React.Fragment>
       );
     } else {
       favouriteLink = (
-        <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
-          <Icon id='star' />
-          <span className='detailed-status__favorites'>
-            <AnimatedNumber value={status.get('favourites_count')} />
-          </span>
-        </a>
+        <React.Fragment>
+          <React.Fragment> · </React.Fragment>
+          <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
+            <Icon id='star' />
+          </a>
+        </React.Fragment>
       );
     }
 
+    const selectorAttribs = {
+      'data-status-by': `@${status.getIn(['account', 'acct'])}`,
+      'data-nest-level': status.get('nest_level'),
+      'data-nest-deep': status.get('nest_level') >= 15,
+      'data-local-only': !!status.get('local_only'),
+    };
+
     return (
       <div style={outerStyle}>
-        <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })} data-status-by={status.getIn(['account', 'acct'])}>
+        <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact, unpublished: status.get('published') === false })} {...selectorAttribs}>
           <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
             <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
             <DisplayName account={status.get('account')} localDomain={this.props.domain} />
@@ -275,13 +278,15 @@ export default class DetailedStatus extends ImmutablePureComponent {
             onUpdate={this.handleChildUpdate}
             tagLinks={settings.get('tag_misleading_links')}
             rewriteMentions={settings.get('rewrite_mentions')}
+            article
             disabled
           />
 
           <div className='detailed-status__meta'>
+            {visibilityLink}
             <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
               <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
-            </a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
+            </a>{applicationLink}{reblogLink}{favouriteLink}
           </div>
         </div>
       </div>
diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
index 9d11f37e0..124de903a 100644
--- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
+++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
@@ -17,7 +17,9 @@ import {
 import {
   muteStatus,
   unmuteStatus,
+  editStatus,
   deleteStatus,
+  publishStatus,
   hideStatus,
   revealStatus,
 } from 'flavours/glitch/actions/statuses';
@@ -34,6 +36,8 @@ const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
   redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
   redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
+  publishConfirm: { id: 'confirmations.publish.confirm', defaultMessage: 'Publish' },
+  publishMessage: { id: 'confirmations.publish.message', defaultMessage: 'Are you ready to publish your post?' },
   replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
   replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
 });
@@ -118,6 +122,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
+  onEdit (status, history) {
+    dispatch(editStatus(status, history));
+  },
+
+  onPublish (status) {
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.publishMessage),
+      confirm: intl.formatMessage(messages.publishConfirm),
+      onConfirm: () => dispatch(publishStatus(status.get('id'))),
+    }));
+  },
+
   onDirect (account, router) {
     dispatch(directCompose(account, router));
   },
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index b330adf3f..74d51f8f8 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -26,7 +26,7 @@ import {
   directCompose,
 } from 'flavours/glitch/actions/compose';
 import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
-import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
+import { muteStatus, unmuteStatus, deleteStatus, editStatus, publishStatus } from 'flavours/glitch/actions/statuses';
 import { initMuteModal } from 'flavours/glitch/actions/mutes';
 import { initBlockModal } from 'flavours/glitch/actions/blocks';
 import { initReport } from 'flavours/glitch/actions/reports';
@@ -50,6 +50,8 @@ const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
   redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
   redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
+  publishConfirm: { id: 'confirmations.publish.confirm', defaultMessage: 'Publish' },
+  publishMessage: { id: 'confirmations.publish.message', defaultMessage: 'Are you ready to publish your post?' },
   revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
   hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
   detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
@@ -306,6 +308,20 @@ class Status extends ImmutablePureComponent {
     }
   }
 
+  handleEditClick = (status, history) => {
+    this.props.dispatch(editStatus(status, history));
+  }
+
+  handlePublishClick = (status) => {
+    const { dispatch, intl } = this.props;
+
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.publishMessage),
+      confirm: intl.formatMessage(messages.publishConfirm),
+      onConfirm: () => dispatch(publishStatus(status.get('id'))),
+    }));
+  }
+
   handleDirectClick = (account, router) => {
     this.props.dispatch(directCompose(account, router));
   }
@@ -591,6 +607,8 @@ class Status extends ImmutablePureComponent {
                   onReblog={this.handleReblogClick}
                   onBookmark={this.handleBookmarkClick}
                   onDelete={this.handleDeleteClick}
+                  onEdit={this.handleEditClick}
+                  onPublish={this.handlePublishClick}
                   onDirect={this.handleDirectClick}
                   onMention={this.handleMentionClick}
                   onMute={this.handleMuteClick}
diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.js b/app/javascript/flavours/glitch/features/ui/components/link_footer.js
index 4d7fc36c2..f8a61d2fb 100644
--- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js
+++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js
@@ -60,6 +60,7 @@ class LinkFooter extends React.PureComponent {
             id='getting_started.open_source_notice'
             defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.'
             values={{
+              monsterware: <span><a href='https://monsterware.dev/monsterpit/monsterpit-mastodon' rel='noopener noreferrer' target='_blank'>MonsterWare</a></span>,
               github: <span><a href='https://github.com/glitch-soc/mastodon' rel='noopener noreferrer' target='_blank'>glitch-soc/mastodon</a> (v{version})</span>,
               Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener noreferrer' target='_blank'>Mastodon</a> }}
           />
diff --git a/app/javascript/flavours/glitch/features/ui/components/list_panel.js b/app/javascript/flavours/glitch/features/ui/components/list_panel.js
index 354e35027..f351e2a01 100644
--- a/app/javascript/flavours/glitch/features/ui/components/list_panel.js
+++ b/app/javascript/flavours/glitch/features/ui/components/list_panel.js
@@ -13,7 +13,10 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
     return lists;
   }
 
-  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
+  return lists.toList().filter(item => !!item).sort((a, b) => {
+    const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0);
+    return r === 0 ? a.get('title').localeCompare(b.get('title')) : r;
+  });
 });
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
index 7d25db316..5970ceddb 100644
--- a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
@@ -7,6 +7,7 @@ import Button from 'flavours/glitch/components/button';
 import { closeModal } from 'flavours/glitch/actions/modal';
 import { muteAccount } from 'flavours/glitch/actions/accounts';
 import { toggleHideNotifications, changeMuteDuration } from 'flavours/glitch/actions/mutes';
+import { toggleTimelinesOnly } from 'flavours/glitch/actions/mutes';
 
 const messages = defineMessages({
   minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
@@ -20,13 +21,14 @@ const mapStateToProps = state => {
     account: state.getIn(['mutes', 'new', 'account']),
     notifications: state.getIn(['mutes', 'new', 'notifications']),
     muteDuration: state.getIn(['mutes', 'new', 'duration']),
+    timelinesOnly: state.getIn(['mutes', 'new', 'timelines_only']),
   };
 };
 
 const mapDispatchToProps = dispatch => {
   return {
-    onConfirm(account, notifications, muteDuration) {
-      dispatch(muteAccount(account.get('id'), notifications, muteDuration));
+    onConfirm(account, notifications, timelinesOnly, muteDuration) {
+      dispatch(muteAccount(account.get('id'), notifications, timelinesOnly, muteDuration));
     },
 
     onClose() {
@@ -40,6 +42,10 @@ const mapDispatchToProps = dispatch => {
     onChangeMuteDuration(e) {
       dispatch(changeMuteDuration(e.target.value));
     },
+
+    onToggleTimelinesOnly() {
+      dispatch(toggleTimelinesOnly());
+    },
   };
 };
 
@@ -50,9 +56,11 @@ class MuteModal extends React.PureComponent {
   static propTypes = {
     account: PropTypes.object.isRequired,
     notifications: PropTypes.bool.isRequired,
+    timelinesOnly: PropTypes.bool.isRequired,
     onClose: PropTypes.func.isRequired,
     onConfirm: PropTypes.func.isRequired,
     onToggleNotifications: PropTypes.func.isRequired,
+    onTimelinesOnly: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
     muteDuration: PropTypes.number.isRequired,
     onChangeMuteDuration: PropTypes.func.isRequired,
@@ -64,7 +72,7 @@ class MuteModal extends React.PureComponent {
 
   handleClick = () => {
     this.props.onClose();
-    this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration);
+    this.props.onConfirm(this.props.account, this.props.notifications, this.props.timelinesOnly, this.props.muteDuration);
   }
 
   handleCancel = () => {
@@ -83,8 +91,12 @@ class MuteModal extends React.PureComponent {
     this.props.onChangeMuteDuration(e);
   }
 
+  toggleTimelinesOnly = () => {
+    this.props.onToggleTimelinesOnly();
+  }
+
   render () {
-    const { account, notifications, muteDuration, intl } = this.props;
+    const { account, notifications, timelinesOnly, muteDuration, intl } = this.props;
 
     return (
       <div className='modal-root__modal mute-modal'>
@@ -109,6 +121,13 @@ class MuteModal extends React.PureComponent {
             </label>
           </div>
           <div>
+            <label htmlFor='mute-modal__timelines-only-checkbox'>
+              <FormattedMessage id='mute_modal.timelines_only' defaultMessage='Hide from timelines only?' />
+              {' '}
+              <Toggle id='mute-modal__timelines-only-checkbox' checked={timelinesOnly} onChange={this.toggleTimelinesOnly} />
+            </label>
+          </div>
+          <div>
             <span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
 
             {/* eslint-disable-next-line jsx-a11y/no-onchange */}
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 9016b08d7..7473cfbe0 100644
--- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
@@ -30,7 +30,7 @@ const makeMapStateToProps = () => {
       account: getAccount(state, accountId),
       comment: state.getIn(['reports', 'new', 'comment']),
       forward: state.getIn(['reports', 'new', 'forward']),
-      statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
+      statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
     };
   };
 
@@ -70,12 +70,12 @@ class ReportModal extends ImmutablePureComponent {
   }
 
   componentDidMount () {
-    this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true }));
+    this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { filter: ':replies' }));
   }
 
   componentWillReceiveProps (nextProps) {
     if (this.props.account !== nextProps.account && nextProps.account) {
-      this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true }));
+      this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { filter: ':replies' }));
     }
   }
 
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index 61a34fd2b..fa18f84b3 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -215,8 +215,10 @@ class SwitchingColumnsArea extends React.PureComponent {
           <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
           <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
 
-          <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
-          <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
+          <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} componentParams={{ filter: '' }} />
+          <WrappedRoute path='/accounts/:accountId/mentions' component={AccountTimeline} content={children} componentParams={{ filter: ':mentions' }} />
+          <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ filter: ':replies' }} />
+          <WrappedRoute path='/accounts/:accountId/reblogs' component={AccountTimeline} content={children} componentParams={{ filter: ':reblogs' }} />
           <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
           <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
           <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />