FIX: show urgent badge for mentions in DM threads (#29821)

When thread tracking level is Normal in a DM channel, we should still show notification badges to the mentioned user.
This commit is contained in:
David Battersby 2024-11-29 12:52:55 +04:00 committed by GitHub
parent 1497b298d2
commit 3cde55b76f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 265 additions and 98 deletions

View File

@ -1,7 +1,8 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { service } from "@ember/service"; import { service } from "@ember/service";
import concatClass from "discourse/helpers/concat-class"; import concatClass from "discourse/helpers/concat-class";
import { hasChatIndicator } from "../lib/chat-user-preferences";
const MAX_UNREAD_COUNT = 99;
export default class ChatChannelUnreadIndicator extends Component { export default class ChatChannelUnreadIndicator extends Component {
@service chat; @service chat;
@ -10,48 +11,41 @@ export default class ChatChannelUnreadIndicator extends Component {
get showUnreadIndicator() { get showUnreadIndicator() {
return ( return (
this.args.channel.tracking.unreadCount > 0 || this.args.channel.tracking.unreadCount +
this.args.channel.tracking.mentionCount > 0 || this.args.channel.tracking.mentionCount +
this.args.channel.unreadThreadsCountSinceLastViewed > 0 this.args.channel.unreadThreadsCountSinceLastViewed >
0
);
}
get publicUrgentCount() {
return (
this.args.channel.tracking.mentionCount +
this.args.channel.tracking.watchedThreadsUnreadCount
);
}
get directUrgentCount() {
return (
this.args.channel.tracking.unreadCount +
this.args.channel.tracking.mentionCount +
this.args.channel.tracking.watchedThreadsUnreadCount
); );
} }
get urgentCount() { get urgentCount() {
if (this.hasChannelMentions) { return this.args.channel.isDirectMessageChannel
return this.args.channel.tracking.mentionCount; ? this.directUrgentCount
} : this.publicUrgentCount;
if (this.hasWatchedThreads) {
return this.args.channel.tracking.watchedThreadsUnreadCount;
}
return this.args.channel.tracking.unreadCount;
} }
get isUrgent() { get isUrgent() {
if (this.onlyMentions) { return this.urgentCount > 0;
return this.hasChannelMentions;
}
return (
this.isDirectMessage || this.hasChannelMentions || this.hasWatchedThreads
);
} }
get isDirectMessage() { get urgentBadgeCount() {
return ( let totalCount = this.urgentCount;
this.args.channel.isDirectMessageChannel && return totalCount > MAX_UNREAD_COUNT ? `${MAX_UNREAD_COUNT}+` : totalCount;
this.args.channel.tracking.unreadCount > 0
);
}
get hasChannelMentions() {
return this.args.channel.tracking.mentionCount > 0;
}
get hasWatchedThreads() {
return this.args.channel.tracking.watchedThreadsUnreadCount > 0;
}
get onlyMentions() {
return hasChatIndicator(this.currentUser).ONLY_MENTIONS;
} }
<template> <template>
@ -62,9 +56,9 @@ export default class ChatChannelUnreadIndicator extends Component {
(if this.isUrgent "-urgent") (if this.isUrgent "-urgent")
}} }}
> >
<div class="chat-channel-unread-indicator__number">{{#if <div class="chat-channel-unread-indicator__number">
this.isUrgent {{#if this.isUrgent}}{{this.urgentBadgeCount}}{{else}}&nbsp;{{/if}}
}}{{this.urgentCount}}{{else}}&nbsp;{{/if}}</div> </div>
</div> </div>
{{/if}} {{/if}}
</template> </template>

View File

@ -27,7 +27,10 @@ export default class FooterUnreadIndicator extends Component {
if (this.badgeType === CHANNELS_TAB) { if (this.badgeType === CHANNELS_TAB) {
return this.chatTrackingStateManager.publicChannelMentionCount; return this.chatTrackingStateManager.publicChannelMentionCount;
} else if (this.badgeType === DMS_TAB) { } else if (this.badgeType === DMS_TAB) {
return this.chatTrackingStateManager.directMessageUnreadCount; return (
this.chatTrackingStateManager.directMessageUnreadCount +
this.chatTrackingStateManager.directMessageMentionCount
);
} else if (this.badgeType === THREADS_TAB) { } else if (this.badgeType === THREADS_TAB) {
return this.chatTrackingStateManager.watchedThreadsUnreadCount; return this.chatTrackingStateManager.watchedThreadsUnreadCount;
} else { } else {

View File

@ -1,21 +1,40 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { gt, not } from "truth-helpers"; import { not } from "truth-helpers";
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title"; import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
export default class Channel extends Component { export default class Channel extends Component {
@service currentUser; @service currentUser;
get tracking() {
return this.args.item.tracking;
}
get isUrgent() { get isUrgent() {
return this.args.item.model.isDirectMessageChannel
? this.hasUnreads || this.hasUrgent
: this.hasUrgent;
}
get hasUnreads() {
return this.tracking?.unreadCount > 0;
}
get hasUrgent() {
return ( return (
this.args.item.model.isDirectMessageChannel || this.tracking?.mentionCount > 0 ||
(this.args.item.model.isCategoryChannel && this.tracking?.watchedThreadsUnreadCount > 0
this.args.item.model.tracking.mentionCount > 0) ||
(this.args.item.model.isCategoryChannel &&
this.args.item.model.tracking.watchedThreadsUnreadCount > 0)
); );
} }
get hasUnreadThreads() {
return this.args.item.unread_thread_count > 0;
}
get showIndicator() {
return this.hasUnreads || this.hasUnreadThreads || this.hasUrgent;
}
<template> <template>
<div <div
class="chat-message-creator__chatable -category-channel" class="chat-message-creator__chatable -category-channel"
@ -23,7 +42,7 @@ export default class Channel extends Component {
> >
<ChannelTitle <ChannelTitle
@channel={{@item.model}} @channel={{@item.model}}
@isUnread={{gt @item.tracking.unreadCount 0}} @isUnread={{this.showIndicator}}
@isUrgent={{this.isUrgent}} @isUrgent={{this.isUrgent}}
/> />
</div> </div>

View File

@ -59,7 +59,14 @@ export default class ChatablesLoader {
] ]
.map((item) => { .map((item) => {
const chatable = ChatChatable.create(item); const chatable = ChatChatable.create(item);
chatable.tracking = this.#injectTracking(chatable); const channel = this.#findChannel(chatable);
if (channel) {
chatable.tracking = channel.tracking;
chatable.unread_thread_count =
channel.unreadThreadsCountSinceLastViewed;
}
return chatable; return chatable;
}) })
.slice(0, MAX_RESULTS); .slice(0, MAX_RESULTS);
@ -74,24 +81,32 @@ export default class ChatablesLoader {
let chatable; let chatable;
if (channel.chatable?.users?.length === 1) { if (channel.chatable?.users?.length === 1) {
chatable = ChatChatable.createUser(channel.chatable.users[0]); chatable = ChatChatable.createUser(channel.chatable.users[0]);
chatable.tracking = this.#injectTracking(chatable);
} else { } else {
chatable = ChatChatable.createChannel(channel); chatable = ChatChatable.createChannel(channel);
chatable.tracking = channel.tracking;
} }
chatable.tracking = channel.tracking;
chatable.unread_thread_count =
channel.unreadThreadsCountSinceLastViewed;
return chatable; return chatable;
}) })
.filter(Boolean) .filter(Boolean)
.slice(0, MAX_RESULTS); .slice(0, MAX_RESULTS);
} }
#injectTracking(chatable) { #findChannel(chatable) {
if (!chatable.type === "channel") { if (!["user", "channel"].includes(chatable.type)) {
return; return;
} }
return this.chatChannelsManager.allChannels.find( const { allChannels } = this.chatChannelsManager;
(channel) => channel.id === chatable.model.id if (chatable.type === "user") {
)?.tracking; return allChannels.find(
({ chatable: { users } }) =>
users?.length === 1 && users[0].id === chatable.model.id
);
} else if (chatable.type === "channel") {
return allChannels.find(({ id }) => id === chatable.model.id);
}
} }
} }

