Mute button progress so far. WIP, doesn't entirely work correctly.
This commit is contained in:
		
							parent
							
								
									89fc2d7f48
								
							
						
					
					
						commit
						442fdbfc53
					
				
					 26 changed files with 390 additions and 33 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue