about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/accounts_controller.rb12
-rw-r--r--app/controllers/activitypub/follows_controller.rb22
-rw-r--r--app/controllers/application_controller.rb17
-rw-r--r--app/controllers/emojis_controller.rb10
-rw-r--r--app/controllers/statuses_controller.rb24
-rw-r--r--app/helpers/admin/action_logs_helper.rb2
-rw-r--r--app/helpers/routing_helper.rb5
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js18
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json27
-rw-r--r--app/javascript/mastodon/locales/sr.json24
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json24
-rw-r--r--app/javascript/styles/mastodon/admin.scss10
-rw-r--r--app/models/user.rb8
-rw-r--r--app/serializers/activitypub/delete_actor_serializer.rb22
-rw-r--r--app/serializers/rest/instance_serializer.rb2
-rw-r--r--app/services/batched_remove_status_service.rb38
-rw-r--r--app/services/fetch_atom_service.rb8
-rw-r--r--app/services/suspend_account_service.rb12
18 files changed, 155 insertions, 130 deletions
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 31144fe05..c9725ed00 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -2,7 +2,8 @@
 
 class AccountsController < ApplicationController
   include AccountControllerConcern
-  include SignatureVerification
+
+  before_action :set_cache_headers
 
   def show
     respond_to do |format|
@@ -27,10 +28,11 @@ class AccountsController < ApplicationController
       end
 
       format.json do
-        render json: @account,
-               serializer: ActivityPub::ActorSerializer,
-               adapter: ActivityPub::Adapter,
-               content_type: 'application/activity+json'
+        skip_session!
+
+        render_cached_json(['activitypub', 'actor', @account.cache_key], content_type: 'application/activity+json') do
+          ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
+        end
       end
     end
   end
diff --git a/app/controllers/activitypub/follows_controller.rb b/app/controllers/activitypub/follows_controller.rb
index 8b1cddeb4..038bcbabc 100644
--- a/app/controllers/activitypub/follows_controller.rb
+++ b/app/controllers/activitypub/follows_controller.rb
@@ -4,15 +4,19 @@ class ActivityPub::FollowsController < Api::BaseController
   include SignatureVerification
 
   def show
