Add notification grouping for follow notifications (#32085)
This commit is contained in:
		
							parent
							
								
									3dc4ddc663
								
							
						
					
					
						commit
						d6f5ee75ab
					
				| @ -68,10 +68,15 @@ function dispatchAssociatedRecords( | |||||||
|     dispatch(importFetchedStatuses(fetchedStatuses)); |     dispatch(importFetchedStatuses(fetchedStatuses)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const supportedGroupedNotificationTypes = ['favourite', 'reblog']; | ||||||
|  | 
 | ||||||
| export const fetchNotifications = createDataLoadingThunk( | export const fetchNotifications = createDataLoadingThunk( | ||||||
|   'notificationGroups/fetch', |   'notificationGroups/fetch', | ||||||
|   async (_params, { getState }) => |   async (_params, { getState }) => | ||||||
|     apiFetchNotificationGroups({ exclude_types: getExcludedTypes(getState()) }), |     apiFetchNotificationGroups({ | ||||||
|  |       grouped_types: supportedGroupedNotificationTypes, | ||||||
|  |       exclude_types: getExcludedTypes(getState()), | ||||||
|  |     }), | ||||||
|   ({ notifications, accounts, statuses }, { dispatch }) => { |   ({ notifications, accounts, statuses }, { dispatch }) => { | ||||||
|     dispatch(importFetchedAccounts(accounts)); |     dispatch(importFetchedAccounts(accounts)); | ||||||
|     dispatch(importFetchedStatuses(statuses)); |     dispatch(importFetchedStatuses(statuses)); | ||||||
| @ -93,6 +98,7 @@ export const fetchNotificationsGap = createDataLoadingThunk( | |||||||
|   'notificationGroups/fetchGap', |   'notificationGroups/fetchGap', | ||||||
|   async (params: { gap: NotificationGap }, { getState }) => |   async (params: { gap: NotificationGap }, { getState }) => | ||||||
|     apiFetchNotificationGroups({ |     apiFetchNotificationGroups({ | ||||||
|  |       grouped_types: supportedGroupedNotificationTypes, | ||||||
|       max_id: params.gap.maxId, |       max_id: params.gap.maxId, | ||||||
|       exclude_types: getExcludedTypes(getState()), |       exclude_types: getExcludedTypes(getState()), | ||||||
|     }), |     }), | ||||||
| @ -109,6 +115,7 @@ export const pollRecentNotifications = createDataLoadingThunk( | |||||||
|   'notificationGroups/pollRecentNotifications', |   'notificationGroups/pollRecentNotifications', | ||||||
|   async (_params, { getState }) => { |   async (_params, { getState }) => { | ||||||
|     return apiFetchNotificationGroups({ |     return apiFetchNotificationGroups({ | ||||||
|  |       grouped_types: supportedGroupedNotificationTypes, | ||||||
|       max_id: undefined, |       max_id: undefined, | ||||||
|       exclude_types: getExcludedTypes(getState()), |       exclude_types: getExcludedTypes(getState()), | ||||||
|       // In slow mode, we don't want to include notifications that duplicate the already-displayed ones
 |       // In slow mode, we don't want to include notifications that duplicate the already-displayed ones
 | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ export const apiFetchNotifications = async ( | |||||||
| 
 | 
 | ||||||
| export const apiFetchNotificationGroups = async (params?: { | export const apiFetchNotificationGroups = async (params?: { | ||||||
|   url?: string; |   url?: string; | ||||||
|  |   grouped_types?: string[]; | ||||||
|   exclude_types?: string[]; |   exclude_types?: string[]; | ||||||
|   max_id?: string; |   max_id?: string; | ||||||
|   since_id?: string; |   since_id?: string; | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ class Notification < ApplicationRecord | |||||||
|   self.inheritance_column = nil |   self.inheritance_column = nil | ||||||
| 
 | 
 | ||||||
|   include Paginable |   include Paginable | ||||||
|  |   include Redisable | ||||||
| 
 | 
 | ||||||
|   LEGACY_TYPE_CLASS_MAP = { |   LEGACY_TYPE_CLASS_MAP = { | ||||||
|     'Mention' => :mention, |     'Mention' => :mention, | ||||||
| @ -30,7 +31,9 @@ class Notification < ApplicationRecord | |||||||
|     'Poll' => :poll, |     'Poll' => :poll, | ||||||
|   }.freeze |   }.freeze | ||||||
| 
 | 
 | ||||||
|   GROUPABLE_NOTIFICATION_TYPES = %i(favourite reblog).freeze |   # `set_group_key!` needs to be updated if this list changes | ||||||
|  |   GROUPABLE_NOTIFICATION_TYPES = %i(favourite reblog follow).freeze | ||||||
|  |   MAXIMUM_GROUP_SPAN_HOURS = 12 | ||||||
| 
 | 
 | ||||||
|   # Please update app/javascript/api_types/notification.ts if you change this |   # Please update app/javascript/api_types/notification.ts if you change this | ||||||
|   PROPERTIES = { |   PROPERTIES = { | ||||||
| @ -123,6 +126,30 @@ class Notification < ApplicationRecord | |||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def set_group_key! | ||||||
|  |     return if filtered? || Notification::GROUPABLE_NOTIFICATION_TYPES.exclude?(type) | ||||||
|  | 
 | ||||||
|  |     type_prefix = case type | ||||||
|  |                   when :favourite, :reblog | ||||||
|  |                     [type, target_status&.id].join('-') | ||||||
|  |                   when :follow | ||||||
|  |                     type | ||||||
|  |                   else | ||||||
|  |                     raise NotImplementedError | ||||||
|  |                   end | ||||||
|  |     redis_key   = "notif-group/#{account.id}/#{type_prefix}" | ||||||
|  |     hour_bucket = activity.created_at.utc.to_i / 1.hour.to_i | ||||||
|  | 
 | ||||||
|  |     # Reuse previous group if it does not span too large an amount of time | ||||||
|  |     previous_bucket = redis.get(redis_key).to_i | ||||||
|  |     hour_bucket = previous_bucket if hour_bucket < previous_bucket + MAXIMUM_GROUP_SPAN_HOURS | ||||||
|  | 
 | ||||||
|  |     # We do not concern ourselves with race conditions since we use hour buckets | ||||||
|  |     redis.set(redis_key, hour_bucket, ex: MAXIMUM_GROUP_SPAN_HOURS.hours.to_i) | ||||||
|  | 
 | ||||||
|  |     self.group_key = "#{type_prefix}-#{hour_bucket}" | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   class << self |   class << self | ||||||
|     def browserable(types: [], exclude_types: [], from_account_id: nil, include_filtered: false) |     def browserable(types: [], exclude_types: [], from_account_id: nil, include_filtered: false) | ||||||
|       requested_types = if types.empty? |       requested_types = if types.empty? | ||||||
|  | |||||||
| @ -3,8 +3,6 @@ | |||||||
| class NotifyService < BaseService | class NotifyService < BaseService | ||||||
|   include Redisable |   include Redisable | ||||||
| 
 | 
 | ||||||
|   MAXIMUM_GROUP_SPAN_HOURS = 12 |  | ||||||
| 
 |  | ||||||
|   # TODO: the severed_relationships type probably warrants email notifications |   # TODO: the severed_relationships type probably warrants email notifications | ||||||
|   NON_EMAIL_TYPES = %i( |   NON_EMAIL_TYPES = %i( | ||||||
|     admin.report |     admin.report | ||||||
| @ -216,7 +214,7 @@ class NotifyService < BaseService | |||||||
|     return if drop? |     return if drop? | ||||||
| 
 | 
 | ||||||
|     @notification.filtered = filter? |     @notification.filtered = filter? | ||||||
|     @notification.group_key = notification_group_key |     @notification.set_group_key! | ||||||
|     @notification.save! |     @notification.save! | ||||||
| 
 | 
 | ||||||
|     # It's possible the underlying activity has been deleted |     # It's possible the underlying activity has been deleted | ||||||
| @ -236,23 +234,6 @@ class NotifyService < BaseService | |||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def notification_group_key |  | ||||||
|     return nil if @notification.filtered || Notification::GROUPABLE_NOTIFICATION_TYPES.exclude?(@notification.type) |  | ||||||
| 
 |  | ||||||
|     type_prefix = "#{@notification.type}-#{@notification.target_status.id}" |  | ||||||
|     redis_key   = "notif-group/#{@recipient.id}/#{type_prefix}" |  | ||||||
|     hour_bucket = @notification.activity.created_at.utc.to_i / 1.hour.to_i |  | ||||||
| 
 |  | ||||||
|     # Reuse previous group if it does not span too large an amount of time |  | ||||||
|     previous_bucket = redis.get(redis_key).to_i |  | ||||||
|     hour_bucket = previous_bucket if hour_bucket < previous_bucket + MAXIMUM_GROUP_SPAN_HOURS |  | ||||||
| 
 |  | ||||||
|     # We do not concern ourselves with race conditions since we use hour buckets |  | ||||||
|     redis.set(redis_key, hour_bucket, ex: MAXIMUM_GROUP_SPAN_HOURS.hours.to_i) |  | ||||||
| 
 |  | ||||||
|     "#{type_prefix}-#{hour_bucket}" |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def drop? |   def drop? | ||||||
|     DropCondition.new(@notification).drop? |     DropCondition.new(@notification).drop? | ||||||
|   end |   end | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user