Merge pull request #212 from aschmitz/feat/mute-reblogs
Allow hiding reblogs on a per-follow basis
This commit is contained in:
		
						commit
						ed1cf698a2
					
				
					 23 changed files with 259 additions and 34 deletions
				
			
		|  | @ -13,9 +13,11 @@ class Api::V1::AccountsController < Api::BaseController | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def follow |   def follow | ||||||
|     FollowService.new.call(current_user.account, @account.acct) |     reblogs_arg = { reblogs: params[:reblogs] } | ||||||
|  |      | ||||||
|  |     FollowService.new.call(current_user.account, @account.acct, reblogs_arg) | ||||||
| 
 | 
 | ||||||
|     options = @account.locked? ? {} : { following_map: { @account.id => true }, requested_map: { @account.id => false } } |     options = @account.locked? ? {} : { following_map: { @account.id => reblogs_arg }, requested_map: { @account.id => false } } | ||||||
| 
 | 
 | ||||||
|     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) |     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -152,7 +152,7 @@ appropriate icon. | ||||||
|             <IconButton |             <IconButton | ||||||
|               size={26} |               size={26} | ||||||
|               icon={following ? 'user-times' : 'user-plus'} |               icon={following ? 'user-times' : 'user-plus'} | ||||||
|               active={following} |               active={following ? true : false} | ||||||
|               title={intl.formatMessage(following ? messages.unfollow : messages.follow)} |               title={intl.formatMessage(following ? messages.unfollow : messages.follow)} | ||||||
|               onClick={this.props.onFollow} |               onClick={this.props.onFollow} | ||||||
|             /> |             /> | ||||||
|  |  | ||||||
|  | @ -105,12 +105,13 @@ export function fetchAccountFail(id, error) { | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export function followAccount(id) { | export function followAccount(id, reblogs = true) { | ||||||
|   return (dispatch, getState) => { |   return (dispatch, getState) => { | ||||||
|  |     const alreadyFollowing = getState().getIn(['relationships', id, 'following']); | ||||||
|     dispatch(followAccountRequest(id)); |     dispatch(followAccountRequest(id)); | ||||||
| 
 | 
 | ||||||
|     api(getState).post(`/api/v1/accounts/${id}/follow`).then(response => { |     api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { | ||||||
|       dispatch(followAccountSuccess(response.data)); |       dispatch(followAccountSuccess(response.data, alreadyFollowing)); | ||||||
|     }).catch(error => { |     }).catch(error => { | ||||||
|       dispatch(followAccountFail(error)); |       dispatch(followAccountFail(error)); | ||||||
|     }); |     }); | ||||||
|  | @ -136,10 +137,11 @@ export function followAccountRequest(id) { | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export function followAccountSuccess(relationship) { | export function followAccountSuccess(relationship, alreadyFollowing) { | ||||||
|   return { |   return { | ||||||
|     type: ACCOUNT_FOLLOW_SUCCESS, |     type: ACCOUNT_FOLLOW_SUCCESS, | ||||||
|     relationship, |     relationship, | ||||||
|  |     alreadyFollowing, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -93,7 +93,7 @@ export default class Account extends ImmutablePureComponent { | ||||||
|           </div> |           </div> | ||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
|         buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; |         buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,6 +19,8 @@ const messages = defineMessages({ | ||||||
|   media: { id: 'account.media', defaultMessage: 'Media' }, |   media: { id: 'account.media', defaultMessage: 'Media' }, | ||||||
|   blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, |   blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, | ||||||
|   unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, |   unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, | ||||||
|  |   hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, | ||||||
|  |   showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| @injectIntl | @injectIntl | ||||||
|  | @ -30,6 +32,7 @@ export default class ActionBar extends React.PureComponent { | ||||||
|     onFollow: PropTypes.func, |     onFollow: PropTypes.func, | ||||||
|     onBlock: PropTypes.func.isRequired, |     onBlock: PropTypes.func.isRequired, | ||||||
|     onMention: PropTypes.func.isRequired, |     onMention: PropTypes.func.isRequired, | ||||||
|  |     onReblogToggle: PropTypes.func.isRequired, | ||||||
|     onReport: PropTypes.func.isRequired, |     onReport: PropTypes.func.isRequired, | ||||||
|     onMute: PropTypes.func.isRequired, |     onMute: PropTypes.func.isRequired, | ||||||
|     onBlockDomain: PropTypes.func.isRequired, |     onBlockDomain: PropTypes.func.isRequired, | ||||||
|  | @ -60,6 +63,15 @@ export default class ActionBar extends React.PureComponent { | ||||||
|     if (account.get('id') === me) { |     if (account.get('id') === me) { | ||||||
|       menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); |       menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); | ||||||
|     } else { |     } else { | ||||||
|  |       const following = account.getIn(['relationship', 'following']); | ||||||
|  |       if (following) { | ||||||
|  |         if (following.get('reblogs')) { | ||||||
|  |           menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); | ||||||
|  |         } else { | ||||||
|  |           menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       if (account.getIn(['relationship', 'muting'])) { |       if (account.getIn(['relationship', 'muting'])) { | ||||||
|         menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); |         menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); | ||||||
|       } else { |       } else { | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ export default class Header extends ImmutablePureComponent { | ||||||
|     onFollow: PropTypes.func.isRequired, |     onFollow: PropTypes.func.isRequired, | ||||||
|     onBlock: PropTypes.func.isRequired, |     onBlock: PropTypes.func.isRequired, | ||||||
|     onMention: PropTypes.func.isRequired, |     onMention: PropTypes.func.isRequired, | ||||||
|  |     onReblogToggle: PropTypes.func.isRequired, | ||||||
|     onReport: PropTypes.func.isRequired, |     onReport: PropTypes.func.isRequired, | ||||||
|     onMute: PropTypes.func.isRequired, |     onMute: PropTypes.func.isRequired, | ||||||
|     onBlockDomain: PropTypes.func.isRequired, |     onBlockDomain: PropTypes.func.isRequired, | ||||||
|  | @ -40,6 +41,10 @@ export default class Header extends ImmutablePureComponent { | ||||||
|     this.props.onReport(this.props.account); |     this.props.onReport(this.props.account); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   handleReblogToggle = () => { | ||||||
|  |     this.props.onReblogToggle(this.props.account); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   handleMute = () => { |   handleMute = () => { | ||||||
|     this.props.onMute(this.props.account); |     this.props.onMute(this.props.account); | ||||||
|   } |   } | ||||||
|  | @ -80,6 +85,7 @@ export default class Header extends ImmutablePureComponent { | ||||||
|           me={me} |           me={me} | ||||||
|           onBlock={this.handleBlock} |           onBlock={this.handleBlock} | ||||||
|           onMention={this.handleMention} |           onMention={this.handleMention} | ||||||
|  |           onReblogToggle={this.handleReblogToggle} | ||||||
|           onReport={this.handleReport} |           onReport={this.handleReport} | ||||||
|           onMute={this.handleMute} |           onMute={this.handleMute} | ||||||
|           onBlockDomain={this.handleBlockDomain} |           onBlockDomain={this.handleBlockDomain} | ||||||
|  |  | ||||||
|  | @ -68,6 +68,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | ||||||
|     dispatch(mentionCompose(account, router)); |     dispatch(mentionCompose(account, router)); | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|  |   onReblogToggle (account) { | ||||||
|  |     if (account.getIn(['relationship', 'following', 'reblogs'])) { | ||||||
|  |       dispatch(followAccount(account.get('id'), false)); | ||||||
|  |     } else { | ||||||
|  |       dispatch(followAccount(account.get('id'), true)); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|   onReport (account) { |   onReport (account) { | ||||||
|     dispatch(initReport(account)); |     dispatch(initReport(account)); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  | @ -126,6 +126,7 @@ export default function accountsCounters(state = initialState, action) { | ||||||
|   case STATUS_FETCH_SUCCESS: |   case STATUS_FETCH_SUCCESS: | ||||||
|     return normalizeAccountFromStatus(state, action.status); |     return normalizeAccountFromStatus(state, action.status); | ||||||
|   case ACCOUNT_FOLLOW_SUCCESS: |   case ACCOUNT_FOLLOW_SUCCESS: | ||||||
|  |     if (action.alreadyFollowing) { return state; } | ||||||
|     return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); |     return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); | ||||||
|   case ACCOUNT_UNFOLLOW_SUCCESS: |   case ACCOUNT_UNFOLLOW_SUCCESS: | ||||||
|     return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1)); |     return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1)); | ||||||
|  |  | ||||||
|  | @ -160,7 +160,9 @@ class FeedManager | ||||||
|       should_filter &&= status.account_id != status.in_reply_to_account_id                                               # and it's not a self-reply |       should_filter &&= status.account_id != status.in_reply_to_account_id                                               # and it's not a self-reply | ||||||
|       return should_filter |       return should_filter | ||||||
|     elsif status.reblog?                                                                                                 # Filter out a reblog |     elsif status.reblog?                                                                                                 # Filter out a reblog | ||||||
|       should_filter   = Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists?        # or if the author of the reblogged status is blocking me |       src_id = status.account_id | ||||||
|  |       should_filter   = Follow.where(account_id: receiver_id, target_account_id: src_id, show_reblogs: false).exists?    # if the reblogger's reblogs are suppressed | ||||||
|  |       should_filter ||= Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists?        # or if the author of the reblogged status is blocking me | ||||||
|       should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists?  # or the author's domain is blocked |       should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists?  # or the author's domain is blocked | ||||||
|       return should_filter |       return should_filter | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -5,7 +5,11 @@ module AccountInteractions | ||||||
| 
 | 
 | ||||||
|   class_methods do |   class_methods do | ||||||
|     def following_map(target_account_ids, account_id) |     def following_map(target_account_ids, account_id) | ||||||
|       follow_mapping(Follow.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) |       Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping| | ||||||
|  |         mapping[follow.target_account_id] = { | ||||||
|  |           reblogs: follow.show_reblogs? | ||||||
|  |         } | ||||||
|  |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def followed_by_map(target_account_ids, account_id) |     def followed_by_map(target_account_ids, account_id) | ||||||
|  | @ -25,7 +29,11 @@ module AccountInteractions | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def requested_map(target_account_ids, account_id) |     def requested_map(target_account_ids, account_id) | ||||||
|       follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) |       FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping| | ||||||
|  |         mapping[follow_request.target_account_id] = { | ||||||
|  |           reblogs: follow_request.show_reblogs? | ||||||
|  |         } | ||||||
|  |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def domain_blocking_map(target_account_ids, account_id) |     def domain_blocking_map(target_account_ids, account_id) | ||||||
|  | @ -66,8 +74,12 @@ module AccountInteractions | ||||||
|     has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy |     has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def follow!(other_account) |   def follow!(other_account, reblogs: nil) | ||||||
|     active_relationships.find_or_create_by!(target_account: other_account) |     reblogs = true if reblogs.nil? | ||||||
|  |     rel = active_relationships.create_with(show_reblogs: reblogs).find_or_create_by!(target_account: other_account) | ||||||
|  |     rel.update!(show_reblogs: reblogs) | ||||||
|  | 
 | ||||||
|  |     rel | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def block!(other_account) |   def block!(other_account) | ||||||
|  | @ -141,6 +153,10 @@ module AccountInteractions | ||||||
|     mute_relationships.where(target_account: other_account, hide_notifications: true).exists? |     mute_relationships.where(target_account: other_account, hide_notifications: true).exists? | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def muting_reblogs?(other_account) | ||||||
|  |     active_relationships.where(target_account: other_account, show_reblogs: false).exists? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def requested?(other_account) |   def requested?(other_account) | ||||||
|     follow_requests.where(target_account: other_account).exists? |     follow_requests.where(target_account: other_account).exists? | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ | ||||||
| #  account_id        :integer          not null | #  account_id        :integer          not null | ||||||
| #  id                :integer          not null, primary key | #  id                :integer          not null, primary key | ||||||
| #  target_account_id :integer          not null | #  target_account_id :integer          not null | ||||||
|  | #  show_reblogs      :boolean          default(TRUE), not null | ||||||
| # | # | ||||||
| 
 | 
 | ||||||
| class Follow < ApplicationRecord | class Follow < ApplicationRecord | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ | ||||||
| #  account_id        :integer          not null | #  account_id        :integer          not null | ||||||
| #  id                :integer          not null, primary key | #  id                :integer          not null, primary key | ||||||
| #  target_account_id :integer          not null | #  target_account_id :integer          not null | ||||||
|  | #  show_reblogs      :boolean          default(TRUE), not null | ||||||
| # | # | ||||||
| 
 | 
 | ||||||
| class FollowRequest < ApplicationRecord | class FollowRequest < ApplicationRecord | ||||||
|  | @ -21,7 +22,7 @@ class FollowRequest < ApplicationRecord | ||||||
|   validates :account_id, uniqueness: { scope: :target_account_id } |   validates :account_id, uniqueness: { scope: :target_account_id } | ||||||
| 
 | 
 | ||||||
|   def authorize! |   def authorize! | ||||||
|     account.follow!(target_account) |     account.follow!(target_account, reblogs: show_reblogs) | ||||||
|     MergeWorker.perform_async(target_account.id, account.id) |     MergeWorker.perform_async(target_account.id, account.id) | ||||||
| 
 | 
 | ||||||
|     destroy! |     destroy! | ||||||
|  |  | ||||||
|  | @ -6,25 +6,38 @@ class FollowService < BaseService | ||||||
|   # Follow a remote user, notify remote user about the follow |   # Follow a remote user, notify remote user about the follow | ||||||
|   # @param [Account] source_account From which to follow |   # @param [Account] source_account From which to follow | ||||||
|   # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) |   # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) | ||||||
|   def call(source_account, uri) |   # @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true | ||||||
|  |   def call(source_account, uri, reblogs: nil) | ||||||
|  |     reblogs = true if reblogs.nil? | ||||||
|     target_account = uri.is_a?(Account) ? uri : ResolveRemoteAccountService.new.call(uri) |     target_account = uri.is_a?(Account) ? uri : ResolveRemoteAccountService.new.call(uri) | ||||||
| 
 | 
 | ||||||
|     raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? |     raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? | ||||||
|     raise Mastodon::NotPermittedError  if target_account.blocking?(source_account) || source_account.blocking?(target_account) |     raise Mastodon::NotPermittedError  if target_account.blocking?(source_account) || source_account.blocking?(target_account) | ||||||
| 
 | 
 | ||||||
|     return if source_account.following?(target_account) || source_account.requested?(target_account) |     if source_account.following?(target_account) | ||||||
|  |       # We're already following this account, but we'll call follow! again to | ||||||
|  |       # make sure the reblogs status is set correctly. | ||||||
|  |       source_account.follow!(target_account, reblogs: reblogs) | ||||||
|  |       return | ||||||
|  |     elsif source_account.requested?(target_account) | ||||||
|  |       # This isn't managed by a method in AccountInteractions, so we modify it | ||||||
|  |       # ourselves if necessary. | ||||||
|  |       req = follow_requests.find_by(target_account: other_account) | ||||||
|  |       req.update!(show_reblogs: reblogs) | ||||||
|  |       return | ||||||
|  |     end | ||||||
| 
 | 
 | ||||||
|     if target_account.locked? || target_account.activitypub? |     if target_account.locked? || target_account.activitypub? | ||||||
|       request_follow(source_account, target_account) |       request_follow(source_account, target_account, reblogs: reblogs) | ||||||
|     else |     else | ||||||
|       direct_follow(source_account, target_account) |       direct_follow(source_account, target_account, reblogs: reblogs) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def request_follow(source_account, target_account) |   def request_follow(source_account, target_account, reblogs: true) | ||||||
|     follow_request = FollowRequest.create!(account: source_account, target_account: target_account) |     follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs) | ||||||
| 
 | 
 | ||||||
|     if target_account.local? |     if target_account.local? | ||||||
|       NotifyService.new.call(target_account, follow_request) |       NotifyService.new.call(target_account, follow_request) | ||||||
|  | @ -38,8 +51,8 @@ class FollowService < BaseService | ||||||
|     follow_request |     follow_request | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def direct_follow(source_account, target_account) |   def direct_follow(source_account, target_account, reblogs: true) | ||||||
|     follow = source_account.follow!(target_account) |     follow = source_account.follow!(target_account, reblogs: reblogs) | ||||||
| 
 | 
 | ||||||
|     if target_account.local? |     if target_account.local? | ||||||
|       NotifyService.new.call(target_account, follow) |       NotifyService.new.call(target_account, follow) | ||||||
|  |  | ||||||
|  | @ -29,7 +29,7 @@ class NotifyService < BaseService | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def blocked_reblog? |   def blocked_reblog? | ||||||
|     false |     @recipient.muting_reblogs?(@notification.from_account) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def blocked_follow_request? |   def blocked_follow_request? | ||||||
|  |  | ||||||
							
								
								
									
										21
									
								
								db/migrate/20171028221157_add_reblogs_to_follows.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								db/migrate/20171028221157_add_reblogs_to_follows.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | require Rails.root.join('lib', 'mastodon', 'migration_helpers') | ||||||
|  | 
 | ||||||
|  | class AddReblogsToFollows < ActiveRecord::Migration[5.1] | ||||||
|  |   include Mastodon::MigrationHelpers | ||||||
|  | 
 | ||||||
|  |   safety_assured do | ||||||
|  |     disable_ddl_transaction! | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     safety_assured do | ||||||
|  |       add_column_with_default :follows, :show_reblogs, :boolean, default: true, allow_null: false | ||||||
|  |       add_column_with_default :follow_requests, :show_reblogs, :boolean, default: true, allow_null: false | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |    | ||||||
|  |   def down | ||||||
|  |     remove_column :follows, :show_reblogs | ||||||
|  |     remove_column :follow_requests, :show_reblogs | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
| # | # | ||||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||||
| 
 | 
 | ||||||
| ActiveRecord::Schema.define(version: 20171021191900) do | ActiveRecord::Schema.define(version: 20171028221157) do | ||||||
| 
 | 
 | ||||||
|   # These are extensions that must be enabled in order to support this database |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "plpgsql" |   enable_extension "plpgsql" | ||||||
|  | @ -145,6 +145,7 @@ ActiveRecord::Schema.define(version: 20171021191900) do | ||||||
|     t.datetime "updated_at", null: false |     t.datetime "updated_at", null: false | ||||||
|     t.bigint "account_id", null: false |     t.bigint "account_id", null: false | ||||||
|     t.bigint "target_account_id", null: false |     t.bigint "target_account_id", null: false | ||||||
|  |     t.boolean "show_reblogs", default: true, null: false | ||||||
|     t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true |     t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -153,6 +154,7 @@ ActiveRecord::Schema.define(version: 20171021191900) do | ||||||
|     t.datetime "updated_at", null: false |     t.datetime "updated_at", null: false | ||||||
|     t.bigint "account_id", null: false |     t.bigint "account_id", null: false | ||||||
|     t.bigint "target_account_id", null: false |     t.bigint "target_account_id", null: false | ||||||
|  |     t.boolean "show_reblogs", default: true, null: false | ||||||
|     t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true |     t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ describe Api::V1::Accounts::RelationshipsController do | ||||||
|         json = body_as_json |         json = body_as_json | ||||||
| 
 | 
 | ||||||
|         expect(json).to be_a Enumerable |         expect(json).to be_a Enumerable | ||||||
|         expect(json.first[:following]).to be true |         expect(json.first[:following]).to be_truthy | ||||||
|         expect(json.first[:followed_by]).to be false |         expect(json.first[:followed_by]).to be false | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | @ -51,7 +51,7 @@ describe Api::V1::Accounts::RelationshipsController do | ||||||
| 
 | 
 | ||||||
|         expect(json).to be_a Enumerable |         expect(json).to be_a Enumerable | ||||||
|         expect(json.first[:id]).to eq simon.id.to_s |         expect(json.first[:id]).to eq simon.id.to_s | ||||||
|         expect(json.first[:following]).to be true |         expect(json.first[:following]).to be_truthy | ||||||
|         expect(json.first[:followed_by]).to be false |         expect(json.first[:followed_by]).to be false | ||||||
|         expect(json.first[:muting]).to be false |         expect(json.first[:muting]).to be false | ||||||
|         expect(json.first[:requested]).to be false |         expect(json.first[:requested]).to be false | ||||||
|  |  | ||||||
|  | @ -31,10 +31,10 @@ RSpec.describe Api::V1::AccountsController, type: :controller do | ||||||
|         expect(response).to have_http_status(:success) |         expect(response).to have_http_status(:success) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'returns JSON with following=true and requested=false' do |       it 'returns JSON with following=truthy and requested=false' do | ||||||
|         json = body_as_json |         json = body_as_json | ||||||
| 
 | 
 | ||||||
|         expect(json[:following]).to be true |         expect(json[:following]).to be_truthy | ||||||
|         expect(json[:requested]).to be false |         expect(json[:requested]).to be false | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -50,11 +50,11 @@ RSpec.describe Api::V1::AccountsController, type: :controller do | ||||||
|         expect(response).to have_http_status(:success) |         expect(response).to have_http_status(:success) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'returns JSON with following=false and requested=true' do |       it 'returns JSON with following=false and requested=truthy' do | ||||||
|         json = body_as_json |         json = body_as_json | ||||||
| 
 | 
 | ||||||
|         expect(json[:following]).to be false |         expect(json[:following]).to be false | ||||||
|         expect(json[:requested]).to be true |         expect(json[:requested]).to be_truthy | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'creates a follow request relation between user and target user' do |       it 'creates a follow request relation between user and target user' do | ||||||
|  |  | ||||||
|  | @ -56,6 +56,13 @@ RSpec.describe FeedManager do | ||||||
|         expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true |         expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       it 'returns true for reblog from account with reblogs disabled' do | ||||||
|  |         status = Fabricate(:status, text: 'Hello world', account: jeff) | ||||||
|  |         reblog = Fabricate(:status, reblog: status, account: alice) | ||||||
|  |         bob.follow!(alice, reblogs: false) | ||||||
|  |         expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       it 'returns false for reply by followee to another followee' do |       it 'returns false for reply by followee to another followee' do | ||||||
|         status = Fabricate(:status, text: 'Hello world', account: jeff) |         status = Fabricate(:status, text: 'Hello world', account: jeff) | ||||||
|         reply  = Fabricate(:status, text: 'Nay', thread: status, account: alice) |         reply  = Fabricate(:status, text: 'Nay', thread: status, account: alice) | ||||||
|  |  | ||||||
|  | @ -37,4 +37,41 @@ describe AccountInteractions do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe 'ignoring reblogs from an account' do | ||||||
|  |     before do | ||||||
|  |       @me = Fabricate(:account, username: 'Me') | ||||||
|  |       @you = Fabricate(:account, username: 'You') | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with the reblogs option unspecified' do | ||||||
|  |       before do | ||||||
|  |         @me.follow!(@you) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'defaults to showing reblogs' do | ||||||
|  |         expect(@me.muting_reblogs?(@you)).to be(false) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with the reblogs option set to false' do | ||||||
|  |       before do | ||||||
|  |         @me.follow!(@you, reblogs: false) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'does mute reblogs' do | ||||||
|  |         expect(@me.muting_reblogs?(@you)).to be(true) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with the reblogs option set to true' do | ||||||
|  |       before do | ||||||
|  |         @me.follow!(@you, reblogs: true) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'does not mute reblogs' do | ||||||
|  |         expect(@me.muting_reblogs?(@you)).to be(false) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -1,7 +1,29 @@ | ||||||
| require 'rails_helper' | require 'rails_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe FollowRequest, type: :model do | RSpec.describe FollowRequest, type: :model do | ||||||
|   describe '#authorize!' |   describe '#authorize!' do | ||||||
|  |     it 'generates a Follow' do | ||||||
|  |       follow_request = Fabricate.create(:follow_request) | ||||||
|  |       follow_request.authorize! | ||||||
|  |       target = follow_request.target_account | ||||||
|  |       expect(follow_request.account.following?(target)).to be true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'correctly passes show_reblogs when true' do | ||||||
|  |       follow_request = Fabricate.create(:follow_request, show_reblogs: true) | ||||||
|  |       follow_request.authorize! | ||||||
|  |       target = follow_request.target_account | ||||||
|  |       expect(follow_request.account.muting_reblogs?(target)).to be false | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'correctly passes show_reblogs when false' do | ||||||
|  |       follow_request = Fabricate.create(:follow_request, show_reblogs: false) | ||||||
|  |       follow_request.authorize! | ||||||
|  |       target = follow_request.target_account | ||||||
|  |       expect(follow_request.account.muting_reblogs?(target)).to be true | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#reject!' |   describe '#reject!' | ||||||
| 
 | 
 | ||||||
|   describe 'validations' do |   describe 'validations' do | ||||||
|  |  | ||||||
|  | @ -13,8 +13,20 @@ RSpec.describe FollowService do | ||||||
|         subject.call(sender, bob.acct) |         subject.call(sender, bob.acct) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'creates a follow request' do |       it 'creates a follow request with reblogs' do | ||||||
|         expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil |         expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: true)).to_not be_nil | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     describe 'locked account, no reblogs' do | ||||||
|  |       let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         subject.call(sender, bob.acct, reblogs: false) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates a follow request without reblogs' do | ||||||
|  |         expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: false)).to_not be_nil | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -25,8 +37,22 @@ RSpec.describe FollowService do | ||||||
|         subject.call(sender, bob.acct) |         subject.call(sender, bob.acct) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'creates a following relation' do |       it 'creates a following relation with reblogs' do | ||||||
|         expect(sender.following?(bob)).to be true |         expect(sender.following?(bob)).to be true | ||||||
|  |         expect(sender.muting_reblogs?(bob)).to be false | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     describe 'unlocked account, no reblogs' do | ||||||
|  |       let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         subject.call(sender, bob.acct, reblogs: false) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates a following relation without reblogs' do | ||||||
|  |         expect(sender.following?(bob)).to be true | ||||||
|  |         expect(sender.muting_reblogs?(bob)).to be true | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -42,6 +68,32 @@ RSpec.describe FollowService do | ||||||
|         expect(sender.following?(bob)).to be true |         expect(sender.following?(bob)).to be true | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     describe 'already followed account, turning reblogs off' do | ||||||
|  |       let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         sender.follow!(bob, reblogs: true) | ||||||
|  |         subject.call(sender, bob.acct, reblogs: false) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'disables reblogs' do | ||||||
|  |         expect(sender.muting_reblogs?(bob)).to be true | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     describe 'already followed account, turning reblogs on' do | ||||||
|  |       let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         sender.follow!(bob, reblogs: false) | ||||||
|  |         subject.call(sender, bob.acct, reblogs: true) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'disables reblogs' do | ||||||
|  |         expect(sender.muting_reblogs?(bob)).to be false | ||||||
|  |       end | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   context 'remote OStatus account' do |   context 'remote OStatus account' do | ||||||
|  |  | ||||||
|  | @ -48,6 +48,26 @@ RSpec.describe NotifyService do | ||||||
|     is_expected.to_not change(Notification, :count) |     is_expected.to_not change(Notification, :count) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe 'reblogs' do | ||||||
|  |     let(:status)   { Fabricate(:status, account: Fabricate(:account)) } | ||||||
|  |     let(:activity) { Fabricate(:status, account: sender, reblog: status) } | ||||||
|  | 
 | ||||||
|  |     it 'shows reblogs by default' do | ||||||
|  |       recipient.follow!(sender) | ||||||
|  |       is_expected.to change(Notification, :count) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'shows reblogs when explicitly enabled' do | ||||||
|  |       recipient.follow!(sender, reblogs: true) | ||||||
|  |       is_expected.to change(Notification, :count) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'hides reblogs when disabled' do | ||||||
|  |       recipient.follow!(sender, reblogs: false) | ||||||
|  |       is_expected.to_not change(Notification, :count) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   context do |   context do | ||||||
|     let(:asshole)  { Fabricate(:account, username: 'asshole') } |     let(:asshole)  { Fabricate(:account, username: 'asshole') } | ||||||
|     let(:reply_to) { Fabricate(:status, account: asshole) } |     let(:reply_to) { Fabricate(:status, account: asshole) } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue