]> cat aescling's git repositories - mastodon.git/commitdiff
Add notifications when a reblogged status has been updated (#17404)
authorEugen Rochko <eugen@zeonfederated.com>
Fri, 11 Feb 2022 21:20:19 +0000 (22:20 +0100)
committerGitHub <noreply@github.com>
Fri, 11 Feb 2022 21:20:19 +0000 (22:20 +0100)
* 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

14 files changed:
app/controllers/api/web/push_subscriptions_controller.rb
app/javascript/mastodon/actions/notifications.js
app/javascript/mastodon/features/notifications/components/column_settings.js
app/javascript/mastodon/features/notifications/components/notification.js
app/javascript/mastodon/reducers/settings.js
app/javascript/mastodon/service_worker/web_push_locales.js
app/javascript/mastodon/service_worker/web_push_notifications.js
app/models/notification.rb
app/models/status.rb
app/serializers/rest/notification_serializer.rb
app/services/fan_out_on_write_service.rb
app/services/notify_service.rb
app/workers/local_notification_worker.rb
config/locales/en.yml

index bed57fc54cae6c5c27f7940410c78813d34a49c6..db2512e5f5f8f7907ef74942d3bcf0708ec8cbe5 100644 (file)
@@ -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
index 663cf21e3414d6d1e9619373fc20b3e8edffcb1b..9370811e0c16d008ac0525d932d121f4d2ca2e42 100644 (file)
@@ -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();
 };
 
index 005f5afda94d9754a9823a699292f6bf36109732..ada8b6e4a83fee155f8f8bb05af8c34bbd2cc792 100644 (file)
@@ -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>
     );
   }
index f9f8a87f2db266e0622048664b8ef6b51929ae04..cd471852b554df7db506c8109bf742ec4c2753e9 100644 (file)
@@ -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);
     }
index 2a89919e1f02eb1c9f933e90a780347802ddf990..5146abe98a38bb65c9085fa9190a66929c35bd4d 100644 (file)
@@ -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,
     }),
   }),
 
index 1265f3cfafb902769bb9bcaf9730bf738139b3fb..807a1bcb942fe584a26fd360d8916d788b1f5cdd 100644 (file)
@@ -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'] || '',
index 926c5c4d7a60f0f1aed739b1faa1296fa515388e..48a2be7e70223d3cab44c881b127241db3d5a9b4 100644 (file)
@@ -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)];
       }
 
index 3bf9dd483e6da921c989c05c3b3ca13c0d489a85..c14eb8a7eb7fd3eb0d32bdd69aaa5aa7a4102b44 100644 (file)
@@ -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
index 4fabf24ef2a90dab67f83385bfdcd0efb10e27cf..2e3df98a1f63990d73c3669b81456f38af525cb4 100644 (file)
@@ -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
index 27b031fcc77a1603b617faca6745550b43216129..69b81f6deb04ad9d6a2988fd726312c3b645d51c 100644 (file)
@@ -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
index 4f847e2939899923726f54d61f2a334f2e98e3ef..2bab91116a011d1202fa345f257943acbf4745eb 100644 (file)
@@ -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|
index 0f3516d28f85498c1938e359f497b18eacbee82c..039e007f504afe6911671592409d5acc1be1d3a3 100644 (file)
@@ -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)
index a22e2834de6913122788caa0c9c0ddb1036d9cfc..749a54b73b028aed6b86c38d567dda5d2691ebc8 100644 (file)
@@ -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
index 0a9f66827c5dbc03551c6081d9eae646e759f0cd..1809f123ed87a19f399af025782e6b8d157db89a 100644 (file)
@@ -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:'