diff --git a/app/assets/javascripts/discourse/app/models/composer.js b/app/assets/javascripts/discourse/app/models/composer.js index 9dbc5dfd803..1b719381dd2 100644 --- a/app/assets/javascripts/discourse/app/models/composer.js +++ b/app/assets/javascripts/discourse/app/models/composer.js @@ -187,6 +187,11 @@ export default class Composer extends RestModel { @service dialog; + @tracked topic; + @tracked post; + @tracked reply; + @tracked whisper; + unlistTopic = false; noBump = false; draftSaving = false; @@ -200,7 +205,6 @@ export default class Composer extends RestModel { @not("creatingPrivateMessage") notCreatingPrivateMessage; @not("privateMessage") notPrivateMessage; @or("creatingTopic", "editingFirstPost") topicFirstPost; - @equal("action", REPLY) replyingToTopic; @equal("composeState", OPEN) viewOpen; @equal("composeState", DRAFT) viewDraft; @equal("composeState", FULLSCREEN) viewFullscreen; @@ -263,6 +267,14 @@ export default class Composer extends RestModel { return categoryId ? Category.findById(categoryId) : null; } + get replyingToTopic() { + return this.get("action") === REPLY; + } + + get editingPost() { + return isEdit(this.get("action")); + } + @discourseComputed("category.minimumRequiredTags") minimumRequiredTags(minimumRequiredTags) { return minimumRequiredTags || 0; @@ -286,11 +298,6 @@ export default class Composer extends RestModel { ); } - @discourseComputed("action") - editingPost(action) { - return isEdit(action); - } - @observes("composeState") composeStateChanged() { const oldOpen = this.composerOpened; diff --git a/app/assets/javascripts/discourse/app/services/presence.js b/app/assets/javascripts/discourse/app/services/presence.js index 9bbcf04694a..d1dec361942 100644 --- a/app/assets/javascripts/discourse/app/services/presence.js +++ b/app/assets/javascripts/discourse/app/services/presence.js @@ -1,4 +1,5 @@ import EmberObject, { computed } from "@ember/object"; +import { dependentKeyCompat } from "@ember/object/compat"; import Evented from "@ember/object/evented"; import { cancel, debounce, next, once, throttle } from "@ember/runloop"; import Service, { service } from "@ember/service"; @@ -108,12 +109,11 @@ class PresenceChannel extends EmberObject.extend(Evented) { this.trigger("change", this); } - @computed("_presenceState.users", "subscribed") + @dependentKeyCompat get users() { - if (!this.subscribed) { - return; + if (this.get("subscribed")) { + return this.get("_presenceState.users"); } - return this._presenceState?.users; } @computed("_presenceState.count", "subscribed") diff --git a/app/assets/javascripts/discourse/tests/helpers/presence-pretender.js b/app/assets/javascripts/discourse/tests/helpers/presence-pretender.js index b105f57e2eb..a43c76d8126 100644 --- a/app/assets/javascripts/discourse/tests/helpers/presence-pretender.js +++ b/app/assets/javascripts/discourse/tests/helpers/presence-pretender.js @@ -34,10 +34,7 @@ export default function (helper) { } export function getChannelInfo(name) { - return ( - channels[name] || - (channels[name] = { count: 0, users: [], last_message_id: 0 }) - ); + return (channels[name] ||= { count: 0, users: [], last_message_id: 0 }); } export async function joinChannel(name, user) { diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.gjs b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.gjs new file mode 100644 index 00000000000..b998f4c8f27 --- /dev/null +++ b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.gjs @@ -0,0 +1,149 @@ +import Component from "@glimmer/component"; +import { cached, tracked } from "@glimmer/tracking"; +import { service } from "@ember/service"; +import { modifier } from "ember-modifier"; +import { gt } from "truth-helpers"; +import UserLink from "discourse/components/user-link"; +import avatar from "discourse/helpers/avatar"; +import i18n from "discourse-common/helpers/i18n"; + +export default class ComposerPresenceDisplay extends Component { + @service presence; + @service composerPresenceManager; + @service currentUser; + @service siteSettings; + + @tracked replyChannel; + @tracked whisperChannel; + @tracked editChannel; + + setupReplyChannel = modifier(() => { + const topic = this.args.model.topic; + if (!topic || !this.isReply) { + return; + } + + const replyChannel = this.presence.getChannel( + `/discourse-presence/reply/${topic.id}` + ); + replyChannel.subscribe(); + this.replyChannel = replyChannel; + + return () => replyChannel.unsubscribe(); + }); + + setupWhisperChannel = modifier(() => { + if ( + !this.args.model.topic || + !this.isReply || + !this.currentUser.staff || + !this.currentUser.whisperer + ) { + return; + } + + const whisperChannel = this.presence.getChannel( + `/discourse-presence/whisper/${this.args.model.topic.id}` + ); + whisperChannel.subscribe(); + this.whisperChannel = whisperChannel; + + return () => whisperChannel.unsubscribe(); + }); + + setupEditChannel = modifier(() => { + if (!this.args.model.post || !this.isEdit) { + return; + } + + const editChannel = this.presence.getChannel( + `/discourse-presence/edit/${this.args.model.post.id}` + ); + editChannel.subscribe(); + this.editChannel = editChannel; + + return () => editChannel.unsubscribe(); + }); + + notifyState = modifier(() => { + const { topic, post, reply } = this.args.model; + const raw = this.isEdit ? post?.raw || "" : ""; + const entity = this.isEdit ? post : topic; + + if (reply !== raw) { + this.composerPresenceManager.notifyState(this.state, entity?.id); + } + + return () => this.composerPresenceManager.leave(); + }); + + get isReply() { + return this.state === "reply" || this.state === "whisper"; + } + + get isEdit() { + return this.state === "edit"; + } + + get state() { + if (this.args.model.editingPost) { + return "edit"; + } else if (this.args.model.whisper) { + return "whisper"; + } else if (this.args.model.replyingToTopic) { + return "reply"; + } + } + + @cached + get users() { + let users; + if (this.isEdit) { + users = this.editChannel?.users || []; + } else { + const replyUsers = this.replyChannel?.users || []; + const whisperUsers = this.whisperChannel?.users || []; + users = [...replyUsers, ...whisperUsers]; + } + + return users + .filter((u) => u.id !== this.currentUser.id) + .slice(0, this.siteSettings.presence_max_users_shown); + } + + +} diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.hbs b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.hbs deleted file mode 100644 index 848800ff3b8..00000000000 --- a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.hbs +++ /dev/null @@ -1,30 +0,0 @@ -
- {{#if this.shouldDisplay}} -
-
- {{#each this.users as |user|}} - - {{avatar user imageSize="small"}} - - {{/each}} -
- - - {{~#if this.isReply~}} - {{i18n "presence.replying" count=this.users.length}} - {{~else~}} - {{i18n "presence.editing" count=this.users.length}} - {{~/if~}} - - - . - . - . - - -
- {{/if}} -
\ No newline at end of file diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js deleted file mode 100644 index 80feba20f9d..00000000000 --- a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js +++ /dev/null @@ -1,137 +0,0 @@ -import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { action } from "@ember/object"; -import { service } from "@ember/service"; - -export default class ComposerPresenceDisplayComponent extends Component { - @service presence; - @service composerPresenceManager; - @service currentUser; - @service siteSettings; - - @tracked replyChannel; - @tracked whisperChannel; - @tracked editChannel; - - get isReply() { - return this.state === "reply" || this.state === "whisper"; - } - - get isEdit() { - return this.state === "edit"; - } - - get state() { - const { editingPost, whisper, replyingToTopic } = this.args.model; - - if (editingPost) { - return "edit"; - } else if (whisper) { - return "whisper"; - } else if (replyingToTopic) { - return "reply"; - } - } - - get replyChannelName() { - const topicId = this.args.model?.topic?.id; - if (topicId && this.isReply) { - return `/discourse-presence/reply/${topicId}`; - } - } - - get whisperChannelName() { - const topicId = this.args.model?.topic?.id; - if (topicId && this.isReply && this.currentUser.whisperer) { - return `/discourse-presence/whisper/${topicId}`; - } - } - - get editChannelName() { - const postId = this.args.model?.post?.id; - if (postId && this.isEdit) { - return `/discourse-presence/edit/${postId}`; - } - } - - get replyUsers() { - return this.replyChannel?.users || []; - } - - get whisperUsers() { - return this.whisperChannel?.users || []; - } - - get replyingUsers() { - return [...this.replyUsers, ...this.whisperUsers]; - } - - get editingUsers() { - return this.editChannel?.users || []; - } - - get users() { - const users = this.isEdit ? this.editingUsers : this.replyingUsers; - return users - .filter((u) => u.id !== this.currentUser.id) - .slice(0, this.siteSettings.presence_max_users_shown); - } - - get shouldDisplay() { - return this.users.length > 0; - } - - @action - setupChannels() { - this.setupReplyChannel(); - this.setupWhisperChannel(); - this.setupEditChannel(); - this.notifyState(); - } - - setupReplyChannel() { - this.setupChannel("replyChannel", this.replyChannelName); - } - - setupWhisperChannel() { - if (this.currentUser.staff) { - this.setupChannel("whisperChannel", this.whisperChannelName); - } - } - - setupEditChannel() { - this.setupChannel("editChannel", this.editChannelName); - } - - setupChannel(key, name) { - if (this[key]?.name !== name) { - this[key]?.unsubscribe(); - if (name) { - this[key] = this.presence.getChannel(name); - this[key].subscribe(); - } - } - } - - notifyState() { - const { reply, post, topic } = this.args.model; - const raw = this.isEdit ? post?.raw || "" : ""; - const entity = this.isEdit ? post : topic; - - if (reply !== raw) { - this.composerPresenceManager.notifyState(this.state, entity?.id); - } - } - - willDestroy() { - super.willDestroy(...arguments); - this.unsubscribeFromChannels(); - this.composerPresenceManager.leave(); - } - - unsubscribeFromChannels() { - this.replyChannel?.unsubscribe(); - this.whisperChannel?.unsubscribe(); - this.editChannel?.unsubscribe(); - } -} diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.gjs b/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.gjs new file mode 100644 index 00000000000..d2ba55fc770 --- /dev/null +++ b/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.gjs @@ -0,0 +1,77 @@ +import Component from "@glimmer/component"; +import { cached, tracked } from "@glimmer/tracking"; +import { service } from "@ember/service"; +import { modifier } from "ember-modifier"; +import { gt } from "truth-helpers"; +import UserLink from "discourse/components/user-link"; +import avatar from "discourse/helpers/avatar"; +import i18n from "discourse-common/helpers/i18n"; + +export default class TopicPresenceDisplay extends Component { + @service presence; + @service currentUser; + + @tracked replyChannel; + @tracked whisperChannel; + + setupReplyChannel = modifier(() => { + const replyChannel = this.presence.getChannel( + `/discourse-presence/reply/${this.args.topic.id}` + ); + replyChannel.subscribe(); + this.replyChannel = replyChannel; + + return () => replyChannel.unsubscribe(); + }); + + setupWhisperChannels = modifier(() => { + if (!this.currentUser.staff) { + return; + } + + const whisperChannel = this.presence.getChannel( + `/discourse-presence/whisper/${this.args.topic.id}` + ); + whisperChannel.subscribe(); + this.whisperChannel = whisperChannel; + + return () => whisperChannel.unsubscribe(); + }); + + @cached + get users() { + const replyUsers = this.replyChannel?.users || []; + const whisperUsers = this.whisperChannel?.users || []; + + return [...replyUsers, ...whisperUsers].filter( + (u) => u.id !== this.currentUser.id + ); + } + + +} diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.hbs b/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.hbs deleted file mode 100644 index 3da513e1bc4..00000000000 --- a/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.hbs +++ /dev/null @@ -1,26 +0,0 @@ -
- {{#if this.shouldDisplay}} -
-
- {{#each this.users as |user|}} - - {{avatar user imageSize="small"}} - - {{/each}} -
- - - {{i18n "presence.replying_to_topic" count=this.users.length}} - - - . - . - . - - -
- {{/if}} -
\ No newline at end of file diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.js b/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.js deleted file mode 100644 index 1018d4b403a..00000000000 --- a/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.js +++ /dev/null @@ -1,72 +0,0 @@ -import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { action } from "@ember/object"; -import { service } from "@ember/service"; - -export default class TopicPresenceDisplayComponent extends Component { - @service presence; - @service currentUser; - - @tracked replyChannel; - @tracked whisperChannel; - - get replyChannelName() { - return `/discourse-presence/reply/${this.args.topic.id}`; - } - - get whisperChannelName() { - return `/discourse-presence/whisper/${this.args.topic.id}`; - } - - get replyUsers() { - return this.replyChannel?.users || []; - } - - get whisperUsers() { - return this.whisperChannel?.users || []; - } - - get users() { - return [...this.replyUsers, ...this.whisperUsers].filter( - (u) => u.id !== this.currentUser.id - ); - } - - get shouldDisplay() { - return this.users.length > 0; - } - - @action - setupChannels() { - this.setupReplyChannel(); - this.setupWhisperChannel(); - } - - willDestroy() { - super.willDestroy(...arguments); - this.unsubscribeFromChannels(); - } - - unsubscribeFromChannels() { - this.replyChannel?.unsubscribe(); - this.whisperChannel?.unsubscribe(); - } - - setupReplyChannel() { - if (this.replyChannel?.name !== this.replyChannelName) { - this.replyChannel?.unsubscribe(); - this.replyChannel = this.presence.getChannel(this.replyChannelName); - this.replyChannel.subscribe(); - } - } - - setupWhisperChannel() { - if (this.currentUser.staff) { - if (this.whisperChannel?.name !== this.whisperChannelName) { - this.whisperChannel?.unsubscribe(); - this.whisperChannel = this.presence.getChannel(this.whisperChannelName); - this.whisperChannel.subscribe(); - } - } - } -} diff --git a/plugins/discourse-presence/assets/javascripts/discourse/connectors/before-composer-controls/presence.hbs b/plugins/discourse-presence/assets/javascripts/discourse/connectors/before-composer-controls/presence.hbs index c4b51c05a3e..b11a699f041 100644 --- a/plugins/discourse-presence/assets/javascripts/discourse/connectors/before-composer-controls/presence.hbs +++ b/plugins/discourse-presence/assets/javascripts/discourse/connectors/before-composer-controls/presence.hbs @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/plugins/discourse-presence/assets/javascripts/discourse/connectors/topic-above-footer-buttons/presence.hbs b/plugins/discourse-presence/assets/javascripts/discourse/connectors/topic-above-footer-buttons/presence.hbs index 58045fd3cc3..56dfb744777 100644 --- a/plugins/discourse-presence/assets/javascripts/discourse/connectors/topic-above-footer-buttons/presence.hbs +++ b/plugins/discourse-presence/assets/javascripts/discourse/connectors/topic-above-footer-buttons/presence.hbs @@ -1,2 +1,2 @@ {{! Note: the topic-above-footer-buttons outlet is only rendered for logged-in users }} - \ No newline at end of file + \ No newline at end of file diff --git a/plugins/discourse-presence/test/javascripts/acceptance/discourse-presence-test.js b/plugins/discourse-presence/test/javascripts/acceptance/discourse-presence-test.js index 607d9034cfb..5db6df80b57 100644 --- a/plugins/discourse-presence/test/javascripts/acceptance/discourse-presence-test.js +++ b/plugins/discourse-presence/test/javascripts/acceptance/discourse-presence-test.js @@ -6,12 +6,7 @@ import { leaveChannel, presentUserIds, } from "discourse/tests/helpers/presence-pretender"; -import { - acceptance, - count, - exists, - query, -} from "discourse/tests/helpers/qunit-helpers"; +import { acceptance } from "discourse/tests/helpers/qunit-helpers"; import selectKit from "discourse/tests/helpers/select-kit-helper"; acceptance("Discourse Presence Plugin", function (needs) { @@ -33,7 +28,7 @@ acceptance("Discourse Presence Plugin", function (needs) { assert.strictEqual( currentURL(), "/t/internationalization-localization/280", - "it transitions to the newly created topic URL" + "transitions to the newly created topic URL" ); }); @@ -41,7 +36,7 @@ acceptance("Discourse Presence Plugin", function (needs) { await visit("/t/internationalization-localization/280"); await click("#topic-footer-buttons .btn.create"); - assert.ok(exists(".d-editor-input"), "the composer input is visible"); + assert.dom(".d-editor-input").exists("the composer input is visible"); assert.deepEqual( presentUserIds("/discourse-presence/reply/280"), @@ -70,7 +65,7 @@ acceptance("Discourse Presence Plugin", function (needs) { await visit("/t/internationalization-localization/280"); await click("#topic-footer-buttons .btn.create"); - assert.ok(exists(".d-editor-input"), "the composer input is visible"); + assert.dom(".d-editor-input").exists("the composer input is visible"); await fillIn(".d-editor-input", "this is the content of my reply"); @@ -84,11 +79,9 @@ acceptance("Discourse Presence Plugin", function (needs) { await menu.expand(); await menu.selectRowByName("toggle-whisper"); - assert.strictEqual( - count(".composer-actions svg.d-icon-far-eye-slash"), - 1, - "it sets the post type to whisper" - ); + assert + .dom(".composer-actions svg.d-icon-far-eye-slash") + .exists("sets the post type to whisper"); assert.deepEqual( presentUserIds("/discourse-presence/reply/280"), @@ -117,11 +110,13 @@ acceptance("Discourse Presence Plugin", function (needs) { await click(".topic-post:nth-of-type(1) button.show-more-actions"); await click(".topic-post:nth-of-type(1) button.edit"); - assert.strictEqual( - query(".d-editor-input").value, - query(".topic-post:nth-of-type(1) .cooked > p").innerText, - "composer has contents of post to be edited" - ); + assert + .dom(".d-editor-input") + .hasValue( + document.querySelector(".topic-post:nth-of-type(1) .cooked > p") + .innerText, + "composer has contents of post to be edited" + ); assert.deepEqual( presentUserIds("/discourse-presence/edit/398"), @@ -158,7 +153,7 @@ acceptance("Discourse Presence Plugin", function (needs) { assert .dom(".topic-above-footer-buttons-outlet.presence") .exists("includes the presence component"); - assert.strictEqual(count(avatarSelector), 0, "no avatars displayed"); + assert.dom(avatarSelector).doesNotExist("no avatars displayed"); await joinChannel("/discourse-presence/reply/280", { id: 123, @@ -166,7 +161,7 @@ acceptance("Discourse Presence Plugin", function (needs) { username: "my-username", }); - assert.strictEqual(count(avatarSelector), 1, "avatar displayed"); + assert.dom(avatarSelector).exists({ count: 1 }, "avatar displayed"); await joinChannel("/discourse-presence/whisper/280", { id: 124, @@ -174,28 +169,28 @@ acceptance("Discourse Presence Plugin", function (needs) { username: "my-username2", }); - assert.strictEqual(count(avatarSelector), 2, "whisper avatar displayed"); + assert.dom(avatarSelector).exists({ count: 2 }, "whisper avatar displayed"); await leaveChannel("/discourse-presence/reply/280", { id: 123, }); - assert.strictEqual(count(avatarSelector), 1, "reply avatar removed"); + assert.dom(avatarSelector).exists({ count: 1 }, "reply avatar removed"); await leaveChannel("/discourse-presence/whisper/280", { id: 124, }); - assert.strictEqual(count(avatarSelector), 0, "whisper avatar removed"); + assert.dom(avatarSelector).doesNotExist("whisper avatar removed"); }); test("Displays replying and whispering presence in composer", async function (assert) { await visit("/t/internationalization-localization/280"); await click("#topic-footer-buttons .btn.create"); - assert.ok(exists(".d-editor-input"), "the composer input is visible"); + assert.dom(".d-editor-input").exists("the composer input is visible"); const avatarSelector = ".reply-to .presence-avatars .avatar"; - assert.strictEqual(count(avatarSelector), 0, "no avatars displayed"); + assert.dom(avatarSelector).doesNotExist("no avatars displayed"); await joinChannel("/discourse-presence/reply/280", { id: 123, @@ -203,7 +198,7 @@ acceptance("Discourse Presence Plugin", function (needs) { username: "my-username", }); - assert.strictEqual(count(avatarSelector), 1, "avatar displayed"); + assert.dom(avatarSelector).exists({ count: 1 }, "avatar displayed"); await joinChannel("/discourse-presence/whisper/280", { id: 124, @@ -211,18 +206,18 @@ acceptance("Discourse Presence Plugin", function (needs) { username: "my-username2", }); - assert.strictEqual(count(avatarSelector), 2, "whisper avatar displayed"); + assert.dom(avatarSelector).exists({ count: 2 }, "whisper avatar displayed"); await leaveChannel("/discourse-presence/reply/280", { id: 123, }); - assert.strictEqual(count(avatarSelector), 1, "reply avatar removed"); + assert.dom(avatarSelector).exists({ count: 1 }, "reply avatar removed"); await leaveChannel("/discourse-presence/whisper/280", { id: 124, }); - assert.strictEqual(count(avatarSelector), 0, "whisper avatar removed"); + assert.dom(avatarSelector).doesNotExist("whisper avatar removed"); }); });