From 19214aff18399b4dd83003ebe190fa7f923419a4 Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Mon, 12 Dec 2022 16:32:25 +0100 Subject: [PATCH] DEV: Clean up all message bus subscriptions (#19268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. "What Goes Up Must Come Down" – if you subscribe to message bus, make sure you also unsubscribe 2. When you unsubscribe - remove only your subscription, not **all** subscriptions on given channel Attempt #2. The first attempt tried to extend a core `@bound` method in new-user-narrative plugin which did not work. I reworked that plugin in the meantime. This new PR also cleans up message bus subscriptions in now core-merged chat plugin. --- .../admin-web-hooks-show-events.js | 22 +- .../modals/admin-merge-users-progress.js | 37 +- .../admin/addon/routes/admin-backups-index.js | 20 +- .../admin/addon/routes/admin-backups.js | 68 ++-- .../app/components/basic-topic-list.js | 42 +- .../app/components/software-update-prompt.js | 65 +-- .../app/components/topic-list-item.js | 22 +- .../discourse/app/controllers/topic.js | 296 +++++++------- .../discourse/app/initializers/banner.js | 20 +- .../app/initializers/live-development.js | 64 +-- .../discourse/app/initializers/logout.js | 38 +- .../discourse/app/initializers/read-only.js | 20 +- .../subscribe-user-notifications.js | 375 +++++++++++------- .../discourse/app/initializers/user-tips.js | 46 ++- .../app/initializers/welcome-topic-banner.js | 21 +- .../app/models/topic-tracking-state.js | 76 ++-- .../inject-discourse-objects.js | 11 +- .../discourse/app/routes/review-index.js | 69 ++-- .../javascripts/discourse/app/routes/user.js | 79 ++-- .../discourse/app/services/logs-notice.js | 60 +-- .../app/services/pm-topic-tracking-state.js | 30 +- .../components/chat-channel-archive-status.js | 29 +- .../discourse/components/chat-live-pane.js | 19 +- .../services/chat-notification-manager.js | 17 +- .../javascripts/discourse/services/chat.js | 260 ++++++------ .../initializers/new-user-narrative.js | 84 ++-- .../initializers/extend-for-poll.js | 15 +- 27 files changed, 1090 insertions(+), 815 deletions(-) diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js index 84b7650ff16..5e9dacb83b7 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js @@ -2,7 +2,7 @@ import Controller from "@ember/controller"; import { ajax } from "discourse/lib/ajax"; import { action } from "@ember/object"; import { alias } from "@ember/object/computed"; -import discourseComputed from "discourse-common/utils/decorators"; +import discourseComputed, { bind } from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; export default Controller.extend({ @@ -23,24 +23,22 @@ export default Controller.extend({ subscribe() { this.messageBus.subscribe( `/web_hook_events/${this.get("model.extras.web_hook_id")}`, - (data) => { - if (data.event_type === "ping") { - this.set("pingDisabled", false); - } - this._addIncoming(data.web_hook_event_id); - } + this._addIncoming ); }, unsubscribe() { - this.messageBus.unsubscribe("/web_hook_events/*"); + this.messageBus.unsubscribe("/web_hook_events/*", this._addIncoming); }, - _addIncoming(eventId) { - const incomingEventIds = this.incomingEventIds; + @bind + _addIncoming(data) { + if (data.event_type === "ping") { + this.set("pingDisabled", false); + } - if (!incomingEventIds.includes(eventId)) { - incomingEventIds.pushObject(eventId); + if (!this.incomingEventIds.includes(data.web_hook_event_id)) { + this.incomingEventIds.pushObject(data.web_hook_event_id); } }, diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js index 3d9248d36d5..bdbcf2d220c 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js @@ -2,30 +2,33 @@ import Controller from "@ember/controller"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; -import messageBus from "message-bus-client"; +import { bind } from "discourse-common/utils/decorators"; export default Controller.extend(ModalFunctionality, { message: I18n.t("admin.user.merging_user"), onShow() { - messageBus.subscribe("/merge_user", (data) => { - if (data.merged) { - if (/^\/admin\/users\/list\//.test(location)) { - DiscourseURL.redirectTo(location); - } else { - DiscourseURL.redirectTo( - `/admin/users/${data.user.id}/${data.user.username}` - ); - } - } else if (data.message) { - this.set("message", data.message); - } else if (data.failed) { - this.set("message", I18n.t("admin.user.merge_failed")); - } - }); + this.messageBus.subscribe("/merge_user", this.onMessage); }, onClose() { - this.messageBus.unsubscribe("/merge_user"); + this.messageBus.unsubscribe("/merge_user", this.onMessage); + }, + + @bind + onMessage(data) { + if (data.merged) { + if (/^\/admin\/users\/list\//.test(location)) { + DiscourseURL.redirectTo(location); + } else { + DiscourseURL.redirectTo( + `/admin/users/${data.user.id}/${data.user.username}` + ); + } + } else if (data.message) { + this.set("message", data.message); + } else if (data.failed) { + this.set("message", I18n.t("admin.user.merge_failed")); + } }, }); diff --git a/app/assets/javascripts/admin/addon/routes/admin-backups-index.js b/app/assets/javascripts/admin/addon/routes/admin-backups-index.js index cb05c26580f..1f7ea93a693 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-backups-index.js +++ b/app/assets/javascripts/admin/addon/routes/admin-backups-index.js @@ -1,14 +1,14 @@ import Backup from "admin/models/backup"; import Route from "@ember/routing/route"; +import { bind } from "discourse-common/utils/decorators"; export default Route.extend({ activate() { - this.messageBus.subscribe("/admin/backups", (backups) => - this.controller.set( - "model", - backups.map((backup) => Backup.create(backup)) - ) - ); + this.messageBus.subscribe("/admin/backups", this.onMessage); + }, + + deactivate() { + this.messageBus.unsubscribe("/admin/backups", this.onMessage); }, model() { @@ -17,7 +17,11 @@ export default Route.extend({ ); }, - deactivate() { - this.messageBus.unsubscribe("/admin/backups"); + @bind + onMessage(backups) { + this.controller.set( + "model", + backups.map((backup) => Backup.create(backup)) + ); }, }); diff --git a/app/assets/javascripts/admin/addon/routes/admin-backups.js b/app/assets/javascripts/admin/addon/routes/admin-backups.js index c78ef4ca1cf..1379a097544 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-backups.js +++ b/app/assets/javascripts/admin/addon/routes/admin-backups.js @@ -10,46 +10,19 @@ import { extractError } from "discourse/lib/ajax-error"; import getURL from "discourse-common/lib/get-url"; import showModal from "discourse/lib/show-modal"; import { inject as service } from "@ember/service"; +import { bind } from "discourse-common/utils/decorators"; + const LOG_CHANNEL = "/admin/backups/logs"; export default DiscourseRoute.extend({ dialog: service(), activate() { - this.messageBus.subscribe(LOG_CHANNEL, (log) => { - if (log.message === "[STARTED]") { - User.currentProp("hideReadOnlyAlert", true); - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - true - ); - this.controllerFor("adminBackupsLogs").get("logs").clear(); - } else if (log.message === "[FAILED]") { - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - false - ); - this.dialog.alert( - I18n.t("admin.backups.operations.failed", { - operation: log.operation, - }) - ); - } else if (log.message === "[SUCCESS]") { - User.currentProp("hideReadOnlyAlert", false); - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - false - ); - if (log.operation === "restore") { - // redirect to homepage when the restore is done (session might be lost) - window.location = getURL("/"); - } - } else { - this.controllerFor("adminBackupsLogs") - .get("logs") - .pushObject(EmberObject.create(log)); - } - }); + this.messageBus.subscribe(LOG_CHANNEL, this.onMessage); + }, + + deactivate() { + this.messageBus.unsubscribe(LOG_CHANNEL, this.onMessage); }, model() { @@ -64,8 +37,31 @@ export default DiscourseRoute.extend({ ); }, - deactivate() { - this.messageBus.unsubscribe(LOG_CHANNEL); + @bind + onMessage(log) { + if (log.message === "[STARTED]") { + User.currentProp("hideReadOnlyAlert", true); + this.controllerFor("adminBackups").set("model.isOperationRunning", true); + this.controllerFor("adminBackupsLogs").get("logs").clear(); + } else if (log.message === "[FAILED]") { + this.controllerFor("adminBackups").set("model.isOperationRunning", false); + this.dialog.alert( + I18n.t("admin.backups.operations.failed", { + operation: log.operation, + }) + ); + } else if (log.message === "[SUCCESS]") { + User.currentProp("hideReadOnlyAlert", false); + this.controllerFor("adminBackups").set("model.isOperationRunning", false); + if (log.operation === "restore") { + // redirect to homepage when the restore is done (session might be lost) + window.location = getURL("/"); + } + } else { + this.controllerFor("adminBackupsLogs") + .get("logs") + .pushObject(EmberObject.create(log)); + } }, actions: { diff --git a/app/assets/javascripts/discourse/app/components/basic-topic-list.js b/app/assets/javascripts/discourse/app/components/basic-topic-list.js index ca8fae33180..b7839ddf478 100644 --- a/app/assets/javascripts/discourse/app/components/basic-topic-list.js +++ b/app/assets/javascripts/discourse/app/components/basic-topic-list.js @@ -1,5 +1,8 @@ import { alias, not } from "@ember/object/computed"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import discourseComputed, { + bind, + observes, +} from "discourse-common/utils/decorators"; import Component from "@ember/component"; export default Component.extend({ @@ -40,18 +43,11 @@ export default Component.extend({ this._super(...arguments); this.topics.forEach((topic) => { - const includeUnreadIndicator = - typeof topic.unread_by_group_member !== "undefined"; - - if (includeUnreadIndicator) { - const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`; - this.messageBus.subscribe(unreadIndicatorChannel, (data) => { - const nodeClassList = document.querySelector( - `.indicator-topic-${data.topic_id}` - ).classList; - - nodeClassList.toggle("read", !data.show_indicator); - }); + if (typeof topic.unread_by_group_member !== "undefined") { + this.messageBus.subscribe( + `/private-messages/unread-indicator/${topic.id}`, + this.onMessage + ); } }); }, @@ -59,15 +55,19 @@ export default Component.extend({ willDestroyElement() { this._super(...arguments); - this.topics.forEach((topic) => { - const includeUnreadIndicator = - typeof topic.unread_by_group_member !== "undefined"; + this.messageBus.unsubscribe( + "/private-messages/unread-indicator/*", + this.onMessage + ); + }, - if (includeUnreadIndicator) { - const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`; - this.messageBus.unsubscribe(unreadIndicatorChannel); - } - }); + @bind + onMessage(data) { + const nodeClassList = document.querySelector( + `.indicator-topic-${data.topic_id}` + ).classList; + + nodeClassList.toggle("read", !data.show_indicator); }, @discourseComputed("topics") diff --git a/app/assets/javascripts/discourse/app/components/software-update-prompt.js b/app/assets/javascripts/discourse/app/components/software-update-prompt.js index 6d32dd77e53..21abaa87bce 100644 --- a/app/assets/javascripts/discourse/app/components/software-update-prompt.js +++ b/app/assets/javascripts/discourse/app/components/software-update-prompt.js @@ -1,7 +1,7 @@ import getURL from "discourse-common/lib/get-url"; import { cancel } from "@ember/runloop"; import discourseLater from "discourse-common/lib/later"; -import discourseComputed, { on } from "discourse-common/utils/decorators"; +import discourseComputed, { bind, on } from "discourse-common/utils/decorators"; import Component from "@ember/component"; import { action } from "@ember/object"; import { isTesting } from "discourse-common/config/environment"; @@ -13,36 +13,49 @@ export default Component.extend({ animatePrompt: false, _timeoutHandler: null, + init() { + this._super(...arguments); + + this.messageBus.subscribe("/refresh_client", this.onRefresh); + this.messageBus.subscribe("/global/asset-version", this.onAsset); + }, + + willDestroy() { + this._super(...arguments); + + this.messageBus.unsubscribe("/refresh_client", this.onRefresh); + this.messageBus.unsubscribe("/global/asset-version", this.onAsset); + }, + + @bind + onRefresh() { + this.session.requiresRefresh = true; + }, + + @bind + onAsset(version) { + if (this.session.assetVersion !== version) { + this.session.requiresRefresh = true; + } + + if (!this._timeoutHandler && this.session.requiresRefresh) { + if (isTesting()) { + this.updatePromptState(true); + } else { + // Since we can do this transparently for people browsing the forum + // hold back the message 24 hours. + this._timeoutHandler = discourseLater(() => { + this.updatePromptState(true); + }, 1000 * 60 * 24 * 60); + } + } + }, + @discourseComputed rootUrl() { return getURL("/"); }, - @on("init") - initSubscriptions() { - this.messageBus.subscribe("/refresh_client", () => { - this.session.requiresRefresh = true; - }); - - this.messageBus.subscribe("/global/asset-version", (version) => { - if (this.session.assetVersion !== version) { - this.session.requiresRefresh = true; - } - - if (!this._timeoutHandler && this.session.requiresRefresh) { - if (isTesting()) { - this.updatePromptState(true); - } else { - // Since we can do this transparently for people browsing the forum - // hold back the message 24 hours. - this._timeoutHandler = discourseLater(() => { - this.updatePromptState(true); - }, 1000 * 60 * 24 * 60); - } - } - }); - }, - updatePromptState(value) { // when adding the message, we inject the HTML then add the animation // when dismissing, things need to happen in the opposite order diff --git a/app/assets/javascripts/discourse/app/components/topic-list-item.js b/app/assets/javascripts/discourse/app/components/topic-list-item.js index 2d6ce3e18fd..16247e89c85 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -77,13 +77,7 @@ export default Component.extend({ this._super(...arguments); if (this.includeUnreadIndicator) { - this.messageBus.subscribe(this.unreadIndicatorChannel, (data) => { - const nodeClassList = document.querySelector( - `.indicator-topic-${data.topic_id}` - ).classList; - - nodeClassList.toggle("read", !data.show_indicator); - }); + this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage); } schedule("afterRender", () => { @@ -101,9 +95,8 @@ export default Component.extend({ willDestroyElement() { this._super(...arguments); - if (this.includeUnreadIndicator) { - this.messageBus.unsubscribe(this.unreadIndicatorChannel); - } + this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage); + if (this._shouldFocusLastVisited()) { const title = this._titleElement(); if (title) { @@ -113,6 +106,15 @@ export default Component.extend({ } }, + @bind + onMessage(data) { + const nodeClassList = document.querySelector( + `.indicator-topic-${data.topic_id}` + ).classList; + + nodeClassList.toggle("read", !data.show_indicator); + }, + @discourseComputed("topic.id") unreadIndicatorChannel(topicId) { return `/private-messages/unread-indicator/${topicId}`; diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index 7c27241370b..dc6d24f9586 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -2,7 +2,10 @@ import Category from "discourse/models/category"; import Controller, { inject as controller } from "@ember/controller"; import DiscourseURL, { userPath } from "discourse/lib/url"; import { alias, and, not, or } from "@ember/object/computed"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import discourseComputed, { + bind, + observes, +} from "discourse-common/utils/decorators"; import { isEmpty, isPresent } from "@ember/utils"; import { next, schedule } from "@ember/runloop"; import discourseLater from "discourse-common/lib/later"; @@ -1599,157 +1602,9 @@ export default Controller.extend(bufferedProperty("model"), { subscribe() { this.unsubscribe(); - const refresh = (args) => - this.appEvents.trigger("post-stream:refresh", args); - this.messageBus.subscribe( `/topic/${this.get("model.id")}`, - (data) => { - const topic = this.model; - - if (isPresent(data.notification_level_change)) { - topic.set( - "details.notification_level", - data.notification_level_change - ); - topic.set( - "details.notifications_reason_id", - data.notifications_reason_id - ); - return; - } - - const postStream = this.get("model.postStream"); - - if (data.reload_topic) { - topic.reload().then(() => { - this.send("postChangedRoute", topic.get("post_number") || 1); - this.appEvents.trigger("header:update-topic", topic); - if (data.refresh_stream) { - postStream.refresh(); - } - }); - - return; - } - - switch (data.type) { - case "acted": - postStream - .triggerChangedPost(data.id, data.updated_at, { - preserveCooked: true, - }) - .then(() => refresh({ id: data.id, refreshLikes: true })); - break; - case "read": { - postStream - .triggerReadPost(data.id, data.readers_count) - .then(() => refresh({ id: data.id, refreshLikes: true })); - break; - } - case "liked": - case "unliked": { - postStream - .triggerLikedPost( - data.id, - data.likes_count, - data.user_id, - data.type - ) - .then(() => refresh({ id: data.id, refreshLikes: true })); - break; - } - case "revised": - case "rebaked": { - postStream - .triggerChangedPost(data.id, data.updated_at) - .then(() => refresh({ id: data.id })); - break; - } - case "deleted": { - postStream - .triggerDeletedPost(data.id) - .then(() => refresh({ id: data.id })); - break; - } - case "destroyed": { - postStream - .triggerDestroyedPost(data.id) - .then(() => refresh({ id: data.id })); - break; - } - case "recovered": { - postStream - .triggerRecoveredPost(data.id) - .then(() => refresh({ id: data.id })); - break; - } - case "created": { - this._newPostsInStream.push(data.id); - - this.retryOnRateLimit(RETRIES_ON_RATE_LIMIT, () => { - const postIds = this._newPostsInStream; - this._newPostsInStream = []; - - return postStream - .triggerNewPostsInStream(postIds, { background: true }) - .then(() => refresh()) - .catch((e) => { - this._newPostsInStream = postIds.concat( - this._newPostsInStream - ); - throw e; - }); - }); - - if (this.get("currentUser.id") !== data.user_id) { - this.documentTitle.incrementBackgroundContextCount(); - } - break; - } - case "move_to_inbox": { - topic.set("message_archived", false); - break; - } - case "archived": { - topic.set("message_archived", true); - break; - } - case "stats": { - let updateStream = false; - ["last_posted_at", "like_count", "posts_count"].forEach( - (property) => { - const value = data[property]; - if (typeof value !== "undefined") { - topic.set(property, value); - updateStream = true; - } - } - ); - - if (data["last_poster"]) { - topic.details.set("last_poster", data["last_poster"]); - updateStream = true; - } - - if (updateStream) { - postStream - .triggerChangedTopicStats() - .then((firstPostId) => refresh({ id: firstPostId })); - } - break; - } - default: { - let callback = customPostMessageCallbacks[data.type]; - if (callback) { - callback(this, data); - } else { - // eslint-disable-next-line no-console - console.warn("unknown topic bus message type", data); - } - } - } - }, + this.onMessage, this.get("model.message_bus_last_id") ); }, @@ -1759,7 +1614,146 @@ export default Controller.extend(bufferedProperty("model"), { if (!this.get("model.id")) { return; } - this.messageBus.unsubscribe("/topic/*"); + + this.messageBus.unsubscribe("/topic/*", this.onMessage); + }, + + @bind + onMessage(data) { + const topic = this.model; + const refresh = (args) => + this.appEvents.trigger("post-stream:refresh", args); + + if (isPresent(data.notification_level_change)) { + topic.set("details.notification_level", data.notification_level_change); + topic.set( + "details.notifications_reason_id", + data.notifications_reason_id + ); + return; + } + + const postStream = this.get("model.postStream"); + + if (data.reload_topic) { + topic.reload().then(() => { + this.send("postChangedRoute", topic.get("post_number") || 1); + this.appEvents.trigger("header:update-topic", topic); + if (data.refresh_stream) { + postStream.refresh(); + } + }); + + return; + } + + switch (data.type) { + case "acted": + postStream + .triggerChangedPost(data.id, data.updated_at, { + preserveCooked: true, + }) + .then(() => refresh({ id: data.id, refreshLikes: true })); + break; + case "read": { + postStream + .triggerReadPost(data.id, data.readers_count) + .then(() => refresh({ id: data.id, refreshLikes: true })); + break; + } + case "liked": + case "unliked": { + postStream + .triggerLikedPost(data.id, data.likes_count, data.user_id, data.type) + .then(() => refresh({ id: data.id, refreshLikes: true })); + break; + } + case "revised": + case "rebaked": { + postStream + .triggerChangedPost(data.id, data.updated_at) + .then(() => refresh({ id: data.id })); + break; + } + case "deleted": { + postStream + .triggerDeletedPost(data.id) + .then(() => refresh({ id: data.id })); + break; + } + case "destroyed": { + postStream + .triggerDestroyedPost(data.id) + .then(() => refresh({ id: data.id })); + break; + } + case "recovered": { + postStream + .triggerRecoveredPost(data.id) + .then(() => refresh({ id: data.id })); + break; + } + case "created": { + this._newPostsInStream.push(data.id); + + this.retryOnRateLimit(RETRIES_ON_RATE_LIMIT, () => { + const postIds = this._newPostsInStream; + this._newPostsInStream = []; + + return postStream + .triggerNewPostsInStream(postIds, { background: true }) + .then(() => refresh()) + .catch((e) => { + this._newPostsInStream = postIds.concat(this._newPostsInStream); + throw e; + }); + }); + + if (this.get("currentUser.id") !== data.user_id) { + this.documentTitle.incrementBackgroundContextCount(); + } + break; + } + case "move_to_inbox": { + topic.set("message_archived", false); + break; + } + case "archived": { + topic.set("message_archived", true); + break; + } + case "stats": { + let updateStream = false; + ["last_posted_at", "like_count", "posts_count"].forEach((property) => { + const value = data[property]; + if (typeof value !== "undefined") { + topic.set(property, value); + updateStream = true; + } + }); + + if (data["last_poster"]) { + topic.details.set("last_poster", data["last_poster"]); + updateStream = true; + } + + if (updateStream) { + postStream + .triggerChangedTopicStats() + .then((firstPostId) => refresh({ id: firstPostId })); + } + break; + } + default: { + let callback = customPostMessageCallbacks[data.type]; + if (callback) { + callback(this, data); + } else { + // eslint-disable-next-line no-console + console.warn("unknown topic bus message type", data); + } + } + } }, reply() { diff --git a/app/assets/javascripts/discourse/app/initializers/banner.js b/app/assets/javascripts/discourse/app/initializers/banner.js index f766003b4b1..09859d7ccbf 100644 --- a/app/assets/javascripts/discourse/app/initializers/banner.js +++ b/app/assets/javascripts/discourse/app/initializers/banner.js @@ -1,4 +1,5 @@ import EmberObject from "@ember/object"; +import { bind } from "discourse-common/utils/decorators"; import PreloadStore from "discourse/lib/preload-store"; export default { @@ -6,14 +7,21 @@ export default { after: "message-bus", initialize(container) { - const site = container.lookup("service:site"); + this.site = container.lookup("service:site"); + this.messageBus = container.lookup("service:message-bus"); + const banner = EmberObject.create(PreloadStore.get("banner") || {}); - const messageBus = container.lookup("service:message-bus"); + this.site.set("banner", banner); - site.set("banner", banner); + this.messageBus.subscribe("/site/banner", this.onMessage); + }, - messageBus.subscribe("/site/banner", (data) => { - site.set("banner", EmberObject.create(data || {})); - }); + teardown() { + this.messageBus.unsubscribe("/site/banner", this.onMessage); + }, + + @bind + onMessage(data = {}) { + this.site.set("banner", EmberObject.create(data)); }, }; diff --git a/app/assets/javascripts/discourse/app/initializers/live-development.js b/app/assets/javascripts/discourse/app/initializers/live-development.js index 65be3e36377..71c9d324998 100644 --- a/app/assets/javascripts/discourse/app/initializers/live-development.js +++ b/app/assets/javascripts/discourse/app/initializers/live-development.js @@ -1,13 +1,14 @@ import DiscourseURL from "discourse/lib/url"; import { isDevelopment } from "discourse-common/config/environment"; import discourseLater from "discourse-common/lib/later"; +import { bind } from "discourse-common/utils/decorators"; // Use the message bus for live reloading of components for faster development. export default { name: "live-development", initialize(container) { - const messageBus = container.lookup("service:message-bus"); + this.messageBus = container.lookup("service:message-bus"); const session = container.lookup("service:session"); // Preserve preview_theme_id=## and pp=async-flamegraph parameters across pages @@ -38,37 +39,46 @@ export default { } // Observe file changes - messageBus.subscribe( + this.messageBus.subscribe( "/file-change", - (data) => { - data.forEach((me) => { - if (me === "refresh") { - // Refresh if necessary - document.location.reload(true); - } else if (me.new_href && me.target) { - const link_target = !!me.theme_id - ? `[data-target='${me.target}'][data-theme-id='${me.theme_id}']` - : `[data-target='${me.target}']`; - - const links = document.querySelectorAll(`link${link_target}`); - if (links.length > 0) { - const lastLink = links[links.length - 1]; - // this check is useful when message-bus has multiple file updates - // it avoids the browser doing a lot of work for nothing - // should the filenames be unchanged - if ( - lastLink.href.split("/").pop() !== me.new_href.split("/").pop() - ) { - this.refreshCSS(lastLink, me.new_href); - } - } - } - }); - }, + this.onFileChange, session.mbLastFileChangeId ); }, + teardown() { + this.messageBus.unsubscribe("/file-change", this.onFileChange); + }, + + @bind + onFileChange(data) { + data.forEach((me) => { + if (me === "refresh") { + // Refresh if necessary + document.location.reload(true); + } else if (me.new_href && me.target) { + let query = `link[data-target='${me.target}']`; + + if (me.theme_id) { + query += `[data-theme-id='${me.theme_id}']`; + } + + const links = document.querySelectorAll(query); + + if (links.length > 0) { + const lastLink = links[links.length - 1]; + + // this check is useful when message-bus has multiple file updates + // it avoids the browser doing a lot of work for nothing + // should the filenames be unchanged + if (lastLink.href.split("/").pop() !== me.new_href.split("/").pop()) { + this.refreshCSS(lastLink, me.new_href); + } + } + } + }); + }, + refreshCSS(node, newHref) { const reloaded = node.cloneNode(true); reloaded.href = newHref; diff --git a/app/assets/javascripts/discourse/app/initializers/logout.js b/app/assets/javascripts/discourse/app/initializers/logout.js index 70eb8068da1..91e0808509d 100644 --- a/app/assets/javascripts/discourse/app/initializers/logout.js +++ b/app/assets/javascripts/discourse/app/initializers/logout.js @@ -1,35 +1,39 @@ import I18n from "I18n"; import logout from "discourse/lib/logout"; +import { bind } from "discourse-common/utils/decorators"; let _showingLogout = false; -// Subscribe to "logout" change events via the Message Bus +// Subscribe to "logout" change events via the Message Bus export default { name: "logout", after: "message-bus", initialize(container) { - const messageBus = container.lookup("service:message-bus"), - dialog = container.lookup("service:dialog"); + this.messageBus = container.lookup("service:message-bus"); + this.dialog = container.lookup("service:dialog"); - if (!messageBus) { + this.messageBus.subscribe("/logout", this.onMessage); + }, + + teardown() { + this.messageBus.unsubscribe("/logout", this.onMessage); + }, + + @bind + onMessage() { + if (_showingLogout) { return; } - messageBus.subscribe("/logout", function () { - if (!_showingLogout) { - _showingLogout = true; + _showingLogout = true; - dialog.alert({ - message: I18n.t("logout"), - confirmButtonLabel: "home", - didConfirm: logout, - didCancel: logout, - shouldDisplayCancel: false, - }); - } - - _showingLogout = true; + this.dialog.alert({ + message: I18n.t("logout"), + confirmButtonLabel: "home", + didConfirm: logout, + didCancel: logout, + shouldDisplayCancel: false, }); }, }; diff --git a/app/assets/javascripts/discourse/app/initializers/read-only.js b/app/assets/javascripts/discourse/app/initializers/read-only.js index 7ab4ddb9a3f..8d322350b75 100644 --- a/app/assets/javascripts/discourse/app/initializers/read-only.js +++ b/app/assets/javascripts/discourse/app/initializers/read-only.js @@ -1,13 +1,23 @@ +import { bind } from "discourse-common/utils/decorators"; + // Subscribe to "read-only" status change events via the Message Bus export default { name: "read-only", after: "message-bus", initialize(container) { - const messageBus = container.lookup("service:message-bus"); - const site = container.lookup("service:site"); - messageBus.subscribe("/site/read-only", (enabled) => { - site.set("isReadOnly", enabled); - }); + this.messageBus = container.lookup("service:message-bus"); + this.site = container.lookup("service:site"); + + this.messageBus.subscribe("/site/read-only", this.onMessage); + }, + + teardown() { + this.messageBus.unsubscribe("/site/read-only", this.onMessage); + }, + + @bind + onMessage(enabled) { + this.site.set("isReadOnly", enabled); }, }; diff --git a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js index 85b4aa071a7..94fb9385ce5 100644 --- a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js +++ b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js @@ -12,175 +12,254 @@ import { } from "discourse/lib/push-notifications"; import { isTesting } from "discourse-common/config/environment"; import Notification from "discourse/models/notification"; +import { bind } from "discourse-common/utils/decorators"; export default { name: "subscribe-user-notifications", after: "message-bus", initialize(container) { - const user = container.lookup("service:current-user"); - const bus = container.lookup("service:message-bus"); - const appEvents = container.lookup("service:app-events"); - const siteSettings = container.lookup("service:site-settings"); + this.currentUser = container.lookup("service:current-user"); - if (user) { - const channel = user.redesigned_user_menu_enabled - ? `/reviewable_counts/${user.id}` - : "/reviewable_counts"; + if (!this.currentUser) { + return; + } - bus.subscribe(channel, (data) => { - if (data.reviewable_count >= 0) { - user.updateReviewableCount(data.reviewable_count); - } + this.messageBus = container.lookup("service:message-bus"); + this.store = container.lookup("service:store"); + this.messageBus = container.lookup("service:message-bus"); + this.appEvents = container.lookup("service:app-events"); + this.siteSettings = container.lookup("service:site-settings"); + this.site = container.lookup("service:site"); + this.router = container.lookup("router:main"); - if (user.redesigned_user_menu_enabled) { - user.set("unseen_reviewable_count", data.unseen_reviewable_count); - } - }); + this.reviewableCountsChannel = this.currentUser.redesigned_user_menu_enabled + ? `/reviewable_counts/${this.currentUser.id}` + : "/reviewable_counts"; - bus.subscribe( - `/notification/${user.id}`, - (data) => { - const store = container.lookup("service:store"); - const oldUnread = user.unread_notifications; - const oldHighPriority = user.unread_high_priority_notifications; - const oldAllUnread = user.all_unread_notifications_count; + this.messageBus.subscribe( + this.reviewableCountsChannel, + this.onReviewableCounts + ); - user.setProperties({ - unread_notifications: data.unread_notifications, - unread_high_priority_notifications: - data.unread_high_priority_notifications, - read_first_notification: data.read_first_notification, - all_unread_notifications_count: data.all_unread_notifications_count, - grouped_unread_notifications: data.grouped_unread_notifications, - new_personal_messages_notifications_count: - data.new_personal_messages_notifications_count, - }); + this.messageBus.subscribe( + `/notification/${this.currentUser.id}`, + this.onNotification, + this.currentUser.notification_channel_position + ); - if ( - oldUnread !== data.unread_notifications || - oldHighPriority !== data.unread_high_priority_notifications || - oldAllUnread !== data.all_unread_notifications_count - ) { - appEvents.trigger("notifications:changed"); + this.messageBus.subscribe( + `/user-drafts/${this.currentUser.id}`, + this.onUserDrafts + ); - if ( - site.mobileView && - (data.unread_notifications - oldUnread > 0 || - data.unread_high_priority_notifications - oldHighPriority > 0 || - data.all_unread_notifications_count - oldAllUnread > 0) - ) { - appEvents.trigger("header:update-topic", null, 5000); - } - } + this.messageBus.subscribe( + `/do-not-disturb/${this.currentUser.id}`, + this.onDoNotDisturb + ); - const stale = store.findStale( - "notification", - {}, - { cacheKey: "recent-notifications" } - ); - const lastNotification = data.last_notification?.notification; + this.messageBus.subscribe(`/user-status`, this.onUserStatus); - if (stale?.hasResults && lastNotification) { - const oldNotifications = stale.results.get("content"); - const staleIndex = oldNotifications.findIndex( - (n) => n.id === lastNotification.id - ); + this.messageBus.subscribe("/categories", this.onCategories); - if (staleIndex === -1) { - let insertPosition = 0; + this.messageBus.subscribe("/client_settings", this.onClientSettings); - // high priority and unread notifications are first - if (!lastNotification.high_priority || lastNotification.read) { - const nextPosition = oldNotifications.findIndex( - (n) => !n.high_priority || n.read - ); + if (!isTesting()) { + this.messageBus.subscribe(alertChannel(this.currentUser), this.onAlert); - if (nextPosition !== -1) { - insertPosition = nextPosition; - } - } + initDesktopNotifications(this.messageBus, this.appEvents); - oldNotifications.insertAt( - insertPosition, - Notification.create(lastNotification) - ); - } - - // remove stale notifications and update existing ones - const read = Object.fromEntries(data.recent); - const newNotifications = oldNotifications - .map((notification) => { - if (read[notification.id] !== undefined) { - notification.set("read", read[notification.id]); - return notification; - } - }) - .filter(Boolean); - - stale.results.set("content", newNotifications); - } - }, - user.notification_channel_position - ); - - bus.subscribe(`/user-drafts/${user.id}`, (data) => { - user.updateDraftProperties(data); - }); - - bus.subscribe(`/do-not-disturb/${user.get("id")}`, (data) => { - user.updateDoNotDisturbStatus(data.ends_at); - }); - - bus.subscribe(`/user-status`, (data) => { - appEvents.trigger("user-status:changed", data); - }); - - const site = container.lookup("service:site"); - const router = container.lookup("router:main"); - - bus.subscribe("/categories", (data) => { - (data.categories || []).forEach((c) => { - const mutedCategoryIds = user.muted_category_ids?.concat( - user.indirectly_muted_category_ids - ); - if ( - mutedCategoryIds && - mutedCategoryIds.includes(c.parent_category_id) && - !mutedCategoryIds.includes(c.id) - ) { - user.set( - "indirectly_muted_category_ids", - user.indirectly_muted_category_ids.concat(c.id) - ); - } - return site.updateCategory(c); - }); - - (data.deleted_categories || []).forEach((id) => - site.removeCategory(id) + if (isPushNotificationsEnabled(this.currentUser)) { + disableDesktopNotifications(); + registerPushNotifications( + this.currentUser, + this.router, + this.appEvents ); - }); - - bus.subscribe( - "/client_settings", - (data) => (siteSettings[data.name] = data.value) - ); - - if (!isTesting()) { - bus.subscribe(alertChannel(user), (data) => - onNotification(data, siteSettings, user) - ); - - initDesktopNotifications(bus, appEvents); - - if (isPushNotificationsEnabled(user)) { - disableDesktopNotifications(); - registerPushNotifications(user, router, appEvents); - } else { - unsubscribePushNotifications(user); - } + } else { + unsubscribePushNotifications(this.currentUser); } } }, + + teardown() { + if (!this.currentUser) { + return; + } + + this.messageBus.unsubscribe( + this.reviewableCountsChannel, + this.onReviewableCounts + ); + + this.messageBus.unsubscribe( + `/notification/${this.currentUser.id}`, + this.onNotification + ); + + this.messageBus.unsubscribe( + `/user-drafts/${this.currentUser.id}`, + this.onUserDrafts + ); + + this.messageBus.unsubscribe( + `/do-not-disturb/${this.currentUser.id}`, + this.onDoNotDisturb + ); + + this.messageBus.unsubscribe(`/user-status`, this.onUserStatus); + + this.messageBus.unsubscribe("/categories", this.onCategories); + + this.messageBus.unsubscribe("/client_settings", this.onClientSettings); + + this.messageBus.unsubscribe(alertChannel(this.currentUser), this.onAlert); + }, + + @bind + onReviewableCounts(data) { + if (data.reviewable_count >= 0) { + this.currentUser.updateReviewableCount(data.reviewable_count); + } + + if (this.currentUser.redesigned_user_menu_enabled) { + this.currentUser.set( + "unseen_reviewable_count", + data.unseen_reviewable_count + ); + } + }, + + @bind + onNotification(data) { + const oldUnread = this.currentUser.unread_notifications; + const oldHighPriority = this.currentUser.unread_high_priority_notifications; + const oldAllUnread = this.currentUser.all_unread_notifications_count; + + this.currentUser.setProperties({ + unread_notifications: data.unread_notifications, + unread_high_priority_notifications: + data.unread_high_priority_notifications, + read_first_notification: data.read_first_notification, + all_unread_notifications_count: data.all_unread_notifications_count, + grouped_unread_notifications: data.grouped_unread_notifications, + new_personal_messages_notifications_count: + data.new_personal_messages_notifications_count, + }); + + if ( + oldUnread !== data.unread_notifications || + oldHighPriority !== data.unread_high_priority_notifications || + oldAllUnread !== data.all_unread_notifications_count + ) { + this.appEvents.trigger("notifications:changed"); + + if ( + this.site.mobileView && + (data.unread_notifications - oldUnread > 0 || + data.unread_high_priority_notifications - oldHighPriority > 0 || + data.all_unread_notifications_count - oldAllUnread > 0) + ) { + this.appEvents.trigger("header:update-topic", null, 5000); + } + } + + const stale = this.store.findStale( + "notification", + {}, + { cacheKey: "recent-notifications" } + ); + const lastNotification = data.last_notification?.notification; + + if (stale?.hasResults && lastNotification) { + const oldNotifications = stale.results.get("content"); + const staleIndex = oldNotifications.findIndex( + (n) => n.id === lastNotification.id + ); + + if (staleIndex === -1) { + let insertPosition = 0; + + // high priority and unread notifications are first + if (!lastNotification.high_priority || lastNotification.read) { + const nextPosition = oldNotifications.findIndex( + (n) => !n.high_priority || n.read + ); + + if (nextPosition !== -1) { + insertPosition = nextPosition; + } + } + + oldNotifications.insertAt( + insertPosition, + Notification.create(lastNotification) + ); + } + + // remove stale notifications and update existing ones + const read = Object.fromEntries(data.recent); + const newNotifications = oldNotifications + .map((notification) => { + if (read[notification.id] !== undefined) { + notification.set("read", read[notification.id]); + return notification; + } + }) + .filter(Boolean); + + stale.results.set("content", newNotifications); + } + }, + + @bind + onUserDrafts(data) { + this.currentUser.updateDraftProperties(data); + }, + + @bind + onDoNotDisturb(data) { + this.currentUser.updateDoNotDisturbStatus(data.ends_at); + }, + + @bind + onUserStatus(data) { + this.appEvents.trigger("user-status:changed", data); + }, + + @bind + onCategories(data) { + (data.categories || []).forEach((c) => { + const mutedCategoryIds = this.currentUser.muted_category_ids?.concat( + this.currentUser.indirectly_muted_category_ids + ); + + if ( + mutedCategoryIds && + mutedCategoryIds.includes(c.parent_category_id) && + !mutedCategoryIds.includes(c.id) + ) { + this.currentUser.set( + "indirectly_muted_category_ids", + this.currentUser.indirectly_muted_category_ids.concat(c.id) + ); + } + + return this.site.updateCategory(c); + }); + + (data.deleted_categories || []).forEach((id) => + this.site.removeCategory(id) + ); + }, + + @bind + onClientSettings(data) { + this.siteSettings[data.name] = data.value; + }, + + @bind + onAlert(data) { + return onNotification(data, this.siteSettings, this.currentUser); + }, }; diff --git a/app/assets/javascripts/discourse/app/initializers/user-tips.js b/app/assets/javascripts/discourse/app/initializers/user-tips.js index 5b4168c0343..16a04cd6e57 100644 --- a/app/assets/javascripts/discourse/app/initializers/user-tips.js +++ b/app/assets/javascripts/discourse/app/initializers/user-tips.js @@ -1,29 +1,41 @@ +import { bind } from "discourse-common/utils/decorators"; + export default { name: "user-tips", after: "message-bus", initialize(container) { - const currentUser = container.lookup("service:current-user"); - if (!currentUser) { + this.currentUser = container.lookup("service:current-user"); + if (!this.currentUser) { return; } - const messageBus = container.lookup("service:message-bus"); - const site = container.lookup("service:site"); + this.messageBus = container.lookup("service:message-bus"); + this.site = container.lookup("service:site"); - messageBus.subscribe("/user-tips", function (seenUserTips) { - currentUser.set("seen_popups", seenUserTips); - if (!currentUser.user_option) { - currentUser.set("user_option", {}); - } - currentUser.set("user_option.seen_popups", seenUserTips); - (seenUserTips || []).forEach((userTipId) => { - currentUser.hideUserTipForever( - Object.keys(site.user_tips).find( - (id) => site.user_tips[id] === userTipId - ) - ); - }); + this.messageBus.subscribe("/user-tips", this.onMessage); + }, + + teardown() { + this.messageBus?.unsubscribe("/user-tips", this.onMessage); + }, + + @bind + onMessage(seenUserTips) { + this.currentUser.set("seen_popups", seenUserTips); + + if (!this.currentUser.user_option) { + this.currentUser.set("user_option", {}); + } + + this.currentUser.set("user_option.seen_popups", seenUserTips); + + (seenUserTips || []).forEach((userTipId) => { + this.currentUser.hideUserTipForever( + Object.keys(this.site.user_tips).find( + (id) => this.site.user_tips[id] === userTipId + ) + ); }); }, }; diff --git a/app/assets/javascripts/discourse/app/initializers/welcome-topic-banner.js b/app/assets/javascripts/discourse/app/initializers/welcome-topic-banner.js index e161df53d38..b00f24f83dc 100644 --- a/app/assets/javascripts/discourse/app/initializers/welcome-topic-banner.js +++ b/app/assets/javascripts/discourse/app/initializers/welcome-topic-banner.js @@ -1,15 +1,24 @@ +import { bind } from "discourse-common/utils/decorators"; + export default { name: "welcome-topic-banner", after: "message-bus", initialize(container) { - const site = container.lookup("service:site"); + this.site = container.lookup("service:site"); + this.messageBus = container.lookup("service:message-bus"); - if (site.show_welcome_topic_banner) { - const messageBus = container.lookup("service:message-bus"); - messageBus.subscribe("/site/welcome-topic-banner", (disabled) => { - site.set("show_welcome_topic_banner", disabled); - }); + if (this.site.show_welcome_topic_banner) { + this.messageBus.subscribe("/site/welcome-topic-banner", this.onMessage); } }, + + teardown() { + this.messageBus.unsubscribe("/site/welcome-topic-banner", this.onMessage); + }, + + @bind + onMessage(disabled) { + this.site.set("show_welcome_topic_banner", disabled); + }, }; diff --git a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js index 4fa65a43b59..50d05a2fde7 100644 --- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js @@ -1,5 +1,5 @@ import EmberObject, { get } from "@ember/object"; -import discourseComputed, { bind, on } from "discourse-common/utils/decorators"; +import discourseComputed, { bind } from "discourse-common/utils/decorators"; import Category from "discourse/models/category"; import { deepEqual, deepMerge } from "discourse-common/lib/object"; import DiscourseURL from "discourse/lib/url"; @@ -46,13 +46,33 @@ function hasMutedTags(topicTags, mutedTags, siteSettings) { const TopicTrackingState = EmberObject.extend({ messageCount: 0, - @on("init") - _setup() { + init() { + this._super(...arguments); + this.states = new Map(); this.stateChangeCallbacks = {}; this._trackedTopicLimit = 4000; }, + willDestroy() { + this._super(...arguments); + + this.messageBus.unsubscribe("/latest", this._processChannelPayload); + + if (this.currentUser) { + this.messageBus.unsubscribe("/new", this._processChannelPayload); + this.messageBus.unsubscribe(`/unread`, this._processChannelPayload); + this.messageBus.unsubscribe( + `/unread/${this.currentUser.id}`, + this._processChannelPayload + ); + } + + this.messageBus.unsubscribe("/delete", this.onDeleteMessage); + this.messageBus.unsubscribe("/recover", this.onRecoverMessage); + this.messageBus.unsubscribe("/destroy", this.onDestroyMessage); + }, + /** * Subscribe to MessageBus channels which are used for publishing changes * to the tracking state. Each message received will modify state for @@ -74,26 +94,34 @@ const TopicTrackingState = EmberObject.extend({ ); } - this.messageBus.subscribe("/delete", (msg) => { - this.modifyStateProp(msg, "deleted", true); - this.incrementMessageCount(); - }); + this.messageBus.subscribe("/delete", this.onDeleteMessage); + this.messageBus.subscribe("/recover", this.onRecoverMessage); + this.messageBus.subscribe("/destroy", this.onDestroyMessage); + }, - this.messageBus.subscribe("/recover", (msg) => { - this.modifyStateProp(msg, "deleted", false); - this.incrementMessageCount(); - }); + @bind + onDeleteMessage(msg) { + this.modifyStateProp(msg, "deleted", true); + this.incrementMessageCount(); + }, - this.messageBus.subscribe("/destroy", (msg) => { - this.incrementMessageCount(); - const currentRoute = DiscourseURL.router.currentRoute.parent; - if ( - currentRoute.name === "topic" && - parseInt(currentRoute.params.id, 10) === msg.topic_id - ) { - DiscourseURL.redirectTo("/"); - } - }); + @bind + onRecoverMessage(msg) { + this.modifyStateProp(msg, "deleted", false); + this.incrementMessageCount(); + }, + + @bind + onDestroyMessage(msg) { + this.incrementMessageCount(); + const currentRoute = DiscourseURL.router.currentRoute.parent; + + if ( + currentRoute.name === "topic" && + parseInt(currentRoute.params.id, 10) === msg.topic_id + ) { + DiscourseURL.redirectTo("/"); + } }, mutedTopics() { @@ -280,7 +308,7 @@ const TopicTrackingState = EmberObject.extend({ * @param {String} filter - Valid values are all, categories, and any topic list * filters e.g. latest, unread, new. As well as this * specific category and tag URLs like tag/test/l/latest, - * c/cat/subcat/6/l/latest or tags/c/cat/subcat/6/test/l/latest. + * c/cat/sub-cat/6/l/latest or tags/c/cat/sub-cat/6/test/l/latest. */ trackIncoming(filter) { this.newIncoming = []; @@ -312,7 +340,7 @@ const TopicTrackingState = EmberObject.extend({ }, /** - * Used to determine whether toshow the message at the top of the topic list + * Used to determine whether to show the message at the top of the topic list * e.g. "see 1 new or updated topic" * * @method incomingCount @@ -555,7 +583,7 @@ const TopicTrackingState = EmberObject.extend({ }, /** - * Using the array of tags provided, tallys up all topics via forEachTracked + * Using the array of tags provided, tallies up all topics via forEachTracked * that we are tracking, separated into new/unread/total. * * Total is only counted if opts.includeTotal is specified. diff --git a/app/assets/javascripts/discourse/app/pre-initializers/inject-discourse-objects.js b/app/assets/javascripts/discourse/app/pre-initializers/inject-discourse-objects.js index ba54502f20c..b8b50155932 100644 --- a/app/assets/javascripts/discourse/app/pre-initializers/inject-discourse-objects.js +++ b/app/assets/javascripts/discourse/app/pre-initializers/inject-discourse-objects.js @@ -40,13 +40,13 @@ export default { // to register a null value for anon app.register("service:current-user", currentUser, { instantiate: false }); - const topicTrackingState = TopicTrackingState.create({ + this.topicTrackingState = TopicTrackingState.create({ messageBus: container.lookup("service:message-bus"), siteSettings, currentUser, }); - app.register("service:topic-tracking-state", topicTrackingState, { + app.register("service:topic-tracking-state", this.topicTrackingState, { instantiate: false, }); @@ -108,6 +108,11 @@ export default { }); } - startTracking(topicTrackingState); + startTracking(this.topicTrackingState); + }, + + teardown() { + // Manually call `willDestroy` as this isn't an actual `Service` + this.topicTrackingState.willDestroy(); }, }; diff --git a/app/assets/javascripts/discourse/app/routes/review-index.js b/app/assets/javascripts/discourse/app/routes/review-index.js index 074b2079948..23fd3ab4450 100644 --- a/app/assets/javascripts/discourse/app/routes/review-index.js +++ b/app/assets/javascripts/discourse/app/routes/review-index.js @@ -1,6 +1,7 @@ import DiscourseRoute from "discourse/routes/discourse"; import { isPresent } from "@ember/utils"; import { action } from "@ember/object"; +import { bind } from "discourse-common/utils/decorators"; export default DiscourseRoute.extend({ model(params) { @@ -51,34 +52,9 @@ export default DiscourseRoute.extend({ }, activate() { - this._updateClaimedBy = (data) => { - const reviewables = this.controller.reviewables; - if (reviewables) { - const user = data.user - ? this.store.createRecord("user", data.user) - : null; - reviewables.forEach((reviewable) => { - if (data.topic_id === reviewable.topic.id) { - reviewable.set("claimed_by", user); - } - }); - } - }; - - this._updateReviewables = (data) => { - if (data.updates) { - this.controller.reviewables.forEach((reviewable) => { - const updates = data.updates[reviewable.id]; - if (updates) { - reviewable.setProperties(updates); - } - }); - } - }; - this.messageBus.subscribe("/reviewable_claimed", this._updateClaimedBy); this.messageBus.subscribe( - this._reviewableCountsChannel(), + this._reviewableCountsChannel, this._updateReviewables ); }, @@ -86,19 +62,46 @@ export default DiscourseRoute.extend({ deactivate() { this.messageBus.unsubscribe("/reviewable_claimed", this._updateClaimedBy); this.messageBus.unsubscribe( - this._reviewableCountsChannel(), + this._reviewableCountsChannel, this._updateReviewables ); }, + @bind + _updateClaimedBy(data) { + const reviewables = this.controller.reviewables; + if (reviewables) { + const user = data.user + ? this.store.createRecord("user", data.user) + : null; + reviewables.forEach((reviewable) => { + if (data.topic_id === reviewable.topic.id) { + reviewable.set("claimed_by", user); + } + }); + } + }, + + @bind + _updateReviewables(data) { + if (data.updates) { + this.controller.reviewables.forEach((reviewable) => { + const updates = data.updates[reviewable.id]; + if (updates) { + reviewable.setProperties(updates); + } + }); + } + }, + + get _reviewableCountsChannel() { + return this.currentUser.redesigned_user_menu_enabled + ? `/reviewable_counts/${this.currentUser.id}` + : "/reviewable_counts"; + }, + @action refreshRoute() { this.refresh(); }, - - _reviewableCountsChannel() { - return this.currentUser.redesigned_user_menu_enabled - ? `/reviewable_counts/${this.currentUser.id}` - : "/reviewable_counts"; - }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user.js b/app/assets/javascripts/discourse/app/routes/user.js index c33c8d1b6cc..b3efdef6878 100644 --- a/app/assets/javascripts/discourse/app/routes/user.js +++ b/app/assets/javascripts/discourse/app/routes/user.js @@ -2,25 +2,9 @@ import DiscourseRoute from "discourse/routes/discourse"; import I18n from "I18n"; import User from "discourse/models/user"; import { action } from "@ember/object"; +import { bind } from "discourse-common/utils/decorators"; export default DiscourseRoute.extend({ - titleToken() { - const username = this.modelFor("user").username; - if (username) { - return [I18n.t("user.profile"), username]; - } - }, - - @action - undoRevokeApiKey(key) { - key.undoRevoke(); - }, - - @action - revokeApiKey(key) { - key.revoke(); - }, - beforeModel() { if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { this.replaceWith("discovery"); @@ -68,29 +52,64 @@ export default DiscourseRoute.extend({ this._super(...arguments); const user = this.modelFor("user"); - this.messageBus.subscribe(`/u/${user.username_lower}`, (data) => - user.loadUserAction(data) + this.messageBus.subscribe(`/u/${user.username_lower}`, this.onUserMessage); + this.messageBus.subscribe( + `/u/${user.username_lower}/counters`, + this.onUserCountersMessage ); - this.messageBus.subscribe(`/u/${user.username_lower}/counters`, (data) => { - user.setProperties(data); - Object.entries(data).forEach(([key, value]) => - this.appEvents.trigger( - `count-updated:${user.username_lower}:${key}`, - value - ) - ); - }); }, deactivate() { this._super(...arguments); const user = this.modelFor("user"); - this.messageBus.unsubscribe(`/u/${user.username_lower}`); - this.messageBus.unsubscribe(`/u/${user.username_lower}/counters`); + this.messageBus.unsubscribe( + `/u/${user.username_lower}`, + this.onUserMessage + ); + this.messageBus.unsubscribe( + `/u/${user.username_lower}/counters`, + this.onUserCountersMessage + ); user.stopTrackingStatus(); // Remove the search context this.searchService.set("searchContext", null); }, + + @bind + onUserMessage(data) { + const user = this.modelFor("user"); + return user.loadUserAction(data); + }, + + @bind + onUserCountersMessage(data) { + const user = this.modelFor("user"); + user.setProperties(data); + + Object.entries(data).forEach(([key, value]) => + this.appEvents.trigger( + `count-updated:${user.username_lower}:${key}`, + value + ) + ); + }, + + titleToken() { + const username = this.modelFor("user").username; + if (username) { + return [I18n.t("user.profile"), username]; + } + }, + + @action + undoRevokeApiKey(key) { + key.undoRevoke(); + }, + + @action + revokeApiKey(key) { + key.revoke(); + }, }); diff --git a/app/assets/javascripts/discourse/app/services/logs-notice.js b/app/assets/javascripts/discourse/app/services/logs-notice.js index 2e3143a9076..2b3276ae76e 100644 --- a/app/assets/javascripts/discourse/app/services/logs-notice.js +++ b/app/assets/javascripts/discourse/app/services/logs-notice.js @@ -1,4 +1,7 @@ -import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import discourseComputed, { + bind, + observes, +} from "discourse-common/utils/decorators"; import Service from "@ember/service"; import I18n from "I18n"; import { autoUpdatingRelativeAge } from "discourse/lib/formatter"; @@ -29,32 +32,41 @@ export default Service.extend({ this.set("text", text); } - this.messageBus.subscribe("/logs_error_rate_exceeded", (data) => { - const duration = data.duration; - const rate = data.rate; - let siteSettingLimit = 0; + this.messageBus.subscribe("/logs_error_rate_exceeded", this.onLogRateLimit); + }, - if (duration === "minute") { - siteSettingLimit = this.siteSettings.alert_admins_if_errors_per_minute; - } else if (duration === "hour") { - siteSettingLimit = this.siteSettings.alert_admins_if_errors_per_hour; - } + willDestroy() { + this._super(...arguments); - let translationKey = rate === siteSettingLimit ? "reached" : "exceeded"; - translationKey += `_${duration}_MF`; + this.messageBus.unsubscribe( + "/logs_error_rate_exceeded", + this.onLogRateLimit + ); + }, - this.set( - "text", - I18n.messageFormat(`logs_error_rate_notice.${translationKey}`, { - relativeAge: autoUpdatingRelativeAge( - new Date(data.publish_at * 1000) - ), - rate, - limit: siteSettingLimit, - url: getURL("/logs"), - }) - ); - }); + @bind + onLogRateLimit(data) { + const { duration, rate } = data; + let siteSettingLimit = 0; + + if (duration === "minute") { + siteSettingLimit = this.siteSettings.alert_admins_if_errors_per_minute; + } else if (duration === "hour") { + siteSettingLimit = this.siteSettings.alert_admins_if_errors_per_hour; + } + + let translationKey = rate === siteSettingLimit ? "reached" : "exceeded"; + translationKey += `_${duration}_MF`; + + this.set( + "text", + I18n.messageFormat(`logs_error_rate_notice.${translationKey}`, { + relativeAge: autoUpdatingRelativeAge(new Date(data.publish_at * 1000)), + rate, + limit: siteSettingLimit, + url: getURL("/logs"), + }) + ); }, @discourseComputed("text") diff --git a/app/assets/javascripts/discourse/app/services/pm-topic-tracking-state.js b/app/assets/javascripts/discourse/app/services/pm-topic-tracking-state.js index 1afefabbff1..4c841b9fbe8 100644 --- a/app/assets/javascripts/discourse/app/services/pm-topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/services/pm-topic-tracking-state.js @@ -1,8 +1,7 @@ import { Promise } from "rsvp"; - import Service from "@ember/service"; import { ajax } from "discourse/lib/ajax"; -import { bind, on } from "discourse-common/utils/decorators"; +import { bind } from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { deepEqual, deepMerge } from "discourse-common/lib/object"; import { @@ -21,8 +20,9 @@ const PrivateMessageTopicTrackingState = Service.extend({ filter: null, activeGroup: null, - @on("init") - _setup() { + init() { + this._super(...arguments); + this.states = new Map(); this.statesModificationCounter = 0; this.isTracking = false; @@ -30,6 +30,16 @@ const PrivateMessageTopicTrackingState = Service.extend({ this.stateChangeCallbacks = new Map(); }, + willDestroy() { + this._super(...arguments); + + if (this.currentUser) { + this.messageBus.unsubscribe(this.userChannel(), this._processMessage); + } + + this.messageBus.unsubscribe(this.groupChannel("*"), this._processMessage); + }, + onStateChange(key, callback) { this.stateChangeCallbacks.set(key, callback); }, @@ -43,14 +53,6 @@ const PrivateMessageTopicTrackingState = Service.extend({ return Promise.resolve(); } - this._establishChannels(); - - return this._loadInitialState().finally(() => { - this.set("isTracking", true); - }); - }, - - _establishChannels() { this.messageBus.subscribe(this.userChannel(), this._processMessage); this.currentUser.groupsWithMessages?.forEach((group) => { @@ -59,6 +61,10 @@ const PrivateMessageTopicTrackingState = Service.extend({ this._processMessage ); }); + + return this._loadInitialState().finally(() => { + this.set("isTracking", true); + }); }, lookupCount(type, opts = {}) { diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js index 6f006cddd09..e8f20834527 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js @@ -5,7 +5,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error"; import { ajax } from "discourse/lib/ajax"; import getURL from "discourse-common/lib/get-url"; import { action } from "@ember/object"; -import discourseComputed from "discourse-common/utils/decorators"; +import discourseComputed, { bind } from "discourse-common/utils/decorators"; export default Component.extend({ channel: null, @@ -56,26 +56,29 @@ export default Component.extend({ didInsertElement() { this._super(...arguments); if (this.currentUser.admin) { - this.messageBus.subscribe("/chat/channel-archive-status", (busData) => { - if (busData.chat_channel_id === this.channel.id) { - this.channel.setProperties({ - archive_failed: busData.archive_failed, - archive_completed: busData.archive_completed, - archived_messages: busData.archived_messages, - archive_topic_id: busData.archive_topic_id, - total_messages: busData.total_messages, - }); - } - }); + this.messageBus.subscribe("/chat/channel-archive-status", this.onMessage); } }, willDestroyElement() { this._super(...arguments); - this.messageBus.unsubscribe("/chat/channel-archive-status"); + this.messageBus.unsubscribe("/chat/channel-archive-status", this.onMessage); }, _getTopicUrl() { return getURL(`/t/-/${this.channel.archive_topic_id}`); }, + + @bind + onMessage(busData) { + if (busData.chat_channel_id === this.channel.id) { + this.channel.setProperties({ + archive_failed: busData.archive_failed, + archive_completed: busData.archive_completed, + archived_messages: busData.archived_messages, + archive_topic_id: busData.archive_topic_id, + total_messages: busData.total_messages, + }); + } + }, }); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js index 47db5ee3875..edfe800ff79 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js @@ -1496,24 +1496,27 @@ export default Component.extend({ }, _unsubscribeToUpdates(channelId) { - this.messageBus.unsubscribe(`/chat/${channelId}`); + this.messageBus.unsubscribe(`/chat/${channelId}`, this.onMessage); }, _subscribeToUpdates(channelId) { this._unsubscribeToUpdates(channelId); this.messageBus.subscribe( `/chat/${channelId}`, - (busData) => { - if (!this.details.can_load_more_future || busData.type !== "sent") { - this.handleMessage(busData); - } else { - this.set("hasNewMessages", true); - } - }, + this.onMessage, this.details.channel_message_bus_last_id ); }, + @bind + onMessage(busData) { + if (!this.details.can_load_more_future || busData.type !== "sent") { + this.handleMessage(busData); + } else { + this.set("hasNewMessages", true); + } + }, + @bind _forceBodyScroll() { // when keyboard is visible this will ensure body diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-notification-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-notification-manager.js index da8b500cafa..686c552515b 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-notification-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-notification-manager.js @@ -102,13 +102,11 @@ export default class ChatNotificationManager extends Service { this.set("_countChatInDocTitle", true); if (!this._subscribedToChat) { - this.messageBus.subscribe(this._chatAlertChannel(), (data) => - onNotification(data, this.siteSettings, this.currentUser) - ); + this.messageBus.subscribe(this._chatAlertChannel(), this.onMessage); } if (opts.only && this._subscribedToCore) { - this.messageBus.unsubscribe(this._coreAlertChannel()); + this.messageBus.unsubscribe(this._coreAlertChannel(), this.onMessage); this.set("_subscribedToCore", false); } } @@ -118,17 +116,20 @@ export default class ChatNotificationManager extends Service { this.set("_countChatInDocTitle", false); } if (!this._subscribedToCore) { - this.messageBus.subscribe(this._coreAlertChannel(), (data) => - onNotification(data, this.siteSettings, this.currentUser) - ); + this.messageBus.subscribe(this._coreAlertChannel(), this.onMessage); } if (this.only && this._subscribedToChat) { - this.messageBus.unsubscribe(this._chatAlertChannel()); + this.messageBus.unsubscribe(this._chatAlertChannel(), this.onMessage); this.set("_subscribedToChat", false); } } + @bind + onMessage(data) { + return onNotification(data, this.siteSettings, this.currentUser); + } + _shouldRun() { return this.chat.userCanChat && !isTesting(); } diff --git a/plugins/chat/assets/javascripts/discourse/services/chat.js b/plugins/chat/assets/javascripts/discourse/services/chat.js index cbbd352b585..26feb212750 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat.js @@ -20,6 +20,7 @@ import EmberObject, { computed } from "@ember/object"; import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api"; import discourseLater from "discourse-common/lib/later"; import userPresent from "discourse/lib/user-presence"; +import { bind } from "discourse-common/utils/decorators"; export const LIST_VIEW = "list_view"; export const CHAT_VIEW = "chat_view"; @@ -57,6 +58,8 @@ export default class Chat extends Service { isNetworkUnreliable = false; @and("currentUser.has_chat_enabled", "siteSettings.chat_enabled") userCanChat; _fetchingChannels = null; + _onNewMentionsCallbacks = new Map(); + _onNewMessagesCallbacks = new Map(); @computed("currentUser.staff", "currentUser.groups.[]") get userCanDirectMessage() { @@ -607,16 +610,7 @@ export default class Chat extends Service { _subscribeToChannelMetadata() { this.messageBus.subscribe( "/chat/channel-metadata", - (busData) => { - this.getChannelBy("id", busData.chat_channel_id).then((channel) => { - if (channel) { - channel.setProperties({ - memberships_count: busData.memberships_count, - }); - this.appEvents.trigger("chat:refresh-channel-members"); - } - }); - }, + this._onChannelMetadata, this.messageBusLastIds.channel_metadata ); } @@ -624,71 +618,40 @@ export default class Chat extends Service { _subscribeToChannelEdits() { this.messageBus.subscribe( "/chat/channel-edits", - (busData) => { - this.getChannelBy("id", busData.chat_channel_id).then((channel) => { - if (channel) { - channel.setProperties({ - title: busData.name, - description: busData.description, - }); - } - }); - }, + this._onChannelEdits, this.messageBusLastIds.channel_edits ); } _subscribeToChannelStatusChange() { - this.messageBus.subscribe("/chat/channel-status", (busData) => { - this.getChannelBy("id", busData.chat_channel_id).then((channel) => { - if (!channel) { - return; - } - - channel.set("status", busData.status); - - // it is not possible for the user to set their last read message id - // if the channel has been archived, because all the messages have - // been deleted. we don't want them seeing the blue dot anymore so - // just completely reset the unreads - if (busData.status === CHANNEL_STATUSES.archived) { - this.currentUser.chat_channel_tracking_state[channel.id] = { - unread_count: 0, - unread_mentions: 0, - chatable_type: channel.chatable_type, - }; - this.userChatChannelTrackingStateChanged(); - } - - this.appEvents.trigger("chat:refresh-channel", channel.id); - }, this.messageBusLastIds.channel_status); - }); + this.messageBus.subscribe("/chat/channel-status", this._onChannelStatus); } _unsubscribeFromChannelStatusChange() { - this.messageBus.unsubscribe("/chat/channel-status"); + this.messageBus.unsubscribe("/chat/channel-status", this._onChannelStatus); } _unsubscribeFromChannelEdits() { - this.messageBus.unsubscribe("/chat/channel-edits"); + this.messageBus.unsubscribe("/chat/channel-edits", this._onChannelEdits); } _unsubscribeFromChannelMetadata() { - this.messageBus.unsubscribe("/chat/channel-metadata"); + this.messageBus.unsubscribe( + "/chat/channel-metadata", + this._onChannelMetadata + ); } _subscribeToNewChannelUpdates() { this.messageBus.subscribe( "/chat/new-channel", - (busData) => { - this.startTrackingChannel(ChatChannel.create(busData.chat_channel)); - }, + this._onNewChannel, this.messageBusLastIds.new_channel ); } _unsubscribeFromNewDmChannelUpdates() { - this.messageBus.unsubscribe("/chat/new-channel"); + this.messageBus.unsubscribe("/chat/new-channel", this._onNewChannel); } _subscribeToSingleUpdateChannel(channel) { @@ -709,59 +672,123 @@ export default class Chat extends Service { } _subscribeToMentionChannel(channel) { + const onNewMentions = () => { + const trackingState = + this.currentUser.chat_channel_tracking_state[channel.id]; + + if (trackingState) { + const count = (trackingState.unread_mentions || 0) + 1; + trackingState.set("unread_mentions", count); + this.userChatChannelTrackingStateChanged(); + } + }; + + this._onNewMentionsCallbacks.set(channel.id, onNewMentions); + this.messageBus.subscribe( `/chat/${channel.id}/new-mentions`, - () => { - const trackingState = - this.currentUser.chat_channel_tracking_state[channel.id]; - if (trackingState) { - trackingState.set( - "unread_mentions", - (trackingState.unread_mentions || 0) + 1 - ); - this.userChatChannelTrackingStateChanged(); - } - }, + onNewMentions, channel.message_bus_last_ids.new_mentions ); } _subscribeToNewMessagesChannel(channel) { - this.messageBus.subscribe( - `/chat/${channel.id}/new-messages`, - (busData) => { - const trackingState = - this.currentUser.chat_channel_tracking_state[channel.id]; + const onNewMessages = (busData) => { + const trackingState = + this.currentUser.chat_channel_tracking_state[channel.id]; - if (busData.user_id === this.currentUser.id) { - // User sent message, update tracking state to no unread + if (busData.user_id === this.currentUser.id) { + // User sent message, update tracking state to no unread + trackingState.set("chat_message_id", busData.message_id); + } else { + // Ignored user sent message, update tracking state to no unread + if (this.currentUser.ignored_users.includes(busData.username)) { trackingState.set("chat_message_id", busData.message_id); } else { - // Ignored user sent message, update tracking state to no unread - if (this.currentUser.ignored_users.includes(busData.username)) { - trackingState.set("chat_message_id", busData.message_id); - } else { - // Message from other user. Increment trackings state - if (busData.message_id > (trackingState.chat_message_id || 0)) { - trackingState.set("unread_count", trackingState.unread_count + 1); - } + // Message from other user. Increment trackings state + if (busData.message_id > (trackingState.chat_message_id || 0)) { + trackingState.set("unread_count", trackingState.unread_count + 1); } } - this.userChatChannelTrackingStateChanged(); - channel.set("last_message_sent_at", new Date()); + } - const directMessageChannel = (this.directMessageChannels || []).findBy( - "id", - parseInt(channel.id, 10) - ); - if (directMessageChannel) { - this.reSortDirectMessageChannels(); - } - }, + this.userChatChannelTrackingStateChanged(); + channel.set("last_message_sent_at", new Date()); + + const directMessageChannel = (this.directMessageChannels || []).findBy( + "id", + parseInt(channel.id, 10) + ); + + if (directMessageChannel) { + this.reSortDirectMessageChannels(); + } + }; + + this._onNewMessagesCallbacks.set(channel.id, onNewMessages); + + this.messageBus.subscribe( + `/chat/${channel.id}/new-messages`, + onNewMessages, channel.message_bus_last_ids.new_messages ); } + @bind + _onChannelMetadata(busData) { + this.getChannelBy("id", busData.chat_channel_id).then((channel) => { + if (channel) { + channel.setProperties({ + memberships_count: busData.memberships_count, + }); + this.appEvents.trigger("chat:refresh-channel-members"); + } + }); + } + + @bind + _onChannelEdits(busData) { + this.getChannelBy("id", busData.chat_channel_id).then((channel) => { + if (channel) { + channel.setProperties({ + title: busData.name, + description: busData.description, + }); + } + }); + } + + @bind + _onChannelStatus(busData) { + this.getChannelBy("id", busData.chat_channel_id).then((channel) => { + if (!channel) { + return; + } + + channel.set("status", busData.status); + + // it is not possible for the user to set their last read message id + // if the channel has been archived, because all the messages have + // been deleted. we don't want them seeing the blue dot anymore so + // just completely reset the unreads + if (busData.status === CHANNEL_STATUSES.archived) { + this.currentUser.chat_channel_tracking_state[channel.id] = { + unread_count: 0, + unread_mentions: 0, + chatable_type: channel.chatable_type, + }; + this.userChatChannelTrackingStateChanged(); + } + + this.appEvents.trigger("chat:refresh-channel", channel.id); + }, this.messageBusLastIds.channel_status); + } + + @bind + _onNewChannel(busData) { + this.startTrackingChannel(ChatChannel.create(busData.chat_channel)); + } + async followChannel(channel) { return ChatApi.followChatChannel(channel).then(() => { this.startTrackingChannel(channel); @@ -787,49 +814,54 @@ export default class Chat extends Service { } _unsubscribeFromChatChannel(channel) { - this.messageBus.unsubscribe(`/chat/${channel.id}/new-messages`); + this.messageBus.unsubscribe("/chat/*", this._onNewMessagesCallbacks); if (!channel.isDirectMessageChannel) { - this.messageBus.unsubscribe(`/chat/${channel.id}/new-mentions`); + this.messageBus.unsubscribe("/chat/*", this._onNewMentionsCallbacks); } } _subscribeToUserTrackingChannel() { this.messageBus.subscribe( `/chat/user-tracking-state/${this.currentUser.id}`, - (busData, _, messageId) => { - const lastId = this.lastUserTrackingMessageId; - - // we don't want this state to go backwards, only catch - // up if messages from messagebus were missed - if (!lastId || messageId > lastId) { - this.lastUserTrackingMessageId = messageId; - } - - // we are too far out of sync, we should resync everything. - // this will trigger a route transition and blur the chat input - if (lastId && messageId > lastId + 1) { - return this.forceRefreshChannels(); - } - - const trackingState = - this.currentUser.chat_channel_tracking_state[busData.chat_channel_id]; - if (trackingState) { - trackingState.set("chat_message_id", busData.chat_message_id); - trackingState.set("unread_count", 0); - trackingState.set("unread_mentions", 0); - this.userChatChannelTrackingStateChanged(); - } - }, + this._onUserTrackingState, this.messageBusLastIds.user_tracking_state ); } _unsubscribeFromUserTrackingChannel() { this.messageBus.unsubscribe( - `/chat/user-tracking-state/${this.currentUser.id}` + `/chat/user-tracking-state/${this.currentUser.id}`, + this._onUserTrackingState ); } + @bind + _onUserTrackingState(busData, _, messageId) { + const lastId = this.lastUserTrackingMessageId; + + // we don't want this state to go backwards, only catch + // up if messages from messagebus were missed + if (!lastId || messageId > lastId) { + this.lastUserTrackingMessageId = messageId; + } + + // we are too far out of sync, we should resync everything. + // this will trigger a route transition and blur the chat input + if (lastId && messageId > lastId + 1) { + return this.forceRefreshChannels(); + } + + const trackingState = + this.currentUser.chat_channel_tracking_state[busData.chat_channel_id]; + + if (trackingState) { + trackingState.set("chat_message_id", busData.chat_message_id); + trackingState.set("unread_count", 0); + trackingState.set("unread_mentions", 0); + this.userChatChannelTrackingStateChanged(); + } + } + resetTrackingStateForChannel(channelId) { const trackingState = this.currentUser.chat_channel_tracking_state[channelId]; diff --git a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js index ef3241193a4..f6123499e76 100644 --- a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js +++ b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js @@ -1,43 +1,61 @@ import { withPluginApi } from "discourse/lib/plugin-api"; - -const PLUGIN_ID = "new-user-narrative"; - -function initialize(api) { - const messageBus = api.container.lookup("service:message-bus"); - const currentUser = api.getCurrentUser(); - const appEvents = api.container.lookup("service:app-events"); - - api.modifyClass("component:site-header", { - pluginId: PLUGIN_ID, - didInsertElement() { - this._super(...arguments); - this.dispatch("header:search-context-trigger", "header"); - }, - }); - - api.attachWidgetAction("header", "headerSearchContextTrigger", function () { - if (this.site.mobileView) { - this.state.skipSearchContext = false; - } else { - this.state.contextEnabled = true; - this.state.searchContextType = "topic"; - } - }); - - if (messageBus && currentUser) { - messageBus.subscribe(`/new_user_narrative/tutorial_search`, () => { - appEvents.trigger("header:search-context-trigger"); - }); - } -} +import { bind } from "discourse-common/utils/decorators"; export default { name: "new-user-narrative", initialize(container) { const siteSettings = container.lookup("service:site-settings"); - if (siteSettings.discourse_narrative_bot_enabled) { - withPluginApi("0.8.7", initialize); + + if (!siteSettings.discourse_narrative_bot_enabled) { + return; } + + this.messageBus = container.lookup("service:message-bus"); + this.appEvents = container.lookup("service:app-events"); + + withPluginApi("0.8.7", (api) => { + const currentUser = api.getCurrentUser(); + + if (!currentUser) { + return; + } + + api.dispatchWidgetAppEvent( + "site-header", + "header", + "header:search-context-trigger" + ); + + api.attachWidgetAction( + "header", + "headerSearchContextTrigger", + function () { + if (this.site.mobileView) { + this.state.skipSearchContext = false; + } else { + this.state.contextEnabled = true; + this.state.searchContextType = "topic"; + } + } + ); + + this.messageBus.subscribe( + "/new_user_narrative/tutorial_search", + this.onMessage + ); + }); + }, + + teardown() { + this.messageBus?.unsubscribe( + "/new_user_narrative/tutorial_search", + this.onMessage + ); + }, + + @bind + onMessage() { + this.appEvents.trigger("header:search-context-trigger"); }, }; diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js index cd04356b1a0..a22b322130e 100644 --- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js +++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js @@ -1,7 +1,7 @@ import EmberObject from "@ember/object"; import WidgetGlue from "discourse/widgets/glue"; import { getRegister } from "discourse-common/lib/get-owner"; -import { observes } from "discourse-common/utils/decorators"; +import { bind, observes } from "discourse-common/utils/decorators"; import { withPluginApi } from "discourse/lib/plugin-api"; const PLUGIN_ID = "discourse-poll"; @@ -34,16 +34,19 @@ function initializePolls(api) { subscribe() { this._super(...arguments); - this.messageBus.subscribe(`/polls/${this.model.id}`, (msg) => { - const post = this.get("model.postStream").findLoadedPost(msg.post_id); - post?.set("polls", msg.polls); - }); + this.messageBus.subscribe(`/polls/${this.model.id}`, this._onPollMessage); }, unsubscribe() { - this.messageBus.unsubscribe("/polls/*"); + this.messageBus.unsubscribe("/polls/*", this._onPollMessage); this._super(...arguments); }, + + @bind + _onPollMessage(msg) { + const post = this.get("model.postStream").findLoadedPost(msg.post_id); + post?.set("polls", msg.polls); + }, }); api.modifyClass("model:post", {