about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorDavid Yip <yipdw@member.fsf.org>2018-04-04 13:55:42 -0500
committerDavid Yip <yipdw@member.fsf.org>2018-04-04 13:55:42 -0500
commitbda1782cd864ed3aabb5a4d87359a1cb7595f4a6 (patch)
tree4ecb8623959b616fec9ab2a9c855048fb8c2da49 /app
parent77b650b69c8146a2acf4e7d270343f89c9838690 (diff)
parent6611100480c86c07972c1223e7231c136966e11d (diff)
Merge remote-tracking branch 'personal/merge/tootsuite/master' into gs-master
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/accounts_controller.rb13
-rw-r--r--app/controllers/admin/report_notes_controller.rb49
-rw-r--r--app/controllers/admin/reports_controller.rb20
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/helpers/admin/action_logs_helper.rb2
-rw-r--r--app/javascript/mastodon/actions/accounts.js2
-rw-r--r--app/javascript/mastodon/actions/alerts.js25
-rw-r--r--app/javascript/mastodon/actions/compose.js7
-rw-r--r--app/javascript/mastodon/actions/lists.js3
-rw-r--r--app/javascript/mastodon/actions/push_notifications/registerer.js15
-rw-r--r--app/javascript/mastodon/actions/settings.js5
-rw-r--r--app/javascript/mastodon/containers/status_container.js6
-rw-r--r--app/javascript/mastodon/features/ui/components/embed_modal.js3
-rw-r--r--app/javascript/mastodon/locales/ar.json6
-rw-r--r--app/javascript/mastodon/locales/ca.json2
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json13
-rw-r--r--app/javascript/mastodon/locales/eo.json18
-rw-r--r--app/javascript/mastodon/locales/fa.json10
-rw-r--r--app/javascript/mastodon/locales/fr.json42
-rw-r--r--app/javascript/mastodon/locales/ja.json2
-rw-r--r--app/javascript/mastodon/locales/ko.json4
-rw-r--r--app/javascript/mastodon/locales/ru.json30
-rw-r--r--app/javascript/mastodon/locales/sk.json8
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json172
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json4
-rw-r--r--app/javascript/mastodon/middleware/errors.js24
-rw-r--r--app/javascript/mastodon/reducers/compose.js2
-rw-r--r--app/javascript/mastodon/reducers/search.js7
-rw-r--r--app/javascript/mastodon/storage/modifier.js30
-rw-r--r--app/models/account.rb2
-rw-r--r--app/models/report.rb4
-rw-r--r--app/models/report_note.rb21
-rw-r--r--app/policies/account_policy.rb4
-rw-r--r--app/policies/report_note_policy.rb17
-rw-r--r--app/services/activitypub/process_account_service.rb2
-rw-r--r--app/views/admin/accounts/show.html.haml8
-rw-r--r--app/views/admin/report_notes/_report_note.html.haml11
-rw-r--r--app/views/admin/reports/_report.html.haml5
-rw-r--r--app/views/admin/reports/index.html.haml1
-rw-r--r--app/views/admin/reports/show.html.haml84
40 files changed, 469 insertions, 218 deletions
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index 7428c3f22..e7ca6b907 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -2,7 +2,7 @@
 
 module Admin
   class AccountsController < BaseController
-    before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize]
+    before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :enable, :disable, :memorialize]
     before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
     before_action :require_local_account!, only: [:enable, :disable, :memorialize]
 
@@ -60,6 +60,17 @@ module Admin
       redirect_to admin_account_path(@account.id)
     end
 
+    def remove_avatar
+      authorize @account, :remove_avatar?
+
+      @account.avatar = nil
+      @account.save!
+
+      log_action :remove_avatar, @account.user
+
+      redirect_to admin_account_path(@account.id)
+    end
+
     private
 
     def set_account
diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb
new file mode 100644
index 000000000..ef8c0f469
--- /dev/null
+++ b/app/controllers/admin/report_notes_controller.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Admin
+  class ReportNotesController < BaseController
+    before_action :set_report_note, only: [:destroy]
+
+    def create
+      authorize ReportNote, :create?
+
+      @report_note = current_account.report_notes.new(resource_params)
+
+      if @report_note.save
+        if params[:create_and_resolve]
+          @report_note.report.update!(action_taken: true, action_taken_by_account_id: current_account.id)
+          log_action :resolve, @report_note.report
+
+          redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
+        else
+          redirect_to admin_report_path(@report_note.report_id), notice: I18n.t('admin.report_notes.created_msg')
+        end
+      else
+        @report       = @report_note.report
+        @report_notes = @report.notes.latest
+        @form = Form::StatusBatch.new
+
+        render template: 'admin/reports/show'
+      end
+    end
+
+    def destroy
+      authorize @report_note, :destroy?
+      @report_note.destroy!
+      redirect_to admin_report_path(@report_note.report_id), notice: I18n.t('admin.report_notes.destroyed_msg')
+    end
+
+    private
+
+    def resource_params
+      params.require(:report_note).permit(
+        :content,
+        :report_id
+      )
+    end
+
+    def set_report_note
+      @report_note = ReportNote.find(params[:id])
+    end
+  end
+end
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index 75db6b78a..fc3785e3b 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -11,19 +11,35 @@ module Admin
 
     def show
       authorize @report, :show?
+      @report_note = @report.notes.new
+      @report_notes = @report.notes.latest
       @form = Form::StatusBatch.new
     end
 
     def update
       authorize @report, :update?
       process_report
-      redirect_to admin_report_path(@report)
+
+      if @report.action_taken?
+        redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
+      else
+        redirect_to admin_report_path(@report)
+      end
     end
 
     private
 
     def process_report
       case params[:outcome].to_s
+      when 'assign_to_self'
+        @report.update!(assigned_account_id: current_account.id)
+        log_action :assigned_to_self, @report
+      when 'unassign'
+        @report.update!(assigned_account_id: nil)
+        log_action :unassigned, @report
+      when 'reopen'
+        @report.update!(action_taken: false, action_taken_by_account_id: nil)
+        log_action :reopen, @report
       when 'resolve'
         @report.update!(action_taken_by_current_attributes)
         log_action :resolve, @report
@@ -32,11 +48,13 @@ module Admin
         log_action :resolve, @report
         log_action :suspend, @report.target_account
         resolve_all_target_account_reports
+        @report.reload
       when 'silence'
         @report.target_account.update!(silenced: true)
         log_action :resolve, @report
         log_action :silence, @report.target_account
         resolve_all_target_account_reports
+        @report.reload
       else
         raise ActiveRecord::RecordNotFound
       end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index fc745eaec..158c0c10e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -40,11 +40,11 @@ class ApplicationController < ActionController::Base
   end
 
   def require_admin!
-    redirect_to root_path unless current_user&.admin?
+    forbidden unless current_user&.admin?
   end
 
   def require_staff!
-    redirect_to root_path unless current_user&.staff?
+    forbidden unless current_user&.staff?
   end
 
   def check_suspension
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index 4475034a5..7c26c0b05 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -86,7 +86,7 @@ module Admin::ActionLogsHelper
       opposite_verbs?(log) ? 'negative' : 'positive'
     when :update, :reset_password, :disable_2fa, :memorialize
       'neutral'
-    when :demote, :silence, :disable, :suspend
+    when :demote, :silence, :disable, :suspend, :remove_avatar, :reopen
       'negative'
     when :destroy
       opposite_verbs?(log) ? 'positive' : 'negative'
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index 7cacff909..28ae56763 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -103,7 +103,7 @@ export function fetchAccount(id) {
       dispatch(importFetchedAccount(response.data));
     })).then(() => {
       dispatch(fetchAccountSuccess());
-    }, error => {
+    }).catch(error => {
       dispatch(fetchAccountFail(id, error));
     });
   };
diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js
index f37fdeeb6..3f5d7ef46 100644
--- a/app/javascript/mastodon/actions/alerts.js
+++ b/app/javascript/mastodon/actions/alerts.js
@@ -1,3 +1,10 @@
+import { defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+  unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
+  unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
+});
+
 export const ALERT_SHOW    = 'ALERT_SHOW';
 export const ALERT_DISMISS = 'ALERT_DISMISS';
 export const ALERT_CLEAR   = 'ALERT_CLEAR';
@@ -22,3 +29,21 @@ export function showAlert(title, message) {
     message,
   };
 };
+
+export function showAlertForError(error) {
+  if (error.response) {
+    const { data, status, statusText } = error.response;
+
+    let message = statusText;
+    let title   = `${status}`;
+
+    if (data.error) {
+      message = data.error;
+    }
+
+    return showAlert(title, message);
+  } else {
+    console.error(error);
+    return showAlert(messages.unexpectedTitle, messages.unexpectedMessage);
+  }
+}
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 2138f9426..59aa6f98d 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -1,11 +1,12 @@
 import api from '../api';
-import { CancelToken } from 'axios';
+import { CancelToken, isCancel } from 'axios';
 import { throttle } from 'lodash';
 import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
 import { tagHistory } from '../settings';
 import { useEmoji } from './emojis';
 import { importFetchedAccounts } from './importer';
 import { updateTimeline } from './timelines';
+import { showAlertForError } from './alerts';
 
 let cancelFetchComposeSuggestionsAccounts;
 
@@ -291,6 +292,10 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
   }).then(response => {
     dispatch(importFetchedAccounts(response.data));
     dispatch(readyComposeSuggestionsAccounts(token, response.data));
+  }).catch(error => {
+    if (!isCancel(error)) {
+      dispatch(showAlertForError(error));
+    }
   });
 }, 200, { leading: true, trailing: true });
 
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
index 12d60e3a3..12cb17159 100644
--- a/app/javascript/mastodon/actions/lists.js
+++ b/app/javascript/mastodon/actions/lists.js
@@ -1,5 +1,6 @@
 import api from '../api';
 import { importFetchedAccounts } from './importer';
+import { showAlertForError } from './alerts';
 
 export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
 export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
@@ -236,7 +237,7 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
   api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
     dispatch(importFetchedAccounts(data));
     dispatch(fetchListSuggestionsReady(q, data));
-  });
+  }).catch(error => dispatch(showAlertForError(error)));
 };
 
 export const fetchListSuggestionsReady = (query, accounts) => ({
diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js
index 51e68cad1..f17d929a6 100644
--- a/app/javascript/mastodon/actions/push_notifications/registerer.js
+++ b/app/javascript/mastodon/actions/push_notifications/registerer.js
@@ -116,14 +116,11 @@ export function register () {
             pushNotificationsSetting.remove(me);
           }
 
-          try {
-            getRegistration()
-              .then(getPushSubscription)
-              .then(unsubscribe);
-          } catch (e) {
-
-          }
-        });
+          return getRegistration()
+            .then(getPushSubscription)
+            .then(unsubscribe);
+        })
+        .catch(console.warn);
     } else {
       console.warn('Your browser does not support Web Push Notifications.');
     }
@@ -143,6 +140,6 @@ export function saveSettings() {
       if (me) {
         pushNotificationsSetting.set(me, data);
       }
-    });
+    }).catch(console.warn);
   };
 }
diff --git a/app/javascript/mastodon/actions/settings.js b/app/javascript/mastodon/actions/settings.js
index b96383daa..5634a11ef 100644
--- a/app/javascript/mastodon/actions/settings.js
+++ b/app/javascript/mastodon/actions/settings.js
@@ -1,5 +1,6 @@
 import api from '../api';
 import { debounce } from 'lodash';
+import { showAlertForError } from './alerts';
 
 export const SETTING_CHANGE = 'SETTING_CHANGE';
 export const SETTING_SAVE   = 'SETTING_SAVE';
@@ -23,7 +24,9 @@ const debouncedSave = debounce((dispatch, getState) => {
 
   const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
 
-  api(getState).put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
+  api(getState).put('/api/web/settings', { data })
+    .then(() => dispatch({ type: SETTING_SAVE }))
+    .catch(error => dispatch(showAlertForError(error)));
 }, 5000, { trailing: true });
 
 export function saveSettings() {
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index 8ba1015b5..4579bd132 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -27,6 +27,7 @@ import { initReport } from '../actions/reports';
 import { openModal } from '../actions/modal';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { boostModal, deleteModal } from '../initial_state';
+import { showAlertForError } from '../actions/alerts';
 
 const messages = defineMessages({
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@@ -83,7 +84,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 
   onEmbed (status) {
-    dispatch(openModal('EMBED', { url: status.get('url') }));
+    dispatch(openModal('EMBED', {
+      url: status.get('url'),
+      onError: error => dispatch(showAlertForError(error)),
+    }));
   },
 
   onDelete (status) {
diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.js b/app/javascript/mastodon/features/ui/components/embed_modal.js
index d440a8826..52aab00d0 100644
--- a/app/javascript/mastodon/features/ui/components/embed_modal.js
+++ b/app/javascript/mastodon/features/ui/components/embed_modal.js
@@ -10,6 +10,7 @@ export default class EmbedModal extends ImmutablePureComponent {
   static propTypes = {
     url: PropTypes.string.isRequired,
     onClose: PropTypes.func.isRequired,
+    onError: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
   }
 
@@ -35,6 +36,8 @@ export default class EmbedModal extends ImmutablePureComponent {
       iframeDocument.body.style.margin = 0;
       this.iframe.width  = iframeDocument.body.scrollWidth;
       this.iframe.height = iframeDocument.body.scrollHeight;
+    }).catch(error => {
+      this.props.onError(error);
     });
   }
 
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 3078b5b8c..34e34411f 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -2,7 +2,7 @@
   "account.block": "حظر @{name}",
   "account.block_domain": "إخفاء كل شيئ قادم من إسم النطاق {domain}",
   "account.blocked": "محظور",
-  "account.direct": "Direct Message @{name}",
+  "account.direct": "Direct message @{name}",
   "account.disclaimer_full": "قد لا تعكس المعلومات أدناه الملف الشخصي الكامل للمستخدم.",
   "account.domain_blocked": "النطاق مخفي",
   "account.edit_profile": "تعديل الملف الشخصي",
@@ -66,7 +66,7 @@
   "compose_form.publish": "بوّق",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "لقد تم تحديد هذه الصورة كحساسة",
-  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
+  "compose_form.sensitive.unmarked": "لم يتم تحديد الصورة كحساسة",
   "compose_form.spoiler.marked": "إنّ النص مخفي وراء تحذير",
   "compose_form.spoiler.unmarked": "النص غير مخفي",
   "compose_form.spoiler_placeholder": "تنبيه عن المحتوى",
@@ -221,7 +221,7 @@
   "reply_indicator.cancel": "إلغاء",
   "report.forward": "التحويل إلى {target}",
   "report.forward_hint": "هذا الحساب ينتمي إلى خادوم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا ؟",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.hint": "سوف يتم إرسال التقرير إلى مُشرِفي مثيل خادومكم. بإمكانك الإدلاء بشرح عن سبب الإبلاغ عن الحساب أسفله :",
   "report.placeholder": "تعليقات إضافية",
   "report.submit": "إرسال",
   "report.target": "إبلاغ",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index ec5a8a1d6..b7f95a664 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -2,7 +2,7 @@
   "account.block": "Bloca @{name}",
   "account.block_domain": "Amaga-ho tot de {domain}",
   "account.blocked": "Bloquejat",
-  "account.direct": "Direct Message @{name}",
+  "account.direct": "Direct message @{name}",
   "account.disclaimer_full": "La informació següent pot reflectir incompleta el perfil de l'usuari.",
   "account.domain_blocked": "Domini ocult",
   "account.edit_profile": "Edita el perfil",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 2a89c1153..dd249adf1 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -2,6 +2,19 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Oops!",
+        "id": "alert.unexpected.title"
+      },
+      {
+        "defaultMessage": "An unexpected error occurred.",
+        "id": "alert.unexpected.message"
+      }
+    ],
+    "path": "app/javascript/mastodon/actions/alerts.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "{name} mentioned you",
         "id": "notification.mention"
       }
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 82b749417..6dee6e544 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -2,7 +2,7 @@
   "account.block": "Bloki @{name}",
   "account.block_domain": "Kaŝi ĉion de {domain}",
   "account.blocked": "Blokita",
