UX: update chat channel sorting to include unread threads (#29617)

Adds channels with unread threads (watching/tracking) to the sorting logic for both public and direct message channels.

Previously channels with unread threads could easily be missed as we didn't bump them to the top when new thread replies were created.

We are also adding a blue unread badge next to DM channels when there is an unread thread, as previously they weren't appearing as unread within the DMs tab (they only showed within the My Threads section).
This commit is contained in:
David Battersby 2024-11-08 12:31:03 +04:00 committed by GitHub
parent f573fd8f5e
commit 534e8c1628
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 167 additions and 57 deletions

View File

@ -11,44 +11,45 @@ export default class ChatChannelUnreadIndicator extends Component {
get showUnreadIndicator() { get showUnreadIndicator() {
return ( return (
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 this.args.channel.unreadThreadsCountSinceLastViewed > 0
// the channel and a new unread thread comes in.
(this.args.channel.isCategoryChannel &&
this.chat.activeChannel?.id !== this.args.channel.id &&
this.args.channel.unreadThreadsCountSinceLastViewed > 0)
); );
} }
get unreadCount() { get urgentCount() {
if (this.#hasChannelMentions()) { if (this.hasChannelMentions) {
return this.args.channel.tracking.mentionCount; return this.args.channel.tracking.mentionCount;
} }
if (this.#hasWatchedThreads()) { if (this.hasWatchedThreads) {
return this.args.channel.tracking.watchedThreadsUnreadCount; return this.args.channel.tracking.watchedThreadsUnreadCount;
} }
return this.args.channel.tracking.unreadCount; return this.args.channel.tracking.unreadCount;
} }
get isUrgent() { get isUrgent() {
if (this.#onlyMentions()) { if (this.onlyMentions) {
return this.#hasChannelMentions(); return this.hasChannelMentions;
} }
return ( return (
this.args.channel.isDirectMessageChannel || this.isDirectMessage || this.hasChannelMentions || this.hasWatchedThreads
this.#hasChannelMentions() ||
this.#hasWatchedThreads()
); );
} }
#hasChannelMentions() { get isDirectMessage() {
return (
this.args.channel.isDirectMessageChannel &&
this.args.channel.tracking.unreadCount > 0
);
}
get hasChannelMentions() {
return this.args.channel.tracking.mentionCount > 0; return this.args.channel.tracking.mentionCount > 0;
} }
#hasWatchedThreads() { get hasWatchedThreads() {
return this.args.channel.tracking.watchedThreadsUnreadCount > 0; return this.args.channel.tracking.watchedThreadsUnreadCount > 0;
} }
#onlyMentions() { get onlyMentions() {
return hasChatIndicator(this.currentUser).ONLY_MENTIONS; return hasChatIndicator(this.currentUser).ONLY_MENTIONS;
} }
@ -62,7 +63,7 @@ export default class ChatChannelUnreadIndicator extends Component {
> >
<div class="chat-channel-unread-indicator__number">{{#if <div class="chat-channel-unread-indicator__number">{{#if
this.isUrgent this.isUrgent
}}{{this.unreadCount}}{{else}}&nbsp;{{/if}}</div> }}{{this.urgentCount}}{{else}}&nbsp;{{/if}}</div>
</div> </div>
{{/if}} {{/if}}
</template> </template>

View File

@ -198,24 +198,37 @@ export default class ChatChannelsManager extends Service {
#sortChannelsByActivity(channels) { #sortChannelsByActivity(channels) {
return channels.sort((a, b) => { return channels.sort((a, b) => {
// if both channels have mention count, sort by slug const stats = {
// otherwise prioritize channel with mention count a: {
if (a.tracking.mentionCount > 0 && b.tracking.mentionCount > 0) { urgent:
a.tracking.mentionCount + a.tracking.watchedThreadsUnreadCount,
unread: a.tracking.unreadCount + a.threadsManager.unreadThreadCount,
},
b: {
urgent:
b.tracking.mentionCount + b.tracking.watchedThreadsUnreadCount,
unread: b.tracking.unreadCount + b.threadsManager.unreadThreadCount,
},
};
// if both channels have urgent count, sort by slug
// otherwise prioritize channel with urgent count
if (stats.a.urgent > 0 && stats.b.urgent > 0) {
return a.slug?.localeCompare?.(b.slug); return a.slug?.localeCompare?.(b.slug);
} }
if (a.tracking.mentionCount > 0 || b.tracking.mentionCount > 0) { if (stats.a.urgent > 0 || stats.b.urgent > 0) {
return a.tracking.mentionCount > b.tracking.mentionCount ? -1 : 1; return stats.a.urgent > stats.b.urgent ? -1 : 1;
} }
// if both channels have unread count, sort by slug // if both channels have unread messages or threads, sort by slug
// otherwise prioritize channel with unread count // otherwise prioritize channel with unread count
if (a.tracking.unreadCount > 0 && b.tracking.unreadCount > 0) { if (stats.a.unread > 0 && stats.b.unread > 0) {
return a.slug?.localeCompare?.(b.slug); return a.slug?.localeCompare?.(b.slug);
} }
if (a.tracking.unreadCount > 0 || b.tracking.unreadCount > 0) { if (stats.a.unread > 0 || stats.b.unread > 0) {
return a.tracking.unreadCount > b.tracking.unreadCount ? -1 : 1; return stats.a.unread > stats.b.unread ? -1 : 1;
} }
return a.slug?.localeCompare?.(b.slug); return a.slug?.localeCompare?.(b.slug);
@ -232,14 +245,30 @@ export default class ChatChannelsManager extends Service {
return -1; return -1;
} }
if (a.tracking.unreadCount === b.tracking.unreadCount) { 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;
}
if (
a.threadsManager.unreadThreadCount > 0 ||
b.threadsManager.unreadThreadCount > 0
) {
return a.threadsManager.unreadThreadCount >
b.threadsManager.unreadThreadCount
? -1
: 1;
}
return new Date(a.lastMessage.createdAt) > return new Date(a.lastMessage.createdAt) >
new Date(b.lastMessage.createdAt) new Date(b.lastMessage.createdAt)
? -1 ? -1
: 1; : 1;
} else {
return a.tracking.unreadCount > b.tracking.unreadCount ? -1 : 1;
}
}); });
} }
} }

View File

@ -220,7 +220,12 @@ Fabricator(:chat_thread, class_name: "Chat::Thread") do
thread.channel = original_message.chat_channel thread.channel = original_message.chat_channel
end end
transient :with_replies, :channel, :original_message_user, :old_om, use_service: false transient :with_replies,
:channel,
:original_message_user,
:old_om,
use_service: false,
notification_level: :tracking
original_message do |attrs| original_message do |attrs|
Fabricate( Fabricate(
@ -239,7 +244,7 @@ Fabricator(:chat_thread, class_name: "Chat::Thread") do
attrs[:created_at] = 1.week.ago if transients[:old_om] attrs[:created_at] = 1.week.ago if transients[:old_om]
thread.original_message.update!(**attrs) thread.original_message.update!(**attrs)
thread.add(thread.original_message_user) thread.add(thread.original_message_user, notification_level: transients[:notification_level])
if transients[:with_replies] if transients[:with_replies]
Fabricate Fabricate

View File

@ -36,8 +36,20 @@ RSpec.describe "List channels | Drawer", type: :system do
context "when multiple channels are present" do context "when multiple channels are present" do
fab!(:channel_1) { Fabricate(:category_channel, name: "a channel") } fab!(:channel_1) { Fabricate(:category_channel, name: "a channel") }
fab!(:channel_2) { Fabricate(:category_channel, name: "b channel") } fab!(:channel_2) { Fabricate(:category_channel, name: "b channel") }
fab!(:channel_3) { Fabricate(:category_channel, name: "c channel") } fab!(:channel_3) { Fabricate(:category_channel, name: "c channel", threading_enabled: true) }
fab!(:channel_4) { Fabricate(:category_channel, name: "d channel") } fab!(:channel_4) { Fabricate(:category_channel, name: "d channel") }
fab!(:message) do
Fabricate(:chat_message, chat_channel: channel_3, user: current_user, use_service: true)
end
fab!(:thread) do
Fabricate(
:chat_thread,
channel: channel_3,
original_message: message,
with_replies: 2,
use_service: true,
)
end
before do before do
channel_1.add(current_user) channel_1.add(current_user)
@ -46,32 +58,31 @@ RSpec.describe "List channels | Drawer", type: :system do
channel_4.add(current_user) channel_4.add(current_user)
end end
it "sorts them by urgent, unread, then by slug" do it "sorts them by urgent, unread messages or threads, then by slug" do
drawer_page.visit_index drawer_page.visit_index
Fabricate( Fabricate(
:chat_message, :chat_message,
chat_channel: channel_1, chat_channel: channel_4,
created_at: 5.minutes.ago,
use_service: true,
)
Fabricate(
:chat_message,
chat_channel: channel_2,
created_at: 2.minutes.ago,
use_service: true,
)
Fabricate(
:chat_message,
chat_channel: channel_3,
message: "@#{current_user.username}", message: "@#{current_user.username}",
use_service: true, use_service: true,
) )
expect(drawer_page).to have_channel_at_position(channel_4, 1)
expect(drawer_page).to have_channel_at_position(channel_3, 2)
expect(drawer_page).to have_channel_at_position(channel_1, 3)
expect(drawer_page).to have_channel_at_position(channel_2, 4)
end
it "sorts by slug when multiple channels have the same unread count" do
drawer_page.visit_index
Fabricate(:chat_message, chat_channel: channel_2, use_service: true)
Fabricate(:chat_message, chat_channel: channel_4, use_service: true) Fabricate(:chat_message, chat_channel: channel_4, use_service: true)
expect(drawer_page).to have_channel_at_position(channel_3, 1) expect(drawer_page).to have_channel_at_position(channel_2, 1)
expect(drawer_page).to have_channel_at_position(channel_1, 2) expect(drawer_page).to have_channel_at_position(channel_3, 2)
expect(drawer_page).to have_channel_at_position(channel_2, 3) expect(drawer_page).to have_channel_at_position(channel_4, 3)
expect(drawer_page).to have_channel_at_position(channel_1, 4)
end end
end end
end end
@ -108,19 +119,71 @@ RSpec.describe "List channels | Drawer", type: :system do
context "when multiple channels are present" do context "when multiple channels are present" do
fab!(:user_1) { Fabricate(:user) } fab!(:user_1) { Fabricate(:user) }
fab!(:user_2) { Fabricate(:user) }
fab!(:user_3) { Fabricate(:user) }
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [current_user]) } fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [current_user]) }
fab!(:dm_channel_2) { Fabricate(:direct_message_channel, users: [current_user, user_1]) } fab!(:dm_channel_2) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
fab!(:dm_channel_3) { Fabricate(:direct_message_channel, users: [current_user, user_2]) }
before do fab!(:dm_channel_4) { Fabricate(:direct_message_channel, users: [current_user, user_3]) }
Fabricate(:chat_message, chat_channel: dm_channel_2, user: user_1, use_service: true)
end
it "sorts them by latest activity" do it "sorts them by latest activity" do
drawer_page.visit_index drawer_page.visit_index
drawer_page.click_direct_messages drawer_page.click_direct_messages
Fabricate(:chat_message, chat_channel: dm_channel_2, user: user_1, use_service: true)
# last message was read but the created at date is used for sorting
message =
Fabricate(:chat_message, chat_channel: dm_channel_4, user: user_3, use_service: true)
dm_channel_4.membership_for(current_user).mark_read!(message.id)
expect(drawer_page).to have_channel_at_position(dm_channel_2, 1) expect(drawer_page).to have_channel_at_position(dm_channel_2, 1)
expect(drawer_page).to have_channel_at_position(dm_channel_1, 2) expect(drawer_page).to have_urgent_channel(dm_channel_2)
expect(drawer_page).to have_channel_at_position(dm_channel_4, 2)
expect(drawer_page).to have_channel_at_position(dm_channel_1, 3)
expect(drawer_page).to have_channel_at_position(dm_channel_3, 4)
end
it "sorts channels with threads by urgency" do
drawer_page.visit_index
drawer_page.click_direct_messages
Fabricate(
:chat_thread,
notification_level: :watching,
original_message:
Fabricate(
:chat_message,
chat_channel: dm_channel_4,
user: current_user,
use_service: true,
),
with_replies: 2,
use_service: true,
)
Fabricate(
:chat_thread,
original_message:
Fabricate(
:chat_message,
chat_channel: dm_channel_3,
user: current_user,
use_service: true,
),
with_replies: 2,
use_service: true,
)
expect(drawer_page).to have_channel_at_position(dm_channel_4, 1)
expect(drawer_page).to have_urgent_channel(dm_channel_4)
expect(drawer_page).to have_channel_at_position(dm_channel_3, 2)
expect(drawer_page).to have_unread_channel(dm_channel_3)
expect(drawer_page).to have_channel_at_position(dm_channel_1, 3)
expect(drawer_page).to have_channel_at_position(dm_channel_2, 4)
end end
end end
end end

View File

@ -37,9 +37,17 @@ module PageObjects
has_no_css?(channel_row_selector(channel)) has_no_css?(channel_row_selector(channel))
end end
def has_unread_channel?(channel, count: nil, wait: Capybara.default_max_wait_time) def has_unread_channel?(
channel,
count: nil,
urgent: false,
wait: Capybara.default_max_wait_time
)
unread_indicator_selector = unread_indicator_selector =
"#{channel_row_selector(channel)} .chat-channel-unread-indicator" "#{channel_row_selector(channel)} .chat-channel-unread-indicator"
unread_indicator_selector += ".-urgent" if urgent
has_css?(unread_indicator_selector) && has_css?(unread_indicator_selector) &&
if count if count
has_css?( has_css?(

View File

@ -70,6 +70,10 @@ module PageObjects
channels_index.has_no_unread_channel?(channel) channels_index.has_no_unread_channel?(channel)
end end
def has_urgent_channel?(channel)
channels_index.has_unread_channel?(channel, urgent: true)
end
def has_user_threads_section? def has_user_threads_section?
has_css?("#c-footer-threads") has_css?("#c-footer-threads")
end end