commit: 4c0e9f85c5c9e3be91f9a76139fab3b55abbf363
parent: 89fc2d7f4810ecdf66b17543f4603c1089a0c3f5
Author: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 2 Mar 2017 18:50:33 +0100
Merge branch 'KitRedgrave-add-mute-button'
Diffstat:
29 files changed, 352 insertions(+), 22 deletions(-)
diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx
@@ -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));
diff --git a/app/assets/javascripts/components/containers/account_container.jsx b/app/assets/javascripts/components/containers/account_container.jsx
@@ -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')));
+ }
}
});
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx
@@ -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')));
+ },
});
diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx
@@ -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 @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
+ mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
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
},
@@ -51,15 +54,19 @@ const ActionBar = React.createClass({
if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
- } else if (account.getIn(['relationship', 'blocking'])) {
- menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
- } else if (account.getIn(['relationship', 'following'])) {
- menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
} else {
- menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
- }
+ if (account.getIn(['relationship', 'muting'])) {
+ menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
+ } else {
+ menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute });
+ }
+
+ if (account.getIn(['relationship', 'blocking'])) {
+ menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
+ } else {
+ menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
+ }
- if (account.get('id') !== me) {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
}
diff --git a/app/assets/javascripts/components/features/account_timeline/components/header.jsx b/app/assets/javascripts/components/features/account_timeline/components/header.jsx
@@ -15,7 +15,8 @@ const Header = React.createClass({
onFollow: React.PropTypes.func.isRequired,
onBlock: 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],
@@ -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>
);
diff --git a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
@@ -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')));
+ }
}
});
diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
@@ -36,7 +36,7 @@ const EmojiPickerDropdown = React.createClass({
return (
<Dropdown ref={this.setRef} style={{ marginLeft: '5px' }}>
- <DropdownTrigger className='icon-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, marginTop: '-1px', display: 'block', marginLeft: '2px' }}>
+ <DropdownTrigger className='icon-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, display: 'block', marginLeft: '2px' }}>
<i className={`fa fa-smile-o`} style={{ verticalAlign: 'middle' }} />
</DropdownTrigger>
diff --git a/app/assets/javascripts/components/reducers/relationships.jsx b/app/assets/javascripts/components/reducers/relationships.jsx
@@ -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);
diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx
@@ -22,7 +22,8 @@ import {
ACCOUNT_TIMELINE_EXPAND_REQUEST,
ACCOUNT_TIMELINE_EXPAND_SUCCESS,
ACCOUNT_TIMELINE_EXPAND_FAIL,
- ACCOUNT_BLOCK_SUCCESS
+ ACCOUNT_BLOCK_SUCCESS,
+ ACCOUNT_MUTE_SUCCESS
} from '../actions/accounts';
import {
CONTEXT_FETCH_SUCCESS
@@ -295,6 +296,7 @@ export default function timelines(state = initialState, action) {
case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses));
case ACCOUNT_BLOCK_SUCCESS:
+ case ACCOUNT_MUTE_SUCCESS:
return filterTimelines(state, action.relationship, action.statuses);
case TIMELINE_SCROLL_TOP:
return updateTop(state, action.timeline, action.top);
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb
@@ -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
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
@@ -95,6 +95,8 @@ class FeedManager
end
def filter_from_home?(status, receiver)
+ return true if receiver.muting?(status.account)
+
should_filter = false
if status.reply? && status.in_reply_to_id.nil?
@@ -105,6 +107,7 @@ class FeedManager
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
+ should_filter ||= receiver.muting?(status.reblog.account) # or muting that person
end
should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked
diff --git a/app/models/account.rb b/app/models/account.rb
@@ -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
diff --git a/app/models/mute.rb b/app/models/mute.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Mute < ApplicationRecord
+ include Paginable
+
+ belongs_to :account
+ belongs_to :target_account, class_name: 'Account'
+
+ validates :account, :target_account, presence: true
+ validates :account_id, uniqueness: { scope: :target_account_id }
+end
diff --git a/app/models/status.rb b/app/models/status.rb
@@ -168,9 +168,9 @@ class Status < ApplicationRecord
private
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?
+ blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) + 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, or muted, or that have blocked us
+ query = query.where('accounts.silenced = TRUE') if account.silenced? # and if we're hellbanned, only people who are also hellbanned
query
end
@@ -192,6 +192,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
diff --git a/app/services/mute_service.rb b/app/services/mute_service.rb
@@ -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
diff --git a/app/services/unmute_service.rb b/app/services/unmute_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class UnmuteService < BaseService
+ def call(account, target_account)
+ return unless account.muting?(target_account)
+
+ account.unmute!(target_account)
+
+ MergeWorker.perform_async(target_account.id, account.id) if account.following?(target_account)
+ end
+end
diff --git a/app/views/api/v1/accounts/relationship.rabl b/app/views/api/v1/accounts/relationship.rabl
@@ -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 }
diff --git a/app/views/api/v1/mutes/index.rabl b/app/views/api/v1/mutes/index.rabl
@@ -0,0 +1,2 @@
+collection @accounts
+extends 'api/v1/accounts/show'
diff --git a/config/routes.rb b/config/routes.rb
@@ -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
diff --git a/db/migrate/20170301222600_create_mutes.rb b/db/migrate/20170301222600_create_mutes.rb
@@ -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
diff --git a/db/schema.rb b/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"
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -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 }
diff --git a/spec/controllers/api/v1/mutes_controller_spec.rb b/spec/controllers/api/v1/mutes_controller_spec.rb
@@ -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
diff --git a/spec/fabricators/mute_fabricator.rb b/spec/fabricators/mute_fabricator.rb
@@ -0,0 +1,3 @@
+Fabricator(:mute) do
+
+end
diff --git a/spec/models/mute_spec.rb b/spec/models/mute_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Mute, type: :model do
+
+end
diff --git a/spec/services/mute_service_spec.rb b/spec/services/mute_service_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe MuteService do
+ subject { MuteService.new }
+end
diff --git a/spec/services/unmute_service_spec.rb b/spec/services/unmute_service_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe UnmuteService do
+ subject { UnmuteService.new }
+end
diff --git a/streaming/index.js b/streaming/index.js
@@ -164,7 +164,7 @@ const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false)
const unpackedPayload = JSON.parse(payload)
const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)).concat(unpackedPayload.reblog ? [unpackedPayload.reblog.account.id] : [])
- client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => {
+ client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)}) UNION SELECT target_account_id FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => {
done()
if (err) {