diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-user-menu.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-user-menu.js
index b2c0d4ca8c5..6ed8440431e 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-user-menu.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-user-menu.js
@@ -105,6 +105,60 @@ export default {
};
}
);
+
+ api.registerNotificationTypeRenderer(
+ "chat_message",
+ (NotificationItemBase) => {
+ return class extends NotificationItemBase {
+ linkTitle = I18n.t("notifications.titles.chat_message");
+ icon = "comment";
+
+ get linkHref() {
+ const slug = slugifyChannel({
+ title: this.notification.data.chat_channel_title,
+ slug: this.notification.data.chat_channel_slug,
+ });
+ return `/chat/c/${slug || "-"}/${
+ this.notification.data.chat_channel_id
+ }/${this.notification.data.chat_message_id}`;
+ }
+
+ get label() {
+ return formatUsername(this.notification.data.username);
+ }
+
+ get description() {
+ const data = this.notification.data;
+
+ if (!data.is_group_message) {
+ return I18n.t("notifications.chat_message.personal", {
+ username: data.username,
+ });
+ }
+
+ if (!data.username2) {
+ return I18n.t("notifications.chat_message.group", {
+ username: data.username,
+ });
+ }
+
+ if (data.user_ids.length > 2) {
+ return I18n.t("notifications.chat_message.group_multiple", {
+ username: data.username,
+ count: data.user_ids.length - 1,
+ });
+ } else {
+ // only 2 users so we show the second username
+ return I18n.t("notifications.chat_message.group_multiple", {
+ username: data.username,
+ username2: data.username2,
+ count: 1,
+ });
+ }
+ }
+ };
+ }
+ );
}
if (api.registerUserMenuTab) {
@@ -125,6 +179,7 @@ export default {
get count() {
return (
this.getUnreadCountForType("chat_mention") +
+ this.getUnreadCountForType("chat_message") +
this.getUnreadCountForType("chat_invitation")
);
}
diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml
index 4b36fb9d635..3b42cc0292a 100644
--- a/plugins/chat/config/locales/client.en.yml
+++ b/plugins/chat/config/locales/client.en.yml
@@ -595,6 +595,12 @@ en:
chat_invitation: "invited you to join a chat channel"
chat_invitation_html: "%{username} invited you to join a chat channel"
chat_quoted: "%{username} %{description}"
+ chat_message:
+ personal: "messaged you in a personal chat"
+ group: "messaged you in a group personal chat"
+ group_multiple:
+ one: "and %{username2} messaged you in a group personal chat"
+ other: "and %{count} others messaged you in a group personal chat"
popup:
chat_mention:
@@ -616,6 +622,7 @@ en:
chat_mention: "Chat mention"
chat_invitation: "Chat invitation"
chat_quoted: "Chat quoted"
+ chat_message: "Chat message"
action_codes:
chat:
enabled: '%{who} enabled %{when}'
diff --git a/plugins/chat/lib/chat/notification_consolidation_extension.rb b/plugins/chat/lib/chat/notification_consolidation_extension.rb
new file mode 100644
index 00000000000..8c0f3f7bc34
--- /dev/null
+++ b/plugins/chat/lib/chat/notification_consolidation_extension.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Chat
+ module NotificationConsolidationExtension
+ CONSOLIDATION_WINDOW = 60.minutes
+ CONSOLIDATION_THRESHOLD = 1
+
+ def self.chat_message_plan
+ Notifications::ConsolidateNotifications.new(
+ from: Notification.types[:chat_message],
+ to: Notification.types[:chat_message],
+ threshold: CONSOLIDATION_THRESHOLD,
+ consolidation_window: CONSOLIDATION_WINDOW,
+ unconsolidated_query_blk:
+ Proc.new do |notifications, data|
+ notifications.where("data::json ->> 'consolidated' IS NULL").where(
+ "data::json ->> 'chat_channel_id' = ?",
+ data[:chat_channel_id].to_s,
+ )
+ end,
+ consolidated_query_blk:
+ Proc.new do |notifications, data|
+ notifications.where("(data::json ->> 'consolidated')::bool").where(
+ "data::json ->> 'chat_channel_id' = ?",
+ data[:chat_channel_id].to_s,
+ )
+ end,
+ ).set_mutations(
+ set_data_blk:
+ lambda do |notification|
+ data = notification.data_hash
+
+ last_chat_message_notification =
+ Notification
+ .where(user_id: notification.user_id)
+ .order("notifications.id DESC")
+ .where("data::json ->> 'chat_channel_id' = ?", data[:chat_channel_id].to_s)
+ .where(notification_type: Notification.types[:chat_message])
+ .where("created_at > ?", CONSOLIDATION_WINDOW.ago)
+ .first
+
+ return data if !last_chat_message_notification
+
+ consolidated_data = last_chat_message_notification.data_hash
+
+ if data[:last_read_message_id].to_i <= consolidated_data[:chat_message_id].to_i
+ data[:chat_message_id] = consolidated_data[:chat_message_id]
+ end
+
+ if !consolidated_data[:username2] && data[:username] != consolidated_data[:username]
+ data.merge(
+ username2: consolidated_data[:username],
+ user_ids: consolidated_data[:user_ids].concat(data[:user_ids]),
+ )
+ else
+ data.merge(
+ username: consolidated_data[:username],
+ username2: consolidated_data[:username2],
+ user_ids: (consolidated_data[:user_ids].concat(data[:user_ids])).uniq,
+ )
+ end
+ end,
+ )
+ end
+ end
+end
diff --git a/plugins/chat/lib/chat/notifier.rb b/plugins/chat/lib/chat/notifier.rb
index 0bc4ab7a765..f94100c5c73 100644
--- a/plugins/chat/lib/chat/notifier.rb
+++ b/plugins/chat/lib/chat/notifier.rb
@@ -78,6 +78,8 @@ module Chat
notify_mentioned_users(to_notify)
notify_watching_users(except: all_mentioned_user_ids << @user.id)
+ notify_personal_chat_users(to_notify, except: all_mentioned_user_ids << @user.id)
+
to_notify
end
@@ -121,6 +123,48 @@ module Chat
[to_notify, inaccessible, all_mentioned_user_ids]
end
+ def notify_personal_chat_users(to_notify, except: [])
+ return if !@chat_channel.direct_message_channel?
+ notify_user_ids =
+ User.where(id: @chat_channel.allowed_user_ids).not_suspended.pluck(:id) - except
+ notified_user_ids = []
+
+ notify_user_ids.each do |user_id|
+ membership = @chat_channel.membership_for(user_id)
+ next if !membership || membership.muted?
+
+ screener = UserCommScreener.new(acting_user: @user, target_user_ids: user_id)
+ next if screener.ignoring_or_muting_actor?(user_id)
+
+ notified_user_ids << user_id
+ create_notification_for(membership, notification_type: Notification.types[:chat_message])
+ end
+
+ to_notify[:direct_messages] = notified_user_ids
+ end
+
+ def create_notification_for(membership, notification_type:)
+ if notification_type == Notification.types[:chat_message]
+ data = {
+ username: @user.username,
+ chat_message_id: @chat_message.id,
+ chat_channel_id: @chat_channel.id,
+ last_read_message_id: membership&.last_read_message_id,
+ is_direct_message_channel: @chat_channel.direct_message_channel?,
+ is_group_message: @chat_channel.allowed_user_ids.size > 2,
+ user_ids: [@user.id],
+ }
+
+ data[:chat_thread_id] = @chat_message.thread_id if @chat_message.in_thread?
+ end
+
+ Notification.consolidate_or_create!(
+ notification_type: notification_type,
+ user_id: membership.user_id,
+ data: data.to_json,
+ )
+ end
+
def expand_global_mention(to_notify, already_covered_ids)
has_all_mention = @parsed_mentions.has_global_mention
diff --git a/plugins/chat/plugin.rb b/plugins/chat/plugin.rb
index 1a9789b85b9..3cee9df58f5 100644
--- a/plugins/chat/plugin.rb
+++ b/plugins/chat/plugin.rb
@@ -59,6 +59,7 @@ after_initialize do
Guardian.prepend Chat::GuardianExtensions
UserNotifications.prepend Chat::UserNotificationsExtension
+ Notifications::ConsolidationPlan.prepend Chat::NotificationConsolidationExtension
UserOption.prepend Chat::UserOptionExtension
Category.prepend Chat::CategoryExtension
Reviewable.prepend Chat::ReviewableExtension
@@ -472,6 +473,10 @@ after_initialize do
)
register_bookmarkable(Chat::MessageBookmarkable)
+
+ register_notification_consolidation_plan(
+ Chat::NotificationConsolidationExtension.chat_message_plan,
+ )
end
if Rails.env == "test"
diff --git a/plugins/chat/spec/lib/chat/notifier_spec.rb b/plugins/chat/spec/lib/chat/notifier_spec.rb
index 88c9ddde707..7e3761d7656 100644
--- a/plugins/chat/spec/lib/chat/notifier_spec.rb
+++ b/plugins/chat/spec/lib/chat/notifier_spec.rb
@@ -689,5 +689,72 @@ describe Chat::Notifier do
)
end
end
+
+ describe "personal chat messages" do
+ fab!(:dm_channel) { Fabricate(:direct_message_channel) }
+
+ before do
+ dm_user_ids = dm_channel.allowed_user_ids
+ @dm_user_1 = User.find(dm_user_ids.first)
+ @dm_user_2 = User.find(dm_user_ids.last)
+ end
+
+ it "notifies the other user when a new message is sent" do
+ msg = build_cooked_msg("Hey guys", @dm_user_1, chat_channel: dm_channel)
+ to_notify = described_class.new(msg, msg.created_at).notify_new
+
+ expect(to_notify[:direct_messages]).to contain_exactly(@dm_user_2.id)
+ end
+
+ it "does not notify users who have muted the chat channel" do
+ dm_channel.membership_for(@dm_user_1.id).update!(muted: true)
+ msg = build_cooked_msg("How are you?", @dm_user_2, chat_channel: dm_channel)
+
+ expect { described_class.new(msg, msg.created_at).notify_new }.not_to change {
+ @dm_user_1.notifications.count
+ }
+ end
+
+ it "does not notify users who have muted the other user" do
+ Fabricate(:muted_user, user: @dm_user_1, muted_user: @dm_user_2)
+ msg = build_cooked_msg("How are you?", @dm_user_2, chat_channel: dm_channel)
+
+ expect { described_class.new(msg, msg.created_at).notify_new }.not_to change {
+ @dm_user_1.notifications.count
+ }
+ end
+
+ it "does not notify users who have ignored the other user" do
+ Fabricate(:ignored_user, user: @dm_user_1, ignored_user: @dm_user_2)
+ msg = build_cooked_msg("How are you?", @dm_user_2, chat_channel: dm_channel)
+
+ expect { described_class.new(msg, msg.created_at).notify_new }.not_to change {
+ @dm_user_1.notifications.count
+ }
+ end
+
+ it "does not notify users who are suspended" do
+ @dm_user_1.update!(suspended_till: 2.years.from_now)
+ msg = build_cooked_msg("How are you?", @dm_user_2, chat_channel: dm_channel)
+
+ expect { described_class.new(msg, msg.created_at).notify_new }.not_to change {
+ @dm_user_1.notifications.count
+ }
+ end
+
+ it "adds correct data to the notification" do
+ msg = build_cooked_msg("Hey guys", @dm_user_1, chat_channel: dm_channel)
+ to_notify = described_class.new(msg, msg.created_at).notify_new
+ notification = Notification.where(user: @dm_user_2).first
+ data = notification.data_hash
+
+ expect(data[:username]).to eq(@dm_user_1.username)
+ expect(data[:chat_channel_id]).to eq(dm_channel.id)
+ expect(data[:chat_message_id]).to eq(msg.id)
+ expect(data[:is_direct_message_channel]).to eq(true)
+ expect(data[:is_group_message]).to eq(false)
+ expect(data[:user_ids]).to eq([@dm_user_1.id])
+ end
+ end
end
end