FIX: show chat thread notifications for direct message channels (#29414)

When adding threads to DM channels in #29170 we intentionally didn't add them to the My Threads section. However this makes it easy to miss notifications as we don't get the new thread badge on the sidebar and footer tabs (drawer/mobile). However they were also missing from the chat header and sidebar too, which is fixed with this PR.

When a new thread or a reply to an existing thread is created within a DM channel (either 1:1 or group), we now show the standard badges like we do for public channels.

We now also show the green dot in the sidebar for My Threads and public channels when they contain an unread watched thread.
This commit is contained in:
David Battersby 2024-10-31 10:50:11 +04:00 committed by GitHub
parent 1a3b9a7352
commit ec480e99ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 89 additions and 44 deletions

View File

@ -46,7 +46,7 @@ module Chat
INNER JOIN user_chat_thread_memberships ON user_chat_thread_memberships.thread_id = chat_threads.id INNER JOIN user_chat_thread_memberships ON user_chat_thread_memberships.thread_id = chat_threads.id
INNER JOIN user_chat_channel_memberships ON user_chat_channel_memberships.chat_channel_id = chat_messages.chat_channel_id INNER JOIN user_chat_channel_memberships ON user_chat_channel_memberships.chat_channel_id = chat_messages.chat_channel_id
INNER JOIN chat_messages AS original_message ON original_message.id = chat_threads.original_message_id INNER JOIN chat_messages AS original_message ON original_message.id = chat_threads.original_message_id
AND chat_messages.thread_id = memberships.thread_id WHERE chat_messages.thread_id = memberships.thread_id
AND chat_messages.user_id != :user_id AND chat_messages.user_id != :user_id
AND user_chat_thread_memberships.user_id = :user_id AND user_chat_thread_memberships.user_id = :user_id
AND chat_messages.id > COALESCE(user_chat_thread_memberships.last_read_message_id, 0) AND chat_messages.id > COALESCE(user_chat_thread_memberships.last_read_message_id, 0)

View File

@ -24,9 +24,11 @@ module Chat
end end
def inject_unread_thread_overview(structured:, guardian:) def inject_unread_thread_overview(structured:, guardian:)
channel_ids =
structured[:public_channels].map(&:id) + structured[:direct_message_channels].map(&:id)
structured[:unread_thread_overview] = ::Chat::TrackingStateReportQuery.call( structured[:unread_thread_overview] = ::Chat::TrackingStateReportQuery.call(
guardian: guardian, guardian: guardian,
channel_ids: structured[:public_channels].map(&:id), channel_ids: channel_ids,
include_threads: true, include_threads: true,
include_read: false, include_read: false,
include_last_reply_details: true, include_last_reply_details: true,

View File

@ -77,13 +77,7 @@ module Chat
::Chat::Channel ::Chat::Channel
.joins(:user_chat_channel_memberships) .joins(:user_chat_channel_memberships)
.where(user_chat_channel_memberships: { user_id: guardian.user.id, following: true }) .where(user_chat_channel_memberships: { user_id: guardian.user.id, following: true })
.where( .where({ threading_enabled: true, status: ::Chat::Channel.statuses[:open] })
{
chatable_type: "Category",
threading_enabled: true,
status: ::Chat::Channel.statuses[:open],
},
)
.select(:id), .select(:id),
) )
.where("original_message.chat_channel_id = chat_threads.channel_id") .where("original_message.chat_channel_id = chat_threads.channel_id")

View File

@ -13,7 +13,8 @@ export default class ChatChannelUnreadIndicator extends Component {
this.args.channel.tracking.unreadCount > 0 || this.args.channel.tracking.unreadCount > 0 ||
// We want to do this so we don't show a blue dot if the user is inside // We want to do this so we don't show a blue dot if the user is inside
// the channel and a new unread thread comes in. // the channel and a new unread thread comes in.
(this.chat.activeChannel?.id !== this.args.channel.id && (this.args.channel.isCategoryChannel &&
this.chat.activeChannel?.id !== this.args.channel.id &&
this.args.channel.unreadThreadsCountSinceLastViewed > 0) this.args.channel.unreadThreadsCountSinceLastViewed > 0)
); );
} }

