diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index 952302a57db..faaf68d48ca 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -226,6 +226,8 @@ {{signup-cta}} {{else}} {{#if currentUser}} + {{plugin-outlet name="topic-above-footer-buttons" args=(hash model=model)}} + {{topic-footer-buttons topic=model toggleMultiSelect=(action "toggleMultiSelect") diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6 b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6 index bbaa9f2166f..b91c262a27a 100644 --- a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6 +++ b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6 @@ -59,8 +59,10 @@ export default Ember.Component.extend({ this.set('presenceState', stateObject); }, + _ACTIONS: ['edit', 'reply'], + shouldSharePresence(action){ - return ['edit','reply'].includes(action); + return this._ACTIONS.includes(action); }, @observes('presenceState') diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.js.es6 b/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.js.es6 new file mode 100644 index 00000000000..9638e1214ec --- /dev/null +++ b/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.js.es6 @@ -0,0 +1,44 @@ +import { ajax } from 'discourse/lib/ajax'; +import { observes, on } from 'ember-addons/ember-computed-decorators'; +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + topicId: null, + + messageBusChannel: null, + presenceUsers: null, + + @on('didInsertElement') + _inserted() { + this.set("presenceUsers", []); + + ajax(`/presence/ping/${this.get("topicId")}`).then((data) => { + this.setProperties({ + messageBusChannel: data.messagebus_channel, + presenceUsers: data.users, + }); + this.messageBus.subscribe(data.messagebus_channel, message => { + console.log(message) + this.set("presenceUsers", message.users); + }, data.messagebus_id); + }); + }, + + @on('willDestroyElement') + _destroyed() { + if (this.get("messageBusChannel")) { + this.messageBus.unsubscribe(this.get("messageBusChannel")); + this.set("messageBusChannel", null); + } + }, + + @computed('presenceUsers', 'currentUser.id') + users(presenceUsers, currentUser_id){ + return (presenceUsers || []).filter(user => user.id !== currentUser_id); + }, + + @computed('users.length') + shouldDisplay(length){ + return length > 0; + } +}); diff --git a/plugins/discourse-presence/assets/javascripts/discourse/templates/components/topic-presence-display.hbs b/plugins/discourse-presence/assets/javascripts/discourse/templates/components/topic-presence-display.hbs new file mode 100644 index 00000000000..a459527ff4f --- /dev/null +++ b/plugins/discourse-presence/assets/javascripts/discourse/templates/components/topic-presence-display.hbs @@ -0,0 +1,12 @@ +{{#if shouldDisplay}} +
+ {{#each users as |user|}} + {{avatar user avatarTemplatePath="avatar_template" usernamePath="username" imageSize="small"}} + {{/each}} + + + {{i18n 'presence.replying_to_topic' count=users.length}}{{!-- (using comment to stop whitespace) + --}}... + +
+{{/if}} diff --git a/plugins/discourse-presence/assets/javascripts/discourse/templates/connectors/topic-above-footer-buttons/presence.hbs b/plugins/discourse-presence/assets/javascripts/discourse/templates/connectors/topic-above-footer-buttons/presence.hbs new file mode 100644 index 00000000000..0ee449cf1da --- /dev/null +++ b/plugins/discourse-presence/assets/javascripts/discourse/templates/connectors/topic-above-footer-buttons/presence.hbs @@ -0,0 +1 @@ +{{topic-presence-display topicId=model.id}} diff --git a/plugins/discourse-presence/assets/javascripts/discourse/templates/connectors/topic-above-footer-buttons/presence.js.es6 b/plugins/discourse-presence/assets/javascripts/discourse/templates/connectors/topic-above-footer-buttons/presence.js.es6 new file mode 100644 index 00000000000..00fbc34d912 --- /dev/null +++ b/plugins/discourse-presence/assets/javascripts/discourse/templates/connectors/topic-above-footer-buttons/presence.js.es6 @@ -0,0 +1,5 @@ +export default { + shouldRender(_, ctx) { + return ctx.siteSettings.presence_enabled; + } +}; diff --git a/plugins/discourse-presence/assets/stylesheets/presence.scss b/plugins/discourse-presence/assets/stylesheets/presence.scss index 9ef571c63ba..2da40bf37a9 100644 --- a/plugins/discourse-presence/assets/stylesheets/presence.scss +++ b/plugins/discourse-presence/assets/stylesheets/presence.scss @@ -1,13 +1,9 @@ -.presence-users{ +.presence-users { background-color: $secondary; color: $primary-medium; padding: 0px 5px; - position: absolute; - top: 18px; - right: 35px; .wave { - .dot { display: inline-block; animation: wave 1.8s linear infinite; @@ -33,10 +29,18 @@ } } -.mobile-view .presence-users{ - top: 3px; - right: 54px; - .description{ - display:none; +.composer-fields .presence-users { + position: absolute; + top: 18px; + right: 35px; +} + +.mobile-view { + .composer-fields .presence-users { + top: 3px; + right: 54px; + .description { + display:none; + } } } diff --git a/plugins/discourse-presence/config/locales/client.en.yml b/plugins/discourse-presence/config/locales/client.en.yml index be37c328f26..ea1af30f81d 100644 --- a/plugins/discourse-presence/config/locales/client.en.yml +++ b/plugins/discourse-presence/config/locales/client.en.yml @@ -2,4 +2,7 @@ en: js: presence: replying: "replying" - editing: "editing" \ No newline at end of file + editing: "editing" + replying_to_topic: + one: "is replying" + other: "are replying" diff --git a/plugins/discourse-presence/plugin.rb b/plugins/discourse-presence/plugin.rb index 07a34a07a08..c0f364d17e9 100644 --- a/plugins/discourse-presence/plugin.rb +++ b/plugins/discourse-presence/plugin.rb @@ -29,44 +29,31 @@ after_initialize do end def self.add(type, id, user_id) - redis_key = get_redis_key(type, id) - response = $redis.hset(redis_key, user_id, Time.zone.now) - - response # Will be true if a new key + # return true if a key was added + $redis.hset(get_redis_key(type, id), user_id, Time.zone.now) end def self.remove(type, id, user_id) - redis_key = get_redis_key(type, id) - response = $redis.hdel(redis_key, user_id) - - response > 0 # Return true if key was actually deleted + # return true if a key was deleted + $redis.hdel(get_redis_key(type, id), user_id) > 0 end def self.get_users(type, id) - redis_key = get_redis_key(type, id) - user_ids = $redis.hkeys(redis_key).map(&:to_i) - + user_ids = $redis.hkeys(get_redis_key(type, id)).map(&:to_i) + # TODO: limit the # of users returned User.where(id: user_ids) end def self.publish(type, id) - topic = - if type == 'post' - Post.find_by(id: id).topic - else - Topic.find_by(id: id) - end - users = get_users(type, id) serialized_users = users.map { |u| BasicUserSerializer.new(u, root: false) } - message = { - users: serialized_users - } - + message = { users: serialized_users } messagebus_channel = get_messagebus_channel(type, id) + + topic = type == 'post' ? Post.find_by(id: id).topic : Topic.find_by(id: id) + if topic.archetype == Archetype.private_message - user_ids = User.where('admin or moderator').pluck(:id) - user_ids += topic.allowed_users.pluck(:id) + user_ids = User.where('admin OR moderator').pluck(:id) + topic.allowed_users.pluck(:id) MessageBus.publish(messagebus_channel, message.as_json, user_ids: user_ids) else MessageBus.publish(messagebus_channel, message.as_json, group_ids: topic.secure_group_ids) @@ -76,19 +63,17 @@ after_initialize do end def self.cleanup(type, id) - hash = $redis.hgetall(get_redis_key(type, id)) - original_hash_size = hash.length - - any_changes = false + has_changed = false # Delete entries older than 20 seconds + hash = $redis.hgetall(get_redis_key(type, id)) hash.each do |user_id, time| if Time.zone.now - Time.parse(time) >= 20 - any_changes ||= remove(type, id, user_id) + has_changed |= remove(type, id, user_id) end end - any_changes + has_changed end end @@ -99,6 +84,8 @@ after_initialize do requires_plugin PLUGIN_NAME before_action :ensure_logged_in + ACTIONS = %w{edit reply}.each(&:freeze) + def publish data = params.permit( :response_needed, @@ -108,60 +95,38 @@ after_initialize do payload = {} - if data[:previous] && data[:previous][:action].in?(['edit', 'reply']) + if data[:previous] && data[:previous][:action].in?(ACTIONS) type = data[:previous][:post_id] ? 'post' : 'topic' id = data[:previous][:post_id] ? data[:previous][:post_id] : data[:previous][:topic_id] - topic = - if type == 'post' - Post.find_by(id: id)&.topic - else - Topic.find_by(id: id) - end + topic = type == 'post' ? Post.find_by(id: id)&.topic : Topic.find_by(id: id) if topic guardian.ensure_can_see!(topic) removed = Presence::PresenceManager.remove(type, id, current_user.id) - any_removed = Presence::PresenceManager.cleanup(type, id) - any_changes = removed || any_removed - - users = Presence::PresenceManager.publish(type, id) if any_changes + cleaned = Presence::PresenceManager.cleanup(type, id) + users = Presence::PresenceManager.publish(type, id) if removed || cleaned end end - if data[:current] && data[:current][:action].in?(['edit', 'reply']) + if data[:current] && data[:current][:action].in?(ACTIONS) type = data[:current][:post_id] ? 'post' : 'topic' id = data[:current][:post_id] ? data[:current][:post_id] : data[:current][:topic_id] - topic = - if type == 'post' - Post.find_by(id: id)&.topic - else - Topic.find_by(id: id) - end + topic = type == 'post' ? Post.find_by(id: id)&.topic : Topic.find_by(id: id) if topic guardian.ensure_can_see!(topic) - added = Presence::PresenceManager.add(type, id, current_user.id) - any_removed = Presence::PresenceManager.cleanup(type, id) - any_changes = added || any_removed - - users = Presence::PresenceManager.publish(type, id) if any_changes + added = Presence::PresenceManager.add(type, id, current_user.id) + cleaned = Presence::PresenceManager.cleanup(type, id) + users = Presence::PresenceManager.publish(type, id) if added || cleaned if data[:response_needed] - users ||= Presence::PresenceManager.get_users(type, id) - - serialized_users = users.map { |u| BasicUserSerializer.new(u, root: false) } - messagebus_channel = Presence::PresenceManager.get_messagebus_channel(type, id) - - payload = { - messagebus_channel: messagebus_channel, - messagebus_id: MessageBus.last_id(messagebus_channel), - users: serialized_users - } + users ||= Presence::PresenceManager.get_users(type, id) + payload = json_payload(messagebus_channel, users) end end end @@ -169,10 +134,30 @@ after_initialize do render json: payload end + def ping + topic_id = params.require(:topic_id) + + Presence::PresenceManager.cleanup("topic", topic_id) + + messagebus_channel = Presence::PresenceManager.get_messagebus_channel("topic", topic_id) + users = Presence::PresenceManager.get_users("topic", topic_id) + + render json: json_payload(messagebus_channel, users) + end + + def json_payload(channel, users) + { + messagebus_channel: channel, + messagebus_id: MessageBus.last_id(channel), + users: users.map { |u| BasicUserSerializer.new(u, root: false) } + } + end + end Presence::Engine.routes.draw do post '/publish' => 'presences#publish' + get '/ping/:topic_id' => 'presences#ping' end Discourse::Application.routes.append do