Mute button progress so far. WIP, doesn't entirely work correctly.
This commit is contained in:
		
							parent
							
								
									89fc2d7f48
								
							
						
					
					
						commit
						442fdbfc53
					
				|  | @ -21,6 +21,14 @@ export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST'; | ||||||
| export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS'; | export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS'; | ||||||
| export const ACCOUNT_UNBLOCK_FAIL    = 'ACCOUNT_UNBLOCK_FAIL'; | 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_REQUEST = 'ACCOUNT_TIMELINE_FETCH_REQUEST'; | ||||||
| export const ACCOUNT_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_TIMELINE_FETCH_SUCCESS'; | export const ACCOUNT_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_TIMELINE_FETCH_SUCCESS'; | ||||||
| export const ACCOUNT_TIMELINE_FETCH_FAIL    = 'ACCOUNT_TIMELINE_FETCH_FAIL'; | 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) { | export function fetchFollowers(id) { | ||||||
|   return (dispatch, getState) => { |   return (dispatch, getState) => { | ||||||
|     dispatch(fetchFollowersRequest(id)); |     dispatch(fetchFollowersRequest(id)); | ||||||
|  |  | ||||||
|  | @ -5,7 +5,9 @@ import { | ||||||
|   followAccount, |   followAccount, | ||||||
|   unfollowAccount, |   unfollowAccount, | ||||||
|   blockAccount, |   blockAccount, | ||||||
|   unblockAccount |   unblockAccount, | ||||||
|  |   muteAccount, | ||||||
|  |   unmuteAccount, | ||||||
| } from '../actions/accounts'; | } from '../actions/accounts'; | ||||||
| 
 | 
 | ||||||
| const makeMapStateToProps = () => { | const makeMapStateToProps = () => { | ||||||
|  | @ -34,6 +36,14 @@ const mapDispatchToProps = (dispatch) => ({ | ||||||
|     } else { |     } else { | ||||||
|       dispatch(blockAccount(account.get('id'))); |       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, |   unreblog, | ||||||
|   unfavourite |   unfavourite | ||||||
| } from '../actions/interactions'; | } from '../actions/interactions'; | ||||||
| import { blockAccount } from '../actions/accounts'; | import { | ||||||
|  |   blockAccount, | ||||||
|  |   muteAccount | ||||||
|  | } from '../actions/accounts'; | ||||||
| import { deleteStatus } from '../actions/statuses'; | import { deleteStatus } from '../actions/statuses'; | ||||||
| import { initReport } from '../actions/reports'; | import { initReport } from '../actions/reports'; | ||||||
| import { openMedia } from '../actions/modal'; | import { openMedia } from '../actions/modal'; | ||||||
|  | @ -69,7 +72,11 @@ const mapDispatchToProps = (dispatch) => ({ | ||||||
| 
 | 
 | ||||||
|   onReport (status) { |   onReport (status) { | ||||||
|     dispatch(initReport(status.get('account'), 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' }, |   edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, | ||||||
|   unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, |   unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, | ||||||
|   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, |   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, | ||||||
|  |   unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }, | ||||||
|   block: { id: 'account.block', defaultMessage: 'Block @{name}' }, |   block: { id: 'account.block', defaultMessage: 'Block @{name}' }, | ||||||
|  |   mute: { id: 'account.mute', defaultMessage: 'Mute' }, | ||||||
|   follow: { id: 'account.follow', defaultMessage: 'Follow' }, |   follow: { id: 'account.follow', defaultMessage: 'Follow' }, | ||||||
|   report: { id: 'account.report', defaultMessage: 'Report @{name}' }, |   report: { id: 'account.report', defaultMessage: 'Report @{name}' }, | ||||||
|   disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' } |   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, |     onBlock: React.PropTypes.func.isRequired, | ||||||
|     onMention: React.PropTypes.func.isRequired, |     onMention: React.PropTypes.func.isRequired, | ||||||
|     onReport: React.PropTypes.func.isRequired, |     onReport: React.PropTypes.func.isRequired, | ||||||
|  |     onMute: React.PropTypes.func.isRequired, | ||||||
|     intl: React.PropTypes.object.isRequired |     intl: React.PropTypes.object.isRequired | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|  | @ -67,6 +70,12 @@ const ActionBar = React.createClass({ | ||||||
|       extraInfo = <abbr title={intl.formatMessage(messages.disclaimer)}>*</abbr>; |       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 ( |     return ( | ||||||
|       <div className='account__action-bar'> |       <div className='account__action-bar'> | ||||||
|         <div style={outerDropdownStyle}> |         <div style={outerDropdownStyle}> | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ const Header = React.createClass({ | ||||||
|     onBlock: React.PropTypes.func.isRequired, |     onBlock: React.PropTypes.func.isRequired, | ||||||
|     onMention: React.PropTypes.func.isRequired, |     onMention: React.PropTypes.func.isRequired, | ||||||
|     onReport: React.PropTypes.func.isRequired |     onReport: React.PropTypes.func.isRequired | ||||||
|  |     onMute: React.PropTypes.func.isRequired, | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   mixins: [PureRenderMixin], |   mixins: [PureRenderMixin], | ||||||
|  | @ -37,6 +38,10 @@ const Header = React.createClass({ | ||||||
|     this.context.router.push('/report'); |     this.context.router.push('/report'); | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|  |   handleMute() { | ||||||
|  |     this.props.onMute(this.props.account); | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { account, me } = this.props; |     const { account, me } = this.props; | ||||||
| 
 | 
 | ||||||
|  | @ -58,6 +63,7 @@ const Header = React.createClass({ | ||||||
|           onBlock={this.handleBlock} |           onBlock={this.handleBlock} | ||||||
|           onMention={this.handleMention} |           onMention={this.handleMention} | ||||||
|           onReport={this.handleReport} |           onReport={this.handleReport} | ||||||
|  |           onMute={this.handleMute} | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  | @ -5,7 +5,9 @@ import { | ||||||
|   followAccount, |   followAccount, | ||||||
|   unfollowAccount, |   unfollowAccount, | ||||||
|   blockAccount, |   blockAccount, | ||||||
|   unblockAccount |   unblockAccount, | ||||||
|  |   muteAccount, | ||||||
|  |   unmuteAccount | ||||||
| } from '../../../actions/accounts'; | } from '../../../actions/accounts'; | ||||||
| import { mentionCompose } from '../../../actions/compose'; | import { mentionCompose } from '../../../actions/compose'; | ||||||
| import { initReport } from '../../../actions/reports'; | import { initReport } from '../../../actions/reports'; | ||||||
|  | @ -44,6 +46,14 @@ const mapDispatchToProps = dispatch => ({ | ||||||
| 
 | 
 | ||||||
|   onReport (account) { |   onReport (account) { | ||||||
|     dispatch(initReport(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_UNFOLLOW_SUCCESS, | ||||||
|   ACCOUNT_BLOCK_SUCCESS, |   ACCOUNT_BLOCK_SUCCESS, | ||||||
|   ACCOUNT_UNBLOCK_SUCCESS, |   ACCOUNT_UNBLOCK_SUCCESS, | ||||||
|  |   ACCOUNT_MUTE_SUCCESS, | ||||||
|  |   ACCOUNT_UNMUTE_SUCCESS, | ||||||
|   RELATIONSHIPS_FETCH_SUCCESS |   RELATIONSHIPS_FETCH_SUCCESS | ||||||
| } from '../actions/accounts'; | } from '../actions/accounts'; | ||||||
| import Immutable from 'immutable'; | import Immutable from 'immutable'; | ||||||
|  | @ -25,6 +27,8 @@ export default function relationships(state = initialState, action) { | ||||||
|     case ACCOUNT_UNFOLLOW_SUCCESS: |     case ACCOUNT_UNFOLLOW_SUCCESS: | ||||||
|     case ACCOUNT_BLOCK_SUCCESS: |     case ACCOUNT_BLOCK_SUCCESS: | ||||||
|     case ACCOUNT_UNBLOCK_SUCCESS: |     case ACCOUNT_UNBLOCK_SUCCESS: | ||||||
|  |     case ACCOUNT_MUTE_SUCCESS: | ||||||
|  |     case ACCOUNT_UNMUTE_SUCCESS: | ||||||
|       return normalizeRelationship(state, action.relationship); |       return normalizeRelationship(state, action.relationship); | ||||||
|     case RELATIONSHIPS_FETCH_SUCCESS: |     case RELATIONSHIPS_FETCH_SUCCESS: | ||||||
|       return normalizeRelationships(state, action.relationships); |       return normalizeRelationships(state, action.relationships); | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class Api::V1::AccountsController < ApiController | class Api::V1::AccountsController < ApiController | ||||||
|   before_action -> { doorkeeper_authorize! :read }, except: [: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] |   before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute] | ||||||
|   before_action :require_user!, except: [:show, :following, :followers, :statuses] |   before_action :require_user!, except: [:show, :following, :followers, :statuses] | ||||||
|   before_action :set_account, except: [:verify_credentials, :suggestions, :search] |   before_action :set_account, except: [:verify_credentials, :suggestions, :search] | ||||||
| 
 | 
 | ||||||
|  | @ -86,10 +86,17 @@ class Api::V1::AccountsController < ApiController | ||||||
|     @followed_by = { @account.id => false } |     @followed_by = { @account.id => false } | ||||||
|     @blocking    = { @account.id => true } |     @blocking    = { @account.id => true } | ||||||
|     @requested   = { @account.id => false } |     @requested   = { @account.id => false } | ||||||
|  |     @muting      = { @account.id => current_user.account.muting?(@account.id) } | ||||||
| 
 | 
 | ||||||
|     render action: :relationship |     render action: :relationship | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def mute | ||||||
|  |     MuteService.new.call(current_user.account, @account) | ||||||
|  |     set_relationship | ||||||
|  |     render action: :relationship | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def unfollow |   def unfollow | ||||||
|     UnfollowService.new.call(current_user.account, @account) |     UnfollowService.new.call(current_user.account, @account) | ||||||
|     set_relationship |     set_relationship | ||||||
|  | @ -102,6 +109,12 @@ class Api::V1::AccountsController < ApiController | ||||||
|     render action: :relationship |     render action: :relationship | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def unmute | ||||||
|  |     UnmuteService.new.call(current_user.account, @account) | ||||||
|  |     set_relationship | ||||||
|  |     render action: :relationship | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def relationships |   def relationships | ||||||
|     ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i] |     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) |     @following   = Account.following_map(ids, current_user.account_id) | ||||||
|     @followed_by = Account.followed_by_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) |     @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) |     @requested   = Account.requested_map(ids, current_user.account_id) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -130,6 +144,7 @@ class Api::V1::AccountsController < ApiController | ||||||
|     @following   = Account.following_map([@account.id], current_user.account_id) |     @following   = Account.following_map([@account.id], current_user.account_id) | ||||||
|     @followed_by = Account.followed_by_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) |     @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) |     @requested   = Account.requested_map([@account.id], current_user.account_id) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -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 |   end | ||||||
| 
 | 
 | ||||||
|   def push(timeline_type, account, status) |   def push(timeline_type, account, status) | ||||||
|     timeline_key = key(timeline_type, account.id) |     redis.zadd(key(timeline_type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.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) |     trim(timeline_type, account.id) | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status)) |     broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status)) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -95,31 +85,47 @@ class FeedManager | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def filter_from_home?(status, receiver) |   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 |       should_filter = true | ||||||
|     elsif status.reply? && !status.in_reply_to_account_id.nil?                # Filter out if it's a reply |     elsif status.reply? && !status.in_reply_to_account_id.nil?                # 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 |       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 &&= !(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 |       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 |     elsif status.reblog?                                                      # If it's a reblog | ||||||
|       should_filter = receiver.blocking?(status.reblog.account)               # if I'm blocking the reblogged person |       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 |     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 |     should_filter | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def filter_from_mentions?(status, receiver) |   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.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 ||= 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 |     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 |     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 |     end | ||||||
| 
 | 
 | ||||||
|     should_filter |     should_filter | ||||||
|  |  | ||||||
|  | @ -46,6 +46,10 @@ class Account < ApplicationRecord | ||||||
|   has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy |   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 |   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 |   # Media | ||||||
|   has_many :media_attachments, dependent: :destroy |   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) |     block_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def mute!(other_account) | ||||||
|  |     mute_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def unfollow!(other_account) |   def unfollow!(other_account) | ||||||
|     follow = active_relationships.find_by(target_account: other_account) |     follow = active_relationships.find_by(target_account: other_account) | ||||||
|     follow&.destroy |     follow&.destroy | ||||||
|  | @ -83,6 +91,11 @@ class Account < ApplicationRecord | ||||||
|     block&.destroy |     block&.destroy | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def unmute!(other_account) | ||||||
|  |     mute = mute_relationships.find_by(target_account: other_account) | ||||||
|  |     mute&.destroy | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def following?(other_account) |   def following?(other_account) | ||||||
|     following.include?(other_account) |     following.include?(other_account) | ||||||
|   end |   end | ||||||
|  | @ -91,6 +104,10 @@ class Account < ApplicationRecord | ||||||
|     blocking.include?(other_account) |     blocking.include?(other_account) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def muting?(other_account) | ||||||
|  |     muting.include?(other_account) | ||||||
|  |   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 | ||||||
|  | @ -188,6 +205,10 @@ class Account < ApplicationRecord | ||||||
|       follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) |       follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) | ||||||
|     end |     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) |     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) |       follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -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 |   class << self | ||||||
|     def as_home_timeline(account) |     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 |     end | ||||||
| 
 | 
 | ||||||
|     def as_public_timeline(account = nil, local_only = false) |     def as_public_timeline(account = nil, local_only = false) | ||||||
|  | @ -169,8 +172,10 @@ class Status < ApplicationRecord | ||||||
| 
 | 
 | ||||||
|     def filter_timeline(query, account) |     def filter_timeline(query, account) | ||||||
|       blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) |       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? |       muted   = Mute.where(account: account).pluck(:target_account_id) | ||||||
|       query   = query.where('accounts.silenced = TRUE') if account.silenced? |       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 |       query | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -192,6 +197,6 @@ class Status < ApplicationRecord | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def filter_from_context?(status, account) |   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 | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -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(:following)   { |account| @following[account.id]   || false } | ||||||
| node(:followed_by) { |account| @followed_by[account.id] || false } | node(:followed_by) { |account| @followed_by[account.id] || false } | ||||||
| node(:blocking)    { |account| @blocking[account.id]    || false } | node(:blocking)    { |account| @blocking[account.id]    || false } | ||||||
|  | node(:muting)      { |account| @muting[account.id]      || false } | ||||||
| node(:requested)   { |account| @requested[account.id]   || false } | node(:requested)   { |account| @requested[account.id]   || false } | ||||||
|  |  | ||||||
|  | @ -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 :media,      only: [:create] | ||||||
|       resources :apps,       only: [:create] |       resources :apps,       only: [:create] | ||||||
|       resources :blocks,     only: [:index] |       resources :blocks,     only: [:index] | ||||||
|  |       resources :mutes,      only: [:index] | ||||||
|       resources :favourites, only: [:index] |       resources :favourites, only: [:index] | ||||||
|       resources :reports,    only: [:index, :create] |       resources :reports,    only: [:index, :create] | ||||||
| 
 | 
 | ||||||
|  | @ -160,6 +161,8 @@ Rails.application.routes.draw do | ||||||
|           post :unfollow |           post :unfollow | ||||||
|           post :block |           post :block | ||||||
|           post :unblock |           post :unblock | ||||||
|  |           post :mute | ||||||
|  |           post :unmute | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -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. | # 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 |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "plpgsql" |   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 |     t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree | ||||||
|   end |   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| |   create_table "notifications", force: :cascade do |t| | ||||||
|     t.integer  "account_id" |     t.integer  "account_id" | ||||||
|     t.integer  "activity_id" |     t.integer  "activity_id" | ||||||
|  |  | ||||||
|  | @ -116,6 +116,44 @@ RSpec.describe Api::V1::AccountsController, type: :controller do | ||||||
|     end |     end | ||||||
|   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 |   describe 'GET #relationships' do | ||||||
|     let(:simon) { Fabricate(:user, email: 'simon@example.com', account: Fabricate(:account, username: 'simon')).account } |     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 } |     let(:lewis) { Fabricate(:user, email: 'lewis@example.com', account: Fabricate(:account, username: 'lewis')).account } | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | Fabricator(:mute) do | ||||||
|  | 
 | ||||||
|  | end | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe Mute, type: :model do | ||||||
|  | 
 | ||||||
|  | end | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe MuteService do | ||||||
|  |   subject { MuteService.new } | ||||||
|  | end | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe UnmuteService do | ||||||
|  |   subject { UnmuteService.new } | ||||||
|  | end | ||||||
		Loading…
	
		Reference in New Issue