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.rb14
-rw-r--r--app/controllers/activitypub/collections_controller.rb17
-rw-r--r--app/controllers/activitypub/outboxes_controller.rb6
-rw-r--r--app/controllers/activitypub/replies_controller.rb21
-rw-r--r--app/controllers/api/v1/polls/votes_controller.rb2
-rw-r--r--app/controllers/api/v1/polls_controller.rb2
-rw-r--r--app/controllers/api/v1/push/subscriptions_controller.rb11
-rw-r--r--app/controllers/api/v1/statuses/mutes_controller.rb3
-rw-r--r--app/controllers/api/v1/statuses_controller.rb2
-rw-r--r--app/controllers/media_controller.rb2
-rw-r--r--app/controllers/remote_interaction_controller.rb2
-rw-r--r--app/controllers/statuses_controller.rb2
-rw-r--r--app/javascript/core/settings.js2
-rw-r--r--app/javascript/flavours/glitch/packs/public.js14
-rw-r--r--app/javascript/mastodon/actions/timelines.js2
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/privacy_dropdown.js2
-rw-r--r--app/javascript/mastodon/reducers/statuses.js2
-rw-r--r--app/javascript/mastodon/reducers/timelines.js10
-rw-r--r--app/javascript/packs/public.js14
-rw-r--r--app/javascript/styles/mastodon/about.scss5
-rw-r--r--app/javascript/styles/mastodon/admin.scss20
-rw-r--r--app/javascript/styles/mastodon/basics.scss16
-rw-r--r--app/javascript/styles/mastodon/components.scss12
-rw-r--r--app/javascript/styles/mastodon/forms.scss21
-rw-r--r--app/javascript/styles/mastodon/polls.scss30
-rw-r--r--app/javascript/styles/mastodon/statuses.scss17
-rw-r--r--app/models/account.rb90
-rw-r--r--app/models/concerns/omniauthable.rb2
-rw-r--r--app/models/custom_emoji.rb29
-rw-r--r--app/models/media_attachment.rb35
-rw-r--r--app/models/preview_card.rb43
-rw-r--r--app/models/status.rb8
-rw-r--r--app/serializers/rest/instance_serializer.rb6
-rw-r--r--app/services/fetch_resource_service.rb13
-rw-r--r--app/views/about/show.html.haml6
-rw-r--r--app/views/accounts/_moved.html.haml6
-rw-r--r--app/views/admin/accounts/_account.html.haml2
-rw-r--r--app/views/admin/accounts/show.html.haml26
-rw-r--r--app/views/admin/instances/index.html.haml2
-rw-r--r--app/views/admin/instances/show.html.haml6
-rw-r--r--app/views/admin/pending_accounts/index.html.haml8
-rw-r--r--app/views/admin/relationships/index.html.haml2
-rw-r--r--app/views/admin/reports/show.html.haml6
-rw-r--r--app/views/admin/statuses/index.html.haml2
-rw-r--r--app/views/admin/statuses/show.html.haml2
-rw-r--r--app/views/admin/tags/index.html.haml8
-rw-r--r--app/views/application/_card.html.haml1
-rw-r--r--app/views/auth/registrations/new.html.haml4
-rw-r--r--app/views/auth/sessions/two_factor.html.haml2
-rw-r--r--app/views/directories/index.html.haml1
-rwxr-xr-xapp/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/embedded.html.haml2
-rw-r--r--app/views/public_timelines/show.html.haml2
-rw-r--r--app/views/settings/preferences/appearance/show.html.haml4
-rw-r--r--app/views/settings/profiles/show.html.haml2
-rw-r--r--app/views/statuses/_detailed_status.html.haml6
-rw-r--r--app/views/statuses/_poll.html.haml8
-rw-r--r--app/views/statuses/_simple_status.html.haml12
59 files changed, 377 insertions, 224 deletions
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index ee48da177..52d09cff8 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -28,7 +28,7 @@ class AccountsController < ApplicationController
         end
 
         @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
-        @statuses        = filtered_status_page(params)
+        @statuses        = filtered_status_page
         @statuses        = cache_collection(@statuses, Status)
         @rss_url         = rss_url
 
@@ -141,12 +141,12 @@ class AccountsController < ApplicationController
     request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
   end
 
-  def filtered_status_page(params)
-    if params[:min_id].present?
-      filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
-    else
-      filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
-    end
+  def filtered_status_page
+    filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id))
+  end
+
+  def params_slice(*keys)
+    params.slice(*keys).permit(*keys)
   end
 
   def restrict_fields_to
diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
index 910fefb1c..c1e7aa550 100644
--- a/app/controllers/activitypub/collections_controller.rb
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -24,20 +24,23 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
   def set_size
     case params[:id]
     when 'featured'
-      @account.pinned_statuses.count
+      @size = @account.pinned_statuses.count
     else
-      raise ActiveRecord::RecordNotFound
+      not_found
     end
   end
 
   def scope_for_collection
     case params[:id]
     when 'featured'
-      return Status.none if @account.blocking?(signed_request_account)
-
-      @account.pinned_statuses
-    else
-      raise ActiveRecord::RecordNotFound
+      # Because in public fetch mode we cache the response, there would be no
+      # benefit from performing the check below, since a blocked account or domain
+      # would likely be served the cache from the reverse proxy anyway
+      if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
+        Status.none
+      else
+        @account.pinned_statuses
+      end
     end
   end
 
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index 891756b7e..e25a4bc07 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -11,7 +11,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
   before_action :set_cache_headers
 
   def show
-    expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
+    expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?))
     render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
   end
 
@@ -50,12 +50,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
     return unless page_requested?
 
     @statuses = @account.statuses.permitted_for(@account, signed_request_account)
-    @statuses = params[:min_id].present? ? @statuses.paginate_by_min_id(LIMIT, params[:min_id]).reverse : @statuses.paginate_by_max_id(LIMIT, params[:max_id])
+    @statuses = @statuses.paginate_by_id(LIMIT, params_slice(:max_id, :min_id, :since_id))
     @statuses = cache_collection(@statuses, Status)
   end
 
   def page_requested?
