FEATURE: Display new/unread count in browse more messages for PMs. (#14188)

FEATURE: Display new/unread count in browse more messages for PMs. (#14188)

In order to include the new/unread count in the browse more message under suggested topics, a couple of technical changes have to be made.

  1. PrivateMessageTopicTrackingState is now auto-injected which is similar to how it is done for TopicTrackingState. This is done so we don’t have to attempt to pass the PrivateMessageTopicTrackingState object multiple levels down into the suggested-topics component. While the object is auto-injected, we only fetch the initial state and start tracking when the relevant private messages routes has been hit and only when a private message’s suggested topics is loaded. This is done as we do not want to add the extra overhead of fetching the inital state to all page loads but instead wait till the private messages routes are hit.

  2. Previously, we would stop tracking once the user-private-messages route has been deactivated. However, that is not ideal since navigating out of the route and back means we send an API call to the server each time. Since PrivateMessageTopicTrackingState is kept in sync cheaply via messageBus, we can just continue to track the state even if the user has navigated away from the relevant stages.

diff --git a/app/assets/javascripts/discourse/app/components/suggested-topics.js b/app/assets/javascripts/discourse/app/components/suggested-topics.js
index f601c1d..02616fd 100644
--- a/app/assets/javascripts/discourse/app/components/suggested-topics.js
+++ b/app/assets/javascripts/discourse/app/components/suggested-topics.js
@@ -5,6 +5,7 @@ import Site from "discourse/models/site";
 import { categoryBadgeHTML } from "discourse/helpers/category-link";
 import discourseComputed from "discourse-common/utils/decorators";
 import getURL from "discourse-common/lib/get-url";
+import { iconHTML } from "discourse-common/lib/icon-library";
 
 export default Component.extend({
   tagName: "",
@@ -18,13 +19,68 @@ export default Component.extend({
     }
   }),
 
-  @discourseComputed("topic", "topicTrackingState.messageCount")
+  @discourseComputed(
+    "topic",
+    "pmTopicTrackingState.isTracking",
+    "pmTopicTrackingState.statesModificationCounter",
+    "topicTrackingState.messageCount"
+  )
   browseMoreMessage(topic) {
-    // TODO decide what to show for pms
-    if (topic.get("isPrivateMessage")) {
-      return;
+    return topic.isPrivateMessage
+      ? this._privateMessageBrowseMoreMessage(topic)
+      : this._topicBrowseMoreMessage(topic);
+  },
+
+  _privateMessageBrowseMoreMessage(topic) {
+    const username = this.currentUser.username;
+    const suggestedGroupName = topic.suggested_group_name;
+    const inboxFilter = suggestedGroupName ? "group" : "user";
+
+    const unreadCount = this.pmTopicTrackingState.lookupCount("unread", {
+      inboxFilter: inboxFilter,
+      groupName: suggestedGroupName,
+    });
+
+    const newCount = this.pmTopicTrackingState.lookupCount("new", {
+      inboxFilter: inboxFilter,
+      groupName: suggestedGroupName,
+    });
+
+    if (unreadCount + newCount > 0) {
+      const hasBoth = unreadCount > 0 && newCount > 0;
+
+      if (suggestedGroupName) {
+        return I18n.messageFormat("user.messages.read_more_group_pm_MF", {
+          BOTH: hasBoth,
+          UNREAD: unreadCount,
+          NEW: newCount,
+          username: username,
+          groupName: suggestedGroupName,
+          groupLink: this._groupLink(username, suggestedGroupName),
+          basePath: getURL(""),
+        });
+      } else {
+        return I18n.messageFormat("user.messages.read_more_personal_pm_MF", {
+          BOTH: hasBoth,
+          UNREAD: unreadCount,
+          NEW: newCount,
+          username,
+          basePath: getURL(""),
+        });
+      }
+    } else if (suggestedGroupName) {
+      return I18n.t("user.messages.read_more_in_group", {
+        groupLink: this._groupLink(username, suggestedGroupName),
+      });
+    } else {
+      return I18n.t("user.messages.read_more", {
+        basePath: getURL(""),
+        username,
+      });
     }
+  },
 
+  _topicBrowseMoreMessage(topic) {
     const opts = {
       latestLink: `<a href="${getURL("/latest")}">${I18n.t(
         "topic.view_latest_topics"
@@ -50,8 +106,13 @@ export default Component.extend({
         "</a>";
     }
 
-    const unreadTopics = this.topicTrackingState.countUnread();
-    const newTopics = this.currentUser ? this.topicTrackingState.countNew() : 0;
+    let unreadTopics = 0;
+    let newTopics = 0;
+
+    if (this.currentUser) {
+      unreadTopics = this.topicTrackingState.countUnread();
+      newTopics = this.topicTrackingState.countNew();
+    }
 
     if (newTopics + unreadTopics > 0) {
       const hasBoth = unreadTopics > 0 && newTopics > 0;
@@ -71,4 +132,10 @@ export default Component.extend({
       return I18n.t("topic.read_more", opts);
     }
   },
+
+  _groupLink(username, groupName) {
+    return `<a class="group-link" href="${getURL(
+      `/u/${username}/messages/group/${groupName}`
+    )}">${iconHTML("users")} ${groupName}</a>`;
+  },
 });
diff --git a/app/assets/javascripts/discourse/app/controllers/user-topics-list.js b/app/assets/javascripts/discourse/app/controllers/user-topics-list.js
index 6800f06..7e95ed1 100644
--- a/app/assets/javascripts/discourse/app/controllers/user-topics-list.js
+++ b/app/assets/javascripts/discourse/app/controllers/user-topics-list.js
@@ -18,7 +18,6 @@ export default Controller.extend(BulkTopicSelection, {
   showPosters: false,
   channel: null,
   tagsForUser: null,
-  pmTopicTrackingState: null,
   incomingCount: reads("pmTopicTrackingState.newIncoming.length"),
 
   @discourseComputed("emptyState", "model.topics.length", "incomingCount")
@@ -46,15 +45,11 @@ export default Controller.extend(BulkTopicSelection, {
   },
 
   subscribe() {
-    this.pmTopicTrackingState?.trackIncoming(
-      this.inbox,
-      this.filter,
-      this.group
-    );
+    this.pmTopicTrackingState.trackIncoming(this.inbox, this.filter);
   },
 
   unsubscribe() {
-    this.pmTopicTrackingState?.resetTracking();
+    this.pmTopicTrackingState.resetIncomingTracking();
   },
 
   @action
@@ -85,7 +80,7 @@ export default Controller.extend(BulkTopicSelection, {
   @action
   showInserted() {
     this.model.loadBefore(this.pmTopicTrackingState.newIncoming);
-    this.pmTopicTrackingState.resetTracking();
+    this.pmTopicTrackingState.resetIncomingTracking();
     return false;
   },
 });
diff --git a/app/assets/javascripts/discourse/app/models/post-stream.js b/app/assets/javascripts/discourse/app/models/post-stream.js
index a28a483..62b96e7 100644
--- a/app/assets/javascripts/discourse/app/models/post-stream.js
+++ b/app/assets/javascripts/discourse/app/models/post-stream.js
@@ -1076,9 +1076,7 @@ export default RestModel.extend({
     const store = this.store;
 
     return ajax(url, { data }).then((result) => {
-      if (result.suggested_topics) {
-        this.set("topic.suggested_topics", result.suggested_topics);
-      }
+      this._setSuggestedTopics(result);
 
       const posts = get(result, "post_stream.posts");
 
@@ -1124,9 +1122,7 @@ export default RestModel.extend({
       data,
       headers,
     }).then((result) => {
-      if (result.suggested_topics) {
-        this.set("topic.suggested_topics", result.suggested_topics);
-      }
+      this._setSuggestedTopics(result);
 
       const posts = get(result, "post_stream.posts");
 
@@ -1245,4 +1241,17 @@ export default RestModel.extend({
       }
     }
   },
+
+  _setSuggestedTopics(result) {
+    if (!result.suggested_topics) {
+      return;
+    }
+
+    this.topic.setProperties({
+      suggested_topics: result.suggested_topics,
+      suggested_group_name: result.suggested_group_name,
+    });
+
+    this.pmTopicTrackingState.startTracking();
+  },
 });
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 4fcb8f9..de46556 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,4 +1,7 @@
 import EmberObject from "@ember/object";
+import { ajax } from "discourse/lib/ajax";
+import { on } from "discourse-common/utils/decorators";
+import { popupAjaxError } from "discourse/lib/ajax-error";
 import {
   ARCHIVE_FILTER,
   INBOX_FILTER,
@@ -15,20 +18,33 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({
   filter: null,
   activeGroup: null,
 
-  startTracking(data) {
+  @on("init")
+  _setup() {
     this.states = new Map();
+    this.statesModificationCounter = 0;
+    this.isTracking = false;
     this.newIncoming = [];
-    this._loadStates(data);
-    this.establishChannels();
   },
 
-  establishChannels() {
+  startTracking() {
+    if (this.isTracking) {
+      return;
+    }
+
+    this._establishChannels();
+
+    this._loadInitialState().finally(() => {
+      this.set("isTracking", true);
+    });
+  },
+
+  _establishChannels() {
     this.messageBus.subscribe(
-      this._userChannel(this.user.id),
+      this._userChannel(),

[... diff too long, it was truncated ...]

GitHub sha: fc1fd1b41689694b3916dc4e10605eb9b8bb89b7

This commit appears in #14188 which was approved by lis2 and martin. It was merged by tgxworld.