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