mention: alerts_enabled,
poll: alerts_enabled,
status: alerts_enabled,
+ update: alerts_enabled,
},
}
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
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';
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();
};
<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>
);
}
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) => {
);
}
+ 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');
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);
}
mention: false,
poll: false,
status: false,
+ update: false,
}),
quickFilter: ImmutableMap({
mention: true,
poll: true,
status: true,
+ update: true,
}),
sounds: ImmutableMap({
mention: true,
poll: true,
status: true,
+ update: true,
}),
}),
'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'] || '',
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)];
}
follow_request
favourite
poll
+ update
).freeze
TARGET_STATUS_INCLUDES_BY_TYPE = {
mention: [mention: :status],
favourite: [favourite: :status],
poll: [poll: :status],
+ update: :status,
}.freeze
belongs_to :account, optional: true
def target_status
case type
- when :status
+ when :status, :update
status
when :reblog
status&.reblog
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
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
end
def status_type?
- [:favourite, :reblog, :status, :mention, :poll].include?(object.type)
+ [:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type)
end
end
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
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|
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)
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
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:'