about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2020-05-10 15:15:39 +0200
committerThibaut Girka <thib@sitedethib.com>2020-05-10 16:19:56 +0200
commit4a70792b4a8393a0cfd83a7e70f72179899111fa (patch)
treed340885b5dfb0f990c81b1a29f529a258c49d591 /app
parentc6ff4c634caf718adf7280e04909c091d15add1d (diff)
parent4b766f984689311523b89e1b68d2a11dff3fc396 (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
- `Gemfile.lock`:
  Not a real conflict, just a glitch-soc-only dependency too close to a
  dependency that got updated upstream. Updated as well.
- `app/models/status.rb`:
  Not a real conflict, just a change too close to glitch-soc-changed code
  for optionally showing boosts in public timelines.
  Applied upstream changes.
- `app/views/layouts/application.html.haml`:
  Upstream a new, static CSS file, conflict due to glitch-soc's theming
  system, include the file regardless of the theme.
- `config/initializers/content_security_policy.rb`:
  Upstream dropped 'unsafe-inline' from the 'style-src' directive, but
  both files are very different. Removed 'unsafe-inline' as well.
Diffstat (limited to 'app')
-rw-r--r--app/controllers/accounts_controller.rb6
-rw-r--r--app/controllers/api/v1/accounts/follower_accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/following_accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/timelines/public_controller.rb4
-rw-r--r--app/controllers/auth/sessions_controller.rb7
-rw-r--r--app/controllers/settings/identity_proofs_controller.rb12
-rw-r--r--app/helpers/home_helper.rb6
-rw-r--r--app/helpers/settings_helper.rb1
-rw-r--r--app/helpers/webfinger_helper.rb19
-rw-r--r--app/javascript/mastodon/actions/streaming.js2
-rw-r--r--app/javascript/mastodon/actions/timelines.js2
-rw-r--r--app/javascript/mastodon/features/public_timeline/components/column_settings.js30
-rw-r--r--app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js2
-rw-r--r--app/javascript/mastodon/features/public_timeline/index.js29
-rw-r--r--app/javascript/mastodon/features/ui/components/__tests__/column-test.js15
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js1
-rw-r--r--app/javascript/styles/mastodon/about.scss12
-rw-r--r--app/javascript/styles/mastodon/components.scss5
-rw-r--r--app/javascript/styles/mastodon/widgets.scss6
-rw-r--r--app/lib/request.rb2
-rw-r--r--app/lib/rss/serializer.rb38
-rw-r--r--app/lib/sidekiq_error_handler.rb11
-rw-r--r--app/models/relationship_filter.rb2
-rw-r--r--app/models/remote_follow.rb3
-rw-r--r--app/models/status.rb21
-rw-r--r--app/serializers/oembed_serializer.rb2
-rw-r--r--app/serializers/rss/account_serializer.rb15
-rw-r--r--app/serializers/rss/tag_serializer.rb15
-rw-r--r--app/services/activitypub/fetch_remote_account_service.rb5
-rw-r--r--app/services/resolve_account_service.rb3
-rw-r--r--app/views/admin/reports/index.html.haml2
-rwxr-xr-xapp/views/layouts/application.html.haml2
-rw-r--r--app/views/settings/identity_proofs/_proof.html.haml1
-rw-r--r--app/workers/activitypub/delivery_worker.rb10
34 files changed, 182 insertions, 113 deletions
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 52d09cff8..e3d8c1061 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -41,7 +41,7 @@ class AccountsController < ApplicationController
       format.rss do
         expires_in 1.minute, public: true
 
-        @statuses = filtered_statuses.without_reblogs.without_replies.limit(PAGE_SIZE)
+        @statuses = filtered_statuses.without_reblogs.limit(PAGE_SIZE)
         @statuses = cache_collection(@statuses, Status)
         render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
       end
@@ -130,11 +130,11 @@ class AccountsController < ApplicationController
   end
 
   def media_requested?
-    request.path.ends_with?('/media') && !tag_requested?
+    request.path.split('.').first.ends_with?('/media') && !tag_requested?
   end
 
   def replies_requested?
-    request.path.ends_with?('/with_replies') && !tag_requested?
+    request.path.split('.').first.ends_with?('/with_replies') && !tag_requested?
   end
 
   def tag_requested?
diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
index 1daa1ed0d..2277067c9 100644
--- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
@@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
     return [] if hide_results?
 
     scope = default_accounts
-    scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
+    scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id
     scope.merge(paginated_follows).to_a
   end
 
diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb
index 6fc23cf75..93d4bd3a4 100644
--- a/app/controllers/api/v1/accounts/following_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb
@@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
     return [] if hide_results?
 
     scope = default_accounts
-    scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
+    scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id
     scope.merge(paginated_follows).to_a
   end
 
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index 581befef1..c6e7854d9 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -39,7 +39,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
   end
 
   def public_timeline_statuses
-    Status.as_public_timeline(current_account, truthy_param?(:local))
+    Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
   end
 
   def insert_pagination_headers
@@ -47,7 +47,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
   end
 
   def pagination_params(core_params)
-    params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params)
+    params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params)
   end
 
   def next_path
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index eac9dde6f..c36561b86 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -113,6 +113,13 @@ class Auth::SessionsController < Devise::SessionsController
     render :two_factor
   end
 
+  def require_no_authentication
+    super
+    # Delete flash message that isn't entirely useful and may be confusing in
+    # most cases because /web doesn't display/clear flash messages.
+    flash.delete(:alert) if flash[:alert] == I18n.t('devise.failure.already_authenticated')
+  end
+
   private
 
   def set_pack
diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb
index e84c1aca6..b217b3c3b 100644
--- a/app/controllers/settings/identity_proofs_controller.rb
+++ b/app/controllers/settings/identity_proofs_controller.rb
@@ -22,8 +22,7 @@ class Settings::IdentityProofsController < Settings::BaseController
     if current_account.username.casecmp(params[:username]).zero?
       render layout: 'auth'
     else
-      flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
-      redirect_to settings_identity_proofs_path
+      redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
     end
   end
 
@@ -35,11 +34,16 @@ class Settings::IdentityProofsController < Settings::BaseController
       PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof?
       redirect_to @proof.on_success_path(params[:user_agent])
     else
-      flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
-      redirect_to settings_identity_proofs_path
+      redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
     end
   end
 
+  def destroy
+    @proof = current_account.identity_proofs.find(params[:id])
+    @proof.destroy!
+    redirect_to settings_identity_proofs_path, success: I18n.t('identity_proofs.removed')
+  end
+
   private
 
   def check_enabled
diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb
index b66e827fe..4da68500a 100644
--- a/app/helpers/home_helper.rb
+++ b/app/helpers/home_helper.rb
@@ -7,13 +7,13 @@ module HomeHelper
     }
   end
 
-  def account_link_to(account, button = '', size: 36, path: nil)
+  def account_link_to(account, button = '', path: nil)
     content_tag(:div, class: 'account') do
       content_tag(:div, class: 'account__wrapper') do
         section = if account.nil?
                     content_tag(:div, class: 'account__display-name') do
                       content_tag(:div, class: 'account__avatar-wrapper') do
-                        content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url('avatars/original/missing.png', skip_pipeline: true)})")
+                        image_tag(full_asset_url('avatars/original/missing.png', skip_pipeline: true), class: 'account__avatar')
                       end +
                         content_tag(:span, class: 'display-name') do
                           content_tag(:strong, t('about.contact_missing')) +
@@ -23,7 +23,7 @@ module HomeHelper
                   else
                     link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
                       content_tag(:div, class: 'account__avatar-wrapper') do
-                        content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)})")
+                        image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar')
                       end +
                         content_tag(:span, class: 'display-name') do
                           content_tag(:bdi) do
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 74544bad9..87718dc05 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -68,6 +68,7 @@ module SettingsHelper
     tr: 'Türkçe',
     uk: 'Українська',
     ur: 'اُردُو',
