about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/about_controller.rb27
-rw-r--r--app/controllers/accounts_controller.rb8
-rw-r--r--app/controllers/api/v1/accounts/statuses_controller.rb8
-rw-r--r--app/controllers/api/v1/timelines/tag_controller.rb2
-rw-r--r--app/controllers/public_timelines_controller.rb34
-rw-r--r--app/controllers/tags_controller.rb18
-rw-r--r--app/helpers/home_helper.rb8
-rw-r--r--app/helpers/jsonld_helper.rb9
-rw-r--r--app/javascript/mastodon/containers/timeline_container.js12
-rw-r--r--app/javascript/mastodon/features/standalone/community_timeline/index.js71
-rw-r--r--app/javascript/mastodon/features/standalone/hashtag_timeline/index.js8
-rw-r--r--app/javascript/mastodon/features/standalone/public_timeline/index.js122
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js2
-rw-r--r--app/javascript/styles/mastodon/about.scss816
-rw-r--r--app/javascript/styles/mastodon/forms.scss21
-rw-r--r--app/javascript/styles/mastodon/polls.scss12
-rw-r--r--app/javascript/styles/mastodon/widgets.scss5
-rw-r--r--app/lib/activitypub/activity/create.rb18
-rw-r--r--app/lib/activitypub/activity/delete.rb9
-rw-r--r--app/lib/activitypub/activity/update.rb14
-rw-r--r--app/models/account.rb5
-rw-r--r--app/models/concerns/expireable.rb6
-rw-r--r--app/models/tag.rb8
-rw-r--r--app/presenters/instance_presenter.rb8
-rw-r--r--app/services/activitypub/fetch_replies_service.rb9
-rw-r--r--app/services/notify_service.rb2
-rw-r--r--app/services/remove_status_service.rb6
-rw-r--r--app/services/vote_service.rb32
-rw-r--r--app/views/about/_features.html.haml25
-rw-r--r--app/views/about/_forms.html.haml15
-rw-r--r--app/views/about/_links.html.haml16
-rw-r--r--app/views/about/_login.html.haml13
-rw-r--r--app/views/about/_registration.html.haml20
-rw-r--r--app/views/about/show.html.haml207
-rw-r--r--app/views/layouts/public.html.haml33
-rw-r--r--app/views/public_timelines/show.html.haml14
-rw-r--r--app/views/remote_follow/new.html.haml3
-rw-r--r--app/views/remote_interaction/new.html.haml3
-rw-r--r--app/views/tags/show.html.haml1
-rw-r--r--app/workers/activitypub/distribute_poll_update_worker.rb15
40 files changed, 590 insertions, 1075 deletions
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index ce1e8293c..f459bab19 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -2,21 +2,17 @@
 
 class AboutController < ApplicationController
   before_action :set_pack
-  before_action :set_body_classes
+  layout 'public'
+
   before_action :set_instance_presenter, only: [:show, :more, :terms]
 
   def show
-    serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
-    @initial_state_json   = serializable_resource.to_json
+    @hide_navbar = true
   end
 
-  def more
-    render layout: 'public'
-  end
+  def more; end
 
-  def terms
-    render layout: 'public'
-  end
+  def terms; end
 
   private
 
@@ -27,21 +23,10 @@ class AboutController < ApplicationController
   helper_method :new_user
 
   def set_pack
-    use_pack action_name == 'show' ? 'about' : 'common'
+    use_pack 'common'
   end
 
   def set_instance_presenter
     @instance_presenter = InstancePresenter.new
   end
-
-  def set_body_classes
-    @body_classes = 'with-modals'
-  end
-
-  def initial_state_params
-    {
-      settings: { known_fediverse: Setting.show_known_fediverse_at_about_page },
-      token: current_session&.token,
-    }
-  end
 end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 442e99089..022355bd0 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -81,7 +81,13 @@ class AccountsController < ApplicationController
   end
 
   def hashtag_scope
-    Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id)
+    tag = Tag.find_normalized(params[:tag])
+
+    if tag
+      Status.tagged_with(tag.id)
+    else
+      Status.none
+    end
   end
 
   def set_account
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index ed10f3f6a..8cd8f8e79 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -69,7 +69,13 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
   end
 
   def hashtag_scope
-    Status.tagged_with(Tag.find_by(name: params[:tagged])&.id)
+    tag = Tag.find_normalized(params[:tagged])
+
+    if tag
+      Status.tagged_with(tag.id)
+    else
+      Status.none
+    end
   end
 
   def pagination_params(core_params)
diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index 92c32c178..9adc4ad29 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -14,7 +14,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
   private
 
   def load_tag
-    @tag = Tag.find_by(name: params[:id].downcase)
+    @tag = Tag.find_normalized(params[:id])
   end
 
   def load_statuses
diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb
new file mode 100644
index 000000000..53d4472d8
--- /dev/null
+++ b/app/controllers/public_timelines_controller.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class PublicTimelinesController < ApplicationController
+  layout 'public'
+
+  before_action :check_enabled
+  before_action :set_body_classes
+  before_action :set_instance_presenter
+
+  def show
+    respond_to do |format|
+      format.html do
+        @initial_state_json = ActiveModelSerializers::SerializableResource.new(
+          InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token),
+          serializer: InitialStateSerializer
+        ).to_json
+      end
+    end
+  end
+
+  private
+
+  def check_enabled
+    raise ActiveRecord::RecordNotFound unless Setting.timeline_preview
+  end
+
+  def set_body_classes
+    @body_classes = 'with-modals'
+  end
+
+  def set_instance_presenter
+    @instance_presenter = InstancePresenter.new
+  end
+end
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 186d276c2..5cb048c1a 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -9,13 +9,15 @@ class TagsController < ApplicationController
   before_action :set_instance_presenter
 
   def show
-    @tag = Tag.find_by!(name: params[:id].downcase)
+    @tag = Tag.find_normalized!(params[:id])
 
     respond_to do |format|
       format.html do
         use_pack 'about'
-        serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
-        @initial_state_json   = serializable_resource.to_json
+        @initial_state_json = ActiveModelSerializers::SerializableResource.new(
+          InitialStatePresenter.new(settings: {}, token: current_session&.token),
+          serializer: InitialStateSerializer
+        ).to_json
       end
 
       format.rss do
@@ -26,8 +28,7 @@ class TagsController < ApplicationController
       end
 
       format.json do
-        @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local])
-                                       .paginate_by_max_id(PAGE_SIZE, params[:max_id])
+        @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
         @statuses = cache_collection(@statuses, Status)
 
         render json: collection_presenter,
@@ -56,11 +57,4 @@ class TagsController < ApplicationController
       items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
     )
   end
-
-  def initial_state_params
-    {
-      settings: {},
-      token: current_session&.token,
-    }
-  end
 end
diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb
index 9b3f1380b..1f648649f 100644
--- a/app/helpers/home_helper.rb
+++ b/app/helpers/home_helper.rb
@@ -56,4 +56,12 @@ module HomeHelper
       'emojify'
     end
   end
+
+  def optional_link_to(condition, path, options = {}, &block)
+    if condition
+      link_to(path, options, &block)
+    else
+      content_tag(:div, &block)
+    end
+  end
 end
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index f0a19e332..5b4011275 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -47,6 +47,15 @@ module JsonLdHelper
     !uri.start_with?('http://', 'https://')
   end
 
+  def invalid_origin?(url)
+    return true if unsupported_uri_scheme?(url)
+
+    needle   = Addressable::URI.parse(url).host
+    haystack = Addressable::URI.parse(@account.uri).host
+
+    !haystack.casecmp(needle).zero?
+  end
+
   def canonicalize(json)
     graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
     graph.dump(:normalize)
diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js
index a1a4bd024..54f8eb310 100644
--- a/app/javascript/mastodon/containers/timeline_container.js
+++ b/app/javascript/mastodon/containers/timeline_container.js
@@ -7,7 +7,6 @@ import { hydrateStore } from '../actions/store';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
 import PublicTimeline from '../features/standalone/public_timeline';
-import CommunityTimeline from '../features/standalone/community_timeline';
 import HashtagTimeline from '../features/standalone/hashtag_timeline';
 import ModalContainer from '../features/ui/containers/modal_container';
 import initialState from '../initial_state';
@@ -26,24 +25,22 @@ export default class TimelineContainer extends React.PureComponent {
   static propTypes = {
     locale: PropTypes.string.isRequired,
     hashtag: PropTypes.string,
-    showPublicTimeline: PropTypes.bool.isRequired,
+    local: PropTypes.bool,
   };
 
   static defaultProps = {
-    showPublicTimeline: initialState.settings.known_fediverse,
+    local: !initialState.settings.known_fediverse,
   };
 
   render () {
-    const { locale, hashtag, showPublicTimeline } = this.props;
+    const { locale, hashtag, local } = this.props;
 
     let timeline;
 
     if (hashtag) {
       timeline = <HashtagTimeline hashtag={hashtag} />;
-    } else if (showPublicTimeline) {
-      timeline = <PublicTimeline />;
     } else {
-      timeline = <CommunityTimeline />;
+      timeline = <PublicTimeline local={local} />;
     }
 
     return (
@@ -51,6 +48,7 @@ export default class TimelineContainer extends React.PureComponent {
         <Provider store={store}>
           <Fragment>
             {timeline}
+
             {ReactDOM.createPortal(
               <ModalContainer />,
               document.getElementById('modal-container'),
diff --git a/app/javascript/mastodon/features/standalone/community_timeline/index.js b/app/javascript/mastodon/features/standalone/community_timeline/index.js
deleted file mode 100644
index f917f41c9..000000000
--- a/app/javascript/mastodon/features/standalone/community_timeline/index.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import StatusListContainer from '../../ui/containers/status_list_container';
-import { expandCommunityTimeline } from '../../../actions/timelines';
-import Column from '../../../components/column';
-import ColumnHeader from '../../../components/column_header';
-import { defineMessages, injectIntl } from 'react-intl';
-import { connectCommunityStream } from '../../../actions/streaming';
-
-const messages = defineMessages({
-  title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
-});
-
-export default @connect()
-@injectIntl
-class CommunityTimeline extends React.PureComponent {
-
-  static propTypes = {
-    dispatch: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-  };
-
-  handleHeaderClick = () => {
-    this.column.scrollTop();
-  }
-
-  setRef = c => {
-    this.column = c;
-  }
-
-  componentDidMount () {
-    const { dispatch } = this.props;
-
-    dispatch(expandCommunityTimeline());
-    this.disconnect = dispatch(connectCommunityStream());
-  }
-
-  componentWillUnmount () {
-    if (this.disconnect) {
-      this.disconnect();
-      this.disconnect = null;
-    }
-  }
-
-  handleLoadMore = maxId => {
-    this.props.dispatch(expandCommunityTimeline({ maxId }));
-  }
-
-  render () {
-    const { intl } = this.props;
-
-    return (
-      <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
-        <ColumnHeader
-          icon='users'
-          title={intl.formatMessage(messages.title)}
-          onClick={this.handleHeaderClick}
-        />
-
-        <StatusListContainer
-          timelineId='community'
-          onLoadMore={this.handleLoadMore}
-          scrollKey='standalone_public_timeline'
-          trackScroll={false}
-        />
-      </Column>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
index 333726f94..0880d98c8 100644
--- a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
@@ -2,13 +2,13 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { expandHashtagTimeline } from '../../../actions/timelines';
-import { connectHashtagStream } from '../../../actions/streaming';
+import { expandHashtagTimeline } from 'mastodon/actions/timelines';
+import { connectHashtagStream } from 'mastodon/actions/streaming';
 import Masonry from 'react-masonry-infinite';
 import { List as ImmutableList } from 'immutable';
-import DetailedStatusContainer from '../../status/containers/detailed_status_container';
+import DetailedStatusContainer from 'mastodon/features/status/containers/detailed_status_container';
 import { debounce } from 'lodash';
-import LoadingIndicator from '../../../components/loading_indicator';
+import LoadingIndicator from 'mastodon/components/loading_indicator';
 
 const mapStateToProps = (state, { hashtag }) => ({
   statusIds: state.getIn(['timelines', `hashtag:${hashtag}`, 'items'], ImmutableList()),
diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js
index 618696eb1..10129e606 100644
--- a/app/javascript/mastodon/features/standalone/public_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js
@@ -1,70 +1,112 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from '../../ui/containers/status_list_container';
-import { expandPublicTimeline } from '../../../actions/timelines';
-import Column from '../../../components/column';
-import ColumnHeader from '../../../components/column_header';
-import { defineMessages, injectIntl } from 'react-intl';
-import { connectPublicStream } from '../../../actions/streaming';
-
-const messages = defineMessages({
-  title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
-});
-
-export default @connect()
-@injectIntl
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
+import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming';
+import Masonry from 'react-masonry-infinite';
+import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
+import DetailedStatusContainer from 'mastodon/features/status/containers/detailed_status_container';
+import { debounce } from 'lodash';
+import LoadingIndicator from 'mastodon/components/loading_indicator';
+
+const mapStateToProps = (state, { local }) => {
+  const timeline = state.getIn(['timelines', local ? 'community' : 'public'], ImmutableMap());
+
+  return {
+    statusIds: timeline.get('items', ImmutableList()),
+    isLoading: timeline.get('isLoading', false),
+    hasMore: timeline.get('hasMore', false),
+  };
+};
+
+export default @connect(mapStateToProps)
 class PublicTimeline extends React.PureComponent {
 
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
+    statusIds: ImmutablePropTypes.list.isRequired,
+    isLoading: PropTypes.bool.isRequired,
+    hasMore: PropTypes.bool.isRequired,
+    local: PropTypes.bool,
   };
 
-  handleHeaderClick = () => {
-    this.column.scrollTop();
+  componentDidMount () {
+    this._connect();
   }
 
-  setRef = c => {
-    this.column = c;
+  componentDidUpdate (prevProps) {
+    if (prevProps.local !== this.props.local) {
+      this._disconnect();
+      this._connect();
+    }
   }
 
-  componentDidMount () {
-    const { dispatch } = this.props;
+  componentWillUnmount () {
+    this._disconnect();
+  }
+
+  _connect () {
+    const { dispatch, local } = this.props;
 
-    dispatch(expandPublicTimeline());
-    this.disconnect = dispatch(connectPublicStream());
+    dispatch(local ? expandCommunityTimeline() : expandPublicTimeline());
+    this.disconnect = dispatch(local ? connectCommunityStream() : connectPublicStream());
   }
 
-  componentWillUnmount () {
+  _disconnect () {
     if (this.disconnect) {
       this.disconnect();
       this.disconnect = null;
     }
   }
 
-  handleLoadMore = maxId => {
-    this.props.dispatch(expandPublicTimeline({ maxId }));
+  handleLoadMore = () => {
+    const { dispatch, statusIds, local } = this.props;
+    const maxId = statusIds.last();
+
+    if (maxId) {
+      dispatch(local ? expandCommunityTimeline({ maxId }) : expandPublicTimeline({ maxId }));
+    }
+  }
+
+  setRef = c => {
+    this.masonry = c;
   }
 
+  handleHeightChange = debounce(() => {
+    if (!this.masonry) {
+      return;
+    }
+
+    this.masonry.forcePack();
+  }, 50)
+
   render () {
-    const { intl } = this.props;
+    const { statusIds, hasMore, isLoading } = this.props;
+
+    const sizes = [
+      { columns: 1, gutter: 0 },
+      { mq: '415px', columns: 1, gutter: 10 },
+      { mq: '640px', columns: 2, gutter: 10 },
+      { mq: '960px', columns: 3, gutter: 10 },
+      { mq: '1255px', columns: 3, gutter: 10 },
+    ];
+
+    const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined;
 
     return (
-      <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
-        <ColumnHeader
-          icon='globe'
-          title={intl.formatMessage(messages.title)}
-          onClick={this.handleHeaderClick}
-        />
-
-        <StatusListContainer
-          timelineId='public'
-          onLoadMore={this.handleLoadMore}
-          scrollKey='standalone_public_timeline'
-          trackScroll={false}
-        />
-      </Column>
+      <Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
+        {statusIds.map(statusId => (
+          <div className='statuses-grid__item' key={statusId}>
+            <DetailedStatusContainer
+              id={statusId}
+              compact
+              measureHeight
+              onHeightChange={this.handleHeightChange}
+            />
+          </div>
+        )).toArray()}
+      </Masonry>
     );
   }
 
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 5cd50f055..5c79f9f19 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -23,7 +23,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
   };
 
   static propTypes = {
-    status: ImmutablePropTypes.map.isRequired,
+    status: ImmutablePropTypes.map,
     onOpenMedia: PropTypes.func.isRequired,
     onOpenVideo: PropTypes.func.isRequired,
     onToggleHidden: PropTypes.func.isRequired,
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index b078d4d24..465ef2c11 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -193,6 +193,7 @@ $small-breakpoint: 960px;
     }
 
     strong {
+      font-family: $font-display, sans-serif;
       font-weight: 500;
       font-size: 32px;
       line-height: 48px;
@@ -280,168 +281,6 @@ $small-breakpoint: 960px;
 }
 
 .landing-page {
-  .grid {
-    display: grid;
-    grid-gap: 10px;
-    grid-template-columns: 1fr 2fr;
-    grid-auto-columns: 25%;
-    grid-auto-rows: max-content;
-
-    .column-0 {
-      display: none;
-    }
-
-    .column-1 {
-      grid-column: 1;
-      grid-row: 1;
-    }
-
-    .column-2 {
-      grid-column: 2;
-      grid-row: 1;
-    }
-
-    .column-3 {
-      grid-column: 3;
-      grid-row: 1 / 3;
-    }
-
-    .column-4 {
-      grid-column: 1 / 3;
-      grid-row: 2;
-    }
-  }
-
-  @media screen and (max-width: $small-breakpoint) {
-    .grid {
-      grid-template-columns: 40% 60%;
-
-      .column-0 {
-        display: none;
-      }
-
-      .column-1 {
-        grid-column: 1;
-        grid-row: 1;
-
-        &.non-preview .landing-page__forms {
-          height: 100%;
-        }
-      }
-
-      .column-2 {
-        grid-column: 2;
-        grid-row: 1 / 3;
-
-        &.non-preview {
-          grid-column: 2;
-          grid-row: 1;
-        }
-      }
-
-      .column-3 {
-        grid-column: 1;
-        grid-row: 2 / 4;
-      }
-
-      .column-4 {
-        grid-column: 2;
-        grid-row: 3;
-
-        &.non-preview {
-          grid-column: 1 / 3;
-          grid-row: 2;
-        }
-      }
-    }
-  }
-
-  @media screen and (max-width: $column-breakpoint) {
-    .grid {
-      grid-template-columns: 100%;
-
-      .column-0 {
-        display: block;
-        grid-column: 1;
-        grid-row: 1;
-      }
-
-      .column-1 {
-        grid-column: 1;
-        grid-row: 3;
-
-        .brand {
-          display: none;
-        }
-      }
-
-      .column-2 {
-        grid-column: 1;
-        grid-row: 2;
-
-        .landing-page__logo,
-        .landing-page__call-to-action {
-          display: none;
-        }
-
-        &.non-preview {
-          grid-column: 1;
-          grid-row: 2;
-        }
-      }
-
-      .column-3 {
-        grid-column: 1;
-        grid-row: 5;
-      }
-
-      .column-4 {
-        grid-column: 1;
-        grid-row: 4;
-
-        &.non-preview {
-          grid-column: 1;
-          grid-row: 4;
-        }
-      }
-    }
-  }
-
-  .column-flex {
-    display: flex;
-    flex-direction: column;
-  }
-
-  .separator-or {
-    position: relative;
-    margin: 40px 0;
-    text-align: center;
-
-    &::before {
-      content: "";
-      display: block;
-      width: 100%;
-      height: 0;
-      border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
-      position: absolute;
-      top: 50%;
-      left: 0;
-    }
-
-    span {
-      display: inline-block;
-      background: $ui-base-color;
-      font-size: 12px;
-      font-weight: 500;
-      color: $darker-text-color;
-      text-transform: uppercase;
-      position: relative;
-      z-index: 1;
-      padding: 0 8px;
-      cursor: default;
-    }
-  }
-
   p,
   li {
     font-family: $font-sans-serif, sans-serif;
@@ -458,28 +297,6 @@ $small-breakpoint: 960px;
     }
   }
 
-  .closed-registrations-message {
-    margin-top: 20px;
-
-    &,
-    p {
-      text-align: center;
-      font-size: 12px;
-      line-height: 18px;
-      color: $darker-text-color;
-      margin-bottom: 0;
-
-      a {
-        color: $highlight-text-color;
-        text-decoration: underline;
-      }
-    }
-
-    p:last-child {
-      margin-bottom: 0;
-    }
-  }
-
   em {
     display: inline;
     margin: 0;
@@ -593,187 +410,6 @@ $small-breakpoint: 960px;
     }
   }
 
-  .container-alt {
-    width: 100%;
-    box-sizing: border-box;
-    max-width: 800px;
-    margin: 0 auto;
-    word-wrap: break-word;
-  }
-
-  .header-wrapper {
-    padding-top: 15px;
-    background: $ui-base-color;
-    background: linear-gradient(150deg, lighten($ui-base-color, 8%), $ui-base-color);
-    position: relative;
-
-    &.compact {
-      background: $ui-base-color;
-      padding-bottom: 15px;
-
-      .hero .heading {
-        padding-bottom: 20px;
-        font-family: $font-sans-serif, sans-serif;
-        font-size: 16px;
-        font-weight: 400;
-        font-size: 16px;
-        line-height: 30px;
-        color: $darker-text-color;
-
-        a {
-          color: $highlight-text-color;
-          text-decoration: underline;
-        }
-      }
-    }
-  }
-
-  .brand {
-    a {
-      padding-left: 0;
-      padding-right: 0;
-      color: $white;
-    }
-
-    img {
-      height: 32px;
-      position: relative;
-      top: 4px;
-      left: -10px;
-    }
-  }
-
-  .header {
-    line-height: 30px;
-    overflow: hidden;
-
-    .container-alt {
-      display: flex;
-      justify-content: space-between;
-    }
-
-    .links {
-      position: relative;
-      z-index: 4;
-
-      a {
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        color: $darker-text-color;
-        text-decoration: none;
-        padding: 12px 16px;
-        line-height: 32px;
-        font-family: $font-display, sans-serif;
-        font-weight: 500;
-        font-size: 14px;
-
-        &:hover {
-          color: $secondary-text-color;
-        }
-      }
-
-      ul {
-        list-style: none;
-        margin: 0;
-
-        li {
-          display: inline-block;
-          vertical-align: bottom;
-          margin: 0;
-
-          &:first-child a {
-            padding-left: 0;
-          }
-
-          &:last-child a {
-            padding-right: 0;
-          }
-        }
-      }
-    }
-
-    .hero {
-      margin-top: 50px;
-      align-items: center;
-      position: relative;
-
-      .heading {
-        position: relative;
-        z-index: 4;
-        padding-bottom: 150px;
-      }
-
-      .simple_form,
-      .closed-registrations-message {
-        background: darken($ui-base-color, 4%);
-        width: 280px;
-        padding: 15px 20px;
-        border-radius: 4px 4px 0 0;
-        line-height: initial;
-        position: relative;
-        z-index: 4;
-
-        .actions {
-          margin-bottom: 0;
-
-          button,
-          .button,
-          .block-button {
-            margin-bottom: 0;
-          }
-        }
-      }
-
-      .closed-registrations-message {
-        min-height: 330px;
-        display: flex;
-        flex-direction: column;
-        justify-content: space-between;
-      }
-    }
-  }
-
-  .about-short {
-    background: darken($ui-base-color, 4%);
-    padding: 50px 0 30px;
-    font-family: $font-sans-serif, sans-serif;
-    font-size: 16px;
-    font-weight: 400;
-    font-size: 16px;
-    line-height: 30px;
-    color: $darker-text-color;
-
-    a {
-      color: $highlight-text-color;
-      text-decoration: underline;
-    }
-  }
-
-  &.alternative {
-    padding: 10px 0;
-
-    .brand {
-      text-align: center;
-      padding: 30px 0;
-      margin-bottom: 10px;
-
-      img {
-        position: static;
-        padding: 10px 0;
-      }
-
-      @media screen and (max-width: $small-breakpoint) {
-        padding: 15px 0;
-      }
-
-      @media screen and (max-width: $column-breakpoint) {
-        padding: 0;
-        margin-bottom: -10px;
-      }
-    }
-  }
-
   &__information,
   &__forms {
     padding: 20px;
@@ -967,353 +603,253 @@ $small-breakpoint: 960px;
     }
   }
 
-  &__forms {
-    height: 100%;
-
-    @media screen and (max-width: $small-breakpoint) {
-      height: auto;
-    }
-
-    @media screen and (max-width: $column-breakpoint) {
-      background: transparent;
-      box-shadow: none;
-      padding: 0 20px;
-      margin-top: 30px;
-      margin-bottom: 40px;
-
-      .separator-or {
-        span {
-          background: darken($ui-base-color, 8%);
-        }
+  @media screen and (max-width: 840px) {
+    .information-board {
+      .container-alt {
+        padding-right: 20px;
       }
-    }
-
-    hr {
-      margin: 40px 0;
-    }
 
-    .button {
-      display: block;
-    }
-
-    .subtle-hint a {
-      text-decoration: none;
+      .panel {
+        position: static;
+        margin-top: 20px;
+        width: 100%;
+        border-radius: 4px;
 
-      &:hover,
-      &:focus,
-      &:active {
-        text-decoration: underline;
+        .panel-header {
+          text-align: center;
+        }
       }
     }
   }
 
-  #mastodon-timeline {
-    display: flex;
-    -webkit-overflow-scrolling: touch;
-    -ms-overflow-style: -ms-autohiding-scrollbar;
-    font-family: $font-sans-serif, sans-serif;
-    font-size: 13px;
-    line-height: 18px;
-    font-weight: 400;
-    color: $primary-text-color;
-    width: 100%;
-    flex: 1 1 auto;
-    overflow: hidden;
-    height: 100%;
-
-    .column-header {
-      color: inherit;
-      font-family: inherit;
-      font-size: 16px;
-      line-height: inherit;
-      font-weight: inherit;
-      margin: 0;
-      padding: 0;
-    }
-
-    .column {
-      padding: 0;
-      border-radius: 4px;
-      overflow: hidden;
-      width: 100%;
-    }
-
-    .scrollable {
-      height: 400px;
-    }
-
-    p {
-      font-size: inherit;
-      line-height: inherit;
-      font-weight: inherit;
-      color: $primary-text-color;
-      margin-bottom: 20px;
-
-      &:last-child {
-        margin-bottom: 0;
-      }
+  @media screen and (max-width: 675px) {
+    .header-wrapper {
+      padding-top: 0;
 
-      a {
-        color: $secondary-text-color;
-        text-decoration: none;
+      &.compact {
+        padding-bottom: 0;
       }
-    }
-
-    .attachment-list__list {
-      margin-left: 0;
-      list-style: none;
-
-      li {
-        font-size: inherit;
-        line-height: inherit;
-        font-weight: inherit;
-        margin-bottom: 0;
-
-        a {
-          color: $dark-text-color;
-          text-decoration: none;
 
-          &:hover {
-            text-decoration: underline;
-          }
-        }
+      &.compact .hero .heading {
+        text-align: initial;
       }
     }
 
-    @media screen and (max-width: $column-breakpoint) {
-      display: none;
+    .header .container-alt,
+    .features .container-alt {
+      display: block;
     }
   }
 
-  &__features {
-    & > p {
-      padding-right: 60px;
-    }
-
-    .features-list {
-      margin: 40px 0;
-      margin-top: 30px;
-    }
-
-    &__action {
-      text-align: center;
-    }
+  .cta {
+    margin: 20px;
   }
+}
 
-  .features-list {
-    .features-list__row {
-      display: flex;
-      padding: 10px 0;
-      justify-content: space-between;
-
-      .visual {
-        flex: 0 0 auto;
-        display: flex;
-        align-items: center;
-        margin-left: 15px;
+.landing {
+  margin-bottom: 100px;
 
-        .fa {
-          display: block;
-          color: $darker-text-color;
-          font-size: 48px;
-        }
-      }
+  @media screen and (max-width: 738px) {
+    margin-bottom: 0;
+  }
 
-      .text {
-        font-size: 16px;
-        line-height: 30px;
-        color: $darker-text-color;
+  &__brand {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: 100px;
 
-        h6 {
-          font-size: inherit;
-          line-height: inherit;
-          margin-bottom: 0;
-        }
-      }
+    img {
+      height: 52px;
     }
 
-    @media screen and (min-width: $small-breakpoint) {
-      display: grid;
-      grid-gap: 30px;
-      grid-template-columns: 1fr 1fr;
-      grid-auto-columns: 50%;
-      grid-auto-rows: max-content;
+    @media screen and (max-width: $no-gap-breakpoint) {
+      padding: 0;
+      margin-bottom: 30px;
     }
   }
 
-  .footer-links {
-    padding-bottom: 50px;
-    text-align: right;
-    color: $dark-text-color;
+  .directory {
+    margin-top: 30px;
+    background: transparent;
+    box-shadow: none;
+    border-radius: 0;
+  }
 
-    p {
-      font-size: 14px;
-    }
+  .hero-widget {
+    margin-top: 30px;
+    margin-bottom: 0;
 
-    a {
-      color: inherit;
-      text-decoration: underline;
+    h4 {
+      padding: 10px;
+      text-transform: uppercase;
+      font-weight: 700;
+      font-size: 13px;
+      color: $darker-text-color;
     }
-  }
 
-  &__footer {
-    margin-top: 10px;
-    text-align: center;
-    color: $dark-text-color;
+    &__text {
+      border-radius: 0;
+      padding-bottom: 0;
+    }
 
-    p {
-      font-size: 14px;
+    &__footer {
+      background: $ui-base-color;
+      padding: 10px;
+      border-radius: 0 0 4px 4px;
+      display: flex;
 
-      a {
-        color: inherit;
-        text-decoration: underline;
+      &__column {
+        flex: 1 1 50%;
       }
     }
-  }
 
-  @media screen and (max-width: 840px) {
-    .container-alt {
-      padding: 0 20px;
-    }
+    .account {
+      padding: 10px 0;
+      border-bottom: 0;
 
-    .information-board {
-      .container-alt {
-        padding-right: 20px;
+      .account__display-name {
+        display: flex;
+        align-items: center;
       }
 
-      .panel {
-        position: static;
-        margin-top: 20px;
-        width: 100%;
-        border-radius: 4px;
-
-        .panel-header {
-          text-align: center;
-        }
+      .account__avatar {
+        width: 44px;
+        height: 44px;
+        background-size: 44px 44px;
       }
     }
-  }
 
-  @media screen and (max-width: 675px) {
-    .header-wrapper {
-      padding-top: 0;
+    &__counter {
+      padding: 10px;
 
-      &.compact {
-        padding-bottom: 0;
+      strong {
+        font-family: $font-display, sans-serif;
+        font-size: 15px;
+        font-weight: 700;
+        display: block;
       }
 
-      &.compact .hero .heading {
-        text-align: initial;
+      span {
+        font-size: 14px;
+        color: $darker-text-color;
       }
     }
+  }
 
-    .header .container-alt,
-    .features .container-alt {
-      display: block;
-    }
-
-    .header {
-      .links {
-        padding-top: 15px;
-        background: darken($ui-base-color, 4%);
+  .simple_form .user_agreement .label_input > label {
+    font-weight: 400;
+    color: $darker-text-color;
+  }
 
-        a {
-          padding: 12px 8px;
-        }
+  .simple_form p.lead {
+    color: $darker-text-color;
+    font-size: 15px;
+    line-height: 20px;
+    font-weight: 400;
+    margin-bottom: 25px;
+  }
 
-        .nav {
-          display: flex;
-          flex-flow: row wrap;
-          justify-content: space-around;
-        }
+  &__grid {
+    max-width: 960px;
+    margin: 0 auto;
+    display: grid;
+    grid-template-columns: minmax(0, 50%) minmax(0, 50%);
+    grid-gap: 30px;
 
-        .brand img {
-          left: 0;
-          top: 0;
-        }
-      }
+    @media screen and (max-width: 738px) {
+      grid-template-columns: minmax(0, 100%);
+      grid-gap: 10px;
 
-      .hero {
-        margin-top: 30px;
-        padding: 0;
+      &__column-login {
+        grid-row: 1;
+        display: flex;
+        flex-direction: column;
 
-        .heading {
-          padding: 30px 20px;
-          text-align: center;
+        .box-widget {
+          order: 2;
+          flex: 0 0 auto;
         }
 
-        .simple_form,
-        .closed-registrations-message {
-          background: darken($ui-base-color, 8%);
-          width: 100%;
-          border-radius: 0;
-          box-sizing: border-box;
+        .hero-widget {
+          margin-top: 0;
+          margin-bottom: 10px;
+          order: 1;
+          flex: 0 0 auto;
         }
       }
-    }
-  }
-
-  .cta {
-    margin: 20px;
-  }
 
-  &.tag-page {
-    @media screen and (max-width: $column-breakpoint) {
-      padding: 0;
-
-      .container {
-        padding: 0;
+      &__column-registration {
+        grid-row: 2;
       }
 
-      #mastodon-timeline {
-        display: flex;
-        height: 100vh;
-        border-radius: 0;
+      .directory {
+        margin-top: 10px;
       }
     }
 
-    .grid {
-      @media screen and (min-width: $small-breakpoint) {
-        grid-template-columns: 33% 67%;
-      }
-
-      .column-2 {
-        grid-column: 2;
-        grid-row: 1;
-      }
-    }
+    @media screen and (max-width: $no-gap-breakpoint) {
+      grid-gap: 0;
 
-    .brand {
-      text-align: unset;
-      padding: 0;
+      .hero-widget {
+        display: block;
+        margin-bottom: 0;
+        box-shadow: none;
 
-      img {
-        height: 48px;
-        width: auto;
+        &__img,
+        &__img img,
+        &__footer {
+          border-radius: 0;
+        }
       }
-    }
 
-    .cta {
-      margin: 0;
-
-      .button {
-        margin-right: 4px;
+      .hero-widget,
+      .box-widget,
+      .directory__tag {
+        border-bottom: 1px solid lighten($ui-base-color, 8%);
       }
-    }
 
-    @media screen and (max-width: $column-breakpoint) {
-      .grid {
-        grid-gap: 0;
+      .directory {
+        margin-top: 0;
 
-        .column-1 {
-          grid-column: 1;
-          grid-row: 1;
-        }
+        &__tag {
+          margin-bottom: 0;
 
-        .column-2 {
-          display: none;
+          & > a,
+          & > div {
+            border-radius: 0;
+            box-shadow: none;
+          }
+
+          &:last-child {
+            border-bottom: 0;
+          }
         }
       }
     }
   }
 }
+
+.brand {
+  position: relative;
+  text-decoration: none;
+}
+
+.brand__tagline {
+  display: block;
+  position: absolute;
+  bottom: -10px;
+  left: 50px;
+  width: 300px;
+  color: $ui-primary-color;
+  text-decoration: none;
+  font-size: 14px;
+
+  @media screen and (max-width: $no-gap-breakpoint) {
+    position: static;
+    width: auto;
+    margin-top: 20px;
+    color: $dark-text-color;
+  }
+}
+
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index bab982706..6051c1d00 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -68,6 +68,17 @@ code {
         top: 2px;
         left: 0;
       }
+
+      label a {
+        color: $highlight-text-color;
+        text-decoration: underline;
+
+        &:hover,
+        &:active,
+        &:focus {
+          text-decoration: none;
+        }
+      }
     }
   }
 
@@ -305,7 +316,7 @@ code {
       box-shadow: none;
     }
 
-    &:focus:invalid {
+    &:focus:invalid:not(:placeholder-shown) {
       border-color: lighten($error-red, 12%);
     }
 
@@ -346,6 +357,10 @@ code {
     }
   }
 
+  .input.disabled {
+    opacity: 0.5;
+  }
+
   .actions {
     margin-top: 30px;
     display: flex;
@@ -392,6 +407,10 @@ code {
       background-color: darken($ui-highlight-color, 5%);
     }
 
+    &:disabled:hover {
+      background-color: $ui-primary-color;
+    }
+
     &.negative {
       background: $error-value-color;
 
diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss
index 4f8c94d83..d8bc5473a 100644
--- a/app/javascript/styles/mastodon/polls.scss
+++ b/app/javascript/styles/mastodon/polls.scss
@@ -190,3 +190,15 @@
     color: darken($simple-background-color, 14%);
   }
 }
+
+.muted .poll {
+  color: $dark-text-color;
+
+  &__chart {
+    background: rgba(darken($ui-primary-color, 14%), 0.2);
+
+    &.leading {
+      background: rgba($ui-highlight-color, 0.2);
+    }
+  }
+}
diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index 1eaf30c5b..645192ea4 100644
--- a/app/javascript/styles/mastodon/widgets.scss
+++ b/app/javascript/styles/mastodon/widgets.scss
@@ -295,6 +295,11 @@
       cursor: default;
     }
 
+    &.disabled > div {
+      opacity: 0.5;
+      cursor: default;
+    }
+
     h4 {
       flex: 1 1 auto;
       font-size: 18px;
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index b49806ecd..8fe7b9138 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -241,9 +241,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 
   def poll_vote?
     return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name'])
-    return true if replied_to_status.poll.expired?
-    replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name']), uri: @object['id'])
-    ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.poll.hide_totals
+
+    unless replied_to_status.poll.expired?
+      replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name']), uri: @object['id'])
+      ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.poll.hide_totals?
+    end
+
     true
   end
 
@@ -371,15 +374,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
   end
 
-  def invalid_origin?(url)
-    return true if unsupported_uri_scheme?(url)
-
-    needle   = Addressable::URI.parse(url).host
-    haystack = Addressable::URI.parse(@account.uri).host
-
-    !haystack.casecmp(needle).zero?
-  end
-
   def reply_to_local?
     !replied_to_status.nil? && replied_to_status.account.local?
   end
diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb
index dc76dd3e2..4236af071 100644
--- a/app/lib/activitypub/activity/delete.rb
+++ b/app/lib/activitypub/activity/delete.rb
@@ -75,13 +75,4 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
   def lock_options
     { redis: Redis.current, key: "create:#{object_uri}" }
   end
-
-  def invalid_origin?(url)
-    return true if unsupported_uri_scheme?(url)
-
-    needle   = Addressable::URI.parse(url).host
-    haystack = Addressable::URI.parse(@account.uri).host
-
-    !haystack.casecmp(needle).zero?
-  end
 end
diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb
index 5f15f5274..bc9a63f98 100644
--- a/app/lib/activitypub/activity/update.rb
+++ b/app/lib/activitypub/activity/update.rb
@@ -4,8 +4,11 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
   SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
 
   def perform
-    update_account if equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
-    update_poll if equals_or_includes_any?(@object['type'], %w(Question))
+    if equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
+      update_account
+    elsif equals_or_includes_any?(@object['type'], %w(Question))
+      update_poll
+    end
   end
 
   private
@@ -18,11 +21,10 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
 
   def update_poll
     return reject_payload! if invalid_origin?(@object['id'])
+
     status = Status.find_by(uri: object_uri, account_id: @account.id)
-    return if status.nil? || status.poll_id.nil?
-    poll = Poll.find(status.poll_id)
-    return if poll.nil?
+    return if status.nil? || status.poll.nil?
 
-    ActivityPub::ProcessPollService.new.call(poll, @object)
+    ActivityPub::ProcessPollService.new.call(status.poll, @object)
   end
 end
diff --git a/app/models/account.rb b/app/models/account.rb
index 79eecc306..0bb20b2f2 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -474,6 +474,7 @@ class Account < ApplicationRecord
 
   before_create :generate_keys
   before_validation :prepare_contents, if: :local?
+  before_validation :prepare_username, on: :create
   before_destroy :clean_feed_manager
 
   private
@@ -483,6 +484,10 @@ class Account < ApplicationRecord
     note&.strip!
   end
 
+  def prepare_username
+    username&.squish!
+  end
+
   def generate_keys
     return unless local? && !Rails.env.test?
 
diff --git a/app/models/concerns/expireable.rb b/app/models/concerns/expireable.rb
index 2c0631476..f7d2bab49 100644
--- a/app/models/concerns/expireable.rb
+++ b/app/models/concerns/expireable.rb
@@ -18,7 +18,11 @@ module Expireable
     end
 
     def expired?
-      !expires_at.nil? && expires_at < Time.now.utc
+      expires? && expires_at < Time.now.utc
+    end
+
+    def expires?
+      !expires_at.nil?
     end
   end
 end
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 788a678bd..7db76d157 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -72,6 +72,14 @@ class Tag < ApplicationRecord
          .limit(limit)
          .offset(offset)
     end
+
+    def find_normalized(name)
+      find_by(name: name.mb_chars.downcase.to_s)
+    end
+
+    def find_normalized!(name)
+      find_normalized(name) || raise(ActiveRecord::RecordNotFound)
+    end
   end
 
   private
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
index d8670f124..58e3541c1 100644
--- a/app/presenters/instance_presenter.rb
+++ b/app/presenters/instance_presenter.rb
@@ -21,6 +21,10 @@ class InstancePresenter
     Rails.cache.fetch('user_count') { User.confirmed.joins(:account).merge(Account.without_suspended).count }
   end
 
+  def active_user_count
+    Rails.cache.fetch('active_user_count') { Redis.current.pfcount(*(0..3).map { |i| "activity:logins:#{i.weeks.ago.utc.to_date.cweek}" }) }
+  end
+
   def status_count
     Rails.cache.fetch('local_status_count') { Account.local.joins(:account_stat).sum('account_stats.statuses_count') }.to_i
   end
@@ -29,6 +33,10 @@ class InstancePresenter
     Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) }
   end
 
+  def sample_accounts
+    Rails.cache.fetch('sample_accounts', expires_in: 12.hours) { Account.discoverable.popular.limit(3) }
+  end
+
   def version_number
     Mastodon::Version
   end
diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb
index 569d0d7c1..8cb309e52 100644
--- a/app/services/activitypub/fetch_replies_service.rb
+++ b/app/services/activitypub/fetch_replies_service.rb
@@ -46,13 +46,4 @@ class ActivityPub::FetchRepliesService < BaseService
     # Also limit to 5 fetched replies to limit potential for DoS.
     @items.map { |item| value_or_id(item) }.reject { |uri| invalid_origin?(uri) }.take(5)
   end
-
-  def invalid_origin?(url)
-    return true if unsupported_uri_scheme?(url)
-
-    needle   = Addressable::URI.parse(url).host
-    haystack = Addressable::URI.parse(@account.uri).host
-
-    !haystack.casecmp(needle).zero?
-  end
 end
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index 7a86879f0..b5c721589 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -92,7 +92,7 @@ class NotifyService < BaseService
 
   def blocked?
     blocked   = @recipient.suspended?                            # Skip if the recipient account is suspended anyway
-    blocked ||= from_self? unless @notification.type == :poll    # Skip for interactions with self
+    blocked ||= from_self? && @notification.type != :poll        # Skip for interactions with self
 
     return blocked if message? && from_staff?
 
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 99c8e6cbb..7eec11ddf 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -10,7 +10,7 @@ class RemoveStatusService < BaseService
     @account      = status.account
     @tags         = status.tags.pluck(:name).to_a
     @mentions     = status.active_mentions.includes(:account).to_a
-    @reblogs      = status.reblogs.to_a
+    @reblogs      = status.reblogs.includes(:account).to_a
     @stream_entry = status.stream_entry
     @options      = options
 
@@ -78,8 +78,8 @@ class RemoveStatusService < BaseService
     end
 
     # ActivityPub
-    ActivityPub::DeliveryWorker.push_bulk(target_accounts.select(&:activitypub?).uniq(&:inbox_url)) do |target_account|
-      [signed_activity_json, @account.id, target_account.inbox_url]
+    ActivityPub::DeliveryWorker.push_bulk(target_accounts.select(&:activitypub?).uniq(&:preferred_inbox_url)) do |target_account|
+      [signed_activity_json, @account.id, target_account.preferred_inbox_url]
     end
   end
 
diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb
index 34a1fe2aa..0cace6c00 100644
--- a/app/services/vote_service.rb
+++ b/app/services/vote_service.rb
@@ -20,21 +20,35 @@ class VoteService < BaseService
     end
 
     if @poll.account.local?
-      ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, @poll.status.id) unless @poll.hide_totals
+      distribute_poll!
     else
-      @votes.each do |vote|
-        ActivityPub::DeliveryWorker.perform_async(
-          build_json(vote),
-          @account.id,
-          @poll.account.inbox_url
-        )
-      end
-      PollExpirationNotifyWorker.perform_at(@poll.expires_at + 5.minutes, @poll.id) unless @poll.expires_at.nil?
+      deliver_votes!
+      queue_final_poll_check!
     end
   end
 
   private
 
+  def distribute_poll!
+    return if @poll.hide_totals?
+    ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, @poll.status.id)
+  end
+
+  def queue_final_poll_check!
+    return unless @poll.expires?
+    PollExpirationNotifyWorker.perform_at(@poll.expires_at + 5.minutes, @poll.id)
+  end
+
+  def deliver_votes!
+    @votes.each do |vote|
+      ActivityPub::DeliveryWorker.perform_async(
+        build_json(vote),
+        @account.id,
+        @poll.account.inbox_url
+      )
+    end
+  end
+
   def build_json(vote)
     ActiveModelSerializers::SerializableResource.new(
       vote,
diff --git a/app/views/about/_features.html.haml b/app/views/about/_features.html.haml
deleted file mode 100644
index 8fbc6b760..000000000
--- a/app/views/about/_features.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-.features-list
-  .features-list__row
-    .text
-      %h6= t 'about.features.real_conversation_title'
-      = t 'about.features.real_conversation_body'
-    .visual
-      = fa_icon 'fw comments'
-  .features-list__row
-    .text
-      %h6= t 'about.features.not_a_product_title'
-      = t 'about.features.not_a_product_body'
-    .visual
-      = fa_icon 'fw users'
-  .features-list__row
-    .text
-      %h6= t 'about.features.within_reach_title'
-      = t 'about.features.within_reach_body'
-    .visual
-      = fa_icon 'fw mobile'
-  .features-list__row
-    .text
-      %h6= t 'about.features.humane_approach_title'
-      = t 'about.features.humane_approach_body'
-    .visual
-      = fa_icon 'fw leaf'
diff --git a/app/views/about/_forms.html.haml b/app/views/about/_forms.html.haml
deleted file mode 100644
index 78a422690..000000000
--- a/app/views/about/_forms.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-- if @instance_presenter.open_registrations
-  = render 'registration'
-- else
-  = link_to t('auth.register_elsewhere'), 'https://joinmastodon.org/#getting-started', class: 'button button-primary'
-
-  .closed-registrations-message
-    - if @instance_presenter.closed_registrations_message.blank?
-      %p= t('about.closed_registrations')
-    - else
-      = @instance_presenter.closed_registrations_message.html_safe
-
-.separator-or
-  %span= t('auth.or')
-
-= link_to t('auth.login'), new_user_session_path, class: 'button button-alternative-2 webapp-btn'
diff --git a/app/views/about/_links.html.haml b/app/views/about/_links.html.haml
deleted file mode 100644
index 381f301f9..000000000
--- a/app/views/about/_links.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-.container-alt.links
-  .brand
-    = link_to root_url do
-      = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
-
-  %ul.nav
-    %li
-      - if user_signed_in?
-        = link_to t('settings.back'), root_url, class: 'webapp-btn'
-      - else
-        = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-    %li= link_to t('about.about_this'), about_more_path
-    %li
-      = link_to 'https://joinmastodon.org/#getting-started' do
-        = "#{t('about.other_instances')}"
-        %i.fa.fa-external-link{ style: 'padding-left: 5px;' }
diff --git a/app/views/about/_login.html.haml b/app/views/about/_login.html.haml
new file mode 100644
index 000000000..d286f0d3c
--- /dev/null
+++ b/app/views/about/_login.html.haml
@@ -0,0 +1,13 @@
+= simple_form_for(new_user, url: user_session_path) do |f|
+  .fields-group
+    - if use_seamless_external_login?
+      = f.input :email, placeholder: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
+    - else
+      = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
+
+    = f.input :password, placeholder: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }, hint: false
+
+  .actions
+    = f.button :button, t('auth.login'), type: :submit, class: 'button button-primary'
+
+  %p.hint.subtle-hint= link_to t('auth.trouble_logging_in'), new_user_password_path
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
index ee4f8fe2e..715bcd37c 100644
--- a/app/views/about/_registration.html.haml
+++ b/app/views/about/_registration.html.haml
@@ -1,12 +1,16 @@
 = simple_form_for(new_user, url: user_registration_path) do |f|
-  = f.simple_fields_for :account do |account_fields|
-    = account_fields.input :username, wrapper: :with_label, autofocus: true, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username') }, append: "@#{site_hostname}", hint: false
+  %p.lead= t('about.federation_hint_html', instance: content_tag(:strong, site_hostname))
 
-  = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false
-  = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false
-  = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false
+  .fields-group
+    = f.simple_fields_for :account do |account_fields|
+      = account_fields.input :username, wrapper: :with_label, autofocus: true, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username') }, append: "@#{site_hostname}", hint: false, disabled: !Setting.open_registrations
 
-  .actions
-    = f.button :button, t('auth.register'), type: :submit, class: 'button button-primary'
+    = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: !Setting.open_registrations
+    = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false, disabled: !Setting.open_registrations
+    = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: !Setting.open_registrations
+
+  .fields-group
+    = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), disabled: !Setting.open_registrations
 
-  %p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)
+  .actions
+    = f.button :button, Setting.open_registrations ? t('auth.register') : t('auth.registration_closed', instance: site_hostname), type: :submit, class: 'button button-primary', disabled: !Setting.open_registrations
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 6c28f83ce..15d0af64e 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -3,143 +3,76 @@
 
 - content_for :header_tags do
   %link{ rel: 'canonical', href: about_url }/
-  %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
   = render partial: 'shared/og'
 
-.landing-page.alternative
-  .container
-    .grid
-      .column-0
-        .brand
-          = link_to root_url do
-            = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
-
-      - if Setting.timeline_preview
-        .column-1
-          .landing-page__forms
-            .brand
-              = link_to root_url do
-                = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
-
-            = render 'forms'
-
-      - else
-        .column-1.non-preview
-          .landing-page__forms
-            .brand
-              = link_to root_url do
-                = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
-
-            = render 'forms'
-
-      - if Setting.timeline_preview
-        .column-2
-          .landing-page__hero
-            = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
-
-          .landing-page__information
-            .landing-page__short-description
-              .row
-                .landing-page__logo
-                  = image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon'
-
-                %h1
-                  = @instance_presenter.site_title
-                  %small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname)
-
-              %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
-
-          .landing-page__call-to-action{ dir: 'ltr' }
-            .row
-              .row__information-board
-                .information-board__section
-                  %span= t 'about.user_count_before'
-                  %strong= number_with_delimiter @instance_presenter.user_count
-                  %span= t 'about.user_count_after', count: @instance_presenter.user_count
-                .information-board__section
-                  %span= t 'about.status_count_before'
-                  %strong= number_with_delimiter @instance_presenter.status_count
-                  %span= t 'about.status_count_after', count: @instance_presenter.status_count
-              .row__mascot
-                .landing-page__mascot
-                  = image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('elephant_ui_plane.svg'), alt: ''
-
-      - else
-        .column-2.non-preview
-          .landing-page__hero
-            = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
-
-          .landing-page__information
-            .landing-page__short-description
-              .row
-                .landing-page__logo
-                  = image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon'
-
-                %h1
-                  = @instance_presenter.site_title
-                  %small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname)
-
-              %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
-
-          .landing-page__call-to-action
-            .row
-              .row__information-board
-                .information-board__section
-                  %span= t 'about.user_count_before'
-                  %strong= number_with_delimiter @instance_presenter.user_count
-                  %span= t 'about.user_count_after', count: @instance_presenter.user_count
-                .information-board__section
-                  %span= t 'about.status_count_before'
-                  %strong= number_with_delimiter @instance_presenter.status_count
-                  %span= t 'about.status_count_after', count: @instance_presenter.status_count
-              .row__mascot
-                .landing-page__mascot
-                  = image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('elephant_ui_plane.svg'), alt: ''
-
-      - if Setting.timeline_preview
-        .column-3
-          #mastodon-timeline{ data: { props: Oj.dump(default_props) } }
-
-      - if Setting.timeline_preview
-        .column-4.landing-page__information
-          .landing-page__features
-            .features-list
-              %div
-                %h3= t 'about.what_is_mastodon'
-                %p= t 'about.about_mastodon_html'
-              %div.contact
-                %h3= t 'about.administered_by'
-                = account_link_to(@instance_presenter.contact_account, link_to(t('about.learn_more'), about_more_path, class: 'button button-alternative'))
-
-            = render 'features'
-
-            .landing-page__features__action
-              = link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-alternative'
-
-          .landing-page__footer
+.landing
+  .landing__brand
+    = link_to root_url, class: 'brand' do
+      = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+      %span.brand__tagline=t 'about.tagline'
+
+  .landing__grid
+    .landing__grid__column.landing__grid__column-registration
+      .box-widget
+        = render 'registration'
+
+      .directory
+        .directory__tag{ class: Setting.profile_directory ? nil : 'disabled' }
+          = optional_link_to Setting.profile_directory, explore_path do
+            %h4
+              = fa_icon 'address-book fw'
+              = t('about.discover_users')
+              %small= t('about.browse_directory')
+
+            .avatar-stack
+              - @instance_presenter.sample_accounts.each do |account|
+                = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar'
+
+        .directory__tag{ class: Setting.timeline_preview ? nil : 'disabled' }
+          = optional_link_to Setting.timeline_preview, public_timeline_path do
+            %h4
+              = fa_icon 'globe fw'
+              = t('about.see_whats_happening')
+              %small= t('about.browse_public_posts')
+
+        .directory__tag
+          = link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener' do
+            %h4
+              = fa_icon 'tablet fw'
+              = t('about.get_apps')
+              %small= t('about.apps_platforms')
+
+    .landing__grid__column.landing__grid__column-login
+      .box-widget
+        = render 'login'
+
+      .hero-widget
+        .hero-widget__img
+          = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
+
+        - if @instance_presenter.site_short_description.present?
+          .hero-widget__text
             %p
-              = link_to t('about.source_code'), @instance_presenter.source_url
-              = " (#{@instance_presenter.version_number})"
-
-      - else
-        .column-4.non-preview.landing-page__information
-          .landing-page__features
-            .features-list
-              %div
-                %h3= t 'about.what_is_mastodon'
-                %p= t 'about.about_mastodon_html'
-              %div.contact
-                %h3= t 'about.administered_by'
-                = account_link_to(@instance_presenter.contact_account, link_to(t('about.learn_more'), about_more_path, class: 'button button-alternative'))
-
-            = render 'features'
-
-            .landing-page__features__action
-              = link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-alternative'
-
-          .landing-page__footer
-            %p
-              = link_to t('about.source_code'), @instance_presenter.source_url
-              = " (#{@instance_presenter.version_number})"
-
-#modal-container
+              = @instance_presenter.site_short_description.html_safe.presence
+              = link_to about_more_path do
+                = t('about.learn_more')
+                = fa_icon 'angle-double-right'
+
+        .hero-widget__footer
+          .hero-widget__footer__column
+            %h4= t 'about.administered_by'
+
+            = account_link_to @instance_presenter.contact_account
+
+          .hero-widget__footer__column
+            %h4= t 'about.server_stats'
+
+            %div{ style: 'display: flex' }
+              .hero-widget__counter{ style: 'width: 50%' }
+                %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%' }
+                %strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true
+                %span
+                  = t 'about.active_count_after'
+                  %abbr{ title: t('about.active_footnote') } *
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index c1c0f4b87..f26271cdc 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -1,22 +1,23 @@
 - content_for :content do
   .public-layout
-    .container
-      %nav.header
-        .nav-left
-          = link_to root_url, class: 'brand' do
-            = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+    - unless @hide_navbar
+      .container
+        %nav.header
+          .nav-left
+            = link_to root_url, class: 'brand' do
+              = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
 
-          - if Setting.profile_directory
-            = link_to t('directories.directory'), explore_path, class: 'nav-link optional'
-          = link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
-          = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
-        .nav-center
-        .nav-right
-          - if user_signed_in?
-            = link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
-          - else
-            = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn nav-link nav-button'
-            = link_to t('auth.register'), open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started', class: 'webapp-btn nav-link nav-button'
+            - if Setting.profile_directory
+              = link_to t('directories.directory'), explore_path, class: 'nav-link optional'
+            = link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
+            = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
+          .nav-center
+          .nav-right
+            - if user_signed_in?
+              = link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
+            - else
+              = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn nav-link nav-button'
+              = link_to t('auth.register'), open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started', class: 'webapp-btn nav-link nav-button'
 
     .container= yield
 
diff --git a/app/views/public_timelines/show.html.haml b/app/views/public_timelines/show.html.haml
new file mode 100644
index 000000000..913d5d855
--- /dev/null
+++ b/app/views/public_timelines/show.html.haml
@@ -0,0 +1,14 @@
+- content_for :page_title do
+  = t('about.see_whats_happening')
+
+- content_for :header_tags do
+  %meta{ name: 'robots', content: 'noindex' }/
+  %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
+  = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
+
+.page-header
+  %h1= t('about.see_whats_happening')
+  %p= t('about.browse_public_posts')
+
+#mastodon-timeline{ data: { props: Oj.dump(default_props) }}
+#modal-container
diff --git a/app/views/remote_follow/new.html.haml b/app/views/remote_follow/new.html.haml
index 5cf6977ba..c90793842 100644
--- a/app/views/remote_follow/new.html.haml
+++ b/app/views/remote_follow/new.html.haml
@@ -1,6 +1,5 @@
 - content_for :header_tags do
-  - if @account.user&.setting_noindex
-    %meta{ name: 'robots', content: 'noindex' }/
+  %meta{ name: 'robots', content: 'noindex' }/
 
 .form-container
   .follow-prompt
diff --git a/app/views/remote_interaction/new.html.haml b/app/views/remote_interaction/new.html.haml
index a0b106814..b2b7826c4 100644
--- a/app/views/remote_interaction/new.html.haml
+++ b/app/views/remote_interaction/new.html.haml
@@ -1,3 +1,6 @@
+- content_for :header_tags do
+  %meta{ name: 'robots', content: 'noindex' }/
+
 .form-container
   .follow-prompt
     %h2= t("remote_interaction.#{@interaction_type}.prompt")
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 850160ac1..1a9c58983 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -2,6 +2,7 @@
   = "##{@tag.name}"
 
 - content_for :header_tags do
+  %meta{ name: 'robots', content: 'noindex' }/
   %link{ rel: 'alternate', type: 'application/rss+xml', href: tag_url(@tag, format: 'rss') }/
 
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
diff --git a/app/workers/activitypub/distribute_poll_update_worker.rb b/app/workers/activitypub/distribute_poll_update_worker.rb
index 02da583cd..5536bd744 100644
--- a/app/workers/activitypub/distribute_poll_update_worker.rb
+++ b/app/workers/activitypub/distribute_poll_update_worker.rb
@@ -28,13 +28,16 @@ class ActivityPub::DistributePollUpdateWorker
 
   def inboxes
     return @inboxes if defined?(@inboxes)
-    target_accounts = @status.mentions.map(&:account).reject(&:local?)
-    target_accounts += @status.reblogs.map(&:account).reject(&:local?)
-    target_accounts += @status.poll.votes.map(&:account).reject(&:local?)
-    target_accounts.uniq!(&:id)
-    @inboxes = target_accounts.select(&:activitypub?).pluck(&:inbox_url)
-    @inboxes += @account.followers.inboxes unless @status.direct_visibility?
+
+    @inboxes = [@status.mentions, @status.reblogs, @status.poll.votes].flat_map do |relation|
+      relation.includes(:account).map do |record|
+        record.account.preferred_inbox_url if !record.account.local? && record.account.activitypub?
+      end
+    end
+
+    @inboxes.concat(@account.followers.inboxes) unless @status.direct_visibility?
     @inboxes.uniq!
+    @inboxes.compact!
     @inboxes
   end