FEATURE: Publish read state on group messages. (#7989)

FEATURE: Publish read state on group messages. (#7989)

  • Enable or disable read state based on group attribute

  • When read state needs to be published, the minimum unread count is calculated in the topic query. This way, we can know if someone reads the last post

  • The option can be enabled/disabled from the UI

  • The read indicator will live-updated using message bus

  • Show read indicator on every post

  • The read indicator now shows read count and can be expanded to see user avatars

  • Read count gets updated everytime someone reads a message

  • Simplify topic-list read indicator logic

  • Unsubscribe from message bus on willDestroyElement, removed unnecesarry values from post-menu, and added a comment to explain where does minimum_unread_count comes from

diff --git a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
index 4121a8b..89a4812 100644
--- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
+++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
@@ -40,7 +40,8 @@ export default MountWidget.extend({
       "gaps",
       "selectedQuery",
       "selectedPostsCount",
-      "searchService"
+      "searchService",
+      "showReadIndicator"
     );
   },
 
@@ -291,6 +292,12 @@ export default MountWidget.extend({
             onRefresh: "refreshLikes"
           });
         }
+
+        if (args.refreshReaders) {
+          this.dirtyKeys.keyDirty(`post-menu-${args.id}`, {
+            onRefresh: "refreshReaders"
+          });
+        }
       } else if (args.force) {
         this.dirtyKeys.forceAll();
       }
diff --git a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/topic-list-item.js.es6
index 564938e..9907e02 100644
--- a/app/assets/javascripts/discourse/components/topic-list-item.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-list-item.js.es6
@@ -35,6 +35,42 @@ export const ListItemDefaults = {
   attributeBindings: ["data-topic-id"],
   "data-topic-id": Ember.computed.alias("topic.id"),
 
+  didInsertElement() {
+    this._super(...arguments);
+
+    if (typeof this.get("topic.read_by_group_member") !== "undefined") {
+      this.messageBus.subscribe(this.readIndicatorChannel, data => {
+        const nodeClassList = document.querySelector(
+          `.indicator-topic-${data.topic_id}`
+        ).classList;
+
+        if (data.show_indicator) {
+          nodeClassList.remove("unread");
+        } else {
+          nodeClassList.add("unread");
+        }
+      });
+    }
+  },
+
+  willDestroyElement() {
+    this._super(...arguments);
+
+    if (typeof this.get("topic.read_by_group_member") !== "undefined") {
+      this.messageBus.unsubscribe(this.readIndicatorChannel);
+    }
+  },
+
+  @computed("topic.id")
+  readIndicatorChannel(topicId) {
+    return `/private-messages/group-read/${topicId}`;
+  },
+
+  @computed("topic.read_by_group_member")
+  unreadClass(readByGroupMember) {
+    return readByGroupMember ? "" : "unread";
+  },
+
   @computed
   newDotText() {
     return this.currentUser && this.currentUser.trust_level > 0
diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index c1983bc..25a635b 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -1348,6 +1348,17 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
               })
               .then(() => refresh({ id: data.id, refreshLikes: true }));
             break;
+          case "read":
+            postStream
+              .triggerChangedPost(data.id, data.updated_at, {
+                preserveCooked: true
+              })
+              .then(() =>
+                refresh({
+                  id: data.id,
+                  refreshReaders: topic.show_read_indicator
+                })
+              );
           case "revised":
           case "rebaked": {
             postStream
diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6
index 7a1872b..5f882d8 100644
--- a/app/assets/javascripts/discourse/lib/transform-post.js.es6
+++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6
@@ -71,7 +71,8 @@ export function transformBasicPost(post) {
     expandablePost: false,
     replyCount: post.reply_count,
     locked: post.locked,
-    userCustomFields: post.user_custom_fields
+    userCustomFields: post.user_custom_fields,
+    readCount: post.readers_count
   };
 
   _additionalAttributes.forEach(a => (postAtts[a] = post[a]));
diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6
index 5ea8f11..0489835 100644
--- a/app/assets/javascripts/discourse/models/group.js.es6
+++ b/app/assets/javascripts/discourse/models/group.js.es6
@@ -178,7 +178,8 @@ const Group = RestModel.extend({
       allow_membership_requests: this.allow_membership_requests,
       full_name: this.full_name,
       default_notification_level: this.default_notification_level,
-      membership_request_template: this.membership_request_template
+      membership_request_template: this.membership_request_template,
+      publish_read_state: this.publish_read_state
     };
 
     if (!this.id) {
diff --git a/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs b/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs
index b2f254c..959e2e1 100644
--- a/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs
+++ b/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs
@@ -52,6 +52,16 @@
       class="groups-form-messageable-level"}}
 </div>
 
+<div class="control-group">
+  <label>
+    {{input type="checkbox"
+          checked=model.publish_read_state
+          class="groups-form-publish-read-state"}}
+
+    {{i18n 'admin.groups.manage.interaction.publish_read_state'}}
+  </label>
+</div>
+
 {{#if showEmailSettings}}
   <div class="control-group">
     <label class="control-label">{{i18n 'admin.groups.manage.interaction.email'}}</label>
diff --git a/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs b/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs
index 6b6b7b2..dba2384 100644
--- a/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs
+++ b/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs
@@ -23,6 +23,9 @@
     {{~#if showTopicPostBadges}}
     {{~raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl newDotText=newDotText}}
     {{~/if}}
+    <span class='read-indicator indicator-topic-{{topic.id}} {{readStatus}}'>
+      {{~d-icon "far-eye"}}
+    </span>
   </span>
   <div class="link-bottom-line">
     {{#unless hideCategory}}
diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs
index f8e5ee3..c10ec5d 100644
--- a/app/assets/javascripts/discourse/templates/topic.hbs
+++ b/app/assets/javascripts/discourse/templates/topic.hbs
@@ -177,6 +177,7 @@
                   selectedPostsCount=selectedPostsCount
                   selectedQuery=selectedQuery
                   gaps=model.postStream.gaps
+                  showReadIndicator=model.show_read_indicator
                   showFlags=(action "showPostFlags")
                   editPost=(action "editPost")
                   showHistory=(route-action "showHistory")
diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6
index 8739675..3d7a068 100644
--- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6
@@ -52,6 +52,36 @@ export function buildButton(name, widget) {
   }
 }
 
+registerButton("read-count", attrs => {
+  if (attrs.showReadIndicator) {
+    const count = attrs.readCount;
+    if (count > 0) {
+      return {
+        action: "toggleWhoRead",
+        title: "post.controls.read_indicator",
+        className: "button-count read-indicator",
+        contents: count,
+        iconRight: true,
+        addContainer: false
+      };
+    }
+  }
+});
+
+registerButton("read", attrs => {
+  const disabled = attrs.readCount === 0;
+  if (attrs.showReadIndicator) {
+    return {
+      action: "toggleWhoRead",
+      title: "post.controls.read_indicator",
+      icon: "far-eye",

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

GitHub sha: 1630dae2

Revert "FEATURE: Publish read state on group messages. (#7989)"