mirror of
https://github.com/discourse/discourse.git
synced 2024-12-02 06:43:54 +08:00
71391cd40d
When chat is enabled, there's a scheduled job that runs every 5 minutes to check whether we need to send a "chat summary" email to users with unread chat messages or mentions. On Discourse with a large number of users, the query used wasn't optimal and sometimes taking minutes. Which isn't good when the query is called every 5 minutes 😬 This PR reworks the query in `Chat::Mailer.send_unread_mentions_summary`. Instead of starting from the `users` table, it starts from the `user_chat_channel_memberships` table which is the main piece tying everything together. The new query is mostly similar to the previous one, with some bug fixes (like ensuring the user has `allow_private_messages` enabled for direct messages) and is also slightly simpler since it doesn't keep track of the `memberships_with_unread_messages` anymore. That part has been moved to the `user_notifications.chat_summary` email method. The `UserEmailExtension` has been deleted since that was using to N+1 update the `user_chat_channel_memberships.last_unread_mention_when_emailed_it`(quite a mouthful 😛) but that's now done directly in the `user_notifications.chat_summary` email method. The "plat de résistance" of that PR - the `user_notifications.chat_summary` method has been re-worked for improved performances 🚀 Instead of doing everything in one query, it does 4 tiny ones. - One to retrieve the list of unread mentions (@something) in "category" channels - One to retrieve the list of unread messages in "direct message" channels (aka. 1-1 and group discussions) - One to load all the chat messages for each "category" channels from the last unread mention - One to load all the chat messages for each "direct message" channels from the last unread message All the specs for both `Chat::Mailer` and `UserNotification.chat_summary` have been rewriten for easier comprehension and faster execution (mostly by not using chat services which makes the specs go 10x slower...) Internal ref - t/129848
469 lines
15 KiB
Ruby
469 lines
15 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
describe UserNotifications do
|
|
fab!(:user) { Fabricate(:user, last_seen_at: 1.hour.ago) }
|
|
fab!(:other) { Fabricate(:user) }
|
|
fab!(:another) { Fabricate(:user) }
|
|
fab!(:someone) { Fabricate(:user) }
|
|
fab!(:group) { Fabricate(:group, users: [user, other]) }
|
|
|
|
fab!(:followed_channel) { Fabricate(:category_channel) }
|
|
fab!(:followed_channel_2) { Fabricate(:category_channel) }
|
|
fab!(:followed_channel_3) { Fabricate(:category_channel) }
|
|
fab!(:non_followed_channel) { Fabricate(:category_channel) }
|
|
fab!(:muted_channel) { Fabricate(:category_channel) }
|
|
fab!(:unseen_channel) { Fabricate(:category_channel) }
|
|
fab!(:private_channel) { Fabricate(:private_category_channel, group:) }
|
|
fab!(:direct_message) { Fabricate(:direct_message_channel, users: [user, other]) }
|
|
fab!(:direct_message_2) { Fabricate(:direct_message_channel, users: [user, another]) }
|
|
fab!(:direct_message_3) { Fabricate(:direct_message_channel, users: [user, someone]) }
|
|
fab!(:group_message) { Fabricate(:direct_message_channel, users: [user, other, another]) }
|
|
|
|
fab!(:site_name) { SiteSetting.email_prefix.presence || SiteSetting.title }
|
|
|
|
before do
|
|
SiteSetting.chat_enabled = true
|
|
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
|
end
|
|
|
|
def create_message(chat_channel, message, mention_klass = nil)
|
|
chat_message = Fabricate(:chat_message, user: other, chat_channel:, message:)
|
|
|
|
if mention_klass
|
|
notification_type = Notification.types[:chat_mention]
|
|
|
|
Fabricate(
|
|
:chat_mention_notification,
|
|
notification: Fabricate(:notification, user:, notification_type:),
|
|
chat_mention: mention_klass.find_by(chat_message:),
|
|
)
|
|
end
|
|
|
|
chat_message
|
|
end
|
|
|
|
def no_chat_summary_email
|
|
email = described_class.chat_summary(user, {})
|
|
expect(email.to).to be_blank
|
|
end
|
|
|
|
def chat_summary_email
|
|
email = described_class.chat_summary(user, {})
|
|
expect(email.to).to contain_exactly(user.email)
|
|
email
|
|
end
|
|
|
|
def chat_summary_with_subject(type, opts = {})
|
|
expect(chat_summary_email.subject).to eq(
|
|
I18n.t("user_notifications.chat_summary.subject.#{type}", { site_name:, **opts }),
|
|
)
|
|
end
|
|
|
|
describe "in a followed channel" do
|
|
before { followed_channel.add(user) }
|
|
|
|
describe "user is mentioned" do
|
|
let!(:chat_mention) do
|
|
create_message(followed_channel, "hello @#{user.username}", Chat::UserMention)
|
|
end
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(:chat_channel_1, channel: followed_channel.name, count: 1)
|
|
end
|
|
|
|
it "pluralizes the subject" do
|
|
create_message(followed_channel, "how are you?")
|
|
chat_summary_with_subject(:chat_channel_1, channel: followed_channel.name, count: 2)
|
|
end
|
|
|
|
it "sends a chat summary email with correct body" do
|
|
html = chat_summary_email.html_part.body.to_s
|
|
|
|
expect(html).to include(followed_channel.title(user))
|
|
expect(html).to include(chat_mention.full_url)
|
|
expect(html).to include(PrettyText.format_for_email(chat_mention.cooked_for_excerpt))
|
|
expect(html).to include(chat_mention.user.small_avatar_url)
|
|
expect(html).to include(chat_mention.user.username)
|
|
expect(html).to include(
|
|
I18n.l(UserOption.user_tzinfo(user.id).to_local(chat_mention.created_at), format: :long),
|
|
)
|
|
expect(html).to include(I18n.t("user_notifications.chat_summary.view_messages", count: 1))
|
|
end
|
|
|
|
it "sends a chat summary email with view more link" do
|
|
create_message(followed_channel, "how are you...")
|
|
create_message(followed_channel, "doing...")
|
|
create_message(followed_channel, "today?")
|
|
|
|
html = chat_summary_email.html_part.body.to_s
|
|
|
|
expect(html).to include(I18n.t("user_notifications.chat_summary.view_more", count: 2))
|
|
end
|
|
|
|
describe "SiteSetting.prioritize_username_in_ux is disabled" do
|
|
before { SiteSetting.prioritize_username_in_ux = false }
|
|
|
|
it "sends a chat summary email with the username instead of the name" do
|
|
html = chat_summary_email.html_part.body.to_s
|
|
|
|
expect(html).to include(chat_mention.user.name)
|
|
expect(html).not_to include(chat_mention.user.username)
|
|
end
|
|
end
|
|
|
|
describe "when using subfolder" do
|
|
before { set_subfolder "/community" }
|
|
|
|
it "sends a chat summary email with the correct URL" do
|
|
html = chat_summary_email.html_part.body.to_s
|
|
|
|
expect(html).to include <<~HTML.strip
|
|
<a class="more-messages-link" href="#{Discourse.base_url}/chat
|
|
HTML
|
|
end
|
|
end
|
|
|
|
it "does not send an email if user can't chat" do
|
|
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:admins]
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the user has been seen recently" do
|
|
user.update!(last_seen_at: 5.minutes.ago)
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the user has disabled chat emails" do
|
|
user.user_option.update!(chat_email_frequency: UserOption.chat_email_frequencies[:never])
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the user has disabled all emails" do
|
|
user.user_option.update!(email_level: UserOption.email_level_types[:never])
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the channel has been deleted" do
|
|
followed_channel.trash!
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the chat message has been deleted" do
|
|
chat_mention.trash!
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the mention is more than a week old" do
|
|
chat_mention.update!(created_at: 10.days.ago)
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the user isn't following the channel anymore" do
|
|
followed_channel.membership_for(user).update!(following: false)
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the user has already read the message" do
|
|
followed_channel.membership_for(user).update!(last_read_message_id: chat_mention.id)
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the user has already received a chat summary email" do
|
|
followed_channel.membership_for(user).update!(
|
|
last_unread_mention_when_emailed_id: chat_mention.id,
|
|
)
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the user has read the mention notification" do
|
|
Notification.find_by(
|
|
user: user,
|
|
notification_type: Notification.types[:chat_mention],
|
|
).update!(read: true)
|
|
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "does not send an email if the sender has been deleted" do
|
|
other.destroy!
|
|
no_chat_summary_email
|
|
end
|
|
|
|
describe "SiteSetting.private_email is enabled" do
|
|
before { SiteSetting.private_email = true }
|
|
|
|
it "sends a chat summary email with a private subject" do
|
|
chat_summary_with_subject(:private_email, count: 1)
|
|
end
|
|
|
|
it "pluralizes the private subject" do
|
|
create_message(followed_channel, "how are you?")
|
|
chat_summary_with_subject(:private_email, count: 2)
|
|
end
|
|
|
|
it "sends a chat summary email with a private body" do
|
|
html = chat_summary_email.html_part.body.to_s
|
|
|
|
expect(html).to include(
|
|
I18n.t("system_messages.private_channel_title", id: followed_channel.id),
|
|
)
|
|
|
|
expect(html).to include(chat_mention.full_url)
|
|
expect(html).to include(I18n.t("user_notifications.chat_summary.view_messages", count: 1))
|
|
|
|
expect(html).not_to include(followed_channel.title(user))
|
|
expect(html).not_to include(PrettyText.format_for_email(chat_mention.cooked_for_excerpt))
|
|
expect(html).not_to include(chat_mention.user.small_avatar_url)
|
|
expect(html).not_to include(chat_mention.user.username)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "user is not mentioned" do
|
|
before { create_message(followed_channel, "hello") }
|
|
|
|
it "does not send a chat summary email" do
|
|
no_chat_summary_email
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "in two followed channels" do
|
|
before do
|
|
followed_channel.add(user)
|
|
followed_channel_2.add(user)
|
|
end
|
|
|
|
describe "user is mentioned in one channel" do
|
|
before do
|
|
create_message(followed_channel, "hello @#{user.username}", Chat::UserMention)
|
|
create_message(followed_channel_2, "hello")
|
|
end
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(:chat_channel_1, channel: followed_channel.name, count: 1)
|
|
end
|
|
end
|
|
|
|
describe "user is mentioned in both channels" do
|
|
before do
|
|
create_message(followed_channel, "hello @#{user.username}", Chat::UserMention)
|
|
create_message(followed_channel_2, "hello @#{user.username}", Chat::UserMention)
|
|
end
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(
|
|
:chat_channel_2,
|
|
channel_1: followed_channel.name,
|
|
channel_2: followed_channel_2.name,
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "in three followed channels" do
|
|
before do
|
|
followed_channel.add(user)
|
|
followed_channel_2.add(user)
|
|
followed_channel_3.add(user)
|
|
end
|
|
|
|
describe "user is mentioned in one channel" do
|
|
before do
|
|
create_message(followed_channel, "hello @#{user.username}", Chat::UserMention)
|
|
create_message(followed_channel_2, "hello")
|
|
create_message(followed_channel_3, "hello")
|
|
end
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(:chat_channel_1, channel: followed_channel.name, count: 1)
|
|
end
|
|
end
|
|
|
|
describe "user is mentioned in two channels" do
|
|
before do
|
|
create_message(followed_channel, "hello @#{user.username}", Chat::UserMention)
|
|
create_message(followed_channel_2, "hello @#{user.username}", Chat::UserMention)
|
|
create_message(followed_channel_3, "hello")
|
|
end
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(
|
|
:chat_channel_2,
|
|
channel_1: followed_channel.name,
|
|
channel_2: followed_channel_2.name,
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "user is mentioned in all channels" do
|
|
before do
|
|
create_message(followed_channel, "hello @#{user.username}", Chat::UserMention)
|
|
create_message(followed_channel_2, "hello @#{user.username}", Chat::UserMention)
|
|
create_message(followed_channel_3, "hello @#{user.username}", Chat::UserMention)
|
|
end
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(:chat_channel_3_or_more, channel: followed_channel.name, count: 2)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "in a non-followed channel" do
|
|
before { non_followed_channel.add(user).update!(following: false) }
|
|
|
|
describe "user is mentioned" do
|
|
before { create_message(non_followed_channel, "hello @#{user.username}", Chat::UserMention) }
|
|
|
|
it "does not send a chat summary email" do
|
|
no_chat_summary_email
|
|
end
|
|
end
|
|
|
|
describe "user is not mentioned" do
|
|
before { create_message(non_followed_channel, "hello") }
|
|
|
|
it "does not send a chat summary email" do
|
|
no_chat_summary_email
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "in a muted channel" do
|
|
before { muted_channel.add(user).update!(muted: true) }
|
|
|
|
describe "user is mentioned" do
|
|
before { create_message(muted_channel, "hello @#{user.username}", Chat::UserMention) }
|
|
|
|
it "does not send a chat summary email" do
|
|
no_chat_summary_email
|
|
end
|
|
end
|
|
|
|
describe "user is not mentioned" do
|
|
before { create_message(muted_channel, "hello") }
|
|
|
|
it "does not send a chat summary email" do
|
|
no_chat_summary_email
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "in an unseen channel" do
|
|
describe "user is mentioned" do
|
|
before { create_message(unseen_channel, "hello @#{user.username}", Chat::UserMention) }
|
|
|
|
it "does not send a chat summary email" do
|
|
no_chat_summary_email
|
|
end
|
|
end
|
|
|
|
describe "user is not mentioned" do
|
|
before { create_message(unseen_channel, "hello") }
|
|
|
|
it "does not send a chat summary email" do
|
|
no_chat_summary_email
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "in a private channel" do
|
|
before { private_channel.add(user) }
|
|
|
|
describe "user is mentioned" do
|
|
before { create_message(private_channel, "hello @#{user.username}", Chat::UserMention) }
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(:chat_channel_1, channel: private_channel.name, count: 1)
|
|
end
|
|
|
|
it "does not send a chat summary email when the user is not member of the group anymore" do
|
|
group.remove(user)
|
|
no_chat_summary_email
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "in a 1:1" do
|
|
before { create_message(direct_message, "Hello 👋") }
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(:chat_dm_1, name: direct_message.title(user), count: 1)
|
|
end
|
|
|
|
it "pluralizes the subject" do
|
|
create_message(direct_message, "How are you?")
|
|
chat_summary_with_subject(:chat_dm_1, name: direct_message.title(user), count: 2)
|
|
end
|
|
|
|
it "does not send an email if the user has disabled private messages" do
|
|
user.user_option.update!(allow_private_messages: false)
|
|
no_chat_summary_email
|
|
end
|
|
|
|
it "sends a chat summary email even if the user isn't following the direct message" do
|
|
direct_message.membership_for(user).update!(following: false)
|
|
chat_summary_with_subject(:chat_dm_1, name: direct_message.title(user), count: 1)
|
|
end
|
|
end
|
|
|
|
describe "in two 1:1s" do
|
|
before do
|
|
create_message(direct_message, "Hello 👋")
|
|
create_message(direct_message_2, "Hello 👋")
|
|
end
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(
|
|
:chat_dm_2,
|
|
name_1: direct_message.title(user),
|
|
name_2: direct_message_2.title(user),
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "in three 1:1s" do
|
|
before do
|
|
create_message(direct_message, "Hello 👋")
|
|
create_message(direct_message_2, "Hello 👋")
|
|
create_message(direct_message_3, "Hello 👋")
|
|
end
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(:chat_dm_3_or_more, name: direct_message.title(user), count: 2)
|
|
end
|
|
end
|
|
|
|
describe "in a 1:many" do
|
|
before { create_message(group_message, "Hello 👋") }
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(:chat_channel_1, channel: group_message.title(user), count: 1)
|
|
end
|
|
|
|
it "pluralizes the subject" do
|
|
create_message(group_message, "How are you?")
|
|
chat_summary_with_subject(:chat_channel_1, channel: group_message.title(user), count: 2)
|
|
end
|
|
end
|
|
|
|
describe "in a followed channel and a 1:1" do
|
|
before { followed_channel.add(user) }
|
|
|
|
describe "user is mentioned in the channel and replied in the 1:1" do
|
|
before do
|
|
create_message(followed_channel, "hello @#{user.username}", Chat::UserMention)
|
|
create_message(direct_message, "hello")
|
|
end
|
|
|
|
it "sends a chat summary email" do
|
|
chat_summary_with_subject(
|
|
:chat_channel_and_dm,
|
|
channel: followed_channel.name,
|
|
name: direct_message.title(user),
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|