diff --git a/app/assets/javascripts/discourse/app/services/presence.js b/app/assets/javascripts/discourse/app/services/presence.js index d0394aef89c..98fa6445f25 100644 --- a/app/assets/javascripts/discourse/app/services/presence.js +++ b/app/assets/javascripts/discourse/app/services/presence.js @@ -1,6 +1,5 @@ import Service from "@ember/service"; -import EmberObject, { computed, defineProperty } from "@ember/object"; -import { readOnly } from "@ember/object/computed"; +import EmberObject, { computed } from "@ember/object"; import { ajax } from "discourse/lib/ajax"; import { cancel, @@ -20,6 +19,7 @@ import userPresent, { removeOnPresenceChange, } from "discourse/lib/user-presence"; import { bind } from "discourse-common/utils/decorators"; +import Evented from "@ember/object/evented"; const PRESENCE_INTERVAL_S = 30; const PRESENCE_DEBOUNCE_MS = isTesting() ? 0 : 500; @@ -45,17 +45,12 @@ export class PresenceChannelNotFound extends Error {} // Instances of this class are handed out to consumers. They act as // convenient proxies to the PresenceService and PresenceServiceState -class PresenceChannel extends EmberObject { +// The 'change' event is fired whenever the users list or the count change +class PresenceChannel extends EmberObject.extend(Evented) { init({ name, presenceService }) { super.init(...arguments); this.name = name; this.presenceService = presenceService; - defineProperty( - this, - "_presenceState", - readOnly(`presenceService._presenceChannelStates.${name}`) - ); - this.set("present", false); this.set("subscribed", false); } @@ -91,8 +86,12 @@ class PresenceChannel extends EmberObject { if (this.subscribed) { return; } - await this.presenceService._subscribe(this, initialData); + const state = await this.presenceService._subscribe(this, initialData); this.set("subscribed", true); + this.set("_presenceState", state); + + this._publishChange(); + state.on("change", this._publishChange); } async unsubscribe() { @@ -101,6 +100,14 @@ class PresenceChannel extends EmberObject { } await this.presenceService._unsubscribe(this); this.set("subscribed", false); + this._presenceState.off("change", this._publishChange); + this.set("_presenceState", null); + this._publishChange(); + } + + @bind + _publishChange() { + this.trigger("change", this); } @computed("_presenceState.users", "subscribed") @@ -128,7 +135,7 @@ class PresenceChannel extends EmberObject { } } -class PresenceChannelState extends EmberObject { +class PresenceChannelState extends EmberObject.extend(Evented) { init({ name, presenceService }) { super.init(...arguments); this.name = name; @@ -179,6 +186,7 @@ class PresenceChannelState extends EmberObject { ); this.set("_subscribedCallback", callback); + this.trigger("change"); } // Stop subscribing to updates from the server about this channel @@ -191,6 +199,7 @@ class PresenceChannelState extends EmberObject { this.set("_subscribedCallback", null); this.set("users", null); this.set("count", null); + this.trigger("change"); } } @@ -221,6 +230,7 @@ class PresenceChannelState extends EmberObject { if (this.countOnly && data.count_delta !== undefined) { this.set("count", this.count + data.count_delta); + this.trigger("change"); } else if ( !this.countOnly && (data.entering_users || data.leaving_user_ids) @@ -235,6 +245,7 @@ class PresenceChannelState extends EmberObject { this.users.removeObjects(toRemove); } this.set("count", this.users.length); + this.trigger("change"); } else { // Unexpected message await this._resubscribe(); @@ -247,7 +258,7 @@ export default class PresenceService extends Service { init() { super.init(...arguments); this._queuedEvents = []; - this._presenceChannelStates = EmberObject.create(); + this._presenceChannelStates = new Map(); this._presentProxies = new Map(); this._subscribedProxies = new Map(); this._initialDataRequests = new Map(); @@ -429,7 +440,7 @@ export default class PresenceService extends Service { this._addSubscribed(channelProxy); const channelName = channelProxy.name; - let state = this._presenceChannelStates[channelName]; + let state = this._presenceChannelStates.get(channelName); if (!state) { state = PresenceChannelState.create({ name: channelName, @@ -438,14 +449,15 @@ export default class PresenceService extends Service { this._presenceChannelStates.set(channelName, state); await state.subscribe(initialData); } + return state; } _unsubscribe(channelProxy) { const subscribedCount = this._removeSubscribed(channelProxy); if (subscribedCount === 0) { const channelName = channelProxy.name; - this._presenceChannelStates[channelName].unsubscribe(); - this._presenceChannelStates.set(channelName, undefined); + this._presenceChannelStates.get(channelName).unsubscribe(); + this._presenceChannelStates.delete(channelName); } } diff --git a/app/assets/javascripts/discourse/tests/unit/services/presence-test.js b/app/assets/javascripts/discourse/tests/unit/services/presence-test.js index f8f63a61c50..40e07a66c3d 100644 --- a/app/assets/javascripts/discourse/tests/unit/services/presence-test.js +++ b/app/assets/javascripts/discourse/tests/unit/services/presence-test.js @@ -59,12 +59,17 @@ acceptance("Presence - Subscribing", function (needs) { test("subscribing and receiving updates", async function (assert) { let presenceService = this.container.lookup("service:presence"); let channel = presenceService.getChannel("/test/ch1"); + let changes = 0; + const countChanges = () => changes++; + channel.on("change", countChanges); + assert.strictEqual(channel.name, "/test/ch1"); await channel.subscribe({ users: usersFixture(), last_message_id: 1, }); + assert.strictEqual(changes, 1); assert.strictEqual(channel.users.length, 3, "it starts with three users"); @@ -78,6 +83,7 @@ acceptance("Presence - Subscribing", function (needs) { ); assert.strictEqual(channel.users.length, 2, "one user is removed"); + assert.strictEqual(changes, 2); publishToMessageBus( "/presence/test/ch1", @@ -89,6 +95,8 @@ acceptance("Presence - Subscribing", function (needs) { ); assert.strictEqual(channel.users.length, 3, "one user is added"); + assert.strictEqual(changes, 3); + channel.off("change", countChanges); }); test("fetches data when no initial state", async function (assert) { @@ -216,14 +224,14 @@ acceptance("Presence - Subscribing", function (needs) { assert.strictEqual(channel.subscribed, false, "channel can unsubscribe"); assert.strictEqual( channelDup._presenceState, - channel._presenceState, - "state is maintained" + presenceService._presenceChannelStates.get(channel.name), + "state is maintained in the subscribed channel" ); await channelDup.unsubscribe(); assert.strictEqual(channel.subscribed, false, "channelDup can unsubscribe"); assert.strictEqual( - channelDup._presenceState, + presenceService._presenceChannelStates.get(channel.name), undefined, "state is cleared" );