Add notifications when a reblogged status has been updated (#17404)
* Add notifications when a reblogged status has been updated * Change wording to say "edit" instead of "update" and add missing controls * Replace previous update notifications with the most up-to-date one
This commit is contained in:
		
					parent
					
						
							
								d0fcf07436
							
						
					
				
			
			
				commit
				
					
						8f03b7a2fb
					
				
			
		
					 14 changed files with 103 additions and 8 deletions
				
			
		|  | @ -26,6 +26,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController | |||
|         mention: alerts_enabled, | ||||
|         poll: alerts_enabled, | ||||
|         status: alerts_enabled, | ||||
|         update: alerts_enabled, | ||||
|       }, | ||||
|     } | ||||
| 
 | ||||
|  | @ -61,6 +62,15 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController | |||
|   end | ||||
| 
 | ||||
|   def data_params | ||||
|     @data_params ||= params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) | ||||
|     @data_params ||= params.require(:data).permit(:policy, alerts: [ | ||||
|       :follow, | ||||
|       :follow_request, | ||||
|       :favourite, | ||||
|       :reblog, | ||||
|       :mention, | ||||
|       :poll, | ||||
|       :status, | ||||
|       :update, | ||||
|     ]) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -34,7 +34,6 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; | |||
| export const NOTIFICATIONS_MOUNT   = 'NOTIFICATIONS_MOUNT'; | ||||
| export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; | ||||
| 
 | ||||
| 
 | ||||
| export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; | ||||
| 
 | ||||
| export const NOTIFICATIONS_SET_BROWSER_SUPPORT    = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; | ||||
|  | @ -124,7 +123,17 @@ export function updateNotifications(notification, intlMessages, intlLocale) { | |||
| const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); | ||||
| 
 | ||||
| const excludeTypesFromFilter = filter => { | ||||
|   const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']); | ||||
|   const allTypes = ImmutableList([ | ||||
|     'follow', | ||||
|     'follow_request', | ||||
|     'favourite', | ||||
|     'reblog', | ||||
|     'mention', | ||||
|     'poll', | ||||
|     'status', | ||||
|     'update', | ||||
|   ]); | ||||
| 
 | ||||
|   return allTypes.filterNot(item => item === filter).toJS(); | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -153,6 +153,17 @@ export default class ColumnSettings extends React.PureComponent { | |||
|             <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} /> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div role='group' aria-labelledby='notifications-update'> | ||||
|           <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span> | ||||
| 
 | ||||
|           <div className='column-settings__row'> | ||||
|             <SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} /> | ||||
|             {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'update']} onChange={this.onPushChange} label={pushStr} />} | ||||
|             <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} /> | ||||
|             <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ const messages = defineMessages({ | |||
|   poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' }, | ||||
|   reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' }, | ||||
|   status: { id: 'notification.status', defaultMessage: '{name} just posted' }, | ||||
|   update: { id: 'notification.update', defaultMessage: '{name} edited a post' }, | ||||
| }); | ||||
| 
 | ||||
