about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/accounts_controller.rb3
-rw-r--r--app/controllers/admin/custom_emojis_controller.rb4
-rw-r--r--app/helpers/admin/filter_helper.rb9
-rw-r--r--app/javascript/mastodon/components/avatar.js5
-rw-r--r--app/javascript/mastodon/components/avatar_overlay.js13
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js12
-rw-r--r--app/javascript/mastodon/features/compose/components/reply_indicator.js8
-rw-r--r--app/javascript/mastodon/features/compose/components/upload.js2
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js2
-rw-r--r--app/javascript/mastodon/features/standalone/hashtag_timeline/index.js12
-rw-r--r--app/javascript/mastodon/features/standalone/public_timeline/index.js12
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js5
-rw-r--r--app/javascript/mastodon/locales/ar.json36
-rw-r--r--app/javascript/mastodon/locales/ca.json20
-rw-r--r--app/javascript/mastodon/locales/en.json2
-rw-r--r--app/javascript/mastodon/locales/fr.json2
-rw-r--r--app/javascript/mastodon/locales/gl.json20
-rw-r--r--app/javascript/mastodon/locales/ja.json16
-rw-r--r--app/javascript/mastodon/locales/nl.json20
-rw-r--r--app/javascript/mastodon/locales/oc.json2
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json24
-rw-r--r--app/javascript/mastodon/locales/pt.json34
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json2
-rw-r--r--app/javascript/mastodon/stream.js8
-rw-r--r--app/javascript/styles/mastodon/components.scss491
-rw-r--r--app/javascript/styles/mastodon/rtl.scss26
-rw-r--r--app/lib/provider_discovery.rb19
-rw-r--r--app/models/account_filter.rb2
-rw-r--r--app/models/custom_emoji_filter.rb2
-rw-r--r--app/services/fetch_link_card_service.rb56
-rw-r--r--app/services/fetch_remote_status_service.rb2
-rw-r--r--app/services/follow_service.rb2
-rw-r--r--app/views/admin/accounts/_account.html.haml17
-rw-r--r--app/views/admin/accounts/index.html.haml9
-rw-r--r--app/views/admin/accounts/show.html.haml3
-rw-r--r--app/views/admin/custom_emojis/_custom_emoji.html.haml2
-rw-r--r--app/views/admin/custom_emojis/index.html.haml14
37 files changed, 475 insertions, 443 deletions
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index e9a512e70..7428c3f22 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -89,7 +89,8 @@ module Admin
         :username,
         :display_name,
         :email,
-        :ip
+        :ip,
+        :staff
       )
     end
   end
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
index 3fa2a0b72..ccab03de4 100644
--- a/app/controllers/admin/custom_emojis_controller.rb
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -92,7 +92,9 @@ module Admin
     def filter_params
       params.permit(
         :local,
-        :remote
+        :remote,
+        :by_domain,
+        :shortcode
       )
     end
   end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 9443934b3..359c43d0e 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -1,11 +1,12 @@
 # frozen_string_literal: true
 
 module Admin::FilterHelper
-  ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze
-  REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
-  INVITE_FILTER = %i(available expired).freeze
+  ACCOUNT_FILTERS      = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze
+  REPORT_FILTERS       = %i(resolved account_id target_account_id).freeze
+  INVITE_FILTER        = %i(available expired).freeze
+  CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
 
-  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER
+  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS
 
   def filter_link_to(text, link_to_params, link_class_params = link_to_params)
     new_url = filtered_url_for(link_to_params)
diff --git a/app/javascript/mastodon/components/avatar.js b/app/javascript/mastodon/components/avatar.js
index f7c484ee3..570505833 100644
--- a/app/javascript/mastodon/components/avatar.js
+++ b/app/javascript/mastodon/components/avatar.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import { autoPlayGif } from '../initial_state';
 
 export default class Avatar extends React.PureComponent {
 
@@ -8,12 +9,12 @@ export default class Avatar extends React.PureComponent {
     account: ImmutablePropTypes.map.isRequired,
     size: PropTypes.number.isRequired,
     style: PropTypes.object,
-    animate: PropTypes.bool,
     inline: PropTypes.bool,
+    animate: PropTypes.bool,
   };
 
   static defaultProps = {
-    animate: false,
+    animate: autoPlayGif,
     size: 20,
     inline: false,
   };
diff --git a/app/javascript/mastodon/components/avatar_overlay.js b/app/javascript/mastodon/components/avatar_overlay.js
index f5d67b34e..3ec1d7730 100644
--- a/app/javascript/mastodon/components/avatar_overlay.js
+++ b/app/javascript/mastodon/components/avatar_overlay.js
@@ -1,22 +1,29 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import { autoPlayGif } from '../initial_state';
 
 export default class AvatarOverlay extends React.PureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
     friend: ImmutablePropTypes.map.isRequired,
+    animate: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    animate: autoPlayGif,
   };
 
   render() {
-    const { account, friend } = this.props;
+    const { account, friend, animate } = this.props;
 
     const baseStyle = {
-      backgroundImage: `url(${account.get('avatar_static')})`,
+      backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
     };
 
     const overlayStyle = {
-      backgroundImage: `url(${friend.get('avatar_static')})`,
+      backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`,
     };
 
     return (
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 7890755f3..a876c5197 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -156,6 +156,8 @@ export default class ComposeForm extends ImmutablePureComponent {
 
     return (
       <div className='compose-form'>
+        <WarningContainer />
+
         <Collapsable isVisible={this.props.spoiler} fullHeight={50}>
           <div className='spoiler-input'>
             <label>
@@ -165,8 +167,6 @@ export default class ComposeForm extends ImmutablePureComponent {
           </div>
         </Collapsable>
 
-        <WarningContainer />
-
         <ReplyIndicatorContainer />
 
         <div className='compose-form__autosuggest-wrapper'>
@@ -199,11 +199,11 @@ export default class ComposeForm extends ImmutablePureComponent {
             <SensitiveButtonContainer />
             <SpoilerButtonContainer />
           </div>
+          <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
+        </div>
 
-          <div className='compose-form__publish'>
-            <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
-            <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
-          </div>
+        <div className='compose-form__publish'>
+          <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js
index 7672440b4..d8cda96f3 100644
--- a/app/javascript/mastodon/features/compose/components/reply_indicator.js
+++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js
@@ -6,6 +6,7 @@ import IconButton from '../../../components/icon_button';
 import DisplayName from '../../../components/display_name';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import { isRtl } from '../../../rtl';
 
 const messages = defineMessages({
   cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
@@ -42,7 +43,10 @@ export default class ReplyIndicator extends ImmutablePureComponent {
       return null;
     }
 
-    const content  = { __html: status.get('contentHtml') };
+    const content = { __html: status.get('contentHtml') };
+    const style   = {
+      direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
+    };
 
     return (
       <div className='reply-indicator'>
@@ -55,7 +59,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
           </a>
         </div>
 
-        <div className='reply-indicator__content' dangerouslySetInnerHTML={content} />
+        <div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js
index 6ab76492a..3a3d17710 100644
--- a/app/javascript/mastodon/features/compose/components/upload.js
+++ b/app/javascript/mastodon/features/compose/components/upload.js
@@ -62,7 +62,7 @@ export default class Upload extends ImmutablePureComponent {
   render () {
     const { intl, media } = this.props;
     const active          = this.state.hovered || this.state.focused;
-    const description     = this.state.dirtyDescription || media.get('description') || '';
+    const description     = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
 
     return (
       <div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
index 1dcd4de14..ae136e48f 100644
--- a/app/javascript/mastodon/features/list_timeline/index.js
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -161,7 +161,7 @@ export default class ListTimeline extends React.PureComponent {
           scrollKey={`list_timeline-${columnId}`}
           timelineId={`list:${id}`}
           loadMore={this.handleLoadMore}
-          emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />}
+          emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
         />
       </Column>
     );
diff --git a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
index f15fbb2f4..f14be2aaf 100644
--- a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
@@ -8,6 +8,7 @@ import {
 } from '../../../actions/timelines';
 import Column from '../../../components/column';
 import ColumnHeader from '../../../components/column_header';
+import { connectHashtagStream } from '../../../actions/streaming';
 
 @connect()
 export default class HashtagTimeline extends React.PureComponent {
@@ -29,16 +30,13 @@ export default class HashtagTimeline extends React.PureComponent {
     const { dispatch, hashtag } = this.props;
 
     dispatch(refreshHashtagTimeline(hashtag));
-
-    this.polling = setInterval(() => {
-      dispatch(refreshHashtagTimeline(hashtag));
-    }, 10000);
+    this.disconnect = dispatch(connectHashtagStream(hashtag));
   }
 
   componentWillUnmount () {
-    if (typeof this.polling !== 'undefined') {
-      clearInterval(this.polling);
-      this.polling = null;
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js
index de4b5320a..5805d1a10 100644
--- a/app/javascript/mastodon/features/standalone/public_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js
@@ -9,6 +9,7 @@ import {
 import Column from '../../../components/column';
 import ColumnHeader from '../../../components/column_header';
 import { defineMessages, injectIntl } from 'react-intl';
+import { connectPublicStream } from '../../../actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
@@ -35,16 +36,13 @@ export default class PublicTimeline extends React.PureComponent {
     const { dispatch } = this.props;
 
     dispatch(refreshPublicTimeline());
-
-    this.polling = setInterval(() => {
-      dispatch(refreshPublicTimeline());
-    }, 3000);
+    this.disconnect = dispatch(connectPublicStream());
   }
 
   componentWillUnmount () {
-    if (typeof this.polling !== 'undefined') {
-      clearInterval(this.polling);
-      this.polling = null;
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 93ed9e605..c5b3c20d4 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -27,6 +27,8 @@ const componentMap = {
   'LIST': ListTimeline,
 };
 
+const isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
+
 @component => injectIntl(component, { withRef: true })
 export default class ColumnsArea extends ImmutablePureComponent {
 
@@ -79,7 +81,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
 
   handleChildrenContentChange() {
     if (!this.props.singleColumn) {
-      this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
+      const modifier = isRtlLayout ? -1 : 1;
+      this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
     }
   }
 
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index ec66a0027..d699a69df 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -36,9 +36,9 @@
   "column.favourites": "المفضلة",
   "column.follow_requests": "طلبات المتابعة",
   "column.home": "الرئيسية",
-  "column.lists": "Lists",
+  "column.lists": "القوائم",
   "column.mutes": "الحسابات المكتومة",
-  "column.notifications": "الإشعارات",
+  "column.notifications": "الإخطارات",
   "column.pins": "التبويقات المثبتة",
   "column.public": "الخيط العام الموحد",
   "column_back_button.label": "العودة",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "حذف",
   "confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟",
   "confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "أكتم",
@@ -109,32 +109,32 @@
   "home.settings": "إعدادات العمود",
   "keyboard_shortcuts.back": "للعودة",
   "keyboard_shortcuts.boost": "للترقية",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.column": "للتركيز على منشور على أحد الأعمدة",
+  "keyboard_shortcuts.compose": "للتركيز على نافذة تحرير النصوص",
   "keyboard_shortcuts.description": "Description",
   "keyboard_shortcuts.down": "للإنتقال إلى أسفل القائمة",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "للإضافة إلى المفضلة",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.hotkey": "مفتاح الإختصار",
+  "keyboard_shortcuts.legend": "لعرض هذا المفتاح",
   "keyboard_shortcuts.mention": "لذِكر الناشر",
   "keyboard_shortcuts.reply": "للردّ",
-  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.search": "للتركيز على البحث",
   "keyboard_shortcuts.toot": "لتحرير تبويق جديد",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
   "lightbox.close": "إغلاق",
   "lightbox.next": "التالي",
   "lightbox.previous": "العودة",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "أضف إلى القائمة",
+  "lists.account.remove": "إحذف من القائمة",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "تعديل القائمة",
+  "lists.new.create": "إنشاء قائمة",
+  "lists.new.title_placeholder": "عنوان القائمة الجديدة",
+  "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
+  "lists.subheading": "قوائمك",
   "loading_indicator.label": "تحميل ...",
   "media_gallery.toggle_visible": "عرض / إخفاء",
   "missing_indicator.label": "تعذر العثور عليه",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "طلبات المتابعة",
   "navigation_bar.info": "معلومات إضافية",
   "navigation_bar.keyboard_shortcuts": "إختصارات لوحة المفاتيح",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "القوائم",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "الحسابات المكتومة",
   "navigation_bar.pins": "التبويقات المثبتة",
@@ -209,7 +209,7 @@
   "search_popout.search_format": "نمط البحث المتقدم",
   "search_popout.tips.hashtag": "وسم",
   "search_popout.tips.status": "حالة",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.text": "جملة قصيرة تُمكّنُك من عرض أسماء و حسابات و كلمات رمزية",
   "search_popout.tips.user": "مستخدِم",
   "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
   "standalone.public_title": "نظرة على ...",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index f705937fd..62d85a5e1 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favorits",
   "column.follow_requests": "Peticions per seguir-te",
   "column.home": "Inici",
-  "column.lists": "Lists",
+  "column.lists": "Llistes",
   "column.mutes": "Usuaris silenciats",
   "column.notifications": "Notificacions",
   "column.pins": "Toot fixat",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Esborrar",
   "confirmations.delete.message": "Estàs segur que vols esborrar aquest estat?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Estàs segur que vols esborrar permanenment aquesta llista?",
   "confirmations.domain_block.confirm": "Amagar tot el domini",
   "confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.",
   "confirmations.mute.confirm": "Silenciar",
@@ -127,14 +127,14 @@
   "lightbox.close": "Tancar",
   "lightbox.next": "Següent",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Afegir a la llista",
+  "lists.account.remove": "Treure de la llista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar llista",
+  "lists.new.create": "Afegir llista",
+  "lists.new.title_placeholder": "Nou títol de llista",
+  "lists.search": "Cercar entre les persones que segueixes",
+  "lists.subheading": "Les teves llistes",
   "loading_indicator.label": "Carregant...",
   "media_gallery.toggle_visible": "Alternar visibilitat",
   "missing_indicator.label": "No trobat",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Sol·licituds de seguiment",
   "navigation_bar.info": "Informació addicional",
   "navigation_bar.keyboard_shortcuts": "Dreceres de teclat",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Llistes",
   "navigation_bar.logout": "Tancar sessió",
   "navigation_bar.mutes": "Usuaris silenciats",
   "navigation_bar.pins": "Toots fixats",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 0f766af6a..3fc4a8c96 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -93,7 +93,7 @@
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.public_timeline": "the public timeline",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
   "follow_request.authorize": "Authorize",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index a7a8876d0..01cbd2657 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -91,7 +91,7 @@
   "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag",
   "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.",
   "empty_column.home.public_timeline": "le fil public",
-  "empty_column.list": "Il n'y a rien dans cette liste pour l'instant.",
+  "empty_column.list": "Il n'y a rien dans cette liste pour l'instant. Dès que des personnes de cette listes publierons de nouveaux statuts ils apparaîtront ici.",
   "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.",
   "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.",
   "follow_request.authorize": "Accepter",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index bb0b1a9fd..6398daa11 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favoritas",
   "column.follow_requests": "Peticións de seguimento",
   "column.home": "Inicio",
-  "column.lists": "Lists",
+  "column.lists": "Listas",
   "column.mutes": "Usuarias acaladas",
   "column.notifications": "Notificacións",
   "column.pins": "Mensaxes fixadas",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Borrar",
   "confirmations.delete.message": "Está segura de que quere eliminar este estado?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Estás seguro de que queres eliminar permanentemente esta lista?",
   "confirmations.domain_block.confirm": "Agochar un dominio completo",
   "confirmations.domain_block.message": "Realmente está segura de que quere bloquear por completo o dominio {domain}? Normalmente é suficiente, e preferible, bloquear de xeito selectivo varios elementos.",
   "confirmations.mute.confirm": "Acalar",
@@ -127,14 +127,14 @@
   "lightbox.close": "Fechar",
   "lightbox.next": "Seguinte",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Engadir á lista",
+  "lists.account.remove": "Eliminar da lista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar lista",
+  "lists.new.create": "Engadir lista",
+  "lists.new.title_placeholder": "Novo título da lista",
+  "lists.search": "Procurar entre a xente que segues",
+  "lists.subheading": "As túas listas",
   "loading_indicator.label": "Cargando...",
   "media_gallery.toggle_visible": "Dar visibilidade",
   "missing_indicator.label": "Non atopado",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Peticións de seguimento",
   "navigation_bar.info": "Sobre esta instancia",
   "navigation_bar.keyboard_shortcuts": "Atallos do teclado",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Usuarias acaladas",
   "navigation_bar.pins": "Mensaxes fixadas",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 40ad66a7f..68abd906f 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -95,7 +95,7 @@
   "empty_column.home.public_timeline": "連合タイムライン",
   "empty_column.list": "このリストにはまだなにもありません。",
   "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
-  "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
+  "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう",
   "follow_request.authorize": "許可",
   "follow_request.reject": "拒否",
   "getting_started.appsshort": "アプリ",
@@ -162,12 +162,12 @@
   "notifications.clear": "通知を消去",
   "notifications.clear_confirmation": "本当に通知を消去しますか?",
   "notifications.column_settings.alert": "デスクトップ通知",
-  "notifications.column_settings.favourite": "お気に入り",
-  "notifications.column_settings.follow": "新しいフォロワー",
-  "notifications.column_settings.mention": "返信",
+  "notifications.column_settings.favourite": "お気に入り:",
+  "notifications.column_settings.follow": "新しいフォロワー:",
+  "notifications.column_settings.mention": "返信:",
   "notifications.column_settings.push": "プッシュ通知",
   "notifications.column_settings.push_meta": "このデバイス",
-  "notifications.column_settings.reblog": "ブースト",
+  "notifications.column_settings.reblog": "ブースト:",
   "notifications.column_settings.show": "カラムに表示",
   "notifications.column_settings.sound": "通知音を再生",
   "onboarding.done": "完了",
@@ -176,7 +176,7 @@
   "onboarding.page_four.home": "「ホーム」タイムラインではあなたがフォローしている人の投稿を表示します。",
   "onboarding.page_four.notifications": "「通知」ではあなたへの他の人からの関わりを表示します。",
   "onboarding.page_one.federation": "Mastodonは誰でも参加できるSNSです。",
-  "onboarding.page_one.handle": "あなたは今数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です。",
+  "onboarding.page_one.handle": "今あなたは数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です",
   "onboarding.page_one.welcome": "Mastodonへようこそ!",
   "onboarding.page_six.admin": "あなたのインスタンスの管理者は{admin}です。",
   "onboarding.page_six.almost_done": "以上です。",
@@ -184,7 +184,7 @@
   "onboarding.page_six.apps_available": "iOS、Androidあるいは他のプラットフォームで使える{apps}があります。",
   "onboarding.page_six.github": "MastodonはOSSです。バグ報告や機能要望あるいは貢献を{github}から行なえます。",
   "onboarding.page_six.guidelines": "コミュニティガイドライン",
-  "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください。",
+  "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください!",
   "onboarding.page_six.various_app": "様々なモバイルアプリ",
   "onboarding.page_three.profile": "「プロフィールを編集」から、あなたの自己紹介や表示名を変更できます。またそこでは他の設定ができます。",
   "onboarding.page_three.search": "検索バーで、{illustration}や{introductions}のように特定のハッシュタグの投稿を見たり、ユーザーを探したりできます。",
@@ -215,7 +215,7 @@
   "search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
   "search_popout.tips.user": "ユーザー",
   "search_results.total": "{count, number}件の結果",
-  "standalone.public_title": "今こんな話をしています",
+  "standalone.public_title": "今こんな話をしています...",
   "status.cannot_reblog": "この投稿はブーストできません",
   "status.delete": "削除",
   "status.embed": "埋め込み",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index c290ed767..9044c2011 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favorieten",
   "column.follow_requests": "Volgverzoeken",
   "column.home": "Start",
-  "column.lists": "Lists",
+  "column.lists": "Lijsten",
   "column.mutes": "Genegeerde gebruikers",
   "column.notifications": "Meldingen",
   "column.pins": "Vastgezette toots",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Verwijderen",
   "confirmations.delete.message": "Weet je het zeker dat je deze toot wilt verwijderen?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Weet je zeker dat je deze lijst permanent wilt verwijderen?",
   "confirmations.domain_block.confirm": "Negeer alles van deze server",
   "confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
   "confirmations.mute.confirm": "Negeren",
@@ -127,14 +127,14 @@
   "lightbox.close": "Sluiten",
   "lightbox.next": "Volgende",
   "lightbox.previous": "Vorige",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Aan lijst toevoegen",
+  "lists.account.remove": "Uit lijst verwijderen",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Lijst bewerken",
+  "lists.new.create": "Lijst toevoegen",
+  "lists.new.title_placeholder": "Naam nieuwe lijst",
+  "lists.search": "Zoek naar mensen die je volgt",
+  "lists.subheading": "Jouw lijsten",
   "loading_indicator.label": "Laden…",
   "media_gallery.toggle_visible": "Media wel/niet tonen",
   "missing_indicator.label": "Niet gevonden",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Volgverzoeken",
   "navigation_bar.info": "Uitgebreide informatie",
   "navigation_bar.keyboard_shortcuts": "Toetsenbord sneltoetsen",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Lijsten",
   "navigation_bar.logout": "Afmelden",
   "navigation_bar.mutes": "Genegeerde gebruikers",
   "navigation_bar.pins": "Vastgezette toots",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index f8b4751d6..0d1f7c971 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -91,7 +91,7 @@
   "empty_column.hashtag": "I a pas encara de contengut ligat a aquesta etiqueta.",
   "empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
   "empty_column.home.public_timeline": "lo flux public",
-  "empty_column.list": "I a pas res dins la lista pel moment.",
+  "empty_column.list": "I a pas res dins la lista pel moment. Quand de membres d’aquesta lista publiquen de novèls estatuts los veiretz aquí.",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
   "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public",
   "follow_request.authorize": "Autorizar",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 6bac65865..70632846c 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores pendentes",
   "column.home": "Página inicial",
-  "column.lists": "Lists",
+  "column.lists": "Listas",
   "column.mutes": "Usuários silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Postagens fixadas",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Excluir",
   "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Você tem certeza que quer deletar permanentemente a lista?",
   "confirmations.domain_block.confirm": "Esconder o domínio inteiro",
   "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
   "confirmations.mute.confirm": "Silenciar",
@@ -110,7 +110,7 @@
   "keyboard_shortcuts.back": "para navegar de volta",
   "keyboard_shortcuts.boost": "para compartilhar",
   "keyboard_shortcuts.column": "Focar um status em uma das colunas",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.compose": "para focar a área de redação",
   "keyboard_shortcuts.description": "Description",
   "keyboard_shortcuts.down": "para mover para baixo na lista",
   "keyboard_shortcuts.enter": "to open status",
@@ -127,14 +127,14 @@
   "lightbox.close": "Fechar",
   "lightbox.next": "Próximo",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Adicionar a listas",
+  "lists.account.remove": "Remover da lista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar lista",
+  "lists.new.create": "Adicionar lista",
+  "lists.new.title_placeholder": "Novo título da lista",
+  "lists.search": "Procurar entre as pessoas que você segue",
+  "lists.subheading": "Suas listas",
   "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
   "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Usuários silenciados",
   "navigation_bar.pins": "Postagens fixadas",
@@ -177,7 +177,7 @@
   "onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!",
   "onboarding.page_six.admin": "O administrador de sua instância é {admin}.",
   "onboarding.page_six.almost_done": "Quase acabando...",
-  "onboarding.page_six.appetoot": "Bon Appetoot!",
+  "onboarding.page_six.appetoot": "Bom Apetoot!",
   "onboarding.page_six.apps_available": "Há {apps} disponíveis para iOS, Android e outras plataformas.",
   "onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Você pode reportar bugs, prequisitar novas funções ou contribuir para o código no {github}.",
   "onboarding.page_six.guidelines": "diretrizes da comunidade",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 728fb3a10..15d5deb93 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -1,7 +1,7 @@
 {
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo do domínio {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de forma incompleta.",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
@@ -19,7 +19,7 @@
   "account.share": "Partilhar o perfil @{name}",
   "account.show_reblogs": "Mostrar partilhas de @{name}",
   "account.unblock": "Não bloquear @{name}",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.unblock_domain": "Mostrar {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
   "account.unmute_notifications": "Deixar de silenciar @{name}",
@@ -36,7 +36,7 @@
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores Pendentes",
   "column.home": "Home",
-  "column.lists": "Lists",
+  "column.lists": "Listas",
   "column.mutes": "Utilizadores silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Pinned toot",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Eliminar",
   "confirmations.delete.message": "De certeza que queres eliminar esta publicação?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Tens a certeza de que desejas apagar permanentemente esta lista?",
   "confirmations.domain_block.confirm": "Esconder tudo deste domínio",
   "confirmations.domain_block.message": "De certeza que queres bloquear por completo o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é o suficiente e o recomendado.",
   "confirmations.mute.confirm": "Silenciar",
@@ -88,12 +88,12 @@
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viagens & Lugares",
   "empty_column.community": "Ainda não existe conteúdo local para mostrar!",
-  "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag",
+  "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.",
   "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
   "empty_column.home.public_timeline": "global",
   "empty_column.list": "Ainda não existem publicações nesta lista.",
   "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
-  "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
+  "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rejeitar",
   "getting_started.appsshort": "Aplicações",
@@ -116,7 +116,7 @@
   "keyboard_shortcuts.enter": "para expandir uma publicação",
   "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
   "keyboard_shortcuts.heading": "Atalhos do teclado",
-  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.hotkey": "Atalho",
   "keyboard_shortcuts.legend": "para mostrar esta legenda",
   "keyboard_shortcuts.mention": "para mencionar o autor",
   "keyboard_shortcuts.reply": "para responder",
@@ -127,14 +127,14 @@
   "lightbox.close": "Fechar",
   "lightbox.next": "Próximo",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Adicionar à lista",
+  "lists.account.remove": "Remover da lista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar lista",
+  "lists.new.create": "Adicionar lista",
+  "lists.new.title_placeholder": "Novo título da lista",
+  "lists.search": "Pesquisa entre as pessoas que segues",
+  "lists.subheading": "As tuas listas",
   "loading_indicator.label": "A carregar...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
   "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Utilizadores silenciados",
   "navigation_bar.pins": "Posts fixos",
@@ -209,13 +209,13 @@
   "search_popout.search_format": "Formato avançado de pesquisa",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags",
   "search_popout.tips.user": "utilizador",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Espreitar lá dentro...",
   "status.cannot_reblog": "Este post não pode ser partilhado",
   "status.delete": "Eliminar",
-  "status.embed": "Embed",
+  "status.embed": "Incorporar",
   "status.favourite": "Adicionar aos favoritos",
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Media escondida",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index cb5607cc5..3d6bd5334 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -91,7 +91,7 @@
   "empty_column.hashtag": "这个话题标签下暂时没有内容。",
   "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
   "empty_column.home.public_timeline": "公共时间轴",
-  "empty_column.list": "这个列表中暂时没有内容。",
+  "empty_column.list": "这个列表中暂时没有内容。列表中用户所发送的的新嘟文将会在这里显示。",
   "empty_column.notifications": "你还没有收到过通知信息,快向其他用户搭讪吧。",
   "empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户,这里就会有嘟文出现了哦!",
   "follow_request.authorize": "同意",
diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js
index 36c68ffc5..9a6f4f26d 100644
--- a/app/javascript/mastodon/stream.js
+++ b/app/javascript/mastodon/stream.js
@@ -62,7 +62,13 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
 
 
 export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
-  const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
+  const params = [ `stream=${stream}` ];
+
+  if (accessToken !== null) {
+    params.push(`access_token=${accessToken}`);
+  }
+
+  const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`);
 
   ws.onopen      = connected;
   ws.onmessage   = e => received(JSON.parse(e.data));
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index da789ba06..be28473e5 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -265,198 +265,286 @@
 
 .compose-form {
   padding: 10px;
-}
-
-.compose-form__warning {
-  color: darken($ui-secondary-color, 65%);
-  margin-bottom: 15px;
-  background: $ui-primary-color;
-  box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
-  padding: 8px 10px;
-  border-radius: 4px;
-  font-size: 13px;
-  font-weight: 400;
 
-  strong {
+  .compose-form__warning {
     color: darken($ui-secondary-color, 65%);
-    font-weight: 500;
+    margin-bottom: 15px;
+    background: $ui-primary-color;
+    box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
+    padding: 8px 10px;
+    border-radius: 4px;
+    font-size: 13px;
+    font-weight: 400;
 
-    @each $lang in $cjk-langs {
-      &:lang(#{$lang}) {
-        font-weight: 700;
+    strong {
+      color: darken($ui-secondary-color, 65%);
+      font-weight: 500;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
+    }
+
+    a {
+      color: darken($ui-primary-color, 33%);
+      font-weight: 500;
+      text-decoration: underline;
+
+      &:hover,
+      &:active,
+      &:focus {
+        text-decoration: none;
       }
     }
   }
 
-  a {
-    color: darken($ui-primary-color, 33%);
-    font-weight: 500;
-    text-decoration: underline;
+  .compose-form__autosuggest-wrapper {
+    position: relative;
+
+    .emoji-picker-dropdown {
+      position: absolute;
+      right: 5px;
+      top: 5px;
+    }
+  }
+
+  .autosuggest-textarea,
+  .spoiler-input {
+    position: relative;
+  }
+
+  .autosuggest-textarea__textarea,
+  .spoiler-input__input {
+    display: block;
+    box-sizing: border-box;
+    width: 100%;
+    margin: 0;
+    color: $ui-base-color;
+    background: $simple-background-color;
+    padding: 10px;
+    font-family: inherit;
+    font-size: 14px;
+    resize: vertical;
+    border: 0;
+    outline: 0;
 
-    &:hover,
-    &:active,
     &:focus {
-      text-decoration: none;
+      outline: 0;
+    }
+
+    @media screen and (max-width: 600px) {
+      font-size: 16px;
     }
   }
-}
 
-.compose-form__modifiers {
-  color: $ui-base-color;
-  font-family: inherit;
-  font-size: 14px;
-  background: $simple-background-color;
-  border-radius: 0 0 4px;
-}
+  .spoiler-input__input {
+    border-radius: 4px;
+  }
 
-.compose-form__buttons-wrapper {
-  display: flex;
-  justify-content: space-between;
-}
+  .autosuggest-textarea__textarea {
+    min-height: 100px;
+    border-radius: 4px 4px 0 0;
+    padding-bottom: 0;
+    padding-right: 10px + 22px;
+    resize: none;
 
-.compose-form__buttons {
-  padding: 10px;
-  background: darken($simple-background-color, 8%);
-  box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05);
-  border-radius: 0 0 4px 4px;
-  display: flex;
+    @media screen and (max-width: 600px) {
+      height: 100px !important; // prevent auto-resize textarea
+      resize: vertical;
+    }
+  }
 
-  .icon-button {
-    box-sizing: content-box;
-    padding: 0 3px;
+  .autosuggest-textarea__suggestions {
+    box-sizing: border-box;
+    display: none;
+    position: absolute;
+    top: 100%;
+    width: 100%;
+    z-index: 99;
+    box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
+    background: $ui-secondary-color;
+    border-radius: 0 0 4px 4px;
+    color: $ui-base-color;
+    font-size: 14px;
+    padding: 6px;
+
+    &.autosuggest-textarea__suggestions--visible {
+      display: block;
+    }
   }
-}
 
-.compose-form__upload-button-icon {
-  line-height: 27px;
-}
+  .autosuggest-textarea__suggestions__item {
+    padding: 10px;
+    cursor: pointer;
+    border-radius: 4px;
 
-.compose-form__sensitive-button {
-  display: none;
+    &:hover,
+    &:focus,
+    &:active,
+    &.selected {
+      background: darken($ui-secondary-color, 10%);
+    }
+  }
+
+  .autosuggest-account,
+  .autosuggest-emoji {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: flex-start;
+    line-height: 18px;
+    font-size: 14px;
+  }
 
-  &.compose-form__sensitive-button--visible {
+  .autosuggest-account-icon,
+  .autosuggest-emoji img {
     display: block;
+    margin-right: 8px;
+    width: 16px;
+    height: 16px;
   }
 
-  .compose-form__sensitive-button__icon {
-    line-height: 27px;
+  .autosuggest-account .display-name__account {
+    color: lighten($ui-base-color, 36%);
   }
-}
 
-.compose-form__upload-wrapper {
-  overflow: hidden;
-}
+  .compose-form__modifiers {
+    color: $ui-base-color;
+    font-family: inherit;
+    font-size: 14px;
+    background: $simple-background-color;
 
-.compose-form__uploads-wrapper {
-  display: flex;
-  flex-direction: row;
-  padding: 5px;
-  flex-wrap: wrap;
-}
+    .compose-form__upload-wrapper {
+      overflow: hidden;
+    }
 
-.compose-form__upload {
-  flex: 1 1 0;
-  min-width: 40%;
-  margin: 5px;
+    .compose-form__uploads-wrapper {
+      display: flex;
+      flex-direction: row;
+      padding: 5px;
+      flex-wrap: wrap;
+    }
 
-  &-description {
-    position: absolute;
-    z-index: 2;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    box-sizing: border-box;
-    background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
-    padding: 10px;
-    opacity: 0;
-    transition: opacity .1s ease;
+    .compose-form__upload {
+      flex: 1 1 0;
+      min-width: 40%;
+      margin: 5px;
 
-    input {
-      background: transparent;
-      color: $ui-secondary-color;
-      border: 0;
-      padding: 0;
-      margin: 0;
-      width: 100%;
-      font-family: inherit;
-      font-size: 14px;
-      font-weight: 500;
+      &-description {
+        position: absolute;
+        z-index: 2;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        box-sizing: border-box;
+        background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
+        padding: 10px;
+        opacity: 0;
+        transition: opacity .1s ease;
+
+        input {
+          background: transparent;
+          color: $ui-secondary-color;
+          border: 0;
+          padding: 0;
+          margin: 0;
+          width: 100%;
+          font-family: inherit;
+          font-size: 14px;
+          font-weight: 500;
+
+          &:focus {
+            color: $white;
+          }
 
-      &:focus {
-        color: $white;
+          &::placeholder {
+            opacity: 0.54;
+            color: $ui-secondary-color;
+          }
+        }
+
+        &.active {
+          opacity: 1;
+        }
       }
 
-      &::placeholder {
-        opacity: 0.54;
-        color: $ui-secondary-color;
+      .icon-button {
+        mix-blend-mode: difference;
       }
     }
 
-    &.active {
-      opacity: 1;
+    .compose-form__upload-thumbnail {
+      border-radius: 4px;
+      background-position: center;
+      background-size: cover;
+      background-repeat: no-repeat;
+      height: 100px;
+      width: 100%;
     }
   }
 
-  .icon-button {
-    mix-blend-mode: difference;
-  }
-}
+  .compose-form__buttons-wrapper {
+    padding: 10px;
+    background: darken($simple-background-color, 8%);
+    border-radius: 0 0 4px 4px;
+    display: flex;
+    justify-content: space-between;
 
-.compose-form__upload-thumbnail {
-  border-radius: 4px;
-  background-position: center;
-  background-size: cover;
-  background-repeat: no-repeat;
-  height: 100px;
-  width: 100%;
-}
+    .compose-form__buttons {
+      display: flex;
 
-.compose-form__label {
-  display: block;
-  line-height: 24px;
-  vertical-align: middle;
+      .compose-form__upload-button-icon {
+        line-height: 27px;
+      }
 
-  &.with-border {
-    border-top: 1px solid $ui-base-color;
-    padding-top: 10px;
-  }
+      .compose-form__sensitive-button {
+        display: none;
 
-  .compose-form__label__text {
-    display: inline-block;
-    vertical-align: middle;
-    margin-bottom: 14px;
-    margin-left: 8px;
-    color: $ui-primary-color;
-  }
-}
+        &.compose-form__sensitive-button--visible {
+          display: block;
+        }
 
-.compose-form__textarea,
-.follow-form__input {
-  background: $simple-background-color;
+        .compose-form__sensitive-button__icon {
+          line-height: 27px;
+        }
+      }
+    }
 
-  &:disabled {
-    background: $ui-secondary-color;
-  }
-}
+    .icon-button {
+      box-sizing: content-box;
+      padding: 0 3px;
+    }
 
-.compose-form__autosuggest-wrapper {
-  position: relative;
+    .character-counter__wrapper {
+      align-self: center;
+      margin-right: 4px;
 
-  .emoji-picker-dropdown {
-    position: absolute;
-    right: 5px;
-    top: 5px;
+      .character-counter {
+        cursor: default;
+        font-family: 'mastodon-font-sans-serif', sans-serif;
+        font-size: 14px;
+        font-weight: 600;
+        color: lighten($ui-base-color, 12%);
+
+        &.character-counter--over {
+          color: $warning-red;
+        }
+      }
+    }
   }
-}
 
-.compose-form__publish {
-  display: flex;
-  min-width: 0;
-}
+  .compose-form__publish {
+    display: flex;
+    justify-content: flex-end;
+    min-width: 0;
 
-.compose-form__publish-button-wrapper {
-  overflow: hidden;
-  padding-top: 10px;
+    .compose-form__publish-button-wrapper {
+      overflow: hidden;
+      padding-top: 10px;
+    }
+  }
 }
 
 .emojione {
@@ -1973,121 +2061,6 @@
   cursor: default;
 }
 
-.autosuggest-textarea,
-.spoiler-input {
-  position: relative;
-}
-
-.autosuggest-textarea__textarea,
-.spoiler-input__input {
-  display: block;
-  box-sizing: border-box;
-  width: 100%;
-  margin: 0;
-  color: $ui-base-color;
-  background: $simple-background-color;
-  padding: 10px;
-  font-family: inherit;
-  font-size: 14px;
-  resize: vertical;
-  border: 0;
-  outline: 0;
-
-  &:focus {
-    outline: 0;
-  }
-
-  @media screen and (max-width: 600px) {
-    font-size: 16px;
-  }
-}
-
-.spoiler-input__input {
-  border-radius: 4px;
-}
-
-.autosuggest-textarea__textarea {
-  min-height: 100px;
-  border-radius: 4px 4px 0 0;
-  padding-bottom: 0;
-  padding-right: 10px + 22px;
-  resize: none;
-
-  @media screen and (max-width: 600px) {
-    height: 100px !important; // prevent auto-resize textarea
-    resize: vertical;
-  }
-}
-
-.autosuggest-textarea__suggestions {
-  box-sizing: border-box;
-  display: none;
-  position: absolute;
-  top: 100%;
-  width: 100%;
-  z-index: 99;
-  box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
-  background: $ui-secondary-color;
-  border-radius: 0 0 4px 4px;
-  color: $ui-base-color;
-  font-size: 14px;
-  padding: 6px;
-
-  &.autosuggest-textarea__suggestions--visible {
-    display: block;
-  }
-}
-
-.autosuggest-textarea__suggestions__item {
-  padding: 10px;
-  cursor: pointer;
-  border-radius: 4px;
-
-  &:hover,
-  &:focus,
-  &:active,
-  &.selected {
-    background: darken($ui-secondary-color, 10%);
-  }
-}
-
-.autosuggest-account,
-.autosuggest-emoji {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: flex-start;
-  line-height: 18px;
-  font-size: 14px;
-}
-
-.autosuggest-account-icon,
-.autosuggest-emoji img {
-  display: block;
-  margin-right: 8px;
-  width: 16px;
-  height: 16px;
-}
-
-.autosuggest-account .display-name__account {
-  color: lighten($ui-base-color, 36%);
-}
-
-.character-counter__wrapper {
-  line-height: 36px;
-  margin: 0 16px 0 8px;
-  padding-top: 10px;
-}
-
-.character-counter {
-  cursor: default;
-  font-size: 16px;
-}
-
-.character-counter--over {
-  color: $warning-red;
-}
-
 .getting-started__wrapper {
   position: relative;
   overflow-y: auto;
diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index 67bfa8a38..77420c84b 100644
--- a/app/javascript/styles/mastodon/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
@@ -7,9 +7,9 @@ body.rtl {
     margin-left: 5px;
   }
 
-  .character-counter__wrapper {
-    margin-right: 8px;
-    margin-left: 16px;
+  .compose-form .compose-form__buttons-wrapper .character-counter__wrapper {
+    margin-right: 0;
+    margin-left: 4px;
   }
 
   .navigation-bar__profile {
@@ -30,6 +30,22 @@ body.rtl {
   .column-header__buttons {
     left: 0;
     right: auto;
+    margin-left: -15px;
+    margin-right: 0;
+  }
+
+  .column-inline-form .icon-button {
+    margin-left: 0;
+    margin-right: 5px;
+  }
+
+  .column-header__links .text-btn {
+    margin-left: 10px;
+    margin-right: 0;
+  }
+
+  .account__avatar-wrapper {
+    float: right;
   }
 
   .column-header__back-button {
@@ -41,10 +57,6 @@ body.rtl {
     float: left;
   }
 
-  .compose-form__modifiers {
-    border-radius: 0 0 0 4px;
-  }
-
   .setting-toggle {
     margin-left: 0;
     margin-right: 8px;
diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb
index bcc4ed500..04ba38101 100644
--- a/app/lib/provider_discovery.rb
+++ b/app/lib/provider_discovery.rb
@@ -2,13 +2,26 @@
 
 class ProviderDiscovery < OEmbed::ProviderDiscovery
   class << self
+    def get(url, **options)
+      provider = discover_provider(url, options)
+
+      options.delete(:html)
+
+      provider.get(url, options)
+    end
+
     def discover_provider(url, **options)
-      res    = Request.new(:get, url).perform
       format = options[:format]
 
-      raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
+      if options[:html]
+        html = Nokogiri::HTML(options[:html])
+      else
+        res = Request.new(:get, url).perform
+
+        raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
 
-      html = Nokogiri::HTML(res.to_s)
+        html = Nokogiri::HTML(res.to_s)
+      end
 
       if format.nil? || format == :json
         provider_endpoint ||= html.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index 189872368..dc7a03039 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -45,6 +45,8 @@ class AccountFilter
       else
         Account.default_scoped
       end
+    when 'staff'
+      accounts_with_users.merge User.staff
     else
       raise "Unknown filter: #{key}"
     end
diff --git a/app/models/custom_emoji_filter.rb b/app/models/custom_emoji_filter.rb
index 2d1394a59..2c09ed65c 100644
--- a/app/models/custom_emoji_filter.rb
+++ b/app/models/custom_emoji_filter.rb
@@ -27,6 +27,8 @@ class CustomEmojiFilter
       CustomEmoji.remote
     when 'by_domain'
       CustomEmoji.where(domain: value)
+    when 'shortcode'
+      CustomEmoji.where(shortcode: value)
     else
       raise "Unknown filter: #{key}"
     end
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 7f4518ea7..d0472a1d7 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -40,6 +40,12 @@ class FetchLinkCardService < BaseService
 
     return if res.code != 405 && (res.code != 200 || res.mime_type != 'text/html')
 
+    @response = Request.new(:get, @url).perform
+
+    return if @response.code != 200 || @response.mime_type != 'text/html'
+
+    @html = @response.to_s
+
     attempt_oembed || attempt_opengraph
   end
 
@@ -70,30 +76,32 @@ class FetchLinkCardService < BaseService
   end
 
   def attempt_oembed
-    response = OEmbed::Providers.get(@url)
+    embed = OEmbed::Providers.get(@url, html: @html)
 
-    return false unless response.respond_to?(:type)
+    return false unless embed.respond_to?(:type)
 
-    @card.type          = response.type
-    @card.title         = response.respond_to?(:title)         ? response.title         : ''
-    @card.author_name   = response.respond_to?(:author_name)   ? response.author_name   : ''
-    @card.author_url    = response.respond_to?(:author_url)    ? response.author_url    : ''
-    @card.provider_name = response.respond_to?(:provider_name) ? response.provider_name : ''
-    @card.provider_url  = response.respond_to?(:provider_url)  ? response.provider_url  : ''
+    @card.type          = embed.type
+    @card.title         = embed.respond_to?(:title)         ? embed.title         : ''
+    @card.author_name   = embed.respond_to?(:author_name)   ? embed.author_name   : ''
+    @card.author_url    = embed.respond_to?(:author_url)    ? embed.author_url    : ''
+    @card.provider_name = embed.respond_to?(:provider_name) ? embed.provider_name : ''
+    @card.provider_url  = embed.respond_to?(:provider_url)  ? embed.provider_url  : ''
     @card.width         = 0
     @card.height        = 0
 
     case @card.type
     when 'link'
-      @card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url)
+      @card.image = URI.parse(embed.thumbnail_url) if embed.respond_to?(:thumbnail_url)
     when 'photo'
-      @card.embed_url = response.url
-      @card.width     = response.width.presence  || 0
-      @card.height    = response.height.presence || 0
+      return false unless embed.respond_to?(:url)
+      @card.embed_url = embed.url
+      @card.image     = URI.parse(embed.url)
+      @card.width     = embed.width.presence  || 0
+      @card.height    = embed.height.presence || 0
     when 'video'
-      @card.width  = response.width.presence  || 0
-      @card.height = response.height.presence || 0
-      @card.html   = Formatter.instance.sanitize(response.html, Sanitize::Config::MASTODON_OEMBED)
+      @card.width  = embed.width.presence  || 0
+      @card.height = embed.height.presence || 0
+      @card.html   = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED)
     when 'rich'
       # Most providers rely on <script> tags, which is a no-no
       return false
@@ -105,17 +113,11 @@ class FetchLinkCardService < BaseService
   end
 
   def attempt_opengraph
-    response = Request.new(:get, @url).perform
-
-    return if response.code != 200 || response.mime_type != 'text/html'
-
-    html = response.to_s
-
     detector = CharlockHolmes::EncodingDetector.new
     detector.strip_tags = true
 
-    guess = detector.detect(html, response.charset)
-    page  = Nokogiri::HTML(html, nil, guess&.fetch(:encoding, nil))
+    guess = detector.detect(@html, @response.charset)
+    page  = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil))
 
     if meta_property(page, 'twitter:player')
       @card.type   = :video
@@ -132,16 +134,16 @@ class FetchLinkCardService < BaseService
       @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image')
     end
 
-    @card.title            = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || ''
-    @card.description      = meta_property(page, 'og:description').presence || meta_property(page, 'description') || ''
+    @card.title       = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || ''
+    @card.description = meta_property(page, 'og:description').presence || meta_property(page, 'description') || ''
 
     return if @card.title.blank? && @card.html.blank?
 
     @card.save_with_optional_image!
   end
 
-  def meta_property(html, property)
-    html.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || html.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
+  def meta_property(page, property)
+    page.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || page.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
   end
 
   def lock_options
diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb
index 9c009335b..9c3008035 100644
--- a/app/services/fetch_remote_status_service.rb
+++ b/app/services/fetch_remote_status_service.rb
@@ -40,6 +40,6 @@ class FetchRemoteStatusService < BaseService
   end
 
   def confirmed_domain?(domain, account)
-    account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url || account.uri).normalized_host).zero?
+    account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero?
   end
 end
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 20579ca63..ac0207a0a 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -22,7 +22,7 @@ class FollowService < BaseService
     elsif source_account.requested?(target_account)
       # This isn't managed by a method in AccountInteractions, so we modify it
       # ourselves if necessary.
-      req = follow_requests.find_by(target_account: other_account)
+      req = source_account.follow_requests.find_by(target_account: target_account)
       req.update!(show_reblogs: reblogs)
       return
     end
diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml
index 5265d77f6..598f6cddd 100644
--- a/app/views/admin/accounts/_account.html.haml
+++ b/app/views/admin/accounts/_account.html.haml
@@ -4,22 +4,11 @@
   %td.domain
     - unless account.local?
       = link_to account.domain, admin_accounts_path(by_domain: account.domain)
-  %td.protocol
-    - unless account.local?
-      = account.protocol.humanize
-  %td.confirmed
-    - if account.local?
-      - if account.user_confirmed?
-        %i.fa.fa-check
-      - else
-        %i.fa.fa-times
-  %td.subscribed
+  %td
     - if account.local?
-      = t('admin.accounts.location.local')
-    - elsif account.subscribed?
-      %i.fa.fa-check
+      = t("admin.accounts.roles.#{account.user&.role}")
     - else
-      %i.fa.fa-times
+      = account.protocol.humanize
   %td
     = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}")
     = table_link_to 'globe', t('admin.accounts.public'), TagManager.instance.url_for(account)
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 27a0682d8..6aa39a80a 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -31,6 +31,11 @@
         - else
           = filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1'
   .filter-subset
+    %strong= t('admin.accounts.role')
+    %ul
+      %li= filter_link_to t('admin.accounts.moderation.all'), staff: nil
+      %li= filter_link_to t('admin.accounts.roles.staff'), staff: '1'
+  .filter-subset
     %strong= t('admin.accounts.order.title')
     %ul
       %li= filter_link_to t('admin.accounts.order.alphabetic'), recent: nil
@@ -56,9 +61,7 @@
       %tr
         %th= t('admin.accounts.username')
         %th= t('admin.accounts.domain')
-        %th= t('admin.accounts.protocol')
-        %th= t('admin.accounts.confirmed')
-        %th= fa_icon 'paper-plane-o'
+        %th
         %th
     %tbody
       = render @accounts
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index ddb1cf15d..5f5d0995c 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -104,7 +104,7 @@
     - else
       = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account)
 
-- unless @account.local?
+- if !@account.local? && @account.hub_url.present?
   %hr
   %h3 OStatus
 
@@ -132,6 +132,7 @@
       - if @account.subscribed?
         = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account)
 
+- if !@account.local? && @account.inbox_url.present?
   %hr
   %h3 ActivityPub
 
diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml
index bab34bc8d..f7fd2538c 100644
--- a/app/views/admin/custom_emojis/_custom_emoji.html.haml
+++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml
@@ -7,7 +7,7 @@
     - if custom_emoji.local?
       = t('admin.accounts.location.local')
     - else
-      = custom_emoji.domain
+      = link_to custom_emoji.domain, admin_custom_emojis_path(by_domain: custom_emoji.domain)
   %td
     - if custom_emoji.local?
       - if custom_emoji.visible_in_picker
diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml
index 20ffb8529..89ea3a6fe 100644
--- a/app/views/admin/custom_emojis/index.html.haml
+++ b/app/views/admin/custom_emojis/index.html.haml
@@ -17,6 +17,20 @@
         - else
           = filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil
 
+= form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do
+  .fields-group
+    - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key|
+      - if params[key].present?
+        = hidden_field_tag key, params[key]
+
+    - %i(shortcode by_domain).each do |key|
+      .input.string.optional
+        = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.custom_emojis.#{key}")
+
+    .actions
+      %button= t('admin.accounts.search')
+      = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
+
 .table-wrapper
   %table.table
     %thead