-  "account.direct": "Direct Message @{name}",
+  "account.direct": "Direct message @{name}",
   "account.disclaimer_full": "Subaj informoj povas reflekti la profilon de la uzanto nekomplete.",
   "account.domain_blocked": "Domajno kaŝita",
   "account.edit_profile": "Redakti profilon",
@@ -17,8 +17,8 @@
   "account.mute": "Silentigi @{name}",
   "account.mute_notifications": "Silentigi sciigojn el @{name}",
   "account.muted": "Silentigita",
-  "account.posts": "Mesaĝoj",
-  "account.posts_with_replies": "Mesaĝoj kun respondoj",
+  "account.posts": "Tootoj",
+  "account.posts_with_replies": "Kun respondoj",
   "account.report": "Signali @{name}",
   "account.requested": "Atendo de aprobo. Alklaku por nuligi peton de sekvado",
   "account.share": "Diskonigi la profilon de @{name}",
@@ -65,10 +65,10 @@
   "compose_form.placeholder": "Pri kio vi pensas?",
   "compose_form.publish": "Hup",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.marked": "Media is marked as sensitive",
-  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
-  "compose_form.spoiler.marked": "Text is hidden behind warning",
-  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.sensitive.marked": "Aŭdovidaĵo markita tikla",
+  "compose_form.sensitive.unmarked": "Aŭdovidaĵo ne markita tikla",
+  "compose_form.spoiler.marked": "Teksto kaŝita malantaŭ averto",
+  "compose_form.spoiler.unmarked": "Teksto ne kaŝita",
   "compose_form.spoiler_placeholder": "Skribu vian averton ĉi tie",
   "confirmation_modal.cancel": "Nuligi",
   "confirmations.block.confirm": "Bloki",
@@ -260,9 +260,9 @@
   "status.sensitive_warning": "Tikla enhavo",
   "status.share": "Diskonigi",
   "status.show_less": "Malgrandigi",
-  "status.show_less_all": "Show less for all",
+  "status.show_less_all": "Malgrandigi ĉiujn",
   "status.show_more": "Grandigi",
-  "status.show_more_all": "Show more for all",
+  "status.show_more_all": "Grandigi ĉiujn",
   "status.unmute_conversation": "Malsilentigi konversacion",
   "status.unpin": "Depingli de profilo",
   "tabs_bar.federated_timeline": "Fratara tempolinio",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 4b64ca353..61cdcd00a 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -1,10 +1,10 @@
 {
   "account.block": "مسدودسازی @{name}",
   "account.block_domain": "پنهان‌سازی همه چیز از سرور {domain}",
-  "account.blocked": "Blocked",
-  "account.direct": "Direct Message @{name}",
+  "account.blocked": "مسدودشده",
+  "account.direct": "Direct message @{name}",
   "account.disclaimer_full": "اطلاعات زیر ممکن است نمایهٔ این کاربر را به تمامی نشان ندهد.",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "دامین پنهان‌شده",
   "account.edit_profile": "ویرایش نمایه",
   "account.follow": "پی بگیرید",
   "account.followers": "پیگیران",
@@ -16,9 +16,9 @@
   "account.moved_to": "{name} منتقل شده است به:",
   "account.mute": "بی‌صدا کردن @{name}",
   "account.mute_notifications": "بی‌صداکردن اعلان‌ها از طرف @{name}",
-  "account.muted": "Muted",
+  "account.muted": "بی‌صداشده",
   "account.posts": "نوشته‌ها",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "نوشته‌ها و پاسخ‌ها",
   "account.report": "گزارش @{name}",
   "account.requested": "در انتظار پذیرش",
   "account.share": "هم‌رسانی نمایهٔ @{name}",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 58e6ad54d..57c55c9bd 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -2,7 +2,7 @@
   "account.block": "Bloquer @{name}",
   "account.block_domain": "Tout masquer venant de {domain}",
   "account.blocked": "Bloqué",
-  "account.direct": "Direct Message @{name}",
+  "account.direct": "Direct message @{name}",
   "account.disclaimer_full": "Les données ci-dessous peuvent ne pas refléter ce profil dans sa totalité.",
   "account.domain_blocked": "Domaine caché",
   "account.edit_profile": "Modifier le profil",
@@ -20,7 +20,7 @@
   "account.posts": "Pouets",
   "account.posts_with_replies": "Pouets avec réponses",
   "account.report": "Signaler",
-  "account.requested": "Invitation envoyée",
+  "account.requested": "En attente d'approbation. Cliquez pour annuler la requête",
   "account.share": "Partager le profil de @{name}",
   "account.show_reblogs": "Afficher les partages de @{name}",
   "account.unblock": "Débloquer",
@@ -88,7 +88,7 @@
   "emoji_button.activity": "Activités",
   "emoji_button.custom": "Personnalisés",
   "emoji_button.flags": "Drapeaux",
-  "emoji_button.food": "Boire et manger",
+  "emoji_button.food": "Nourriture & Boisson",
   "emoji_button.label": "Insérer un émoji",
   "emoji_button.nature": "Nature",
   "emoji_button.not_found": "Pas d'emojis !! (╯°□°)╯︵ ┻━┻",
@@ -98,14 +98,14 @@
   "emoji_button.search": "Recherche…",
   "emoji_button.search_results": "Résultats de la recherche",
   "emoji_button.symbols": "Symboles",
-  "emoji_button.travel": "Lieux et voyages",
+  "emoji_button.travel": "Lieux & Voyages",
   "empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir !",
   "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": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres personnes.",
   "empty_column.home.public_timeline": "le fil public",
   "empty_column.list": "Il n'y a rien dans cette liste pour l'instant. Dès que des personnes de cette liste 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",
+  "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres personnes pour débuter la conversation.",
+  "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des personnes d’autres instances pour remplir le fil public",
   "follow_request.authorize": "Accepter",
   "follow_request.reject": "Rejeter",
   "getting_started.appsshort": "Applications",
@@ -122,9 +122,9 @@
   "keyboard_shortcuts.back": "revenir en arrière",
   "keyboard_shortcuts.boost": "partager",
   "keyboard_shortcuts.column": "focaliser un statut dans l'une des colonnes",
-  "keyboard_shortcuts.compose": "pour centrer la zone de redaction",
+  "keyboard_shortcuts.compose": "pour centrer la zone de rédaction",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "descendre dans la liste",
+  "keyboard_shortcuts.down": "pour descendre dans la liste",
   "keyboard_shortcuts.enter": "pour ouvrir le statut",
   "keyboard_shortcuts.favourite": "vers les favoris",
   "keyboard_shortcuts.heading": "Raccourcis clavier",
@@ -151,7 +151,7 @@
   "media_gallery.toggle_visible": "Modifier la visibilité",
   "missing_indicator.label": "Non trouvé",
   "missing_indicator.sublabel": "Ressource introuvable",
-  "mute_modal.hide_notifications": "Masquer les notifications de cet utilisateur ?",
+  "mute_modal.hide_notifications": "Masquer les notifications de cette personne ?",
   "navigation_bar.blocks": "Comptes bloqués",
   "navigation_bar.community_timeline": "Fil public local",
   "navigation_bar.domain_blocks": "Hidden domains",
@@ -170,7 +170,7 @@
   "notification.follow": "{name} vous suit",
   "notification.mention": "{name} vous a mentionné⋅e :",
   "notification.reblog": "{name} a partagé votre statut :",
-  "notifications.clear": "Nettoyer",
+  "notifications.clear": "Nettoyer les notifications",
   "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
   "notifications.column_settings.alert": "Notifications locales",
   "notifications.column_settings.favourite": "Favoris :",
@@ -183,12 +183,12 @@
   "notifications.column_settings.sound": "Émettre un son",
   "onboarding.done": "Effectué",
   "onboarding.next": "Suivant",
-  "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateur⋅ice⋅s de {domain}.",
-  "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te·s les utilisateur⋅ice·s que vous suivez.",
-  "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous.",
-  "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.",
-  "onboarding.page_one.full_handle": "Votre pleine maîtrise",
-  "onboarding.page_one.handle_hint": "C'est ce que vous diriez à vos amis de rechercher.",
+  "onboarding.page_five.public_timelines": "Le fil public global affiche les messages de toutes les personnes suivies par les membres de {domain}. Le fil public local est identique mais se limite aux membres de {domain}.",
+  "onboarding.page_four.home": "L’Accueil affiche les messages des personnes que vous suivez.",
+  "onboarding.page_four.notifications": "La colonne de notification vous avertit lors d'une interaction avec vous.",
+  "onboarding.page_one.federation": "Mastodon est un réseau de serveurs indépendants qui se joignent pour former un réseau social plus vaste. Nous appelons ces serveurs des instances.",
+  "onboarding.page_one.full_handle": "Votre identifiant complet",
+  "onboarding.page_one.handle_hint": "C'est ce que vos amis devront rechercher.",
   "onboarding.page_one.welcome": "Bienvenue sur Mastodon !",
   "onboarding.page_six.admin": "L’administrateur⋅ice de votre instance est {admin}.",
   "onboarding.page_six.almost_done": "Nous y sommes presque…",
@@ -199,7 +199,7 @@
   "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !",
   "onboarding.page_six.various_app": "applications mobiles",
   "onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d’autres préférences.",
-  "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateur⋅ice⋅s et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅ice complet.",
+  "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateur⋅ice⋅s ou regardez des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son identifiant complet.",
   "onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.",
   "onboarding.skip": "Passer",
   "privacy.change": "Ajuster la confidentialité du message",
@@ -227,16 +227,16 @@
   "report.target": "Signalement",
   "search.placeholder": "Rechercher",
   "search_popout.search_format": "Recherche avancée",
-  "search_popout.tips.full_text": "Les textes simples retournent les pouets que vous avez écris, mis en favori, épinglés, ou ayant été mentionnés, ainsi que les noms d'utilisateurs, les noms affichés, et les hashtags correspondant.",
+  "search_popout.tips.full_text": "Les textes simples retournent les pouets que vous avez écris, mis en favori, épinglés, ou ayant été mentionnés, ainsi que les identifiants, les noms affichés, et les hashtags des personnes et messages correspondant.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "statuts",
-  "search_popout.tips.text": "Un texte simple renvoie les noms affichés, les noms d’utilisateur⋅ice et les hashtags correspondants",
+  "search_popout.tips.text": "Un texte simple renvoie les noms affichés, les identifiants et les hashtags correspondants",
   "search_popout.tips.user": "utilisateur⋅ice",
   "search_results.accounts": "Personnes",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Pouets",
   "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
-  "standalone.public_title": "Jeter un coup d’œil…",
+  "standalone.public_title": "Un aperçu …",
   "status.block": "Block @{name}",
   "status.cannot_reblog": "Cette publication ne peut être boostée",
   "status.delete": "Effacer",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index cd3023795..dbea02989 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -27,7 +27,7 @@
   "account.unblock_domain": "{domain}を表示",
   "account.unfollow": "フォロー解除",
   "account.unmute": "@{name}さんのミュートを解除",
-  "account.unmute_notifications": "@{name}さんからの通知を受け取る",
+  "account.unmute_notifications": "@{name}さんからの通知を受け取るようにする",
   "account.view_full_profile": "全ての情報を見る",
   "alert.unexpected.message": "不明なエラーが発生しました",
   "alert.unexpected.title": "エラー",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 449df42b8..89fde8966 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -2,7 +2,7 @@
   "account.block": "@{name}을 차단",
   "account.block_domain": "{domain} 전체를 숨김",
   "account.blocked": "차단 됨",
-  "account.direct": "Direct Message @{name}",
+  "account.direct": "Direct message @{name}",
   "account.disclaimer_full": "여기 있는 정보는 유저의 프로파일을 정확히 반영하지 못 할 수도 있습니다.",
   "account.domain_blocked": "도메인 숨겨짐",
   "account.edit_profile": "프로필 편집",
@@ -17,7 +17,7 @@
   "account.mute": "@{name} 뮤트",
   "account.mute_notifications": "@{name}의 알림을 뮤트",
   "account.muted": "뮤트 됨",
-  "account.posts": "게시물",
+  "account.posts": "툿",
   "account.posts_with_replies": "툿과 답장",
   "account.report": "@{name} 신고",
   "account.requested": "승인 대기 중. 클릭해서 취소하기",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 15959092c..8616ef98f 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -1,10 +1,10 @@
 {
   "account.block": "Блокировать",
   "account.block_domain": "Блокировать все с {domain}",
-  "account.blocked": "Blocked",
-  "account.direct": "Direct Message @{name}",
+  "account.blocked": "Заблокирован(а)",
+  "account.direct": "Написать @{name}",
   "account.disclaimer_full": "Нижеуказанная информация может не полностью отражать профиль пользователя.",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "Домен скрыт",
   "account.edit_profile": "Изменить профиль",
   "account.follow": "Подписаться",
   "account.followers": "Подписаны",
@@ -16,9 +16,9 @@
   "account.moved_to": "Ищите {name} здесь:",
   "account.mute": "Заглушить",
   "account.mute_notifications": "Скрыть уведомления от @{name}",
-  "account.muted": "Muted",
+  "account.muted": "Приглушён",
   "account.posts": "Посты",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "Посты с ответами",
   "account.report": "Пожаловаться",
   "account.requested": "Ожидает подтверждения",
   "account.share": "Поделиться профилем @{name}",
@@ -29,8 +29,8 @@
   "account.unmute": "Снять глушение",
   "account.unmute_notifications": "Показывать уведомления от @{name}",
   "account.view_full_profile": "Показать полный профиль",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "Что-то пошло не так.",
+  "alert.unexpected.title": "Ой!",
   "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
   "bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.",
   "bundle_column_error.retry": "Попробовать снова",
@@ -40,7 +40,7 @@
   "bundle_modal_error.retry": "Попробовать снова",
   "column.blocks": "Список блокировки",
   "column.community": "Локальная лента",
-  "column.domain_blocks": "Hidden domains",
+  "column.domain_blocks": "Скрытые домены",
   "column.favourites": "Понравившееся",
   "column.follow_requests": "Запросы на подписку",
   "column.home": "Главная",
@@ -65,10 +65,10 @@
   "compose_form.placeholder": "О чем Вы думаете?",
   "compose_form.publish": "Трубить",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.marked": "Media is marked as sensitive",
-  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
-  "compose_form.spoiler.marked": "Text is hidden behind warning",
-  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.sensitive.marked": "Медиафайлы не отмечены как чувствительные",
+  "compose_form.sensitive.unmarked": "Медиафайлы не отмечены как чувствительные",
+  "compose_form.spoiler.marked": "Текст скрыт за предупреждением",
+  "compose_form.spoiler.unmarked": "Текст не скрыт",
   "compose_form.spoiler_placeholder": "Напишите свое предупреждение здесь",
   "confirmation_modal.cancel": "Отмена",
   "confirmations.block.confirm": "Заблокировать",
@@ -154,7 +154,7 @@
   "mute_modal.hide_notifications": "Убрать уведомления от этого пользователя?",
   "navigation_bar.blocks": "Список блокировки",
   "navigation_bar.community_timeline": "Локальная лента",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.domain_blocks": "Скрытые домены",
   "navigation_bar.edit_profile": "Изменить профиль",
   "navigation_bar.favourites": "Понравившееся",
   "navigation_bar.follow_requests": "Запросы на подписку",
@@ -221,13 +221,13 @@
   "reply_indicator.cancel": "Отмена",
   "report.forward": "Forward to {target}",
   "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.hint": "Жалоба будет отправлена модераторам Вашего сервера. Вы также можете указать подробную причину жалобы ниже:",
   "report.placeholder": "Комментарий",
   "report.submit": "Отправить",
   "report.target": "Жалуемся на",
   "search.placeholder": "Поиск",
   "search_popout.search_format": "Продвинутый формат поиска",
-  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.full_text": "Возвращает посты, которые Вы написали, отметили как 'избранное', продвинули или в которых были упомянуты, а также содержащие юзернейм, имя и хэштеги.",
   "search_popout.tips.hashtag": "хэштег",
   "search_popout.tips.status": "статус",
   "search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 7bfae0302..925b46df6 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -2,7 +2,7 @@
   "account.block": "Blokovať @{name}",
   "account.block_domain": "Ukryť všetko z {domain}",
   "account.blocked": "Blokovaný/á",
-  "account.direct": "Direct Message @{name}",
+  "account.direct": "Direct message @{name}",
   "account.disclaimer_full": "Inofrmácie nižšie nemusia byť úplným odrazom uživateľovho účtu.",
   "account.domain_blocked": "Doména ukrytá",
   "account.edit_profile": "Upraviť profil",
@@ -42,12 +42,12 @@
   "column.community": "Lokálna časová os",
   "column.domain_blocks": "Hidden domains",
   "column.favourites": "Obľúbené",
-  "column.follow_requests": "Žiadosti o sledovaní",
+  "column.follow_requests": "Žiadosti o sledovanie",
   "column.home": "Domov",
   "column.lists": "Zoznamy",
   "column.mutes": "Ignorovaní užívatelia",
   "column.notifications": "Notifikácie",
-  "column.pins": "Pripnuté toots",
+  "column.pins": "Pripnuté tooty",
   "column.public": "Federovaná časová os",
   "column_back_button.label": "Späť",
   "column_header.hide_settings": "Skryť nastavenia",
@@ -163,7 +163,7 @@
   "navigation_bar.lists": "Zoznamy",
   "navigation_bar.logout": "Odhlásiť",
   "navigation_bar.mutes": "Ignorovaní užívatelia",
-  "navigation_bar.pins": "Pripnuté toots",
+  "navigation_bar.pins": "Pripnuté tooty",
   "navigation_bar.preferences": "Voľby",
   "navigation_bar.public_timeline": "Federovaná časová os",
   "notification.favourite": "{name} sa páči tvoj status",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 0504a8c7a..1cbc9f1c5 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -1,36 +1,36 @@
 {
   "account.block": "封鎖 @{name}",
   "account.block_domain": "隱藏來自 {domain} 的一切文章",
-  "account.blocked": "Blocked",
-  "account.direct": "Direct Message @{name}",
+  "account.blocked": "封鎖",
+  "account.direct": "私訊 @{name}",
   "account.disclaimer_full": "下列資料不一定完整。",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "服務站被隱藏",
   "account.edit_profile": "修改個人資料",
   "account.follow": "關注",
   "account.followers": "關注的人",
   "account.follows": "正關注",
   "account.follows_you": "關注你",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "隱藏 @{name} 的轉推",
   "account.media": "媒體",
   "account.mention": "提及 @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} 已經遷移到:",
   "account.mute": "將 @{name} 靜音",
-  "account.mute_notifications": "Mute notifications from @{name}",
-  "account.muted": "Muted",
+  "account.mute_notifications": "將來自 @{name} 的通知靜音",
+  "account.muted": "靜音",
   "account.posts": "文章",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "包含回覆的文章",
   "account.report": "舉報 @{name}",
   "account.requested": "等候審批",
   "account.share": "分享 @{name} 的個人資料",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "顯示 @{name} 的推文",
   "account.unblock": "解除對 @{name} 的封鎖",
   "account.unblock_domain": "不再隱藏 {domain}",
   "account.unfollow": "取消關注",
   "account.unmute": "取消 @{name} 的靜音",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "取消來自 @{name} 通知的靜音",
   "account.view_full_profile": "查看完整資料",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "發生不可預期的錯誤。",
+  "alert.unexpected.title": "噢!",
   "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
   "bundle_column_error.body": "加載本組件出錯。",
   "bundle_column_error.retry": "重試",
@@ -40,11 +40,11 @@
   "bundle_modal_error.retry": "重試",
   "column.blocks": "封鎖用戶",
   "column.community": "本站時間軸",
-  "column.domain_blocks": "Hidden domains",
+  "column.domain_blocks": "隱藏的服務站",
   "column.favourites": "最愛的文章",
   "column.follow_requests": "關注請求",
   "column.home": "主頁",
-  "column.lists": "Lists",
+  "column.lists": "列表",
   "column.mutes": "靜音名單",
   "column.notifications": "通知",
   "column.pins": "置頂文章",
@@ -58,25 +58,25 @@
   "column_header.unpin": "取下",
   "column_subheading.navigation": "瀏覽",
   "column_subheading.settings": "設定",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
-  "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+  "compose_form.direct_message_warning": "這文章只有被提及的用戶才可以看到。",
+  "compose_form.hashtag_warning": "這文章因為不是公開,所以不會被標籤搜索。只有公開的文章才會被標籤搜索。",
   "compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。",
   "compose_form.lock_disclaimer.lock": "公共",
   "compose_form.placeholder": "你在想甚麼?",
   "compose_form.publish": "發文",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.marked": "Media is marked as sensitive",
-  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
-  "compose_form.spoiler.marked": "Text is hidden behind warning",
-  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.sensitive.marked": "媒體被標示為敏感",
+  "compose_form.sensitive.unmarked": "媒體沒有被標示為敏感",
+  "compose_form.spoiler.marked": "文字被警告隱藏",
+  "compose_form.spoiler.unmarked": "文字沒有被隱藏",
   "compose_form.spoiler_placeholder": "敏感警告訊息",
   "confirmation_modal.cancel": "取消",
   "confirmations.block.confirm": "封鎖",
   "confirmations.block.message": "你確定要封鎖{name}嗎?",
   "confirmations.delete.confirm": "刪除",
   "confirmations.delete.message": "你確定要刪除{name}嗎?",
-  "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.confirm": "刪除",
+  "confirmations.delete_list.message": "你確定要永久刪除這列表嗎?",
   "confirmations.domain_block.confirm": "隱藏整個網站",
   "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。",
   "confirmations.mute.confirm": "靜音",
@@ -86,7 +86,7 @@
   "embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。",
   "embed.preview": "看上去會是這樣:",
   "emoji_button.activity": "活動",
-  "emoji_button.custom": "Custom",
+  "emoji_button.custom": "自訂",
   "emoji_button.flags": "旗幟",
   "emoji_button.food": "飲飲食食",
   "emoji_button.label": "加入表情符號",
@@ -94,9 +94,9 @@
   "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "物品",
   "emoji_button.people": "人物",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "常用",
   "emoji_button.search": "搜尋…",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "搜尋結果",
   "emoji_button.symbols": "符號",
   "emoji_button.travel": "旅遊景物",
   "empty_column.community": "本站時間軸暫時未有內容,快文章來搶頭香啊!",
@@ -119,48 +119,48 @@
   "home.column_settings.show_reblogs": "顯示被轉推的文章",
   "home.column_settings.show_replies": "顯示回應文章",
   "home.settings": "欄位設定",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
-  "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.back": "後退",
+  "keyboard_shortcuts.boost": "轉推",
+  "keyboard_shortcuts.column": "把標示移動到其中一列",
+  "keyboard_shortcuts.compose": "把標示移動到文字輸入區",
+  "keyboard_shortcuts.description": "描述",
+  "keyboard_shortcuts.down": "在列表往下移動",
+  "keyboard_shortcuts.enter": "打開文章",
+  "keyboard_shortcuts.favourite": "收藏",
+  "keyboard_shortcuts.heading": "鍵盤快速鍵",
+  "keyboard_shortcuts.hotkey": "快速鍵",
+  "keyboard_shortcuts.legend": "顯示這個說明",
+  "keyboard_shortcuts.mention": "提及作者",
+  "keyboard_shortcuts.reply": "回覆",
+  "keyboard_shortcuts.search": "把標示移動到搜索",
+  "keyboard_shortcuts.toot": "新的推文",
+  "keyboard_shortcuts.unfocus": "把標示移離文字輸入和搜索",
+  "keyboard_shortcuts.up": "在列表往上移動",
   "lightbox.close": "關閉",
   "lightbox.next": "繼續",
   "lightbox.previous": "回退",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
-  "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.account.add": "新增到列表",
+  "lists.account.remove": "從列表刪除",
+  "lists.delete": "刪除列表",
+  "lists.edit": "編輯列表",
+  "lists.new.create": "新增列表",
+  "lists.new.title_placeholder": "新列表標題",
+  "lists.search": "從你關注的用戶中搜索",
+  "lists.subheading": "列表",
   "loading_indicator.label": "載入中...",
   "media_gallery.toggle_visible": "打開或關上",
   "missing_indicator.label": "找不到內容",
-  "missing_indicator.sublabel": "This resource could not be found",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "missing_indicator.sublabel": "無法找到內容",
+  "mute_modal.hide_notifications": "隱藏來自這用戶的通知嗎?",
   "navigation_bar.blocks": "被你封鎖的用戶",
   "navigation_bar.community_timeline": "本站時間軸",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.domain_blocks": "隱藏的服務站",
   "navigation_bar.edit_profile": "修改個人資料",
   "navigation_bar.favourites": "最愛的內容",
   "navigation_bar.follow_requests": "關注請求",
   "navigation_bar.info": "關於本服務站",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.keyboard_shortcuts": "鍵盤快速鍵",
+  "navigation_bar.lists": "列表",
   "navigation_bar.logout": "登出",
   "navigation_bar.mutes": "被你靜音的用戶",
   "navigation_bar.pins": "置頂文章",
@@ -187,8 +187,8 @@
   "onboarding.page_four.home": "「主頁」顯示你所關注用戶的文章",
   "onboarding.page_four.notifications": "「通知」欄顯示你和其他人的互動。",
   "onboarding.page_one.federation": "Mastodon(萬象社交)是由一批獨立網站組成的龐大網絡,我們將這些獨立又互連網站稱為「服務站」(instance)",
-  "onboarding.page_one.full_handle": "Your full handle",
-  "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+  "onboarding.page_one.full_handle": "你的帳號全名",
+  "onboarding.page_one.handle_hint": "朋友可以從這個帳號全名找到你",
   "onboarding.page_one.welcome": "歡迎使用 Mastodon(萬象社交)",
   "onboarding.page_six.admin": "你服務站的管理員是{admin}",
   "onboarding.page_six.almost_done": "差不多了……",
@@ -211,33 +211,33 @@
   "privacy.public.short": "公共",
   "privacy.unlisted.long": "公開,但不在公共時間軸顯示",
   "privacy.unlisted.short": "公開",
-  "regeneration_indicator.label": "Loading…",
-  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
-  "relative_time.days": "{number}d",
-  "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
-  "relative_time.minutes": "{number}m",
-  "relative_time.seconds": "{number}s",
+  "regeneration_indicator.label": "載入中……",
+  "regeneration_indicator.sublabel": "你的主頁時間軸正在準備中!",
+  "relative_time.days": "{number}日",
+  "relative_time.hours": "{number}小時",
+  "relative_time.just_now": "剛剛",
+  "relative_time.minutes": "{number}分鐘",
+  "relative_time.seconds": "{number}秒",
   "reply_indicator.cancel": "取消",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.forward": "轉寄到 {target}",
+  "report.forward_hint": "這個帳戶屬於其他服務站。要向該服務站發送匿名的舉報訊息嗎?",
+  "report.hint": "這訊息會發送到你服務站的管理員。你可以提供舉報這個帳戶的理由:",
   "report.placeholder": "額外訊息",
   "report.submit": "提交",
   "report.target": "舉報",
   "search.placeholder": "搜尋",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
-  "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.user": "user",
-  "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "search_popout.search_format": "高級搜索格式",
+  "search_popout.tips.full_text": "輸入簡單的文字,搜索由你發放、收藏、轉推和提及你的文章,以及符合的用戶名稱,帳號名稱和標籤。",
+  "search_popout.tips.hashtag": "標籤",
+  "search_popout.tips.status": "文章",
+  "search_popout.tips.text": "輸入簡單的文字,搜索符合的用戶名稱,帳號名稱和標籤。",
+  "search_popout.tips.user": "用戶",
+  "search_results.accounts": "使用者",
+  "search_results.hashtags": "標籤",
+  "search_results.statuses": "文章",
   "search_results.total": "{count, number} 項結果",
   "standalone.public_title": "站點一瞥…",
-  "status.block": "Block @{name}",
+  "status.block": "封鎖 @{name}",
   "status.cannot_reblog": "這篇文章無法被轉推",
   "status.delete": "刪除",
   "status.embed": "鑲嵌",
@@ -245,12 +245,12 @@
   "status.load_more": "載入更多",
   "status.media_hidden": "隱藏媒體內容",
   "status.mention": "提及 @{name}",
-  "status.more": "More",
-  "status.mute": "Mute @{name}",
+  "status.more": "更多",
+  "status.mute": "把 @{name} 靜音",
   "status.mute_conversation": "靜音對話",
   "status.open": "展開文章",
   "status.pin": "置頂到資料頁",
-  "status.pinned": "Pinned toot",
+  "status.pinned": "置頂文章",
   "status.reblog": "轉推",
   "status.reblogged_by": "{name} 轉推",
   "status.reply": "回應",
@@ -258,22 +258,22 @@
   "status.report": "舉報 @{name}",
   "status.sensitive_toggle": "點擊顯示",
   "status.sensitive_warning": "敏感內容",
-  "status.share": "Share",
+  "status.share": "分享",
   "status.show_less": "減少顯示",
-  "status.show_less_all": "Show less for all",
+  "status.show_less_all": "減少顯示這類文章",
   "status.show_more": "顯示更多",
-  "status.show_more_all": "Show more for all",
+  "status.show_more_all": "顯示更多這類文章",
   "status.unmute_conversation": "解禁對話",
   "status.unpin": "解除置頂",
   "tabs_bar.federated_timeline": "跨站",
   "tabs_bar.home": "主頁",
   "tabs_bar.local_timeline": "本站",
   "tabs_bar.notifications": "通知",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "如果你現在離開 Mastodon,你的草稿內容將會被丟棄。",
   "upload_area.title": "將檔案拖放至此上載",
   "upload_button.label": "上載媒體檔案",
-  "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
+  "upload_form.description": "為視覺障礙人士添加文字說明",
+  "upload_form.focus": "裁切",
   "upload_form.undo": "還原",
   "upload_progress.label": "上載中……",
   "video.close": "關閉影片",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index fab7ecf06..c7925829f 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -2,7 +2,7 @@
   "account.block": "封鎖 @{name}",
   "account.block_domain": "隱藏來自 {domain} 的一切貼文",
   "account.blocked": "Blocked",
-  "account.direct": "Direct Message @{name}",
+  "account.direct": "Direct message @{name}",
   "account.disclaimer_full": "下列資料不一定完整。",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "編輯用者資訊",
@@ -103,7 +103,7 @@
   "empty_column.hashtag": "這個主題標籤下什麼都沒有。",
   "empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用者。",
   "empty_column.home.public_timeline": "公開時間軸",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "此份清單尚未有東西。當此清單的成員貼出了新的狀態時,它們就會出現在這裡。",
   "empty_column.notifications": "還沒有任何通知。和別的使用者互動來開始對話。",
   "empty_column.public": "這裡什麼都沒有!公開寫些什麼,或是關注其他副本的使用者。",
   "follow_request.authorize": "授權",
diff --git a/app/javascript/mastodon/middleware/errors.js b/app/javascript/mastodon/middleware/errors.js
index 72e5631e6..3cebb42e0 100644
--- a/app/javascript/mastodon/middleware/errors.js
+++ b/app/javascript/mastodon/middleware/errors.js
@@ -1,34 +1,14 @@
-import { defineMessages } from 'react-intl';
-import { showAlert } from '../actions/alerts';
+import { showAlertForError } from '../actions/alerts';
 
 const defaultFailSuffix = 'FAIL';
 
-const messages = defineMessages({
-  unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
-  unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
-});
-
 export default function errorsMiddleware() {
   return ({ dispatch }) => next => action => {
     if (action.type && !action.skipAlert) {
       const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
 
       if (action.type.match(isFail)) {
-        if (action.error.response) {
-          const { data, status, statusText } = action.error.response;
-
-          let message = statusText;
-          let title   = `${status}`;
-
-          if (data.error) {
-            message = data.error;
-          }
-
-          dispatch(showAlert(title, message));
-        } else {
-          console.error(action.error);
-          dispatch(showAlert(messages.unexpectedTitle, messages.unexpectedMessage));
-        }
+        dispatch(showAlertForError(action.error));
       }
     }
 
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index a48c46941..1f4177585 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -265,7 +265,7 @@ export default function compose(state = initialState, action) {
       .set('idempotencyKey', uuid());
   case COMPOSE_DIRECT:
     return state
-      .update('text', text => `${text}@${action.account.get('acct')} `)
+      .update('text', text => `@${action.account.get('acct')} `)
       .set('privacy', 'direct')
       .set('focusDate', new Date())
       .set('idempotencyKey', uuid());
diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js
index 08d90e4e8..56fd7226b 100644
--- a/app/javascript/mastodon/reducers/search.js
+++ b/app/javascript/mastodon/reducers/search.js
@@ -4,7 +4,11 @@ import {
   SEARCH_FETCH_SUCCESS,
   SEARCH_SHOW,
 } from '../actions/search';
-import { COMPOSE_MENTION, COMPOSE_REPLY } from '../actions/compose';
+import {
+  COMPOSE_MENTION,
+  COMPOSE_REPLY,
+  COMPOSE_DIRECT,
+} from '../actions/compose';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableMap({
@@ -29,6 +33,7 @@ export default function search(state = initialState, action) {
     return state.set('hidden', false);
   case COMPOSE_REPLY:
   case COMPOSE_MENTION:
+  case COMPOSE_DIRECT:
     return state.set('hidden', true);
   case SEARCH_FETCH_SUCCESS:
     return state.set('results', ImmutableMap({
diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js
index 1bec04d0f..4773d07a9 100644
--- a/app/javascript/mastodon/storage/modifier.js
+++ b/app/javascript/mastodon/storage/modifier.js
@@ -9,6 +9,12 @@ const limit = 1024;
 // https://webkit.org/status/#specification-service-workers
 const asyncCache = window.caches ? caches.open('mastodon-system') : Promise.reject();
 
+function printErrorIfAvailable(error) {
+  if (error) {
+    console.warn(error);
+  }
+}
+
 function put(name, objects, onupdate, oncreate) {
   return asyncDB.then(db => new Promise((resolve, reject) => {
     const putTransaction = db.transaction(name, 'readwrite');
@@ -77,7 +83,9 @@ function evictAccountsByRecords(records) {
 
     function evict(toEvict) {
       toEvict.forEach(record => {
-        asyncCache.then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])));
+        asyncCache
+          .then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])))
+          .catch(printErrorIfAvailable);
 
         accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result);
 
@@ -90,11 +98,11 @@ function evictAccountsByRecords(records) {
     }
 
     evict(records);
-  });
+  }).catch(printErrorIfAvailable);
 }
 
 export function evictStatus(id) {
-  return evictStatuses([id]);
+  evictStatuses([id]);
 }
 
 export function evictStatuses(ids) {
@@ -110,7 +118,7 @@ export function evictStatuses(ids) {
       idIndex.getKey(id).onsuccess =
         ({ target }) => target.result && store.delete(target.result);
     });
-  });
+  }).catch(printErrorIfAvailable);
 }
 
 function evictStatusesByRecords(records) {
@@ -127,7 +135,9 @@ export function putAccounts(records) {
         const oldURL = target.result[key];
 
         if (newURL !== oldURL) {
-          asyncCache.then(cache => cache.delete(oldURL));
+          asyncCache
+            .then(cache => cache.delete(oldURL))
+            .catch(printErrorIfAvailable);
         }
       });
 
@@ -145,10 +155,14 @@ export function putAccounts(records) {
     oncomplete();
   }).then(records => {
     evictAccountsByRecords(records);
-    asyncCache.then(cache => cache.addAll(newURLs));
-  });
+    asyncCache
+      .then(cache => cache.addAll(newURLs))
+      .catch(printErrorIfAvailable);
+  }).catch(printErrorIfAvailable);
 }
 
 export function putStatuses(records) {
-  put('statuses', records).then(evictStatusesByRecords);
+  put('statuses', records)
+    .then(evictStatusesByRecords)
+    .catch(printErrorIfAvailable);
 }
diff --git a/app/models/account.rb b/app/models/account.rb
index 4b73f1fa3..79d5bf742 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -97,6 +97,8 @@ class Account < ApplicationRecord
   has_many :reports
   has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id
 
+  has_many :report_notes, dependent: :destroy
+
   # Moderation notes
   has_many :account_moderation_notes, dependent: :destroy
   has_many :targeted_moderation_notes, class_name: 'AccountModerationNote', foreign_key: :target_account_id, dependent: :destroy
diff --git a/app/models/report.rb b/app/models/report.rb
index dd123fc15..f5b37cb6d 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -12,12 +12,16 @@
 #  account_id                 :integer          not null
 #  action_taken_by_account_id :integer
 #  target_account_id          :integer          not null
+#  assigned_account_id        :integer
 #
 
 class Report < ApplicationRecord
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
   belongs_to :action_taken_by_account, class_name: 'Account', optional: true
+  belongs_to :assigned_account, class_name: 'Account', optional: true
+
+  has_many :notes, class_name: 'ReportNote', foreign_key: :report_id, inverse_of: :report, dependent: :destroy
 
   scope :unresolved, -> { where(action_taken: false) }
   scope :resolved,   -> { where(action_taken: true) }
diff --git a/app/models/report_note.rb b/app/models/report_note.rb
new file mode 100644
index 000000000..3d12cf7b6
--- /dev/null
+++ b/app/models/report_note.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: report_notes
+#
+#  id         :integer          not null, primary key
+#  content    :text             not null
+#  report_id  :integer          not null
+#  account_id :integer          not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class ReportNote < ApplicationRecord
+  belongs_to :account
+  belongs_to :report, inverse_of: :notes
+
+  scope :latest, -> { reorder('created_at ASC') }
+
+  validates :content, presence: true, length: { maximum: 500 }
+end
diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb
index 85e2c8419..efabe80d0 100644
--- a/app/policies/account_policy.rb
+++ b/app/policies/account_policy.rb
@@ -29,6 +29,10 @@ class AccountPolicy < ApplicationPolicy
     admin?
   end
 
+  def remove_avatar?
+    staff?
+  end
+
   def subscribe?
     admin?
   end
diff --git a/app/policies/report_note_policy.rb b/app/policies/report_note_policy.rb
new file mode 100644
index 000000000..694bc096b
--- /dev/null
+++ b/app/policies/report_note_policy.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ReportNotePolicy < ApplicationPolicy
+  def create?
+    staff?
+  end
+
+  def destroy?
+    admin? || owner?
+  end
+
+  private
+
+  def owner?
+    record.account_id == current_account&.id
+  end
+end
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index cf8462821..21c2fc57a 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -28,7 +28,7 @@ class ActivityPub::ProcessAccountService < BaseService
 
     after_protocol_change! if protocol_changed?
     after_key_change! if key_changed?
-    check_featured_collection! if @account.featured_collection_url.present?
+    check_featured_collection! if @account&.featured_collection_url&.present?
 
     @account
   rescue Oj::ParseError
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index dbbf5fc09..fecfd6cc8 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -14,6 +14,14 @@
         %th= t('admin.accounts.display_name')
         %td= @account.display_name
 
+      %tr
+        %th= t('admin.accounts.avatar')
+        %th
+          = link_to @account.avatar.url(:original) do
+            = image_tag @account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
+          - if @account.local? && @account.avatar?
+            = table_link_to 'trash', t('admin.accounts.remove_avatar'), remove_avatar_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:remove_avatar, @account)
+
       - if @account.local?
         %tr
           %th= t('admin.accounts.role')
diff --git a/app/views/admin/report_notes/_report_note.html.haml b/app/views/admin/report_notes/_report_note.html.haml
new file mode 100644
index 000000000..60ac5d0d5
--- /dev/null
+++ b/app/views/admin/report_notes/_report_note.html.haml
@@ -0,0 +1,11 @@
+%tr
+  %td
+    %p
+      %strong= report_note.account.acct
+      on
+      %time.formatted{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) }
+        = l report_note.created_at
+      = table_link_to 'trash', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete if can?(:destroy, report_note)
+      %br/
+      %br/
+    = simple_format(h(report_note.content))
diff --git a/app/views/admin/reports/_report.html.haml b/app/views/admin/reports/_report.html.haml
index d5eb161b9..d266f4840 100644
--- a/app/views/admin/reports/_report.html.haml
+++ b/app/views/admin/reports/_report.html.haml
@@ -18,4 +18,9 @@
         = fa_icon('camera')
         = report.media_attachments.count
   %td
+    - if report.assigned_account.nil?
+      \-
+    - else
+      = link_to report.assigned_account.acct, admin_account_path(report.assigned_account.id)
+  %td
     = table_link_to 'circle', t('admin.reports.view'), admin_report_path(report)
diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml
index 577c68a86..3b127c4fc 100644
--- a/app/views/admin/reports/index.html.haml
+++ b/app/views/admin/reports/index.html.haml
@@ -20,6 +20,7 @@
           %th= t('admin.reports.reported_by')
           %th= t('admin.reports.comment.label')
           %th= t('admin.reports.report_contents')