| const notificationForScreenReader = (intl, message, timestamp) => { | ||||
|  | @ -273,6 +274,38 @@ class Notification extends ImmutablePureComponent { | |||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   renderUpdate (notification, link) { | ||||
|     const { intl, unread } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <HotKeys handlers={this.getHandlers()}> | ||||
|         <div className={classNames('notification notification-update focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.update, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}> | ||||
|           <div className='notification__message'> | ||||
|             <div className='notification__favourite-icon-wrapper'> | ||||
|               <Icon id='pencil' fixedWidth /> | ||||
|             </div> | ||||
| 
 | ||||
|             <span title={notification.get('created_at')}> | ||||
|               <FormattedMessage id='notification.update' defaultMessage='{name} edited a post' values={{ name: link }} /> | ||||
|             </span> | ||||
|           </div> | ||||
| 
 | ||||
|           <StatusContainer | ||||
|             id={notification.get('status')} | ||||
|             account={notification.get('account')} | ||||
|             muted | ||||
|             withDismiss | ||||
|             hidden={this.props.hidden} | ||||
|             getScrollPosition={this.props.getScrollPosition} | ||||
|             updateScrollBottom={this.props.updateScrollBottom} | ||||
|             cachedMediaWidth={this.props.cachedMediaWidth} | ||||
|             cacheMediaWidth={this.props.cacheMediaWidth} | ||||
|           /> | ||||
|         </div> | ||||
|       </HotKeys> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   renderPoll (notification, account) { | ||||
|     const { intl, unread } = this.props; | ||||
|     const ownPoll  = me === account.get('id'); | ||||
|  | @ -330,6 +363,8 @@ class Notification extends ImmutablePureComponent { | |||
|       return this.renderReblog(notification, link); | ||||
|     case 'status': | ||||
|       return this.renderStatus(notification, link); | ||||
|     case 'update': | ||||
|       return this.renderUpdate(notification, link); | ||||
|     case 'poll': | ||||
|       return this.renderPoll(notification, account); | ||||
|     } | ||||
|  |  | |||
|  | @ -36,6 +36,7 @@ const initialState = ImmutableMap({ | |||
|       mention: false, | ||||
|       poll: false, | ||||
|       status: false, | ||||
|       update: false, | ||||
|     }), | ||||
| 
 | ||||
|     quickFilter: ImmutableMap({ | ||||
|  | @ -55,6 +56,7 @@ const initialState = ImmutableMap({ | |||
|       mention: true, | ||||
|       poll: true, | ||||
|       status: true, | ||||
|       update: true, | ||||
|     }), | ||||
| 
 | ||||
|     sounds: ImmutableMap({ | ||||
|  | @ -65,6 +67,7 @@ const initialState = ImmutableMap({ | |||
|       mention: true, | ||||
|       poll: true, | ||||
|       status: true, | ||||
|       update: true, | ||||
|     }), | ||||
|   }), | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,6 +20,8 @@ filenames.forEach(filename => { | |||
|     'notification.mention': full['notification.mention'] || '', | ||||
|     'notification.reblog': full['notification.reblog'] || '', | ||||
|     'notification.poll': full['notification.poll'] || '', | ||||
|     'notification.status': full['notification.status'] || '', | ||||
|     'notification.update': full['notification.update'] || '', | ||||
| 
 | ||||
|     'status.show_more': full['status.show_more'] || '', | ||||
|     'status.reblog': full['status.reblog'] || '', | ||||
|  |  | |||
|  | @ -102,7 +102,7 @@ const handlePush = (event) => { | |||
| 
 | ||||
|         options.image   = undefined; | ||||
|         options.actions = [actionExpand(preferred_locale)]; | ||||
|       } else if (notification.type === 'mention') { | ||||
|       } else if (['mention', 'status'].includes(notification.type)) { | ||||
|         options.actions = [actionReblog(preferred_locale), actionFavourite(preferred_locale)]; | ||||
|       } | ||||
| 
 | ||||
|  |  | |||
|  | @ -35,6 +35,7 @@ class Notification < ApplicationRecord | |||
|     follow_request | ||||
|     favourite | ||||
|     poll | ||||
|     update | ||||
|   ).freeze | ||||
| 
 | ||||
|   TARGET_STATUS_INCLUDES_BY_TYPE = { | ||||
|  | @ -43,6 +44,7 @@ class Notification < ApplicationRecord | |||
|     mention: [mention: :status], | ||||
|     favourite: [favourite: :status], | ||||
|     poll: [poll: :status], | ||||
|     update: :status, | ||||
|   }.freeze | ||||
| 
 | ||||
|   belongs_to :account, optional: true | ||||
|  | @ -76,7 +78,7 @@ class Notification < ApplicationRecord | |||
| 
 | ||||
|   def target_status | ||||
|     case type | ||||
|     when :status | ||||
|     when :status, :update | ||||
|       status | ||||
|     when :reblog | ||||
|       status&.reblog | ||||
|  | @ -110,7 +112,7 @@ class Notification < ApplicationRecord | |||
|         cached_status = cached_statuses_by_id[notification.target_status.id] | ||||
| 
 | ||||
|         case notification.type | ||||
|         when :status | ||||
|         when :status, :update | ||||
|           notification.status = cached_status | ||||
|         when :reblog | ||||
|           notification.status.reblog = cached_status | ||||
|  |  | |||
|  | @ -62,6 +62,7 @@ class Status < ApplicationRecord | |||
|   has_many :favourites, inverse_of: :status, dependent: :destroy | ||||
|   has_many :bookmarks, inverse_of: :status, dependent: :destroy | ||||
|   has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy | ||||
|   has_many :reblogged_by_accounts, through: :reblogs, class_name: 'Account', source: :account | ||||
|   has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread | ||||
|   has_many :mentions, dependent: :destroy, inverse_of: :status | ||||
|   has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status | ||||
|  |  | |||
|  | @ -11,6 +11,6 @@ class REST::NotificationSerializer < ActiveModel::Serializer | |||
|   end | ||||
| 
 | ||||
|   def status_type? | ||||
|     [:favourite, :reblog, :status, :mention, :poll].include?(object.type) | ||||
|     [:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ class FanOutOnWriteService < BaseService | |||
|   def fan_out_to_local_recipients! | ||||
|     deliver_to_self! | ||||
|     notify_mentioned_accounts! | ||||
|     notify_about_update! if update? | ||||
| 
 | ||||
|     case @status.visibility.to_sym | ||||
|     when :public, :unlisted, :private | ||||
|  | @ -64,6 +65,14 @@ class FanOutOnWriteService < BaseService | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def notify_about_update! | ||||
|     @status.reblogged_by_accounts.merge(Account.local).select(:id).reorder(nil).find_in_batches do |accounts| | ||||
|       LocalNotificationWorker.push_bulk(accounts) do |account| | ||||
|         [account.id, @status.id, 'Status', 'update'] | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def deliver_to_all_followers! | ||||
|     @account.followers_for_local_distribution.select(:id).reorder(nil).find_in_batches do |followers| | ||||
|       FeedInsertWorker.push_bulk(followers) do |follower| | ||||
|  |  | |||
|  | @ -46,6 +46,10 @@ class NotifyService < BaseService | |||
|     false | ||||
|   end | ||||
| 
 | ||||
|   def blocked_update? | ||||
|     false | ||||
|   end | ||||
| 
 | ||||
|   def following_sender? | ||||
|     return @following_sender if defined?(@following_sender) | ||||
|     @following_sender = @recipient.following?(@notification.from_account) || @recipient.requested?(@notification.from_account) | ||||
|  |  | |||
|  | @ -12,7 +12,14 @@ class LocalNotificationWorker | |||
|       activity = activity_class_name.constantize.find(activity_id) | ||||
|     end | ||||
| 
 | ||||
|     return if Notification.where(account: receiver, activity: activity).any? | ||||
|     # For most notification types, only one notification should exist, and the older one is | ||||
|     # preferred. For updates, such as when a status is edited, the new notification | ||||
|     # should replace the previous ones. | ||||
|     if type == 'update' | ||||
|       Notification.where(account: receiver, activity: activity, type: 'update').in_batches.delete_all | ||||
|     elsif Notification.where(account: receiver, activity: activity, type: type).any? | ||||
|       return | ||||
|     end | ||||
| 
 | ||||
|     NotifyService.new.call(receiver, type || activity_class_name.underscore, activity) | ||||
|   rescue ActiveRecord::RecordNotFound | ||||
|  |  | |||
|  | @ -1148,6 +1148,8 @@ en: | |||
|       title: New boost | ||||
|     status: | ||||
|       subject: "%{name} just posted" | ||||
|     update: | ||||
|       subject: "%{name} edited a post" | ||||
|   notifications: | ||||
|     email_events: Events for e-mail notifications | ||||
|     email_events_hint: 'Select events that you want to receive notifications for:' | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue