~cytrogen/masto-fe

11c28abcfebcf8827f99a86540076347f1a1edff — ThibG 6 years ago 8360019 + aec3fa3
Merge pull request #1097 from ThibG/glitch-soc/merge-upstream

Merge upstream changes
68 files changed, 278 insertions(+), 542 deletions(-)

M .rubocop.yml
M .sass-lint.yml
M Gemfile
M Gemfile.lock
M app/controllers/accounts_controller.rb
M app/controllers/activitypub/collections_controller.rb
M app/controllers/activitypub/outboxes_controller.rb
M app/controllers/admin/accounts_controller.rb
M app/controllers/application_controller.rb
M app/controllers/concerns/account_controller_concern.rb
M app/controllers/custom_css_controller.rb
M app/controllers/emojis_controller.rb
M app/controllers/follower_accounts_controller.rb
M app/controllers/following_accounts_controller.rb
M app/controllers/statuses_controller.rb
M app/controllers/stream_entries_controller.rb
M app/javascript/flavours/glitch/components/autosuggest_textarea.js
M app/javascript/flavours/glitch/features/compose/components/compose_form.js
M app/javascript/flavours/glitch/features/list_timeline/index.js
M app/javascript/flavours/glitch/reducers/compose.js
M app/javascript/flavours/glitch/reducers/timelines.js
M app/javascript/mastodon/components/autosuggest_textarea.js
M app/javascript/mastodon/features/compose/components/compose_form.js
M app/javascript/mastodon/features/compose/components/search.js
M app/javascript/mastodon/features/list_timeline/index.js
M app/javascript/mastodon/reducers/compose.js
M app/javascript/mastodon/reducers/timelines.js
M app/javascript/styles/contrast/diff.scss
M app/javascript/styles/contrast/variables.scss
M app/javascript/styles/mailer.scss
M app/javascript/styles/mastodon/_mixins.scss
M app/javascript/styles/mastodon/admin.scss
M app/javascript/styles/mastodon/basics.scss
M app/javascript/styles/mastodon/components.scss
M app/javascript/styles/mastodon/containers.scss
M app/javascript/styles/mastodon/emoji_picker.scss
M app/javascript/styles/mastodon/forms.scss
M app/javascript/styles/mastodon/polls.scss
M app/javascript/styles/mastodon/rtl.scss
M app/lib/activitypub/activity.rb
M app/lib/activitypub/activity/follow.rb
M app/models/account.rb
M app/models/form/account_batch.rb
M app/models/status.rb
M app/services/after_block_domain_from_account_service.rb
M app/services/authorize_follow_service.rb
M app/services/block_service.rb
A app/services/concerns/payloadable.rb
M app/services/favourite_service.rb
M app/services/follow_service.rb
M app/services/process_mentions_service.rb
M app/services/reblog_service.rb
M app/services/reject_follow_service.rb
M app/services/remove_status_service.rb
M app/services/report_service.rb
M app/services/suspend_account_service.rb
M app/services/unblock_service.rb
M app/services/unfavourite_service.rb
M app/services/unfollow_service.rb
M app/services/vote_service.rb
M app/views/admin/pending_accounts/_account.html.haml
M app/workers/activitypub/distribute_poll_update_worker.rb
M app/workers/activitypub/distribution_worker.rb
M app/workers/activitypub/reply_distribution_worker.rb
M app/workers/activitypub/update_distribution_worker.rb
M config/locales/en.yml
M package.json
M spec/lib/activitypub/activity/announce_spec.rb
M .rubocop.yml => .rubocop.yml +6 -0
@@ 1,3 1,6 @@
require:
  - rubocop-rails

AllCops:
  TargetRubyVersion: 2.3
  Exclude:


@@ 82,6 85,9 @@ Rails/Exit:
    - 'lib/mastodon/*'
    - 'lib/cli.rb'

Rails/HelperInstanceVariable:
  Enabled: false

Style/ClassAndModuleChildren:
  Enabled: false


M .sass-lint.yml => .sass-lint.yml +31 -258
@@ 4,261 4,34 @@
files:
  include: app/javascript/styles/**/*.scss
  ignore:
    - app/javascript/styles/reset.scss

linters:
  # Reports when you use improper spacing around ! (the "bang") in !default,
  # !global, !important, and !optional flags.
  BangFormat:
    enabled: false

  # Whether or not to prefer `border: 0` over `border: none`.
  BorderZero:
    enabled: false

  # Reports when you define a rule set using a selector with chained classes
  # (a.k.a. adjoining classes).
  ChainedClasses:
    enabled: false

  # Prefer hexadecimal color codes over color keywords.
  # (e.g. `color: green` is a color keyword)
  ColorKeyword:
    enabled: false

  # Prefer color literals (keywords or hexadecimal codes) to be used only in
  # variable declarations. They should be referred to via variables everywhere
  # else.
  ColorVariable:
    enabled: true

  # Which form of comments to prefer in CSS.
  Comment:
    enabled: false

  # Reports @debug statements (which you probably left behind accidentally).
  DebugStatement:
    enabled: false

  # Rule sets should be ordered as follows:
  # - @extend declarations
  # - @include declarations without inner @content
  # - properties, @include declarations with inner @content
  # - nested rule sets.
  DeclarationOrder:
    enabled: false

  # `scss-lint:disable` control comments should be preceded by a comment
  # explaining why these linters are being disabled for this file.
  # See https://github.com/brigade/scss-lint#disabling-linters-via-source for
  # more information.
  DisableLinterReason:
    enabled: true

  # Reports when you define the same property twice in a single rule set.
  DuplicateProperty:
    enabled: false

  # Separate rule, function, and mixin declarations with empty lines.
  EmptyLineBetweenBlocks:
    enabled: true

  # Reports when you have an empty rule set.
  EmptyRule:
    enabled: true

  # Reports when you have an @extend directive.
  ExtendDirective:
    enabled: false

  # Files should always have a final newline. This results in better diffs
  # when adding lines to the file, since SCM systems such as git won't
  # think that you touched the last line.
  FinalNewline:
    enabled: false

  # HEX colors should use three-character values where possible.
  HexLength:
    enabled: false

  # HEX color values should use lower-case colors to differentiate between
  # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
  HexNotation:
    enabled: true

  # Avoid using ID selectors.
  IdSelector:
    enabled: false

  # The basenames of @imported SCSS partials should not begin with an
  # underscore and should not include the filename extension.
  ImportPath:
    enabled: false

  # Avoid using !important in properties. It is usually indicative of a
  # misunderstanding of CSS specificity and can lead to brittle code.
  ImportantRule:
    enabled: false

  # Indentation should always be done in increments of 2 spaces.
  Indentation:
    enabled: true
    width: 2

  # Don't write leading zeros for numeric values with a decimal point.
  LeadingZero:
    enabled: false

  # Reports when you define the same selector twice in a single sheet.
  MergeableSelector:
    enabled: false

  # Functions, mixins, variables, and placeholders should be declared
  # with all lowercase letters and hyphens instead of underscores.
  NameFormat:
    enabled: false

  # Avoid nesting selectors too deeply.
  NestingDepth:
    enabled: false

  # Always use placeholder selectors in @extend.
  PlaceholderInExtend:
    enabled: false

  # Sort properties in a strict order.
  PropertySortOrder:
    enabled: false

  # Reports when you use an unknown or disabled CSS property
  # (ignoring vendor-prefixed properties).
  PropertySpelling:
    enabled: false

  # Configure which units are allowed for property values.
  PropertyUnits:
    enabled: false

  # Pseudo-elements, like ::before, and ::first-letter, should be declared
  # with two colons. Pseudo-classes, like :hover and :first-child, should
  # be declared with one colon.
  PseudoElement:
    enabled: true

  # Avoid qualifying elements in selectors (also known as "tag-qualifying").
  QualifyingElement:
    enabled: false

  # Don't write selectors with a depth of applicability greater than 3.
  SelectorDepth:
    enabled: false

  # Selectors should always use hyphenated-lowercase, rather than camelCase or
  # snake_case.
  SelectorFormat:
    enabled: false
    convention: hyphenated_lowercase

  # Prefer the shortest shorthand form possible for properties that support it.
  Shorthand:
    enabled: true

  # Each property should have its own line, except in the special case of
  # single line rulesets.
  SingleLinePerProperty:
    enabled: true
    allow_single_line_rule_sets: true

  # Split selectors onto separate lines after each comma, and have each
  # individual selector occupy a single line.
  SingleLinePerSelector:
    enabled: true

  # Commas in lists should be followed by a space.
  SpaceAfterComma:
    enabled: false

  # Properties should be formatted with a single space separating the colon
  # from the property's value.
  SpaceAfterPropertyColon:
    enabled: true

  # Properties should be formatted with no space between the name and the
  # colon.
  SpaceAfterPropertyName:
    enabled: true

  # Variables should be formatted with a single space separating the colon
  # from the variable's value.
  SpaceAfterVariableColon:
    enabled: true

  # Variables should be formatted with no space between the name and the
  # colon.
  SpaceAfterVariableName:
    enabled: false

  # Operators should be formatted with a single space on both sides of an
  # infix operator.
  SpaceAroundOperator:
    enabled: true

  # Opening braces should be preceded by a single space.
  SpaceBeforeBrace:
    enabled: true

  # Parentheses should not be padded with spaces.
  SpaceBetweenParens:
    enabled: false

  # Enforces that string literals should be written with a consistent form
  # of quotes (single or double).
  StringQuotes:
    enabled: false

  # Property values, @extend, @include, and @import directives, and variable
  # declarations should always end with a semicolon.
  TrailingSemicolon:
    enabled: true

  # Reports lines containing trailing whitespace.
  TrailingWhitespace:
    enabled: true

  # Don't write trailing zeros for numeric values with a decimal point.
  TrailingZero:
    enabled: false

  # Don't use the `all` keyword to specify transition properties.
  TransitionAll:
    enabled: false

  # Numeric values should not contain unnecessary fractional portions.
  UnnecessaryMantissa:
    enabled: false

  # Do not use parent selector references (&) when they would otherwise
  # be unnecessary.
  UnnecessaryParentReference:
    enabled: false

  # URLs should be valid and not contain protocols or domain names.
  UrlFormat:
    enabled: true

  # URLs should always be enclosed within quotes.
  UrlQuotes:
    enabled: true

  # Properties, like color and font, are easier to read and maintain
  # when defined using variables rather than literals.
  VariableForProperty:
    enabled: false

  # Avoid vendor prefixes. Or rather: don't write them yourself.
  VendorPrefix:
    enabled: false

  # Omit length units on zero values, e.g. `0px` vs. `0`.
  ZeroUnit:
    enabled: true
    - app/javascript/styles/mastodon/reset.scss

rules:
  # Disallows
  no-color-literals: 0
  no-css-comments: 0
  no-duplicate-properties: 0
  no-ids: 0
  no-important: 0
  no-mergeable-selectors: 0
  no-misspelled-properties: 0
  no-qualifying-elements: 0
  no-transition-all: 0
  no-vendor-prefixes: 0

  # Nesting
  force-element-nesting: 0
  force-attribute-nesting: 0
  force-pseudo-nesting: 0

  # Name Formats
  class-name-format: 0
  leading-zero: 0

  # Style Guide
  attribute-quotes: 0
  hex-length: 0
  indentation: 0
  nesting-depth: 0
  property-sort-order: 0
  quotes: 0

M Gemfile => Gemfile +1 -0
@@ 132,6 132,7 @@ group :development do
  gem 'letter_opener_web', '~> 1.3'
  gem 'memory_profiler'
  gem 'rubocop', '~> 0.71', require: false
  gem 'rubocop-rails', '~> 2.0', require: false
  gem 'brakeman', '~> 4.5', require: false
  gem 'bundler-audit', '~> 0.6', require: false


M Gemfile.lock => Gemfile.lock +4 -0
@@ 534,6 534,9 @@ GEM
      rainbow (>= 2.2.2, < 4.0)
      ruby-progressbar (~> 1.7)
      unicode-display_width (>= 1.4.0, < 1.7)
    rubocop-rails (2.0.0)
      rack (>= 2.0)
      rubocop (>= 0.70.0)
    ruby-progressbar (1.10.1)
    ruby-saml (1.9.0)
      nokogiri (>= 1.5.10)


@@ 740,6 743,7 @@ DEPENDENCIES
  rspec-rails (~> 3.8)
  rspec-sidekiq (~> 3.0)
  rubocop (~> 0.71)
  rubocop-rails (~> 2.0)
  sanitize (~> 5.0)
  sidekiq (~> 5.2)
  sidekiq-bulk (~> 0.2.0)

M app/controllers/accounts_controller.rb => app/controllers/accounts_controller.rb +0 -2
@@ 47,8 47,6 @@ class AccountsController < ApplicationController
      end

      format.json do
        mark_cacheable!

        render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
          ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
        end

M app/controllers/activitypub/collections_controller.rb => app/controllers/activitypub/collections_controller.rb +0 -2
@@ 9,8 9,6 @@ class ActivityPub::CollectionsController < Api::BaseController
  before_action :set_cache_headers

  def show
    skip_session!

    render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
      ActiveModelSerializers::SerializableResource.new(
        collection_presenter,

M app/controllers/activitypub/outboxes_controller.rb => app/controllers/activitypub/outboxes_controller.rb +1 -4
@@ 10,10 10,7 @@ class ActivityPub::OutboxesController < Api::BaseController
  before_action :set_cache_headers

  def show
    unless page_requested?
      skip_session!
      expires_in 1.minute, public: true
    end
    expires_in 1.minute, public: true unless page_requested?

    render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
  end

M app/controllers/admin/accounts_controller.rb => app/controllers/admin/accounts_controller.rb +2 -2
@@ 48,13 48,13 @@ module Admin
    def approve
      authorize @account.user, :approve?
      @account.user.approve!
      redirect_to admin_accounts_path(pending: '1')
      redirect_to admin_pending_accounts_path
    end

    def reject
      authorize @account.user, :reject?
      SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
      redirect_to admin_accounts_path(pending: '1')
      redirect_to admin_pending_accounts_path
    end

    def unsilence

M app/controllers/application_controller.rb => app/controllers/application_controller.rb +0 -5
@@ 228,11 228,6 @@ class ApplicationController < ActionController::Base
  end

  def mark_cacheable!
    skip_session!
    expires_in 0, public: true
  end

  def skip_session!
    request.session_options[:skip] = true
  end
end

M app/controllers/concerns/account_controller_concern.rb => app/controllers/concerns/account_controller_concern.rb +0 -1
@@ 70,7 70,6 @@ module AccountControllerConcern

  def check_account_suspension
    if @account.suspended?
      skip_session!
      expires_in(3.minutes, public: true)
      gone
    end

M app/controllers/custom_css_controller.rb => app/controllers/custom_css_controller.rb +0 -1
@@ 4,7 4,6 @@ class CustomCssController < ApplicationController
  before_action :set_cache_headers

  def show
    skip_session!
    render plain: Setting.custom_css || '', content_type: 'text/css'
  end
end

M app/controllers/emojis_controller.rb => app/controllers/emojis_controller.rb +0 -2
@@ 7,8 7,6 @@ class EmojisController < ApplicationController
  def show
    respond_to do |format|
      format.json do
        skip_session!

        render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do
          ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
        end

M app/controllers/follower_accounts_controller.rb => app/controllers/follower_accounts_controller.rb +1 -4
@@ 20,10 20,7 @@ class FollowerAccountsController < ApplicationController
      format.json do
        raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?

        if params[:page].blank?
          skip_session!
          expires_in 3.minutes, public: true
        end
        expires_in 3.minutes, public: true if params[:page].blank?

        render json: collection_presenter,
               serializer: ActivityPub::CollectionSerializer,

M app/controllers/following_accounts_controller.rb => app/controllers/following_accounts_controller.rb +1 -4
@@ 20,10 20,7 @@ class FollowingAccountsController < ApplicationController
      format.json do
        raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?

        if params[:page].blank?
          skip_session!
          expires_in 3.minutes, public: true
        end
        expires_in 3.minutes, public: true if params[:page].blank?

        render json: collection_presenter,
               serializer: ActivityPub::CollectionSerializer,

M app/controllers/statuses_controller.rb => app/controllers/statuses_controller.rb +1 -11
@@ 29,10 29,7 @@ class StatusesController < ApplicationController
      format.html do
        use_pack 'public'

        unless user_signed_in?
          skip_session!
          expires_in 10.seconds, public: true
        end
        expires_in 10.seconds, public: true if current_account.nil?

        @body_classes = 'with-modals'



@@ 43,8 40,6 @@ class StatusesController < ApplicationController
      end

      format.json do
        mark_cacheable! unless @stream_entry.hidden?

        render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
          ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
        end


@@ 53,8 48,6 @@ class StatusesController < ApplicationController
  end

  def activity
    skip_session!

    render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
      ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
    end


@@ 64,7 57,6 @@ class StatusesController < ApplicationController
    use_pack 'embed'
    raise ActiveRecord::RecordNotFound if @status.hidden?

    skip_session!
    expires_in 180, public: true
    response.headers['X-Frame-Options'] = 'ALLOWALL'
    @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])


@@ 73,8 65,6 @@ class StatusesController < ApplicationController
  end

  def replies
    skip_session!

    render json: replies_collection_presenter,
           serializer: ActivityPub::CollectionSerializer,
           adapter: ActivityPub::Adapter,

M app/controllers/stream_entries_controller.rb => app/controllers/stream_entries_controller.rb +4 -10
@@ 17,19 17,13 @@ class StreamEntriesController < ApplicationController
      format.html do
        use_pack 'public'

        unless user_signed_in?
          skip_session!
          expires_in 5.minutes, public: true
        end
        expires_in 5.minutes, public: true unless @stream_entry.hidden?

        redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
        redirect_to short_account_status_url(params[:account_username], @stream_entry.activity)
      end

      format.atom do
        unless @stream_entry.hidden?
          skip_session!
          expires_in 3.minutes, public: true
        end
        expires_in 3.minutes, public: true unless @stream_entry.hidden?

        render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
      end


@@ 57,7 51,7 @@ class StreamEntriesController < ApplicationController

  def set_stream_entry
    @stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id])
    @type         = @stream_entry.activity_type.downcase
    @type         = 'status'

    raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
    authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only?

M app/javascript/flavours/glitch/components/autosuggest_textarea.js => app/javascript/flavours/glitch/components/autosuggest_textarea.js +4 -1
@@ 138,8 138,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
    this.setState({ suggestionsHidden: true, focused: false });
  }

  onFocus = () => {
  onFocus = (e) => {
    this.setState({ focused: true });
    if (this.props.onFocus) {
      this.props.onFocus(e);
    }
  }

  onSuggestionClick = (e) => {

M app/javascript/flavours/glitch/features/compose/components/compose_form.js => app/javascript/flavours/glitch/features/compose/components/compose_form.js +10 -1
@@ 28,6 28,10 @@ const messages = defineMessages({
export default @injectIntl
class ComposeForm extends ImmutablePureComponent {

  setRef = c => {
    this.composeForm = c;
  };

  static contextTypes = {
    router: PropTypes.object,
  };


@@ 208,6 212,10 @@ class ComposeForm extends ImmutablePureComponent {
    }
  }

  handleFocus = () => {
    this.composeForm.scrollIntoView();
  }

  //  This statement does several things:
  //  - If we're beginning a reply, and,
  //      - Replying to zero or one users, places the cursor at the end


@@ 302,7 310,7 @@ class ComposeForm extends ImmutablePureComponent {
    let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);

    return (
      <div className='composer'>
      <div className='composer' ref={this.setRef}>
        <WarningContainer />

        <ReplyIndicatorContainer />


@@ 337,6 345,7 @@ class ComposeForm extends ImmutablePureComponent {
            value={this.props.text}
            onChange={this.handleChange}
            suggestions={this.props.suggestions}
            onFocus={this.handleFocus}
            onKeyDown={this.handleKeyDown}
            onSuggestionsFetchRequested={onFetchSuggestions}
            onSuggestionsClearRequested={onClearSuggestions}

M app/javascript/flavours/glitch/features/list_timeline/index.js => app/javascript/flavours/glitch/features/list_timeline/index.js +17 -0
@@ 75,6 75,23 @@ export default class ListTimeline extends React.PureComponent {
    this.disconnect = dispatch(connectListStream(id));
  }

  componentWillReceiveProps (nextProps) {
    const { dispatch } = this.props;
    const { id } = nextProps.params;

    if (id !== this.props.params.id) {
      if (this.disconnect) {
        this.disconnect();
        this.disconnect = null;
      }

      dispatch(fetchList(id));
      dispatch(expandListTimeline(id));

      this.disconnect = dispatch(connectListStream(id));
    }
  }

  componentWillUnmount () {
    if (this.disconnect) {
      this.disconnect();

M app/javascript/flavours/glitch/reducers/compose.js => app/javascript/flavours/glitch/reducers/compose.js +1 -0
@@ 442,6 442,7 @@ export default function compose(state = initialState, action) {
      map.set('focusDate', new Date());
      map.set('caretPosition', null);
      map.set('idempotencyKey', uuid());
      map.set('sensitive', action.status.get('sensitive'));

      if (action.status.get('spoiler_text').length > 0) {
        map.set('spoiler', true);

M app/javascript/flavours/glitch/reducers/timelines.js => app/javascript/flavours/glitch/reducers/timelines.js +3 -1
@@ 35,7 35,9 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is

    if (!next && !isLoadingRecent) mMap.set('hasMore', false);

    if (!statuses.isEmpty()) {
    if (timeline.endsWith(':pinned')) {
      mMap.set('items', statuses.map(status => status.get('id')));
    } else if (!statuses.isEmpty()) {
      mMap.update('items', ImmutableList(), oldIds => {
        const newIds = statuses.map(status => status.get('id'));
        const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;

M app/javascript/mastodon/components/autosuggest_textarea.js => app/javascript/mastodon/components/autosuggest_textarea.js +4 -1
@@ 138,8 138,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
    this.setState({ suggestionsHidden: true, focused: false });
  }

  onFocus = () => {
  onFocus = (e) => {
    this.setState({ focused: true });
    if (this.props.onFocus) {
      this.props.onFocus(e);
    }
  }

  onSuggestionClick = (e) => {

M app/javascript/mastodon/features/compose/components/compose_form.js => app/javascript/mastodon/features/compose/components/compose_form.js +11 -2
@@ 34,6 34,10 @@ const messages = defineMessages({
export default @injectIntl
class ComposeForm extends ImmutablePureComponent {

  setRef = c => {
    this.composeForm = c;
  };

  static contextTypes = {
    router: PropTypes.object,
  };


@@ 115,6 119,10 @@ class ComposeForm extends ImmutablePureComponent {
    this.props.onChangeSpoilerText(e.target.value);
  }

  handleFocus = () => {
    this.composeForm.scrollIntoView();
  }

  componentDidUpdate (prevProps) {
    // This statement does several things:
    // - If we're beginning a reply, and,


@@ 178,7 186,7 @@ class ComposeForm extends ImmutablePureComponent {
    }

    return (
      <div className='compose-form'>
      <div className='compose-form' ref={this.setRef}>
        <WarningContainer />

        <ReplyIndicatorContainer />


@@ 201,7 209,7 @@ class ComposeForm extends ImmutablePureComponent {
          />
        </div>

        <div className='emoji-picker-wrapper'>
        <div className={`emoji-picker-wrapper ${this.props.showSearch ? 'emoji-picker-wrapper--hidden' : ''}`}>
          <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
        </div>



@@ 212,6 220,7 @@ class ComposeForm extends ImmutablePureComponent {
          value={this.props.text}
          onChange={this.handleChange}
          suggestions={this.props.suggestions}
          onFocus={this.handleFocus}
          onKeyDown={this.handleKeyDown}
          onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
          onSuggestionsClearRequested={this.onSuggestionsClearRequested}

M app/javascript/mastodon/features/compose/components/search.js => app/javascript/mastodon/features/compose/components/search.js +1 -1
@@ 21,7 21,7 @@ class SearchPopout extends React.PureComponent {
    const { style } = this.props;
    const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
    return (
      <div style={{ ...style, position: 'absolute', width: 285 }}>
      <div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}>
        <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
          {({ opacity, scaleX, scaleY }) => (
            <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>

M app/javascript/mastodon/features/list_timeline/index.js => app/javascript/mastodon/features/list_timeline/index.js +17 -0
@@ 75,6 75,23 @@ class ListTimeline extends React.PureComponent {
    this.disconnect = dispatch(connectListStream(id));
  }

  componentWillReceiveProps (nextProps) {
    const { dispatch } = this.props;
    const { id } = nextProps.params;

    if (id !== this.props.params.id) {
      if (this.disconnect) {
        this.disconnect();
        this.disconnect = null;
      }

      dispatch(fetchList(id));
      dispatch(expandListTimeline(id));

      this.disconnect = dispatch(connectListStream(id));
    }
  }

  componentWillUnmount () {
    if (this.disconnect) {
      this.disconnect();

M app/javascript/mastodon/reducers/compose.js => app/javascript/mastodon/reducers/compose.js +1 -0
@@ 338,6 338,7 @@ export default function compose(state = initialState, action) {
      map.set('focusDate', new Date());
      map.set('caretPosition', null);
      map.set('idempotencyKey', uuid());
      map.set('sensitive', action.status.get('sensitive'));

      if (action.status.get('spoiler_text').length > 0) {
        map.set('spoiler', true);

M app/javascript/mastodon/reducers/timelines.js => app/javascript/mastodon/reducers/timelines.js +3 -5
@@ 35,14 35,12 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is

    if (!next && !isLoadingRecent) mMap.set('hasMore', false);

    if (!statuses.isEmpty()) {
    if (timeline.endsWith(':pinned')) {
      mMap.set('items', statuses.map(status => status.get('id')));
    } else if (!statuses.isEmpty()) {
      mMap.update('items', ImmutableList(), oldIds => {
        const newIds = statuses.map(status => status.get('id'));

        if (timeline.indexOf(':pinned') !== -1) {
          return newIds;
        }

        const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
        const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);


M app/javascript/styles/contrast/diff.scss => app/javascript/styles/contrast/diff.scss +1 -1
@@ 5,7 5,7 @@
      &-description {
        input {
          &::placeholder {
            opacity: 1.0;
            opacity: 1;
          }
        }
      }

M app/javascript/styles/contrast/variables.scss => app/javascript/styles/contrast/variables.scss +1 -1
@@ 20,5 20,5 @@ $highlight-text-color: $classic-highlight-color !default;
$action-button-color: #8d9ac2;

$inverted-text-color: $black !default;
$lighter-text-color: darken($ui-base-color,6%) !default;
$lighter-text-color: darken($ui-base-color, 6%) !default;
$light-text-color: darken($ui-primary-color, 40%) !default;

M app/javascript/styles/mailer.scss => app/javascript/styles/mailer.scss +2 -2
@@ 279,6 279,8 @@ h5 {
}

.hero-with-button {
  padding-bottom: 16px;

  h1 {
    margin-bottom: 4px;
  }


@@ 286,8 288,6 @@ h5 {
  p.lead {
    margin-bottom: 32px;
  }

  padding-bottom: 16px;
}

.header {

M app/javascript/styles/mastodon/_mixins.scss => app/javascript/styles/mastodon/_mixins.scss +5 -5
@@ 1,21 1,21 @@
@mixin avatar-radius() {
@mixin avatar-radius {
  border-radius: 4px;
  background: transparent no-repeat;
  background-position: 50%;
  background-clip: padding-box;
}

@mixin avatar-size($size:48px) {
@mixin avatar-size($size: 48px) {
  width: $size;
  height: $size;
  background-size: $size $size;
}

@mixin search-input() {
@mixin search-input {
  outline: 0;
  box-sizing: border-box;
  width: 100%;
  border: none;
  border: 0;
  box-shadow: none;
  font-family: inherit;
  background: $ui-base-color;


@@ 42,7 42,7 @@
  }
}

@mixin search-popout() {
@mixin search-popout {
  background: $simple-background-color;
  border-radius: 4px;
  padding: 10px 14px;

M app/javascript/styles/mastodon/admin.scss => app/javascript/styles/mastodon/admin.scss +1 -1
@@ 171,7 171,7 @@ $content-width: 840px;
      text-transform: none;
      padding-bottom: 0;
      margin-bottom: 0;
      border-bottom: none;
      border-bottom: 0;
    }

    & > p {

M app/javascript/styles/mastodon/basics.scss => app/javascript/styles/mastodon/basics.scss +3 -2
@@ 2,7 2,8 @@
  @if type-of($color) == 'color' {
    $color: str-slice(ie-hex-str($color), 4);
  }
  @return '%23' + unquote($color)

  @return '%23' + unquote($color);
}

body {


@@ 15,7 16,7 @@ body {
  text-rendering: optimizelegibility;
  font-feature-settings: "kern";
  text-size-adjust: none;
  -webkit-tap-highlight-color: rgba(0,0,0,0);
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  -webkit-tap-highlight-color: transparent;

  &.system-font {

M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +42 -29
@@ 128,7 128,7 @@
  display: inline-block;
  padding: 0;
  color: $action-button-color;
  border: none;
  border: 0;
  background: transparent;
  cursor: pointer;
  transition: color 100ms ease-in;


@@ 196,7 196,7 @@

.text-icon-button {
  color: $lighter-text-color;
  border: none;
  border: 0;
  background: transparent;
  cursor: pointer;
  font-weight: 600;


@@ 353,12 353,12 @@
  .spoiler-input {
    height: 0;
    transform-origin: bottom;
    opacity: 0.0;
    opacity: 0;

    &.spoiler-input--visible {
      height: 36px;
      margin-bottom: 11px;
      opacity: 1.0;
      opacity: 1;
    }
  }



@@ 408,12 408,20 @@
    }
  }

  .emoji-picker-wrapper,
  .autosuggest-textarea__suggestions-wrapper {
    position: relative;
    height: 0;
  }

  .emoji-picker-wrapper {
    position: relative;
    height: 0;

    &.emoji-picker-wrapper--hidden {
      display: none;
    }
  }

  .autosuggest-textarea__suggestions {
    box-sizing: border-box;
    display: none;


@@ 1185,7 1193,7 @@
}

.account__avatar {
  @include avatar-radius();
  @include avatar-radius;
  position: relative;

  &-inline {


@@ 1195,11 1203,11 @@
  }

  &-composite {
    @include avatar-radius();
    @include avatar-radius;
    overflow: hidden;

    & > div {
      @include avatar-radius();
      @include avatar-radius;
      float: left;
      position: relative;
      box-sizing: border-box;


@@ 1215,12 1223,12 @@ a .account__avatar {
  @include avatar-size(48px);

  &-base {
    @include avatar-radius();
    @include avatar-radius;
    @include avatar-size(36px);
  }

  &-overlay {
    @include avatar-radius();
    @include avatar-radius;
    @include avatar-size(24px);

    position: absolute;


@@ 1598,13 1606,13 @@ a.account__display-name {
    .icon-button.close {
      position: absolute;
      pointer-events: none;
      transform: scale(0.0, 1.0) translate(-100%, 0);
      transform: scale(0, 1) translate(-100%, 0);
      opacity: 0;
    }

    .compose__action-bar .icon-button {
      pointer-events: auto;
      transform: scale(1.0, 1.0) translate(0, 0);
      transform: scale(1, 1) translate(0, 0);
      opacity: 1;
    }
  }


@@ 2071,6 2079,10 @@ a.account__display-name {

    .account {
      padding: 15px 10px;

      &__header__bio {
        margin: 0 -10px;
      }
    }

    .notification {


@@ 2699,7 2711,7 @@ a.account__display-name {
.setting-text {
  color: $darker-text-color;
  background: transparent;
  border: none;
  border: 0;
  border-bottom: 2px solid $ui-primary-color;
  box-sizing: border-box;
  display: block;


@@ 3037,7 3049,7 @@ a.status-card.compact:hover {

  & > button {
    margin: 0;
    border: none;
    border: 0;
    padding: 15px 0 15px 15px;
    color: inherit;
    background: transparent;


@@ 3202,11 3214,11 @@ a.status-card.compact:hover {
}

.no-reduce-motion .loading-indicator span {
  animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000);
  animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
}

.no-reduce-motion .loading-indicator__figure {
  animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000);
  animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
}

@keyframes loader-figure {


@@ 3373,7 3385,7 @@ a.status-card.compact:hover {

  .column-select {
    &__control {
      @include search-input();
      @include search-input;
    }

    &__placeholder {


@@ 3424,7 3436,7 @@ a.status-card.compact:hover {
    }

    &__menu {
      @include search-popout();
      @include search-popout;
      padding: 0;
      background: $ui-secondary-color;
    }


@@ 3585,7 3597,7 @@ a.status-card.compact:hover {

.no-reduce-motion .shake-bottom {
  transform-origin: 50% 100%;
  animation: shake-bottom 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) 2s 2 both;
  animation: shake-bottom 0.8s cubic-bezier(0.455, 0.03, 0.515, 0.955) 2s 2 both;
}

.emoji-picker-dropdown__menu {


@@ 3880,10 3892,11 @@ a.status-card.compact:hover {
}

.search__input {
  @include search-input;

  display: block;
  padding: 10px;
  padding-right: 30px;
  @include search-input();
}

.search__icon {


@@ 4491,14 4504,14 @@ a.status-card.compact:hover {
}

.actions-modal {
  max-height: 80vh;
  max-width: 80vw;

  .status {
    overflow-y: auto;
    max-height: 300px;
  }

  max-height: 80vh;
  max-width: 80vw;

  .actions-modal__item-label {
    font-weight: 500;
  }


@@ 4713,7 4726,7 @@ a.status-card.compact:hover {
}

.media-gallery__item {
  border: none;
  border: 0;
  box-sizing: border-box;
  display: block;
  float: left;


@@ 5173,7 5186,7 @@ a.status-card.compact:hover {
}

.account-gallery__item {
  border: none;
  border: 0;
  box-sizing: border-box;
  display: block;
  position: relative;


@@ 5247,7 5260,7 @@ a.status-card.compact:hover {
}

.search-popout {
  @include search-popout();
  @include search-popout;
}

noscript {


@@ 5349,14 5362,14 @@ noscript {
        .icon-button.close {
          pointer-events: auto;
          opacity: 1;
          transform: scale(1.0, 1.0) translate(0, 0);
          transform: scale(1, 1) translate(0, 0);
          bottom: 5px;
        }

        .compose__action-bar .icon-button {
          pointer-events: none;
          opacity: 0;
          transform: scale(0.0, 1.0) translate(100%, 0);
          transform: scale(0, 1) translate(100%, 0);
        }
      }
    }


@@ 5386,7 5399,7 @@ noscript {
      box-sizing: border-box;
      display: block;
      width: 100%;
      border: none;
      border: 0;
      padding: 10px;
      font-family: $font-monospace, monospace;
      background: $ui-base-color;

M app/javascript/styles/mastodon/containers.scss => app/javascript/styles/mastodon/containers.scss +2 -2
@@ 121,7 121,7 @@
  grid-auto-rows: max-content;

  .column-0 {
    grid-column: 1/3;
    grid-column: 1 / 3;
    grid-row: 1;
  }



@@ 136,7 136,7 @@
  }

  .column-3 {
    grid-column: 1/3;
    grid-column: 1 / 3;
    grid-row: 3;
  }


M app/javascript/styles/mastodon/emoji_picker.scss => app/javascript/styles/mastodon/emoji_picker.scss +4 -4
@@ 1,14 1,14 @@
.emoji-mart {
  font-size: 13px;
  display: inline-block;
  color: $inverted-text-color;

  &,
  * {
    box-sizing: border-box;
    line-height: 1.15;
  }

  font-size: 13px;
  display: inline-block;
  color: $inverted-text-color;

  .emoji-mart-emoji {
    padding: 6px;
  }

M app/javascript/styles/mastodon/forms.scss => app/javascript/styles/mastodon/forms.scss +1 -1
@@ 553,7 553,7 @@ code {
    box-sizing: border-box;
    display: block;
    width: 100%;
    border: none;
    border: 0;
    padding: 10px;
    font-family: $font-monospace, monospace;
    background: $ui-base-color;

M app/javascript/styles/mastodon/polls.scss => app/javascript/styles/mastodon/polls.scss +0 -1
@@ 47,7 47,6 @@
      width: 100%;
      font-size: 14px;
      color: $inverted-text-color;
      display: block;
      outline: 0;
      font-family: inherit;
      background: $simple-background-color;

M app/javascript/styles/mastodon/rtl.scss => app/javascript/styles/mastodon/rtl.scss +0 -1
@@ 180,7 180,6 @@ body.rtl {
  }

  .fa-ul {
    margin-left: 0;
    margin-left: 2.14285714em;
  }


M app/lib/activitypub/activity.rb => app/lib/activitypub/activity.rb +1 -1
@@ 143,7 143,7 @@ class ActivityPub::Activity

    # If the boosted toot is embedded and it is a self-boost, handle it like a Create
    unless unsupported_object_type?
      actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
      actor_id = value_or_id(first_of_value(@object['attributedTo']))

      if actor_id == @account.uri
        return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform

M app/lib/activitypub/activity/follow.rb => app/lib/activitypub/activity/follow.rb +3 -1
@@ 1,6 1,8 @@
# frozen_string_literal: true

class ActivityPub::Activity::Follow < ActivityPub::Activity
  include Payloadable

  def perform
    target_account = account_from_uri(object_uri)



@@ 28,7 30,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
  end

  def reject_follow_request!(target_account)
    json = ActiveModelSerializers::SerializableResource.new(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).to_json
    json = Oj.dump(serialize_payload(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), ActivityPub::RejectFollowSerializer))
    ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url)
  end
end

M app/models/account.rb => app/models/account.rb +4 -0
@@ 208,6 208,10 @@ class Account < ApplicationRecord
    end
  end

  def sign?
    true
  end

  def keypair
    @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
  end

M app/models/form/account_batch.rb => app/models/form/account_batch.rb +2 -7
@@ 3,6 3,7 @@
class Form::AccountBatch
  include ActiveModel::Model
  include Authorization
  include Payloadable

  attr_accessor :account_ids, :action, :current_account



@@ 54,13 55,7 @@ class Form::AccountBatch

    return unless follow.account.activitypub?

    json = ActiveModelSerializers::SerializableResource.new(
      follow,
      serializer: ActivityPub::RejectFollowSerializer,
      adapter: ActivityPub::Adapter
    ).to_json

    ActivityPub::DeliveryWorker.perform_async(json, current_account.id, follow.account.inbox_url)
    ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), current_account.id, follow.account.inbox_url)
  end

  def approve!

M app/models/status.rb => app/models/status.rb +4 -2
@@ 211,6 211,8 @@ class Status < ApplicationRecord
    public_visibility? || unlisted_visibility?
  end

  alias sign? distributable?

  def with_media?
    media_attachments.any?
  end


@@ 529,7 531,7 @@ class Status < ApplicationRecord
    return if direct_visibility?

    account&.increment_count!(:statuses_count)
    reblog&.increment_count!(:reblogs_count) if reblog? && (public_visibility? || unlisted_visibility?)
    reblog&.increment_count!(:reblogs_count) if reblog?
    thread&.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
  end



@@ 537,7 539,7 @@ class Status < ApplicationRecord
    return if direct_visibility? || marked_for_mass_destruction?

    account&.decrement_count!(:statuses_count)
    reblog&.decrement_count!(:reblogs_count) if reblog? && (public_visibility? || unlisted_visibility?)
    reblog&.decrement_count!(:reblogs_count) if reblog?
    thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
  end


M app/services/after_block_domain_from_account_service.rb => app/services/after_block_domain_from_account_service.rb +3 -7
@@ 1,6 1,8 @@
# frozen_string_literal: true

class AfterBlockDomainFromAccountService < BaseService
  include Payloadable

  # This service does not create an AccountDomainBlock record,
  # it's meant to be called after such a record has been created
  # synchronously, to "clean up"


@@ 31,12 33,6 @@ class AfterBlockDomainFromAccountService < BaseService

    return unless follow.account.activitypub?

    json = ActiveModelSerializers::SerializableResource.new(
      follow,
      serializer: ActivityPub::RejectFollowSerializer,
      adapter: ActivityPub::Adapter
    ).to_json

    ActivityPub::DeliveryWorker.perform_async(json, @account.id, follow.account.inbox_url)
    ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), @account.id, follow.account.inbox_url)
  end
end

M app/services/authorize_follow_service.rb => app/services/authorize_follow_service.rb +3 -5
@@ 1,6 1,8 @@
# frozen_string_literal: true

class AuthorizeFollowService < BaseService
  include Payloadable

  def call(source_account, target_account, **options)
    if options[:skip_follow_request]
      follow_request = FollowRequest.new(account: source_account, target_account: target_account, uri: options[:follow_request_uri])


@@ 24,11 26,7 @@ class AuthorizeFollowService < BaseService
  end

  def build_json(follow_request)
    ActiveModelSerializers::SerializableResource.new(
      follow_request,
      serializer: ActivityPub::AcceptFollowSerializer,
      adapter: ActivityPub::Adapter
    ).to_json
    Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer))
  end

  def build_xml(follow_request)

M app/services/block_service.rb => app/services/block_service.rb +3 -5
@@ 1,6 1,8 @@
# frozen_string_literal: true

class BlockService < BaseService
  include Payloadable

  def call(account, target_account)
    return if account.id == target_account.id



@@ 26,11 28,7 @@ class BlockService < BaseService
  end

  def build_json(block)
    ActiveModelSerializers::SerializableResource.new(
      block,
      serializer: ActivityPub::BlockSerializer,
      adapter: ActivityPub::Adapter
    ).to_json
    Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer))
  end

  def build_xml(block)

A app/services/concerns/payloadable.rb => app/services/concerns/payloadable.rb +19 -0
@@ 0,0 1,19 @@
# frozen_string_literal: true

module Payloadable
  def serialize_payload(record, serializer, options = {})
    signer    = options.delete(:signer)
    sign_with = options.delete(:sign_with)
    payload   = ActiveModelSerializers::SerializableResource.new(record, options.merge(serializer: serializer, adapter: ActivityPub::Adapter)).as_json

    if (record.respond_to?(:sign?) && record.sign?) && signer && signing_enabled?
      ActivityPub::LinkedDataSignature.new(payload).sign!(signer, sign_with: sign_with)
    else
      payload
    end
  end

  def signing_enabled?
    true
  end
end

M app/services/favourite_service.rb => app/services/favourite_service.rb +2 -5
@@ 2,6 2,7 @@

class FavouriteService < BaseService
  include Authorization
  include Payloadable

  # Favourite a status and notify remote user
  # @param [Account] account


@@ 43,11 44,7 @@ class FavouriteService < BaseService
  end

  def build_json(favourite)
    Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
      favourite,
      serializer: ActivityPub::LikeSerializer,
      adapter: ActivityPub::Adapter
    ).as_json).sign!(favourite.account))
    Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer))
  end

  def build_xml(favourite)

M app/services/follow_service.rb => app/services/follow_service.rb +2 -5
@@ 2,6 2,7 @@

class FollowService < BaseService
  include Redisable
  include Payloadable

  # Follow a remote user, notify remote user about the follow
  # @param [Account] source_account From which to follow


@@ 78,10 79,6 @@ class FollowService < BaseService
  end

  def build_json(follow_request)
    ActiveModelSerializers::SerializableResource.new(
      follow_request,
      serializer: ActivityPub::FollowSerializer,
      adapter: ActivityPub::Adapter
    ).to_json
    Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer))
  end
end

M app/services/process_mentions_service.rb => app/services/process_mentions_service.rb +2 -6
@@ 2,6 2,7 @@

class ProcessMentionsService < BaseService
  include StreamEntryRenderer
  include Payloadable

  # Scan status for mentions and fetch remote mentioned users, create
  # local mention pointers, send Salmon notifications to mentioned


@@ 61,12 62,7 @@ class ProcessMentionsService < BaseService

  def activitypub_json
    return @activitypub_json if defined?(@activitypub_json)
    payload = ActiveModelSerializers::SerializableResource.new(
      @status,
      serializer: ActivityPub::ActivitySerializer,
      adapter: ActivityPub::Adapter
    ).as_json
    @activitypub_json = Oj.dump(@status.distributable? ? ActivityPub::LinkedDataSignature.new(payload).sign!(@status.account) : payload)
    @activitypub_json = Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account))
  end

  def resolve_account_service

M app/services/reblog_service.rb => app/services/reblog_service.rb +2 -5
@@ 3,6 3,7 @@
class ReblogService < BaseService
  include Authorization
  include StreamEntryRenderer
  include Payloadable

  # Reblog a status and notify its remote author
  # @param [Account] account Account to reblog from


@@ 56,10 57,6 @@ class ReblogService < BaseService
  end

  def build_json(reblog)
    Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
      reblog,
      serializer: ActivityPub::ActivitySerializer,
      adapter: ActivityPub::Adapter
    ).as_json).sign!(reblog.account))
    Oj.dump(serialize_payload(reblog, ActivityPub::ActivitySerializer, signer: reblog.account))
  end
end

M app/services/reject_follow_service.rb => app/services/reject_follow_service.rb +3 -5
@@ 1,6 1,8 @@
# frozen_string_literal: true

class RejectFollowService < BaseService
  include Payloadable

  def call(source_account, target_account)
    follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
    follow_request.reject!


@@ 19,11 21,7 @@ class RejectFollowService < BaseService
  end

  def build_json(follow_request)
    ActiveModelSerializers::SerializableResource.new(
      follow_request,
      serializer: ActivityPub::RejectFollowSerializer,
      adapter: ActivityPub::Adapter
    ).to_json
    Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer))
  end

  def build_xml(follow_request)

M app/services/remove_status_service.rb => app/services/remove_status_service.rb +2 -9
@@ 3,6 3,7 @@
class RemoveStatusService < BaseService
  include StreamEntryRenderer
  include Redisable
  include Payloadable

  def call(status, **options)
    @payload      = Oj.dump(event: :delete, payload: status.id.to_s)


@@ 116,15 117,7 @@ class RemoveStatusService < BaseService
  end

  def signed_activity_json
    @signed_activity_json ||= Oj.dump(ActivityPub::LinkedDataSignature.new(activity_json).sign!(@account))
  end

  def activity_json
    @activity_json ||= ActiveModelSerializers::SerializableResource.new(
      @status,
      serializer: @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer,
      adapter: ActivityPub::Adapter
    ).as_json
    @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account))
  end

  def remove_reblogs

M app/services/report_service.rb => app/services/report_service.rb +3 -6
@@ 1,6 1,8 @@
# frozen_string_literal: true

class ReportService < BaseService
  include Payloadable

  def call(source_account, target_account, options = {})
    @source_account = source_account
    @target_account = target_account


@@ 44,12 46,7 @@ class ReportService < BaseService
  end

  def payload
    Oj.dump(ActiveModelSerializers::SerializableResource.new(
      @report,
      serializer: ActivityPub::FlagSerializer,
      adapter: ActivityPub::Adapter,
      account: some_local_account
    ).as_json)
    Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account))
  end

  def some_local_account

M app/services/suspend_account_service.rb => app/services/suspend_account_service.rb +4 -14
@@ 1,6 1,8 @@
# frozen_string_literal: true

class SuspendAccountService < BaseService
  include Payloadable

  ASSOCIATIONS_ON_SUSPEND = %w(
    account_pins
    active_relationships


@@ 118,23 120,11 @@ class SuspendAccountService < BaseService
  end

  def delete_actor_json
    return @delete_actor_json if defined?(@delete_actor_json)

    payload = ActiveModelSerializers::SerializableResource.new(
      @account,
      serializer: ActivityPub::DeleteActorSerializer,
      adapter: ActivityPub::Adapter
    ).as_json

    @delete_actor_json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
    @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
  end

  def build_reject_json(follow)
    ActiveModelSerializers::SerializableResource.new(
      follow,
      serializer: ActivityPub::RejectFollowSerializer,
      adapter: ActivityPub::Adapter
    ).to_json
    Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
  end

  def delivery_inboxes

M app/services/unblock_service.rb => app/services/unblock_service.rb +3 -5
@@ 1,6 1,8 @@
# frozen_string_literal: true

class UnblockService < BaseService
  include Payloadable

  def call(account, target_account)
    return unless account.blocking?(target_account)



@@ 20,11 22,7 @@ class UnblockService < BaseService
  end

  def build_json(unblock)
    ActiveModelSerializers::SerializableResource.new(
      unblock,
      serializer: ActivityPub::UndoBlockSerializer,
      adapter: ActivityPub::Adapter
    ).to_json
    Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer))
  end

  def build_xml(block)

M app/services/unfavourite_service.rb => app/services/unfavourite_service.rb +3 -5
@@ 1,6 1,8 @@
# frozen_string_literal: true

class UnfavouriteService < BaseService
  include Payloadable

  def call(account, status)
    favourite = Favourite.find_by!(account: account, status: status)
    favourite.destroy!


@@ 21,11 23,7 @@ class UnfavouriteService < BaseService
  end

  def build_json(favourite)
    Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
      favourite,
      serializer: ActivityPub::UndoLikeSerializer,
      adapter: ActivityPub::Adapter
    ).as_json).sign!(favourite.account))
    Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer))
  end

  def build_xml(favourite)

M app/services/unfollow_service.rb => app/services/unfollow_service.rb +4 -10
@@ 1,6 1,8 @@
# frozen_string_literal: true

class UnfollowService < BaseService
  include Payloadable

  # Unfollow and notify the remote user
  # @param [Account] source_account Where to unfollow from
  # @param [Account] target_account Which to unfollow


@@ 50,19 52,11 @@ class UnfollowService < BaseService
  end

  def build_json(follow)
    ActiveModelSerializers::SerializableResource.new(
      follow,
      serializer: ActivityPub::UndoFollowSerializer,
      adapter: ActivityPub::Adapter
    ).to_json
    Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
  end

  def build_reject_json(follow)
    ActiveModelSerializers::SerializableResource.new(
      follow,
      serializer: ActivityPub::RejectFollowSerializer,
      adapter: ActivityPub::Adapter
    ).to_json
    Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
  end

  def build_xml(follow)

M app/services/vote_service.rb => app/services/vote_service.rb +2 -5
@@ 2,6 2,7 @@

class VoteService < BaseService
  include Authorization
  include Payloadable

  def call(account, poll, choices)
    authorize_with account, poll, :vote?


@@ 50,10 51,6 @@ class VoteService < BaseService
  end

  def build_json(vote)
    ActiveModelSerializers::SerializableResource.new(
      vote,
      serializer: ActivityPub::VoteSerializer,
      adapter: ActivityPub::Adapter
    ).to_json
    Oj.dump(serialize_payload(vote, ActivityPub::VoteSerializer))
  end
end

M app/views/admin/pending_accounts/_account.html.haml => app/views/admin/pending_accounts/_account.html.haml +2 -0
@@ 8,6 8,8 @@
        = "(@#{account.username})"
      %br/
      = account.user_current_sign_in_ip
      = t 'admin.accounts.time_in_queue', time: time_ago_in_words(account.user&.created_at)

    - if account.user&.invite_request&.text&.present?
      .pending-account__body

M app/workers/activitypub/distribute_poll_update_worker.rb => app/workers/activitypub/distribute_poll_update_worker.rb +2 -13
@@ 2,6 2,7 @@

class ActivityPub::DistributePollUpdateWorker
  include Sidekiq::Worker
  include Payloadable

  sidekiq_options queue: 'push', unique: :until_executed, retry: 0



@@ 41,20 42,8 @@ class ActivityPub::DistributePollUpdateWorker
    @inboxes
  end

  def signed_payload
    Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@account))
  end

  def unsigned_payload
    ActiveModelSerializers::SerializableResource.new(
      @status,
      serializer: ActivityPub::UpdatePollSerializer,
      adapter: ActivityPub::Adapter
    ).as_json
  end

  def payload
    @payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload)
    @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::UpdatePollSerializer, signer: @account))
  end

  def relay!

M app/workers/activitypub/distribution_worker.rb => app/workers/activitypub/distribution_worker.rb +2 -13
@@ 2,6 2,7 @@

class ActivityPub::DistributionWorker
  include Sidekiq::Worker
  include Payloadable

  sidekiq_options queue: 'push'



@@ 41,20 42,8 @@ class ActivityPub::DistributionWorker
                 end
  end

  def signed_payload
    Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@account))
  end

  def unsigned_payload
    ActiveModelSerializers::SerializableResource.new(
      @status,
      serializer: ActivityPub::ActivitySerializer,
      adapter: ActivityPub::Adapter
    ).as_json
  end

  def payload
    @payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload)
    @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @account))
  end

  def relay!

M app/workers/activitypub/reply_distribution_worker.rb => app/workers/activitypub/reply_distribution_worker.rb +2 -13
@@ 5,6 5,7 @@

class ActivityPub::ReplyDistributionWorker
  include Sidekiq::Worker
  include Payloadable

  sidekiq_options queue: 'push'



@@ 27,19 28,7 @@ class ActivityPub::ReplyDistributionWorker
    @inboxes ||= @account.followers.inboxes
  end

  def signed_payload
    Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@status.account))
  end

  def unsigned_payload
    ActiveModelSerializers::SerializableResource.new(
      @status,
      serializer: ActivityPub::ActivitySerializer,
      adapter: ActivityPub::Adapter
    ).as_json
  end

  def payload
    @payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload)
    @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account))
  end
end

M app/workers/activitypub/update_distribution_worker.rb => app/workers/activitypub/update_distribution_worker.rb +2 -9
@@ 2,6 2,7 @@

class ActivityPub::UpdateDistributionWorker
  include Sidekiq::Worker
  include Payloadable

  sidekiq_options queue: 'push'



@@ 27,14 28,6 @@ class ActivityPub::UpdateDistributionWorker
  end

  def signed_payload
    @signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account, sign_with: @options[:sign_with]))
  end

  def payload
    @payload ||= ActiveModelSerializers::SerializableResource.new(
      @account,
      serializer: ActivityPub::UpdateSerializer,
      adapter: ActivityPub::Adapter
    ).as_json
    @signed_payload ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account, sign_with: @options[:sign_with]))
  end
end

M config/locales/en.yml => config/locales/en.yml +1 -0
@@ 174,6 174,7 @@ en:
      statuses: Statuses
      subscribe: Subscribe
      suspended: Suspended
      time_in_queue: Waiting in queue %{time}
      title: Accounts
      unconfirmed_email: Unconfirmed email
      undo_silenced: Undo silence

M package.json => package.json +4 -3
@@ 10,9 10,10 @@
    "build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
    "manage:translations": "node ./config/webpack/translationRunner.js",
    "start": "node ./streaming/index.js",
    "test": "${npm_execpath} run test:lint && ${npm_execpath} run test:jest",
    "test:lint": "eslint --ext=js .",
    "test:lint:sass": "sass-lint .",
    "test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:jest",
    "test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass",
    "test:lint:js": "eslint --ext=js .",
    "test:lint:sass": "sass-lint -v",
    "test:jest": "cross-env NODE_ENV=test jest --coverage"
  },
  "repository": {

M spec/lib/activitypub/activity/announce_spec.rb => spec/lib/activitypub/activity/announce_spec.rb +3 -15
@@ 58,21 58,6 @@ RSpec.describe ActivityPub::Activity::Announce do
        end
      end

      context 'self-boost of a previously unknown status with missing attributedTo' do
        let(:object_json) do
          {
            id: 'https://example.com/actor#bar',
            type: 'Note',
            content: 'Lorem ipsum',
            to: 'http://example.com/followers',
          }
        end

        it 'creates a reblog by sender of status' do
          expect(sender.reblogged?(sender.statuses.first)).to be true
        end
      end

      context 'self-boost of a previously unknown status with correct attributedTo' do
        let(:object_json) do
          {


@@ 122,6 107,7 @@ RSpec.describe ActivityPub::Activity::Announce do
            type: 'Note',
            content: 'Lorem ipsum',
            to: 'http://example.com/followers',
            attributedTo: 'https://example.com/actor',
          }
        end



@@ 141,6 127,7 @@ RSpec.describe ActivityPub::Activity::Announce do
            type: 'Note',
            content: 'Lorem ipsum',
            to: 'http://example.com/followers',
            attributedTo: 'https://example.com/actor',
          }
        end



@@ 161,6 148,7 @@ RSpec.describe ActivityPub::Activity::Announce do
          type: 'Note',
          content: 'Lorem ipsum',
          to: 'http://example.com/followers',
          attributedTo: 'https://example.com/actor',
        }
      end