+    vi: 'Tiếng Việt',
     'zh-CN': '简体中文',
     'zh-HK': '繁體中文(香港)',
     'zh-TW': '繁體中文(臺灣)',
diff --git a/app/helpers/webfinger_helper.rb b/app/helpers/webfinger_helper.rb
new file mode 100644
index 000000000..70c493210
--- /dev/null
+++ b/app/helpers/webfinger_helper.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module WebfingerHelper
+  def webfinger!(uri)
+    hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri)
+
+    raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && hidden_service_uri
+
+    opts = {
+      ssl: !hidden_service_uri,
+
+      headers: {
+        'User-Agent': Mastodon::Version.user_agent,
+      },
+    }
+
+    Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger
+  end
+end
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index 79b08bdda..080d665f4 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
 
 export const connectUserStream      = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
 export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
-export const connectPublicStream    = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
+export const connectPublicStream    = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
 export const connectHashtagStream   = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
 export const connectDirectStream    = () => connectTimelineStream('direct', 'direct');
 export const connectListStream      = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 861827d33..01f0fb015 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -107,7 +107,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
 };
 
 export const expandHomeTimeline            = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
-export const expandPublicTimeline          = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
+export const expandPublicTimeline          = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
 export const expandCommunityTimeline       = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
 export const expandAccountTimeline         = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
 export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
diff --git a/app/javascript/mastodon/features/public_timeline/components/column_settings.js b/app/javascript/mastodon/features/public_timeline/components/column_settings.js
new file mode 100644
index 000000000..756b6fe06
--- /dev/null
+++ b/app/javascript/mastodon/features/public_timeline/components/column_settings.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import SettingToggle from '../../notifications/components/setting_toggle';
+
+export default @injectIntl
+class ColumnSettings extends React.PureComponent {
+
+  static propTypes = {
+    settings: ImmutablePropTypes.map.isRequired,
+    onChange: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    columnId: PropTypes.string,
+  };
+
+  render () {
+    const { settings, onChange } = this.props;
+
+    return (
+      <div>
+        <div className='column-settings__row'>
+          <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
+          <SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js
index c56caa59e..8c9e8aef4 100644
--- a/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js
+++ b/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js
@@ -1,5 +1,5 @@
 import { connect } from 'react-redux';
-import ColumnSettings from '../../community_timeline/components/column_settings';
+import ColumnSettings from '../components/column_settings';
 import { changeSetting } from '../../../actions/settings';
 import { changeColumnParams } from '../../../actions/columns';
 
diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js
index 7aabd7f6e..988b1b070 100644
--- a/app/javascript/mastodon/features/public_timeline/index.js
+++ b/app/javascript/mastodon/features/public_timeline/index.js
@@ -19,11 +19,13 @@ const mapStateToProps = (state, { columnId }) => {
   const columns = state.getIn(['settings', 'columns']);
   const index = columns.findIndex(c => c.get('uuid') === uuid);
   const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
+  const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']);
   const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);
 
   return {
     hasUnread: !!timelineState && timelineState.get('unread') > 0,
     onlyMedia,
+    onlyRemote,
   };
 };
 
@@ -47,15 +49,16 @@ class PublicTimeline extends React.PureComponent {
     multiColumn: PropTypes.bool,
     hasUnread: PropTypes.bool,
     onlyMedia: PropTypes.bool,
+    onlyRemote: PropTypes.bool,
   };
 
   handlePin = () => {
-    const { columnId, dispatch, onlyMedia } = this.props;
+    const { columnId, dispatch, onlyMedia, onlyRemote } = this.props;
 
     if (columnId) {
       dispatch(removeColumn(columnId));
     } else {
-      dispatch(addColumn('PUBLIC', { other: { onlyMedia } }));
+      dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } }));
     }
   }
 
@@ -69,19 +72,19 @@ class PublicTimeline extends React.PureComponent {
   }
 
   componentDidMount () {
-    const { dispatch, onlyMedia } = this.props;
+    const { dispatch, onlyMedia, onlyRemote } = this.props;
 
-    dispatch(expandPublicTimeline({ onlyMedia }));
-    this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
+    dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
+    this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
   }
 
   componentDidUpdate (prevProps) {
-    if (prevProps.onlyMedia !== this.props.onlyMedia) {
-      const { dispatch, onlyMedia } = this.props;
+    if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) {
+      const { dispatch, onlyMedia, onlyRemote } = this.props;
 
       this.disconnect();
-      dispatch(expandPublicTimeline({ onlyMedia }));
-      this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
+      dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
+      this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
     }
   }
 
@@ -97,13 +100,13 @@ class PublicTimeline extends React.PureComponent {
   }
 
   handleLoadMore = maxId => {
-    const { dispatch, onlyMedia } = this.props;
+    const { dispatch, onlyMedia, onlyRemote } = this.props;
 
-    dispatch(expandPublicTimeline({ maxId, onlyMedia }));
+    dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
   }
 
   render () {
-    const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia } = this.props;
+    const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
     const pinned = !!columnId;
 
     return (
@@ -122,7 +125,7 @@ class PublicTimeline extends React.PureComponent {
         </ColumnHeader>
 
         <StatusListContainer
-          timelineId={`public${onlyMedia ? ':media' : ''}`}
+          timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
           onLoadMore={this.handleLoadMore}
           trackScroll={!pinned}
           scrollKey={`public_timeline-${columnId}`}
diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
index 89cb2458d..d2791ce08 100644
--- a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
+++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
@@ -5,30 +5,21 @@ import ColumnHeader from '../column_header';
 
 describe('<Column />', () => {
   describe('<ColumnHeader /> click handler', () => {
-    const originalRaf = global.requestAnimationFrame;
-
-    beforeEach(() => {
-      global.requestAnimationFrame = jest.fn();
-    });
-
-    afterAll(() => {
-      global.requestAnimationFrame = originalRaf;
-    });
-
     it('runs the scroll animation if the column contains scrollable content', () => {
       const wrapper = mount(
         <Column heading='notifications'>
           <div className='scrollable' />
         </Column>,
       );
+      const scrollToMock = jest.fn();
+      wrapper.find(Column).find('.scrollable').getDOMNode().scrollTo = scrollToMock;
       wrapper.find(ColumnHeader).find('button').simulate('click');
-      expect(global.requestAnimationFrame.mock.calls.length).toEqual(1);
+      expect(scrollToMock).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 });
     });
 
     it('does not try to scroll if there is no scrollable content', () => {
       const wrapper = mount(<Column heading='notifications' />);
       wrapper.find(ColumnHeader).find('button').simulate('click');
-      expect(global.requestAnimationFrame.mock.calls.length).toEqual(0);
     });
   });
 });
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 8bc4bfc0e..9b03cf26d 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -37,6 +37,7 @@ const componentMap = {
   'HOME': HomeTimeline,
   'NOTIFICATIONS': Notifications,
   'PUBLIC': PublicTimeline,
+  'REMOTE': PublicTimeline,
   'COMMUNITY': CommunityTimeline,
   'HASHTAG': HashtagTimeline,
   'DIRECT': DirectTimeline,
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 711f34965..3be0aee49 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -543,12 +543,6 @@ $small-breakpoint: 960px;
         flex: 0 0 auto;
       }
 
-      &__avatar {
-        width: 44px;
-        height: 44px;
-        background-size: 44px 44px;
-      }
-
       .display-name {
         font-size: 15px;
 
@@ -749,12 +743,6 @@ $small-breakpoint: 960px;
         display: flex;
         align-items: center;
       }
