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 5e9dacb83b7..84b7650ff16 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, { bind } from "discourse-common/utils/decorators"; +import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; export default Controller.extend({ @@ -23,22 +23,24 @@ export default Controller.extend({ subscribe() { this.messageBus.subscribe( `/web_hook_events/${this.get("model.extras.web_hook_id")}`, - this._addIncoming + (data) => { + if (data.event_type === "ping") { + this.set("pingDisabled", false); + } + this._addIncoming(data.web_hook_event_id); + } ); }, unsubscribe() { - this.messageBus.unsubscribe("/web_hook_events/*", this._addIncoming); + this.messageBus.unsubscribe("/web_hook_events/*"); }, - @bind - _addIncoming(data) { - if (data.event_type === "ping") { - this.set("pingDisabled", false); - } + _addIncoming(eventId) { + const incomingEventIds = this.incomingEventIds; - if (!this.incomingEventIds.includes(data.web_hook_event_id)) { - this.incomingEventIds.pushObject(data.web_hook_event_id); + if (!incomingEventIds.includes(eventId)) { + incomingEventIds.pushObject(eventId); } }, 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 bdbcf2d220c..3d9248d36d5 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,33 +2,30 @@ import Controller from "@ember/controller"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { bind } from "discourse-common/utils/decorators"; +import messageBus from "message-bus-client"; export default Controller.extend(ModalFunctionality, { message: I18n.t("admin.user.merging_user"), onShow() { - this.messageBus.subscribe("/merge_user", this.onMessage); + 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")); + } + }); }, onClose() { - 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")); - } + this.messageBus.unsubscribe("/merge_user"); }, }); 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 1f7ea93a693..cb05c26580f 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", this.onMessage); - }, - - deactivate() { - this.messageBus.unsubscribe("/admin/backups", this.onMessage); + this.messageBus.subscribe("/admin/backups", (backups) => + this.controller.set( + "model", + backups.map((backup) => Backup.create(backup)) + ) + ); }, model() { @@ -17,11 +17,7 @@ export default Route.extend({ ); }, - @bind - onMessage(backups) { - this.controller.set( - "model", - backups.map((backup) => Backup.create(backup)) - ); + deactivate() { + this.messageBus.unsubscribe("/admin/backups"); }, }); diff --git a/app/assets/javascripts/admin/addon/routes/admin-backups.js b/app/assets/javascripts/admin/addon/routes/admin-backups.js index 1379a097544..c78ef4ca1cf 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-backups.js +++ b/app/assets/javascripts/admin/addon/routes/admin-backups.js @@ -10,19 +10,46 @@ 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, this.onMessage); - }, - - deactivate() { - this.messageBus.unsubscribe(LOG_CHANNEL, this.onMessage); + 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)); + } + }); }, model() { @@ -37,31 +64,8 @@ export default DiscourseRoute.extend({ ); }, - @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)); - } + deactivate() { + this.messageBus.unsubscribe(LOG_CHANNEL); }, 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 b7839ddf478..ca8fae33180 100644 --- a/app/assets/javascripts/discourse/app/components/basic-topic-list.js +++ b/app/assets/javascripts/discourse/app/components/basic-topic-list.js @@ -1,8 +1,5 @@ import { alias, not } from "@ember/object/computed"; -import discourseComputed, { - bind, - observes, -} from "discourse-common/utils/decorators"; +import discourseComputed, { observes } from "discourse-common/utils/decorators"; import Component from "@ember/component"; export default Component.extend({ @@ -43,11 +40,18 @@ export default Component.extend({ this._super(...arguments); this.topics.forEach((topic) => { - if (typeof topic.unread_by_group_member !== "undefined") { - this.messageBus.subscribe( - `/private-messages/unread-indicator/${topic.id}`, - this.onMessage - ); + 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); + }); } }); }, @@ -55,19 +59,15 @@ export default Component.extend({ willDestroyElement() { this._super(...arguments); - this.messageBus.unsubscribe( - "/private-messages/unread-indicator/*", - this.onMessage - ); - }, + this.topics.forEach((topic) => { + const includeUnreadIndicator = + typeof topic.unread_by_group_member !== "undefined"; - @bind - onMessage(data) { - const nodeClassList = document.querySelector( - `.indicator-topic-${data.topic_id}` - ).classList; - - nodeClassList.toggle("read", !data.show_indicator); + if (includeUnreadIndicator) { + const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`; + this.messageBus.unsubscribe(unreadIndicatorChannel); + } + }); }, @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 21abaa87bce..6d32dd77e53 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, { bind, on } from "discourse-common/utils/decorators"; +import discourseComputed, { on } from "discourse-common/utils/decorators"; import Component from "@ember/component"; import { action } from "@ember/object"; import { isTesting } from "discourse-common/config/environment"; @@ -13,49 +13,36 @@ 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 16247e89c85..2d6ce3e18fd 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -77,7 +77,13 @@ export default Component.extend({ this._super(...arguments); if (this.includeUnreadIndicator) { - this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage); + this.messageBus.subscribe(this.unreadIndicatorChannel, (data) => { + const nodeClassList = document.querySelector( + `.indicator-topic-${data.topic_id}` + ).classList; + + nodeClassList.toggle("read", !data.show_indicator); + }); } schedule("afterRender", () => { @@ -95,8 +101,9 @@ export default Component.extend({ willDestroyElement() { this._super(...arguments); - this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage); - + if (this.includeUnreadIndicator) { + this.messageBus.unsubscribe(this.unreadIndicatorChannel); + } if (this._shouldFocusLastVisited()) { const title = this._titleElement(); if (title) { @@ -106,15 +113,6 @@ 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 7018e62431d..eca4aca13dd 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -2,10 +2,7 @@ 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, { - bind, - observes, -} from "discourse-common/utils/decorators"; +import discourseComputed, { 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"; @@ -1602,9 +1599,157 @@ 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")}`, - this.onMessage, + (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.get("model.message_bus_last_id") ); }, @@ -1614,148 +1759,9 @@ export default Controller.extend(bufferedProperty("model"), { if (!this.get("model.id")) { return; } - this.messageBus.unsubscribe("/topic/*"); }, - @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() { this.replyToPost(); }, diff --git a/app/assets/javascripts/discourse/app/initializers/banner.js b/app/assets/javascripts/discourse/app/initializers/banner.js index 09859d7ccbf..f766003b4b1 100644 --- a/app/assets/javascripts/discourse/app/initializers/banner.js +++ b/app/assets/javascripts/discourse/app/initializers/banner.js @@ -1,5 +1,4 @@ import EmberObject from "@ember/object"; -import { bind } from "discourse-common/utils/decorators"; import PreloadStore from "discourse/lib/preload-store"; export default { @@ -7,21 +6,14 @@ export default { after: "message-bus", initialize(container) { - this.site = container.lookup("service:site"); - this.messageBus = container.lookup("service:message-bus"); - + const site = container.lookup("service:site"); const banner = EmberObject.create(PreloadStore.get("banner") || {}); - this.site.set("banner", banner); + const messageBus = container.lookup("service:message-bus"); - this.messageBus.subscribe("/site/banner", this.onMessage); - }, + site.set("banner", banner); - teardown() { - this.messageBus.unsubscribe("/site/banner", this.onMessage); - }, - - @bind - onMessage(data = {}) { - this.site.set("banner", EmberObject.create(data)); + messageBus.subscribe("/site/banner", (data) => { + 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 71c9d324998..65be3e36377 100644 --- a/app/assets/javascripts/discourse/app/initializers/live-development.js +++ b/app/assets/javascripts/discourse/app/initializers/live-development.js @@ -1,14 +1,13 @@ 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) { - this.messageBus = container.lookup("service:message-bus"); + const messageBus = container.lookup("service:message-bus"); const session = container.lookup("service:session"); // Preserve preview_theme_id=## and pp=async-flamegraph parameters across pages @@ -39,46 +38,37 @@ export default { } // Observe file changes - this.messageBus.subscribe( + messageBus.subscribe( "/file-change", - this.onFileChange, + (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); + } + } + } + }); + }, 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 ed9ca4e5f38..70eb8068da1 100644 --- a/app/assets/javascripts/discourse/app/initializers/logout.js +++ b/app/assets/javascripts/discourse/app/initializers/logout.js @@ -1,43 +1,35 @@ 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) { - this.messageBus = container.lookup("service:message-bus"); - this.dialog = container.lookup("service:dialog"); + const messageBus = container.lookup("service:message-bus"), + dialog = container.lookup("service:dialog"); - if (!this.messageBus) { + if (!messageBus) { return; } - this.messageBus.subscribe("/logout", this.onMessage); - }, + messageBus.subscribe("/logout", function () { + if (!_showingLogout) { + _showingLogout = true; - teardown() { - this.messageBus.unsubscribe("/logout", this.onMessage); - }, + dialog.alert({ + message: I18n.t("logout"), + confirmButtonLabel: "home", + didConfirm: logout, + didCancel: logout, + shouldDisplayCancel: false, + }); + } - @bind - onMessage() { - if (_showingLogout) { - return; - } - - _showingLogout = true; - - this.dialog.alert({ - message: I18n.t("logout"), - confirmButtonLabel: "home", - didConfirm: logout, - didCancel: logout, - shouldDisplayCancel: false, + _showingLogout = true; }); }, }; diff --git a/app/assets/javascripts/discourse/app/initializers/read-only.js b/app/assets/javascripts/discourse/app/initializers/read-only.js index 8d322350b75..7ab4ddb9a3f 100644 --- a/app/assets/javascripts/discourse/app/initializers/read-only.js +++ b/app/assets/javascripts/discourse/app/initializers/read-only.js @@ -1,23 +1,13 @@ -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) { - 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); + const messageBus = container.lookup("service:message-bus"); + const site = container.lookup("service:site"); + messageBus.subscribe("/site/read-only", (enabled) => { + site.set("isReadOnly", enabled); + }); }, }; 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 b00f24f83dc..e161df53d38 100644 --- a/app/assets/javascripts/discourse/app/initializers/welcome-topic-banner.js +++ b/app/assets/javascripts/discourse/app/initializers/welcome-topic-banner.js @@ -1,24 +1,15 @@ -import { bind } from "discourse-common/utils/decorators"; - export default { name: "welcome-topic-banner", after: "message-bus", initialize(container) { - this.site = container.lookup("service:site"); - this.messageBus = container.lookup("service:message-bus"); + const site = container.lookup("service:site"); - if (this.site.show_welcome_topic_banner) { - this.messageBus.subscribe("/site/welcome-topic-banner", this.onMessage); + 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); + }); } }, - - 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 50d05a2fde7..4fa65a43b59 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 } from "discourse-common/utils/decorators"; +import discourseComputed, { bind, on } 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,33 +46,13 @@ function hasMutedTags(topicTags, mutedTags, siteSettings) { const TopicTrackingState = EmberObject.extend({ messageCount: 0, - init() { - this._super(...arguments); - + @on("init") + _setup() { 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 @@ -94,34 +74,26 @@ const TopicTrackingState = EmberObject.extend({ ); } - this.messageBus.subscribe("/delete", this.onDeleteMessage); - this.messageBus.subscribe("/recover", this.onRecoverMessage); - this.messageBus.subscribe("/destroy", this.onDestroyMessage); - }, + this.messageBus.subscribe("/delete", (msg) => { + this.modifyStateProp(msg, "deleted", true); + this.incrementMessageCount(); + }); - @bind - onDeleteMessage(msg) { - this.modifyStateProp(msg, "deleted", true); - this.incrementMessageCount(); - }, + this.messageBus.subscribe("/recover", (msg) => { + this.modifyStateProp(msg, "deleted", false); + this.incrementMessageCount(); + }); - @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("/"); - } + 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("/"); + } + }); }, mutedTopics() { @@ -308,7 +280,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/sub-cat/6/l/latest or tags/c/cat/sub-cat/6/test/l/latest. + * c/cat/subcat/6/l/latest or tags/c/cat/subcat/6/test/l/latest. */ trackIncoming(filter) { this.newIncoming = []; @@ -340,7 +312,7 @@ const TopicTrackingState = EmberObject.extend({ }, /** - * Used to determine whether to show the message at the top of the topic list + * Used to determine whether toshow the message at the top of the topic list * e.g. "see 1 new or updated topic" * * @method incomingCount @@ -583,7 +555,7 @@ const TopicTrackingState = EmberObject.extend({ }, /** - * Using the array of tags provided, tallies up all topics via forEachTracked + * Using the array of tags provided, tallys 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 b8b50155932..ba54502f20c 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 }); - this.topicTrackingState = TopicTrackingState.create({ + const topicTrackingState = TopicTrackingState.create({ messageBus: container.lookup("service:message-bus"), siteSettings, currentUser, }); - app.register("service:topic-tracking-state", this.topicTrackingState, { + app.register("service:topic-tracking-state", topicTrackingState, { instantiate: false, }); @@ -108,11 +108,6 @@ export default { }); } - startTracking(this.topicTrackingState); - }, - - teardown() { - // Manually call `willDestroy` as this isn't an actual `Service` - this.topicTrackingState.willDestroy(); + startTracking(topicTrackingState); }, }; diff --git a/app/assets/javascripts/discourse/app/routes/review-index.js b/app/assets/javascripts/discourse/app/routes/review-index.js index 23fd3ab4450..074b2079948 100644 --- a/app/assets/javascripts/discourse/app/routes/review-index.js +++ b/app/assets/javascripts/discourse/app/routes/review-index.js @@ -1,7 +1,6 @@ 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) { @@ -52,9 +51,34 @@ 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 ); }, @@ -62,46 +86,19 @@ 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 b3efdef6878..c33c8d1b6cc 100644 --- a/app/assets/javascripts/discourse/app/routes/user.js +++ b/app/assets/javascripts/discourse/app/routes/user.js @@ -2,9 +2,25 @@ 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"); @@ -52,64 +68,29 @@ export default DiscourseRoute.extend({ this._super(...arguments); const user = this.modelFor("user"); - 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}`, (data) => + user.loadUserAction(data) ); + 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.onUserMessage - ); - this.messageBus.unsubscribe( - `/u/${user.username_lower}/counters`, - this.onUserCountersMessage - ); + this.messageBus.unsubscribe(`/u/${user.username_lower}`); + this.messageBus.unsubscribe(`/u/${user.username_lower}/counters`); 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 2b3276ae76e..2e3143a9076 100644 --- a/app/assets/javascripts/discourse/app/services/logs-notice.js +++ b/app/assets/javascripts/discourse/app/services/logs-notice.js @@ -1,7 +1,4 @@ -import discourseComputed, { - bind, - observes, -} from "discourse-common/utils/decorators"; +import discourseComputed, { observes } from "discourse-common/utils/decorators"; import Service from "@ember/service"; import I18n from "I18n"; import { autoUpdatingRelativeAge } from "discourse/lib/formatter"; @@ -32,41 +29,32 @@ export default Service.extend({ this.set("text", text); } - this.messageBus.subscribe("/logs_error_rate_exceeded", this.onLogRateLimit); - }, + this.messageBus.subscribe("/logs_error_rate_exceeded", (data) => { + const duration = data.duration; + const rate = data.rate; + let siteSettingLimit = 0; - willDestroy() { - this._super(...arguments); + 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; + } - this.messageBus.unsubscribe( - "/logs_error_rate_exceeded", - this.onLogRateLimit - ); - }, + let translationKey = rate === siteSettingLimit ? "reached" : "exceeded"; + translationKey += `_${duration}_MF`; - @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"), - }) - ); + 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 4c841b9fbe8..1afefabbff1 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,7 +1,8 @@ import { Promise } from "rsvp"; + import Service from "@ember/service"; import { ajax } from "discourse/lib/ajax"; -import { bind } from "discourse-common/utils/decorators"; +import { bind, on } from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { deepEqual, deepMerge } from "discourse-common/lib/object"; import { @@ -20,9 +21,8 @@ const PrivateMessageTopicTrackingState = Service.extend({ filter: null, activeGroup: null, - init() { - this._super(...arguments); - + @on("init") + _setup() { this.states = new Map(); this.statesModificationCounter = 0; this.isTracking = false; @@ -30,16 +30,6 @@ 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); }, @@ -53,6 +43,14 @@ 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) => { @@ -61,10 +59,6 @@ const PrivateMessageTopicTrackingState = Service.extend({ this._processMessage ); }); - - return this._loadInitialState().finally(() => { - this.set("isTracking", true); - }); }, lookupCount(type, opts = {}) { 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 d34421cf670..4b12e22bad2 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,4 +1,4 @@ -import { bind, debounce } from "discourse-common/utils/decorators"; +import { debounce } from "discourse-common/utils/decorators"; import { ajax } from "discourse/lib/ajax"; import { headerOffset } from "discourse/lib/offset-calculator"; import isElementInViewport from "discourse/lib/is-element-in-viewport"; @@ -43,32 +43,35 @@ function initialize(api) { return this._super(bookmark, post); }, - @bind - onMessage(data) { + subscribe() { this._super(...arguments); - const topic = this.model; - - // scroll only for discobot (-2 is discobot id) - if ( - topic.isPrivateMessage && - this.currentUser && - this.currentUser.id !== data.user_id && - data.user_id === -2 && - data.type === "created" - ) { - const postNumber = data.post_number; - const notInPostStream = topic.get("highest_post_number") <= postNumber; - const postNumberDifference = postNumber - topic.currentPost; + this.messageBus.subscribe(`/topic/${this.model.id}`, (data) => { + const topic = this.model; + // scroll only for discobot (-2 is discobot id) if ( - notInPostStream && - postNumberDifference > 0 && - postNumberDifference < 7 + topic.isPrivateMessage && + this.currentUser && + this.currentUser.id !== data.user_id && + data.user_id === -2 && + data.type === "created" ) { - this._scrollToDiscobotPost(data.post_number); + const postNumber = data.post_number; + const notInPostStream = + topic.get("highest_post_number") <= postNumber; + const postNumberDifference = postNumber - topic.currentPost; + + if ( + notInPostStream && + postNumberDifference > 0 && + postNumberDifference < 7 + ) { + this._scrollToDiscobotPost(data.post_number); + } } - } + }); + // No need to unsubscribe, core unsubscribes /topic/* routes }, @debounce(500) diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js index a22b322130e..cd04356b1a0 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 { bind, observes } from "discourse-common/utils/decorators"; +import { observes } from "discourse-common/utils/decorators"; import { withPluginApi } from "discourse/lib/plugin-api"; const PLUGIN_ID = "discourse-poll"; @@ -34,19 +34,16 @@ function initializePolls(api) { subscribe() { this._super(...arguments); - this.messageBus.subscribe(`/polls/${this.model.id}`, this._onPollMessage); + this.messageBus.subscribe(`/polls/${this.model.id}`, (msg) => { + const post = this.get("model.postStream").findLoadedPost(msg.post_id); + post?.set("polls", msg.polls); + }); }, unsubscribe() { - this.messageBus.unsubscribe("/polls/*", this._onPollMessage); + this.messageBus.unsubscribe("/polls/*"); 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", {