Change e-mail notifications to only be sent when recipient is offline (#17984)

* Change e-mail notifications to only be sent when recipient is offline

Change the default for follow and mention notifications back on

* Add preference to always send e-mail notifications

* Change wording
This commit is contained in:
Eugen Rochko 2022-04-08 18:03:31 +02:00 committed by GitHub
parent fd9a9b07c2
commit 8e20e16cf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 58 additions and 25 deletions

View File

@ -54,7 +54,8 @@ class Settings::PreferencesController < Settings::BaseController
:setting_use_pending_items, :setting_use_pending_items,
:setting_trends, :setting_trends,
:setting_crop_images, :setting_crop_images,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag), :setting_always_send_emails,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag appeal),
interactions: %i(must_be_follower must_be_following must_be_following_dm) interactions: %i(must_be_follower must_be_following must_be_following_dm)
) )
end end

View File

@ -38,6 +38,7 @@ class UserSettingsDecorator
user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items') user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items')
user.settings['trends'] = trends_preference if change?('setting_trends') user.settings['trends'] = trends_preference if change?('setting_trends')
user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images') user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images')
user.settings['always_send_emails'] = always_send_emails_preference if change?('setting_always_send_emails')
end end
def merged_notification_emails def merged_notification_emails
@ -132,6 +133,10 @@ class UserSettingsDecorator
boolean_cast_setting 'setting_crop_images' boolean_cast_setting 'setting_crop_images'
end end
def always_send_emails_preference
boolean_cast_setting 'setting_always_send_emails'
end
def boolean_cast_setting(key) def boolean_cast_setting(key)
ActiveModel::Type::Boolean.new.cast(settings[key]) ActiveModel::Type::Boolean.new.cast(settings[key])
end end

View File

@ -130,7 +130,7 @@ class User < ApplicationRecord
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :reduce_motion, :system_font_ui, :noindex, :theme, :display_media,
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, :advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
:disable_swiping, :disable_swiping, :always_send_emails,
to: :settings, prefix: :setting, allow_nil: false to: :settings, prefix: :setting, allow_nil: false
attr_reader :invite_code attr_reader :invite_code

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class NotifyService < BaseService class NotifyService < BaseService
include Redisable
def call(recipient, type, activity) def call(recipient, type, activity)
@recipient = recipient @recipient = recipient
@activity = activity @activity = activity
@ -8,10 +10,15 @@ class NotifyService < BaseService
return if recipient.user.nil? || blocked? return if recipient.user.nil? || blocked?
create_notification! @notification.save!
# It's possible the underlying activity has been deleted
# between the save call and now
return if @notification.activity.nil?
push_notification! push_notification!
push_to_conversation! if direct_message? push_to_conversation! if direct_message?
send_email! if email_enabled? send_email! if email_needed?
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
nil nil
end end
@ -92,8 +99,8 @@ class NotifyService < BaseService
end end
def blocked? def blocked?
blocked = @recipient.suspended? # Skip if the recipient account is suspended anyway blocked = @recipient.suspended?
blocked ||= from_self? && @notification.type != :poll # Skip for interactions with self blocked ||= from_self? && @notification.type != :poll
return blocked if message? && from_staff? return blocked if message? && from_staff?
@ -117,38 +124,52 @@ class NotifyService < BaseService
end end
end end
def create_notification! def push_notification!
@notification.save! push_to_streaming_api! if subscribed_to_streaming_api?
push_to_web_push_subscriptions!
end end
def push_notification! def push_to_streaming_api!
return if @notification.activity.nil? redis.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))
end
Redis.current.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) def subscribed_to_streaming_api?
send_push_notifications! redis.exists?("subscribed:timeline:#{@recipient.id}") || redis.exists?("subscribed:timeline:#{@recipient.id}:notifications")
end end
def push_to_conversation! def push_to_conversation!
return if @notification.activity.nil?
AccountConversation.add_status(@recipient, @notification.target_status) AccountConversation.add_status(@recipient, @notification.target_status)
end end
def send_push_notifications! def push_to_web_push_subscriptions!
subscriptions_ids = ::Web::PushSubscription.where(user_id: @recipient.user.id) ::Web::PushNotificationWorker.push_bulk(web_push_subscriptions.select { |subscription| subscription.pushable?(@notification) }) { |subscription| [subscription.id, @notification.id] }
.select { |subscription| subscription.pushable?(@notification) }
.map(&:id)
::Web::PushNotificationWorker.push_bulk(subscriptions_ids) do |subscription_id|
[subscription_id, @notification.id]
end end
def web_push_subscriptions
@web_push_subscriptions ||= ::Web::PushSubscription.where(user_id: @recipient.user.id).to_a
end
def subscribed_to_web_push?
web_push_subscriptions.any?
end end
def send_email! def send_email!
return if @notification.activity.nil? NotificationMailer.public_send(@notification.type, @recipient, @notification).deliver_later(wait: 2.minutes) if NotificationMailer.respond_to?(@notification.type)
NotificationMailer.public_send(@notification.type, @recipient, @notification).deliver_later(wait: 2.minutes)
end end
def email_enabled? def email_needed?
(!recipient_online? || always_send_emails?) && send_email_for_notification_type?
end
def recipient_online?
subscribed_to_streaming_api? || subscribed_to_web_push?
end
def always_send_emails?
@recipient.user.settings.always_send_emails
end
def send_email_for_notification_type?
@recipient.user.settings.notification_emails[@notification.type.to_s] @recipient.user.settings.notification_emails[@notification.type.to_s]
end end
end end

View File

@ -25,6 +25,9 @@
= ff.input :pending_account, as: :boolean, wrapper: :with_label = ff.input :pending_account, as: :boolean, wrapper: :with_label
= ff.input :trending_tag, as: :boolean, wrapper: :with_label = ff.input :trending_tag, as: :boolean, wrapper: :with_label
.fields-group
= f.input :setting_always_send_emails, as: :boolean, wrapper: :with_label
.fields-group .fields-group
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
= ff.input :digest, as: :boolean, wrapper: :with_label = ff.input :digest, as: :boolean, wrapper: :with_label

View File

@ -49,6 +49,7 @@ en:
phrase: Will be matched regardless of casing in text or content warning of a post phrase: Will be matched regardless of casing in text or content warning of a post
scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones.
setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts) setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts)
setting_always_send_emails: Normally e-mail notifications won't be sent when you are actively using Mastodon
setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click
setting_display_media_default: Hide media marked as sensitive setting_display_media_default: Hide media marked as sensitive
setting_display_media_hide_all: Always hide media setting_display_media_hide_all: Always hide media
@ -151,6 +152,7 @@ en:
phrase: Keyword or phrase phrase: Keyword or phrase
setting_advanced_layout: Enable advanced web interface setting_advanced_layout: Enable advanced web interface
setting_aggregate_reblogs: Group boosts in timelines setting_aggregate_reblogs: Group boosts in timelines
setting_always_send_emails: Always send e-mail notifications
setting_auto_play_gif: Auto-play animated GIFs setting_auto_play_gif: Auto-play animated GIFs
setting_boost_modal: Show confirmation dialog before boosting setting_boost_modal: Show confirmation dialog before boosting
setting_crop_images: Crop images in non-expanded posts to 16x9 setting_crop_images: Crop images in non-expanded posts to 16x9

View File

@ -38,16 +38,17 @@ defaults: &defaults
trendable_by_default: false trendable_by_default: false
crop_images: true crop_images: true
notification_emails: notification_emails:
follow: false follow: true
reblog: false reblog: false
favourite: false favourite: false
mention: false mention: true
follow_request: true follow_request: true
digest: true digest: true
report: true report: true
pending_account: true pending_account: true
trending_tag: true trending_tag: true
appeal: true appeal: true
always_send_emails: false
interactions: interactions:
must_be_follower: false must_be_follower: false
must_be_following: false must_be_following: false