DEV: Clean up all message bus subscriptions ()

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 . 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.
This commit is contained in:
Jarek Radosz 2022-12-12 16:32:25 +01:00 committed by GitHub
parent 93a4012ecb
commit 19214aff18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1090 additions and 815 deletions

@ -2,7 +2,7 @@ import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { alias } from "@ember/object/computed"; 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"; import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({ export default Controller.extend({
@ -23,24 +23,22 @@ export default Controller.extend({
subscribe() { subscribe() {
this.messageBus.subscribe( this.messageBus.subscribe(
`/web_hook_events/${this.get("model.extras.web_hook_id")}`, `/web_hook_events/${this.get("model.extras.web_hook_id")}`,
(data) => { this._addIncoming
if (data.event_type === "ping") {
this.set("pingDisabled", false);
}
this._addIncoming(data.web_hook_event_id);
}
); );
}, },
unsubscribe() { unsubscribe() {
this.messageBus.unsubscribe("/web_hook_events/*"); this.messageBus.unsubscribe("/web_hook_events/*", this._addIncoming);
}, },
_addIncoming(eventId) { @bind
const incomingEventIds = this.incomingEventIds; _addIncoming(data) {
if (data.event_type === "ping") {
this.set("pingDisabled", false);
}
if (!incomingEventIds.includes(eventId)) { if (!this.incomingEventIds.includes(data.web_hook_event_id)) {
incomingEventIds.pushObject(eventId); this.incomingEventIds.pushObject(data.web_hook_event_id);
} }
}, },

@ -2,13 +2,21 @@ import Controller from "@ember/controller";
import DiscourseURL from "discourse/lib/url"; import DiscourseURL from "discourse/lib/url";
import I18n from "I18n"; import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality"; 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, { export default Controller.extend(ModalFunctionality, {
message: I18n.t("admin.user.merging_user"), message: I18n.t("admin.user.merging_user"),
onShow() { onShow() {
messageBus.subscribe("/merge_user", (data) => { this.messageBus.subscribe("/merge_user", this.onMessage);
},
onClose() {
this.messageBus.unsubscribe("/merge_user", this.onMessage);
},
@bind
onMessage(data) {
if (data.merged) { if (data.merged) {
if (/^\/admin\/users\/list\//.test(location)) { if (/^\/admin\/users\/list\//.test(location)) {
DiscourseURL.redirectTo(location); DiscourseURL.redirectTo(location);
@ -22,10 +30,5 @@ export default Controller.extend(ModalFunctionality, {
} else if (data.failed) { } else if (data.failed) {
this.set("message", I18n.t("admin.user.merge_failed")); this.set("message", I18n.t("admin.user.merge_failed"));
} }
});
},
onClose() {
this.messageBus.unsubscribe("/merge_user");
}, },
}); });

@ -1,14 +1,14 @@
import Backup from "admin/models/backup"; import Backup from "admin/models/backup";
import Route from "@ember/routing/route"; import Route from "@ember/routing/route";
import { bind } from "discourse-common/utils/decorators";
export default Route.extend({ export default Route.extend({
activate() { activate() {
this.messageBus.subscribe("/admin/backups", (backups) => this.messageBus.subscribe("/admin/backups", this.onMessage);
this.controller.set( },
"model",
backups.map((backup) => Backup.create(backup)) deactivate() {
) this.messageBus.unsubscribe("/admin/backups", this.onMessage);
);
}, },
model() { model() {
@ -17,7 +17,11 @@ export default Route.extend({
); );
}, },
deactivate() { @bind
this.messageBus.unsubscribe("/admin/backups"); onMessage(backups) {
this.controller.set(
"model",
backups.map((backup) => Backup.create(backup))
);
}, },
}); });

@ -10,46 +10,19 @@ import { extractError } from "discourse/lib/ajax-error";
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
import showModal from "discourse/lib/show-modal"; import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { bind } from "discourse-common/utils/decorators";
const LOG_CHANNEL = "/admin/backups/logs"; const LOG_CHANNEL = "/admin/backups/logs";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
dialog: service(), dialog: service(),
activate() { activate() {
this.messageBus.subscribe(LOG_CHANNEL, (log) => { this.messageBus.subscribe(LOG_CHANNEL, this.onMessage);
if (log.message === "[STARTED]") { },
User.currentProp("hideReadOnlyAlert", true);
this.controllerFor("adminBackups").set( deactivate() {
"model.isOperationRunning", this.messageBus.unsubscribe(LOG_CHANNEL, this.onMessage);
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() { model() {
@ -64,8 +37,31 @@ export default DiscourseRoute.extend({
); );
}, },
deactivate() { @bind
this.messageBus.unsubscribe(LOG_CHANNEL); 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: { actions: {

@ -1,5 +1,8 @@
import { alias, not } from "@ember/object/computed"; 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"; import Component from "@ember/component";
export default Component.extend({ export default Component.extend({
@ -40,18 +43,11 @@ export default Component.extend({
this._super(...arguments); this._super(...arguments);
this.topics.forEach((topic) => { this.topics.forEach((topic) => {
const includeUnreadIndicator = if (typeof topic.unread_by_group_member !== "undefined") {
typeof topic.unread_by_group_member !== "undefined"; this.messageBus.subscribe(
`/private-messages/unread-indicator/${topic.id}`,
if (includeUnreadIndicator) { this.onMessage
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);
});
} }
}); });
}, },
@ -59,15 +55,19 @@ export default Component.extend({
willDestroyElement() { willDestroyElement() {
this._super(...arguments); this._super(...arguments);
this.topics.forEach((topic) => { this.messageBus.unsubscribe(
const includeUnreadIndicator = "/private-messages/unread-indicator/*",
typeof topic.unread_by_group_member !== "undefined"; this.onMessage
);
},
if (includeUnreadIndicator) { @bind
const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`; onMessage(data) {
this.messageBus.unsubscribe(unreadIndicatorChannel); const nodeClassList = document.querySelector(
} `.indicator-topic-${data.topic_id}`
}); ).classList;
nodeClassList.toggle("read", !data.show_indicator);
}, },
@discourseComputed("topics") @discourseComputed("topics")

@ -1,7 +1,7 @@
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
import { cancel } from "@ember/runloop"; import { cancel } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later"; 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 Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { isTesting } from "discourse-common/config/environment"; import { isTesting } from "discourse-common/config/environment";
@ -13,18 +13,27 @@ export default Component.extend({
animatePrompt: false, animatePrompt: false,
_timeoutHandler: null, _timeoutHandler: null,
@discourseComputed init() {
rootUrl() { this._super(...arguments);
return getURL("/");
this.messageBus.subscribe("/refresh_client", this.onRefresh);
this.messageBus.subscribe("/global/asset-version", this.onAsset);
}, },
@on("init") willDestroy() {
initSubscriptions() { this._super(...arguments);
this.messageBus.subscribe("/refresh_client", () => {
this.session.requiresRefresh = true;
});
this.messageBus.subscribe("/global/asset-version", (version) => { 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) { if (this.session.assetVersion !== version) {
this.session.requiresRefresh = true; this.session.requiresRefresh = true;
} }
@ -40,7 +49,11 @@ export default Component.extend({
}, 1000 * 60 * 24 * 60); }, 1000 * 60 * 24 * 60);
} }
} }
}); },
@discourseComputed
rootUrl() {
return getURL("/");
}, },
updatePromptState(value) { updatePromptState(value) {

@ -77,13 +77,7 @@ export default Component.extend({
this._super(...arguments); this._super(...arguments);
if (this.includeUnreadIndicator) { if (this.includeUnreadIndicator) {
this.messageBus.subscribe(this.unreadIndicatorChannel, (data) => { this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage);
const nodeClassList = document.querySelector(
`.indicator-topic-${data.topic_id}`
).classList;
nodeClassList.toggle("read", !data.show_indicator);
});
} }
schedule("afterRender", () => { schedule("afterRender", () => {
@ -101,9 +95,8 @@ export default Component.extend({
willDestroyElement() { willDestroyElement() {
this._super(...arguments); this._super(...arguments);
if (this.includeUnreadIndicator) { this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage);
this.messageBus.unsubscribe(this.unreadIndicatorChannel);
}
if (this._shouldFocusLastVisited()) { if (this._shouldFocusLastVisited()) {
const title = this._titleElement(); const title = this._titleElement();
if (title) { 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") @discourseComputed("topic.id")
unreadIndicatorChannel(topicId) { unreadIndicatorChannel(topicId) {
return `/private-messages/unread-indicator/${topicId}`; return `/private-messages/unread-indicator/${topicId}`;

@ -2,7 +2,10 @@ import Category from "discourse/models/category";
import Controller, { inject as controller } from "@ember/controller"; import Controller, { inject as controller } from "@ember/controller";
import DiscourseURL, { userPath } from "discourse/lib/url"; import DiscourseURL, { userPath } from "discourse/lib/url";
import { alias, and, not, or } from "@ember/object/computed"; 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 { isEmpty, isPresent } from "@ember/utils";
import { next, schedule } from "@ember/runloop"; import { next, schedule } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
@ -1599,19 +1602,30 @@ export default Controller.extend(bufferedProperty("model"), {
subscribe() { subscribe() {
this.unsubscribe(); this.unsubscribe();
this.messageBus.subscribe(
`/topic/${this.get("model.id")}`,
this.onMessage,
this.get("model.message_bus_last_id")
);
},
unsubscribe() {
// never unsubscribe when navigating from topic to topic
if (!this.get("model.id")) {
return;
}
this.messageBus.unsubscribe("/topic/*", this.onMessage);
},
@bind
onMessage(data) {
const topic = this.model;
const refresh = (args) => const refresh = (args) =>
this.appEvents.trigger("post-stream: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)) { if (isPresent(data.notification_level_change)) {
topic.set( topic.set("details.notification_level", data.notification_level_change);
"details.notification_level",
data.notification_level_change
);
topic.set( topic.set(
"details.notifications_reason_id", "details.notifications_reason_id",
data.notifications_reason_id data.notifications_reason_id
@ -1650,12 +1664,7 @@ export default Controller.extend(bufferedProperty("model"), {
case "liked": case "liked":
case "unliked": { case "unliked": {
postStream postStream
.triggerLikedPost( .triggerLikedPost(data.id, data.likes_count, data.user_id, data.type)
data.id,
data.likes_count,
data.user_id,
data.type
)
.then(() => refresh({ id: data.id, refreshLikes: true })); .then(() => refresh({ id: data.id, refreshLikes: true }));
break; break;
} }
@ -1695,9 +1704,7 @@ export default Controller.extend(bufferedProperty("model"), {
.triggerNewPostsInStream(postIds, { background: true }) .triggerNewPostsInStream(postIds, { background: true })
.then(() => refresh()) .then(() => refresh())
.catch((e) => { .catch((e) => {
this._newPostsInStream = postIds.concat( this._newPostsInStream = postIds.concat(this._newPostsInStream);
this._newPostsInStream
);
throw e; throw e;
}); });
}); });
@ -1717,15 +1724,13 @@ export default Controller.extend(bufferedProperty("model"), {
} }
case "stats": { case "stats": {
let updateStream = false; let updateStream = false;
["last_posted_at", "like_count", "posts_count"].forEach( ["last_posted_at", "like_count", "posts_count"].forEach((property) => {
(property) => {
const value = data[property]; const value = data[property];
if (typeof value !== "undefined") { if (typeof value !== "undefined") {
topic.set(property, value); topic.set(property, value);
updateStream = true; updateStream = true;
} }
} });
);
if (data["last_poster"]) { if (data["last_poster"]) {
topic.details.set("last_poster", data["last_poster"]); topic.details.set("last_poster", data["last_poster"]);
@ -1750,17 +1755,6 @@ export default Controller.extend(bufferedProperty("model"), {
} }
} }
}, },
this.get("model.message_bus_last_id")
);
},
unsubscribe() {
// never unsubscribe when navigating from topic to topic
if (!this.get("model.id")) {
return;
}
this.messageBus.unsubscribe("/topic/*");
},
reply() { reply() {
this.replyToPost(); this.replyToPost();

@ -1,4 +1,5 @@
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
import { bind } from "discourse-common/utils/decorators";
import PreloadStore from "discourse/lib/preload-store"; import PreloadStore from "discourse/lib/preload-store";
export default { export default {
@ -6,14 +7,21 @@ export default {
after: "message-bus", after: "message-bus",
initialize(container) { 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 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) => { teardown() {
site.set("banner", EmberObject.create(data || {})); this.messageBus.unsubscribe("/site/banner", this.onMessage);
}); },
@bind
onMessage(data = {}) {
this.site.set("banner", EmberObject.create(data));
}, },
}; };

@ -1,13 +1,14 @@
import DiscourseURL from "discourse/lib/url"; import DiscourseURL from "discourse/lib/url";
import { isDevelopment } from "discourse-common/config/environment"; import { isDevelopment } from "discourse-common/config/environment";
import discourseLater from "discourse-common/lib/later"; 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. // Use the message bus for live reloading of components for faster development.
export default { export default {
name: "live-development", name: "live-development",
initialize(container) { initialize(container) {
const messageBus = container.lookup("service:message-bus"); this.messageBus = container.lookup("service:message-bus");
const session = container.lookup("service:session"); const session = container.lookup("service:session");
// Preserve preview_theme_id=## and pp=async-flamegraph parameters across pages // Preserve preview_theme_id=## and pp=async-flamegraph parameters across pages
@ -38,36 +39,45 @@ export default {
} }
// Observe file changes // Observe file changes
messageBus.subscribe( this.messageBus.subscribe(
"/file-change", "/file-change",
(data) => { this.onFileChange,
session.mbLastFileChangeId
);
},
teardown() {
this.messageBus.unsubscribe("/file-change", this.onFileChange);
},
@bind
onFileChange(data) {
data.forEach((me) => { data.forEach((me) => {
if (me === "refresh") { if (me === "refresh") {
// Refresh if necessary // Refresh if necessary
document.location.reload(true); document.location.reload(true);
} else if (me.new_href && me.target) { } else if (me.new_href && me.target) {
const link_target = !!me.theme_id let query = `link[data-target='${me.target}']`;
? `[data-target='${me.target}'][data-theme-id='${me.theme_id}']`
: `[data-target='${me.target}']`; if (me.theme_id) {
query += `[data-theme-id='${me.theme_id}']`;
}
const links = document.querySelectorAll(query);
const links = document.querySelectorAll(`link${link_target}`);
if (links.length > 0) { if (links.length > 0) {
const lastLink = links[links.length - 1]; const lastLink = links[links.length - 1];
// this check is useful when message-bus has multiple file updates // this check is useful when message-bus has multiple file updates
// it avoids the browser doing a lot of work for nothing // it avoids the browser doing a lot of work for nothing
// should the filenames be unchanged // should the filenames be unchanged
if ( if (lastLink.href.split("/").pop() !== me.new_href.split("/").pop()) {
lastLink.href.split("/").pop() !== me.new_href.split("/").pop()
) {
this.refreshCSS(lastLink, me.new_href); this.refreshCSS(lastLink, me.new_href);
} }
} }
} }
}); });
}, },
session.mbLastFileChangeId
);
},
refreshCSS(node, newHref) { refreshCSS(node, newHref) {
const reloaded = node.cloneNode(true); const reloaded = node.cloneNode(true);

@ -1,5 +1,6 @@
import I18n from "I18n"; import I18n from "I18n";
import logout from "discourse/lib/logout"; import logout from "discourse/lib/logout";
import { bind } from "discourse-common/utils/decorators";
let _showingLogout = false; let _showingLogout = false;
@ -9,27 +10,30 @@ export default {
after: "message-bus", after: "message-bus",
initialize(container) { initialize(container) {
const messageBus = container.lookup("service:message-bus"), this.messageBus = container.lookup("service:message-bus");
dialog = container.lookup("service:dialog"); 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; return;
} }
messageBus.subscribe("/logout", function () {
if (!_showingLogout) {
_showingLogout = true; _showingLogout = true;
dialog.alert({ this.dialog.alert({
message: I18n.t("logout"), message: I18n.t("logout"),
confirmButtonLabel: "home", confirmButtonLabel: "home",
didConfirm: logout, didConfirm: logout,
didCancel: logout, didCancel: logout,
shouldDisplayCancel: false, shouldDisplayCancel: false,
}); });
}
_showingLogout = true;
});
}, },
}; };

@ -1,13 +1,23 @@
import { bind } from "discourse-common/utils/decorators";
// Subscribe to "read-only" status change events via the Message Bus // Subscribe to "read-only" status change events via the Message Bus
export default { export default {
name: "read-only", name: "read-only",
after: "message-bus", after: "message-bus",
initialize(container) { initialize(container) {
const messageBus = container.lookup("service:message-bus"); this.messageBus = container.lookup("service:message-bus");
const site = container.lookup("service:site"); this.site = container.lookup("service:site");
messageBus.subscribe("/site/read-only", (enabled) => {
site.set("isReadOnly", enabled); 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);
}, },
}; };

@ -12,41 +12,131 @@ import {
} from "discourse/lib/push-notifications"; } from "discourse/lib/push-notifications";
import { isTesting } from "discourse-common/config/environment"; import { isTesting } from "discourse-common/config/environment";
import Notification from "discourse/models/notification"; import Notification from "discourse/models/notification";
import { bind } from "discourse-common/utils/decorators";
export default { export default {
name: "subscribe-user-notifications", name: "subscribe-user-notifications",
after: "message-bus", after: "message-bus",
initialize(container) { initialize(container) {
const user = container.lookup("service:current-user"); this.currentUser = 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");
if (user) { if (!this.currentUser) {
const channel = user.redesigned_user_menu_enabled return;
? `/reviewable_counts/${user.id}` }
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");
this.reviewableCountsChannel = this.currentUser.redesigned_user_menu_enabled
? `/reviewable_counts/${this.currentUser.id}`
: "/reviewable_counts"; : "/reviewable_counts";
bus.subscribe(channel, (data) => { this.messageBus.subscribe(
this.reviewableCountsChannel,
this.onReviewableCounts
);
this.messageBus.subscribe(
`/notification/${this.currentUser.id}`,
this.onNotification,
this.currentUser.notification_channel_position
);
this.messageBus.subscribe(
`/user-drafts/${this.currentUser.id}`,
this.onUserDrafts
);
this.messageBus.subscribe(
`/do-not-disturb/${this.currentUser.id}`,
this.onDoNotDisturb
);
this.messageBus.subscribe(`/user-status`, this.onUserStatus);
this.messageBus.subscribe("/categories", this.onCategories);
this.messageBus.subscribe("/client_settings", this.onClientSettings);
if (!isTesting()) {
this.messageBus.subscribe(alertChannel(this.currentUser), this.onAlert);
initDesktopNotifications(this.messageBus, this.appEvents);
if (isPushNotificationsEnabled(this.currentUser)) {
disableDesktopNotifications();
registerPushNotifications(
this.currentUser,
this.router,
this.appEvents
);
} 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) { if (data.reviewable_count >= 0) {
user.updateReviewableCount(data.reviewable_count); this.currentUser.updateReviewableCount(data.reviewable_count);
} }
if (user.redesigned_user_menu_enabled) { if (this.currentUser.redesigned_user_menu_enabled) {
user.set("unseen_reviewable_count", data.unseen_reviewable_count); this.currentUser.set(
"unseen_reviewable_count",
data.unseen_reviewable_count
);
} }
}); },
bus.subscribe( @bind
`/notification/${user.id}`, onNotification(data) {
(data) => { const oldUnread = this.currentUser.unread_notifications;
const store = container.lookup("service:store"); const oldHighPriority = this.currentUser.unread_high_priority_notifications;
const oldUnread = user.unread_notifications; const oldAllUnread = this.currentUser.all_unread_notifications_count;
const oldHighPriority = user.unread_high_priority_notifications;
const oldAllUnread = user.all_unread_notifications_count;
user.setProperties({ this.currentUser.setProperties({
unread_notifications: data.unread_notifications, unread_notifications: data.unread_notifications,
unread_high_priority_notifications: unread_high_priority_notifications:
data.unread_high_priority_notifications, data.unread_high_priority_notifications,
@ -62,19 +152,19 @@ export default {
oldHighPriority !== data.unread_high_priority_notifications || oldHighPriority !== data.unread_high_priority_notifications ||
oldAllUnread !== data.all_unread_notifications_count oldAllUnread !== data.all_unread_notifications_count
) { ) {
appEvents.trigger("notifications:changed"); this.appEvents.trigger("notifications:changed");
if ( if (
site.mobileView && this.site.mobileView &&
(data.unread_notifications - oldUnread > 0 || (data.unread_notifications - oldUnread > 0 ||
data.unread_high_priority_notifications - oldHighPriority > 0 || data.unread_high_priority_notifications - oldHighPriority > 0 ||
data.all_unread_notifications_count - oldAllUnread > 0) data.all_unread_notifications_count - oldAllUnread > 0)
) { ) {
appEvents.trigger("header:update-topic", null, 5000); this.appEvents.trigger("header:update-topic", null, 5000);
} }
} }
const stale = store.findStale( const stale = this.store.findStale(
"notification", "notification",
{}, {},
{ cacheKey: "recent-notifications" } { cacheKey: "recent-notifications" }
@ -121,66 +211,55 @@ export default {
stale.results.set("content", newNotifications); stale.results.set("content", newNotifications);
} }
}, },
user.notification_channel_position
);
bus.subscribe(`/user-drafts/${user.id}`, (data) => { @bind
user.updateDraftProperties(data); onUserDrafts(data) {
}); this.currentUser.updateDraftProperties(data);
},
bus.subscribe(`/do-not-disturb/${user.get("id")}`, (data) => { @bind
user.updateDoNotDisturbStatus(data.ends_at); onDoNotDisturb(data) {
}); this.currentUser.updateDoNotDisturbStatus(data.ends_at);
},
bus.subscribe(`/user-status`, (data) => { @bind
appEvents.trigger("user-status:changed", data); onUserStatus(data) {
}); this.appEvents.trigger("user-status:changed", data);
},
const site = container.lookup("service:site"); @bind
const router = container.lookup("router:main"); onCategories(data) {
bus.subscribe("/categories", (data) => {
(data.categories || []).forEach((c) => { (data.categories || []).forEach((c) => {
const mutedCategoryIds = user.muted_category_ids?.concat( const mutedCategoryIds = this.currentUser.muted_category_ids?.concat(
user.indirectly_muted_category_ids this.currentUser.indirectly_muted_category_ids
); );
if ( if (
mutedCategoryIds && mutedCategoryIds &&
mutedCategoryIds.includes(c.parent_category_id) && mutedCategoryIds.includes(c.parent_category_id) &&
!mutedCategoryIds.includes(c.id) !mutedCategoryIds.includes(c.id)
) { ) {
user.set( this.currentUser.set(
"indirectly_muted_category_ids", "indirectly_muted_category_ids",
user.indirectly_muted_category_ids.concat(c.id) this.currentUser.indirectly_muted_category_ids.concat(c.id)
); );
} }
return site.updateCategory(c);
return this.site.updateCategory(c);
}); });
(data.deleted_categories || []).forEach((id) => (data.deleted_categories || []).forEach((id) =>
site.removeCategory(id) this.site.removeCategory(id)
); );
}); },
bus.subscribe( @bind
"/client_settings", onClientSettings(data) {
(data) => (siteSettings[data.name] = data.value) this.siteSettings[data.name] = data.value;
); },
if (!isTesting()) { @bind
bus.subscribe(alertChannel(user), (data) => onAlert(data) {
onNotification(data, siteSettings, user) return onNotification(data, this.siteSettings, this.currentUser);
);
initDesktopNotifications(bus, appEvents);
if (isPushNotificationsEnabled(user)) {
disableDesktopNotifications();
registerPushNotifications(user, router, appEvents);
} else {
unsubscribePushNotifications(user);
}
}
}
}, },
}; };

@ -1,29 +1,41 @@
import { bind } from "discourse-common/utils/decorators";
export default { export default {
name: "user-tips", name: "user-tips",
after: "message-bus", after: "message-bus",
initialize(container) { initialize(container) {
const currentUser = container.lookup("service:current-user"); this.currentUser = container.lookup("service:current-user");
if (!currentUser) { if (!this.currentUser) {
return; return;
} }
const messageBus = container.lookup("service:message-bus"); this.messageBus = container.lookup("service:message-bus");
const site = container.lookup("service:site"); this.site = container.lookup("service:site");
messageBus.subscribe("/user-tips", function (seenUserTips) { this.messageBus.subscribe("/user-tips", this.onMessage);
currentUser.set("seen_popups", seenUserTips); },
if (!currentUser.user_option) {
currentUser.set("user_option", {}); 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", {});
} }
currentUser.set("user_option.seen_popups", seenUserTips);
this.currentUser.set("user_option.seen_popups", seenUserTips);
(seenUserTips || []).forEach((userTipId) => { (seenUserTips || []).forEach((userTipId) => {
currentUser.hideUserTipForever( this.currentUser.hideUserTipForever(
Object.keys(site.user_tips).find( Object.keys(this.site.user_tips).find(
(id) => site.user_tips[id] === userTipId (id) => this.site.user_tips[id] === userTipId
) )
); );
}); });
});
}, },
}; };

@ -1,15 +1,24 @@
import { bind } from "discourse-common/utils/decorators";
export default { export default {
name: "welcome-topic-banner", name: "welcome-topic-banner",
after: "message-bus", after: "message-bus",
initialize(container) { 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) { if (this.site.show_welcome_topic_banner) {
const messageBus = container.lookup("service:message-bus"); this.messageBus.subscribe("/site/welcome-topic-banner", this.onMessage);
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);
},
}; };

@ -1,5 +1,5 @@
import EmberObject, { get } from "@ember/object"; 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 Category from "discourse/models/category";
import { deepEqual, deepMerge } from "discourse-common/lib/object"; import { deepEqual, deepMerge } from "discourse-common/lib/object";
import DiscourseURL from "discourse/lib/url"; import DiscourseURL from "discourse/lib/url";
@ -46,13 +46,33 @@ function hasMutedTags(topicTags, mutedTags, siteSettings) {
const TopicTrackingState = EmberObject.extend({ const TopicTrackingState = EmberObject.extend({
messageCount: 0, messageCount: 0,
@on("init") init() {
_setup() { this._super(...arguments);
this.states = new Map(); this.states = new Map();
this.stateChangeCallbacks = {}; this.stateChangeCallbacks = {};
this._trackedTopicLimit = 4000; 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 * Subscribe to MessageBus channels which are used for publishing changes
* to the tracking state. Each message received will modify state for * 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.messageBus.subscribe("/delete", this.onDeleteMessage);
this.messageBus.subscribe("/recover", this.onRecoverMessage);
this.messageBus.subscribe("/destroy", this.onDestroyMessage);
},
@bind
onDeleteMessage(msg) {
this.modifyStateProp(msg, "deleted", true); this.modifyStateProp(msg, "deleted", true);
this.incrementMessageCount(); this.incrementMessageCount();
}); },
this.messageBus.subscribe("/recover", (msg) => { @bind
onRecoverMessage(msg) {
this.modifyStateProp(msg, "deleted", false); this.modifyStateProp(msg, "deleted", false);
this.incrementMessageCount(); this.incrementMessageCount();
}); },
this.messageBus.subscribe("/destroy", (msg) => { @bind
onDestroyMessage(msg) {
this.incrementMessageCount(); this.incrementMessageCount();
const currentRoute = DiscourseURL.router.currentRoute.parent; const currentRoute = DiscourseURL.router.currentRoute.parent;
if ( if (
currentRoute.name === "topic" && currentRoute.name === "topic" &&
parseInt(currentRoute.params.id, 10) === msg.topic_id parseInt(currentRoute.params.id, 10) === msg.topic_id
) { ) {
DiscourseURL.redirectTo("/"); DiscourseURL.redirectTo("/");
} }
});
}, },
mutedTopics() { mutedTopics() {
@ -280,7 +308,7 @@ const TopicTrackingState = EmberObject.extend({
* @param {String} filter - Valid values are all, categories, and any topic list * @param {String} filter - Valid values are all, categories, and any topic list
* filters e.g. latest, unread, new. As well as this * filters e.g. latest, unread, new. As well as this
* specific category and tag URLs like tag/test/l/latest, * 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) { trackIncoming(filter) {
this.newIncoming = []; this.newIncoming = [];
@ -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. * that we are tracking, separated into new/unread/total.
* *
* Total is only counted if opts.includeTotal is specified. * Total is only counted if opts.includeTotal is specified.

@ -40,13 +40,13 @@ export default {
// to register a null value for anon // to register a null value for anon
app.register("service:current-user", currentUser, { instantiate: false }); app.register("service:current-user", currentUser, { instantiate: false });
const topicTrackingState = TopicTrackingState.create({ this.topicTrackingState = TopicTrackingState.create({
messageBus: container.lookup("service:message-bus"), messageBus: container.lookup("service:message-bus"),
siteSettings, siteSettings,
currentUser, currentUser,
}); });
app.register("service:topic-tracking-state", topicTrackingState, { app.register("service:topic-tracking-state", this.topicTrackingState, {
instantiate: false, 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();
}, },
}; };

@ -1,6 +1,7 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import { isPresent } from "@ember/utils"; import { isPresent } from "@ember/utils";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { bind } from "discourse-common/utils/decorators";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
model(params) { model(params) {
@ -51,7 +52,23 @@ export default DiscourseRoute.extend({
}, },
activate() { activate() {
this._updateClaimedBy = (data) => { this.messageBus.subscribe("/reviewable_claimed", this._updateClaimedBy);
this.messageBus.subscribe(
this._reviewableCountsChannel,
this._updateReviewables
);
},
deactivate() {
this.messageBus.unsubscribe("/reviewable_claimed", this._updateClaimedBy);
this.messageBus.unsubscribe(
this._reviewableCountsChannel,
this._updateReviewables
);
},
@bind
_updateClaimedBy(data) {
const reviewables = this.controller.reviewables; const reviewables = this.controller.reviewables;
if (reviewables) { if (reviewables) {
const user = data.user const user = data.user
@ -63,9 +80,10 @@ export default DiscourseRoute.extend({
} }
}); });
} }
}; },
this._updateReviewables = (data) => { @bind
_updateReviewables(data) {
if (data.updates) { if (data.updates) {
this.controller.reviewables.forEach((reviewable) => { this.controller.reviewables.forEach((reviewable) => {
const updates = data.updates[reviewable.id]; const updates = data.updates[reviewable.id];
@ -74,31 +92,16 @@ export default DiscourseRoute.extend({
} }
}); });
} }
};
this.messageBus.subscribe("/reviewable_claimed", this._updateClaimedBy);
this.messageBus.subscribe(
this._reviewableCountsChannel(),
this._updateReviewables
);
}, },
deactivate() { get _reviewableCountsChannel() {
this.messageBus.unsubscribe("/reviewable_claimed", this._updateClaimedBy); return this.currentUser.redesigned_user_menu_enabled
this.messageBus.unsubscribe( ? `/reviewable_counts/${this.currentUser.id}`
this._reviewableCountsChannel(), : "/reviewable_counts";
this._updateReviewables
);
}, },
@action @action
refreshRoute() { refreshRoute() {
this.refresh(); this.refresh();
}, },
_reviewableCountsChannel() {
return this.currentUser.redesigned_user_menu_enabled
? `/reviewable_counts/${this.currentUser.id}`
: "/reviewable_counts";
},
}); });

@ -2,25 +2,9 @@ import DiscourseRoute from "discourse/routes/discourse";
import I18n from "I18n"; import I18n from "I18n";
import User from "discourse/models/user"; import User from "discourse/models/user";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { bind } from "discourse-common/utils/decorators";
export default DiscourseRoute.extend({ 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() { beforeModel() {
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
this.replaceWith("discovery"); this.replaceWith("discovery");
@ -68,29 +52,64 @@ export default DiscourseRoute.extend({
this._super(...arguments); this._super(...arguments);
const user = this.modelFor("user"); const user = this.modelFor("user");
this.messageBus.subscribe(`/u/${user.username_lower}`, (data) => this.messageBus.subscribe(`/u/${user.username_lower}`, this.onUserMessage);
user.loadUserAction(data) 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() { deactivate() {
this._super(...arguments); this._super(...arguments);
const user = this.modelFor("user"); const user = this.modelFor("user");
this.messageBus.unsubscribe(`/u/${user.username_lower}`); this.messageBus.unsubscribe(
this.messageBus.unsubscribe(`/u/${user.username_lower}/counters`); `/u/${user.username_lower}`,
this.onUserMessage
);
this.messageBus.unsubscribe(
`/u/${user.username_lower}/counters`,
this.onUserCountersMessage
);
user.stopTrackingStatus(); user.stopTrackingStatus();
// Remove the search context // Remove the search context
this.searchService.set("searchContext", null); 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();
},
}); });

@ -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 Service from "@ember/service";
import I18n from "I18n"; import I18n from "I18n";
import { autoUpdatingRelativeAge } from "discourse/lib/formatter"; import { autoUpdatingRelativeAge } from "discourse/lib/formatter";
@ -29,9 +32,21 @@ export default Service.extend({
this.set("text", text); this.set("text", text);
} }
this.messageBus.subscribe("/logs_error_rate_exceeded", (data) => { this.messageBus.subscribe("/logs_error_rate_exceeded", this.onLogRateLimit);
const duration = data.duration; },
const rate = data.rate;
willDestroy() {
this._super(...arguments);
this.messageBus.unsubscribe(
"/logs_error_rate_exceeded",
this.onLogRateLimit
);
},
@bind
onLogRateLimit(data) {
const { duration, rate } = data;
let siteSettingLimit = 0; let siteSettingLimit = 0;
if (duration === "minute") { if (duration === "minute") {
@ -46,15 +61,12 @@ export default Service.extend({
this.set( this.set(
"text", "text",
I18n.messageFormat(`logs_error_rate_notice.${translationKey}`, { I18n.messageFormat(`logs_error_rate_notice.${translationKey}`, {
relativeAge: autoUpdatingRelativeAge( relativeAge: autoUpdatingRelativeAge(new Date(data.publish_at * 1000)),
new Date(data.publish_at * 1000)
),
rate, rate,
limit: siteSettingLimit, limit: siteSettingLimit,
url: getURL("/logs"), url: getURL("/logs"),
}) })
); );
});
}, },
@discourseComputed("text") @discourseComputed("text")

@ -1,8 +1,7 @@
import { Promise } from "rsvp"; import { Promise } from "rsvp";
import Service from "@ember/service"; import Service from "@ember/service";
import { ajax } from "discourse/lib/ajax"; 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 { popupAjaxError } from "discourse/lib/ajax-error";
import { deepEqual, deepMerge } from "discourse-common/lib/object"; import { deepEqual, deepMerge } from "discourse-common/lib/object";
import { import {
@ -21,8 +20,9 @@ const PrivateMessageTopicTrackingState = Service.extend({
filter: null, filter: null,
activeGroup: null, activeGroup: null,
@on("init") init() {
_setup() { this._super(...arguments);
this.states = new Map(); this.states = new Map();
this.statesModificationCounter = 0; this.statesModificationCounter = 0;
this.isTracking = false; this.isTracking = false;
@ -30,6 +30,16 @@ const PrivateMessageTopicTrackingState = Service.extend({
this.stateChangeCallbacks = new Map(); 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) { onStateChange(key, callback) {
this.stateChangeCallbacks.set(key, callback); this.stateChangeCallbacks.set(key, callback);
}, },
@ -43,14 +53,6 @@ const PrivateMessageTopicTrackingState = Service.extend({
return Promise.resolve(); return Promise.resolve();
} }
this._establishChannels();
return this._loadInitialState().finally(() => {
this.set("isTracking", true);
});
},
_establishChannels() {
this.messageBus.subscribe(this.userChannel(), this._processMessage); this.messageBus.subscribe(this.userChannel(), this._processMessage);
this.currentUser.groupsWithMessages?.forEach((group) => { this.currentUser.groupsWithMessages?.forEach((group) => {
@ -59,6 +61,10 @@ const PrivateMessageTopicTrackingState = Service.extend({
this._processMessage this._processMessage
); );
}); });
return this._loadInitialState().finally(() => {
this.set("isTracking", true);
});
}, },
lookupCount(type, opts = {}) { lookupCount(type, opts = {}) {

@ -5,7 +5,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
import { action } from "@ember/object"; import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed, { bind } from "discourse-common/utils/decorators";
export default Component.extend({ export default Component.extend({
channel: null, channel: null,
@ -56,7 +56,21 @@ export default Component.extend({
didInsertElement() { didInsertElement() {
this._super(...arguments); this._super(...arguments);
if (this.currentUser.admin) { if (this.currentUser.admin) {
this.messageBus.subscribe("/chat/channel-archive-status", (busData) => { this.messageBus.subscribe("/chat/channel-archive-status", this.onMessage);
}
},
willDestroyElement() {
this._super(...arguments);
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) { if (busData.chat_channel_id === this.channel.id) {
this.channel.setProperties({ this.channel.setProperties({
archive_failed: busData.archive_failed, archive_failed: busData.archive_failed,
@ -66,16 +80,5 @@ export default Component.extend({
total_messages: busData.total_messages, total_messages: busData.total_messages,
}); });
} }
});
}
},
willDestroyElement() {
this._super(...arguments);
this.messageBus.unsubscribe("/chat/channel-archive-status");
},
_getTopicUrl() {
return getURL(`/t/-/${this.channel.archive_topic_id}`);
}, },
}); });

@ -1496,23 +1496,26 @@ export default Component.extend({
}, },
_unsubscribeToUpdates(channelId) { _unsubscribeToUpdates(channelId) {
this.messageBus.unsubscribe(`/chat/${channelId}`); this.messageBus.unsubscribe(`/chat/${channelId}`, this.onMessage);
}, },
_subscribeToUpdates(channelId) { _subscribeToUpdates(channelId) {
this._unsubscribeToUpdates(channelId); this._unsubscribeToUpdates(channelId);
this.messageBus.subscribe( this.messageBus.subscribe(
`/chat/${channelId}`, `/chat/${channelId}`,
(busData) => { this.onMessage,
this.details.channel_message_bus_last_id
);
},
@bind
onMessage(busData) {
if (!this.details.can_load_more_future || busData.type !== "sent") { if (!this.details.can_load_more_future || busData.type !== "sent") {
this.handleMessage(busData); this.handleMessage(busData);
} else { } else {
this.set("hasNewMessages", true); this.set("hasNewMessages", true);
} }
}, },
this.details.channel_message_bus_last_id
);
},
@bind @bind
_forceBodyScroll() { _forceBodyScroll() {

@ -102,13 +102,11 @@ export default class ChatNotificationManager extends Service {
this.set("_countChatInDocTitle", true); this.set("_countChatInDocTitle", true);
if (!this._subscribedToChat) { if (!this._subscribedToChat) {
this.messageBus.subscribe(this._chatAlertChannel(), (data) => this.messageBus.subscribe(this._chatAlertChannel(), this.onMessage);
onNotification(data, this.siteSettings, this.currentUser)
);
} }
if (opts.only && this._subscribedToCore) { if (opts.only && this._subscribedToCore) {
this.messageBus.unsubscribe(this._coreAlertChannel()); this.messageBus.unsubscribe(this._coreAlertChannel(), this.onMessage);
this.set("_subscribedToCore", false); this.set("_subscribedToCore", false);
} }
} }
@ -118,17 +116,20 @@ export default class ChatNotificationManager extends Service {
this.set("_countChatInDocTitle", false); this.set("_countChatInDocTitle", false);
} }
if (!this._subscribedToCore) { if (!this._subscribedToCore) {
this.messageBus.subscribe(this._coreAlertChannel(), (data) => this.messageBus.subscribe(this._coreAlertChannel(), this.onMessage);
onNotification(data, this.siteSettings, this.currentUser)
);
} }
if (this.only && this._subscribedToChat) { if (this.only && this._subscribedToChat) {
this.messageBus.unsubscribe(this._chatAlertChannel()); this.messageBus.unsubscribe(this._chatAlertChannel(), this.onMessage);
this.set("_subscribedToChat", false); this.set("_subscribedToChat", false);
} }
} }
@bind
onMessage(data) {
return onNotification(data, this.siteSettings, this.currentUser);
}
_shouldRun() { _shouldRun() {
return this.chat.userCanChat && !isTesting(); return this.chat.userCanChat && !isTesting();
} }

@ -20,6 +20,7 @@ import EmberObject, { computed } from "@ember/object";
import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api"; import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
import userPresent from "discourse/lib/user-presence"; import userPresent from "discourse/lib/user-presence";
import { bind } from "discourse-common/utils/decorators";
export const LIST_VIEW = "list_view"; export const LIST_VIEW = "list_view";
export const CHAT_VIEW = "chat_view"; export const CHAT_VIEW = "chat_view";
@ -57,6 +58,8 @@ export default class Chat extends Service {
isNetworkUnreliable = false; isNetworkUnreliable = false;
@and("currentUser.has_chat_enabled", "siteSettings.chat_enabled") userCanChat; @and("currentUser.has_chat_enabled", "siteSettings.chat_enabled") userCanChat;
_fetchingChannels = null; _fetchingChannels = null;
_onNewMentionsCallbacks = new Map();
_onNewMessagesCallbacks = new Map();
@computed("currentUser.staff", "currentUser.groups.[]") @computed("currentUser.staff", "currentUser.groups.[]")
get userCanDirectMessage() { get userCanDirectMessage() {
@ -607,7 +610,132 @@ export default class Chat extends Service {
_subscribeToChannelMetadata() { _subscribeToChannelMetadata() {
this.messageBus.subscribe( this.messageBus.subscribe(
"/chat/channel-metadata", "/chat/channel-metadata",
(busData) => { this._onChannelMetadata,
this.messageBusLastIds.channel_metadata
);
}
_subscribeToChannelEdits() {
this.messageBus.subscribe(
"/chat/channel-edits",
this._onChannelEdits,
this.messageBusLastIds.channel_edits
);
}
_subscribeToChannelStatusChange() {
this.messageBus.subscribe("/chat/channel-status", this._onChannelStatus);
}
_unsubscribeFromChannelStatusChange() {
this.messageBus.unsubscribe("/chat/channel-status", this._onChannelStatus);
}
_unsubscribeFromChannelEdits() {
this.messageBus.unsubscribe("/chat/channel-edits", this._onChannelEdits);
}
_unsubscribeFromChannelMetadata() {
this.messageBus.unsubscribe(
"/chat/channel-metadata",
this._onChannelMetadata
);
}
_subscribeToNewChannelUpdates() {
this.messageBus.subscribe(
"/chat/new-channel",
this._onNewChannel,
this.messageBusLastIds.new_channel
);
}
_unsubscribeFromNewDmChannelUpdates() {
this.messageBus.unsubscribe("/chat/new-channel", this._onNewChannel);
}
_subscribeToSingleUpdateChannel(channel) {
if (channel.current_user_membership.muted) {
return;
}
// We do this first so we don't multi-subscribe to mention + messages
// messageBus channels for this chat channel, since _subscribeToSingleUpdateChannel
// is called from multiple places.
this._unsubscribeFromChatChannel(channel);
if (!channel.isDirectMessageChannel) {
this._subscribeToMentionChannel(channel);
}
this._subscribeToNewMessagesChannel(channel);
}
_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`,
onNewMentions,
channel.message_bus_last_ids.new_mentions
);
}
_subscribeToNewMessagesChannel(channel) {
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
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);
}
}
}
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) => { this.getChannelBy("id", busData.chat_channel_id).then((channel) => {
if (channel) { if (channel) {
channel.setProperties({ channel.setProperties({
@ -616,15 +744,10 @@ export default class Chat extends Service {
this.appEvents.trigger("chat:refresh-channel-members"); this.appEvents.trigger("chat:refresh-channel-members");
} }
}); });
},
this.messageBusLastIds.channel_metadata
);
} }
_subscribeToChannelEdits() { @bind
this.messageBus.subscribe( _onChannelEdits(busData) {
"/chat/channel-edits",
(busData) => {
this.getChannelBy("id", busData.chat_channel_id).then((channel) => { this.getChannelBy("id", busData.chat_channel_id).then((channel) => {
if (channel) { if (channel) {
channel.setProperties({ channel.setProperties({
@ -633,13 +756,10 @@ export default class Chat extends Service {
}); });
} }
}); });
},
this.messageBusLastIds.channel_edits
);
} }
_subscribeToChannelStatusChange() { @bind
this.messageBus.subscribe("/chat/channel-status", (busData) => { _onChannelStatus(busData) {
this.getChannelBy("id", busData.chat_channel_id).then((channel) => { this.getChannelBy("id", busData.chat_channel_id).then((channel) => {
if (!channel) { if (!channel) {
return; return;
@ -662,104 +782,11 @@ export default class Chat extends Service {
this.appEvents.trigger("chat:refresh-channel", channel.id); this.appEvents.trigger("chat:refresh-channel", channel.id);
}, this.messageBusLastIds.channel_status); }, this.messageBusLastIds.channel_status);
});
} }
_unsubscribeFromChannelStatusChange() { @bind
this.messageBus.unsubscribe("/chat/channel-status"); _onNewChannel(busData) {
}
_unsubscribeFromChannelEdits() {
this.messageBus.unsubscribe("/chat/channel-edits");
}
_unsubscribeFromChannelMetadata() {
this.messageBus.unsubscribe("/chat/channel-metadata");
}
_subscribeToNewChannelUpdates() {
this.messageBus.subscribe(
"/chat/new-channel",
(busData) => {
this.startTrackingChannel(ChatChannel.create(busData.chat_channel)); this.startTrackingChannel(ChatChannel.create(busData.chat_channel));
},
this.messageBusLastIds.new_channel
);
}
_unsubscribeFromNewDmChannelUpdates() {
this.messageBus.unsubscribe("/chat/new-channel");
}
_subscribeToSingleUpdateChannel(channel) {
if (channel.current_user_membership.muted) {
return;
}
// We do this first so we don't multi-subscribe to mention + messages
// messageBus channels for this chat channel, since _subscribeToSingleUpdateChannel
// is called from multiple places.
this._unsubscribeFromChatChannel(channel);
if (!channel.isDirectMessageChannel) {
this._subscribeToMentionChannel(channel);
}
this._subscribeToNewMessagesChannel(channel);
}
_subscribeToMentionChannel(channel) {
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();
}
},
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];
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 {
// 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();
}
},
channel.message_bus_last_ids.new_messages
);
} }
async followChannel(channel) { async followChannel(channel) {
@ -787,16 +814,29 @@ export default class Chat extends Service {
} }
_unsubscribeFromChatChannel(channel) { _unsubscribeFromChatChannel(channel) {
this.messageBus.unsubscribe(`/chat/${channel.id}/new-messages`); this.messageBus.unsubscribe("/chat/*", this._onNewMessagesCallbacks);
if (!channel.isDirectMessageChannel) { if (!channel.isDirectMessageChannel) {
this.messageBus.unsubscribe(`/chat/${channel.id}/new-mentions`); this.messageBus.unsubscribe("/chat/*", this._onNewMentionsCallbacks);
} }
} }
_subscribeToUserTrackingChannel() { _subscribeToUserTrackingChannel() {
this.messageBus.subscribe( this.messageBus.subscribe(
`/chat/user-tracking-state/${this.currentUser.id}`, `/chat/user-tracking-state/${this.currentUser.id}`,
(busData, _, messageId) => { this._onUserTrackingState,
this.messageBusLastIds.user_tracking_state
);
}
_unsubscribeFromUserTrackingChannel() {
this.messageBus.unsubscribe(
`/chat/user-tracking-state/${this.currentUser.id}`,
this._onUserTrackingState
);
}
@bind
_onUserTrackingState(busData, _, messageId) {
const lastId = this.lastUserTrackingMessageId; const lastId = this.lastUserTrackingMessageId;
// we don't want this state to go backwards, only catch // we don't want this state to go backwards, only catch
@ -813,21 +853,13 @@ export default class Chat extends Service {
const trackingState = const trackingState =
this.currentUser.chat_channel_tracking_state[busData.chat_channel_id]; this.currentUser.chat_channel_tracking_state[busData.chat_channel_id];
if (trackingState) { if (trackingState) {
trackingState.set("chat_message_id", busData.chat_message_id); trackingState.set("chat_message_id", busData.chat_message_id);
trackingState.set("unread_count", 0); trackingState.set("unread_count", 0);
trackingState.set("unread_mentions", 0); trackingState.set("unread_mentions", 0);
this.userChatChannelTrackingStateChanged(); this.userChatChannelTrackingStateChanged();
} }
},
this.messageBusLastIds.user_tracking_state
);
}
_unsubscribeFromUserTrackingChannel() {
this.messageBus.unsubscribe(
`/chat/user-tracking-state/${this.currentUser.id}`
);
} }
resetTrackingStateForChannel(channelId) { resetTrackingStateForChannel(channelId) {

@ -1,43 +1,61 @@
import { withPluginApi } from "discourse/lib/plugin-api"; import { withPluginApi } from "discourse/lib/plugin-api";
import { bind } from "discourse-common/utils/decorators";
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");
});
}
}
export default { export default {
name: "new-user-narrative", name: "new-user-narrative",
initialize(container) { initialize(container) {
const siteSettings = container.lookup("service:site-settings"); 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");
}, },
}; };

@ -1,7 +1,7 @@
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
import WidgetGlue from "discourse/widgets/glue"; import WidgetGlue from "discourse/widgets/glue";
import { getRegister } from "discourse-common/lib/get-owner"; 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"; import { withPluginApi } from "discourse/lib/plugin-api";
const PLUGIN_ID = "discourse-poll"; const PLUGIN_ID = "discourse-poll";
@ -34,16 +34,19 @@ function initializePolls(api) {
subscribe() { subscribe() {
this._super(...arguments); this._super(...arguments);
this.messageBus.subscribe(`/polls/${this.model.id}`, (msg) => { this.messageBus.subscribe(`/polls/${this.model.id}`, this._onPollMessage);
const post = this.get("model.postStream").findLoadedPost(msg.post_id);
post?.set("polls", msg.polls);
});
}, },
unsubscribe() { unsubscribe() {
this.messageBus.unsubscribe("/polls/*"); this.messageBus.unsubscribe("/polls/*", this._onPollMessage);
this._super(...arguments); 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", { api.modifyClass("model:post", {