-    params[:page] == 'true'
+    truthy_param?(:page)
   end
 
   def page_params
diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb
index c62061555..43bf4e657 100644
--- a/app/controllers/activitypub/replies_controller.rb
+++ b/app/controllers/activitypub/replies_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ActivityPub::RepliesController < ActivityPub::BaseController
-  include SignatureAuthentication
+  include SignatureVerification
   include Authorization
   include AccountOwnedConcern
 
@@ -19,15 +19,19 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
 
   private
 
+  def pundit_user
+    signed_request_account
+  end
+
   def set_status
     @status = @account.statuses.find(params[:status_id])
     authorize @status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def set_replies
-    @replies = page_params[:only_other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
+    @replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses
     @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
     @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
   end
@@ -38,7 +42,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
       type: :unordered,
       part_of: account_status_replies_url(@account, @status),
       next: next_page,
-      items: @replies.map { |status| status.local ? status : status.uri }
+      items: @replies.map { |status| status.local? ? status : status.uri }
     )
 
     return page if page_requested?
@@ -51,16 +55,21 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
   end
 
   def page_requested?
-    params[:page] == 'true'
+    truthy_param?(:page)
+  end
+
+  def only_other_accounts?
+    truthy_param?(:only_other_accounts)
   end
 
   def next_page
     only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
+
     account_status_replies_url(
       @account,
       @status,
       page: true,
-      min_id: only_other_accounts && !page_params[:only_other_accounts] ? nil : @replies&.last&.id,
+      min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
       only_other_accounts: only_other_accounts
     )
   end
diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb
index e1d26106a..513b937ef 100644
--- a/app/controllers/api/v1/polls/votes_controller.rb
+++ b/app/controllers/api/v1/polls/votes_controller.rb
@@ -18,7 +18,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
     @poll = Poll.attached.find(params[:poll_id])
     authorize @poll.status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def vote_params
diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb
index 744baf7bb..6435e9f0d 100644
--- a/app/controllers/api/v1/polls_controller.rb
+++ b/app/controllers/api/v1/polls_controller.rb
@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
     @poll = Poll.attached.find(params[:id])
     authorize @poll.status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def refresh_poll
diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb
index 1cbc92b93..d34b333eb 100644
--- a/app/controllers/api/v1/push/subscriptions_controller.rb
+++ b/app/controllers/api/v1/push/subscriptions_controller.rb
@@ -4,6 +4,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
   before_action -> { doorkeeper_authorize! :push }
   before_action :require_user!
   before_action :set_web_push_subscription
+  before_action :check_web_push_subscription, only: [:show, :update]
 
   def create
     @web_subscription&.destroy!
@@ -21,16 +22,11 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
   end
 
   def show
-    raise ActiveRecord::RecordNotFound if @web_subscription.nil?
-
     render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
   end
 
   def update
-    raise ActiveRecord::RecordNotFound if @web_subscription.nil?
-
     @web_subscription.update!(data: data_params)
-
     render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
   end
 
@@ -45,12 +41,17 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
     @web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
   end
 
+  def check_web_push_subscription
+    not_found if @web_subscription.nil?
+  end
+
   def subscription_params
     params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
   end
 
   def data_params
     return {} if params[:data].blank?
+
     params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
   end
 end
diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb
index 43c7a525a..87071a2b9 100644
--- a/app/controllers/api/v1/statuses/mutes_controller.rb
+++ b/app/controllers/api/v1/statuses/mutes_controller.rb
@@ -28,8 +28,7 @@ class Api::V1::Statuses::MutesController < Api::BaseController
     @status = Status.find(params[:status_id])
     authorize @status, :show?
   rescue Mastodon::NotPermittedError
-    # Reraise in order to get a 404 instead of a 403 error code
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def set_conversation
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 29ae91762..b3edce676 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -68,7 +68,7 @@ class Api::V1::StatusesController < Api::BaseController
     @status = Status.find(params[:id])
     authorize @status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def set_thread
diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb
index 05cf09c28..1d166d6e7 100644
--- a/app/controllers/media_controller.rb
+++ b/app/controllers/media_controller.rb
@@ -33,7 +33,7 @@ class MediaController < ApplicationController
   def verify_permitted_status!
     authorize @media_attachment.status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def check_playable
diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb
index e058d0ed5..51bb9bdea 100644
--- a/app/controllers/remote_interaction_controller.rb
+++ b/app/controllers/remote_interaction_controller.rb
@@ -42,7 +42,7 @@ class RemoteInteractionController < ApplicationController
     @status = Status.find(params[:id])
     authorize @status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def set_body_classes
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 588063d01..a1b7f4320 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -49,7 +49,7 @@ class StatusesController < ApplicationController
 
   def embed
     use_pack 'embed'
-    return not_found if @status.hidden?
+    return not_found if @status.hidden? || @status.reblog?
 
     expires_in 180, public: true
     response.headers['X-Frame-Options'] = 'ALLOWALL'
diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js
index e02c91cc7..9fe03f90c 100644
--- a/app/javascript/core/settings.js
+++ b/app/javascript/core/settings.js
@@ -10,7 +10,7 @@ delegate(document, '#account_display_name', 'input', ({ target }) => {
     if (target.value) {
       name.innerHTML = emojify(escapeTextContentForBrowser(target.value));
     } else {
-      name.textContent = document.querySelector('#default_account_display_name').textContent;
+      name.textContent = name.textContent = target.dataset.default;
     }
   }
 });
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
index e5a567205..58febcf5b 100644
--- a/app/javascript/flavours/glitch/packs/public.js
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -99,15 +99,13 @@ function main() {
     delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
 
     delegate(document, '.status__content__spoiler-link', 'click', function() {
-      const contentEl = this.parentNode.parentNode.querySelector('.e-content');
+      const statusEl = this.parentNode.parentNode;
 
-      if (contentEl.style.display === 'block') {
-        contentEl.style.display = 'none';
-        this.parentNode.style.marginBottom = 0;
+      if (statusEl.dataset.spoiler === 'expanded') {
+        statusEl.dataset.spoiler = 'folded';
         this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
       } else {
-        contentEl.style.display = 'block';
-        this.parentNode.style.marginBottom = null;
+        statusEl.dataset.spoiler = 'expanded';
         this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
       }
 
@@ -115,8 +113,8 @@ function main() {
     });
 
     [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
-      const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content');
-      const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
+      const statusEl = spoilerLink.parentNode.parentNode;
+      const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
       spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
     });
   });
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 50840cacc..861827d33 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -42,7 +42,7 @@ export function updateTimeline(timeline, status, accept) {
 export function deleteFromTimelines(id) {
   return (dispatch, getState) => {
     const accountId  = getState().getIn(['statuses', id, 'account']);
-    const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
+    const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id'));
     const reblogOf   = getState().getIn(['statuses', id, 'reblog'], null);
 
     dispatch({
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 31c02d735..4734e0f3f 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -46,7 +46,7 @@ class DropdownMenu extends React.PureComponent {
     document.addEventListener('keydown', this.handleKeyDown, false);
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
     if (this.focusedItem && this.props.openedViaKeyboard) {
-      this.focusedItem.focus();
+      this.focusedItem.focus({ preventScroll: true });
     }
     this.setState({ mounted: true });
   }
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
index 57588fe96..96028e042 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
@@ -100,7 +100,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
   componentDidMount () {
     document.addEventListener('click', this.handleDocumentClick, false);
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
-    if (this.focusedItem) this.focusedItem.focus();
+    if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
     this.setState({ mounted: true });
   }
 
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index 2554c008d..53dec9585 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -25,7 +25,7 @@ const importStatuses = (state, statuses) =>
 
 const deleteStatus = (state, id, references) => {
   references.forEach(ref => {
-    state = deleteStatus(state, ref[0], []);
+    state = deleteStatus(state, ref, []);
   });
 
   return state.delete(id);
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index 63b76773d..9156db021 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -89,7 +89,7 @@ const updateTimeline = (state, timeline, status, usePendingItems) => {
   }));
 };
 
-const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
+const deleteStatus = (state, id, references, exclude_account = null) => {
   state.keySeq().forEach(timeline => {
     if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) {
       const helper = list => list.filterNot(item => item === id);
@@ -99,7 +99,7 @@ const deleteStatus = (state, id, accountId, references, exclude_account = null)
 
   // Remove reblogs of deleted status
   references.forEach(ref => {
-    state = deleteStatus(state, ref[0], ref[1], [], exclude_account);
+    state = deleteStatus(state, ref, [], exclude_account);
   });
 
   return state;
@@ -117,8 +117,8 @@ const filterTimelines = (state, relationship, statuses) => {
       return;
     }
 
-    references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]);
-    state      = deleteStatus(state, status.get('id'), status.get('account'), references, relationship.id);
+    references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id'));
+    state      = deleteStatus(state, status.get('id'), references, relationship.id);
   });
 
   return state;
@@ -150,7 +150,7 @@ export default function timelines(state = initialState, action) {
   case TIMELINE_UPDATE:
     return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
   case TIMELINE_DELETE:
-    return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
+    return deleteStatus(state, action.id, action.references, action.reblogOf);
   case TIMELINE_CLEAR:
     return clearTimeline(state, action.timeline);
   case ACCOUNT_BLOCK_SUCCESS:
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 5b699e767..3d190d2da 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -103,15 +103,13 @@ function main() {
     delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
 
     delegate(document, '.status__content__spoiler-link', 'click', function() {
-      const contentEl = this.parentNode.parentNode.querySelector('.e-content');
+      const statusEl = this.parentNode.parentNode;
 
-      if (contentEl.style.display === 'block') {
-        contentEl.style.display = 'none';
-        this.parentNode.style.marginBottom = 0;
+      if (statusEl.dataset.spoiler === 'expanded') {
+        statusEl.dataset.spoiler = 'folded';
         this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
       } else {
-        contentEl.style.display = 'block';
-        this.parentNode.style.marginBottom = null;
+        statusEl.dataset.spoiler = 'expanded';
         this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
       }
 
@@ -119,8 +117,8 @@ function main() {
     });
 
     [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
-      const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content');
-      const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
+      const statusEl = spoilerLink.parentNode.parentNode;
+      const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
       spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
     });
   });
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index cf16b54ac..711f34965 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -757,8 +757,13 @@ $small-breakpoint: 960px;
       }
     }
 
+    &__counters__wrapper {
+      display: flex;
+    }
+
     &__counter {
       padding: 10px;
+      width: 50%;
 
       strong {
         font-family: $font-display, sans-serif;
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 7bff2daa1..78dea92b9 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -583,6 +583,18 @@ body,
   }
 }
 
+.special-action-button,
+.back-link {
+  text-align: right;
+  flex: 1 1 auto;
+}
+
+.action-buttons {
+  display: flex;
+  overflow: hidden;
+  justify-content: space-between;
+}
+
 .spacer {
   flex: 1 1 auto;
 }
@@ -920,3 +932,11 @@ a.name-tag,
     }
   }
 }
+
+.account-badges {
+  margin: -2px 0;
+}
+
+.dashboard__counters.admin-account-counters {
+  margin-top: 10px;
+}
diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss
index 2b10b5ad3..a5dbe75fb 100644
--- a/app/javascript/styles/mastodon/basics.scss
+++ b/app/javascript/styles/mastodon/basics.scss
@@ -229,3 +229,19 @@ button {
     }
   }
 }