-
-      .account__avatar {
-        width: 44px;
-        height: 44px;
-        background-size: 44px 44px;
-      }
     }
 
     &__counters__wrapper {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 6c33b709d..65a3bab8d 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1318,8 +1318,13 @@
 
 .account__avatar {
   @include avatar-radius;
+  display: block;
   position: relative;
 
+  width: 36px;
+  height: 36px;
+  background-size: 36px 36px;
+
   &-inline {
     display: inline-block;
     vertical-align: middle;
diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index ca050a8d9..5b97d1ec4 100644
--- a/app/javascript/styles/mastodon/widgets.scss
+++ b/app/javascript/styles/mastodon/widgets.scss
@@ -93,12 +93,6 @@
       display: flex;
       align-items: center;
     }
-
-    .account__avatar {
-      width: 44px;
-      height: 44px;
-      background-size: 44px 44px;
-    }
   }
 
   .trends__item {
diff --git a/app/lib/request.rb b/app/lib/request.rb
index c476e7785..247c32958 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -73,8 +73,6 @@ class Request
       response.body_with_limit if http_client.persistent?
 
       yield response if block_given?
-    rescue => e
-      raise e.class, e.message, e.backtrace[0]
     ensure
       http_client.close unless http_client.persistent?
     end
diff --git a/app/lib/rss/serializer.rb b/app/lib/rss/serializer.rb
new file mode 100644
index 000000000..fd56c568c
--- /dev/null
+++ b/app/lib/rss/serializer.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class RSS::Serializer
+  private
+
+  def render_statuses(builder, statuses)
+    statuses.each do |status|
+      builder.item do |item|
+        item.title(status_title(status))
+            .link(ActivityPub::TagManager.instance.url_for(status))
+            .pub_date(status.created_at)
+            .description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str)
+
+        status.media_attachments.each do |media|
+          item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
+        end
+      end
+    end
+  end
+
+  def status_title(status)
+    return "#{status.account.acct} deleted status" if status.destroyed?
+
+    preview = status.proper.spoiler_text.presence || status.proper.text
+    if preview.length > 30 || preview[0, 30].include?("\n")
+      preview = preview[0, 30]
+      preview = preview[0, preview.index("\n").presence || 30] + '…'
+    end
+
+    preview = "#{status.proper.spoiler_text.present? ? 'CW ' : ''}“#{preview}”#{status.proper.sensitive? ? ' (sensitive)' : ''}"
+
+    if status.reblog?
+      "#{status.account.acct} boosted #{status.reblog.account.acct}: #{preview}"
+    else
+      "#{status.account.acct}: #{preview}"
+    end
+  end
+end
diff --git a/app/lib/sidekiq_error_handler.rb b/app/lib/sidekiq_error_handler.rb
index 8eb6b942d..b07817d45 100644
--- a/app/lib/sidekiq_error_handler.rb
+++ b/app/lib/sidekiq_error_handler.rb
@@ -1,13 +1,24 @@
 # frozen_string_literal: true
 
 class SidekiqErrorHandler
+  BACKTRACE_LIMIT = 3
+
   def call(*)
     yield
   rescue Mastodon::HostValidationError
     # Do not retry
+  rescue => e
+    limit_backtrace_and_raise(e)
   ensure
     socket = Thread.current[:statsd_socket]
     socket&.close
     Thread.current[:statsd_socket] = nil
   end
+
+  private
+
+  def limit_backtrace_and_raise(e)
+    e.set_backtrace(e.backtrace.first(BACKTRACE_LIMIT))
+    raise e
+  end
 end
diff --git a/app/models/relationship_filter.rb b/app/models/relationship_filter.rb
index e6859bf3d..9135ff144 100644
--- a/app/models/relationship_filter.rb
+++ b/app/models/relationship_filter.rb
@@ -23,7 +23,7 @@ class RelationshipFilter
     scope = scope_for('relationship', params['relationship'].to_s.strip)
 
     params.each do |key, value|
-      next if key.to_s == 'page'
+      next if %w(relationship page).include?(key)
 
       scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present?
     end
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index 5ea535287..30b84f7d5 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -3,6 +3,7 @@
 class RemoteFollow
   include ActiveModel::Validations
   include RoutingHelper
+  include WebfingerHelper
 
   attr_accessor :acct, :addressable_template
 
@@ -71,7 +72,7 @@ class RemoteFollow
   end
 
   def acct_resource
-    @acct_resource ||= Goldfinger.finger("acct:#{acct}")
+    @acct_resource ||= webfinger!("acct:#{acct}")
   rescue Goldfinger::Error, HTTP::ConnectionError
     nil
   end
diff --git a/app/models/status.rb b/app/models/status.rb
index 34fa00912..341f72090 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -203,14 +203,6 @@ class Status < ApplicationRecord
     preview_cards.first
   end
 
-  def title
-    if destroyed?
-      "#{account.acct} deleted status"
-    else
-      reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
-    end
-  end
-
   def hidden?
     !distributable?
   end
@@ -342,7 +334,7 @@ class Status < ApplicationRecord
       query = timeline_scope(local_only)
       query = query.without_replies unless Setting.show_replies_in_public_timelines
 
-      apply_timeline_filters(query, account, local_only)
+      apply_timeline_filters(query, account, [:local, true].include?(local_only))
     end
 
     def as_tag_timeline(tag, account = nil, local_only = false)
@@ -434,8 +426,15 @@ class Status < ApplicationRecord
 
     private
 
-    def timeline_scope(local_only = false)
-      starting_scope = local_only ? Status.local : Status
+    def timeline_scope(scope = false)
+      starting_scope = case scope
+                       when :local, true
+                         Status.local
+                       when :remote
+                         Status.remote
+                       else
+                         Status
+                       end
       starting_scope = starting_scope.with_public_visibility
       if Setting.show_reblogs_in_public_timelines
         starting_scope
diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb
index 01689633b..d6261d724 100644
--- a/app/serializers/oembed_serializer.rb
+++ b/app/serializers/oembed_serializer.rb
@@ -4,7 +4,7 @@ class OEmbedSerializer < ActiveModel::Serializer
   include RoutingHelper
   include ActionView::Helpers::TagHelper
 
-  attributes :type, :version, :title, :author_name,
+  attributes :type, :version, :author_name,
              :author_url, :provider_name, :provider_url,
              :cache_age, :html, :width, :height
 
diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb
index ee972ff96..81e24af0d 100644
--- a/app/serializers/rss/account_serializer.rb
+++ b/app/serializers/rss/account_serializer.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class RSS::AccountSerializer
+class RSS::AccountSerializer < RSS::Serializer
   include ActionView::Helpers::NumberHelper
   include AccountsHelper
   include RoutingHelper
@@ -17,18 +17,7 @@ class RSS::AccountSerializer
     builder.image(full_asset_url(account.avatar.url(:original))) if account.avatar?
     builder.cover(full_asset_url(account.header.url(:original))) if account.header?
 
-    statuses.each do |status|
-      builder.item do |item|
-        item.title(status.title)
-            .link(ActivityPub::TagManager.instance.url_for(status))
-            .pub_date(status.created_at)
-            .description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str)
-
-        status.media_attachments.each do |media|
-          item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
-        end
-      end
-    end
+    render_statuses(builder, statuses)
 
     builder.to_xml
   end
diff --git a/app/serializers/rss/tag_serializer.rb b/app/serializers/rss/tag_serializer.rb
index ea26189a2..e549ac367 100644
--- a/app/serializers/rss/tag_serializer.rb
+++ b/app/serializers/rss/tag_serializer.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class RSS::TagSerializer
+class RSS::TagSerializer < RSS::Serializer
   include ActionView::Helpers::NumberHelper
   include ActionView::Helpers::SanitizeHelper
   include RoutingHelper
@@ -14,18 +14,7 @@ class RSS::TagSerializer
            .logo(full_pack_url('media/images/logo.svg'))
            .accent_color('2b90d9')
 
-    statuses.each do |status|
-      builder.item do |item|
-        item.title(status.title)
-            .link(ActivityPub::TagManager.instance.url_for(status))
-            .pub_date(status.created_at)
-            .description(status.spoiler_text.presence || Formatter.instance.format(status).to_str)
-
-        status.media_attachments.each do |media|
-          item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
-        end
-      end
-    end
+    render_statuses(builder, statuses)
 
     builder.to_xml
   end
diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb
index d65c8f951..83fbf6d07 100644
--- a/app/services/activitypub/fetch_remote_account_service.rb
+++ b/app/services/activitypub/fetch_remote_account_service.rb
@@ -3,6 +3,7 @@
 class ActivityPub::FetchRemoteAccountService < BaseService
   include JsonLdHelper
   include DomainControlHelper
