DEV: Migrate discourse core to Ember initializers (#22095)

https://meta.discourse.org/t/updating-our-initializer-naming-patterns/241919

For historical reasons, Discourse has different initializers conventions than standard Ember:

```
| Ember                 | Discourse          |                        |
| initializers          | pre-initializers   | runs once per app load |
| instance-initializers | (api-)initializers | runs once per app boot |
```

In addition, the arguments to the initialize function is different – Ember initializers get either the `Application` or `ApplicationInstance` as the only argument, but the "Discourse style" gets an extra container argument preceding that.

This is confusing, but it also causes problems with Ember addons, which expects the standard naming and argument conventions:

1. Typically, V1 addons will define their (app, instance) initializers in the `addon/(instance-)initializers/*`, which appears as `ember-some-addon-package-name/(instance-)initializers/*` in the require registry.

2. Just having those modules defined isn't supposed to do anything, so typically they also re-export them in `app/(instance-)initializers/*`, which gets merged into `discourse/(instance-)initializers/*` in the require registry.

3. The `ember-cli-load-initializers` package supplies a function called `loadInitializers`, which typically gets called in `app.js` to load the initializers according to the conventions above. Since we don't follow the same conventions, we can't use this function and instead have custom code in `app.js`, loosely based on official version but attempts to account for the different conventions.

The custom code that loads initializers is written with Discourse core and plug-ins/themes in mind, but does not take into account the fact that addons can also bring initializers, which causes the following problems:

* It does not check for the `discourse/` module prefix, so initializers in the `addon/` folders (point 1 above) get picked up as well. This means the initializer code is probably registered twice (once from the `addon/` folder, once from the `app/` re-export). This either causes a dev mode assertion (if they have the same name) or causes the code to run twice (if they have different names somehow).

* In modern Ember blueprints, it is customary to omit the `"name"` of the initializer since `ember-cli-load-initializers` can infer it from the module name. Our custom code does not do this and causes a dev mode assertion instead.

* It runs what then addon intends to be application initializers as instance initializers due to the naming difference. There is at least one known case of this where the `ember-export-application-global` application initialize is currently incorrectly registered as an instance initializer. (It happens to not use the `/addon` folder convention and explicitly names the initializer, so it does not trigger the previous error scenarios.)

* It runs the initializers with the wrong arguments. If all the addon initializer does is lookup stuff from the container, it happens to work, otherwise... ???

* It does not check for the `/instance-initializers/` module path so any instance initializers introduced by addons are silently ignored.

These issues were discovered when trying to install an addon that brings an application initializer in #22023.

To resolve these issues, this commit:

* Migrates Discourse core to use the standard Ember conventions – both in the naming and the arguments of the initialize function

* Updates the custom code for loading initializers:
  * For Discourse core, it essentially does the same thing as `ember-cli-load-initializers`
  * For plugins and themes, it preserves the existing Discourse conventions and semantics (to be revisited at a later time)

This ensures that going forward, Ember addons will function correctly.
This commit is contained in:
Godfrey Chan
2023-06-15 05:17:43 -07:00
committed by GitHub
parent 79a260a6bb
commit fa509224f0
66 changed files with 449 additions and 460 deletions

View File

@ -1,5 +1,6 @@
import "./global-compat"; import "./global-compat";
import require from "require";
import Application from "@ember/application"; import Application from "@ember/application";
import { buildResolver } from "discourse-common/resolver"; import { buildResolver } from "discourse-common/resolver";
import { isTesting } from "discourse-common/config/environment"; import { isTesting } from "discourse-common/config/environment";
@ -19,40 +20,6 @@ const Discourse = Application.extend({
Resolver: buildResolver("discourse"), Resolver: buildResolver("discourse"),
_prepareInitializer(moduleName) {
const themeId = moduleThemeId(moduleName);
let module = null;
try {
module = requirejs(moduleName, null, null, true);
if (!module) {
throw new Error(moduleName + " must export an initializer.");
}
} catch (error) {
if (!themeId || isTesting()) {
throw error;
}
fireThemeErrorEvent({ themeId, error });
return;
}
const init = module.default;
const oldInitialize = init.initialize;
init.initialize = (app) => {
try {
return oldInitialize.call(init, app.__container__, app);
} catch (error) {
if (!themeId || isTesting()) {
throw error;
}
fireThemeErrorEvent({ themeId, error });
}
};
return init;
},
// Start up the Discourse application by running all the initializers we've defined. // Start up the Discourse application by running all the initializers we've defined.
start() { start() {
document.querySelector("noscript")?.remove(); document.querySelector("noscript")?.remove();
@ -66,30 +33,7 @@ const Discourse = Application.extend({
Error.stackTraceLimit = Infinity; Error.stackTraceLimit = Infinity;
} }
Object.keys(requirejs._eak_seen).forEach((key) => { loadInitializers(this);
if (/\/pre\-initializers\//.test(key)) {
const initializer = this._prepareInitializer(key);
if (initializer) {
this.initializer(initializer);
}
} else if (/\/(api\-)?initializers\//.test(key)) {
const initializer = this._prepareInitializer(key);
if (initializer) {
this.instanceInitializer(initializer);
}
}
});
// Plugins that are registered via `<script>` tags.
const withPluginApi = requirejs("discourse/lib/plugin-api").withPluginApi;
let initCount = 0;
_pluginCallbacks.forEach((cb) => {
this.instanceInitializer({
name: `_discourse_plugin_${++initCount}`,
after: "inject-objects",
initialize: () => withPluginApi(cb.version, cb.code),
});
});
}, },
_registerPluginCode(version, code) { _registerPluginCode(version, code) {
@ -129,4 +73,136 @@ export function getAndClearUnhandledThemeErrors() {
return copy; return copy;
} }
/**
* Logic for loading initializers. Similar to ember-cli-load-initializers, but
* has some discourse-specific logic to handle loading initializers from
* plugins and themes.
*/
function loadInitializers(app) {
let initializers = [];
let instanceInitializers = [];
let discourseInitializers = [];
let discourseInstanceInitializers = [];
for (let moduleName of Object.keys(requirejs._eak_seen)) {
if (moduleName.startsWith("discourse/") && !moduleName.endsWith("-test")) {
// In discourse core, initializers follow standard Ember conventions
if (moduleName.startsWith("discourse/initializers/")) {
initializers.push(moduleName);
} else if (moduleName.startsWith("discourse/instance-initializers/")) {
instanceInitializers.push(moduleName);
} else {
// https://meta.discourse.org/t/updating-our-initializer-naming-patterns/241919
//
// For historical reasons, the naming conventions in plugins and themes
// differs from Ember:
//
// | Ember | Discourse | |
// | initializers | pre-initializers | runs once per app load |
// | instance-initializers | (api-)initializers | runs once per app boot |
//
// In addition, the arguments to the initialize function is different –
// Ember initializers get either the `Application` or `ApplicationInstance`
// as the only argument, but the "discourse style" gets an extra container
// argument preceding that.
const themeId = moduleThemeId(moduleName);
if (
themeId !== undefined ||
moduleName.startsWith("discourse/plugins/")
) {
if (moduleName.includes("/pre-initializers/")) {
discourseInitializers.push([moduleName, themeId]);
} else if (
moduleName.includes("/initializers/") ||
moduleName.includes("/api-initializers/")
) {
discourseInstanceInitializers.push([moduleName, themeId]);
}
}
}
}
}
for (let moduleName of initializers) {
app.initializer(resolveInitializer(moduleName));
}
for (let moduleName of instanceInitializers) {
app.instanceInitializer(resolveInitializer(moduleName));
}
for (let [moduleName, themeId] of discourseInitializers) {
app.initializer(resolveDiscourseInitializer(moduleName, themeId));
}
for (let [moduleName, themeId] of discourseInstanceInitializers) {
app.instanceInitializer(resolveDiscourseInitializer(moduleName, themeId));
}
// Plugins that are registered via `<script>` tags.
const { withPluginApi } = require("discourse/lib/plugin-api");
for (let [i, callback] of _pluginCallbacks.entries()) {
app.instanceInitializer({
name: `_discourse_plugin_${i}`,
after: "inject-objects",
initialize: () => withPluginApi(callback.version, callback.code),
});
}
}
function resolveInitializer(moduleName) {
const module = require(moduleName, null, null, true);
if (!module) {
throw new Error(moduleName + " must export an initializer.");
}
const initializer = module["default"];
if (!initializer) {
throw new Error(moduleName + " must have a default export");
}
if (!initializer.name) {
initializer.name = moduleName.slice(moduleName.lastIndexOf("/") + 1);
}
return initializer;
}
function resolveDiscourseInitializer(moduleName, themeId) {
let initializer;
try {
initializer = resolveInitializer(moduleName);
} catch (error) {
if (!themeId || isTesting()) {
throw error;
} else {
fireThemeErrorEvent({ themeId, error });
return;
}
}
const oldInitialize = initializer.initialize;
initializer.initialize = (app) => {
try {
return oldInitialize.call(initializer, app.__container__, app);
} catch (error) {
if (!themeId || isTesting()) {
throw error;
} else {
fireThemeErrorEvent({ themeId, error });
}
}
};
return initializer;
}
export default Discourse; export default Discourse;

View File

@ -16,16 +16,14 @@ import runloop from "@ember/runloop";
import { DEBUG } from "@glimmer/env"; import { DEBUG } from "@glimmer/env";
export default { export default {
name: "discourse-bootstrap",
// The very first initializer to run // The very first initializer to run
initialize(container) { initialize(app) {
if (DEBUG) { if (DEBUG) {
runloop._backburner.ASYNC_STACKS = true; runloop._backburner.ASYNC_STACKS = true;
} }
setURLContainer(container); setURLContainer(app.__container__);
setDefaultOwner(container); setDefaultOwner(app.__container__);
// Our test environment has its own bootstrap code // Our test environment has its own bootstrap code
if (isTesting()) { if (isTesting()) {

View File

@ -8,9 +8,8 @@ import { dasherize } from "@ember/string";
export default { export default {
after: "inject-discourse-objects", after: "inject-discourse-objects",
name: "dynamic-route-builders",
initialize(_container, app) { initialize(app) {
app.register( app.register(
"controller:discovery.category", "controller:discovery.category",
DiscoverySortableController.extend() DiscoverySortableController.extend()

View File

@ -9,11 +9,10 @@ import User from "discourse/models/user";
import { registerDiscourseImplicitInjections } from "discourse/lib/implicit-injections"; import { registerDiscourseImplicitInjections } from "discourse/lib/implicit-injections";
export default { export default {
name: "inject-discourse-objects",
after: "discourse-bootstrap", after: "discourse-bootstrap",
initialize(container, app) { initialize(app) {
const siteSettings = container.lookup("service:site-settings"); const siteSettings = app.__container__.lookup("service:site-settings");
const currentUser = User.current(); const currentUser = User.current();
@ -22,7 +21,7 @@ export default {
app.register("service:current-user", currentUser, { instantiate: false }); app.register("service:current-user", currentUser, { instantiate: false });
this.topicTrackingState = TopicTrackingState.create({ this.topicTrackingState = TopicTrackingState.create({
messageBus: container.lookup("service:message-bus"), messageBus: app.__container__.lookup("service:message-bus"),
siteSettings, siteSettings,
currentUser, currentUser,
}); });

View File

@ -1,10 +1,9 @@
import { mapRoutes } from "discourse/mapping-router"; import { mapRoutes } from "discourse/mapping-router";
export default { export default {
name: "map-routes",
after: "inject-discourse-objects", after: "inject-discourse-objects",
initialize(_, app) { initialize(app) {
this.routerClass = mapRoutes(); this.routerClass = mapRoutes();
app.register("router:main", this.routerClass); app.register("router:main", this.routerClass);
}, },

View File

@ -1,10 +0,0 @@
import { registerServiceWorker } from "discourse/lib/register-service-worker";
export default {
name: "register-service-worker",
initialize(container) {
let { serviceWorkerURL } = container.lookup("service:session");
registerServiceWorker(container, serviceWorkerURL);
},
};

View File

@ -31,8 +31,6 @@ function animatedImgs() {
} }
export default { export default {
name: "animated-images-pause-on-click",
initialize() { initialize() {
withPluginApi("0.8.7", (api) => { withPluginApi("0.8.7", (api) => {
function _cleanUp() { function _cleanUp() {

View File

@ -1,8 +1,8 @@
import { next } from "@ember/runloop"; import { next } from "@ember/runloop";
export default { export default {
name: "auth-complete",
after: "inject-objects", after: "inject-objects",
initialize(container) { initialize(owner) {
let lastAuthResult; let lastAuthResult;
if (document.getElementById("data-authentication")) { if (document.getElementById("data-authentication")) {
@ -12,14 +12,14 @@ export default {
} }
if (lastAuthResult) { if (lastAuthResult) {
const router = container.lookup("router:main"); const router = owner.lookup("router:main");
router.one("didTransition", () => { router.one("didTransition", () => {
const controllerName = const controllerName =
router.currentPath === "invites.show" ? "invites-show" : "login"; router.currentPath === "invites.show" ? "invites-show" : "login";
next(() => { next(() => {
let controller = container.lookup(`controller:${controllerName}`); let controller = owner.lookup(`controller:${controllerName}`);
controller.authenticationComplete(JSON.parse(lastAuthResult)); controller.authenticationComplete(JSON.parse(lastAuthResult));
}); });
}); });

View File

@ -7,7 +7,7 @@ import RawHandlebars from "discourse-common/lib/raw-handlebars";
import { registerRawHelpers } from "discourse-common/lib/raw-handlebars-helpers"; import { registerRawHelpers } from "discourse-common/lib/raw-handlebars-helpers";
import { setOwner } from "@ember/application"; import { setOwner } from "@ember/application";
export function autoLoadModules(container, registry) { export function autoLoadModules(owner, registry) {
Object.keys(requirejs.entries).forEach((entry) => { Object.keys(requirejs.entries).forEach((entry) => {
if (/\/helpers\//.test(entry) && !/-test/.test(entry)) { if (/\/helpers\//.test(entry) && !/-test/.test(entry)) {
requirejs(entry, null, null, true); requirejs(entry, null, null, true);
@ -18,16 +18,16 @@ export function autoLoadModules(container, registry) {
}); });
let context = { let context = {
siteSettings: container.lookup("service:site-settings"), siteSettings: owner.lookup("service:site-settings"),
keyValueStore: container.lookup("service:key-value-store"), keyValueStore: owner.lookup("service:key-value-store"),
capabilities: container.lookup("service:capabilities"), capabilities: owner.lookup("service:capabilities"),
currentUser: container.lookup("service:current-user"), currentUser: owner.lookup("service:current-user"),
site: container.lookup("service:site"), site: owner.lookup("service:site"),
session: container.lookup("service:session"), session: owner.lookup("service:session"),
topicTrackingState: container.lookup("service:topic-tracking-state"), topicTrackingState: owner.lookup("service:topic-tracking-state"),
registry, registry,
}; };
setOwner(context, container); setOwner(context, owner);
createHelperContext(context); createHelperContext(context);
registerHelpers(registry); registerHelpers(registry);
@ -35,7 +35,8 @@ export function autoLoadModules(container, registry) {
} }
export default { export default {
name: "auto-load-modules",
after: "inject-objects", after: "inject-objects",
initialize: (container) => autoLoadModules(container, container.registry), initialize: (owner) => {
autoLoadModules(owner, owner.__container__.registry);
},
}; };

View File

@ -1,19 +1,18 @@
// Updates the PWA badging if available // Updates the PWA badging if available
export default { export default {
name: "badging",
after: "message-bus", after: "message-bus",
initialize(container) { initialize(owner) {
if (!navigator.setAppBadge) { if (!navigator.setAppBadge) {
return; return;
} // must have the Badging API } // must have the Badging API
const user = container.lookup("service:current-user"); const user = owner.lookup("service:current-user");
if (!user) { if (!user) {
return; return;
} // must be logged in } // must be logged in
const appEvents = container.lookup("service:app-events"); const appEvents = owner.lookup("service:app-events");
appEvents.on("notifications:changed", () => { appEvents.on("notifications:changed", () => {
let notifications; let notifications;
notifications = user.all_unread_notifications_count; notifications = user.all_unread_notifications_count;

View File

@ -3,12 +3,11 @@ 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 {
name: "banner",
after: "message-bus", after: "message-bus",
initialize(container) { initialize(owner) {
this.site = container.lookup("service:site"); this.site = owner.lookup("service:site");
this.messageBus = container.lookup("service:message-bus"); this.messageBus = owner.lookup("service:message-bus");
const banner = EmberObject.create(PreloadStore.get("banner") || {}); const banner = EmberObject.create(PreloadStore.get("banner") || {});
this.site.set("banner", banner); this.site.set("banner", banner);

View File

@ -1,5 +1,4 @@
export default { export default {
name: "category-color-css-generator",
after: "register-hashtag-types", after: "register-hashtag-types",
/** /**
@ -9,8 +8,8 @@ export default {
* It is also used when styling hashtag icons, since they are colored * It is also used when styling hashtag icons, since they are colored
* based on the category color. * based on the category color.
*/ */
initialize(container) { initialize(owner) {
this.site = container.lookup("service:site"); this.site = owner.lookup("service:site");
// If the site is login_required and the user is anon there will be no categories preloaded. // If the site is login_required and the user is anon there will be no categories preloaded.
if (!this.site.categories) { if (!this.site.categories) {

View File

@ -33,18 +33,17 @@ function _clean(transition) {
} }
export default { export default {
name: "clean-dom-on-route-change",
after: "inject-objects", after: "inject-objects",
initialize(container) { initialize(owner) {
const router = container.lookup("router:main"); const router = owner.lookup("router:main");
router.on("routeDidChange", (transition) => { router.on("routeDidChange", (transition) => {
if (transition.isAborted) { if (transition.isAborted) {
return; return;
} }
scheduleOnce("afterRender", container, _clean, transition); scheduleOnce("afterRender", owner, _clean, transition);
}); });
}, },
}; };

View File

@ -2,9 +2,8 @@ import DiscourseURL from "discourse/lib/url";
import interceptClick from "discourse/lib/intercept-click"; import interceptClick from "discourse/lib/intercept-click";
export default { export default {
name: "click-interceptor", initialize(owner) {
initialize(container, app) { this.selector = owner.rootElement;
this.selector = app.rootElement;
$(this.selector).on("click.discourse", "a", interceptClick); $(this.selector).on("click.discourse", "a", interceptClick);
window.addEventListener("hashchange", this.hashChanged); window.addEventListener("hashchange", this.hashChanged);
}, },

View File

@ -5,10 +5,8 @@ import CodeblockButtons from "discourse/lib/codeblock-buttons";
let _codeblockButtons = []; let _codeblockButtons = [];
export default { export default {
name: "codeblock-buttons", initialize(owner) {
const siteSettings = owner.lookup("service:site-settings");
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
withPluginApi("0.8.7", (api) => { withPluginApi("0.8.7", (api) => {
function _cleanUp() { function _cleanUp() {

View File

@ -14,11 +14,10 @@ GlimmerManager.getComponentTemplate = (component) => {
}; };
export default { export default {
name: "colocated-template-overrides",
after: ["populate-template-map", "mobile"], after: ["populate-template-map", "mobile"],
initialize(container) { initialize(owner) {
this.site = container.lookup("service:site"); this.site = owner.lookup("service:site");
this.eachThemePluginTemplate((templateKey, moduleNames, mobile) => { this.eachThemePluginTemplate((templateKey, moduleNames, mobile) => {
if (!mobile && DiscourseTemplateMap.coreTemplates.has(templateKey)) { if (!mobile && DiscourseTemplateMap.coreTemplates.has(templateKey)) {
@ -32,9 +31,7 @@ export default {
} }
componentName = componentName.slice("components/".length); componentName = componentName.slice("components/".length);
const component = container.owner.resolveRegistration( const component = owner.resolveRegistration(`component:${componentName}`);
`component:${componentName}`
);
if (component && originalGetTemplate(component)) { if (component && originalGetTemplate(component)) {
const finalOverrideModuleName = moduleNames[moduleNames.length - 1]; const finalOverrideModuleName = moduleNames[moduleNames.length - 1];

View File

@ -4,10 +4,9 @@ let installed = false;
let callbacks = $.Callbacks(); let callbacks = $.Callbacks();
export default { export default {
name: "csrf-token", initialize(owner) {
initialize(container) {
// Add a CSRF token to all AJAX requests // Add a CSRF token to all AJAX requests
let session = container.lookup("service:session"); let session = owner.lookup("service:session");
session.set( session.set(
"csrfToken", "csrfToken",
document.head.querySelector("meta[name=csrf-token]")?.content document.head.querySelector("meta[name=csrf-token]")?.content

View File

@ -1,8 +1,6 @@
import { showPopover } from "discourse/lib/d-popover"; import { showPopover } from "discourse/lib/d-popover";
export default { export default {
name: "d-popover",
initialize() { initialize() {
["click", "mouseover"].forEach((eventType) => { ["click", "mouseover"].forEach((eventType) => {
document.addEventListener(eventType, (e) => { document.addEventListener(eventType, (e) => {

View File

@ -1,8 +1,6 @@
import { eagerLoadRawTemplateModules } from "discourse-common/lib/raw-templates"; import { eagerLoadRawTemplateModules } from "discourse-common/lib/raw-templates";
export default { export default {
name: "eager-load-raw-templates",
initialize() { initialize() {
eagerLoadRawTemplateModules(); eagerLoadRawTemplateModules();
}, },

View File

@ -3,10 +3,8 @@ import { registerEmoji } from "pretty-text/emoji";
import { withPluginApi } from "discourse/lib/plugin-api"; import { withPluginApi } from "discourse/lib/plugin-api";
export default { export default {
name: "enable-emoji", initialize(owner) {
const siteSettings = owner.lookup("service:site-settings");
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
if (!siteSettings.enable_emoji) { if (!siteSettings.enable_emoji) {
return; return;
} }

View File

@ -7,8 +7,6 @@ import { isTesting } from "discourse-common/config/environment";
const DELAY = isTesting() ? 0 : 5000; const DELAY = isTesting() ? 0 : 5000;
export default { export default {
name: "handle-cookies",
initialize() { initialize() {
// No need to block boot for this housekeeping - we can defer it a few seconds // No need to block boot for this housekeeping - we can defer it a few seconds
later(() => { later(() => {

View File

@ -1,7 +1,6 @@
import { getHashtagTypeClasses } from "discourse/lib/hashtag-autocomplete"; import { getHashtagTypeClasses } from "discourse/lib/hashtag-autocomplete";
export default { export default {
name: "hashtag-css-generator",
after: "category-color-css-generator", after: "category-color-css-generator",
/** /**
@ -13,8 +12,8 @@ export default {
* with the hastag type via api.registerHashtagType. The default * with the hastag type via api.registerHashtagType. The default
* ones in core are CategoryHashtagType and TagHashtagType. * ones in core are CategoryHashtagType and TagHashtagType.
*/ */
initialize(container) { initialize(owner) {
this.site = container.lookup("service:site"); this.site = owner.lookup("service:site");
// If the site is login_required and the user is anon there will be no categories // If the site is login_required and the user is anon there will be no categories
// preloaded, so there will be no category color CSS variables generated by // preloaded, so there will be no category color CSS variables generated by

View File

@ -2,12 +2,11 @@ import { withPluginApi } from "discourse/lib/plugin-api";
import { replaceHashtagIconPlaceholder } from "discourse/lib/hashtag-autocomplete"; import { replaceHashtagIconPlaceholder } from "discourse/lib/hashtag-autocomplete";
export default { export default {
name: "hashtag-post-decorations",
after: "hashtag-css-generator", after: "hashtag-css-generator",
initialize(container) { initialize(owner) {
const siteSettings = container.lookup("service:site-settings"); const siteSettings = owner.lookup("service:site-settings");
const site = container.lookup("service:site"); const site = owner.lookup("service:site");
withPluginApi("0.8.7", (api) => { withPluginApi("0.8.7", (api) => {
if (siteSettings.enable_experimental_hashtag_autocomplete) { if (siteSettings.enable_experimental_hashtag_autocomplete) {

View File

@ -11,8 +11,6 @@ import { withPluginApi } from "discourse/lib/plugin-api";
// the lifetime of all `<img` elements. // the lifetime of all `<img` elements.
export default { export default {
name: "image-aspect-ratio",
initWithApi(api) { initWithApi(api) {
const supportsAspectRatio = CSS.supports("aspect-ratio: 1"); const supportsAspectRatio = CSS.supports("aspect-ratio: 1");

View File

@ -4,14 +4,13 @@ import Site from "discourse/models/site";
import deprecated from "discourse-common/lib/deprecated"; import deprecated from "discourse-common/lib/deprecated";
export default { export default {
name: "inject-objects",
after: "sniff-capabilities", after: "sniff-capabilities",
initialize(container, app) { initialize(owner) {
// This is required for Ember CLI tests to work // This is required for Ember CLI tests to work
setDefaultOwner(app.__container__); setDefaultOwner(owner.__container__);
Object.defineProperty(app, "SiteSettings", { Object.defineProperty(owner, "SiteSettings", {
get() { get() {
deprecated( deprecated(
`use injected siteSettings instead of Discourse.SiteSettings`, `use injected siteSettings instead of Discourse.SiteSettings`,
@ -21,10 +20,10 @@ export default {
id: "discourse.global.site-settings", id: "discourse.global.site-settings",
} }
); );
return container.lookup("service:site-settings"); return owner.lookup("service:site-settings");
}, },
}); });
Object.defineProperty(app, "User", { Object.defineProperty(owner, "User", {
get() { get() {
deprecated( deprecated(
`import discourse/models/user instead of using Discourse.User`, `import discourse/models/user instead of using Discourse.User`,
@ -37,7 +36,7 @@ export default {
return User; return User;
}, },
}); });
Object.defineProperty(app, "Site", { Object.defineProperty(owner, "Site", {
get() { get() {
deprecated( deprecated(
`import discourse/models/site instead of using Discourse.Site`, `import discourse/models/site instead of using Discourse.Site`,

View File

@ -6,7 +6,6 @@ import deprecated from "discourse-common/lib/deprecated";
let jqueryPluginsConfigured = false; let jqueryPluginsConfigured = false;
export default { export default {
name: "jquery-plugins",
initialize() { initialize() {
if (jqueryPluginsConfigured) { if (jqueryPluginsConfigured) {
return; return;

View File

@ -2,10 +2,8 @@ import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import ItsATrap from "@discourse/itsatrap"; import ItsATrap from "@discourse/itsatrap";
export default { export default {
name: "keyboard-shortcuts", initialize(owner) {
KeyboardShortcuts.init(ItsATrap, owner);
initialize(container) {
KeyboardShortcuts.init(ItsATrap, container);
KeyboardShortcuts.bindEvents(); KeyboardShortcuts.bindEvents();
}, },

View File

@ -5,11 +5,9 @@ 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", initialize(owner) {
this.messageBus = owner.lookup("service:message-bus");
initialize(container) { const session = owner.lookup("service:session");
this.messageBus = container.lookup("service:message-bus");
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
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);

View File

@ -2,11 +2,10 @@ import I18n from "I18n";
import bootbox from "bootbox"; import bootbox from "bootbox";
export default { export default {
name: "localization",
after: "inject-objects", after: "inject-objects",
isVerboseLocalizationEnabled(container) { isVerboseLocalizationEnabled(owner) {
const siteSettings = container.lookup("service:site-settings"); const siteSettings = owner.lookup("service:site-settings");
if (siteSettings.verbose_localization) { if (siteSettings.verbose_localization) {
return true; return true;
} }
@ -18,8 +17,8 @@ export default {
} }
}, },
initialize(container) { initialize(owner) {
if (this.isVerboseLocalizationEnabled(container)) { if (this.isVerboseLocalizationEnabled(owner)) {
I18n.enableVerboseLocalization(); I18n.enableVerboseLocalization();
} }

View File

@ -6,13 +6,12 @@ let _showingLogout = false;
// Subscribe to "logout" change events via the Message Bus // Subscribe to "logout" change events via the Message Bus
export default { export default {
name: "logout",
after: "message-bus", after: "message-bus",
initialize(container) { initialize(owner) {
this.messageBus = container.lookup("service:message-bus"); this.messageBus = owner.lookup("service:message-bus");
this.dialog = container.lookup("service:dialog"); this.dialog = owner.lookup("service:dialog");
this.currentUser = container.lookup("service:current-user"); this.currentUser = owner.lookup("service:current-user");
if (this.currentUser) { if (this.currentUser) {
this.messageBus.subscribe( this.messageBus.subscribe(

View File

@ -3,18 +3,17 @@ import Singleton from "discourse/mixins/singleton";
let initializedOnce = false; let initializedOnce = false;
export default { export default {
name: "logs-notice",
after: "message-bus", after: "message-bus",
initialize(container) { initialize(owner) {
if (initializedOnce) { if (initializedOnce) {
return; return;
} }
const siteSettings = container.lookup("service:site-settings"); const siteSettings = owner.lookup("service:site-settings");
const messageBus = container.lookup("service:message-bus"); const messageBus = owner.lookup("service:message-bus");
const keyValueStore = container.lookup("service:key-value-store"); const keyValueStore = owner.lookup("service:key-value-store");
const currentUser = container.lookup("service:current-user"); const currentUser = owner.lookup("service:current-user");
LogsNotice.reopenClass(Singleton, { LogsNotice.reopenClass(Singleton, {
createCurrent() { createCurrent() {
return this.create({ return this.create({

View File

@ -25,22 +25,19 @@ function ajax(opts, messageBusConnectivity, appState) {
} }
export default { export default {
name: "message-bus",
after: "inject-objects", after: "inject-objects",
initialize(container) { initialize(owner) {
// We don't use the message bus in testing // We don't use the message bus in testing
if (isTesting()) { if (isTesting()) {
return; return;
} }
const messageBus = container.lookup("service:message-bus"), const messageBus = owner.lookup("service:message-bus"),
user = container.lookup("service:current-user"), user = owner.lookup("service:current-user"),
siteSettings = container.lookup("service:site-settings"), siteSettings = owner.lookup("service:site-settings"),
appState = container.lookup("service:app-state"), appState = owner.lookup("service:app-state"),
messageBusConnectivity = container.lookup( messageBusConnectivity = owner.lookup("service:message-bus-connectivity");
"service:message-bus-connectivity"
);
messageBus.alwaysLongPoll = !isProduction(); messageBus.alwaysLongPoll = !isProduction();
messageBus.shouldLongPollCallback = () => messageBus.shouldLongPollCallback = () =>

View File

@ -1,12 +1,11 @@
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
export default { export default {
name: "mobile-keyboard",
after: "mobile", after: "mobile",
initialize(container) { initialize(owner) {
const site = container.lookup("service:site"); const site = owner.lookup("service:site");
this.capabilities = container.lookup("service:capabilities"); this.capabilities = owner.lookup("service:capabilities");
if (!this.capabilities.isIpadOS && !site.mobileView) { if (!this.capabilities.isIpadOS && !site.mobileView) {
return; return;

View File

@ -3,12 +3,11 @@ import { setResolverOption } from "discourse-common/resolver";
// Initializes the `Mobile` helper object. // Initializes the `Mobile` helper object.
export default { export default {
name: "mobile",
after: "inject-objects", after: "inject-objects",
initialize(container) { initialize(owner) {
Mobile.init(); Mobile.init();
const site = container.lookup("service:site"); const site = owner.lookup("service:site");
site.set("mobileView", Mobile.mobileView); site.set("mobileView", Mobile.mobileView);
site.set("desktopView", !Mobile.mobileView); site.set("desktopView", !Mobile.mobileView);

View File

@ -1,5 +1,4 @@
export default { export default {
name: "moment",
after: "message-bus", after: "message-bus",
initialize() { initialize() {

View File

@ -1,19 +1,17 @@
import NarrowDesktop from "discourse/lib/narrow-desktop"; import NarrowDesktop from "discourse/lib/narrow-desktop";
export default { export default {
name: "narrow-desktop", initialize(owner) {
initialize(container) {
NarrowDesktop.init(); NarrowDesktop.init();
let site; let site;
if (!container.isDestroyed) { if (!owner.isDestroyed) {
site = container.lookup("service:site"); site = owner.lookup("service:site");
site.set("narrowDesktopView", NarrowDesktop.narrowDesktopView); site.set("narrowDesktopView", NarrowDesktop.narrowDesktopView);
} }
if ("ResizeObserver" in window) { if ("ResizeObserver" in window) {
this._resizeObserver = new ResizeObserver((entries) => { this._resizeObserver = new ResizeObserver((entries) => {
if (container.isDestroyed) { if (owner.isDestroyed) {
return; return;
} }
for (let entry of entries) { for (let entry of entries) {
@ -22,7 +20,7 @@ export default {
entry.contentRect.width entry.contentRect.width
); );
if (oldNarrowDesktopView !== newNarrowDesktopView) { if (oldNarrowDesktopView !== newNarrowDesktopView) {
const applicationController = container.lookup( const applicationController = owner.lookup(
"controller:application" "controller:application"
); );
site.set("narrowDesktopView", newNarrowDesktopView); site.set("narrowDesktopView", newNarrowDesktopView);

View File

@ -38,8 +38,6 @@ function _cleanUp() {
} }
export default { export default {
name: "onebox-decorators",
initialize() { initialize() {
withPluginApi("0.8.42", (api) => { withPluginApi("0.8.42", (api) => {
api.decorateCookedElement( api.decorateCookedElement(

View File

@ -1,9 +1,7 @@
import { getAbsoluteURL } from "discourse-common/lib/get-url"; import { getAbsoluteURL } from "discourse-common/lib/get-url";
export default { export default {
name: "opengraph-tag-updater", initialize(owner) {
initialize(container) {
// workaround for Safari on iOS 14.3 // workaround for Safari on iOS 14.3
// seems it has started using opengraph tags when sharing // seems it has started using opengraph tags when sharing
const ogTitle = document.querySelector("meta[property='og:title']"); const ogTitle = document.querySelector("meta[property='og:title']");
@ -17,7 +15,7 @@ export default {
return; return;
} }
const appEvents = container.lookup("service:app-events"); const appEvents = owner.lookup("service:app-events");
appEvents.on("page:changed", ({ title, url }) => { appEvents.on("page:changed", ({ title, url }) => {
ogTitle.setAttribute("content", title); ogTitle.setAttribute("content", title);
ogUrl.setAttribute("content", getAbsoluteURL(url)); ogUrl.setAttribute("content", getAbsoluteURL(url));

View File

@ -6,16 +6,15 @@ import {
import { viewTrackingRequired } from "discourse/lib/ajax"; import { viewTrackingRequired } from "discourse/lib/ajax";
export default { export default {
name: "page-tracking",
after: "inject-objects", after: "inject-objects",
initialize(container) { initialize(owner) {
// Tell our AJAX system to track a page transition // Tell our AJAX system to track a page transition
const router = container.lookup("router:main"); const router = owner.lookup("router:main");
router.on("routeWillChange", viewTrackingRequired); router.on("routeWillChange", viewTrackingRequired);
let appEvents = container.lookup("service:app-events"); let appEvents = owner.lookup("service:app-events");
let documentTitle = container.lookup("service:document-title"); let documentTitle = owner.lookup("service:document-title");
startPageTracking(router, appEvents, documentTitle); startPageTracking(router, appEvents, documentTitle);

View File

@ -1,7 +1,6 @@
import discourseTemplateMap from "discourse-common/lib/discourse-template-map"; import discourseTemplateMap from "discourse-common/lib/discourse-template-map";
export default { export default {
name: "populate-template-map",
initialize() { initialize() {
discourseTemplateMap.setModuleNames(Object.keys(requirejs.entries)); discourseTemplateMap.setModuleNames(Object.keys(requirejs.entries));
}, },

View File

@ -12,12 +12,11 @@ import { create } from "virtual-dom";
import showModal from "discourse/lib/show-modal"; import showModal from "discourse/lib/show-modal";
export default { export default {
name: "post-decorations", initialize(owner) {
initialize(container) {
withPluginApi("0.1", (api) => { withPluginApi("0.1", (api) => {
const siteSettings = container.lookup("service:site-settings"); const siteSettings = owner.lookup("service:site-settings");
const session = container.lookup("service:session"); const session = owner.lookup("service:session");
const site = container.lookup("service:site"); const site = owner.lookup("service:site");
api.decorateCookedElement( api.decorateCookedElement(
(elem) => { (elem) => {
return highlightSyntax(elem, siteSettings, session); return highlightSyntax(elem, siteSettings, session);
@ -78,7 +77,7 @@ export default {
{ id: "discourse-audio" } { id: "discourse-audio" }
); );
const caps = container.lookup("service:capabilities"); const caps = owner.lookup("service:capabilities");
if (caps.isSafari || caps.isIOS) { if (caps.isSafari || caps.isIOS) {
api.decorateCookedElement( api.decorateCookedElement(
(elem) => { (elem) => {

View File

@ -2,12 +2,11 @@ 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",
after: "message-bus", after: "message-bus",
initialize(container) { initialize(owner) {
this.messageBus = container.lookup("service:message-bus"); this.messageBus = owner.lookup("service:message-bus");
this.site = container.lookup("service:site"); this.site = owner.lookup("service:site");
this.messageBus.subscribe("/site/read-only", this.onMessage); this.messageBus.subscribe("/site/read-only", this.onMessage);
}, },

View File

@ -3,13 +3,12 @@ import CategoryHashtagType from "discourse/lib/hashtag-types/category";
import TagHashtagType from "discourse/lib/hashtag-types/tag"; import TagHashtagType from "discourse/lib/hashtag-types/tag";
export default { export default {
name: "register-hashtag-types",
before: "hashtag-css-generator", before: "hashtag-css-generator",
initialize(container) { initialize(owner) {
withPluginApi("0.8.7", (api) => { withPluginApi("0.8.7", (api) => {
api.registerHashtagType("category", new CategoryHashtagType(container)); api.registerHashtagType("category", new CategoryHashtagType(owner));
api.registerHashtagType("tag", new TagHashtagType(container)); api.registerHashtagType("tag", new TagHashtagType(owner));
}); });
}, },
}; };

View File

@ -3,11 +3,9 @@ import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin"
import { Promise } from "rsvp"; import { Promise } from "rsvp";
export default { export default {
name: "register-media-optimization-upload-processor", initialize(owner) {
const siteSettings = owner.lookup("service:site-settings");
initialize(container) { const capabilities = owner.lookup("service:capabilities");
const siteSettings = container.lookup("service:site-settings");
const capabilities = container.lookup("service:capabilities");
if (siteSettings.composer_media_optimization_image_enabled) { if (siteSettings.composer_media_optimization_image_enabled) {
// NOTE: There are various performance issues with the Canvas // NOTE: There are various performance issues with the Canvas
@ -32,11 +30,11 @@ export default {
({ isMobileDevice }) => { ({ isMobileDevice }) => {
return { return {
optimizeFn: (data, opts) => { optimizeFn: (data, opts) => {
if (container.isDestroyed || container.isDestroying) { if (owner.isDestroyed || owner.isDestroying) {
return Promise.resolve(); return Promise.resolve();
} }
return container return owner
.lookup("service:media-optimization-worker") .lookup("service:media-optimization-worker")
.optimizeImage(data, opts); .optimizeImage(data, opts);
}, },

View File

@ -0,0 +1,8 @@
import { registerServiceWorker } from "discourse/lib/register-service-worker";
export default {
initialize(owner) {
let { serviceWorkerURL } = owner.lookup("service:session");
registerServiceWorker(serviceWorkerURL);
},
};

View File

@ -2,8 +2,6 @@ import { updateRelativeAge } from "discourse/lib/formatter";
// Updates the relative ages of dates on the screen. // Updates the relative ages of dates on the screen.
export default { export default {
name: "relative-ages",
initialize() { initialize() {
this._interval = setInterval(function () { this._interval = setInterval(function () {
updateRelativeAge(document.querySelectorAll(".relative-date")); updateRelativeAge(document.querySelectorAll(".relative-date"));

View File

@ -2,10 +2,8 @@ import I18n from "I18n";
import Sharing from "discourse/lib/sharing"; import Sharing from "discourse/lib/sharing";
export default { export default {
name: "sharing-sources", initialize(owner) {
const siteSettings = owner.lookup("service:site-settings");
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
Sharing.addSource({ Sharing.addSource({
id: "twitter", id: "twitter",

View File

@ -1,9 +1,7 @@
export default { export default {
name: "show-footer", initialize(owner) {
const router = owner.lookup("router:main");
initialize(container) { const application = owner.lookup("controller:application");
const router = container.lookup("router:main");
const application = container.lookup("controller:application");
// only take care of hiding the footer here // only take care of hiding the footer here
// controllers MUST take care of displaying it // controllers MUST take care of displaying it

View File

@ -6,15 +6,13 @@ const ONE_DAY = 24 * 60 * 60 * 1000;
const PROMPT_HIDE_DURATION = ONE_DAY; const PROMPT_HIDE_DURATION = ONE_DAY;
export default { export default {
name: "signup-cta", initialize(owner) {
const screenTrack = owner.lookup("service:screen-track");
initialize(container) {
const screenTrack = container.lookup("service:screen-track");
const session = Session.current(); const session = Session.current();
const siteSettings = container.lookup("service:site-settings"); const siteSettings = owner.lookup("service:site-settings");
const keyValueStore = container.lookup("service:key-value-store"); const keyValueStore = owner.lookup("service:key-value-store");
const user = container.lookup("service:current-user"); const user = owner.lookup("service:current-user");
const appEvents = container.lookup("service:app-events"); const appEvents = owner.lookup("service:app-events");
// Preconditions // Preconditions
if (user) { if (user) {

View File

@ -1,9 +1,6 @@
export default { export default {
name: "sniff-capabilities", initialize(owner) {
after: "export-application-global", const caps = owner.lookup("service:capabilities");
initialize(container) {
const caps = container.lookup("service:capabilities");
const html = document.documentElement; const html = document.documentElement;
if (caps.touch) { if (caps.touch) {

View File

@ -1,11 +1,10 @@
import StickyAvatars from "discourse/lib/sticky-avatars"; import StickyAvatars from "discourse/lib/sticky-avatars";
export default { export default {
name: "sticky-avatars",
after: "inject-objects", after: "inject-objects",
initialize(container) { initialize(owner) {
this._stickyAvatars = StickyAvatars.init(container); this._stickyAvatars = StickyAvatars.init(owner);
}, },
teardown() { teardown() {

View File

@ -1,6 +1,4 @@
export default { export default {
name: "strip-mobile-app-url-params",
initialize() { initialize() {
let queryStrings = window.location.search; let queryStrings = window.location.search;

View File

@ -15,23 +15,22 @@ import Notification from "discourse/models/notification";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
export default { export default {
name: "subscribe-user-notifications",
after: "message-bus", after: "message-bus",
initialize(container) { initialize(owner) {
this.currentUser = container.lookup("service:current-user"); this.currentUser = owner.lookup("service:current-user");
if (!this.currentUser) { if (!this.currentUser) {
return; return;
} }
this.messageBus = container.lookup("service:message-bus"); this.messageBus = owner.lookup("service:message-bus");
this.store = container.lookup("service:store"); this.store = owner.lookup("service:store");
this.messageBus = container.lookup("service:message-bus"); this.messageBus = owner.lookup("service:message-bus");
this.appEvents = container.lookup("service:app-events"); this.appEvents = owner.lookup("service:app-events");
this.siteSettings = container.lookup("service:site-settings"); this.siteSettings = owner.lookup("service:site-settings");
this.site = container.lookup("service:site"); this.site = owner.lookup("service:site");
this.router = container.lookup("router:main"); this.router = owner.lookup("router:main");
this.reviewableCountsChannel = `/reviewable_counts/${this.currentUser.id}`; this.reviewableCountsChannel = `/reviewable_counts/${this.currentUser.id}`;

View File

@ -1,11 +1,8 @@
import { loadSprites } from "discourse/lib/svg-sprite-loader"; import { loadSprites } from "discourse/lib/svg-sprite-loader";
export default { export default {
name: "svg-sprite-fontawesome", initialize(owner) {
after: "export-application-global", const session = owner.lookup("service:session");
initialize(container) {
const session = container.lookup("service:session");
if (session.svgSpritePath) { if (session.svgSpritePath) {
loadSprites(session.svgSpritePath, "fontawesome"); loadSprites(session.svgSpritePath, "fontawesome");

View File

@ -13,15 +13,12 @@ import Ember from "ember";
const showingErrors = new Set(); const showingErrors = new Set();
export default { export default {
name: "theme-errors-handler", initialize(owner) {
after: "export-application-global",
initialize(container) {
if (isTesting()) { if (isTesting()) {
return; return;
} }
this.currentUser = container.lookup("service:current-user"); this.currentUser = owner.lookup("service:current-user");
getAndClearUnhandledThemeErrors().forEach((e) => this.reportThemeError(e)); getAndClearUnhandledThemeErrors().forEach((e) => this.reportThemeError(e));

View File

@ -13,8 +13,6 @@ const FLAG_PRIORITY = 700;
const DEFER_PRIORITY = 500; const DEFER_PRIORITY = 500;
export default { export default {
name: "topic-footer-buttons",
initialize() { initialize() {
registerTopicFooterButton({ registerTopicFooterButton({
id: "share-and-invite", id: "share-and-invite",

View File

@ -3,11 +3,10 @@ import { initializeDefaultHomepage } from "discourse/lib/utilities";
import escapeRegExp from "discourse-common/utils/escape-regexp"; import escapeRegExp from "discourse-common/utils/escape-regexp";
export default { export default {
name: "url-redirects",
after: "inject-objects", after: "inject-objects",
initialize(container) { initialize(owner) {
const currentUser = container.lookup("service:current-user"); const currentUser = owner.lookup("service:current-user");
if (currentUser) { if (currentUser) {
const username = currentUser.get("username"); const username = currentUser.get("username");
const escapedUsername = escapeRegExp(username); const escapedUsername = escapeRegExp(username);
@ -23,11 +22,11 @@ export default {
DiscourseURL.rewrite(/^\/groups\//, "/g/"); DiscourseURL.rewrite(/^\/groups\//, "/g/");
// Initialize default homepage // Initialize default homepage
let siteSettings = container.lookup("service:site-settings"); let siteSettings = owner.lookup("service:site-settings");
initializeDefaultHomepage(siteSettings); initializeDefaultHomepage(siteSettings);
let defaultUserRoute = siteSettings.view_user_route || "summary"; let defaultUserRoute = siteSettings.view_user_route || "summary";
if (!container.lookup(`route:user.${defaultUserRoute}`)) { if (!owner.lookup(`route:user.${defaultUserRoute}`)) {
defaultUserRoute = "summary"; defaultUserRoute = "summary";
} }

View File

@ -1,17 +1,16 @@
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
export default { export default {
name: "user-tips",
after: "message-bus", after: "message-bus",
initialize(container) { initialize(owner) {
this.currentUser = container.lookup("service:current-user"); this.currentUser = owner.lookup("service:current-user");
if (!this.currentUser) { if (!this.currentUser) {
return; return;
} }
this.messageBus = container.lookup("service:message-bus"); this.messageBus = owner.lookup("service:message-bus");
this.site = container.lookup("service:site"); this.site = owner.lookup("service:site");
this.messageBus.subscribe( this.messageBus.subscribe(
`/user-tips/${this.currentUser.id}`, `/user-tips/${this.currentUser.id}`,

View File

@ -3,11 +3,10 @@ import discourseLater from "discourse-common/lib/later";
// Send bg color to webview so iOS status bar matches site theme // Send bg color to webview so iOS status bar matches site theme
export default { export default {
name: "webview-background",
after: "inject-objects", after: "inject-objects",
initialize(container) { initialize(owner) {
const caps = container.lookup("service:capabilities"); const caps = owner.lookup("service:capabilities");
if (caps.isAppWebview) { if (caps.isAppWebview) {
window window
.matchMedia("(prefers-color-scheme: dark)") .matchMedia("(prefers-color-scheme: dark)")

View File

@ -1,8 +1,8 @@
import { setOwner } from "@ember/application"; import { setOwner } from "@ember/application";
export default class HashtagTypeBase { export default class HashtagTypeBase {
constructor(container) { constructor(owner) {
setOwner(this, container); setOwner(this, owner);
} }
get type() { get type() {

View File

@ -1,5 +1,6 @@
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
import { getOwner, setOwner } from "@ember/application";
import { run, throttle } from "@ember/runloop"; import { run, throttle } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
import { import {
@ -121,7 +122,9 @@ function preventKeyboardEvent(event) {
} }
export default { export default {
init(keyTrapper, container) { init(keyTrapper, owner) {
setOwner(this, owner);
// Sometimes the keyboard shortcut initializer is not torn down. This makes sure // Sometimes the keyboard shortcut initializer is not torn down. This makes sure
// we clear any previous test state. // we clear any previous test state.
if (this.keyTrapper) { if (this.keyTrapper) {
@ -130,14 +133,13 @@ export default {
} }
this.keyTrapper = new keyTrapper(); this.keyTrapper = new keyTrapper();
this.container = container;
this._stopCallback(); this._stopCallback();
this.searchService = this.container.lookup("service:search"); this.searchService = owner.lookup("service:search");
this.appEvents = this.container.lookup("service:app-events"); this.appEvents = owner.lookup("service:app-events");
this.currentUser = this.container.lookup("service:current-user"); this.currentUser = owner.lookup("service:current-user");
this.siteSettings = this.container.lookup("service:site-settings"); this.siteSettings = owner.lookup("service:site-settings");
this.site = this.container.lookup("service:site"); this.site = owner.lookup("service:site");
// Disable the shortcut if private messages are disabled // Disable the shortcut if private messages are disabled
if (!this.currentUser?.can_send_private_messages) { if (!this.currentUser?.can_send_private_messages) {
@ -158,11 +160,10 @@ export default {
this.keyTrapper?.destroy(); this.keyTrapper?.destroy();
this.keyTrapper = null; this.keyTrapper = null;
this.container = null;
}, },
isTornDown() { isTornDown() {
return this.keyTrapper == null || this.container == null; return this.keyTrapper == null;
}, },
bindKey(key, binding = null) { bindKey(key, binding = null) {
@ -298,12 +299,12 @@ export default {
const topic = this.currentTopic(); const topic = this.currentTopic();
if (topic && document.querySelectorAll(".posts-wrapper").length) { if (topic && document.querySelectorAll(".posts-wrapper").length) {
preventKeyboardEvent(event); preventKeyboardEvent(event);
this.container.lookup("controller:topic").send("toggleBookmark"); getOwner(this).lookup("controller:topic").send("toggleBookmark");
} }
}, },
logout() { logout() {
this.container.lookup("route:application").send("logout"); getOwner(this).lookup("route:application").send("logout");
}, },
quoteReply() { quoteReply() {
@ -354,7 +355,7 @@ export default {
if (el) { if (el) {
el.click(); el.click();
} else { } else {
const controller = this.container.lookup("controller:topic"); const controller = getOwner(this).lookup("controller:topic");
// Only the last page contains list of suggested topics. // Only the last page contains list of suggested topics.
const url = `/t/${controller.get("model.id")}/last.json`; const url = `/t/${controller.get("model.id")}/last.json`;
ajax(url).then((result) => { ajax(url).then((result) => {
@ -383,7 +384,7 @@ export default {
_jumpTo(direction) { _jumpTo(direction) {
if (document.querySelector(".container.posts")) { if (document.querySelector(".container.posts")) {
this.container.lookup("controller:topic").send(direction); getOwner(this).lookup("controller:topic").send(direction);
} }
}, },
@ -426,7 +427,7 @@ export default {
run(() => { run(() => {
if (document.querySelector(".container.posts")) { if (document.querySelector(".container.posts")) {
event.preventDefault(); // We need to stop printing the current page in Firefox event.preventDefault(); // We need to stop printing the current page in Firefox
this.container.lookup("controller:topic").print(); getOwner(this).lookup("controller:topic").print();
} }
}); });
}, },
@ -445,14 +446,14 @@ export default {
return; return;
} }
this.container.lookup("service:composer").open({ getOwner(this).lookup("service:composer").open({
action: Composer.CREATE_TOPIC, action: Composer.CREATE_TOPIC,
draftKey: Composer.NEW_TOPIC_KEY, draftKey: Composer.NEW_TOPIC_KEY,
}); });
}, },
focusComposer(event) { focusComposer(event) {
const composer = this.container.lookup("service:composer"); const composer = getOwner(this).lookup("service:composer");
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -461,14 +462,14 @@ export default {
}, },
fullscreenComposer() { fullscreenComposer() {
const composer = this.container.lookup("service:composer"); const composer = getOwner(this).lookup("service:composer");
if (composer.get("model")) { if (composer.get("model")) {
composer.toggleFullscreen(); composer.toggleFullscreen();
} }
}, },
pinUnpinTopic() { pinUnpinTopic() {
this.container.lookup("controller:topic").togglePinnedState(); getOwner(this).lookup("controller:topic").togglePinnedState();
}, },
goToPost(event) { goToPost(event) {
@ -497,7 +498,7 @@ export default {
}, },
showHelpModal() { showHelpModal() {
this.container getOwner(this)
.lookup("controller:application") .lookup("controller:application")
.send("showKeyboardShortcutsHelp"); .send("showKeyboardShortcutsHelp");
}, },
@ -531,7 +532,7 @@ export default {
sendToTopicListItemView(action, elem) { sendToTopicListItemView(action, elem) {
elem = elem || document.querySelector("tr.selected.topic-list-item"); elem = elem || document.querySelector("tr.selected.topic-list-item");
if (elem) { if (elem) {
const registry = this.container.lookup("-view-registry:main"); const registry = getOwner(this).lookup("-view-registry:main");
if (registry) { if (registry) {
const view = registry[elem.id]; const view = registry[elem.id];
view.send(action); view.send(action);
@ -540,7 +541,7 @@ export default {
}, },
currentTopic() { currentTopic() {
const topicController = this.container.lookup("controller:topic"); const topicController = getOwner(this).lookup("controller:topic");
if (topicController) { if (topicController) {
const topic = topicController.get("model"); const topic = topicController.get("model");
if (topic) { if (topic) {
@ -550,7 +551,7 @@ export default {
}, },
isPostTextSelected() { isPostTextSelected() {
const topicController = this.container.lookup("controller:topic"); const topicController = getOwner(this).lookup("controller:topic");
return !!topicController?.get("quoteState")?.postId; return !!topicController?.get("quoteState")?.postId;
}, },
@ -565,7 +566,7 @@ export default {
} }
if (selectedPostId) { if (selectedPostId) {
const topicController = this.container.lookup("controller:topic"); const topicController = getOwner(this).lookup("controller:topic");
const post = topicController const post = topicController
.get("model.postStream.posts") .get("model.postStream.posts")
.findBy("id", selectedPostId); .findBy("id", selectedPostId);
@ -574,7 +575,7 @@ export default {
let actionMethod = topicController.actions[action]; let actionMethod = topicController.actions[action];
if (!actionMethod) { if (!actionMethod) {
const topicRoute = this.container.lookup("route:topic"); const topicRoute = getOwner(this).lookup("route:topic");
actionMethod = topicRoute.actions[action]; actionMethod = topicRoute.actions[action];
} }
@ -849,7 +850,7 @@ export default {
}, },
_replyToPost() { _replyToPost() {
this.container.lookup("controller:topic").send("replyToPost"); getOwner(this).lookup("controller:topic").send("replyToPost");
}, },
_getSelectedPost() { _getSelectedPost() {
@ -861,7 +862,7 @@ export default {
}, },
deferTopic() { deferTopic() {
this.container.lookup("controller:topic").send("deferTopic"); getOwner(this).lookup("controller:topic").send("deferTopic");
}, },
toggleAdminActions() { toggleAdminActions() {

View File

@ -1,10 +1,6 @@
import getAbsoluteURL, { isAbsoluteURL } from "discourse-common/lib/get-url"; import getAbsoluteURL, { isAbsoluteURL } from "discourse-common/lib/get-url";
export function registerServiceWorker( export function registerServiceWorker(serviceWorkerURL, registerOptions = {}) {
container,
serviceWorkerURL,
registerOptions = {}
) {
if (window.isSecureContext && "serviceWorker" in navigator) { if (window.isSecureContext && "serviceWorker" in navigator) {
if (serviceWorkerURL) { if (serviceWorkerURL) {
navigator.serviceWorker.getRegistrations().then((registrations) => { navigator.serviceWorker.getRegistrations().then((registrations) => {

View File

@ -2,11 +2,12 @@ import { addWidgetCleanCallback } from "discourse/components/mount-widget";
import Site from "discourse/models/site"; import Site from "discourse/models/site";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import { headerOffset } from "discourse/lib/offset-calculator"; import { headerOffset } from "discourse/lib/offset-calculator";
import { getOwner, setOwner } from "@ember/application";
import { schedule } from "@ember/runloop"; import { schedule } from "@ember/runloop";
export default class StickyAvatars { export default class StickyAvatars {
static init(container) { static init(owner) {
return new this(container).init(); return new this(owner).init();
} }
stickyClass = "sticky-avatar"; stickyClass = "sticky-avatar";
@ -15,8 +16,8 @@ export default class StickyAvatars {
direction = "⬇️"; direction = "⬇️";
prevOffset = -1; prevOffset = -1;
constructor(container) { constructor(owner) {
this.container = container; setOwner(this, owner);
} }
init() { init() {
@ -24,7 +25,7 @@ export default class StickyAvatars {
return; return;
} }
const appEvents = this.container.lookup("service:app-events"); const appEvents = getOwner(this).lookup("service:app-events");
appEvents.on("topic:current-post-scrolled", this._handlePostNodes); appEvents.on("topic:current-post-scrolled", this._handlePostNodes);
appEvents.on("topic:scrolled", this._handleScroll); appEvents.on("topic:scrolled", this._handleScroll);
appEvents.on("page:topic-loaded", this._initIntersectionObserver); appEvents.on("page:topic-loaded", this._initIntersectionObserver);
@ -34,9 +35,7 @@ export default class StickyAvatars {
return this; return this;
} }
destroy() { destroy() {}
this.container = null;
}
@bind @bind
_handleScroll(offset) { _handleScroll(offset) {

View File

@ -3,7 +3,7 @@ import Session from "discourse/models/session";
import Site from "discourse/models/site"; import Site from "discourse/models/site";
import TopicTrackingState from "discourse/models/topic-tracking-state"; import TopicTrackingState from "discourse/models/topic-tracking-state";
import User from "discourse/models/user"; import User from "discourse/models/user";
import { autoLoadModules } from "discourse/initializers/auto-load-modules"; import { autoLoadModules } from "discourse/instance-initializers/auto-load-modules";
import QUnit, { test } from "qunit"; import QUnit, { test } from "qunit";
import { setupRenderingTest as emberSetupRenderingTest } from "ember-qunit"; import { setupRenderingTest as emberSetupRenderingTest } from "ember-qunit";
import { currentSettings } from "discourse/tests/helpers/site-settings"; import { currentSettings } from "discourse/tests/helpers/site-settings";

View File

@ -1,17 +1,19 @@
import { module, test } from "qunit"; import { module, test } from "qunit";
import I18n from "I18n"; import I18n from "I18n";
import LocalizationInitializer from "discourse/initializers/localization"; import LocalizationInitializer from "discourse/instance-initializers/localization";
import { getApplication } from "@ember/test-helpers"; import { setupTest } from "ember-qunit";
module("initializer:localization", { module("initializer:localization", function (hooks) {
_locale: I18n.locale, setupTest(hooks);
_translations: I18n.translations,
_extras: I18n.extras, hooks.beforeEach(function () {
_compiledMFs: I18n._compiledMFs, this._locale = I18n.locale;
_overrides: I18n._overrides, this._translations = I18n.translations;
_mfOverrides: I18n._mfOverrides, this._extras = I18n.extras;
this._compiledMFs = I18n._compiledMFs;
this._overrides = I18n._overrides;
this._mfOverrides = I18n._mfOverrides;
beforeEach() {
I18n.locale = "fr"; I18n.locale = "fr";
I18n.translations = { I18n.translations = {
@ -59,131 +61,131 @@ module("initializer:localization", {
}, },
}, },
}; };
}, });
afterEach() { hooks.afterEach(function () {
I18n.locale = this._locale; I18n.locale = this._locale;
I18n.translations = this._translations; I18n.translations = this._translations;
I18n.extras = this._extras; I18n.extras = this._extras;
I18n._compiledMFs = this._compiledMFs; I18n._compiledMFs = this._compiledMFs;
I18n._overrides = this._overrides; I18n._overrides = this._overrides;
I18n._mfOverrides = this._mfOverrides; I18n._mfOverrides = this._mfOverrides;
}, });
});
test("translation overrides", function (assert) { test("translation overrides", function (assert) {
I18n._overrides = { I18n._overrides = {
fr: { fr: {
"js.composer.both_languages1": "composer.both_languages1 (FR override)", "js.composer.both_languages1": "composer.both_languages1 (FR override)",
"js.composer.only_english2": "composer.only_english2 (FR override)", "js.composer.only_english2": "composer.only_english2 (FR override)",
}, },
en: { en: {
"js.composer.both_languages2": "composer.both_languages2 (EN override)", "js.composer.both_languages2": "composer.both_languages2 (EN override)",
"js.composer.only_english1": "composer.only_english1 (EN override)", "js.composer.only_english1": "composer.only_english1 (EN override)",
}, },
}; };
LocalizationInitializer.initialize(getApplication()); LocalizationInitializer.initialize(this.owner);
assert.strictEqual( assert.strictEqual(
I18n.t("composer.both_languages1"), I18n.t("composer.both_languages1"),
"composer.both_languages1 (FR override)", "composer.both_languages1 (FR override)",
"overrides existing translation in current locale" "overrides existing translation in current locale"
); );
assert.strictEqual( assert.strictEqual(
I18n.t("composer.only_english1"), I18n.t("composer.only_english1"),
"composer.only_english1 (EN override)", "composer.only_english1 (EN override)",
"overrides translation in fallback locale" "overrides translation in fallback locale"
); );
assert.strictEqual( assert.strictEqual(
I18n.t("composer.only_english2"), I18n.t("composer.only_english2"),
"composer.only_english2 (FR override)", "composer.only_english2 (FR override)",
"overrides translation that doesn't exist in current locale" "overrides translation that doesn't exist in current locale"
); );
assert.strictEqual( assert.strictEqual(
I18n.t("composer.both_languages2"), I18n.t("composer.both_languages2"),
"composer.both_languages2 (FR)", "composer.both_languages2 (FR)",
"prefers translation in current locale over override in fallback locale" "prefers translation in current locale over override in fallback locale"
); );
}); });
test("translation overrides (admin_js)", function (assert) { test("translation overrides (admin_js)", function (assert) {
I18n._overrides = { I18n._overrides = {
fr: { fr: {
"admin_js.admin.api.both_languages1": "admin_js.admin.api.both_languages1":
"admin.api.both_languages1 (FR override)", "admin.api.both_languages1 (FR override)",
"admin_js.admin.api.only_english2": "admin_js.admin.api.only_english2":
"admin.api.only_english2 (FR override)", "admin.api.only_english2 (FR override)",
"admin_js.type_to_filter": "type_to_filter (FR override)", "admin_js.type_to_filter": "type_to_filter (FR override)",
}, },
en: { en: {
"admin_js.admin.api.both_languages2": "admin_js.admin.api.both_languages2":
"admin.api.both_languages2 (EN override)", "admin.api.both_languages2 (EN override)",
"admin_js.admin.api.only_english1": "admin_js.admin.api.only_english1":
"admin.api.only_english1 (EN override)", "admin.api.only_english1 (EN override)",
}, },
}; };
LocalizationInitializer.initialize(getApplication()); LocalizationInitializer.initialize(this.owner);
assert.strictEqual( assert.strictEqual(
I18n.t("admin.api.both_languages1"), I18n.t("admin.api.both_languages1"),
"admin.api.both_languages1 (FR override)", "admin.api.both_languages1 (FR override)",
"overrides existing translation in current locale" "overrides existing translation in current locale"
); );
assert.strictEqual( assert.strictEqual(
I18n.t("admin.api.only_english1"), I18n.t("admin.api.only_english1"),
"admin.api.only_english1 (EN override)", "admin.api.only_english1 (EN override)",
"overrides translation in fallback locale" "overrides translation in fallback locale"
); );
assert.strictEqual( assert.strictEqual(
I18n.t("admin.api.only_english2"), I18n.t("admin.api.only_english2"),
"admin.api.only_english2 (FR override)", "admin.api.only_english2 (FR override)",
"overrides translation that doesn't exist in current locale" "overrides translation that doesn't exist in current locale"
); );
assert.strictEqual( assert.strictEqual(
I18n.t("admin.api.both_languages2"), I18n.t("admin.api.both_languages2"),
"admin.api.both_languages2 (FR)", "admin.api.both_languages2 (FR)",
"prefers translation in current locale over override in fallback locale" "prefers translation in current locale over override in fallback locale"
); );
assert.strictEqual( assert.strictEqual(
I18n.t("type_to_filter"), I18n.t("type_to_filter"),
"type_to_filter (FR override)", "type_to_filter (FR override)",
"correctly changes the translation key by removing `admin_js`" "correctly changes the translation key by removing `admin_js`"
); );
}); });
test("translation overrides for MessageFormat strings", function (assert) { test("translation overrides for MessageFormat strings", function (assert) {
I18n._mfOverrides = { I18n._mfOverrides = {
"js.user.messages.some_key_MF": () => "js.user.messages.some_key_MF": () =>
"user.messages.some_key_MF (FR override)",
};
LocalizationInitializer.initialize(this.owner);
assert.strictEqual(
I18n.messageFormat("user.messages.some_key_MF", {}),
"user.messages.some_key_MF (FR override)", "user.messages.some_key_MF (FR override)",
}; "overrides existing MessageFormat string"
);
});
LocalizationInitializer.initialize(getApplication()); test("skip translation override if parent node is not an object", function (assert) {
I18n._overrides = {
fr: {
"js.composer.both_languages1.foo":
"composer.both_languages1.foo (FR override)",
},
};
LocalizationInitializer.initialize(this.owner);
assert.strictEqual( assert.strictEqual(
I18n.messageFormat("user.messages.some_key_MF", {}), I18n.t("composer.both_languages1.foo"),
"user.messages.some_key_MF (FR override)", "[fr.composer.both_languages1.foo]"
"overrides existing MessageFormat string" );
); });
});
test("skip translation override if parent node is not an object", function (assert) {
I18n._overrides = {
fr: {
"js.composer.both_languages1.foo":
"composer.both_languages1.foo (FR override)",
},
};
LocalizationInitializer.initialize(getApplication());
assert.strictEqual(
I18n.t("composer.both_languages1.foo"),
"[fr.composer.both_languages1.foo]"
);
}); });

View File

@ -1,4 +1,4 @@
import { decorateGithubOneboxBody } from "discourse/initializers/onebox-decorators"; import { decorateGithubOneboxBody } from "discourse/instance-initializers/onebox-decorators";
import { replaceHashtagIconPlaceholder } from "discourse/lib/hashtag-autocomplete"; import { replaceHashtagIconPlaceholder } from "discourse/lib/hashtag-autocomplete";
import { withPluginApi } from "discourse/lib/plugin-api"; import { withPluginApi } from "discourse/lib/plugin-api";
import highlightSyntax from "discourse/lib/highlight-syntax"; import highlightSyntax from "discourse/lib/highlight-syntax";