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 { service } from "@ember/service";
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 {
@service chat;
@ -10,48 +11,41 @@ export default class ChatChannelUnreadIndicator extends Component {
get showUnreadIndicator() {
return (
this.args.channel.tracking.unreadCount > 0 ||
this.args.channel.tracking.mentionCount > 0 ||
this.args.channel.unreadThreadsCountSinceLastViewed > 0
this.args.channel.tracking.unreadCount +
this.args.channel.tracking.mentionCount +
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() {
if (this.hasChannelMentions) {
return this.args.channel.tracking.mentionCount;
}
if (this.hasWatchedThreads) {
return this.args.channel.tracking.watchedThreadsUnreadCount;
}
return this.args.channel.tracking.unreadCount;
return this.args.channel.isDirectMessageChannel
? this.directUrgentCount
: this.publicUrgentCount;
}
get isUrgent() {
if (this.onlyMentions) {
return this.hasChannelMentions;
}
return (
this.isDirectMessage || this.hasChannelMentions || this.hasWatchedThreads
);
return this.urgentCount > 0;
}
get isDirectMessage() {
return (
this.args.channel.isDirectMessageChannel &&
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;
get urgentBadgeCount() {
let totalCount = this.urgentCount;
return totalCount > MAX_UNREAD_COUNT ? `${MAX_UNREAD_COUNT}+` : totalCount;
}
<template>
@ -62,9 +56,9 @@ export default class ChatChannelUnreadIndicator extends Component {
(if this.isUrgent "-urgent")
}}
>
<div class="chat-channel-unread-indicator__number">{{#if
this.isUrgent
}}{{this.urgentCount}}{{else}}&nbsp;{{/if}}</div>
<div class="chat-channel-unread-indicator__number">
{{#if this.isUrgent}}{{this.urgentBadgeCount}}{{else}}&nbsp;{{/if}}
</div>
</div>
{{/if}}
</template>

View File

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

View File

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

View File

@ -59,7 +59,14 @@ export default class ChatablesLoader {
]
.map((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;
})
.slice(0, MAX_RESULTS);
@ -74,24 +81,32 @@ export default class ChatablesLoader {
let chatable;
if (channel.chatable?.users?.length === 1) {
chatable = ChatChatable.createUser(channel.chatable.users[0]);
chatable.tracking = this.#injectTracking(chatable);
} else {
chatable = ChatChatable.createChannel(channel);
chatable.tracking = channel.tracking;
}
chatable.tracking = channel.tracking;
chatable.unread_thread_count =
channel.unreadThreadsCountSinceLastViewed;
return chatable;
})
.filter(Boolean)
.slice(0, MAX_RESULTS);
}
#injectTracking(chatable) {
if (!chatable.type === "channel") {
#findChannel(chatable) {
if (!["user", "channel"].includes(chatable.type)) {
return;
}
return this.chatChannelsManager.allChannels.find(
(channel) => channel.id === chatable.model.id
)?.tracking;
const { allChannels } = this.chatChannelsManager;
if (chatable.type === "user") {
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 { service } from "@ember/service";
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 concatClass from "discourse/helpers/concat-class";
import userStatus from "discourse/helpers/user-status";
import { i18n } from "discourse-i18n";
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>
<div
class="chat-message-creator__chatable -user"
@ -29,8 +46,10 @@ export default class ChatableUser extends Component {
<ChatUserAvatar @user={{@item.model}} @interactive={{false}} />
<ChatUserDisplayName @user={{@item.model}} />
{{#if (gt @item.tracking.unreadCount 0)}}
<div class="unread-indicator -urgent"></div>
{{#if this.showIndicator}}
<div
class={{concatClass "unread-indicator" (if this.isUrgent "-urgent")}}
></div>
{{/if}}
{{userStatus @item.model currentUser=this.currentUser}}

View File

@ -293,7 +293,6 @@ export default {
const SidebarChatDirectMessagesSectionLink = class extends BaseCustomSidebarSectionLink {
route = "chat.channel";
suffixType = "icon";
suffixCSSClass = "urgent";
hoverType = "icon";
hoverValue = "xmark";
hoverTitle = i18n("chat.direct_messages.close");
@ -424,7 +423,18 @@ export default {
}
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() {

View File

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

View File

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

View File

@ -122,7 +122,7 @@ RSpec.describe "Mobile Chat footer", type: :system, mobile: true do
context "for direct messages" do
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
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")
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
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}\"]",
)
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

View File

@ -6,6 +6,7 @@ RSpec.describe "Message notifications - with sidebar", type: :system do
let!(:chat_page) { PageObjects::Pages::Chat.new }
let!(:channel_page) { PageObjects::Pages::ChatChannel.new }
let!(:thread_page) { PageObjects::Pages::ChatThread.new }
let!(:sidebar) { PageObjects::Pages::Sidebar.new }
before do
SiteSetting.navigation_menu = "sidebar"
@ -233,36 +234,75 @@ RSpec.describe "Message notifications - with sidebar", type: :system do
end
end
context "with a thread" do
fab!(:channel) { Fabricate(:category_channel, threading_enabled: true) }
context "with threads" do
fab!(:other_user) { Fabricate(:user) }
fab!(:thread) do
chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
end
before do
channel.membership_for(current_user).mark_read!
thread.membership_for(current_user).mark_read!
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],
)
context "with public channels" do
fab!(:channel) { Fabricate(:category_channel, threading_enabled: true) }
fab!(:thread) do
chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
end
context "when a reply is created" do
it "shows the unread indicator in the header" do
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
before do
channel.membership_for(current_user).mark_read!
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")
end
it "shows the unread badge in chat header" do
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