+  include WebfingerHelper
 
   SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
 
@@ -35,12 +36,12 @@ class ActivityPub::FetchRemoteAccountService < BaseService
   private
 
   def verified_webfinger?
-    webfinger                            = Goldfinger.finger("acct:#{@username}@#{@domain}")
+    webfinger                            = webfinger!("acct:#{@username}@#{@domain}")
     confirmed_username, confirmed_domain = split_acct(webfinger.subject)
 
     return webfinger.link('self')&.href == @uri if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
 
-    webfinger                            = Goldfinger.finger("acct:#{confirmed_username}@#{confirmed_domain}")
+    webfinger                            = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
     @username, @domain                   = split_acct(webfinger.subject)
     self_reference                       = webfinger.link('self')
 
diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb
index 1ad9ed407..17ace100c 100644
--- a/app/services/resolve_account_service.rb
+++ b/app/services/resolve_account_service.rb
@@ -3,6 +3,7 @@
 class ResolveAccountService < BaseService
   include JsonLdHelper
   include DomainControlHelper
+  include WebfingerHelper
 
   class WebfingerRedirectError < StandardError; end
 
@@ -76,7 +77,7 @@ class ResolveAccountService < BaseService
   end
 
   def process_webfinger!(uri, redirected = false)
-    @webfinger                           = Goldfinger.finger("acct:#{uri}")
+    @webfinger                           = webfinger!("acct:#{uri}")
     confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
 
     if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml
index 0263b80fb..2149fcc46 100644
--- a/app/views/admin/reports/index.html.haml
+++ b/app/views/admin/reports/index.html.haml
@@ -25,7 +25,7 @@
   - target_account = reports.first.target_account
   .report-card
     .report-card__profile
-      = account_link_to target_account, '', size: 36, path: admin_account_path(target_account.id)
+      = account_link_to target_account, '', path: admin_account_path(target_account.id)
       .report-card__profile__stats
         = link_to t('admin.reports.account.notes', count: target_account.targeted_moderation_notes.count), admin_account_path(target_account.id)
         %br/
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 99ab3729e..92edaea3c 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -28,6 +28,8 @@
         = javascript_pack_tag "locales/#{@theme[:flavour]}/en", integrity: true, crossorigin: 'anonymous'
     = csrf_meta_tags
 
+    = stylesheet_link_tag '/inert.css', skip_pipeline: true, media: 'all', id: 'inert-style'
+
     = yield :header_tags
 
     -#  These must come after :header_tags to ensure our initial state has been defined.
diff --git a/app/views/settings/identity_proofs/_proof.html.haml b/app/views/settings/identity_proofs/_proof.html.haml
index 524827ad7..14e8e91be 100644
--- a/app/views/settings/identity_proofs/_proof.html.haml
+++ b/app/views/settings/identity_proofs/_proof.html.haml
@@ -18,3 +18,4 @@
 
   %td
     = table_link_to 'external-link', t('identity_proofs.view_proof'), proof.badge.proof_url if proof.badge.proof_url
+    = table_link_to 'trash', t('identity_proofs.remove'), settings_identity_proof_path(proof), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb
index 14e37dc34..60775787a 100644
--- a/app/workers/activitypub/delivery_worker.rb
+++ b/app/workers/activitypub/delivery_worker.rb
@@ -52,13 +52,9 @@ class ActivityPub::DeliveryWorker
       end
     end
 
-    begin
-      light.with_threshold(STOPLIGHT_FAILURE_THRESHOLD)
-           .with_cool_off_time(STOPLIGHT_COOLDOWN)
-           .run
-    rescue Stoplight::Error::RedLight => e
-      raise e.class, e.message, e.backtrace.first(3)
-    end
+    light.with_threshold(STOPLIGHT_FAILURE_THRESHOLD)
+         .with_cool_off_time(STOPLIGHT_COOLDOWN)
+         .run
   end
 
   def failure_tracker