API param to exclude notification types from response (#1341)
* Add exclude_types param to /api/v1/notifications * Exclude notification types in web UI through exclude_types in the API
This commit is contained in:
		
							parent
							
								
									0687ab8ae3
								
							
						
					
					
						commit
						2810013b93
					
				
					 6 changed files with 100 additions and 17 deletions
				
			
		
							
								
								
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							| 
						 | 
					@ -68,6 +68,7 @@ end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
group :test do
 | 
					group :test do
 | 
				
			||||||
  gem 'faker'
 | 
					  gem 'faker'
 | 
				
			||||||
 | 
					  gem 'rails-controller-testing'
 | 
				
			||||||
  gem 'rspec-sidekiq'
 | 
					  gem 'rspec-sidekiq'
 | 
				
			||||||
  gem 'simplecov', require: false
 | 
					  gem 'simplecov', require: false
 | 
				
			||||||
  gem 'webmock'
 | 
					  gem 'webmock'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -286,6 +286,10 @@ GEM
 | 
				
			||||||
      bundler (>= 1.3.0, < 2.0)
 | 
					      bundler (>= 1.3.0, < 2.0)
 | 
				
			||||||
      railties (= 5.0.2)
 | 
					      railties (= 5.0.2)
 | 
				
			||||||
      sprockets-rails (>= 2.0.0)
 | 
					      sprockets-rails (>= 2.0.0)
 | 
				
			||||||
 | 
					    rails-controller-testing (1.0.1)
 | 
				
			||||||
 | 
					      actionpack (~> 5.x)
 | 
				
			||||||
 | 
					      actionview (~> 5.x)
 | 
				
			||||||
 | 
					      activesupport (~> 5.x)
 | 
				
			||||||
    rails-dom-testing (2.0.2)
 | 
					    rails-dom-testing (2.0.2)
 | 
				
			||||||
      activesupport (>= 4.2.0, < 6.0)
 | 
					      activesupport (>= 4.2.0, < 6.0)
 | 
				
			||||||
      nokogiri (~> 1.6)
 | 
					      nokogiri (~> 1.6)
 | 
				
			||||||
| 
						 | 
					@ -487,6 +491,7 @@ DEPENDENCIES
 | 
				
			||||||
  rack-cors
 | 
					  rack-cors
 | 
				
			||||||
  rack-timeout
 | 
					  rack-timeout
 | 
				
			||||||
  rails (~> 5.0.2)
 | 
					  rails (~> 5.0.2)
 | 
				
			||||||
 | 
					  rails-controller-testing
 | 
				
			||||||
  rails-settings-cached
 | 
					  rails-settings-cached
 | 
				
			||||||
  rails_12factor
 | 
					  rails_12factor
 | 
				
			||||||
  react-rails
 | 
					  react-rails
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,6 +61,8 @@ export function refreshNotifications() {
 | 
				
			||||||
      params.since_id = ids.first().get('id');
 | 
					      params.since_id = ids.first().get('id');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api(getState).get('/api/v1/notifications', { params }).then(response => {
 | 
					    api(getState).get('/api/v1/notifications', { params }).then(response => {
 | 
				
			||||||
      const next = getLinks(response).refs.find(link => link.rel === 'next');
 | 
					      const next = getLinks(response).refs.find(link => link.rel === 'next');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,11 +107,11 @@ export function expandNotifications() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch(expandNotificationsRequest());
 | 
					    dispatch(expandNotificationsRequest());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api(getState).get(url, {
 | 
					    const params = {};
 | 
				
			||||||
      params: {
 | 
					
 | 
				
			||||||
        limit: 5
 | 
					    params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
 | 
				
			||||||
      }
 | 
					
 | 
				
			||||||
    }).then(response => {
 | 
					    api(getState).get(url, params).then(response => {
 | 
				
			||||||
      const next = getLinks(response).refs.find(link => link.rel === 'next');
 | 
					      const next = getLinks(response).refs.find(link => link.rel === 'next');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
 | 
					      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ class Api::V1::NotificationsController < ApiController
 | 
				
			||||||
  DEFAULT_NOTIFICATIONS_LIMIT = 15
 | 
					  DEFAULT_NOTIFICATIONS_LIMIT = 15
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
    @notifications = Notification.where(account: current_account).browserable.paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id])
 | 
					    @notifications = Notification.where(account: current_account).browserable(exclude_types).paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id])
 | 
				
			||||||
    @notifications = cache_collection(@notifications, Notification)
 | 
					    @notifications = cache_collection(@notifications, Notification)
 | 
				
			||||||
    statuses       = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
 | 
					    statuses       = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,7 +32,13 @@ class Api::V1::NotificationsController < ApiController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def exclude_types
 | 
				
			||||||
 | 
					    val = params.permit(exclude_types: [])[:exclude_types] || []
 | 
				
			||||||
 | 
					    val = [val] unless val.is_a?(Enumerable)
 | 
				
			||||||
 | 
					    val
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def pagination_params(core_params)
 | 
					  def pagination_params(core_params)
 | 
				
			||||||
    params.permit(:limit).merge(core_params)
 | 
					    params.permit(:limit, exclude_types: []).merge(core_params)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,10 +16,17 @@ class Notification < ApplicationRecord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
 | 
					  validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TYPE_CLASS_MAP = {
 | 
				
			||||||
 | 
					    mention:        'Mention',
 | 
				
			||||||
 | 
					    reblog:         'Status',
 | 
				
			||||||
 | 
					    follow:         'Follow',
 | 
				
			||||||
 | 
					    follow_request: 'FollowRequest',
 | 
				
			||||||
 | 
					    favourite:      'Favourite',
 | 
				
			||||||
 | 
					  }.freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze
 | 
					  STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) }
 | 
					  scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) }
 | 
				
			||||||
  scope :browserable, -> { where.not(activity_type: ['FollowRequest']) }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account
 | 
					  cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,12 +35,7 @@ class Notification < ApplicationRecord
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def type
 | 
					  def type
 | 
				
			||||||
    case activity_type
 | 
					    @type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
 | 
				
			||||||
    when 'Status'
 | 
					 | 
				
			||||||
      :reblog
 | 
					 | 
				
			||||||
    else
 | 
					 | 
				
			||||||
      activity_type.underscore.to_sym
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def target_status
 | 
					  def target_status
 | 
				
			||||||
| 
						 | 
					@ -50,6 +52,11 @@ class Notification < ApplicationRecord
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  class << self
 | 
					  class << self
 | 
				
			||||||
 | 
					    def browserable(types = [])
 | 
				
			||||||
 | 
					      types.concat([:follow_request])
 | 
				
			||||||
 | 
					      where.not(activity_type: activity_types_from_types(types))
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reload_stale_associations!(cached_items)
 | 
					    def reload_stale_associations!(cached_items)
 | 
				
			||||||
      account_ids = cached_items.map(&:from_account_id).uniq
 | 
					      account_ids = cached_items.map(&:from_account_id).uniq
 | 
				
			||||||
      accounts    = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
 | 
					      accounts    = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
 | 
				
			||||||
| 
						 | 
					@ -58,6 +65,12 @@ class Notification < ApplicationRecord
 | 
				
			||||||
        item.from_account = accounts[item.from_account_id]
 | 
					        item.from_account = accounts[item.from_account_id]
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def activity_types_from_types(types)
 | 
				
			||||||
 | 
					      types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  after_initialize :set_from_account
 | 
					  after_initialize :set_from_account
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,15 +5,71 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
 | 
					  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
 | 
				
			||||||
  let(:token) { double acceptable?: true, resource_owner_id: user.id }
 | 
					  let(:token) { double acceptable?: true, resource_owner_id: user.id }
 | 
				
			||||||
 | 
					  let(:other) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before do
 | 
					  before do
 | 
				
			||||||
    allow(controller).to receive(:doorkeeper_token) { token }
 | 
					    allow(controller).to receive(:doorkeeper_token) { token }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'GET #index' do
 | 
					  describe 'GET #index' do
 | 
				
			||||||
    it 'returns http success' do
 | 
					    before do
 | 
				
			||||||
      get :index
 | 
					      status     = PostStatusService.new.call(user.account, 'Test')
 | 
				
			||||||
      expect(response).to have_http_status(:success)
 | 
					      @reblog    = ReblogService.new.call(other.account, status)
 | 
				
			||||||
 | 
					      @mention   = PostStatusService.new.call(other.account, 'Hello @alice')
 | 
				
			||||||
 | 
					      @favourite = FavouriteService.new.call(other.account, status)
 | 
				
			||||||
 | 
					      @follow    = FollowService.new.call(other.account, 'alice')
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe 'with no options' do
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        get :index
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'returns http success' do
 | 
				
			||||||
 | 
					        expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'includes reblog' do
 | 
				
			||||||
 | 
					        expect(assigns(:notifications).map(&:activity_id)).to include(@reblog.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'includes mention' do
 | 
				
			||||||
 | 
					        expect(assigns(:notifications).map(&:activity_id)).to include(@mention.mentions.first.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'includes favourite' do
 | 
				
			||||||
 | 
					        expect(assigns(:notifications).map(&:activity_id)).to include(@favourite.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'includes follow' do
 | 
				
			||||||
 | 
					        expect(assigns(:notifications).map(&:activity_id)).to include(@follow.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe 'with excluded mentions' do
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        get :index, params: { exclude_types: ['mention'] }
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'returns http success' do
 | 
				
			||||||
 | 
					        expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'includes reblog' do
 | 
				
			||||||
 | 
					        expect(assigns(:notifications).map(&:activity_id)).to include(@reblog.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'excludes mention' do
 | 
				
			||||||
 | 
					        expect(assigns(:notifications).map(&:activity_id)).to_not include(@mention.mentions.first.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'includes favourite' do
 | 
				
			||||||
 | 
					        expect(assigns(:notifications).map(&:activity_id)).to include(@favourite.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'includes follow' do
 | 
				
			||||||
 | 
					        expect(assigns(:notifications).map(&:activity_id)).to include(@follow.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue