diff --git a/app/assets/javascripts/discourse/app/components/sidebar/messages-section.js b/app/assets/javascripts/discourse/app/components/sidebar/messages-section.js index ac57cf32180..7c2ff92c3ab 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/messages-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/messages-section.js @@ -2,13 +2,14 @@ import { cached } from "@glimmer/tracking"; import { getOwner } from "discourse-common/lib/get-owner"; import GlimmerComponent from "discourse/components/glimmer"; +import { bind } from "discourse-common/utils/decorators"; import GroupMessageSectionLink from "discourse/lib/sidebar/messages-section/group-message-section-link"; import PersonalMessageSectionLink from "discourse/lib/sidebar/messages-section/personal-message-section-link"; export const INBOX = "inbox"; -const UNREAD = "unread"; +export const UNREAD = "unread"; const SENT = "sent"; -const NEW = "new"; +export const NEW = "new"; const ARCHIVE = "archive"; export const PERSONAL_MESSAGES_INBOX_FILTERS = [ @@ -30,6 +31,24 @@ export default class SidebarMessagesSection extends GlimmerComponent { this, this._refreshSectionLinksDisplayState ); + + this.pmTopicTrackingState + .startTracking() + .then(this._refreshSectionLinkCounts); + + this._pmTopicTrackingStateKey = "messages-section"; + + this.pmTopicTrackingState.onStateChange( + this._pmTopicTrackingStateKey, + this._refreshSectionLinkCounts + ); + } + + @bind + _refreshSectionLinkCounts() { + for (const sectionLink of this.allSectionLinks) { + sectionLink.refreshCount(); + } } willDestroy() { @@ -38,6 +57,11 @@ export default class SidebarMessagesSection extends GlimmerComponent { this, this._refreshSectionLinksDisplayState ); + + this.pmTopicTrackingState.offStateChange( + this._pmTopicTrackingStateKey, + this._refreshSectionLinkCounts + ); } _refreshSectionLinksDisplayState({ @@ -81,6 +105,7 @@ export default class SidebarMessagesSection extends GlimmerComponent { new PersonalMessageSectionLink({ currentUser: this.currentUser, type, + pmTopicTrackingState: this.pmTopicTrackingState, }) ); }); @@ -99,6 +124,7 @@ export default class SidebarMessagesSection extends GlimmerComponent { group, type: groupMessageLink, currentUser: this.currentUser, + pmTopicTrackingState: this.pmTopicTrackingState, }) ); }); diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/group-message-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/group-message-section-link.js index 83192a87654..ce6ec8bc7de 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/group-message-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/group-message-section-link.js @@ -40,6 +40,10 @@ export default class GroupMessageSectionLink extends MessageSectionLink { get text() { if (this._isInbox) { return this.group.name; + } else if (this.count > 0) { + return I18n.t(`sidebar.sections.messages.links.${this.type}_with_count`, { + count: this.count, + }); } else { return I18n.t(`sidebar.sections.messages.links.${this.type}`); } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/message-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/message-section-link.js index fcd029e2187..bef0aef0951 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/message-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/message-section-link.js @@ -1,18 +1,42 @@ import { tracked } from "@glimmer/tracking"; -import { INBOX } from "discourse/components/sidebar/messages-section"; +import { + INBOX, + NEW, + UNREAD, +} from "discourse/components/sidebar/messages-section"; export default class MessageSectionLink { @tracked shouldDisplay = this._isInbox; + @tracked count = 0; - constructor({ group, currentUser, type }) { + constructor({ group, currentUser, type, pmTopicTrackingState }) { this.group = group; this.currentUser = currentUser; this.type = type; + this.pmTopicTrackingState = pmTopicTrackingState; + } + + refreshCount() { + this._refreshCount(); + } + + _refreshCount() { + if (this.shouldDisplay && this._shouldTrack) { + this.count = this.pmTopicTrackingState.lookupCount(this.type, { + inboxFilter: this.group ? "group" : "user", + groupName: this.group?.name, + }); + } } set setDisplayState(value) { + const changed = this.shouldDisplay !== value; this.shouldDisplay = value; + + if (changed) { + this._refreshCount(); + } } get inboxFilter() { @@ -43,4 +67,8 @@ export default class MessageSectionLink { get _isInbox() { return this.type === INBOX; } + + get _shouldTrack() { + return this.type === NEW || this.type === UNREAD; + } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/personal-message-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/personal-message-section-link.js index dc2c42646fb..bbf813f352e 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/personal-message-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/messages-section/personal-message-section-link.js @@ -38,7 +38,13 @@ export default class PersonalMessageSectionLink extends MessageSectionLink { } get text() { - return I18n.t(`sidebar.sections.messages.links.${this.type}`); + if (this.count > 0) { + return I18n.t(`sidebar.sections.messages.links.${this.type}_with_count`, { + count: this.count, + }); + } else { + return I18n.t(`sidebar.sections.messages.links.${this.type}`); + } } pageChanged({ currentRouteName, privateMessageTopic }) { diff --git a/app/assets/javascripts/discourse/app/models/private-message-topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/private-message-topic-tracking-state.js index 5a96b7064f4..1c95bbfcaa8 100644 --- a/app/assets/javascripts/discourse/app/models/private-message-topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/private-message-topic-tracking-state.js @@ -1,3 +1,5 @@ +import { Promise } from "rsvp"; + import EmberObject from "@ember/object"; import { ajax } from "discourse/lib/ajax"; import { bind, on } from "discourse-common/utils/decorators"; @@ -25,16 +27,20 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({ this.statesModificationCounter = 0; this.isTracking = false; this.newIncoming = []; - this.stateChangeCallbacks = {}; + this.stateChangeCallbacks = new Map(); }, - onStateChange(name, callback) { - this.stateChangeCallbacks[name] = callback; + onStateChange(key, callback) { + this.stateChangeCallbacks.set(key, callback); + }, + + offStateChange(key) { + this.stateChangeCallbacks.delete(key); }, startTracking() { if (this.isTracking) { - return; + return Promise.resolve(); } this._establishChannels(); @@ -46,13 +52,13 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({ _establishChannels() { this.messageBus.subscribe( - this._userChannel(), + this.userChannel(), this._processMessage.bind(this) ); this.currentUser.groupsWithMessages?.forEach((group) => { this.messageBus.subscribe( - this._groupChannel(group.id), + this.groupChannel(group.id), this._processMessage.bind(this) ); }); @@ -111,11 +117,11 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({ return this.states.get(topicId); }, - _userChannel() { + userChannel() { return `${this.CHANNEL_PREFIX}/user/${this.currentUser.id}`; }, - _groupChannel(groupId) { + groupChannel(groupId) { return `${this.CHANNEL_PREFIX}/group/${groupId}`; }, @@ -263,7 +269,7 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({ _afterStateChange() { this.incrementProperty("statesModificationCounter"); - Object.values(this.stateChangeCallbacks).forEach((callback) => callback()); + this.stateChangeCallbacks.forEach((callback) => callback()); }, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-messages-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-messages-section-test.js index 1b293afc886..4d6770a665d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-messages-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-messages-section-test.js @@ -1,13 +1,17 @@ import { test } from "qunit"; +import I18n from "I18n"; -import { click, currentURL, visit } from "@ember/test-helpers"; +import { click, currentURL, settled, visit } from "@ember/test-helpers"; import { acceptance, exists, + publishToMessageBus, + query, queryAll, updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; +import { NotificationLevels } from "discourse/lib/notification-levels"; acceptance( "Sidebar - Messages Section - enable_personal_messages disabled", @@ -355,5 +359,192 @@ acceptance( "foo_group messages inbox filter links are not shown" ); }); + + test("new and unread counts for group messages", async function (assert) { + updateCurrentUser({ + groups: [ + { + id: 1, + name: "group1", + has_messages: true, + }, + ], + }); + + await visit("/"); + + const pmTopicTrackingState = this.container.lookup( + "pm-topic-tracking-state:main" + ); + + publishToMessageBus(pmTopicTrackingState.groupChannel(1), { + topic_id: 1, + message_type: "unread", + payload: { + last_read_post_number: 1, + highest_post_number: 2, + notification_level: NotificationLevels.TRACKING, + group_ids: [1], + }, + }); + + publishToMessageBus(pmTopicTrackingState.groupChannel(1), { + topic_id: 2, + message_type: "new_topic", + payload: { + last_read_post_number: null, + highest_post_number: 1, + notification_level: NotificationLevels.TRACKING, + group_ids: [1], + }, + }); + + await click( + ".sidebar-section-messages .sidebar-section-link-group-messages-inbox.group1" + ); + + assert.strictEqual( + query( + ".sidebar-section-messages .sidebar-section-link-group-messages-unread.group1" + ).textContent.trim(), + I18n.t("sidebar.sections.messages.links.unread_with_count", { + count: 1, + }), + "displays 1 count for group1 unread inbox filter link" + ); + + assert.strictEqual( + query( + ".sidebar-section-messages .sidebar-section-link-group-messages-new.group1" + ).textContent.trim(), + I18n.t("sidebar.sections.messages.links.new_with_count", { + count: 1, + }), + "displays 1 count for group1 new inbox filter link" + ); + + publishToMessageBus(pmTopicTrackingState.groupChannel(1), { + topic_id: 2, + message_type: "read", + payload: { + last_read_post_number: 1, + highest_post_number: 1, + notification_level: NotificationLevels.TRACKING, + group_ids: [1], + }, + }); + + await settled(); + + assert.strictEqual( + query( + ".sidebar-section-messages .sidebar-section-link-group-messages-new.group1" + ).textContent.trim(), + I18n.t("sidebar.sections.messages.links.new"), + "removes count for group1 new inbox filter link" + ); + }); + + test("new and unread counts for personal messages", async function (assert) { + await visit("/"); + + const pmTopicTrackingState = this.container.lookup( + "pm-topic-tracking-state:main" + ); + + publishToMessageBus(pmTopicTrackingState.userChannel(), { + topic_id: 1, + message_type: "unread", + payload: { + last_read_post_number: 1, + highest_post_number: 2, + notification_level: NotificationLevels.TRACKING, + group_ids: [], + }, + }); + + await settled(); + + await click( + ".sidebar-section-messages .sidebar-section-link-personal-messages-inbox" + ); + + assert.strictEqual( + query( + ".sidebar-section-messages .sidebar-section-link-personal-messages-unread" + ).textContent.trim(), + I18n.t("sidebar.sections.messages.links.unread_with_count", { + count: 1, + }), + "displays 1 count for the unread inbox filter link" + ); + + publishToMessageBus(pmTopicTrackingState.userChannel(), { + topic_id: 2, + message_type: "unread", + payload: { + last_read_post_number: 1, + highest_post_number: 2, + notification_level: NotificationLevels.TRACKING, + group_ids: [], + }, + }); + + await settled(); + + assert.strictEqual( + query( + ".sidebar-section-messages .sidebar-section-link-personal-messages-unread" + ).textContent.trim(), + I18n.t("sidebar.sections.messages.links.unread_with_count", { + count: 2, + }), + "displays 2 count for the unread inbox filter link" + ); + + publishToMessageBus(pmTopicTrackingState.userChannel(), { + topic_id: 3, + message_type: "new_topic", + payload: { + last_read_post_number: null, + highest_post_number: 1, + notification_level: NotificationLevels.TRACKING, + group_ids: [], + }, + }); + + await settled(); + + assert.strictEqual( + query( + ".sidebar-section-messages .sidebar-section-link-personal-messages-new" + ).textContent.trim(), + I18n.t("sidebar.sections.messages.links.new_with_count", { + count: 1, + }), + "displays 1 count for the new inbox filter link" + ); + + publishToMessageBus(pmTopicTrackingState.userChannel(), { + topic_id: 3, + message_type: "read", + payload: { + last_read_post_number: 1, + highest_post_number: 1, + notification_level: NotificationLevels.TRACKING, + group_ids: [], + }, + }); + + await settled(); + + assert.strictEqual( + query( + ".sidebar-section-messages .sidebar-section-link-personal-messages-new" + ).textContent.trim(), + I18n.t("sidebar.sections.messages.links.new"), + "removes the count from the new inbox filter link" + ); + }); } ); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a18024bd71d..7838f313de0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4051,7 +4051,9 @@ en: inbox: "Inbox" sent: "Sent" new: "New" + new_with_count: "New (%{count})" unread: "Unread" + unread_with_count: "Unread (%{count})" archive: "Archive" tags: no_tracked_tags: "You are not tracking any tags."