View File

@ -1,8 +1,9 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { modifier } from "ember-modifier"; import { modifier } from "ember-modifier";
import { gt, not } from "truth-helpers"; import { not } from "truth-helpers";
import UserStatusMessage from "discourse/components/user-status-message"; import UserStatusMessage from "discourse/components/user-status-message";
import concatClass from "discourse/helpers/concat-class";
import userStatus from "discourse/helpers/user-status"; import userStatus from "discourse/helpers/user-status";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-user-avatar"; import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-user-avatar";
@ -21,6 +22,22 @@ export default class ChatableUser extends Component {
}; };
}); });
get showIndicator() {
return this.isUrgent || this.isUnread;
}
get isUrgent() {
return (
this.args.item.tracking?.unreadCount > 0 ||
this.args.item.tracking?.mentionCount > 0 ||
this.args.item.tracking?.watchedThreadsUnreadCount > 0
);
}
get isUnread() {
return this.args.item.unread_thread_count > 0;
}
<template> <template>
<div <div
class="chat-message-creator__chatable -user" class="chat-message-creator__chatable -user"
@ -29,8 +46,10 @@ export default class ChatableUser extends Component {
<ChatUserAvatar @user={{@item.model}} @interactive={{false}} /> <ChatUserAvatar @user={{@item.model}} @interactive={{false}} />
<ChatUserDisplayName @user={{@item.model}} /> <ChatUserDisplayName @user={{@item.model}} />
{{#if (gt @item.tracking.unreadCount 0)}} {{#if this.showIndicator}}
<div class="unread-indicator -urgent"></div> <div
class={{concatClass "unread-indicator" (if this.isUrgent "-urgent")}}
></div>
{{/if}} {{/if}}
{{userStatus @item.model currentUser=this.currentUser}} {{userStatus @item.model currentUser=this.currentUser}}

View File

@ -293,7 +293,6 @@ export default {
const SidebarChatDirectMessagesSectionLink = class extends BaseCustomSidebarSectionLink { const SidebarChatDirectMessagesSectionLink = class extends BaseCustomSidebarSectionLink {
route = "chat.channel"; route = "chat.channel";
suffixType = "icon"; suffixType = "icon";
suffixCSSClass = "urgent";
hoverType = "icon"; hoverType = "icon";
hoverValue = "xmark"; hoverValue = "xmark";
hoverTitle = i18n("chat.direct_messages.close"); hoverTitle = i18n("chat.direct_messages.close");
@ -424,7 +423,18 @@ export default {
} }
get suffixValue() { get suffixValue() {
return this.channel.tracking.unreadCount > 0 ? "circle" : ""; return this.channel.tracking.unreadCount > 0 ||
this.channel.unreadThreadsCountSinceLastViewed > 0
? "circle"
: "";
}
get suffixCSSClass() {
return this.channel.tracking.unreadCount > 0 ||
this.channel.tracking.mentionCount > 0 ||
this.channel.tracking.watchedThreadsUnreadCount > 0
? "urgent"
: "unread";
} }
get hoverAction() { get hoverAction() {

View File

@ -210,12 +210,12 @@ export default class ChatChannelsManager extends Service {
a: { a: {
urgent: urgent:
a.tracking.mentionCount + a.tracking.watchedThreadsUnreadCount, a.tracking.mentionCount + a.tracking.watchedThreadsUnreadCount,
unread: a.tracking.unreadCount + a.threadsManager.unreadThreadCount, unread: a.tracking.unreadCount + a.unreadThreadsCountSinceLastViewed,
}, },
b: { b: {
urgent: urgent:
b.tracking.mentionCount + b.tracking.watchedThreadsUnreadCount, b.tracking.mentionCount + b.tracking.watchedThreadsUnreadCount,
unread: b.tracking.unreadCount + b.threadsManager.unreadThreadCount, unread: b.tracking.unreadCount + b.unreadThreadsCountSinceLastViewed,
}, },
}; };
@ -253,22 +253,26 @@ export default class ChatChannelsManager extends Service {
return -1; return -1;
} }
if ( const aUrgent =
a.tracking.unreadCount + a.tracking.watchedThreadsUnreadCount > 0 || a.tracking.unreadCount +
b.tracking.unreadCount + b.tracking.watchedThreadsUnreadCount > 0 a.tracking.mentionCount +
) { a.tracking.watchedThreadsUnreadCount;
return a.tracking.unreadCount + a.tracking.watchedThreadsUnreadCount >
b.tracking.unreadCount + b.tracking.watchedThreadsUnreadCount const bUrgent =
? -1 b.tracking.unreadCount +
: 1; b.tracking.mentionCount +
b.tracking.watchedThreadsUnreadCount;
if (aUrgent > 0 || bUrgent > 0) {
return aUrgent > bUrgent ? -1 : 1;
} }
if ( if (
a.threadsManager.unreadThreadCount > 0 || a.unreadThreadsCountSinceLastViewed > 0 ||
b.threadsManager.unreadThreadCount > 0 b.unreadThreadsCountSinceLastViewed > 0
) { ) {
return a.threadsManager.unreadThreadCount > return a.unreadThreadsCountSinceLastViewed >
b.threadsManager.unreadThreadCount b.unreadThreadsCountSinceLastViewed
? -1 ? -1
: 1; : 1;
} }

View File

@ -76,7 +76,7 @@ export default class ChatTrackingStateManager extends Service {
get allChannelUrgentCount() { get allChannelUrgentCount() {
return ( return (
this.publicChannelMentionCount + this.allChannelMentionCount +
this.directMessageUnreadCount + this.directMessageUnreadCount +
this.watchedThreadsUnreadCount this.watchedThreadsUnreadCount
); );

View File

@ -122,7 +122,7 @@ RSpec.describe "Mobile Chat footer", type: :system, mobile: true do
context "for direct messages" do context "for direct messages" do
fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [current_user]) } fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [current_user]) }
fab!(:dm_message) { Fabricate(:chat_message, chat_channel: dm_channel) } fab!(:dm_message) { Fabricate(:chat_message, chat_channel: dm_channel, user: current_user) }
it "is urgent" do it "is urgent" do
visit("/") visit("/")
@ -130,6 +130,40 @@ RSpec.describe "Mobile Chat footer", type: :system, mobile: true do
expect(page).to have_css("#c-footer-direct-messages .c-unread-indicator.-urgent") expect(page).to have_css("#c-footer-direct-messages .c-unread-indicator.-urgent")
end end
context "with threads" do
fab!(:thread) { Fabricate(:chat_thread, channel: dm_channel, original_message: dm_message) }
before do
SiteSetting.chat_threads_enabled = true
dm_channel.membership_for(current_user).mark_read!(dm_message.id)
end
it "is urgent for thread mentions" do
Jobs.run_immediately!
thread.membership_for(current_user).update!(
notification_level: ::Chat::NotificationLevels.all[:normal],
)
visit("/")
chat_page.open_from_header
expect(page).to have_no_css("#c-footer-direct-messages .c-unread-indicator.-urgent")
Fabricate(
:chat_message_with_service,
chat_channel: dm_channel,
thread: thread,
message: "hello @#{current_user.username}",
)
expect(page).to have_css(
"#c-footer-direct-messages .c-unread-indicator.-urgent",
text: "1",
)
end
end
end end
context "for my threads" do context "for my threads" do

View File

@ -177,6 +177,35 @@ RSpec.describe "Message notifications - mobile", type: :system, mobile: true do
".chat-channel-row:nth-child(2)[data-chat-channel-id=\"#{dm_channel_1.id}\"]", ".chat-channel-row:nth-child(2)[data-chat-channel-id=\"#{dm_channel_1.id}\"]",
) )
end end
context "with threads" do
fab!(:message) do
Fabricate(:chat_message, chat_channel: dm_channel_1, user: current_user)
end
fab!(:thread) do
Fabricate(:chat_thread, channel: dm_channel_1, original_message: message)
end
before { dm_channel_1.membership_for(current_user).mark_read!(message.id) }
it "shows urgent badge for mentions" do
Jobs.run_immediately!
visit("/chat/direct-messages")
expect(channels_index_page).to have_no_unread_channel(dm_channel_1)
Fabricate(
:chat_message_with_service,
chat_channel: dm_channel_1,
thread: thread,
message: "hello @#{current_user.username}",
user: user_1,
)
expect(channels_index_page).to have_unread_channel(dm_channel_1, urgent: true)
end
end
end end
end end

View File

@ -6,6 +6,7 @@ RSpec.describe "Message notifications - with sidebar", type: :system do
let!(:chat_page) { PageObjects::Pages::Chat.new } let!(:chat_page) { PageObjects::Pages::Chat.new }
let!(:channel_page) { PageObjects::Pages::ChatChannel.new } let!(:channel_page) { PageObjects::Pages::ChatChannel.new }
let!(:thread_page) { PageObjects::Pages::ChatThread.new } let!(:thread_page) { PageObjects::Pages::ChatThread.new }
let!(:sidebar) { PageObjects::Pages::Sidebar.new }
before do before do
SiteSetting.navigation_menu = "sidebar" SiteSetting.navigation_menu = "sidebar"
@ -233,36 +234,75 @@ RSpec.describe "Message notifications - with sidebar", type: :system do
end end
end end
context "with a thread" do context "with threads" do
fab!(:channel) { Fabricate(:category_channel, threading_enabled: true) }
fab!(:other_user) { Fabricate(:user) } fab!(:other_user) { Fabricate(:user) }
fab!(:thread) do
chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
end
before do context "with public channels" do
channel.membership_for(current_user).mark_read! fab!(:channel) { Fabricate(:category_channel, threading_enabled: true) }
thread.membership_for(current_user).mark_read! fab!(:thread) do
chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
visit("/")
end
context "when chat_header_indicator_preference is 'all_new'" do
before do
current_user.user_option.update!(
chat_header_indicator_preference:
UserOption.chat_header_indicator_preferences[:all_new],
)
end end
context "when a reply is created" do before do
it "shows the unread indicator in the header" do channel.membership_for(current_user).mark_read!
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator") thread.membership_for(current_user).mark_read!
create_message(thread: thread, creator: other_user) visit("/")
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator") it "shows the unread badge in chat header" do
end expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
create_message(thread: thread, creator: other_user, text: "this is a test")
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator")
end
end
context "with direct message channels" do
fab!(:dm_channel) do
Fabricate(:direct_message_channel, users: [current_user, other_user])
end
fab!(:thread) do
chat_thread_chain_bootstrap(channel: dm_channel, users: [current_user, other_user])
end
before do
dm_channel.membership_for(current_user).mark_read!
thread.membership_for(current_user).mark_read!
visit("/")
end
it "shows the unread indicator in the sidebar for tracked threads" do
expect(page).to have_no_css(".sidebar-row.channel-#{dm_channel.id} .unread")
create_message(channel: dm_channel, thread: thread, creator: other_user)
expect(page).to have_css(".sidebar-row.channel-#{dm_channel.id} .unread")
end
it "shows the urgent indicator in the sidebar for tracked threads" do
expect(page).to have_no_css(".sidebar-row.channel-#{dm_channel.id} .urgent")
thread.membership_for(current_user).update!(notification_level: :watching)
create_message(channel: dm_channel, thread: thread, creator: other_user)
expect(page).to have_css(".sidebar-row.channel-#{dm_channel.id} .urgent")
end
it "shows the urgent indicator in the chat sidebar for mentions" do
expect(page).to have_no_css(".sidebar-row.channel-#{dm_channel.id} .urgent")
create_message(
channel: dm_channel,
thread: thread,
creator: other_user,
text: "hey @#{current_user.username}",
)
expect(page).to have_css(".sidebar-row.channel-#{dm_channel.id} .urgent")
end end
end end
end end