+
+.logo-resources {
+  display: none;
+}
+
+// NoScript adds a __ns__pop2top class to the full ancestry of blocked elements,
+// to set the z-index to a high value, which messes with modals and dropdowns.
+// Blocked elements can in theory only be media and frames/embeds, so they
+// should only appear in statuses, under divs and articles.
+body,
+div,
+article {
+  .__ns__pop2top {
+    z-index: unset !important;
+  }
+}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index c7835b878..6c33b709d 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1362,6 +1362,12 @@ a .account__avatar {
   &-base {
     @include avatar-radius;
     @include avatar-size(36px);
+
+    img {
+      @include avatar-radius;
+      width: 100%;
+      height: 100%;
+    }
   }
 
   &-overlay {
@@ -1372,6 +1378,12 @@ a .account__avatar {
     bottom: 0;
     right: 0;
     z-index: 1;
+
+    img {
+      @include avatar-radius;
+      width: 100%;
+      height: 100%;
+    }
   }
 }
 
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index c9ad68f94..0e5b00e8f 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -142,6 +142,10 @@ code {
     }
   }
 
+  .otp-hint {
+    margin-bottom: 25px;
+  }
+
   .card {
     margin-bottom: 15px;
   }
@@ -285,6 +289,14 @@ code {
         margin-bottom: 25px;
       }
     }
+
+    .fields-group.invited-by {
+      margin-bottom: 30px;
+
+      .hint {
+        text-align: center;
+      }
+    }
   }
 
   .input.radio_buttons .radio label {
@@ -635,6 +647,15 @@ code {
   @media screen and (max-width: 740px) and (min-width: 441px) {
     margin-top: 40px;
   }
+
+  &.translation-prompt {
+    text-align: unset;
+    color: unset;
+
+    a {
+      text-decoration: underline;
+    }
+  }
 }
 
 .form-footer {
diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss
index 1ecc8434d..ad7088982 100644
--- a/app/javascript/styles/mastodon/polls.scss
+++ b/app/javascript/styles/mastodon/polls.scss
@@ -19,6 +19,36 @@
     }
   }
 
+  progress {
+    border: 0;
+    display: block;
+    width: 100%;
+    height: 5px;
+    appearance: none;
+    background: transparent;
+
+    &::-webkit-progress-bar {
+      background: transparent;
+    }
+
+    // Those rules need to be entirely separate or they won't work, hence the
+    // duplication
+    &::-moz-progress-bar {
+      border-radius: 4px;
+      background: darken($ui-primary-color, 5%);
+    }
+
+    &::-ms-fill {
+      border-radius: 4px;
+      background: darken($ui-primary-color, 5%);
+    }
+
+    &::-webkit-progress-value {
+      border-radius: 4px;
+      background: darken($ui-primary-color, 5%);
+    }
+  }
+
   &__option {
     position: relative;
     display: flex;
diff --git a/app/javascript/styles/mastodon/statuses.scss b/app/javascript/styles/mastodon/statuses.scss
index 19ce0ab8f..0b7be7afd 100644
--- a/app/javascript/styles/mastodon/statuses.scss
+++ b/app/javascript/styles/mastodon/statuses.scss
@@ -128,6 +128,16 @@
 
 .embed,
 .public-layout {
+  .status__content[data-spoiler=folded] {
+    .e-content {
+      display: none;
+    }
+
+    p:first-child {
+      margin-bottom: 0;
+    }
+  }
+
   .detailed-status {
     padding: 15px;
   }
@@ -159,5 +169,12 @@
     .video-player {
       margin-top: 10px;
     }
+
+    &__action-bar-button {
+      font-size: 18px;
+      width: 23.1429px;
+      height: 23.1429px;
+      line-height: 23.15px;
+    }
   }
 }
diff --git a/app/models/account.rb b/app/models/account.rb
index e56db3126..5038d4768 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -3,50 +3,52 @@
 #
 # Table name: accounts
 #
-#  id                      :bigint(8)        not null, primary key
-#  username                :string           default(""), not null
-#  domain                  :string
-#  secret                  :string           default(""), not null
-#  private_key             :text
-#  public_key              :text             default(""), not null
-#  remote_url              :string           default(""), not null
-#  salmon_url              :string           default(""), not null
-#  hub_url                 :string           default(""), not null
-#  created_at              :datetime         not null
-#  updated_at              :datetime         not null
-#  note                    :text             default(""), not null
-#  display_name            :string           default(""), not null
-#  uri                     :string           default(""), not null
-#  url                     :string
-#  avatar_file_name        :string
-#  avatar_content_type     :string
-#  avatar_file_size        :integer
-#  avatar_updated_at       :datetime
-#  header_file_name        :string
-#  header_content_type     :string
-#  header_file_size        :integer
-#  header_updated_at       :datetime
-#  avatar_remote_url       :string
-#  subscription_expires_at :datetime
-#  locked                  :boolean          default(FALSE), not null
-#  header_remote_url       :string           default(""), not null
-#  last_webfingered_at     :datetime
-#  inbox_url               :string           default(""), not null
-#  outbox_url              :string           default(""), not null
-#  shared_inbox_url        :string           default(""), not null
-#  followers_url           :string           default(""), not null
-#  protocol                :integer          default("ostatus"), not null
-#  memorial                :boolean          default(FALSE), not null
-#  moved_to_account_id     :bigint(8)
-#  featured_collection_url :string
-#  fields                  :jsonb
-#  actor_type              :string
-#  discoverable            :boolean
-#  also_known_as           :string           is an Array
-#  silenced_at             :datetime
-#  suspended_at            :datetime
-#  trust_level             :integer
-#  hide_collections        :boolean
+#  id                            :bigint(8)        not null, primary key
+#  username                      :string           default(""), not null
+#  domain                        :string
+#  secret                        :string           default(""), not null
+#  private_key                   :text
+#  public_key                    :text             default(""), not null
+#  remote_url                    :string           default(""), not null
+#  salmon_url                    :string           default(""), not null
+#  hub_url                       :string           default(""), not null
+#  created_at                    :datetime         not null
+#  updated_at                    :datetime         not null
+#  note                          :text             default(""), not null
+#  display_name                  :string           default(""), not null
+#  uri                           :string           default(""), not null
+#  url                           :string
+#  avatar_file_name              :string
+#  avatar_content_type           :string
+#  avatar_file_size              :integer
+#  avatar_updated_at             :datetime
+#  header_file_name              :string
+#  header_content_type           :string
+#  header_file_size              :integer
+#  header_updated_at             :datetime
+#  avatar_remote_url             :string
+#  subscription_expires_at       :datetime
+#  locked                        :boolean          default(FALSE), not null
+#  header_remote_url             :string           default(""), not null
+#  last_webfingered_at           :datetime
+#  inbox_url                     :string           default(""), not null
+#  outbox_url                    :string           default(""), not null
+#  shared_inbox_url              :string           default(""), not null
+#  followers_url                 :string           default(""), not null
+#  protocol                      :integer          default("ostatus"), not null
+#  memorial                      :boolean          default(FALSE), not null
+#  moved_to_account_id           :bigint(8)
+#  featured_collection_url       :string
+#  fields                        :jsonb
+#  actor_type                    :string
+#  discoverable                  :boolean
+#  also_known_as                 :string           is an Array
+#  silenced_at                   :datetime
+#  suspended_at                  :datetime
+#  trust_level                   :integer
+#  hide_collections              :boolean
+#  avatar_storage_schema_version :integer
+#  header_storage_schema_version :integer
 #
 
 class Account < ApplicationRecord
diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb
index 960784222..736da6c1d 100644
--- a/app/models/concerns/omniauthable.rb
+++ b/app/models/concerns/omniauthable.rb
@@ -82,7 +82,7 @@ module Omniauthable
       username = starting_username
       i        = 0
 
-      while Account.exists?(username: username)
+      while Account.exists?(username: username, domain: nil)
         i       += 1
         username = "#{starting_username}_#{i}"
       end
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index d177cf281..7cb03b819 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -3,20 +3,21 @@
 #
 # Table name: custom_emojis
 #
-#  id                 :bigint(8)        not null, primary key
-#  shortcode          :string           default(""), not null
-#  domain             :string
-#  image_file_name    :string
-#  image_content_type :string
-#  image_file_size    :integer
-#  image_updated_at   :datetime
-#  created_at         :datetime         not null
-#  updated_at         :datetime         not null
-#  disabled           :boolean          default(FALSE), not null
-#  uri                :string
-#  image_remote_url   :string
-#  visible_in_picker  :boolean          default(TRUE), not null
-#  category_id        :bigint(8)
+#  id                           :bigint(8)        not null, primary key
+#  shortcode                    :string           default(""), not null
+#  domain                       :string
+#  image_file_name              :string
+#  image_content_type           :string
+#  image_file_size              :integer
+#  image_updated_at             :datetime
+#  created_at                   :datetime         not null
+#  updated_at                   :datetime         not null
+#  disabled                     :boolean          default(FALSE), not null
+#  uri                          :string
+#  image_remote_url             :string
+#  visible_in_picker            :boolean          default(TRUE), not null
+#  category_id                  :bigint(8)
+#  image_storage_schema_version :integer
 #
 
 class CustomEmoji < ApplicationRecord
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 40624c73c..f789bdc55 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -3,23 +3,24 @@
 #
 # Table name: media_attachments
 #
-#  id                  :bigint(8)        not null, primary key
-#  status_id           :bigint(8)
-#  file_file_name      :string
-#  file_content_type   :string
-#  file_file_size      :integer
-#  file_updated_at     :datetime
-#  remote_url          :string           default(""), not null
-#  created_at          :datetime         not null
-#  updated_at          :datetime         not null
-#  shortcode           :string
-#  type                :integer          default("image"), not null
-#  file_meta           :json
-#  account_id          :bigint(8)
-#  description         :text
-#  scheduled_status_id :bigint(8)
-#  blurhash            :string
-#  processing          :integer
+#  id                          :bigint(8)        not null, primary key
+#  status_id                   :bigint(8)
+#  file_file_name              :string
+#  file_content_type           :string
+#  file_file_size              :integer
+#  file_updated_at             :datetime
+#  remote_url                  :string           default(""), not null
+#  created_at                  :datetime         not null
+#  updated_at                  :datetime         not null
+#  shortcode                   :string
+#  type                        :integer          default("image"), not null
+#  file_meta                   :json
+#  account_id                  :bigint(8)
+#  description                 :text
+#  scheduled_status_id         :bigint(8)
+#  blurhash                    :string
+#  processing                  :integer
+#  file_storage_schema_version :integer
 #
 
 class MediaAttachment < ApplicationRecord
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index 4e89fbf85..2802f4667 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -3,25 +3,26 @@
 #
 # Table name: preview_cards
 #
-#  id                 :bigint(8)        not null, primary key
-#  url                :string           default(""), not null
-#  title              :string           default(""), not null
-#  description        :string           default(""), not null
-#  image_file_name    :string
-#  image_content_type :string
-#  image_file_size    :integer
-#  image_updated_at   :datetime
-#  type               :integer          default("link"), not null
-#  html               :text             default(""), not null
-#  author_name        :string           default(""), not null
-#  author_url         :string           default(""), not null
-#  provider_name      :string           default(""), not null
-#  provider_url       :string           default(""), not null
-#  width              :integer          default(0), not null
-#  height             :integer          default(0), not null
-#  created_at         :datetime         not null
-#  updated_at         :datetime         not null
-#  embed_url          :string           default(""), not null
+#  id                           :bigint(8)        not null, primary key
+#  url                          :string           default(""), not null
+#  title                        :string           default(""), not null
+#  description                  :string           default(""), not null
+#  image_file_name              :string
+#  image_content_type           :string
+#  image_file_size              :integer
+#  image_updated_at             :datetime
+#  type                         :integer          default("link"), not null
+#  html                         :text             default(""), not null
+#  author_name                  :string           default(""), not null
+#  author_url                   :string           default(""), not null
+#  provider_name                :string           default(""), not null
+#  provider_url                 :string           default(""), not null
+#  width                        :integer          default(0), not null
+#  height                       :integer          default(0), not null
+#  created_at                   :datetime         not null
+#  updated_at                   :datetime         not null
+#  embed_url                    :string           default(""), not null
+#  image_storage_schema_version :integer
 #
 
 class PreviewCard < ApplicationRecord
