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.rb2
-rw-r--r--app/controllers/activitypub/collections_controller.rb2
-rw-r--r--app/controllers/activitypub/outboxes_controller.rb5
-rw-r--r--app/controllers/admin/accounts_controller.rb4
-rw-r--r--app/controllers/application_controller.rb5
-rw-r--r--app/controllers/concerns/account_controller_concern.rb1
-rw-r--r--app/controllers/custom_css_controller.rb1
-rw-r--r--app/controllers/emojis_controller.rb2
-rw-r--r--app/controllers/follower_accounts_controller.rb5
-rw-r--r--app/controllers/following_accounts_controller.rb5
-rw-r--r--app/controllers/statuses_controller.rb12
-rw-r--r--app/controllers/stream_entries_controller.rb14
-rw-r--r--app/javascript/flavours/glitch/components/autosuggest_textarea.js5
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/compose_form.js11
-rw-r--r--app/javascript/flavours/glitch/features/list_timeline/index.js17
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js1
-rw-r--r--app/javascript/flavours/glitch/reducers/timelines.js4
-rw-r--r--app/javascript/mastodon/components/autosuggest_textarea.js5
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js13
-rw-r--r--app/javascript/mastodon/features/compose/components/search.js2
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js17
-rw-r--r--app/javascript/mastodon/reducers/compose.js1
-rw-r--r--app/javascript/mastodon/reducers/timelines.js8
-rw-r--r--app/javascript/styles/contrast/diff.scss2
-rw-r--r--app/javascript/styles/contrast/variables.scss2
-rw-r--r--app/javascript/styles/mailer.scss4
-rw-r--r--app/javascript/styles/mastodon/_mixins.scss10
-rw-r--r--app/javascript/styles/mastodon/admin.scss2
-rw-r--r--app/javascript/styles/mastodon/basics.scss5
-rw-r--r--app/javascript/styles/mastodon/components.scss71
-rw-r--r--app/javascript/styles/mastodon/containers.scss4
-rw-r--r--app/javascript/styles/mastodon/emoji_picker.scss8
-rw-r--r--app/javascript/styles/mastodon/forms.scss2
-rw-r--r--app/javascript/styles/mastodon/polls.scss1
-rw-r--r--app/javascript/styles/mastodon/rtl.scss1
-rw-r--r--app/lib/activitypub/activity.rb2
-rw-r--r--app/lib/activitypub/activity/follow.rb4
-rw-r--r--app/models/account.rb4
-rw-r--r--app/models/form/account_batch.rb9
-rw-r--r--app/models/status.rb6
-rw-r--r--app/services/after_block_domain_from_account_service.rb10
-rw-r--r--app/services/authorize_follow_service.rb8
-rw-r--r--app/services/block_service.rb8
-rw-r--r--app/services/concerns/payloadable.rb19
-rw-r--r--app/services/favourite_service.rb7
-rw-r--r--app/services/follow_service.rb7
-rw-r--r--app/services/process_mentions_service.rb8
-rw-r--r--app/services/reblog_service.rb7
-rw-r--r--app/services/reject_follow_service.rb8
-rw-r--r--app/services/remove_status_service.rb11
-rw-r--r--app/services/report_service.rb9
-rw-r--r--app/services/suspend_account_service.rb18
-rw-r--r--app/services/unblock_service.rb8
-rw-r--r--app/services/unfavourite_service.rb8
-rw-r--r--app/services/unfollow_service.rb14
-rw-r--r--app/services/vote_service.rb7
-rw-r--r--app/views/admin/pending_accounts/_account.html.haml2
-rw-r--r--app/workers/activitypub/distribute_poll_update_worker.rb15
-rw-r--r--app/workers/activitypub/distribution_worker.rb15
-rw-r--r--app/workers/activitypub/reply_distribution_worker.rb15
-rw-r--r--app/workers/activitypub/update_distribution_worker.rb11
61 files changed, 228 insertions, 266 deletions
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index fcdebb47f..051b6ecbd 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -47,8 +47,6 @@ class AccountsController < ApplicationController
       end
 
       format.json do
-        mark_cacheable!
-
         render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
           ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
         end
diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
index 853f4f907..012c3c538 100644
--- a/app/controllers/activitypub/collections_controller.rb
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -9,8 +9,6 @@ class ActivityPub::CollectionsController < Api::BaseController
   before_action :set_cache_headers
 
   def show
-    skip_session!
-
     render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
       ActiveModelSerializers::SerializableResource.new(
         collection_presenter,
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index 438fa226e..5147afbf7 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -10,10 +10,7 @@ class ActivityPub::OutboxesController < Api::BaseController
   before_action :set_cache_headers
 
   def show
-    unless page_requested?
-      skip_session!
-      expires_in 1.minute, public: true
-    end
+    expires_in 1.minute, public: true unless page_requested?
 
     render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
   end
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index e7795e95c..b0d45ce47 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -48,13 +48,13 @@ module Admin
     def approve
       authorize @account.user, :approve?
       @account.user.approve!
-      redirect_to admin_accounts_path(pending: '1')
+      redirect_to admin_pending_accounts_path
     end
 
     def reject
       authorize @account.user, :reject?
       SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
-      redirect_to admin_accounts_path(pending: '1')
+      redirect_to admin_pending_accounts_path
     end
 
     def unsilence
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 5401b9d59..333082f68 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -228,11 +228,6 @@ class ApplicationController < ActionController::Base
   end
 
   def mark_cacheable!
-    skip_session!
     expires_in 0, public: true
   end
-
-  def skip_session!
-    request.session_options[:skip] = true
-  end
 end
diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb
index 4f28941ae..1c422096c 100644
--- a/app/controllers/concerns/account_controller_concern.rb
+++ b/app/controllers/concerns/account_controller_concern.rb
@@ -70,7 +70,6 @@ module AccountControllerConcern
 
   def check_account_suspension
     if @account.suspended?
-      skip_session!
       expires_in(3.minutes, public: true)
       gone
     end
diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb
index 31e501609..be768c089 100644
--- a/app/controllers/custom_css_controller.rb
+++ b/app/controllers/custom_css_controller.rb
@@ -4,7 +4,6 @@ class CustomCssController < ApplicationController
   before_action :set_cache_headers
 
   def show
-    skip_session!
     render plain: Setting.custom_css || '', content_type: 'text/css'
   end
 end
diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb
index 5d306e600..3feb08132 100644
--- a/app/controllers/emojis_controller.rb
+++ b/app/controllers/emojis_controller.rb
@@ -7,8 +7,6 @@ class EmojisController < ApplicationController
   def show
     respond_to do |format|
       format.json do
-        skip_session!
-
         render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do
           ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
         end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 1462b94fc..fab9c8462 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -20,10 +20,7 @@ class FollowerAccountsController < ApplicationController
       format.json do
         raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
 
-        if params[:page].blank?
-          skip_session!
-          expires_in 3.minutes, public: true
-        end
+        expires_in 3.minutes, public: true if params[:page].blank?
 
         render json: collection_presenter,
                serializer: ActivityPub::CollectionSerializer,
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 181f85221..272116040 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -20,10 +20,7 @@ class FollowingAccountsController < ApplicationController
       format.json do
         raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
 
-        if params[:page].blank?
-          skip_session!
-          expires_in 3.minutes, public: true
-        end
+        expires_in 3.minutes, public: true if params[:page].blank?
 
         render json: collection_presenter,
                serializer: ActivityPub::CollectionSerializer,
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 28eebda28..66ba260aa 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -29,10 +29,7 @@ class StatusesController < ApplicationController
       format.html do
         use_pack 'public'
 
-        unless user_signed_in?
-          skip_session!
-          expires_in 10.seconds, public: true
-        end
+        expires_in 10.seconds, public: true if current_account.nil?
 
         @body_classes = 'with-modals'
 
@@ -43,8 +40,6 @@ class StatusesController < ApplicationController
       end
 
       format.json do
-        mark_cacheable! unless @stream_entry.hidden?
-
         render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
           ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
         end
@@ -53,8 +48,6 @@ class StatusesController < ApplicationController
   end
 
   def activity
-    skip_session!
-
     render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
       ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
     end
@@ -64,7 +57,6 @@ class StatusesController < ApplicationController
     use_pack 'embed'
     raise ActiveRecord::RecordNotFound if @status.hidden?
 
-    skip_session!
     expires_in 180, public: true
     response.headers['X-Frame-Options'] = 'ALLOWALL'
     @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])
@@ -73,8 +65,6 @@ class StatusesController < ApplicationController
   end
 
   def replies
-    skip_session!
-
     render json: replies_collection_presenter,
            serializer: ActivityPub::CollectionSerializer,
            adapter: ActivityPub::Adapter,
diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb
index 1e16c5157..1ee85592c 100644
--- a/app/controllers/stream_entries_controller.rb
+++ b/app/controllers/stream_entries_controller.rb
@@ -17,19 +17,13 @@ class StreamEntriesController < ApplicationController
       format.html do
         use_pack 'public'
 
-        unless user_signed_in?
-          skip_session!
-          expires_in 5.minutes, public: true
-        end
+        expires_in 5.minutes, public: true unless @stream_entry.hidden?
 
-        redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
+        redirect_to short_account_status_url(params[:account_username], @stream_entry.activity)
       end
 
       format.atom do
-        unless @stream_entry.hidden?
-          skip_session!
-          expires_in 3.minutes, public: true
-        end
+        expires_in 3.minutes, public: true unless @stream_entry.hidden?
 
         render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
       end
@@ -57,7 +51,7 @@ class StreamEntriesController < ApplicationController
 
   def set_stream_entry
     @stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id])
-    @type         = @stream_entry.activity_type.downcase
+    @type         = 'status'
 
     raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
     authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only?
diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js
index e1ded2b3a..cf3907fbf 100644
--- a/app/javascript/flavours/glitch/components/autosuggest_textarea.js
+++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js
@@ -138,8 +138,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
     this.setState({ suggestionsHidden: true, focused: false });
   }
 
-  onFocus = () => {
+  onFocus = (e) => {
     this.setState({ focused: true });
+    if (this.props.onFocus) {
+      this.props.onFocus(e);
+    }
   }
 
   onSuggestionClick = (e) => {
diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
index cc82a50d4..0120be28f 100644
--- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
@@ -28,6 +28,10 @@ const messages = defineMessages({
 export default @injectIntl
 class ComposeForm extends ImmutablePureComponent {
 
+  setRef = c => {
+    this.composeForm = c;
+  };
+
   static contextTypes = {
     router: PropTypes.object,
   };
@@ -208,6 +212,10 @@ class ComposeForm extends ImmutablePureComponent {
     }
   }
 
+  handleFocus = () => {
+    this.composeForm.scrollIntoView();
+  }
+
   //  This statement does several things:
   //  - If we're beginning a reply, and,
   //      - Replying to zero or one users, places the cursor at the end
@@ -302,7 +310,7 @@ class ComposeForm extends ImmutablePureComponent {
     let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);
 
     return (
-      <div className='composer'>
+      <div className='composer' ref={this.setRef}>
         <WarningContainer />
 
         <ReplyIndicatorContainer />
@@ -337,6 +345,7 @@ class ComposeForm extends ImmutablePureComponent {
             value={this.props.text}
             onChange={this.handleChange}
             suggestions={this.props.suggestions}
+            onFocus={this.handleFocus}
             onKeyDown={this.handleKeyDown}
             onSuggestionsFetchRequested={onFetchSuggestions}
             onSuggestionsClearRequested={onClearSuggestions}
diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js
index ef829b937..0405073c5 100644
--- a/app/javascript/flavours/glitch/features/list_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/list_timeline/index.js
@@ -75,6 +75,23 @@ export default class ListTimeline extends React.PureComponent {
     this.disconnect = dispatch(connectListStream(id));
   }
 
+  componentWillReceiveProps (nextProps) {
+    const { dispatch } = this.props;
+    const { id } = nextProps.params;
+
+    if (id !== this.props.params.id) {
+      if (this.disconnect) {
+        this.disconnect();
+        this.disconnect = null;
+      }
+
+      dispatch(fetchList(id));
+      dispatch(expandListTimeline(id));
+
+      this.disconnect = dispatch(connectListStream(id));
+    }
+  }
+
   componentWillUnmount () {
     if (this.disconnect) {
       this.disconnect();
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index 51a341c42..36dfb8f15 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -442,6 +442,7 @@ export default function compose(state = initialState, action) {
       map.set('focusDate', new Date());
       map.set('caretPosition', null);
       map.set('idempotencyKey', uuid());
+      map.set('sensitive', action.status.get('sensitive'));
 
       if (action.status.get('spoiler_text').length > 0) {
         map.set('spoiler', true);
diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js
index cb233de1c..440b370e6 100644
--- a/app/javascript/flavours/glitch/reducers/timelines.js
+++ b/app/javascript/flavours/glitch/reducers/timelines.js
@@ -35,7 +35,9 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
 
     if (!next && !isLoadingRecent) mMap.set('hasMore', false);
 
-    if (!statuses.isEmpty()) {
+    if (timeline.endsWith(':pinned')) {
+      mMap.set('items', statuses.map(status => status.get('id')));
+    } else if (!statuses.isEmpty()) {
       mMap.update('items', ImmutableList(), oldIds => {
         const newIds = statuses.map(status => status.get('id'));
         const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js
index 5cad43e82..4c50294ba 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.js
+++ b/app/javascript/mastodon/components/autosuggest_textarea.js
@@ -138,8 +138,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
     this.setState({ suggestionsHidden: true, focused: false });
   }
 
-  onFocus = () => {
+  onFocus = (e) => {
     this.setState({ focused: true });
+    if (this.props.onFocus) {
+      this.props.onFocus(e);
+    }
   }
 
   onSuggestionClick = (e) => {
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index ff6c521f2..ff22a2953 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -34,6 +34,10 @@ const messages = defineMessages({
 export default @injectIntl
 class ComposeForm extends ImmutablePureComponent {
 
+  setRef = c => {
+    this.composeForm = c;
+  };
+
   static contextTypes = {
     router: PropTypes.object,
   };
@@ -115,6 +119,10 @@ class ComposeForm extends ImmutablePureComponent {
     this.props.onChangeSpoilerText(e.target.value);
   }
 
+  handleFocus = () => {
+    this.composeForm.scrollIntoView();
+  }
+
   componentDidUpdate (prevProps) {
     // This statement does several things:
     // - If we're beginning a reply, and,
@@ -178,7 +186,7 @@ class ComposeForm extends ImmutablePureComponent {
     }
 
     return (
-      <div className='compose-form'>
+      <div className='compose-form' ref={this.setRef}>
         <WarningContainer />
 
         <ReplyIndicatorContainer />
@@ -201,7 +209,7 @@ class ComposeForm extends ImmutablePureComponent {
           />
         </div>
 
-        <div className='emoji-picker-wrapper'>
+        <div className={`emoji-picker-wrapper ${this.props.showSearch ? 'emoji-picker-wrapper--hidden' : ''}`}>
           <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
         </div>
 
@@ -212,6 +220,7 @@ class ComposeForm extends ImmutablePureComponent {
           value={this.props.text}
           onChange={this.handleChange}
           suggestions={this.props.suggestions}
+          onFocus={this.handleFocus}
           onKeyDown={this.handleKeyDown}
           onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
           onSuggestionsClearRequested={this.onSuggestionsClearRequested}
diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js
index 6833c43ef..7f9edfeee 100644
--- a/app/javascript/mastodon/features/compose/components/search.js
+++ b/app/javascript/mastodon/features/compose/components/search.js
@@ -21,7 +21,7 @@ class SearchPopout extends React.PureComponent {
     const { style } = this.props;
     const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
     return (
-      <div style={{ ...style, position: 'absolute', width: 285 }}>
+      <div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}>
         <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
           {({ opacity, scaleX, scaleY }) => (
             <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
index b6722e91a..ad7d16f95 100644
--- a/app/javascript/mastodon/features/list_timeline/index.js
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -75,6 +75,23 @@ class ListTimeline extends React.PureComponent {
     this.disconnect = dispatch(connectListStream(id));
   }
 
+  componentWillReceiveProps (nextProps) {
+    const { dispatch } = this.props;
+    const { id } = nextProps.params;
+
+    if (id !== this.props.params.id) {
+      if (this.disconnect) {
+        this.disconnect();
+        this.disconnect = null;
+      }
+
+      dispatch(fetchList(id));
+      dispatch(expandListTimeline(id));
+
+      this.disconnect = dispatch(connectListStream(id));
+    }
+  }
+
   componentWillUnmount () {
     if (this.disconnect) {
       this.disconnect();
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 708272591..29c691144 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -338,6 +338,7 @@ export default function compose(state = initialState, action) {
       map.set('focusDate', new Date());
       map.set('caretPosition', null);
       map.set('idempotencyKey', uuid());
+      map.set('sensitive', action.status.get('sensitive'));
 
       if (action.status.get('spoiler_text').length > 0) {
         map.set('spoiler', true);
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index 6a972f967..309a95a19 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -35,14 +35,12 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
 
     if (!next && !isLoadingRecent) mMap.set('hasMore', false);
 
-    if (!statuses.isEmpty()) {
+    if (timeline.endsWith(':pinned')) {
+      mMap.set('items', statuses.map(status => status.get('id')));
+    } else if (!statuses.isEmpty()) {
       mMap.update('items', ImmutableList(), oldIds => {
         const newIds = statuses.map(status => status.get('id'));
 
-        if (timeline.indexOf(':pinned') !== -1) {
-          return newIds;
-        }
-
         const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
         const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);
 
diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss
index f78e60597..5a40e7d79 100644
--- a/app/javascript/styles/contrast/diff.scss
+++ b/app/javascript/styles/contrast/diff.scss
@@ -5,7 +5,7 @@
       &-description {
         input {
           &::placeholder {
-            opacity: 1.0;
+            opacity: 1;
           }
         }
       }
diff --git a/app/javascript/styles/contrast/variables.scss b/app/javascript/styles/contrast/variables.scss
index f6cadf029..cfe3b21db 100644
--- a/app/javascript/styles/contrast/variables.scss
+++ b/app/javascript/styles/contrast/variables.scss
@@ -20,5 +20,5 @@ $highlight-text-color: $classic-highlight-color !default;
 $action-button-color: #8d9ac2;
 
 $inverted-text-color: $black !default;
-$lighter-text-color: darken($ui-base-color,6%) !default;
+$lighter-text-color: darken($ui-base-color, 6%) !default;
 $light-text-color: darken($ui-primary-color, 40%) !default;
diff --git a/app/javascript/styles/mailer.scss b/app/javascript/styles/mailer.scss
index 74d1df8ed..b4fb1d709 100644
--- a/app/javascript/styles/mailer.scss
+++ b/app/javascript/styles/mailer.scss
@@ -279,6 +279,8 @@ h5 {
 }
 
 .hero-with-button {
+  padding-bottom: 16px;
+
   h1 {
     margin-bottom: 4px;
   }
@@ -286,8 +288,6 @@ h5 {
   p.lead {
     margin-bottom: 32px;
   }
-
-  padding-bottom: 16px;
 }
 
 .header {
diff --git a/app/javascript/styles/mastodon/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss
index 08806599e..faaffb30f 100644
--- a/app/javascript/styles/mastodon/_mixins.scss
+++ b/app/javascript/styles/mastodon/_mixins.scss
@@ -1,21 +1,21 @@
-@mixin avatar-radius() {
+@mixin avatar-radius {
   border-radius: 4px;
   background: transparent no-repeat;
   background-position: 50%;
   background-clip: padding-box;
 }
 
-@mixin avatar-size($size:48px) {
+@mixin avatar-size($size: 48px) {
   width: $size;
   height: $size;
   background-size: $size $size;
 }
 
-@mixin search-input() {
+@mixin search-input {
   outline: 0;
   box-sizing: border-box;
   width: 100%;
-  border: none;
+  border: 0;
   box-shadow: none;
   font-family: inherit;
   background: $ui-base-color;
@@ -42,7 +42,7 @@
   }
 }
 
-@mixin search-popout() {
+@mixin search-popout {
   background: $simple-background-color;
   border-radius: 4px;
   padding: 10px 14px;
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 74f91599a..692d86852 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -171,7 +171,7 @@ $content-width: 840px;
       text-transform: none;
       padding-bottom: 0;
       margin-bottom: 0;
-      border-bottom: none;
+      border-bottom: 0;
     }
 
     & > p {
diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss
index 4411ca0b4..b5a77ce94 100644
--- a/app/javascript/styles/mastodon/basics.scss
+++ b/app/javascript/styles/mastodon/basics.scss
@@ -2,7 +2,8 @@
   @if type-of($color) == 'color' {
     $color: str-slice(ie-hex-str($color), 4);
   }
-  @return '%23' + unquote($color)
+
+  @return '%23' + unquote($color);
 }
 
 body {
@@ -15,7 +16,7 @@ body {
   text-rendering: optimizelegibility;
   font-feature-settings: "kern";
   text-size-adjust: none;
-  -webkit-tap-highlight-color: rgba(0,0,0,0);
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
   -webkit-tap-highlight-color: transparent;
 
   &.system-font {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index cd5ff35a4..63c38ff42 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -128,7 +128,7 @@
   display: inline-block;
   padding: 0;
   color: $action-button-color;
-  border: none;
+  border: 0;
   background: transparent;
   cursor: pointer;
   transition: color 100ms ease-in;
@@ -196,7 +196,7 @@
 
 .text-icon-button {
   color: $lighter-text-color;
-  border: none;
+  border: 0;
   background: transparent;
   cursor: pointer;
   font-weight: 600;
@@ -353,12 +353,12 @@
   .spoiler-input {
     height: 0;
     transform-origin: bottom;
-    opacity: 0.0;
+    opacity: 0;
 
     &.spoiler-input--visible {
       height: 36px;
       margin-bottom: 11px;
-      opacity: 1.0;
+      opacity: 1;
     }
   }
 
@@ -408,12 +408,20 @@
     }
   }
 
-  .emoji-picker-wrapper,
   .autosuggest-textarea__suggestions-wrapper {
     position: relative;
     height: 0;
   }
 
+  .emoji-picker-wrapper {
+    position: relative;
+    height: 0;
+
+    &.emoji-picker-wrapper--hidden {
+      display: none;
+    }
+  }
+
   .autosuggest-textarea__suggestions {
     box-sizing: border-box;
     display: none;
@@ -1185,7 +1193,7 @@
 }
 
 .account__avatar {
-  @include avatar-radius();
+  @include avatar-radius;
   position: relative;
 
   &-inline {
@@ -1195,11 +1203,11 @@
   }
 
   &-composite {
-    @include avatar-radius();
+    @include avatar-radius;
     overflow: hidden;
 
     & > div {
-      @include avatar-radius();
+      @include avatar-radius;
       float: left;
       position: relative;
       box-sizing: border-box;
@@ -1215,12 +1223,12 @@ a .account__avatar {
   @include avatar-size(48px);
 
   &-base {
-    @include avatar-radius();
+    @include avatar-radius;
     @include avatar-size(36px);
   }
 
   &-overlay {
-    @include avatar-radius();
+    @include avatar-radius;
     @include avatar-size(24px);
 
     position: absolute;
@@ -1598,13 +1606,13 @@ a.account__display-name {
     .icon-button.close {
       position: absolute;
       pointer-events: none;
-      transform: scale(0.0, 1.0) translate(-100%, 0);
+      transform: scale(0, 1) translate(-100%, 0);
       opacity: 0;
     }
 
     .compose__action-bar .icon-button {
       pointer-events: auto;
-      transform: scale(1.0, 1.0) translate(0, 0);
+      transform: scale(1, 1) translate(0, 0);
       opacity: 1;
     }
   }
@@ -2071,6 +2079,10 @@ a.account__display-name {
 
     .account {
       padding: 15px 10px;
+
+      &__header__bio {
+        margin: 0 -10px;
+      }
     }
 
     .notification {
@@ -2699,7 +2711,7 @@ a.account__display-name {
 .setting-text {
   color: $darker-text-color;
   background: transparent;
-  border: none;
+  border: 0;
   border-bottom: 2px solid $ui-primary-color;
   box-sizing: border-box;
   display: block;
@@ -3037,7 +3049,7 @@ a.status-card.compact:hover {
 
   & > button {
     margin: 0;
-    border: none;
+    border: 0;
     padding: 15px 0 15px 15px;
     color: inherit;
     background: transparent;
@@ -3202,11 +3214,11 @@ a.status-card.compact:hover {
 }
 
 .no-reduce-motion .loading-indicator span {
-  animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000);
+  animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
 }
 
 .no-reduce-motion .loading-indicator__figure {
-  animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000);
+  animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
 }
 
 @keyframes loader-figure {
@@ -3373,7 +3385,7 @@ a.status-card.compact:hover {
 
   .column-select {
     &__control {
-      @include search-input();
+      @include search-input;
     }
 
     &__placeholder {
@@ -3424,7 +3436,7 @@ a.status-card.compact:hover {
     }
 
     &__menu {
-      @include search-popout();
+      @include search-popout;
       padding: 0;
       background: $ui-secondary-color;
     }
@@ -3585,7 +3597,7 @@ a.status-card.compact:hover {
 
 .no-reduce-motion .shake-bottom {
   transform-origin: 50% 100%;
-  animation: shake-bottom 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) 2s 2 both;
+  animation: shake-bottom 0.8s cubic-bezier(0.455, 0.03, 0.515, 0.955) 2s 2 both;
 }
 
 .emoji-picker-dropdown__menu {
@@ -3880,10 +3892,11 @@ a.status-card.compact:hover {
 }
 
 .search__input {
+  @include search-input;
+
   display: block;
   padding: 10px;
   padding-right: 30px;
-  @include search-input();
 }
 
 .search__icon {
@@ -4491,14 +4504,14 @@ a.status-card.compact:hover {
 }
 
 .actions-modal {
+  max-height: 80vh;
+  max-width: 80vw;
+
   .status {
     overflow-y: auto;
     max-height: 300px;
   }
 
-  max-height: 80vh;
-  max-width: 80vw;
-
   .actions-modal__item-label {
     font-weight: 500;
   }
@@ -4713,7 +4726,7 @@ a.status-card.compact:hover {
 }
 
 .media-gallery__item {
-  border: none;
+  border: 0;
   box-sizing: border-box;
   display: block;
   float: left;
@@ -5173,7 +5186,7 @@ a.status-card.compact:hover {
 }
 
 .account-gallery__item {
-  border: none;
+  border: 0;
   box-sizing: border-box;
   display: block;
   position: relative;
@@ -5247,7 +5260,7 @@ a.status-card.compact:hover {
 }
 
 .search-popout {
-  @include search-popout();
+  @include search-popout;
 }
 
 noscript {
@@ -5349,14 +5362,14 @@ noscript {
         .icon-button.close {
           pointer-events: auto;
           opacity: 1;
-          transform: scale(1.0, 1.0) translate(0, 0);
+          transform: scale(1, 1) translate(0, 0);
           bottom: 5px;
         }
 
         .compose__action-bar .icon-button {
           pointer-events: none;
           opacity: 0;
-          transform: scale(0.0, 1.0) translate(100%, 0);
+          transform: scale(0, 1) translate(100%, 0);
         }
       }
     }
@@ -5386,7 +5399,7 @@ noscript {
       box-sizing: border-box;
       display: block;
       width: 100%;
-      border: none;
+      border: 0;
       padding: 10px;
       font-family: $font-monospace, monospace;
       background: $ui-base-color;
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index 0eae4939f..2d68d2b70 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -121,7 +121,7 @@
   grid-auto-rows: max-content;
 
   .column-0 {
-    grid-column: 1/3;
+    grid-column: 1 / 3;
     grid-row: 1;
   }
 
@@ -136,7 +136,7 @@
   }
 
   .column-3 {
-    grid-column: 1/3;
+    grid-column: 1 / 3;
     grid-row: 3;
   }
 
diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss
index e49084b5f..4bfd66504 100644
--- a/app/javascript/styles/mastodon/emoji_picker.scss
+++ b/app/javascript/styles/mastodon/emoji_picker.scss
@@ -1,14 +1,14 @@
 .emoji-mart {
+  font-size: 13px;
+  display: inline-block;
+  color: $inverted-text-color;
+
   &,
   * {
     box-sizing: border-box;
     line-height: 1.15;
   }
 
-  font-size: 13px;
-  display: inline-block;
-  color: $inverted-text-color;
-
   .emoji-mart-emoji {
     padding: 6px;
   }
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 2b8d7a682..f3de87791 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -553,7 +553,7 @@ code {
     box-sizing: border-box;
     display: block;
     width: 100%;
-    border: none;
+    border: 0;
     padding: 10px;
     font-family: $font-monospace, monospace;
     background: $ui-base-color;
diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss
index 0d55afda4..12f57b7a9 100644
--- a/app/javascript/styles/mastodon/polls.scss
+++ b/app/javascript/styles/mastodon/polls.scss
@@ -47,7 +47,6 @@
       width: 100%;
       font-size: 14px;
       color: $inverted-text-color;
-      display: block;
       outline: 0;
       font-family: inherit;
       background: $simple-background-color;
diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index 940dc8af2..a59f59f59 100644
--- a/app/javascript/styles/mastodon/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
@@ -180,7 +180,6 @@ body.rtl {
   }
 
   .fa-ul {
-    margin-left: 0;
     margin-left: 2.14285714em;
   }
 
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 54b175613..66b5763a9 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -143,7 +143,7 @@ class ActivityPub::Activity
 
     # If the boosted toot is embedded and it is a self-boost, handle it like a Create
     unless unsupported_object_type?
-      actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
+      actor_id = value_or_id(first_of_value(@object['attributedTo']))
 
       if actor_id == @account.uri
         return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb
index 1e805c0d1..3eb88339a 100644
--- a/app/lib/activitypub/activity/follow.rb
+++ b/app/lib/activitypub/activity/follow.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class ActivityPub::Activity::Follow < ActivityPub::Activity
+  include Payloadable
+
   def perform
     target_account = account_from_uri(object_uri)
 
@@ -28,7 +30,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
   end
 
   def reject_follow_request!(target_account)
-    json = ActiveModelSerializers::SerializableResource.new(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).to_json
+    json = Oj.dump(serialize_payload(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), ActivityPub::RejectFollowSerializer))
     ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url)
   end
 end
diff --git a/app/models/account.rb b/app/models/account.rb
index 70697db30..520b183e8 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -208,6 +208,10 @@ class Account < ApplicationRecord
     end
   end
 
+  def sign?
+    true
+  end
+
   def keypair
     @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
   end
diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb
index 5bc44e809..f1b7a4566 100644
--- a/app/models/form/account_batch.rb
+++ b/app/models/form/account_batch.rb
@@ -3,6 +3,7 @@
 class Form::AccountBatch
   include ActiveModel::Model
   include Authorization
+  include Payloadable
 
   attr_accessor :account_ids, :action, :current_account
 
@@ -54,13 +55,7 @@ class Form::AccountBatch
 
     return unless follow.account.activitypub?
 
-    json = ActiveModelSerializers::SerializableResource.new(
-      follow,
-      serializer: ActivityPub::RejectFollowSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
-
-    ActivityPub::DeliveryWorker.perform_async(json, current_account.id, follow.account.inbox_url)
+    ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), current_account.id, follow.account.inbox_url)
   end
 
   def approve!
diff --git a/app/models/status.rb b/app/models/status.rb
index 6f3ba4cc3..5ddce72de 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -211,6 +211,8 @@ class Status < ApplicationRecord
     public_visibility? || unlisted_visibility?
   end
 
+  alias sign? distributable?
+
   def with_media?
     media_attachments.any?
   end
@@ -529,7 +531,7 @@ class Status < ApplicationRecord
     return if direct_visibility?
 
     account&.increment_count!(:statuses_count)
-    reblog&.increment_count!(:reblogs_count) if reblog? && (public_visibility? || unlisted_visibility?)
+    reblog&.increment_count!(:reblogs_count) if reblog?
     thread&.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
   end
 
@@ -537,7 +539,7 @@ class Status < ApplicationRecord
     return if direct_visibility? || marked_for_mass_destruction?
 
     account&.decrement_count!(:statuses_count)
-    reblog&.decrement_count!(:reblogs_count) if reblog? && (public_visibility? || unlisted_visibility?)
+    reblog&.decrement_count!(:reblogs_count) if reblog?
     thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
   end
 
diff --git a/app/services/after_block_domain_from_account_service.rb b/app/services/after_block_domain_from_account_service.rb
index 180f13403..a87c2e792 100644
--- a/app/services/after_block_domain_from_account_service.rb
+++ b/app/services/after_block_domain_from_account_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class AfterBlockDomainFromAccountService < BaseService
+  include Payloadable
+
   # This service does not create an AccountDomainBlock record,
   # it's meant to be called after such a record has been created
   # synchronously, to "clean up"
@@ -31,12 +33,6 @@ class AfterBlockDomainFromAccountService < BaseService
 
     return unless follow.account.activitypub?
 
-    json = ActiveModelSerializers::SerializableResource.new(
-      follow,
-      serializer: ActivityPub::RejectFollowSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
-
-    ActivityPub::DeliveryWorker.perform_async(json, @account.id, follow.account.inbox_url)
+    ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), @account.id, follow.account.inbox_url)
   end
 end
diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb
index f2e3ebe7d..29b8700c7 100644
--- a/app/services/authorize_follow_service.rb
+++ b/app/services/authorize_follow_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class AuthorizeFollowService < BaseService
+  include Payloadable
+
   def call(source_account, target_account, **options)
     if options[:skip_follow_request]
       follow_request = FollowRequest.new(account: source_account, target_account: target_account, uri: options[:follow_request_uri])
@@ -24,11 +26,7 @@ class AuthorizeFollowService < BaseService
   end
 
   def build_json(follow_request)
-    ActiveModelSerializers::SerializableResource.new(
-      follow_request,
-      serializer: ActivityPub::AcceptFollowSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
+    Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer))
   end
 
   def build_xml(follow_request)
diff --git a/app/services/block_service.rb b/app/services/block_service.rb
index 10ed470e0..9050a4858 100644
--- a/app/services/block_service.rb
+++ b/app/services/block_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class BlockService < BaseService
+  include Payloadable
+
   def call(account, target_account)
     return if account.id == target_account.id
 
@@ -26,11 +28,7 @@ class BlockService < BaseService
   end
 
   def build_json(block)
-    ActiveModelSerializers::SerializableResource.new(
-      block,
-      serializer: ActivityPub::BlockSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
+    Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer))
   end
 
   def build_xml(block)
diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb
new file mode 100644
index 000000000..13d9c3548
--- /dev/null
+++ b/app/services/concerns/payloadable.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Payloadable
+  def serialize_payload(record, serializer, options = {})
+    signer    = options.delete(:signer)
+    sign_with = options.delete(:sign_with)
+    payload   = ActiveModelSerializers::SerializableResource.new(record, options.merge(serializer: serializer, adapter: ActivityPub::Adapter)).as_json
+
+    if (record.respond_to?(:sign?) && record.sign?) && signer && signing_enabled?
+      ActivityPub::LinkedDataSignature.new(payload).sign!(signer, sign_with: sign_with)
+    else
+      payload
+    end
+  end
+
+  def signing_enabled?
+    true
+  end
+end
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index b565bcc32..128a24ad6 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -2,6 +2,7 @@
 
 class FavouriteService < BaseService
   include Authorization
+  include Payloadable
 
   # Favourite a status and notify remote user
   # @param [Account] account
@@ -43,11 +44,7 @@ class FavouriteService < BaseService
   end
 
   def build_json(favourite)
-    Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
-      favourite,
-      serializer: ActivityPub::LikeSerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json).sign!(favourite.account))
+    Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer))
   end
 
   def build_xml(favourite)
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 92d8c864a..0305e2d62 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -2,6 +2,7 @@
 
 class FollowService < BaseService
   include Redisable
+  include Payloadable
 
   # Follow a remote user, notify remote user about the follow
   # @param [Account] source_account From which to follow
@@ -78,10 +79,6 @@ class FollowService < BaseService
   end
 
   def build_json(follow_request)
-    ActiveModelSerializers::SerializableResource.new(
-      follow_request,
-      serializer: ActivityPub::FollowSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
+    Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer))
   end
 end
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index 989cc19a6..1804e0c93 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -2,6 +2,7 @@
 
 class ProcessMentionsService < BaseService
   include StreamEntryRenderer
+  include Payloadable
 
   # Scan status for mentions and fetch remote mentioned users, create
   # local mention pointers, send Salmon notifications to mentioned
@@ -61,12 +62,7 @@ class ProcessMentionsService < BaseService
 
   def activitypub_json
     return @activitypub_json if defined?(@activitypub_json)
-    payload = ActiveModelSerializers::SerializableResource.new(
-      @status,
-      serializer: ActivityPub::ActivitySerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json
-    @activitypub_json = Oj.dump(@status.distributable? ? ActivityPub::LinkedDataSignature.new(payload).sign!(@status.account) : payload)
+    @activitypub_json = Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account))
   end
 
   def resolve_account_service
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 77ec52ab8..09403bae0 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -3,6 +3,7 @@
 class ReblogService < BaseService
   include Authorization
   include StreamEntryRenderer
+  include Payloadable
 
   # Reblog a status and notify its remote author
   # @param [Account] account Account to reblog from
@@ -56,10 +57,6 @@ class ReblogService < BaseService
   end
 
   def build_json(reblog)
-    Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
-      reblog,
-      serializer: ActivityPub::ActivitySerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json).sign!(reblog.account))
+    Oj.dump(serialize_payload(reblog, ActivityPub::ActivitySerializer, signer: reblog.account))
   end
 end
diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb
index a91266aa4..f87d0ba91 100644
--- a/app/services/reject_follow_service.rb
+++ b/app/services/reject_follow_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class RejectFollowService < BaseService
+  include Payloadable
+
   def call(source_account, target_account)
     follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
     follow_request.reject!
@@ -19,11 +21,7 @@ class RejectFollowService < BaseService
   end
 
   def build_json(follow_request)
-    ActiveModelSerializers::SerializableResource.new(
-      follow_request,
-      serializer: ActivityPub::RejectFollowSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
+    Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer))
   end
 
   def build_xml(follow_request)
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 6e4998e07..98972fc70 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -3,6 +3,7 @@
 class RemoveStatusService < BaseService
   include StreamEntryRenderer
   include Redisable
+  include Payloadable
 
   def call(status, **options)
     @payload      = Oj.dump(event: :delete, payload: status.id.to_s)
@@ -116,15 +117,7 @@ class RemoveStatusService < BaseService
   end
 
   def signed_activity_json
-    @signed_activity_json ||= Oj.dump(ActivityPub::LinkedDataSignature.new(activity_json).sign!(@account))
-  end
-
-  def activity_json
-    @activity_json ||= ActiveModelSerializers::SerializableResource.new(
-      @status,
-      serializer: @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json
+    @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account))
   end
 
   def remove_reblogs
diff --git a/app/services/report_service.rb b/app/services/report_service.rb
index 73bd6694f..1e955c1e7 100644
--- a/app/services/report_service.rb
+++ b/app/services/report_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class ReportService < BaseService
+  include Payloadable
+
   def call(source_account, target_account, options = {})
     @source_account = source_account
     @target_account = target_account
@@ -44,12 +46,7 @@ class ReportService < BaseService
   end
 
   def payload
-    Oj.dump(ActiveModelSerializers::SerializableResource.new(
-      @report,
-      serializer: ActivityPub::FlagSerializer,
-      adapter: ActivityPub::Adapter,
-      account: some_local_account
-    ).as_json)
+    Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account))
   end
 
   def some_local_account
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 412873f84..a5ce3dbd9 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class SuspendAccountService < BaseService
+  include Payloadable
+
   ASSOCIATIONS_ON_SUSPEND = %w(
     account_pins
     active_relationships
@@ -118,23 +120,11 @@ class SuspendAccountService < BaseService
   end
 
   def delete_actor_json
-    return @delete_actor_json if defined?(@delete_actor_json)
-
-    payload = ActiveModelSerializers::SerializableResource.new(
-      @account,
-      serializer: ActivityPub::DeleteActorSerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json
-
-    @delete_actor_json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
+    @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
   end
 
   def build_reject_json(follow)
-    ActiveModelSerializers::SerializableResource.new(
-      follow,
-      serializer: ActivityPub::RejectFollowSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
+    Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
   end
 
   def delivery_inboxes
diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb
index 72fc5ab15..95a858e9f 100644
--- a/app/services/unblock_service.rb
+++ b/app/services/unblock_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class UnblockService < BaseService
+  include Payloadable
+
   def call(account, target_account)
     return unless account.blocking?(target_account)
 
@@ -20,11 +22,7 @@ class UnblockService < BaseService
   end
 
   def build_json(unblock)
-    ActiveModelSerializers::SerializableResource.new(
-      unblock,
-      serializer: ActivityPub::UndoBlockSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
+    Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer))
   end
 
   def build_xml(block)
diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb
index 2fda11bd6..dcc890b7d 100644
--- a/app/services/unfavourite_service.rb
+++ b/app/services/unfavourite_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class UnfavouriteService < BaseService
+  include Payloadable
+
   def call(account, status)
     favourite = Favourite.find_by!(account: account, status: status)
     favourite.destroy!
@@ -21,11 +23,7 @@ class UnfavouriteService < BaseService
   end
 
   def build_json(favourite)
-    Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
-      favourite,
-      serializer: ActivityPub::UndoLikeSerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json).sign!(favourite.account))
+    Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer))
   end
 
   def build_xml(favourite)
diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb
index 95da2a667..17dc29735 100644
--- a/app/services/unfollow_service.rb
+++ b/app/services/unfollow_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class UnfollowService < BaseService
+  include Payloadable
+
   # Unfollow and notify the remote user
   # @param [Account] source_account Where to unfollow from
   # @param [Account] target_account Which to unfollow
@@ -50,19 +52,11 @@ class UnfollowService < BaseService
   end
 
   def build_json(follow)
-    ActiveModelSerializers::SerializableResource.new(
-      follow,
-      serializer: ActivityPub::UndoFollowSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
+    Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
   end
 
   def build_reject_json(follow)
-    ActiveModelSerializers::SerializableResource.new(
-      follow,
-      serializer: ActivityPub::RejectFollowSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
+    Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
   end
 
   def build_xml(follow)
diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb
index 81af9ef3a..0eeb8fd56 100644
--- a/app/services/vote_service.rb
+++ b/app/services/vote_service.rb
@@ -2,6 +2,7 @@
 
 class VoteService < BaseService
   include Authorization
+  include Payloadable
 
   def call(account, poll, choices)
     authorize_with account, poll, :vote?
@@ -50,10 +51,6 @@ class VoteService < BaseService
   end
 
   def build_json(vote)
-    ActiveModelSerializers::SerializableResource.new(
-      vote,
-      serializer: ActivityPub::VoteSerializer,
-      adapter: ActivityPub::Adapter
-    ).to_json
+    Oj.dump(serialize_payload(vote, ActivityPub::VoteSerializer))
   end
 end
diff --git a/app/views/admin/pending_accounts/_account.html.haml b/app/views/admin/pending_accounts/_account.html.haml
index 1ed5dafdd..7a9796a67 100644
--- a/app/views/admin/pending_accounts/_account.html.haml
+++ b/app/views/admin/pending_accounts/_account.html.haml
@@ -8,6 +8,8 @@
         = "(@#{account.username})"
       %br/
       = account.user_current_sign_in_ip
+      •
+      = t 'admin.accounts.time_in_queue', time: time_ago_in_words(account.user&.created_at)
 
     - if account.user&.invite_request&.text&.present?
       .pending-account__body
diff --git a/app/workers/activitypub/distribute_poll_update_worker.rb b/app/workers/activitypub/distribute_poll_update_worker.rb
index 98b227111..37a13db2b 100644
--- a/app/workers/activitypub/distribute_poll_update_worker.rb
+++ b/app/workers/activitypub/distribute_poll_update_worker.rb
@@ -2,6 +2,7 @@
 
 class ActivityPub::DistributePollUpdateWorker
   include Sidekiq::Worker
+  include Payloadable
 
   sidekiq_options queue: 'push', unique: :until_executed, retry: 0
 
@@ -41,20 +42,8 @@ class ActivityPub::DistributePollUpdateWorker
     @inboxes
   end
 
-  def signed_payload
-    Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@account))
-  end
-
-  def unsigned_payload
-    ActiveModelSerializers::SerializableResource.new(
-      @status,
-      serializer: ActivityPub::UpdatePollSerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json
-  end
-
   def payload
-    @payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload)
+    @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::UpdatePollSerializer, signer: @account))
   end
 
   def relay!
diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb
index d83f01700..11b6a6111 100644
--- a/app/workers/activitypub/distribution_worker.rb
+++ b/app/workers/activitypub/distribution_worker.rb
@@ -2,6 +2,7 @@
 
 class ActivityPub::DistributionWorker
   include Sidekiq::Worker
+  include Payloadable
 
   sidekiq_options queue: 'push'
 
@@ -41,20 +42,8 @@ class ActivityPub::DistributionWorker
                  end
   end
 
-  def signed_payload
-    Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@account))
-  end
-
-  def unsigned_payload
-    ActiveModelSerializers::SerializableResource.new(
-      @status,
-      serializer: ActivityPub::ActivitySerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json
-  end
-
   def payload
-    @payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload)
+    @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @account))
   end
 
   def relay!
diff --git a/app/workers/activitypub/reply_distribution_worker.rb b/app/workers/activitypub/reply_distribution_worker.rb
index d8fea6c4e..1ff8a657e 100644
--- a/app/workers/activitypub/reply_distribution_worker.rb
+++ b/app/workers/activitypub/reply_distribution_worker.rb
@@ -5,6 +5,7 @@
 
 class ActivityPub::ReplyDistributionWorker
   include Sidekiq::Worker
+  include Payloadable
 
   sidekiq_options queue: 'push'
 
@@ -27,19 +28,7 @@ class ActivityPub::ReplyDistributionWorker
     @inboxes ||= @account.followers.inboxes
   end
 
-  def signed_payload
-    Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@status.account))
-  end
-
-  def unsigned_payload
-    ActiveModelSerializers::SerializableResource.new(
-      @status,
-      serializer: ActivityPub::ActivitySerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json
-  end
-
   def payload
-    @payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload)
+    @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account))
   end
 end
diff --git a/app/workers/activitypub/update_distribution_worker.rb b/app/workers/activitypub/update_distribution_worker.rb
index b9e5ff064..3a207f071 100644
--- a/app/workers/activitypub/update_distribution_worker.rb
+++ b/app/workers/activitypub/update_distribution_worker.rb
@@ -2,6 +2,7 @@
 
 class ActivityPub::UpdateDistributionWorker
   include Sidekiq::Worker
+  include Payloadable
 
   sidekiq_options queue: 'push'
 
@@ -27,14 +28,6 @@ class ActivityPub::UpdateDistributionWorker
   end
 
   def signed_payload
-    @signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account, sign_with: @options[:sign_with]))
-  end
-
-  def payload
-    @payload ||= ActiveModelSerializers::SerializableResource.new(
-      @account,
-      serializer: ActivityPub::UpdateSerializer,
-      adapter: ActivityPub::Adapter
-    ).as_json
+    @signed_payload ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account, sign_with: @options[:sign_with]))
   end
 end