From 1630dae2dbf26b2a0eec639c69ff60643cf6f8d1 Mon Sep 17 00:00:00 2001 From: Roman Rizzi Date: Tue, 20 Aug 2019 09:46:57 -0300 Subject: [PATCH] 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 --- .../components/scrolling-post-stream.js.es6 | 9 +- .../components/topic-list-item.js.es6 | 36 ++++++++ .../discourse/controllers/topic.js.es6 | 11 +++ .../discourse/lib/transform-post.js.es6 | 3 +- .../javascripts/discourse/models/group.js.es6 | 3 +- .../groups-form-interaction-fields.hbs | 10 +++ .../templates/list/topic-list-item.raw.hbs | 3 + .../javascripts/discourse/templates/topic.hbs | 1 + .../discourse/widgets/post-menu.js.es6 | 86 +++++++++++++++++- .../discourse/widgets/post-stream.js.es6 | 1 + .../stylesheets/common/base/_topic-list.scss | 6 ++ .../stylesheets/common/base/topic-post.scss | 3 +- .../stylesheets/desktop/topic-post.scss | 4 +- app/assets/stylesheets/mobile/topic-post.scss | 1 + app/controllers/admin/groups_controller.rb | 3 +- app/controllers/groups_controller.rb | 3 +- app/controllers/post_readers_controller.rb | 26 ++++++ app/models/group.rb | 1 + app/models/topic_list.rb | 5 +- app/models/topic_tracking_state.rb | 36 +++++++- app/serializers/basic_group_serializer.rb | 3 +- app/serializers/listable_topic_serializer.rb | 15 +++- app/serializers/post_serializer.rb | 8 ++ app/serializers/topic_view_serializer.rb | 7 +- app/serializers/web_hook_post_serializer.rb | 3 + .../web_hook_topic_view_serializer.rb | 4 + config/locales/client.en.yml | 3 + config/routes.rb | 1 + config/site_settings.yml | 3 +- ...0190807194043_groups_publish_read_state.rb | 7 ++ lib/topic_query.rb | 27 +++++- lib/topic_view.rb | 8 ++ spec/components/topic_query_spec.rb | 43 +++++++++ spec/models/topic_tracking_state_spec.rb | 72 +++++++++++++++ spec/requests/post_readers_controller_spec.rb | 90 +++++++++++++++++++ 35 files changed, 524 insertions(+), 21 deletions(-) create mode 100644 app/controllers/post_readers_controller.rb create mode 100644 db/migrate/20190807194043_groups_publish_read_state.rb create mode 100644 spec/requests/post_readers_controller_spec.rb 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 4121a8b0b33..89a48126f30 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 564938e0566..9907e0286a8 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 c1983bc4c16..25a635b48bf 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 7a1872b4fe5..5f882d8a933 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 5ea8f11616f..04898356354 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 b2f254ca328..959e2e12d13 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"}} +
+ +
+ {{#if showEmailSettings}}
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 6b6b7b2af14..dba2384f786 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}} + + {{~d-icon "far-eye"}} +