Skip to content

Commit 2f49d56

Browse files
Gargronhiyuki2578
authored andcommitted
Improve e-mail digest (mastodon#9689)
- Reduce time-to-digest from 20 to 7 days - Fetch mentions starting from +1 day since last login - Fix case when last login is more recent than last e-mail - Do not render all mentions, only 40, but show number in subject - Do not send digest to moved accounts - Do send digest to silenced accounts
1 parent 7b5a9c6 commit 2f49d56

7 files changed

Lines changed: 25 additions & 30 deletions

File tree

app/mailers/notification_mailer.rb

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,20 @@ def follow_request(recipient, notification)
6666
end
6767

6868
def digest(recipient, **opts)
69-
@me = recipient
70-
@since = opts[:since] || @me.user.last_emailed_at || @me.user.current_sign_in_at
71-
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since)
72-
@follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
69+
return if recipient.user.disabled?
70+
71+
@me = recipient
72+
@since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
73+
@notifications_count = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).count
7374

74-
return if @me.user.disabled? || @notifications.empty?
75+
return if @notifications_count.zero?
76+
77+
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).limit(40)
78+
@follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
7579

7680
locale_for_account(@me) do
7781
mail to: @me.user.email,
78-
subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications.size)
82+
subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications_count)
7983
end
8084
end
8185

app/models/account.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,16 @@ class Account < ApplicationRecord
8686
scope :silenced, -> { where(silenced: true) }
8787
scope :suspended, -> { where(suspended: true) }
8888
scope :without_suspended, -> { where(suspended: false) }
89+
scope :without_silenced, -> { where(silenced: false) }
8990
scope :recent, -> { reorder(id: :desc) }
9091
scope :bots, -> { where(actor_type: %w(Application Service)) }
9192
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
9293
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
9394
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
9495
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
9596
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
96-
scope :searchable, -> { where(suspended: false).where(moved_to_account_id: nil) }
97-
scope :discoverable, -> { searchable.where(silenced: false).where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)).by_recent_status }
97+
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
98+
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)).by_recent_status }
9899
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
99100
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) }
100101
scope :popular, -> { order('account_stats.followers_count desc') }

app/models/user.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class User < ApplicationRecord
5050
# every day. Raising the duration reduces the amount of expensive
5151
# RegenerationWorker jobs that need to be run when those people come
5252
# to check their feed
53-
ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days
53+
ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days.freeze
5454

5555
devise :two_factor_authenticatable,
5656
otp_secret_encryption_key: Rails.configuration.x.otp_secret
@@ -83,9 +83,11 @@ class User < ApplicationRecord
8383
scope :moderators, -> { where(moderator: true) }
8484
scope :staff, -> { admins.or(moderators) }
8585
scope :confirmed, -> { where.not(confirmed_at: nil) }
86+
scope :enabled, -> { where(disabled: false) }
8687
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
8788
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended: false }) }
8889
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
90+
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
8991

9092
before_validation :sanitize_languages
9193

app/views/notification_mailer/digest.html.haml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
%tr
1515
%td.column-cell.text-center.padded
1616
%h1= t 'notification_mailer.digest.title'
17-
%p.lead= t('notification_mailer.digest.body', since: l(@since.to_date, format: :short), instance: site_hostname)
17+
%p.lead= t('notification_mailer.digest.body', since: l((@me.user_current_sign_in_at || @since).to_date, format: :short), instance: site_hostname)
1818
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
1919
%tbody
2020
%tr

app/views/notification_mailer/digest.text.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
22

3-
<%= raw t('notification_mailer.digest.body', since: l(@since), instance: root_url) %>
3+
<%= raw t('notification_mailer.digest.body', since: l(@me.user_current_sign_in_at || @since), instance: root_url) %>
44
<% @notifications.each do |notification| %>
55

66
* <%= raw t('notification_mailer.digest.mention', name: notification.from_account.acct) %>

app/workers/scheduler/email_scheduler.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ class Scheduler::EmailScheduler
55

66
sidekiq_options unique: :until_executed, retry: 0
77

8+
FREQUENCY = 7.days.freeze
9+
SIGN_IN_OFFSET = 1.day.freeze
10+
811
def perform
912
eligible_users.reorder(nil).find_each do |user|
1013
next unless user.allows_digest_emails?
@@ -15,11 +18,8 @@ def perform
1518
private
1619

1720
def eligible_users
18-
User.confirmed
19-
.joins(:account)
20-
.where(accounts: { silenced: false, suspended: false })
21-
.where(disabled: false)
22-
.where('current_sign_in_at < ?', 20.days.ago)
23-
.where('last_emailed_at IS NULL OR last_emailed_at < ?', 20.days.ago)
21+
User.emailable
22+
.where('current_sign_in_at < ?', (FREQUENCY + SIGN_IN_OFFSET).ago)
23+
.where('last_emailed_at IS NULL OR last_emailed_at < ?', FREQUENCY.ago)
2424
end
2525
end

spec/mailers/notification_mailer_spec.rb

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,7 @@
126126
end
127127
end
128128

129-
it 'includes activities since the date specified by :since option' do
130-
receiver.update!(last_emailed_at: '2000-02-01T00:00:00Z', current_sign_in_at: '2000-03-01T00:00:00Z')
131-
mail = NotificationMailer.digest(receiver.account, since: Time.parse('2000-01-01T00:00:00Z'))
132-
expect(mail.body.encoded).to include 'Jan 01, 2000, 00:00'
133-
end
134-
135-
it 'includes activities since the receiver was last emailed if :since option is unavailable' do
136-
receiver.update!(last_emailed_at: '2000-02-01T00:00:00Z', current_sign_in_at: '2000-03-01T00:00:00Z')
137-
mail = NotificationMailer.digest(receiver.account)
138-
expect(mail.body.encoded).to include 'Feb 01, 2000, 00:00'
139-
end
140-
141-
it 'includes activities since the receiver last signed in if :since option and the last emailed date are unavailable' do
129+
it 'includes activities since the receiver last signed in' do
142130
receiver.update!(last_emailed_at: nil, current_sign_in_at: '2000-03-01T00:00:00Z')
143131
mail = NotificationMailer.digest(receiver.account)
144132
expect(mail.body.encoded).to include 'Mar 01, 2000, 00:00'

0 commit comments

Comments
 (0)