@@ -47,6 +48,10 @@ class PreviewCard < ApplicationRecord
 
   before_save :extract_dimensions, if: :link?
 
+  def local?
+    false
+  end
+
   def missing_image?
     width.present? && height.present? && image_file_name.blank?
   end
diff --git a/app/models/status.rb b/app/models/status.rb
index 31e77770d..34fa00912 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -206,12 +206,8 @@ class Status < ApplicationRecord
   def title
     if destroyed?
       "#{account.acct} deleted status"
-    elsif reblog?
-      preview = sensitive ? '<sensitive>' : text.slice(0, 10).split("\n")[0]
-      "#{account.acct} shared #{reblog.account.acct}'s: #{preview}"
     else
-      preview = sensitive ? '<sensitive>' : text.slice(0, 20).split("\n")[0]
-      "#{account.acct}: #{preview}"
+      reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
     end
   end
 
@@ -404,7 +400,7 @@ class Status < ApplicationRecord
 
       if account.nil?
         where(visibility: visibility).not_local_only
-      elsif target_account.blocking?(account) # get rid of blocked peeps
+      elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps
         none
       elsif account.id == target_account.id # author can see own stuff
         all
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index e913f0c64..54e7c450c 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -5,7 +5,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
 
   attributes :uri, :title, :short_description, :description, :email,
              :version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits,
-             :languages, :registrations, :approval_required
+             :languages, :registrations, :approval_required, :invites_enabled
 
   has_one :contact_account, serializer: REST::AccountSerializer
 
@@ -76,6 +76,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
     Setting.registrations_mode == 'approved'
   end
 
+  def invites_enabled
+    Setting.min_invite_role == 'user'
+  end
+
   private
 
   def instance_presenter
diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb
index 880cdde92..6c0093cd4 100644
--- a/app/services/fetch_resource_service.rb
+++ b/app/services/fetch_resource_service.rb
@@ -25,7 +25,18 @@ class FetchResourceService < BaseService
   end
 
   def perform_request(&block)
-    Request.new(:get, @url).add_headers('Accept' => ACCEPT_HEADER).on_behalf_of(Account.representative).perform(&block)
+    Request.new(:get, @url).tap do |request|
+      request.add_headers('Accept' => ACCEPT_HEADER)
+
+      # In a real setting we want to sign all outgoing requests,
+      # in case the remote server has secure mode enabled and requires
+      # authentication on all resources. However, during development,
+      # sending request signatures with an inaccessible host is useless
+      # and prevents even public resources from being fetched, so
+      # don't do it
+
+      request.on_behalf_of(Account.representative) unless Rails.env.development?
+    end.perform(&block)
   end
 
   def process_response(response, terminal = false)
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index e0ec98ec9..07e06100a 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -68,11 +68,11 @@
           .hero-widget__footer__column
             %h4= t 'about.server_stats'
 
-            %div{ style: 'display: flex' }
-              .hero-widget__counter{ style: 'width: 50%' }
+            .hero-widget__counters__wrapper
+              .hero-widget__counter
                 %strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
                 %span= t 'about.user_count_after', count: @instance_presenter.user_count
-              .hero-widget__counter{ style: 'width: 50%' }
+              .hero-widget__counter
                 %strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true
                 %span
                   = t 'about.active_count_after'
diff --git a/app/views/accounts/_moved.html.haml b/app/views/accounts/_moved.html.haml
index a82f277b1..4f71b062d 100644
--- a/app/views/accounts/_moved.html.haml
+++ b/app/views/accounts/_moved.html.haml
@@ -9,8 +9,10 @@
     = link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener noreferrer' do
       .detailed-status__display-avatar
         .account__avatar-overlay
-          .account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" }
-          .account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" }
+          .account__avatar-overlay-base
+            = image_tag moved_to_account.avatar_static_url
+          .account__avatar-overlay-overlay
+            = image_tag account.avatar_static_url
 
       %span.display-name
         %bdi
diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml
index 44b10af6e..c9bd8c686 100644
--- a/app/views/admin/accounts/_account.html.haml
+++ b/app/views/admin/accounts/_account.html.haml
@@ -2,7 +2,7 @@
   %td
     = admin_account_link_to(account)
   %td
-    %div{ style: 'margin: -2px 0' }= account_badge(account, all: true)
+    %div.account-badges= account_badge(account, all: true)
   %td
     - if account.user_current_sign_in_ip
       %samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 408f94eed..e6461aad0 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -31,7 +31,7 @@
       %div
         .account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true)
 
-.dashboard__counters{ style: 'margin-top: 10px' }
+.dashboard__counters.admin-account-counters
   %div
     = link_to admin_account_statuses_path(@account.id) do
       .dashboard__counters__num= number_with_delimiter @account.statuses_count
@@ -178,18 +178,8 @@
               = @account.shared_inbox_url
               = fa_icon DeliveryFailureTracker.available?(@account.shared_inbox_url) ? 'check': 'times'
 
-  %div{ style: 'overflow: hidden' }
-    %div{ style: 'float: right' }
-      - if @account.local?
-        = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
-        - if @account.user&.otp_required_for_login?
-          = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
-        - if !@account.memorial? && @account.user_approved?
-          = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
-      - else
-        = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
-
-    %div{ style: 'float: left' }
+  %div.action-buttons
+    %div
       - if @account.local? && @account.user_approved?
         = link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
       - if @account.silenced?
@@ -216,6 +206,16 @@
         - else
           = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive'
 
+    %div
+      - if @account.local?
+        = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
+        - if @account.user&.otp_required_for_login?
+          = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
+        - if !@account.memorial? && @account.user_approved?
+          = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
+      - else
+        = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
+
   %hr.spacer/
 
   - unless @warnings.empty?
diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml
index 0b299acc5..bd67eb4fc 100644
--- a/app/views/admin/instances/index.html.haml
+++ b/app/views/admin/instances/index.html.haml
@@ -10,7 +10,7 @@
       - unless whitelist_mode?
         %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'
 
-  %div{ style: 'flex: 1 1 auto; text-align: right' }
+  %div.special-action-button
     - if whitelist_mode?
       = link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button'
     - else
diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml
index 49a666a5a..92e14c0df 100644
--- a/app/views/admin/instances/show.html.haml
+++ b/app/views/admin/instances/show.html.haml
@@ -45,11 +45,11 @@
 
 %hr.spacer/
 
-%div{ style: 'overflow: hidden' }
-  %div{ style: 'float: left' }
+%div.action-buttons
+  %div
     = link_to t('admin.accounts.title'), admin_accounts_path(remote: '1', by_domain: @instance.domain), class: 'button'
 
-  %div{ style: 'float: right' }
+  %div
     - if @domain_allow
       = link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
     - elsif @domain_block
diff --git a/app/views/admin/pending_accounts/index.html.haml b/app/views/admin/pending_accounts/index.html.haml
index 171976e33..8101d7f99 100644
--- a/app/views/admin/pending_accounts/index.html.haml
+++ b/app/views/admin/pending_accounts/index.html.haml
@@ -22,9 +22,9 @@
 
 %hr.spacer/
 
-%div{ style: 'overflow: hidden' }
-  %div{ style: 'float: right' }
-    = link_to t('admin.accounts.reject_all'), reject_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
-
+%div.action-buttons
   %div
     = link_to t('admin.accounts.approve_all'), approve_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
+
+  %div
+    = link_to t('admin.accounts.reject_all'), reject_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
diff --git a/app/views/admin/relationships/index.html.haml b/app/views/admin/relationships/index.html.haml
index 3afaff615..907477f24 100644
--- a/app/views/admin/relationships/index.html.haml
+++ b/app/views/admin/relationships/index.html.haml
@@ -17,7 +17,7 @@
       %li= filter_link_to t('admin.accounts.location.local'), location: 'local'
       %li= filter_link_to t('admin.accounts.location.remote'), location: 'remote'
 
-  .back-link{ style: 'flex: 1 1 auto; text-align: right' }
+  .back-link
     = link_to admin_account_path(@account.id) do
       = fa_icon 'chevron-left fw'
       = t('admin.statuses.back_to_account')
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index b12ea4270..4ecc8dc93 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -65,9 +65,11 @@
 
 %hr.spacer
 
-%div{ style: 'overflow: hidden; margin-bottom: 20px; clear: both' }
+%div.action-buttons
+  %div
+
   - if @report.unresolved?
-    %div{ style: 'float: right' }
+    %div
       - if @report.target_account.local?
         = link_to t('admin.accounts.warn'), new_admin_account_action_path(@report.target_account_id, type: 'none', report_id: @report.id), class: 'button'
         = link_to t('admin.accounts.disable'), new_admin_account_action_path(@report.target_account_id, type: 'disable', report_id: @report.id), class: 'button button--destructive'
diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml
index 55926f3b3..5414d69d5 100644
--- a/app/views/admin/statuses/index.html.haml
+++ b/app/views/admin/statuses/index.html.haml
@@ -9,7 +9,7 @@
     %ul
       %li= link_to t('admin.statuses.no_media'), admin_account_statuses_path(@account.id, current_params.merge(media: nil)), class: !params[:media] && 'selected'
       %li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected'
-  .back-link{ style: 'flex: 1 1 auto; text-align: right' }
+  .back-link
     = link_to admin_account_path(@account.id) do
       = fa_icon 'chevron-left fw'
       = t('admin.statuses.back_to_account')
diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml
index a7a392272..e2470198d 100644
--- a/app/views/admin/statuses/show.html.haml
+++ b/app/views/admin/statuses/show.html.haml
@@ -4,7 +4,7 @@
   = "@#{@account.acct}"
 
 .filters
-  .back-link{ style: 'flex: 1 1 auto; text-align: right' }
+  .back-link
     = link_to admin_account_path(@account.id) do
       %i.fa.fa-chevron-left.fa-fw
       = t('admin.statuses.back_to_account')
diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml
index d20ed80f8..e64802275 100644
--- a/app/views/admin/tags/index.html.haml
+++ b/app/views/admin/tags/index.html.haml
@@ -68,9 +68,9 @@
 - if params[:pending_review] == '1' || params[:unreviewed] == '1'
   %hr.spacer/
 
-  %div{ style: 'overflow: hidden' }
-    %div{ style: 'float: right' }
-      = link_to t('admin.accounts.reject_all'), reject_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
-
+  %div.action-buttons
     %div
       = link_to t('admin.accounts.approve_all'), approve_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
+
+    %div
+      = link_to t('admin.accounts.reject_all'), reject_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml
index 808dce514..e7ecfecd9 100644
--- a/app/views/application/_card.html.haml
+++ b/app/views/application/_card.html.haml
@@ -9,7 +9,6 @@
         = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
 
       .display-name
-        %span{ id: "default_account_display_name", style: "display: none" }= account.username
         %bdi
           %strong.emojify.p-name= display_name(account, custom_emojify: true)
         %span
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index bcd66fb8a..457bc1d23 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -8,8 +8,8 @@
   = render 'shared/error_messages', object: resource
 
   - if @invite.present? && @invite.autofollow?
-    .fields-group{ style: 'margin-bottom: 30px' }
-      %p.hint{ style: 'text-align: center' }= t('invites.invited_by')
+    .fields-group.invited-by
+      %p.hint= t('invites.invited_by')
       = render 'application/card', account: @invite.user.account
 
   = f.simple_fields_for :account do |ff|
diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml
index 4e6bbd7a9..b2e36f6bc 100644
--- a/app/views/auth/sessions/two_factor.html.haml
+++ b/app/views/auth/sessions/two_factor.html.haml
@@ -2,7 +2,7 @@
   = t('auth.login')
 
 = simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
-  %p.hint{ style: 'margin-bottom: 25px' }= t('simple_form.hints.sessions.otp')
+  %p.hint.otp-hint= t('simple_form.hints.sessions.otp')
 
   .fields-group
     = f.input :otp_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, autofocus: true
diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml
index ecf12b649..1170332ff 100644
--- a/app/views/directories/index.html.haml
+++ b/app/views/directories/index.html.haml
@@ -28,7 +28,6 @@
               = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
 
             .display-name
-              %span{ id: "default_account_display_name", style: "display: none" }= account.username
               %bdi
                 %strong.emojify.p-name= display_name(account, custom_emojify: true)
               %span= acct(account)
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 2be9427c5..99ab3729e 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -40,6 +40,6 @@
   %body{ class: body_classes }
     = content_for?(:content) ? yield(:content) : yield
 
-    %div{ style: 'display: none'}
+    .logo-resources
       = render file: Rails.root.join('app', 'javascript', 'images', 'logo_transparent.svg')
       = render file: Rails.root.join('app', 'javascript', 'images', 'logo_full.svg')
diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml
index 6695b12dd..75441b452 100644
--- a/app/views/layouts/embedded.html.haml
+++ b/app/views/layouts/embedded.html.haml
@@ -23,5 +23,5 @@
   %body.embed
     = yield
 
-    %div{ style: 'display: none'}
+    .logo-resources
       = render file: Rails.root.join('app', 'javascript', 'images', 'logo_transparent.svg')
diff --git a/app/views/public_timelines/show.html.haml b/app/views/public_timelines/show.html.haml
index 063089a7f..e32bd49ec 100644
--- a/app/views/public_timelines/show.html.haml
+++ b/app/views/public_timelines/show.html.haml
@@ -12,5 +12,5 @@
   - else
     %p= t('about.browse_local_posts')
 
-#mastodon-timeline{ data: { props: Oj.dump(default_props) }}
+#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(local: !Setting.show_known_fediverse_at_about_page)) }}
 #modal-container
diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml
index 5453177fd..5fc865814 100644
--- a/app/views/settings/preferences/appearance/show.html.haml
+++ b/app/views/settings/preferences/appearance/show.html.haml
@@ -9,8 +9,8 @@
     = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale, hint: false
 
   - unless I18n.locale == :en
-    .flash-message{ style: "text-align: unset; color: unset" }
-      #{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener", style: "text-decoration: underline")}
+    .flash-message.translation-prompt
+      #{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener")}
 
   %h4= t 'appearance.advanced_web_interface'
 
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index 841c01fd7..6061e9cfd 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -9,7 +9,7 @@
 
   .fields-row
     .fields-row__column.fields-group.fields-row__column-6
-      = f.input :display_name, wrapper: :with_label, input_html: { maxlength: Account::MAX_DISPLAY_NAME_LENGTH }, hint: false
+      = f.input :display_name, wrapper: :with_label, input_html: { maxlength: Account::MAX_DISPLAY_NAME_LENGTH, data: { default: @account.username } }, hint: false
       = f.input :note, wrapper: :with_label, input_html: { maxlength: Account::MAX_NOTE_LENGTH }, hint: false
 
   .fields-row
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index 021390e47..544b92330 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -15,12 +15,12 @@
 
   = account_action_button(status.account)
 
-  .status__content.emojify<
+  .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
     - if status.spoiler_text?
-      %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
+      %p<
         %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
         %button.status__content__spoiler-link= t('statuses.show_more')
-    .e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }
+    .e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
       = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
       - if status.preloadable_poll
         = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
diff --git a/app/views/statuses/_poll.html.haml b/app/views/statuses/_poll.html.haml
index de5357e6d..64e62e97c 100644
--- a/app/views/statuses/_poll.html.haml
+++ b/app/views/statuses/_poll.html.haml
@@ -10,13 +10,15 @@
           - percent = total_votes_count > 0 ? 100 * option.votes_count / total_votes_count : 0
           %label.poll__option><
             %span.poll__number><
-              - if own_votes.include?(index)
-                %i.poll__voted__mark.fa.fa-check
               = "#{percent.round}%"
             %span.poll__option__text
               = Formatter.instance.format_poll_option(status, option, autoplay: autoplay)
+            - if own_votes.include?(index)
+              %span.poll__voted
+                %i.poll__voted__mark.fa.fa-check
 
-          %span.poll__chart{ style: "width: #{percent}%" }
+          %progress{ max: 100, value: percent < 1 ? 1 : percent, 'aria-hidden': 'true' }
+            %span.poll__chart
         - else
           %label.poll__option><
             %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}><
diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml
index 8a418a1d5..f959056cd 100644
--- a/app/views/statuses/_simple_status.html.haml
+++ b/app/views/statuses/_simple_status.html.haml
@@ -19,12 +19,12 @@
           %span.display-name__account
             = acct(status.account)
             = fa_icon('lock') if status.account.locked?
-  .status__content.emojify<
+  .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
     - if status.spoiler_text?
-      %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
+      %p<
         %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
         %button.status__content__spoiler-link= t('statuses.show_more')
-    .e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
+    .e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }<
       = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
       - if status.preloadable_poll
         = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
@@ -51,18 +51,18 @@
 
   .status__action-bar
     .status__action-bar__counter
-      = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+      = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button' do
         - if status.in_reply_to_id.nil?
           = fa_icon 'reply fw'
         - else
           = fa_icon 'reply-all fw'
       .status__action-bar__counter__label= obscured_counter status.replies_count
-    = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+    = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do
       - if status.distributable?
         = fa_icon 'retweet fw'
       - elsif status.private_visibility? || status.limited_visibility?
         = fa_icon 'lock fw'
       - else
         = fa_icon 'envelope fw'
-    = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+    = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button' do
       = fa_icon 'star fw'