discourse/plugins/chat/test/javascripts/unit/services/chat-channels-manager-test.js
Keegan George 36145ed2a8
UX: Update sort order for starred channels on drawer/mobile (#36921)
## 🔍 Overview
This PR updates the sort order for chat channels in the starred tab.
Instead of sorting by Channels then DMs each by activity. We sort by:

1. Unread public channels (sorted by activity)
2. Unread DMs/Groups (sorted by activity)
3. Read public channels (sorted by activity)
4. Read DMs/Groups (sorted by activity)
2025-12-30 13:27:16 -08:00

618 lines
19 KiB
JavaScript
Vendored

import { getOwner } from "@ember/owner";
import { setupTest } from "ember-qunit";
import { module, test } from "qunit";
import ChatFabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
module(
"Discourse Chat | Unit | Service | chat-channels-manager",
function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
this.subject = getOwner(this).lookup("service:chat-channels-manager");
this.fabricators = new ChatFabricators(getOwner(this));
});
module("#sortChannelsByActivity with starred channels", function () {
test("prioritizes starred channels over unstarred", function (assert) {
const channelA = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "channel-a",
}),
});
const channelB = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "channel-b",
}),
});
channelA.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
channelB.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: false,
});
this.subject.store(channelA);
this.subject.store(channelB);
const result = this.subject.publicMessageChannels;
assert.strictEqual(
result[0].id,
channelA.id,
"starred channel comes first"
);
assert.strictEqual(
result[1].id,
channelB.id,
"unstarred channel comes second"
);
});
test("sorts starred channels alphabetically by slug", function (assert) {
const channelC = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "channel-c",
}),
});
const channelA = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "channel-a",
}),
});
const channelB = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "channel-b",
}),
});
channelC.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
channelA.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
channelB.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
this.subject.store(channelC);
this.subject.store(channelA);
this.subject.store(channelB);
const result = this.subject.publicMessageChannels;
assert.strictEqual(
result[0].slug,
"channel-a",
"first starred channel is A"
);
assert.strictEqual(
result[1].slug,
"channel-b",
"second starred channel is B"
);
assert.strictEqual(
result[2].slug,
"channel-c",
"third starred channel is C"
);
});
test("keeps unstarred channels sorted by activity after starred ones", function (assert) {
const starredChannel = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "starred-channel",
}),
});
const unstarredChannel = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "unstarred-channel",
}),
});
starredChannel.currentUserMembership = UserChatChannelMembership.create(
{
following: true,
starred: true,
}
);
unstarredChannel.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: false,
});
this.subject.store(unstarredChannel);
this.subject.store(starredChannel);
const result = this.subject.publicMessageChannels;
assert.strictEqual(
result[0].id,
starredChannel.id,
"starred channel is first"
);
assert.strictEqual(
result[1].id,
unstarredChannel.id,
"unstarred channel is after starred"
);
});
});
module("#unstarredPublicMessageChannelsByActivity", function () {
test("excludes starred channels", function (assert) {
const starredChannel = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "starred-channel",
}),
});
const unstarredChannel = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "unstarred-channel",
}),
});
starredChannel.currentUserMembership = UserChatChannelMembership.create(
{
following: true,
starred: true,
}
);
unstarredChannel.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: false,
});
this.subject.store(starredChannel);
this.subject.store(unstarredChannel);
const result = this.subject.unstarredPublicMessageChannelsByActivity;
assert.strictEqual(result.length, 1, "returns only unstarred channels");
assert.strictEqual(
result[0].id,
unstarredChannel.id,
"returns the unstarred channel"
);
});
test("sorts unstarred channels by activity with unreads first", function (assert) {
const channelWithUnread = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "channel-with-unread",
}),
});
const channelNoUnread = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "channel-no-unread",
}),
});
channelWithUnread.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: false,
});
channelNoUnread.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: false,
});
channelWithUnread.tracking.unreadCount = 5;
channelNoUnread.tracking.unreadCount = 0;
this.subject.store(channelNoUnread);
this.subject.store(channelWithUnread);
const result = this.subject.unstarredPublicMessageChannelsByActivity;
assert.strictEqual(
result[0].id,
channelWithUnread.id,
"channel with unreads comes first"
);
assert.strictEqual(
result[1].id,
channelNoUnread.id,
"channel without unreads comes second"
);
});
});
module("#starredChannelsByActivity", function () {
test("sorts starred channels with unreads first", function (assert) {
const channelWithUnread = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "channel-with-unread",
}),
});
const channelNoUnread = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "channel-no-unread",
}),
});
channelWithUnread.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: true,
});
channelNoUnread.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: true,
});
channelWithUnread.tracking.unreadCount = 5;
channelNoUnread.tracking.unreadCount = 0;
this.subject.store(channelNoUnread);
this.subject.store(channelWithUnread);
const result = this.subject.starredChannelsByActivity;
assert.strictEqual(
result[0].id,
channelWithUnread.id,
"channel with unreads comes first"
);
assert.strictEqual(
result[1].id,
channelNoUnread.id,
"channel without unreads comes second"
);
});
test("includes both public and DM starred channels", function (assert) {
const publicChannel = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "public",
}),
});
const dmChannel = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "DM User",
});
publicChannel.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
dmChannel.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
this.subject.store(publicChannel);
this.subject.store(dmChannel);
const result = this.subject.starredChannelsByActivity;
assert.strictEqual(result.length, 2, "returns both channels");
});
test("prioritizes unread status over channel type", function (assert) {
const readPublicChannel = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "read-public",
}),
});
const unreadDmChannel = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Unread DM",
});
readPublicChannel.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: true,
});
unreadDmChannel.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: true,
});
readPublicChannel.tracking.unreadCount = 0;
unreadDmChannel.tracking.unreadCount = 3;
this.subject.store(readPublicChannel);
this.subject.store(unreadDmChannel);
const result = this.subject.starredChannelsByActivity;
assert.strictEqual(
result[0].id,
unreadDmChannel.id,
"unread DM comes before read public channel"
);
assert.strictEqual(
result[1].id,
readPublicChannel.id,
"read public channel comes after unread DM"
);
});
test("sorts unread public channels before unread DMs", function (assert) {
const unreadPublicChannel = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "unread-public",
}),
});
const unreadDmChannel = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Unread DM",
});
unreadPublicChannel.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: true,
});
unreadDmChannel.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: true,
});
unreadPublicChannel.tracking.unreadCount = 2;
unreadDmChannel.tracking.unreadCount = 5;
this.subject.store(unreadDmChannel);
this.subject.store(unreadPublicChannel);
const result = this.subject.starredChannelsByActivity;
assert.strictEqual(
result[0].id,
unreadPublicChannel.id,
"unread public channel comes first"
);
assert.strictEqual(
result[1].id,
unreadDmChannel.id,
"unread DM comes second"
);
});
test("sorts read public channels before read DMs", function (assert) {
const readPublicChannel = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "read-public",
}),
});
const readDmChannel = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Read DM",
});
readPublicChannel.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: true,
});
readDmChannel.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
readPublicChannel.tracking.unreadCount = 0;
readDmChannel.tracking.unreadCount = 0;
this.subject.store(readDmChannel);
this.subject.store(readPublicChannel);
const result = this.subject.starredChannelsByActivity;
assert.strictEqual(
result[0].id,
readPublicChannel.id,
"read public channel comes first"
);
assert.strictEqual(
result[1].id,
readDmChannel.id,
"read DM comes second"
);
});
test("complete ordering: unread public, unread DM, read public, read DM", function (assert) {
const readPublicChannel = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "read-public",
}),
});
const unreadPublicChannel = this.fabricators.channel({
chatable: this.fabricators.coreFabricators.category({
slug: "unread-public",
}),
});
const readDmChannel = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Read DM",
});
const unreadDmChannel = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Unread DM",
});
readPublicChannel.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: true,
});
unreadPublicChannel.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: true,
});
readDmChannel.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
unreadDmChannel.currentUserMembership =
UserChatChannelMembership.create({
following: true,
starred: true,
});
readPublicChannel.tracking.unreadCount = 0;
unreadPublicChannel.tracking.unreadCount = 2;
readDmChannel.tracking.unreadCount = 0;
unreadDmChannel.tracking.unreadCount = 3;
this.subject.store(readDmChannel);
this.subject.store(unreadDmChannel);
this.subject.store(readPublicChannel);
this.subject.store(unreadPublicChannel);
const result = this.subject.starredChannelsByActivity;
assert.strictEqual(result.length, 4, "returns all 4 channels");
assert.strictEqual(
result[0].id,
unreadPublicChannel.id,
"1st: unread public channel"
);
assert.strictEqual(result[1].id, unreadDmChannel.id, "2nd: unread DM");
assert.strictEqual(
result[2].id,
readPublicChannel.id,
"3rd: read public channel"
);
assert.strictEqual(result[3].id, readDmChannel.id, "4th: read DM");
});
});
module("#sortDirectMessageChannels with starred channels", function () {
test("prioritizes starred DM channels over unstarred", function (assert) {
const dmA = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Alice",
});
const dmB = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Bob",
});
dmA.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
dmB.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: false,
});
this.subject.store(dmA);
this.subject.store(dmB);
const result = this.subject.directMessageChannels;
assert.strictEqual(
result[0].id,
dmA.id,
"starred DM channel comes first"
);
assert.strictEqual(
result[1].id,
dmB.id,
"unstarred DM channel comes second"
);
});
test("sorts starred DM channels alphabetically by title", function (assert) {
const dmCharlie = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Charlie",
});
const dmAlice = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Alice",
});
const dmBob = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Bob",
});
dmCharlie.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
dmAlice.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
dmBob.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
this.subject.store(dmCharlie);
this.subject.store(dmAlice);
this.subject.store(dmBob);
const result = this.subject.directMessageChannels;
assert.strictEqual(
result[0].title,
"Alice",
"first starred DM is Alice"
);
assert.strictEqual(result[1].title, "Bob", "second starred DM is Bob");
assert.strictEqual(
result[2].title,
"Charlie",
"third starred DM is Charlie"
);
});
test("keeps unstarred DM channels sorted by activity after starred ones", function (assert) {
const starredDM = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Starred User",
});
const unstarredDM = this.fabricators.channel({
chatable: this.fabricators.directMessage(),
title: "Unstarred User",
});
starredDM.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: true,
});
unstarredDM.currentUserMembership = UserChatChannelMembership.create({
following: true,
starred: false,
});
this.subject.store(unstarredDM);
this.subject.store(starredDM);
const result = this.subject.directMessageChannels;
assert.strictEqual(
result[0].id,
starredDM.id,
"starred DM channel is first"
);
assert.strictEqual(
result[1].id,
unstarredDM.id,
"unstarred DM channel is after starred"
);
});
});
}
);