View File

@ -63,7 +63,6 @@ export default {
prefixType = "icon"; prefixType = "icon";
prefixValue = "discourse-threads"; prefixValue = "discourse-threads";
suffixType = "icon"; suffixType = "icon";
suffixCSSClass = "unread";
constructor() { constructor() {
super(...arguments); super(...arguments);
@ -74,12 +73,20 @@ export default {
} }
get suffixValue() { get suffixValue() {
return chatChannelsManager.publicMessageChannels.some( return chatChannelsManager.allChannels.some(
(channel) => channel.unreadThreadsCount > 0 (channel) => channel.unreadThreadsCount > 0
) )
? "circle" ? "circle"
: ""; : "";
} }
get suffixCSSClass() {
return chatChannelsManager.allChannels.some(
(channel) => channel.tracking.watchedThreadsUnreadCount > 0
)
? "urgent"
: "unread";
}
}; };
const SidebarChatMyThreadsSection = class extends BaseCustomSidebarSection { const SidebarChatMyThreadsSection = class extends BaseCustomSidebarSection {
@ -196,7 +203,8 @@ export default {
} }
get suffixCSSClass() { get suffixCSSClass() {
return this.channel.tracking.mentionCount > 0 return this.channel.tracking.mentionCount > 0 ||
this.channel.tracking.watchedThreadsUnreadCount > 0
? "urgent" ? "urgent"
: "unread"; : "unread";
} }

View File

@ -111,7 +111,7 @@ export default class ChatChannel {
} }
get unreadThreadsCount() { get unreadThreadsCount() {
return Array.from(this.threadsManager.unreadThreadOverview.values()).length; return this.threadsManager.unreadThreadCount;
} }
get watchedThreadsUnreadCount() { get watchedThreadsUnreadCount() {

View File

@ -108,9 +108,7 @@ export default class ChatChannelsManager extends Service {
@cached @cached
get hasThreadedChannels() { get hasThreadedChannels() {
return this.publicMessageChannels?.some( return this.allChannels?.some((channel) => channel.threadingEnabled);
(channel) => channel.threadingEnabled
);
} }
get allChannels() { get allChannels() {

View File

@ -78,13 +78,11 @@ export default class ChatTrackingStateManager extends Service {
} }
get hasUnreadThreads() { get hasUnreadThreads() {
return this.#publicChannels.some( return this.#allChannels.some((channel) => channel.unreadThreadsCount > 0);
(channel) => channel.unreadThreadsCount > 0
);
} }
get watchedThreadsUnreadCount() { get watchedThreadsUnreadCount() {
return this.#publicChannels.reduce((unreadCount, channel) => { return this.#allChannels.reduce((unreadCount, channel) => {
return unreadCount + channel.tracking.watchedThreadsUnreadCount; return unreadCount + channel.tracking.watchedThreadsUnreadCount;
}, 0); }, 0);
} }
@ -129,4 +127,8 @@ export default class ChatTrackingStateManager extends Service {
get #directMessageChannels() { get #directMessageChannels() {
return this.chatChannelsManager.directMessageChannels; return this.chatChannelsManager.directMessageChannels;
} }
get #allChannels() {
return this.chatChannelsManager.allChannels;
}
} }

View File

@ -11,8 +11,11 @@ RSpec.describe Chat::CreateThread do
subject(:result) { described_class.call(params:, **dependencies) } subject(:result) { described_class.call(params:, **dependencies) }
fab!(:current_user) { Fabricate(:user) } fab!(:current_user) { Fabricate(:user) }
fab!(:another_user) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: true) } fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: true) }
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) } fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [current_user, another_user]) }
fab!(:dm_message) { Fabricate(:chat_message, chat_channel: dm_channel) }
let(:guardian) { Guardian.new(current_user) } let(:guardian) { Guardian.new(current_user) }
let(:title) { nil } let(:title) { nil }
@ -39,11 +42,20 @@ RSpec.describe Chat::CreateThread do
expect(result.membership).to eq(result.thread.membership_for(current_user)) expect(result.membership).to eq(result.thread.membership_for(current_user))
end end
it "publishes a `thread_created` MessageBus event" do it "publishes a `thread_created` MessageBus event for public channels" do
message = MessageBus.track_publish("/chat/#{channel_1.id}") { result }.first message = MessageBus.track_publish("/chat/#{channel_1.id}") { result }.first
expect(message.data["type"]).to eq("thread_created") expect(message.data["type"]).to eq("thread_created")
end end
it "publishes a `thread_created` MessageBus event for DM channels" do
params[:channel_id] = dm_channel.id
params[:original_message_id] = dm_message.id
params[:guardian] = Guardian.new(another_user)
message = MessageBus.track_publish("/chat/#{dm_channel.id}") { result }.first
expect(message.data["type"]).to eq("thread_created")
end
it "triggers a discourse event `chat_thread_created`" do it "triggers a discourse event `chat_thread_created`" do
event = DiscourseEvent.track_events(:chat_thread_created) { result }.first event = DiscourseEvent.track_events(:chat_thread_created) { result }.first

View File

@ -133,36 +133,64 @@ RSpec.describe "Mobile Chat footer", type: :system, mobile: true do
end end
context "for my threads" do context "for my threads" do
fab!(:thread) { Fabricate(:chat_thread, channel: channel, original_message: message) } context "with public channels" do
fab!(:thread_message) { Fabricate(:chat_message, chat_channel: channel, thread: thread) } fab!(:thread) { Fabricate(:chat_thread, channel: channel, original_message: message) }
fab!(:thread_message) { Fabricate(:chat_message, chat_channel: channel, thread: thread) }
before { SiteSetting.chat_threads_enabled = true } before { SiteSetting.chat_threads_enabled = true }
it "is unread" do it "is unread" do
visit("/") visit("/")
chat_page.open_from_header chat_page.open_from_header
expect(page).to have_css("#c-footer-threads .c-unread-indicator") expect(page).to have_css("#c-footer-threads .c-unread-indicator")
end
it "is not unread when thread is from a muted channel" do
channel.membership_for(current_user).update!(muted: true)
visit("/")
chat_page.open_from_header
expect(page).to have_no_css("#c-footer-threads .c-unread-indicator")
end
it "is urgent for watched thread messages" do
thread.membership_for(current_user).update!(
notification_level: ::Chat::NotificationLevels.all[:watching],
)
visit("/")
chat_page.open_from_header
expect(page).to have_css("#c-footer-threads .c-unread-indicator.-urgent")
end
end end
it "is not unread when thread is from a muted channel" do context "with direct messages" do
channel.membership_for(current_user).update!(muted: true) fab!(:dm_channel) do
Fabricate(
:direct_message_channel,
threading_enabled: true,
users: [current_user, other_user],
)
end
fab!(:dm_message) { Fabricate(:chat_message, chat_channel: dm_channel, user: current_user) }
fab!(:dm_thread) do
Fabricate(:chat_thread, channel: dm_channel, original_message: dm_message)
end
fab!(:dm_thread_message) do
Fabricate(:chat_message, chat_channel: dm_channel, thread: dm_thread, user: other_user)
end
visit("/") before { SiteSetting.chat_threads_enabled = true }
chat_page.open_from_header
expect(page).to have_no_css("#c-footer-threads .c-unread-indicator") it "is unread" do
end visit("/")
chat_page.open_from_header
it "is urgent for watched thread messages" do expect(page).to have_css("#c-footer-threads .c-unread-indicator")
thread.membership_for(current_user).update!( end
notification_level: ::Chat::NotificationLevels.all[:watching],
)
visit("/")
chat_page.open_from_header
expect(page).to have_css("#c-footer-threads .c-unread-indicator.-urgent")
end end
end end
end end