+          %th= t('admin.reports.assigned')
           %th
       %tbody
         = render @reports
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index 7dd962bf2..12a52eb33 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -1,24 +1,68 @@
 - content_for :page_title do
   = t('admin.reports.report', id: @report.id)
 
+%div{ style: 'overflow: hidden; margin-bottom: 20px' }
+  - if !@report.action_taken?
+    %div{ style: 'float: right' }
+      = link_to t('admin.reports.silence_account'), admin_report_path(@report, outcome: 'silence'), method: :put, class: 'button'
+      = link_to t('admin.reports.suspend_account'), admin_report_path(@report, outcome: 'suspend'), method: :put, class: 'button'
+    %div{ style: 'float: left' }
+      = link_to t('admin.reports.mark_as_resolved'), admin_report_path(@report, outcome: 'resolve'), method: :put, class: 'button'
+  - else
+    = link_to t('admin.reports.mark_as_unresolved'), admin_report_path(@report, outcome: 'reopen'), method: :put, class: 'button'
+
+.table-wrapper
+  %table.table.inline-table
+    %tbody
+      %tr
+        %th= t('admin.reports.updated_at')
+        %td{colspan: 2}
+          %time.formatted{ datetime: @report.updated_at.iso8601 }
+      %tr
+        %th= t('admin.reports.status')
+        %td{colspan: 2}
+          - if @report.action_taken?
+            = t('admin.reports.resolved')
+            = table_link_to 'envelope-open', t('admin.reports.reopen'), admin_report_path(@report, outcome: 'reopen'), method: :put
+          - else
+            = t('admin.reports.unresolved')
+      - if !@report.action_taken_by_account.nil?
+        %tr
+          %th= t('admin.reports.action_taken_by')
+          %td= @report.action_taken_by_account.acct
+      - else
+        %tr
+          %th= t('admin.reports.assigned')
+          %td
+            - if @report.assigned_account.nil?
+              \-
+            - else
+              = link_to @report.assigned_account.acct, admin_account_path(@report.assigned_account.id)
+          %td{style: "text-align: right"}
+            - if @report.assigned_account != current_user.account
+              = table_link_to 'user', t('admin.reports.assign_to_self'), admin_report_path(@report, outcome: 'assign_to_self'), method: :put
+            - if !@report.assigned_account.nil?
+              = table_link_to 'trash', t('admin.reports.unassign'), admin_report_path(@report, outcome: 'unassign'), method: :put
+
 .report-accounts
   .report-accounts__item
-    %strong= t('admin.reports.reported_account')
+    %h3= t('admin.reports.reported_account')
     = render 'authorize_follows/card', account: @report.target_account, admin: true
     = render 'admin/accounts/card', account: @report.target_account
   .report-accounts__item
-    %strong= t('admin.reports.reported_by')
+    %h3= t('admin.reports.reported_by')
     = render 'authorize_follows/card', account: @report.account, admin: true
     = render 'admin/accounts/card', account: @report.account
 
-%p
-  %strong= t('admin.reports.comment.label')
-  \:
-  = simple_format(@report.comment.presence || t('admin.reports.comment.none'))
+%h3= t('admin.reports.comment.label')
+
+= simple_format(@report.comment.presence || t('admin.reports.comment.none'))
 
 - unless @report.statuses.empty?
   %hr/
 
+  %h3= t('admin.reports.statuses')
+
   = form_for(@form, url: admin_report_reported_statuses_path(@report.id)) do |f|
     .batch-form-box
       .batch-checkbox-all
@@ -43,14 +87,20 @@
 
 %hr/
 
-- if !@report.action_taken?
-  %div{ style: 'overflow: hidden' }
-    %div{ style: 'float: right' }
-      = link_to t('admin.reports.silence_account'), admin_report_path(@report, outcome: 'silence'), method: :put, class: 'button'
-      = link_to t('admin.reports.suspend_account'), admin_report_path(@report, outcome: 'suspend'), method: :put, class: 'button'
-    %div{ style: 'float: left' }
-      = link_to t('admin.reports.mark_as_resolved'), admin_report_path(@report, outcome: 'resolve'), method: :put, class: 'button'
-- elsif !@report.action_taken_by_account.nil?
-  %p
-    %strong #{t('admin.reports.action_taken_by')}:
-    = @report.action_taken_by_account.acct
+%h3= t('admin.reports.notes.label')
+
+- if @report_notes.length > 0
+  .table-wrapper
+    %table.table
+      %thead
+        %tr
+          %th
+      %tbody
+        = render @report_notes
+
+= simple_form_for @report_note, url: admin_report_notes_path do |f|
+  = render 'shared/error_messages', object: @report_note
+  = f.input :content
+  = f.hidden_field :report_id
+  = f.button :button, t('admin.reports.notes.create'), type: :submit
+  = f.button :button, t('admin.reports.notes.create_and_resolve'), type: :submit, name: :create_and_resolve