-    render(
-      json: FollowRequest.includes(:account).references(:account).find_by!(
-        id: params.require(:id),
-        accounts: { domain: nil, username: params.require(:account_username) },
-        target_account: signed_request_account
-      ),
-      serializer: ActivityPub::FollowSerializer,
-      adapter: ActivityPub::Adapter,
-      content_type: 'application/activity+json'
+    render json: follow_request,
+           serializer: ActivityPub::FollowSerializer,
+           adapter: ActivityPub::Adapter,
+           content_type: 'application/activity+json'
+  end
+
+  private
+
+  def follow_request
+    FollowRequest.includes(:account).references(:account).find_by!(
+      id: params.require(:id),
+      accounts: { domain: nil, username: params.require(:account_username) },
+      target_account: signed_request_account
     )
   end
 end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 46367f202..679b94f1e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -198,11 +198,24 @@ class ApplicationController < ActionController::Base
   end
 
   def render_cached_json(cache_key, **options)
+    options[:expires_in] ||= 3.minutes
+    options[:public]     ||= true
+    cache_key              = cache_key.join(':') if cache_key.is_a?(Enumerable)
+    content_type           = options.delete(:content_type) || 'application/json'
+
     data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do
       yield.to_json
     end
 
-    expires_in options[:expires_in], public: true
-    render json: data
+    expires_in options[:expires_in], public: options[:public]
+    render json: data, content_type: content_type
+  end
+
+  def set_cache_headers
+    response.headers['Vary'] = 'Accept'
+  end
+
+  def skip_session!
+    request.session_options[:skip] = true
   end
 end
diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb
index a82b9340b..c9725ccc0 100644
--- a/app/controllers/emojis_controller.rb
+++ b/app/controllers/emojis_controller.rb
@@ -2,14 +2,16 @@
 
 class EmojisController < ApplicationController
   before_action :set_emoji
+  before_action :set_cache_headers
 
   def show
     respond_to do |format|
       format.json do
-        render json: @emoji,
-               serializer: ActivityPub::EmojiSerializer,
-               adapter: ActivityPub::Adapter,
-               content_type: 'application/activity+json'
+        skip_session!
+
+        render_cached_json(['activitypub', 'emoji', @emoji.cache_key], content_type: 'application/activity+json') do
+          ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
+        end
       end
     end
   end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 6a635fba2..d67fac0e5 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -10,7 +10,7 @@ class StatusesController < ApplicationController
   before_action :set_link_headers
   before_action :check_account_suspension
   before_action :redirect_to_original, only: [:show]
-  before_action { response.headers['Vary'] = 'Accept' }
+  before_action :set_cache_headers
 
   def show
     respond_to do |format|
@@ -23,25 +23,21 @@ class StatusesController < ApplicationController
       end
 
       format.json do
-        render json: @status,
-               serializer: ActivityPub::NoteSerializer,
-               adapter: ActivityPub::Adapter,
-               content_type: 'application/activity+json'
-
-        # Allow HTTP caching for 3 minutes if the status is public
-        unless @stream_entry.hidden?
-          request.session_options[:skip] = true
-          expires_in(3.minutes, public: true)
+        skip_session! unless @stream_entry.hidden?
+
+        render_cached_json(['activitypub', 'note', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
+          ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
         end
       end
     end
   end
 
   def activity
-    render json: @status,
-           serializer: ActivityPub::ActivitySerializer,
-           adapter: ActivityPub::Adapter,
-           content_type: 'application/activity+json'
+    skip_session!
+
+    render_cached_json(['activitypub', 'activity', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
+      ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
+    end
   end
 
   def embed
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index e85243e57..4475034a5 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -34,7 +34,7 @@ module Admin::ActionLogsHelper
       link_to attributes['domain'], "https://#{attributes['domain']}"
     when 'Status'
       tmp_status = Status.new(attributes)
-      link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
+      link_to tmp_status.account&.acct || "##{tmp_status.account_id}", TagManager.instance.url_for(tmp_status)
     end
   end
 
diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb
index 11894a895..998b7566f 100644
--- a/app/helpers/routing_helper.rb
+++ b/app/helpers/routing_helper.rb
@@ -4,6 +4,7 @@ module RoutingHelper
   extend ActiveSupport::Concern
   include Rails.application.routes.url_helpers
   include ActionView::Helpers::AssetTagHelper
+  include Webpacker::Helper
 
   included do
     def default_url_options
@@ -17,6 +18,10 @@ module RoutingHelper
     URI.join(root_url, source).to_s
   end
 
+  def full_pack_url(source, **options)
+    full_asset_url(asset_pack_path(source, options))
+  end
+
   private
 
   def use_storage?
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 3c1619c24..ee789e180 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -70,30 +70,28 @@ export default class GettingStarted extends ImmutablePureComponent {
 
     navItems.push(
       <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
-      <ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
-      <ColumnLink key='6' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
+      <ColumnLink key='5' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
     );
 
     if (myAccount.get('locked')) {
-      navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
+      navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
     }
 
-    navItems.push(
-      <ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
-      <ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
-    );
-
     if (multiColumn) {
-      navItems.push(<ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
+      navItems.push(<ColumnLink key='7' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
     }
 
+    navItems.push(<ColumnLink key='8' icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />);
+
     return (
       <Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
         <div className='getting-started__wrapper'>
           <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
           {navItems}
           <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
-          <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
+          <ColumnLink icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />
+          <ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
+          <ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
           <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
           <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
         </div>
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index 971ffb5c5..88631e332 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -11,7 +11,7 @@
   "account.media": "Mediji",
   "account.mention": "Pomeni korisnika @{name}",
   "account.moved_to": "{name} se pomerio na:",
-  "account.mute": "Mutiraj @{name}",
+  "account.mute": "Ućutkaj korisnika @{name}",
   "account.mute_notifications": "Isključi obaveštenja od korisnika @{name}",
   "account.posts": "Statusa",
   "account.report": "Prijavi @{name}",
@@ -21,7 +21,7 @@
   "account.unblock": "Odblokiraj korisnika @{name}",
   "account.unblock_domain": "Odblokiraj domen {domain}",
   "account.unfollow": "Otprati",
-  "account.unmute": "Odmutiraj @{name}",
+  "account.unmute": "Ukloni ućutkavanje korisniku @{name}",
   "account.unmute_notifications": "Uključi nazad obaveštenja od korisnika @{name}",
   "account.view_full_profile": "Vidi ceo profil",
   "boost_modal.combo": "Možete pritisnuti {combo} da preskočite ovo sledeći put",
@@ -37,10 +37,10 @@
   "column.follow_requests": "Zahtevi za praćenje",
   "column.home": "Početna",
   "column.lists": "Liste",
-  "column.mutes": "Mutirani korisnici",
+  "column.mutes": "Ućutkani korisnici",
   "column.notifications": "Obaveštenja",
   "column.pins": "Prikačeni tutovi",
-  "column.public": "Združena lajna",
+  "column.public": "Federisana lajna",
   "column_back_button.label": "Nazad",
   "column_header.hide_settings": "Sakrij postavke",
   "column_header.moveLeft_settings": "Pomeri kolonu ulevo",
@@ -50,6 +50,7 @@
   "column_header.unpin": "Otkači",
   "column_subheading.navigation": "Navigacija",
   "column_subheading.settings": "Postavke",
+  "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.lock_disclaimer": "Vaš nalog nije {locked}. Svako može da Vas zaprati i da vidi objave namenjene samo Vašim pratiocima.",
   "compose_form.lock_disclaimer.lock": "zaključan",
   "compose_form.placeholder": "Šta Vam je na umu?",
@@ -66,9 +67,9 @@
   "confirmations.delete_list.confirm": "Obriši",
   "confirmations.delete_list.message": "Da li ste sigurni da želite da bespovratno obrišete ovu listu?",
   "confirmations.domain_block.confirm": "Sakrij ceo domen",
-  "confirmations.domain_block.message": "Da li ste stvarno, stvarno sigurno da želite da blokirate ceo domen {domain}? U većini slučajeva, par dobrih blokiranja ili mutiranja su dovoljna i preporučljiva.",
-  "confirmations.mute.confirm": "Mutiraj",
-  "confirmations.mute.message": "Da li stvarno želite da mutirate korisnika {name}?",
+  "confirmations.domain_block.message": "Da li ste stvarno, stvarno sigurno da želite da blokirate ceo domen {domain}? U većini slučajeva, par dobrih blokiranja ili ućutkavanja su dovoljna i preporučljiva.",
+  "confirmations.mute.confirm": "Ućutkaj",
+  "confirmations.mute.message": "Da li stvarno želite da ućutkate korisnika {name}?",
   "confirmations.unfollow.confirm": "Otprati",
   "confirmations.unfollow.message": "Da li ste sigurni da želite da otpratite korisnika {name}?",
   "embed.instructions": "Ugradi ovaj status na Vaš veb sajt kopiranjem koda ispod.",
@@ -148,10 +149,10 @@
   "navigation_bar.keyboard_shortcuts": "Prečice na tastaturi",
   "navigation_bar.lists": "Liste",
   "navigation_bar.logout": "Odjava",
-  "navigation_bar.mutes": "Mutirani korisnici",
+  "navigation_bar.mutes": "Ućutkani korisnici",
   "navigation_bar.pins": "Prikačeni tutovi",
   "navigation_bar.preferences": "Podešavanja",
-  "navigation_bar.public_timeline": "Združena lajna",
+  "navigation_bar.public_timeline": "Federisana lajna",
   "notification.favourite": "{name} je stavio Vaš status kao omiljeni",
   "notification.follow": "{name} Vas je zapratio",
   "notification.mention": "{name} Vas je pomenuo",
@@ -169,7 +170,7 @@
   "notifications.column_settings.sound": "Puštaj zvuk",
   "onboarding.done": "Gotovo",
   "onboarding.next": "Sledeće",
-  "onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Združena lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.",
+  "onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Federisana lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.",
   "onboarding.page_four.home": "Početna lajna prikazuje statuse ljudi koje Vi pratite.",
   "onboarding.page_four.notifications": "Kolona sa obaveštenjima Vam prikazuje kada neko priča sa Vama.",
   "onboarding.page_one.federation": "Mastodont je mreža nezavisnih servera koji se uvezuju da naprave jednu veću društvenu mrežu. Ove servere zovemo instancama.",
@@ -213,6 +214,7 @@
   "search_popout.tips.user": "korisnik",
   "search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}",
   "standalone.public_title": "Pogled iznutra...",
+  "status.block": "Block @{name}",
   "status.cannot_reblog": "Ovaj status ne može da se podrži",
   "status.delete": "Obriši",
   "status.embed": "Ugradi na sajt",
@@ -221,7 +223,8 @@
   "status.media_hidden": "Multimedija sakrivena",
   "status.mention": "Pomeni korisnika @{name}",
   "status.more": "Još",
-  "status.mute_conversation": "Mutiraj prepisku",
+  "status.mute": "Mute @{name}",
+  "status.mute_conversation": "Ućutkaj prepisku",
   "status.open": "Proširi ovaj status",
   "status.pin": "Prikači na profil",
   "status.reblog": "Podrži",
@@ -237,7 +240,7 @@
   "status.unmute_conversation": "Uključi prepisku",
   "status.unpin": "Otkači sa profila",
   "tabs_bar.compose": "Napiši",
-  "tabs_bar.federated_timeline": "Združeno",
+  "tabs_bar.federated_timeline": "Federisano",
   "tabs_bar.home": "Početna",
   "tabs_bar.local_timeline": "Lokalno",
   "tabs_bar.notifications": "Obaveštenja",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index 7824be2b2..e65c02ab7 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -11,7 +11,7 @@
   "account.media": "Медији",
   "account.mention": "Помени корисника @{name}",
   "account.moved_to": "{name} се померио на:",
-  "account.mute": "Мутирај @{name}",
+  "account.mute": "Ућуткај корисника @{name}",
   "account.mute_notifications": "Искључи обавештења од корисника @{name}",
   "account.posts": "Статуса",
   "account.report": "Пријави @{name}",
@@ -21,7 +21,7 @@
   "account.unblock": "Одблокирај корисника @{name}",
   "account.unblock_domain": "Одблокирај домен {domain}",
   "account.unfollow": "Отпрати",
-  "account.unmute": "Одмутирај @{name}",
+  "account.unmute": "Уклони ућуткавање кориснику @{name}",
   "account.unmute_notifications": "Укључи назад обавештења од корисника @{name}",
   "account.view_full_profile": "Види цео профил",
   "boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут",
@@ -37,10 +37,10 @@
   "column.follow_requests": "Захтеви за праћење",
   "column.home": "Почетна",
   "column.lists": "Листе",
-  "column.mutes": "Мутирани корисници",
+  "column.mutes": "Ућуткани корисници",
   "column.notifications": "Обавештења",
   "column.pins": "Прикачени тутови",
-  "column.public": "Здружена лајна",
+  "column.public": "Федерисана лајна",
   "column_back_button.label": "Назад",
   "column_header.hide_settings": "Сакриј поставке",
   "column_header.moveLeft_settings": "Помери колону улево",
@@ -67,9 +67,9 @@
   "confirmations.delete_list.confirm": "Обриши",
   "confirmations.delete_list.message": "Да ли сте сигурни да желите да бесповратно обришете ову листу?",
   "confirmations.domain_block.confirm": "Сакриј цео домен",
-  "confirmations.domain_block.message": "Да ли сте стварно, стварно сигурно да желите да блокирате цео домен {domain}? У већини случајева, пар добрих блокирања или мутирања су довољна и препоручљива.",
-  "confirmations.mute.confirm": "Мутирај",
-  "confirmations.mute.message": "Да ли стварно желите да мутирате корисника {name}?",
+  "confirmations.domain_block.message": "Да ли сте стварно, стварно сигурно да желите да блокирате цео домен {domain}? У већини случајева, пар добрих блокирања или ућуткавања су довољна и препоручљива.",
+  "confirmations.mute.confirm": "Ућуткај",
+  "confirmations.mute.message": "Да ли стварно желите да ућуткате корисника {name}?",
   "confirmations.unfollow.confirm": "Отпрати",
   "confirmations.unfollow.message": "Да ли сте сигурни да желите да отпратите корисника {name}?",
   "embed.instructions": "Угради овај статус на Ваш веб сајт копирањем кода испод.",
@@ -149,10 +149,10 @@
   "navigation_bar.keyboard_shortcuts": "Пречице на тастатури",
   "navigation_bar.lists": "Листе",
   "navigation_bar.logout": "Одјава",
-  "navigation_bar.mutes": "Мутирани корисници",
+  "navigation_bar.mutes": "Ућуткани корисници",
   "navigation_bar.pins": "Прикачени тутови",
   "navigation_bar.preferences": "Подешавања",
-  "navigation_bar.public_timeline": "Здружена лајна",
+  "navigation_bar.public_timeline": "Федерисана лајна",
   "notification.favourite": "{name} је ставио Ваш статус као омиљени",
   "notification.follow": "{name} Вас је запратио",
   "notification.mention": "{name} Вас је поменуо",
@@ -170,7 +170,7 @@
   "notifications.column_settings.sound": "Пуштај звук",
   "onboarding.done": "Готово",
   "onboarding.next": "Следеће",
-  "onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Здружена лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
+  "onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Федерисана лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
   "onboarding.page_four.home": "Почетна лајна приказује статусе људи које Ви пратите.",
   "onboarding.page_four.notifications": "Колона са обавештењима Вам приказује када неко прича са Вама.",
   "onboarding.page_one.federation": "Мастодонт је мрежа независних сервера који се увезују да направе једну већу друштвену мрежу. Ове сервере зовемо инстанцама.",
@@ -224,7 +224,7 @@
   "status.mention": "Помени корисника @{name}",
   "status.more": "Још",
   "status.mute": "Mute @{name}",
-  "status.mute_conversation": "Мутирај преписку",
+  "status.mute_conversation": "Ућуткај преписку",
   "status.open": "Прошири овај статус",
   "status.pin": "Прикачи на профил",
   "status.reblog": "Подржи",
@@ -240,7 +240,7 @@
   "status.unmute_conversation": "Укључи преписку",
   "status.unpin": "Откачи са профила",
   "tabs_bar.compose": "Напиши",
-  "tabs_bar.federated_timeline": "Здружено",
+  "tabs_bar.federated_timeline": "Федерисано",
   "tabs_bar.home": "Почетна",
   "tabs_bar.local_timeline": "Локално",
   "tabs_bar.notifications": "Обавештења",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 876fb0e13..65b174ab5 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -64,8 +64,8 @@
   "confirmations.block.message": "你確定要封鎖 {name} ?",
   "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.confirm": "刪除",
+  "confirmations.delete_list.message": "確定要永久性地刪除這個名單嗎?",
   "confirmations.domain_block.confirm": "隱藏整個網域",
   "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
   "confirmations.mute.confirm": "消音",
@@ -128,14 +128,14 @@
   "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": "找不到",
@@ -146,8 +146,8 @@
   "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": "置頂貼文",
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index bddea557b..0c343e1df 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -398,10 +398,12 @@
     }
   }
 
+  &__content {
+    max-width: calc(100% - 90px);
+  }
+
   &__title {
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
+    word-wrap: break-word;
   }
 
   &__timestamp {
@@ -415,7 +417,7 @@
     color: $ui-primary-color;
     font-family: 'mastodon-font-monospace', monospace;
     font-size: 12px;
-    white-space: nowrap;
+    word-wrap: break-word;
     min-height: 20px;
   }
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 231271f73..855ae908d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -126,18 +126,18 @@ class User < ApplicationRecord
   end
 
   def confirm
-    return if confirmed?
+    new_user = !confirmed?
 
     super
-    update_statistics!
+    update_statistics! if new_user
   end
 
   def confirm!
-    return if confirmed?
+    new_user = !confirmed?
 
     skip_confirmation!
     save!
-    update_statistics!
+    update_statistics! if new_user
   end
 
   def promote!
diff --git a/app/serializers/activitypub/delete_actor_serializer.rb b/app/serializers/activitypub/delete_actor_serializer.rb
new file mode 100644
index 000000000..dfea9db4a
--- /dev/null
+++ b/app/serializers/activitypub/delete_actor_serializer.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class ActivityPub::DeleteActorSerializer < ActiveModel::Serializer
+  attributes :id, :type, :actor
+  attribute :virtual_object, key: :object
+
+  def id
+    [ActivityPub::TagManager.instance.uri_for(object), '#delete'].join
+  end
+
+  def type
+    'Delete'
+  end
+
+  def actor
+    ActivityPub::TagManager.instance.uri_for(object)
+  end
+
+  def virtual_object
+    actor
+  end
+end
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index abbacc374..65907dad2 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -27,7 +27,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
   end
 
   def thumbnail
-    full_asset_url(instance_presenter.thumbnail.file.url) if instance_presenter.thumbnail
+    instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('preview.jpg')
   end
 
   def max_toot_chars
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index 21c775208..cb65a2256 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -17,9 +17,7 @@ class BatchedRemoveStatusService < BaseService
 
     @stream_entry_batches  = []
     @salmon_batches        = []
-    @activity_json_batches = []
     @json_payloads         = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id.to_s)] }.to_h
-    @activity_json         = {}
     @activity_xml          = {}
 
     # Ensure that rendered XML reflects destroyed state
@@ -32,10 +30,7 @@ class BatchedRemoveStatusService < BaseService
       unpush_from_home_timelines(account, account_statuses)
       unpush_from_list_timelines(account, account_statuses)
 
-      if account.local?
-        batch_stream_entries(account, account_statuses)
-        batch_activity_json(account, account_statuses)
-      end
+      batch_stream_entries(account, account_statuses) if account.local?
     end
 
     # Cannot be batched
@@ -47,7 +42,6 @@ class BatchedRemoveStatusService < BaseService
 
     Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch }
     NotificationWorker.push_bulk(@salmon_batches) { |batch| batch }
-    ActivityPub::DeliveryWorker.push_bulk(@activity_json_batches) { |batch| batch }
   end
 
   private
@@ -58,22 +52,6 @@ class BatchedRemoveStatusService < BaseService
     end
   end
 
-  def batch_activity_json(account, statuses)
-    account.followers.inboxes.each do |inbox_url|
-      statuses.each do |status|
-        @activity_json_batches << [build_json(status), account.id, inbox_url]
-      end
-    end
-
-    statuses.each do |status|
-      other_recipients = (status.mentions + status.reblogs).map(&:account).reject(&:local?).select(&:activitypub?).uniq(&:id)
-
-      other_recipients.each do |target_account|
-        @activity_json_batches << [build_json(status), account.id, target_account.inbox_url]
-      end
-    end
-  end
-
   def unpush_from_home_timelines(account, statuses)
     recipients = account.followers.local.to_a
 
@@ -134,23 +112,9 @@ class BatchedRemoveStatusService < BaseService
     Redis.current
   end
 
-  def build_json(status)
-    return @activity_json[status.id] if @activity_json.key?(status.id)
-
-    @activity_json[status.id] = sign_json(status, ActiveModelSerializers::SerializableResource.new(
-      status,
-      serializer: status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json)
-  end
-
   def build_xml(stream_entry)
     return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
 
     @activity_xml[stream_entry.id] = stream_entry_to_xml(stream_entry)
   end
-
-  def sign_json(status, json)
-    Oj.dump(ActivityPub::LinkedDataSignature.new(json).sign!(status.account))
-  end
 end
diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb
index 1c47a22da..c01e8d071 100644
--- a/app/services/fetch_atom_service.rb
+++ b/app/services/fetch_atom_service.rb
@@ -50,7 +50,7 @@ class FetchAtomService < BaseService
         @unsupported_activity = true
         nil
       end
-    elsif @response['Link'] && !terminal
+    elsif @response['Link'] && !terminal && link_header.find_link(%w(rel alternate))
       process_headers
     elsif @response.mime_type == 'text/html' && !terminal
       process_html
@@ -70,8 +70,6 @@ class FetchAtomService < BaseService
   end
 
   def process_headers
-    link_header = LinkHeader.parse(@response['Link'].is_a?(Array) ? @response['Link'].first : @response['Link'])
-
     json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'])
     atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml))
 
@@ -80,4 +78,8 @@ class FetchAtomService < BaseService
 
     result
   end
+
+  def link_header
+    @link_header ||= LinkHeader.parse(@response['Link'].is_a?(Array) ? @response['Link'].first : @response['Link'])
+  end
 end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 958b28cdc..56fa2d8dd 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -22,6 +22,8 @@ class SuspendAccountService < BaseService
   end
 
   def purge_content!
+    ActivityPub::RawDistributionWorker.perform_async(delete_actor_json, @account.id) if @account.local?
+
     @account.statuses.reorder(nil).find_in_batches do |statuses|
       BatchedRemoveStatusService.new.call(statuses)
     end
@@ -54,4 +56,14 @@ class SuspendAccountService < BaseService
   def destroy_all(association)
     association.in_batches.destroy_all
   end
+
+  def delete_actor_json
+    payload = ActiveModelSerializers::SerializableResource.new(
+      @account,
+      serializer: ActivityPub::DeleteActorSerializer,
+      adapter: ActivityPub::Adapter
+    ).as_json
+
+    Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
+  end
 end