Mute button progress so far. WIP, doesn't entirely work correctly.
This commit is contained in:
		| @@ -21,6 +21,14 @@ export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST'; | ||||
| export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS'; | ||||
| export const ACCOUNT_UNBLOCK_FAIL    = 'ACCOUNT_UNBLOCK_FAIL'; | ||||
|  | ||||
| export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST'; | ||||
| export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS'; | ||||
| export const ACCOUNT_MUTE_FAIL    = 'ACCOUNT_MUTE_FAIL'; | ||||
|  | ||||
| export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST'; | ||||
| export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS'; | ||||
| export const ACCOUNT_UNMUTE_FAIL    = 'ACCOUNT_UNMUTE_FAIL'; | ||||
|  | ||||
| export const ACCOUNT_TIMELINE_FETCH_REQUEST = 'ACCOUNT_TIMELINE_FETCH_REQUEST'; | ||||
| export const ACCOUNT_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_TIMELINE_FETCH_SUCCESS'; | ||||
| export const ACCOUNT_TIMELINE_FETCH_FAIL    = 'ACCOUNT_TIMELINE_FETCH_FAIL'; | ||||
| @@ -328,6 +336,76 @@ export function unblockAccountFail(error) { | ||||
|   }; | ||||
| }; | ||||
|  | ||||
|  | ||||
| export function muteAccount(id) { | ||||
|   return (dispatch, getState) => { | ||||
|     dispatch(muteAccountRequest(id)); | ||||
|  | ||||
|     api(getState).post(`/api/v1/accounts/${id}/mute`).then(response => { | ||||
|       // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers | ||||
|       dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); | ||||
|     }).catch(error => { | ||||
|       dispatch(muteAccountFail(id, error)); | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function unmuteAccount(id) { | ||||
|   return (dispatch, getState) => { | ||||
|     dispatch(unmuteAccountRequest(id)); | ||||
|  | ||||
|     api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => { | ||||
|       dispatch(unmuteAccountSuccess(response.data)); | ||||
|     }).catch(error => { | ||||
|       dispatch(unmuteAccountFail(id, error)); | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function muteAccountRequest(id) { | ||||
|   return { | ||||
|     type: ACCOUNT_MUTE_REQUEST, | ||||
|     id | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function muteAccountSuccess(relationship, statuses) { | ||||
|   return { | ||||
|     type: ACCOUNT_MUTE_SUCCESS, | ||||
|     relationship, | ||||
|     statuses | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function muteAccountFail(error) { | ||||
|   return { | ||||
|     type: ACCOUNT_MUTE_FAIL, | ||||
|     error | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function unmuteAccountRequest(id) { | ||||
|   return { | ||||
|     type: ACCOUNT_UNMUTE_REQUEST, | ||||
|     id | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function unmuteAccountSuccess(relationship) { | ||||
|   return { | ||||
|     type: ACCOUNT_UNMUTE_SUCCESS, | ||||
|     relationship | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function unmuteAccountFail(error) { | ||||
|   return { | ||||
|     type: ACCOUNT_UNMUTE_FAIL, | ||||
|     error | ||||
|   }; | ||||
| }; | ||||
|  | ||||
|  | ||||
| export function fetchFollowers(id) { | ||||
|   return (dispatch, getState) => { | ||||
|     dispatch(fetchFollowersRequest(id)); | ||||
|   | ||||
| @@ -5,7 +5,9 @@ import { | ||||
|   followAccount, | ||||
|   unfollowAccount, | ||||
|   blockAccount, | ||||
|   unblockAccount | ||||
|   unblockAccount, | ||||
|   muteAccount, | ||||
|   unmuteAccount, | ||||
| } from '../actions/accounts'; | ||||
|  | ||||
| const makeMapStateToProps = () => { | ||||
| @@ -34,6 +36,14 @@ const mapDispatchToProps = (dispatch) => ({ | ||||
|     } else { | ||||
|       dispatch(blockAccount(account.get('id'))); | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   onMute (account) { | ||||
|     if (account.getIn(['relationship', 'muting'])) { | ||||
|       dispatch(unmuteAccount(account.get('id'))); | ||||
|     } else { | ||||
|       dispatch(muteAccount(account.get('id'))); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,10 @@ import { | ||||
|   unreblog, | ||||
|   unfavourite | ||||
| } from '../actions/interactions'; | ||||
| import { blockAccount } from '../actions/accounts'; | ||||
| import { | ||||
|   blockAccount, | ||||
|   muteAccount | ||||
| } from '../actions/accounts'; | ||||
| import { deleteStatus } from '../actions/statuses'; | ||||
| import { initReport } from '../actions/reports'; | ||||
| import { openMedia } from '../actions/modal'; | ||||
| @@ -69,7 +72,11 @@ const mapDispatchToProps = (dispatch) => ({ | ||||
|  | ||||
|   onReport (status) { | ||||
|     dispatch(initReport(status.get('account'), status)); | ||||
|   } | ||||
|   }, | ||||
|  | ||||
|   onMute (account) { | ||||
|     dispatch(muteAccount(account.get('id'))); | ||||
|   }, | ||||
|  | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,9 @@ const messages = defineMessages({ | ||||
|   edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, | ||||
|   unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, | ||||
|   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, | ||||
|   unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }, | ||||
|   block: { id: 'account.block', defaultMessage: 'Block @{name}' }, | ||||
|   mute: { id: 'account.mute', defaultMessage: 'Mute' }, | ||||
|   follow: { id: 'account.follow', defaultMessage: 'Follow' }, | ||||
|   report: { id: 'account.report', defaultMessage: 'Report @{name}' }, | ||||
|   disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' } | ||||
| @@ -35,6 +37,7 @@ const ActionBar = React.createClass({ | ||||
|     onBlock: React.PropTypes.func.isRequired, | ||||
|     onMention: React.PropTypes.func.isRequired, | ||||
|     onReport: React.PropTypes.func.isRequired, | ||||
|     onMute: React.PropTypes.func.isRequired, | ||||
|     intl: React.PropTypes.object.isRequired | ||||
|   }, | ||||
|  | ||||
| @@ -67,6 +70,12 @@ const ActionBar = React.createClass({ | ||||
|       extraInfo = <abbr title={intl.formatMessage(messages.disclaimer)}>*</abbr>; | ||||
|     } | ||||
|  | ||||
|     if (account.getIn(['relationship', 'muting'])) { | ||||
|       menu.push({ text: intl.formatMessage(messages.unmute), action: this.props.onMute }); | ||||
|     } else { | ||||
|       menu.push({ text: intl.formatMessage(messages.mute), action: this.props.onMute }); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <div className='account__action-bar'> | ||||
|         <div style={outerDropdownStyle}> | ||||
|   | ||||
| @@ -16,6 +16,7 @@ const Header = React.createClass({ | ||||
|     onBlock: React.PropTypes.func.isRequired, | ||||
|     onMention: React.PropTypes.func.isRequired, | ||||
|     onReport: React.PropTypes.func.isRequired | ||||
|     onMute: React.PropTypes.func.isRequired, | ||||
|   }, | ||||
|  | ||||
|   mixins: [PureRenderMixin], | ||||
| @@ -37,6 +38,10 @@ const Header = React.createClass({ | ||||
|     this.context.router.push('/report'); | ||||
|   }, | ||||
|  | ||||
|   handleMute() { | ||||
|     this.props.onMute(this.props.account); | ||||
|   }, | ||||
|  | ||||
|   render () { | ||||
|     const { account, me } = this.props; | ||||
|  | ||||
| @@ -58,6 +63,7 @@ const Header = React.createClass({ | ||||
|           onBlock={this.handleBlock} | ||||
|           onMention={this.handleMention} | ||||
|           onReport={this.handleReport} | ||||
|           onMute={this.handleMute} | ||||
|         /> | ||||
|       </div> | ||||
|     ); | ||||
|   | ||||
| @@ -5,7 +5,9 @@ import { | ||||
|   followAccount, | ||||
|   unfollowAccount, | ||||
|   blockAccount, | ||||
|   unblockAccount | ||||
|   unblockAccount, | ||||
|   muteAccount, | ||||
|   unmuteAccount | ||||
| } from '../../../actions/accounts'; | ||||
| import { mentionCompose } from '../../../actions/compose'; | ||||
| import { initReport } from '../../../actions/reports'; | ||||
| @@ -44,6 +46,14 @@ const mapDispatchToProps = dispatch => ({ | ||||
|  | ||||
|   onReport (account) { | ||||
|     dispatch(initReport(account)); | ||||
|   }, | ||||
|  | ||||
|   onMute (account) { | ||||
|     if (account.getIn(['relationship', 'muting'])) { | ||||
|       dispatch(unmuteAccount(account.get('id'))); | ||||
|     } else { | ||||
|       dispatch(muteAccount(account.get('id'))); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,8 @@ import { | ||||
|   ACCOUNT_UNFOLLOW_SUCCESS, | ||||
|   ACCOUNT_BLOCK_SUCCESS, | ||||
|   ACCOUNT_UNBLOCK_SUCCESS, | ||||
|   ACCOUNT_MUTE_SUCCESS, | ||||
|   ACCOUNT_UNMUTE_SUCCESS, | ||||
|   RELATIONSHIPS_FETCH_SUCCESS | ||||
| } from '../actions/accounts'; | ||||
| import Immutable from 'immutable'; | ||||
| @@ -25,6 +27,8 @@ export default function relationships(state = initialState, action) { | ||||
|     case ACCOUNT_UNFOLLOW_SUCCESS: | ||||
|     case ACCOUNT_BLOCK_SUCCESS: | ||||
|     case ACCOUNT_UNBLOCK_SUCCESS: | ||||
|     case ACCOUNT_MUTE_SUCCESS: | ||||
|     case ACCOUNT_UNMUTE_SUCCESS: | ||||
|       return normalizeRelationship(state, action.relationship); | ||||
|     case RELATIONSHIPS_FETCH_SUCCESS: | ||||
|       return normalizeRelationships(state, action.relationships); | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Api::V1::AccountsController < ApiController | ||||
|   before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock] | ||||
|   before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock] | ||||
|   before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute] | ||||
|   before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute] | ||||
|   before_action :require_user!, except: [:show, :following, :followers, :statuses] | ||||
|   before_action :set_account, except: [:verify_credentials, :suggestions, :search] | ||||
|  | ||||
| @@ -86,10 +86,17 @@ class Api::V1::AccountsController < ApiController | ||||
|     @followed_by = { @account.id => false } | ||||
|     @blocking    = { @account.id => true } | ||||
|     @requested   = { @account.id => false } | ||||
|     @muting      = { @account.id => current_user.account.muting?(@account.id) } | ||||
|  | ||||
|     render action: :relationship | ||||
|   end | ||||
|  | ||||
|   def mute | ||||
|     MuteService.new.call(current_user.account, @account) | ||||
|     set_relationship | ||||
|     render action: :relationship | ||||
|   end | ||||
|  | ||||
|   def unfollow | ||||
|     UnfollowService.new.call(current_user.account, @account) | ||||
|     set_relationship | ||||
| @@ -102,6 +109,12 @@ class Api::V1::AccountsController < ApiController | ||||
|     render action: :relationship | ||||
|   end | ||||
|  | ||||
|   def unmute | ||||
|     UnmuteService.new.call(current_user.account, @account) | ||||
|     set_relationship | ||||
|     render action: :relationship | ||||
|   end | ||||
|  | ||||
|   def relationships | ||||
|     ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i] | ||||
|  | ||||
| @@ -109,6 +122,7 @@ class Api::V1::AccountsController < ApiController | ||||
|     @following   = Account.following_map(ids, current_user.account_id) | ||||
|     @followed_by = Account.followed_by_map(ids, current_user.account_id) | ||||
|     @blocking    = Account.blocking_map(ids, current_user.account_id) | ||||
|     @muting      = Account.muting_map(ids, current_user.account_id) | ||||
|     @requested   = Account.requested_map(ids, current_user.account_id) | ||||
|   end | ||||
|  | ||||
| @@ -130,6 +144,7 @@ class Api::V1::AccountsController < ApiController | ||||
|     @following   = Account.following_map([@account.id], current_user.account_id) | ||||
|     @followed_by = Account.followed_by_map([@account.id], current_user.account_id) | ||||
|     @blocking    = Account.blocking_map([@account.id], current_user.account_id) | ||||
|     @muting      = Account.muting_map([@account.id], current_user.account_id) | ||||
|     @requested   = Account.requested_map([@account.id], current_user.account_id) | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										21
									
								
								app/controllers/api/v1/mutes_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/controllers/api/v1/mutes_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Api::V1::MutesController < ApiController | ||||
|   before_action -> { doorkeeper_authorize! :follow } | ||||
|   before_action :require_user! | ||||
|  | ||||
|   respond_to :json | ||||
|  | ||||
|   def index | ||||
|     results   = Mute.where(account: current_account).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) | ||||
|     accounts  = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h | ||||
|     @accounts = results.map { |f| accounts[f.target_account_id] } | ||||
|  | ||||
|     set_account_counters_maps(@accounts) | ||||
|  | ||||
|     next_path = api_v1_mutes_url(max_id: results.last.id)    if results.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) | ||||
|     prev_path = api_v1_mutes_url(since_id: results.first.id) unless results.empty? | ||||
|  | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|   end | ||||
| end | ||||
| @@ -22,18 +22,8 @@ class FeedManager | ||||
|   end | ||||
|  | ||||
|   def push(timeline_type, account, status) | ||||
|     timeline_key = key(timeline_type, account.id) | ||||
|  | ||||
|     if status.reblog? | ||||
|       # If the original status is within 40 statuses from top, do not re-insert it into the feed | ||||
|       rank = redis.zrevrank(timeline_key, status.reblog_of_id) | ||||
|       return if !rank.nil? && rank < 40 | ||||
|       redis.zadd(timeline_key, status.id, status.reblog_of_id) | ||||
|     else | ||||
|       redis.zadd(timeline_key, status.id, status.id) | ||||
|       trim(timeline_type, account.id) | ||||
|     end | ||||
|  | ||||
|     redis.zadd(key(timeline_type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) | ||||
|     trim(timeline_type, account.id) | ||||
|     broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status)) | ||||
|   end | ||||
|  | ||||
| @@ -95,31 +85,47 @@ class FeedManager | ||||
|   end | ||||
|  | ||||
|   def filter_from_home?(status, receiver) | ||||
|     should_filter = false | ||||
|     should_filter = receiver.muting?(status.account_id)                       # Filter if I'm muting this person | ||||
|  | ||||
|     if status.reply? && status.in_reply_to_id.nil? | ||||
|     if status.reply? && status.in_reply_to_id.nil?                            # Filter out replies to nobody | ||||
|       should_filter = true | ||||
|     elsif status.reply? && !status.in_reply_to_account_id.nil?                # Filter out if it's a reply | ||||
|       should_filter   = !receiver.following?(status.in_reply_to_account)      # and I'm not following the person it's a reply to | ||||
|     elsif status.reply? && !status.in_reply_to_account_id.nil?                # If it's a reply | ||||
|       should_filter   = !receiver.following?(status.in_reply_to_account)      # filter if I'm not following the person it's a reply to | ||||
|       should_filter &&= !(receiver.id == status.in_reply_to_account_id)       # and it's not a reply to me | ||||
|       should_filter &&= !(status.account_id == status.in_reply_to_account_id) # and it's not a self-reply | ||||
|     elsif status.reblog?                                                      # Filter out a reblog | ||||
|       should_filter = receiver.blocking?(status.reblog.account)               # if I'm blocking the reblogged person | ||||
|     elsif status.reblog?                                                      # If it's a reblog | ||||
|       should_filter   = receiver.blocking?(status.reblog.account)             # filter if I'm blocking the reblogged person | ||||
|       should_filter ||= receiver.muting?(status.reblog.account)               # or if I'm muting the reblogged person | ||||
|     end | ||||
|  | ||||
|     should_filter ||= receiver.blocking?(status.mentions.map(&:account_id))   # or if it mentions someone I blocked | ||||
|  | ||||
|     should_filter ||= receiver.blocking?(status.mentions.map(&:account_id))   # Filter if it mentions someone I blocked | ||||
|     should_filter | ||||
|   end | ||||
|  | ||||
|   def filter_from_mentions?(status, receiver) | ||||
|     should_filter   = receiver.id == status.account_id                                      # Filter if I'm mentioning myself | ||||
|     should_filter   = receiver.id == status.account_id                                      # Filter out if I'm mentioning myself | ||||
|     should_filter ||= receiver.blocking?(status.account)                                    # or it's from someone I blocked | ||||
|     should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked | ||||
|     should_filter ||= (status.account.silenced? && !receiver.following?(status.account))    # of if the account is silenced and I'm not following them | ||||
|  | ||||
|     if status.reply? && !status.in_reply_to_account_id.nil? | ||||
|       should_filter ||= receiver.blocking?(status.in_reply_to_account)                      # or if it's a reply to a user I blocked | ||||
|     end | ||||
|  | ||||
|     should_filter | ||||
|   end | ||||
|  | ||||
|   def filter_from_public?(status, receiver) | ||||
|     should_filter   = receiver.blocking?(status.account)                                    # Filter out if I'm blocking that account | ||||
|     should_filter ||= receiver.muting?(status.account_id)                                   # or if I'm muting this person | ||||
|     should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked | ||||
|  | ||||
|     if status.reply? && !status.in_reply_to_account_id.nil?                                 # or it's a reply | ||||
|       should_filter ||= receiver.blocking?(status.in_reply_to_account)                      # to a user I blocked | ||||
|       should_filter ||= receiver.blocking?(status.in_reply_to_account)                      # to somebody I've blocked | ||||
|       should_filter ||= receiver.muting?(status.in_reply_to_account)                        # or to somebody I'm muting | ||||
|     elsif status.reblog?                                                                    # or if it's a reblog | ||||
|       should_filter ||= receiver.blocking?(status.reblog.account)                           # if I'm blocking the reblogged person | ||||
|       should_filter ||= receiver.muting?(status.reblog.account)                             # or if I'm muting the reblogged person | ||||
|     end | ||||
|  | ||||
|     should_filter | ||||
|   | ||||
| @@ -46,6 +46,10 @@ class Account < ApplicationRecord | ||||
|   has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy | ||||
|   has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account | ||||
|  | ||||
|   # Mute relationships | ||||
|   has_many :mute_relationships, class_name: 'Mute', foreign_key: 'account_id', dependent: :destroy | ||||
|   has_many :muting, -> { order('mutes.id desc') }, through: :mute_relationships, source: :target_account | ||||
|  | ||||
|   # Media | ||||
|   has_many :media_attachments, dependent: :destroy | ||||
|  | ||||
| @@ -73,6 +77,10 @@ class Account < ApplicationRecord | ||||
|     block_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) | ||||
|   end | ||||
|  | ||||
|   def mute!(other_account) | ||||
|     mute_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) | ||||
|   end | ||||
|  | ||||
|   def unfollow!(other_account) | ||||
|     follow = active_relationships.find_by(target_account: other_account) | ||||
|     follow&.destroy | ||||
| @@ -83,6 +91,11 @@ class Account < ApplicationRecord | ||||
|     block&.destroy | ||||
|   end | ||||
|  | ||||
|   def unmute!(other_account) | ||||
|     mute = mute_relationships.find_by(target_account: other_account) | ||||
|     mute&.destroy | ||||
|   end | ||||
|  | ||||
|   def following?(other_account) | ||||
|     following.include?(other_account) | ||||
|   end | ||||
| @@ -91,6 +104,10 @@ class Account < ApplicationRecord | ||||
|     blocking.include?(other_account) | ||||
|   end | ||||
|  | ||||
|   def muting?(other_account) | ||||
|     muting.include?(other_account) | ||||
|   end | ||||
|  | ||||
|   def requested?(other_account) | ||||
|     follow_requests.where(target_account: other_account).exists? | ||||
|   end | ||||
| @@ -188,6 +205,10 @@ class Account < ApplicationRecord | ||||
|       follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) | ||||
|     end | ||||
|  | ||||
|     def muting_map(target_account_ids, account_id) | ||||
|       follow_mapping(Mute.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) | ||||
|     end | ||||
|  | ||||
|     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) | ||||
|     end | ||||
|   | ||||
							
								
								
									
										32
									
								
								app/models/mute.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/models/mute.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Mute < ApplicationRecord | ||||
|   include Paginable | ||||
|   include Streamable | ||||
|  | ||||
|   belongs_to :account | ||||
|   belongs_to :target_account, class_name: 'Account' | ||||
|  | ||||
|   validates :account, :target_account, presence: true | ||||
|   validates :account_id, uniqueness: { scope: :target_account_id } | ||||
|  | ||||
|   def verb | ||||
|     destroyed? ? :unmute : :mute | ||||
|   end | ||||
|  | ||||
|   def target | ||||
|     target_account | ||||
|   end | ||||
|  | ||||
|   def object_type | ||||
|     :person | ||||
|   end | ||||
|  | ||||
|   def hidden? | ||||
|     true | ||||
|   end | ||||
|  | ||||
|   def title | ||||
|     destroyed? ? "#{account.acct} is no longer muting #{target_account.acct}" : "#{account.acct} muted #{target_account.acct}" | ||||
|   end | ||||
| end | ||||
| @@ -103,7 +103,10 @@ class Status < ApplicationRecord | ||||
|  | ||||
|   class << self | ||||
|     def as_home_timeline(account) | ||||
|       where(account: [account] + account.following) | ||||
|       muted = Mute.where(account: account).pluck(:target_account_id) | ||||
|       query = where(account: [account] + account.following) | ||||
|       query = query.where('statuses.account_id NOT IN (?)', muted) unless muted.empty? | ||||
|       query | ||||
|     end | ||||
|  | ||||
|     def as_public_timeline(account = nil, local_only = false) | ||||
| @@ -169,8 +172,10 @@ class Status < ApplicationRecord | ||||
|  | ||||
|     def filter_timeline(query, account) | ||||
|       blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) | ||||
|       query   = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? | ||||
|       query   = query.where('accounts.silenced = TRUE') if account.silenced? | ||||
|       muted   = Mute.where(account: account).pluck(:target_account_id) | ||||
|       query   = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty?  # Only give us statuses from people we haven't blocked | ||||
|       query   = query.where('statuses.account_id NOT IN (?)', muted) unless muted.empty?      # and out of those, only people we haven't muted | ||||
|       query   = query.where('accounts.silenced = TRUE') if account.silenced?                  # and if we're hellbanned, only people who are also hellbanned | ||||
|       query | ||||
|     end | ||||
|  | ||||
| @@ -192,6 +197,6 @@ class Status < ApplicationRecord | ||||
|   private | ||||
|  | ||||
|   def filter_from_context?(status, account) | ||||
|     account&.blocking?(status.account_id) || (status.account.silenced? && !account&.following?(status.account_id)) || !status.permitted?(account) | ||||
|     account&.blocking?(status.account_id) || account&.muting?(status.account_id) || (status.account.silenced? && !account&.following?(status.account_id)) || !status.permitted?(account) | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										23
									
								
								app/services/mute_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/services/mute_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class MuteService < BaseService | ||||
|   def call(account, target_account) | ||||
|     return if account.id == target_account.id | ||||
|     clear_home_timeline(account, target_account) | ||||
|     account.mute!(target_account) | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def clear_home_timeline(account, target_account) | ||||
|     home_key = FeedManager.instance.key(:home, account.id) | ||||
|  | ||||
|     target_account.statuses.select('id').find_each do |status| | ||||
|       redis.zrem(home_key, status.id) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def redis | ||||
|     Redis.current | ||||
|   end | ||||
| end | ||||
							
								
								
									
										9
									
								
								app/services/unmute_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/services/unmute_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class UnmuteService < BaseService | ||||
|   def call(account, target_account) | ||||
|     return unless account.muting?(target_account) | ||||
|  | ||||
|     account.unmute!(target_account) | ||||
|   end | ||||
| end | ||||
| @@ -4,4 +4,5 @@ attribute :id | ||||
| node(:following)   { |account| @following[account.id]   || false } | ||||
| node(:followed_by) { |account| @followed_by[account.id] || false } | ||||
| node(:blocking)    { |account| @blocking[account.id]    || false } | ||||
| node(:muting)      { |account| @muting[account.id]      || false } | ||||
| node(:requested)   { |account| @requested[account.id]   || false } | ||||
|   | ||||
							
								
								
									
										2
									
								
								app/views/api/v1/mutes/index.rabl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/views/api/v1/mutes/index.rabl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| collection @accounts | ||||
| extends 'api/v1/accounts/show' | ||||
| @@ -127,6 +127,7 @@ Rails.application.routes.draw do | ||||
|       resources :media,      only: [:create] | ||||
|       resources :apps,       only: [:create] | ||||
|       resources :blocks,     only: [:index] | ||||
|       resources :mutes,      only: [:index] | ||||
|       resources :favourites, only: [:index] | ||||
|       resources :reports,    only: [:index, :create] | ||||
|  | ||||
| @@ -160,6 +161,8 @@ Rails.application.routes.draw do | ||||
|           post :unfollow | ||||
|           post :block | ||||
|           post :unblock | ||||
|           post :mute | ||||
|           post :unmute | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   | ||||
							
								
								
									
										12
									
								
								db/migrate/20170301222600_create_mutes.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								db/migrate/20170301222600_create_mutes.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| class CreateMutes < ActiveRecord::Migration[5.0] | ||||
|   def change | ||||
|     create_table :mutes do |t| | ||||
|       t.integer :account_id, null: false | ||||
|       t.integer :target_account_id, null: false | ||||
|       t.timestamps null: false | ||||
|     end | ||||
|  | ||||
|     add_index :mutes, [:account_id, :target_account_id], unique: true | ||||
|  | ||||
|   end | ||||
| end | ||||
							
								
								
									
										10
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								db/schema.rb
									
									
									
									
									
								
							| @@ -10,7 +10,7 @@ | ||||
| # | ||||
| # It's strongly recommended that you check this file into your version control system. | ||||
|  | ||||
| ActiveRecord::Schema.define(version: 20170217012631) do | ||||
| ActiveRecord::Schema.define(version: 20170301222600) do | ||||
|  | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "plpgsql" | ||||
| @@ -110,6 +110,14 @@ ActiveRecord::Schema.define(version: 20170217012631) do | ||||
|     t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree | ||||
|   end | ||||
|  | ||||
|   create_table "mutes", force: :cascade do |t| | ||||
|     t.integer  "account_id",        null: false | ||||
|     t.integer  "target_account_id", null: false | ||||
|     t.datetime "created_at",        null: false | ||||
|     t.datetime "updated_at",        null: false | ||||
|     t.index ["account_id", "target_account_id"], name: "index_mutes_on_account_id_and_target_account_id", unique: true, using: :btree | ||||
|   end | ||||
|  | ||||
|   create_table "notifications", force: :cascade do |t| | ||||
|     t.integer  "account_id" | ||||
|     t.integer  "activity_id" | ||||
|   | ||||
| @@ -116,6 +116,44 @@ RSpec.describe Api::V1::AccountsController, type: :controller do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'POST #mute' do | ||||
|     let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } | ||||
|  | ||||
|     before do | ||||
|       user.account.follow!(other_account) | ||||
|       post :mute, params: {id: other_account.id } | ||||
|     end | ||||
|  | ||||
|     it 'returns http success' do | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
|  | ||||
|     it 'does not remove the following relation between user and target user' do | ||||
|       expect(user.account.following?(other_account)).to be true | ||||
|     end | ||||
|  | ||||
|     it 'creates a muting relation' do | ||||
|       expect(user.account.muting?(other_account)).to be true | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'POST #unmute' do | ||||
|     let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } | ||||
|  | ||||
|     before do | ||||
|       user.account.mute!(other_account) | ||||
|       post :unmute, params: { id: other_account.id } | ||||
|     end | ||||
|  | ||||
|     it 'returns http success' do | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
|  | ||||
|     it 'removes the muting relation between user and target user' do | ||||
|       expect(user.account.muting?(other_account)).to be false | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'GET #relationships' do | ||||
|     let(:simon) { Fabricate(:user, email: 'simon@example.com', account: Fabricate(:account, username: 'simon')).account } | ||||
|     let(:lewis) { Fabricate(:user, email: 'lewis@example.com', account: Fabricate(:account, username: 'lewis')).account } | ||||
|   | ||||
							
								
								
									
										19
									
								
								spec/controllers/api/v1/mutes_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								spec/controllers/api/v1/mutes_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe Api::V1::MutesController, type: :controller do | ||||
|   render_views | ||||
|  | ||||
|   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } | ||||
|   let(:token) { double acceptable?: true, resource_owner_id: user.id } | ||||
|  | ||||
|   before do | ||||
|     allow(controller).to receive(:doorkeeper_token) { token } | ||||
|   end | ||||
|  | ||||
|   describe 'GET #index' do | ||||
|     it 'returns http success' do | ||||
|       get :index | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										3
									
								
								spec/fabricators/mute_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								spec/fabricators/mute_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| Fabricator(:mute) do | ||||
|  | ||||
| end | ||||
							
								
								
									
										5
									
								
								spec/models/mute_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/models/mute_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe Mute, type: :model do | ||||
|  | ||||
| end | ||||
							
								
								
									
										5
									
								
								spec/services/mute_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/services/mute_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe MuteService do | ||||
|   subject { MuteService.new } | ||||
| end | ||||
							
								
								
									
										5
									
								
								spec/services/unmute_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/services/unmute_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe UnmuteService do | ||||
|   subject { UnmuteService.new } | ||||
| end | ||||
		Reference in New Issue
	
	Block a user