Add support for private pinned posts (#16954)
* Add support for private pinned toots * Allow local user to pin private toots * Change wording to avoid "direct message"
This commit is contained in:
		
							parent
							
								
									081e4426f8
								
							
						
					
					
						commit
						d5c9feb7b7
					
				
					 18 changed files with 164 additions and 28 deletions
				
			
		| 
						 | 
				
			
			@ -28,7 +28,7 @@ class AccountsController < ApplicationController
 | 
			
		|||
          return
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
 | 
			
		||||
        @pinned_statuses = cached_filtered_status_pins if show_pinned_statuses?
 | 
			
		||||
        @statuses        = cached_filtered_status_page
 | 
			
		||||
        @rss_url         = rss_url
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +64,10 @@ class AccountsController < ApplicationController
 | 
			
		|||
    [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def filtered_pinned_statuses
 | 
			
		||||
    @account.pinned_statuses.where(visibility: [:public, :unlisted])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def filtered_statuses
 | 
			
		||||
    default_statuses.tap do |statuses|
 | 
			
		||||
      statuses.merge!(hashtag_scope)    if tag_requested?
 | 
			
		||||
| 
						 | 
				
			
			@ -142,6 +146,13 @@ class AccountsController < ApplicationController
 | 
			
		|||
    request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def cached_filtered_status_pins
 | 
			
		||||
    cache_collection(
 | 
			
		||||
      filtered_pinned_statuses,
 | 
			
		||||
      Status
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def cached_filtered_status_page
 | 
			
		||||
    cache_collection_paginated_by_id(
 | 
			
		||||
      filtered_statuses,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
 | 
			
		|||
    case params[:id]
 | 
			
		||||
    when 'featured'
 | 
			
		||||
      @items = for_signed_account { cache_collection(@account.pinned_statuses, Status) }
 | 
			
		||||
      @items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) }
 | 
			
		||||
    when 'tags'
 | 
			
		||||
      @items = for_signed_account { @account.featured_tags }
 | 
			
		||||
    when 'devices'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,9 +46,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def pinned_scope
 | 
			
		||||
    return Status.none if @account.blocking?(current_account)
 | 
			
		||||
 | 
			
		||||
    @account.pinned_statuses
 | 
			
		||||
    @account.pinned_statuses.permitted_for(@account, current_account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def no_replies_scope
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -225,6 +225,7 @@ class StatusActionBar extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
    const anonymousAccess    = !me;
 | 
			
		||||
    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
 | 
			
		||||
    const pinnableStatus     = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
 | 
			
		||||
    const mutingConversation = status.get('muted');
 | 
			
		||||
    const account            = status.get('account');
 | 
			
		||||
    const writtenByMe        = status.getIn(['account', 'id']) === me;
 | 
			
		||||
| 
						 | 
				
			
			@ -242,7 +243,7 @@ class StatusActionBar extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
    menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
 | 
			
		||||
 | 
			
		||||
    if (writtenByMe && publicStatus) {
 | 
			
		||||
    if (writtenByMe && pinnableStatus) {
 | 
			
		||||
      menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -188,6 +188,7 @@ class ActionBar extends React.PureComponent {
 | 
			
		|||
    const { status, relationship, intl } = this.props;
 | 
			
		||||
 | 
			
		||||
    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
 | 
			
		||||
    const pinnableStatus     = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
 | 
			
		||||
    const mutingConversation = status.get('muted');
 | 
			
		||||
    const account            = status.get('account');
 | 
			
		||||
    const writtenByMe        = status.getIn(['account', 'id']) === me;
 | 
			
		||||
| 
						 | 
				
			
			@ -201,7 +202,7 @@ class ActionBar extends React.PureComponent {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (writtenByMe) {
 | 
			
		||||
      if (publicStatus) {
 | 
			
		||||
      if (pinnableStatus) {
 | 
			
		||||
        menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
 | 
			
		||||
        menu.push(null);
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
class ActivityPub::Activity::Accept < ActivityPub::Activity
 | 
			
		||||
  def perform
 | 
			
		||||
    return accept_follow_for_relay if relay_follow?
 | 
			
		||||
    return follow_request_from_object.authorize! unless follow_request_from_object.nil?
 | 
			
		||||
    return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil?
 | 
			
		||||
 | 
			
		||||
    case @object['type']
 | 
			
		||||
    when 'Follow'
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +19,16 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
 | 
			
		|||
    return if target_account.nil? || !target_account.local?
 | 
			
		||||
 | 
			
		||||
    follow_request = FollowRequest.find_by(account: target_account, target_account: @account)
 | 
			
		||||
    follow_request&.authorize!
 | 
			
		||||
    accept_follow!(follow_request)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def accept_follow!(request)
 | 
			
		||||
    return if request.nil?
 | 
			
		||||
 | 
			
		||||
    is_first_follow = !request.target_account.followers.local.exists?
 | 
			
		||||
    request.authorize!
 | 
			
		||||
 | 
			
		||||
    RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def accept_follow_for_relay
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,8 +4,7 @@ class ActivityPub::Activity::Add < ActivityPub::Activity
 | 
			
		|||
  def perform
 | 
			
		||||
    return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url
 | 
			
		||||
 | 
			
		||||
    status   = status_from_uri(object_uri)
 | 
			
		||||
    status ||= fetch_remote_original_status
 | 
			
		||||
    status = status_from_object
 | 
			
		||||
 | 
			
		||||
    return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
 | 
			
		|||
 | 
			
		||||
  def process_items(items)
 | 
			
		||||
    status_ids = items.map { |item| value_or_id(item) }
 | 
			
		||||
                      .filter_map { |uri| ActivityPub::FetchRemoteStatusService.new.call(uri) unless ActivityPub::TagManager.instance.local_uri?(uri) }
 | 
			
		||||
                      .filter_map { |uri| ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: local_follower) unless ActivityPub::TagManager.instance.local_uri?(uri) }
 | 
			
		||||
                      .filter_map { |status| status.id if status.account_id == @account.id }
 | 
			
		||||
    to_remove = []
 | 
			
		||||
    to_add    = status_ids
 | 
			
		||||
| 
						 | 
				
			
			@ -46,4 +46,8 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
 | 
			
		|||
  def supported_context?
 | 
			
		||||
    super(@json)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def local_follower
 | 
			
		||||
    @local_follower ||= account.followers.local.without_suspended.first
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ class StatusPinValidator < ActiveModel::Validator
 | 
			
		|||
  def validate(pin)
 | 
			
		||||
    pin.errors.add(:base, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog?
 | 
			
		||||
    pin.errors.add(:base, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id
 | 
			
		||||
    pin.errors.add(:base, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility)
 | 
			
		||||
    pin.errors.add(:base, I18n.t('statuses.pin_errors.direct')) if pin.status.direct_visibility?
 | 
			
		||||
    pin.errors.add(:base, I18n.t('statuses.pin_errors.limit')) if pin.account.status_pins.count > 4 && pin.account.local?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										24
									
								
								app/workers/remote_account_refresh_worker.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/workers/remote_account_refresh_worker.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class RemoteAccountRefreshWorker
 | 
			
		||||
  include Sidekiq::Worker
 | 
			
		||||
  include ExponentialBackoff
 | 
			
		||||
  include JsonLdHelper
 | 
			
		||||
 | 
			
		||||
  sidekiq_options queue: 'pull', retry: 3
 | 
			
		||||
 | 
			
		||||
  def perform(id)
 | 
			
		||||
    account = Account.find_by(id: id)
 | 
			
		||||
    return if account.nil? || account.local?
 | 
			
		||||
 | 
			
		||||
    ActivityPub::FetchRemoteAccountService.new.call(account.uri)
 | 
			
		||||
  rescue Mastodon::UnexpectedResponseError => e
 | 
			
		||||
    response = e.response
 | 
			
		||||
 | 
			
		||||
    if response_error_unsalvageable?(response)
 | 
			
		||||
      # Give up
 | 
			
		||||
    else
 | 
			
		||||
      raise e
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1300,9 +1300,9 @@ en:
 | 
			
		|||
    open_in_web: Open in web
 | 
			
		||||
    over_character_limit: character limit of %{max} exceeded
 | 
			
		||||
    pin_errors:
 | 
			
		||||
      direct: Posts that are only visible to mentioned users cannot be pinned
 | 
			
		||||
      limit: You have already pinned the maximum number of posts
 | 
			
		||||
      ownership: Someone else's post cannot be pinned
 | 
			
		||||
      private: Non-public posts cannot be pinned
 | 
			
		||||
      reblog: A boost cannot be pinned
 | 
			
		||||
    poll:
 | 
			
		||||
      total_people:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,7 @@ RSpec.describe AccountsController, type: :controller do
 | 
			
		|||
    before do
 | 
			
		||||
      status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image)
 | 
			
		||||
      account.pinned_statuses << status_pinned
 | 
			
		||||
      account.pinned_statuses << status_private
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    shared_examples 'preliminary checks' do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ require 'rails_helper'
 | 
			
		|||
 | 
			
		||||
RSpec.describe ActivityPub::CollectionsController, type: :controller do
 | 
			
		||||
  let!(:account) { Fabricate(:account) }
 | 
			
		||||
  let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) }
 | 
			
		||||
  let(:remote_account) { nil }
 | 
			
		||||
 | 
			
		||||
  shared_examples 'cachable response' do
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +28,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
 | 
			
		|||
 | 
			
		||||
    Fabricate(:status_pin, account: account)
 | 
			
		||||
    Fabricate(:status_pin, account: account)
 | 
			
		||||
    Fabricate(:status_pin, account: account, status: private_pinned)
 | 
			
		||||
    Fabricate(:status, account: account, visibility: :private)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +52,15 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
 | 
			
		|||
 | 
			
		||||
        it 'returns orderedItems with pinned statuses' do
 | 
			
		||||
          expect(body[:orderedItems]).to be_an Array
 | 
			
		||||
          expect(body[:orderedItems].size).to eq 2
 | 
			
		||||
          expect(body[:orderedItems].size).to eq 3
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'includes URI of private pinned status' do
 | 
			
		||||
          expect(body[:orderedItems]).to include(ActivityPub::TagManager.instance.uri_for(private_pinned))
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'does not include contents of private pinned status' do
 | 
			
		||||
          expect(response.body).not_to include(private_pinned.text)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'when account is permanently suspended' do
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +106,16 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
 | 
			
		|||
          it 'returns orderedItems with pinned statuses' do
 | 
			
		||||
            json = body_as_json
 | 
			
		||||
            expect(json[:orderedItems]).to be_an Array
 | 
			
		||||
            expect(json[:orderedItems].size).to eq 2
 | 
			
		||||
            expect(json[:orderedItems].size).to eq 3
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          it 'includes URI of private pinned status' do
 | 
			
		||||
            json = body_as_json
 | 
			
		||||
            expect(json[:orderedItems]).to include(ActivityPub::TagManager.instance.uri_for(private_pinned))
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          it 'does not include contents of private pinned status' do
 | 
			
		||||
            expect(response.body).not_to include(private_pinned.text)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,7 +39,7 @@ describe Api::V1::Accounts::StatusesController do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with only pinned' do
 | 
			
		||||
    context 'with only own pinned' do
 | 
			
		||||
      before do
 | 
			
		||||
        Fabricate(:status_pin, account: user.account, status: Fabricate(:status, account: user.account))
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			@ -50,5 +50,38 @@ describe Api::V1::Accounts::StatusesController do
 | 
			
		|||
        expect(response).to have_http_status(200)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "with someone else's pinned statuses" do
 | 
			
		||||
      let(:account)        { Fabricate(:account, username: 'bob', domain: 'example.com') }
 | 
			
		||||
      let(:status)         { Fabricate(:status, account: account) }
 | 
			
		||||
      let(:private_status) { Fabricate(:status, account: account, visibility: :private) }
 | 
			
		||||
      let!(:pin)           { Fabricate(:status_pin, account: account, status: status) }
 | 
			
		||||
      let!(:private_pin)   { Fabricate(:status_pin, account: account, status: private_status) }
 | 
			
		||||
 | 
			
		||||
      it 'returns http success' do
 | 
			
		||||
        get :index, params: { account_id: account.id, pinned: true }
 | 
			
		||||
        expect(response).to have_http_status(200)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'when user does not follow account' do
 | 
			
		||||
        it 'lists the public status only' do
 | 
			
		||||
          get :index, params: { account_id: account.id, pinned: true }
 | 
			
		||||
          json = body_as_json
 | 
			
		||||
          expect(json.map { |item| item[:id].to_i }).to eq [status.id]
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'when user follows account' do
 | 
			
		||||
        before do
 | 
			
		||||
          user.account.follow!(account)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'lists both the public and the private statuses' do
 | 
			
		||||
          get :index, params: { account_id: account.id, pinned: true }
 | 
			
		||||
          json = body_as_json
 | 
			
		||||
          expect(json.map { |item| item[:id].to_i }.sort).to eq [status.id, private_status.id].sort
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@ RSpec.describe ActivityPub::Activity::Accept do
 | 
			
		|||
    subject { described_class.new(json, sender) }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      allow(RemoteAccountRefreshWorker).to receive(:perform_async)
 | 
			
		||||
      Fabricate(:follow_request, account: recipient, target_account: sender)
 | 
			
		||||
      subject.perform
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +35,10 @@ RSpec.describe ActivityPub::Activity::Accept do
 | 
			
		|||
    it 'removes the follow request' do
 | 
			
		||||
      expect(recipient.requested?(sender)).to be false
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'queues a refresh' do
 | 
			
		||||
      expect(RemoteAccountRefreshWorker).to have_received(:perform_async).with(sender.id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'given a relay' do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe ActivityPub::Activity::Add do
 | 
			
		||||
  let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') }
 | 
			
		||||
  let(:status) { Fabricate(:status, account: sender) }
 | 
			
		||||
  let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured', domain: 'example.com') }
 | 
			
		||||
  let(:status) { Fabricate(:status, account: sender, visibility: :private) }
 | 
			
		||||
 | 
			
		||||
  let(:json) do
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +24,8 @@ RSpec.describe ActivityPub::Activity::Add do
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when status was not known before' do
 | 
			
		||||
      let(:service_stub) { double }
 | 
			
		||||
 | 
			
		||||
      let(:json) do
 | 
			
		||||
        {
 | 
			
		||||
          '@context': 'https://www.w3.org/ns/activitystreams',
 | 
			
		||||
| 
						 | 
				
			
			@ -36,12 +38,40 @@ RSpec.describe ActivityPub::Activity::Add do
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      before do
 | 
			
		||||
        stub_request(:get, 'https://example.com/unknown').to_return(status: 410)
 | 
			
		||||
        allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_stub)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'fetches the status' do
 | 
			
		||||
        subject.perform
 | 
			
		||||
        expect(a_request(:get, 'https://example.com/unknown')).to have_been_made.at_least_once
 | 
			
		||||
      context 'when there is a local follower' do
 | 
			
		||||
        before do
 | 
			
		||||
          account = Fabricate(:account)
 | 
			
		||||
          account.follow!(sender)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'fetches the status and pins it' do
 | 
			
		||||
          allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil|
 | 
			
		||||
            expect(uri).to eq 'https://example.com/unknown'
 | 
			
		||||
            expect(id).to eq true
 | 
			
		||||
            expect(on_behalf_of&.following?(sender)).to eq true
 | 
			
		||||
            status
 | 
			
		||||
          end
 | 
			
		||||
          subject.perform
 | 
			
		||||
          expect(service_stub).to have_received(:call)
 | 
			
		||||
          expect(sender.pinned?(status)).to be true
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'when there is no local follower' do
 | 
			
		||||
        it 'tries to fetch the status' do
 | 
			
		||||
          allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil|
 | 
			
		||||
            expect(uri).to eq 'https://example.com/unknown'
 | 
			
		||||
            expect(id).to eq true
 | 
			
		||||
            expect(on_behalf_of).to eq nil
 | 
			
		||||
            nil
 | 
			
		||||
          end
 | 
			
		||||
          subject.perform
 | 
			
		||||
          expect(service_stub).to have_received(:call)
 | 
			
		||||
          expect(sender.pinned?(status)).to be false
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,11 +24,11 @@ RSpec.describe StatusPin, type: :model do
 | 
			
		|||
      expect(StatusPin.new(account: account, status: reblog).save).to be false
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'does not allow pins of private statuses' do
 | 
			
		||||
    it 'does allow pins of direct statuses' do
 | 
			
		||||
      account = Fabricate(:account)
 | 
			
		||||
      status  = Fabricate(:status, account: account, visibility: :private)
 | 
			
		||||
 | 
			
		||||
      expect(StatusPin.new(account: account, status: status).save).to be false
 | 
			
		||||
      expect(StatusPin.new(account: account, status: status).save).to be true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'does not allow pins of direct statuses' do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ RSpec.describe StatusPinValidator, type: :validator do
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    let(:pin) { double(account: account, errors: errors, status: status, account_id: pin_account_id) }
 | 
			
		||||
    let(:status) { double(reblog?: reblog, account_id: status_account_id, visibility: visibility) }
 | 
			
		||||
    let(:status) { double(reblog?: reblog, account_id: status_account_id, visibility: visibility, direct_visibility?: visibility == 'direct') }
 | 
			
		||||
    let(:account)     { double(status_pins: status_pins, local?: local) }
 | 
			
		||||
    let(:status_pins) { double(count: count) }
 | 
			
		||||
    let(:errors)      { double(add: nil) }
 | 
			
		||||
| 
						 | 
				
			
			@ -37,11 +37,11 @@ RSpec.describe StatusPinValidator, type: :validator do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'unless %w(public unlisted).include?(pin.status.visibility)' do
 | 
			
		||||
      let(:visibility) { '' }
 | 
			
		||||
    context 'if pin.status.direct_visibility?' do
 | 
			
		||||
      let(:visibility) { 'direct' }
 | 
			
		||||
 | 
			
		||||
      it 'calls errors.add' do
 | 
			
		||||
        expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.private'))
 | 
			
		||||
        expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.direct'))
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue