diff --git a/renderer/src/modules/webpackmodules.js b/renderer/src/modules/webpackmodules.js index cb768f28..e22c194d 100644 --- a/renderer/src/modules/webpackmodules.js +++ b/renderer/src/modules/webpackmodules.js @@ -244,6 +244,36 @@ export default class WebpackModules { return this.getModule(Filters.byString(...strings), false); } + /** + * Finds a module that lazily loaded. + * @param {(m) => boolean} filter A function to use to filter modules. + * @returns {Promise} + */ + static getLazy(filter) { + const fromCache = this.getModule(filter); + if (fromCache) return Promise.resolve(fromCache); + + return new Promise((resolve) => { + const cancel = () => {this.removeListener(listener)}; + const listener = function (m) { + const directMatch = filter(m); + + if (directMatch) { + cancel(); + return resolve(directMatch); + } + + const defaultMatch = filter(m.default); + if (!defaultMatch) return; + + cancel(); + resolve(m.default); + }; + + this.addListener(listener); + }); + } + /** * Discord's __webpack_require__ function. */ @@ -276,4 +306,75 @@ export default class WebpackModules { return this.require.c; } -} \ No newline at end of file + // Webpack Chunk Observing + static get chunkName() {return "webpackChunkdiscord_app";} + + static initialize() { + this.handlePush = this.handlePush.bind(this); + this.listeners = new Set(); + + this.__ORIGINAL_PUSH__ = window[this.chunkName].push; + Object.defineProperty(window[this.chunkName], "push", { + configurable: true, + get: () => this.handlePush, + set: (newPush) => { + this.__ORIGINAL_PUSH__ = newPush; + + Object.defineProperty(window[this.chunkName], "push", { + value: this.handlePush, + configurable: true, + writable: true + }); + } + }); + } + + /** + * Adds a listener for when discord loaded a chunk. Useful for subscribing to lazy loaded modules. + * @param {Function} listener - Function to subscribe for chunks + * @returns {Function} A cancelling function + */ + static addListener(listener) { + this.listeners.add(listener); + return this.removeListener.bind(this, listener); + } + + /** + * Removes a listener for when discord loaded a chunk. + * @param {Function} listener + * @returns {boolean} + */ + static removeListener(listener) {return this.listeners.delete(listener);} + + static handlePush(chunk) { + const [, modules] = chunk; + + for (const moduleId in modules) { + const originalModule = modules[moduleId]; + + modules[moduleId] = (module, exports, require) => { + try { + Reflect.apply(originalModule, null, [module, exports, require]); + + const listeners = [...this.listeners]; + for (let i = 0; i < listeners.length; i++) { + try {listeners[i](exports);} + catch (error) { + Logger.err("WebpackModules", "Could not fire callback listener:", error); + } + } + } catch (error) { + console.error(error); + } + }; + + Object.assign(modules[moduleId], originalModule, { + toString: () => originalModule.toString() + }); + } + + return Reflect.apply(this.__ORIGINAL_PUSH__, window[this.chunkName], [chunk]); + } +} + +WebpackModules.initialize(); \ No newline at end of file diff --git a/renderer/src/ui/settings.js b/renderer/src/ui/settings.js index bf80e1db..94be7254 100644 --- a/renderer/src/ui/settings.js +++ b/renderer/src/ui/settings.js @@ -4,6 +4,7 @@ import AddonList from "./settings/addonlist"; import SettingsGroup from "./settings/group"; import SettingsTitle from "./settings/title"; import Header from "./settings/sidebarheader"; +import {Filters} from "../modules/webpackmodules"; export default new class SettingsRenderer { @@ -60,8 +61,9 @@ export default new class SettingsRenderer { }, options)); } - patchSections() { - const UserSettings = WebpackModules.getByDisplayName("SettingsView"); + async patchSections() { + const UserSettings = await WebpackModules.getLazy(Filters.byDisplayName("SettingsView")); + Patcher.after("SettingsManager", UserSettings.prototype, "getPredicateSections", (thisObject, args, returnValue) => { let location = returnValue.findIndex(s => s.section.toLowerCase() == "changelog") - 1; if (location < 0) return; @@ -91,7 +93,7 @@ export default new class SettingsRenderer { } forceUpdate() { - const viewClass = WebpackModules.getByProps("standardSidebarView").standardSidebarView.split(" ")[0]; + const viewClass = WebpackModules.getByProps("standardSidebarView")?.standardSidebarView.split(" ")[0]; const node = document.querySelector(`.${viewClass}`); if (!node) return; const stateNode = Utilities.findInReactTree(Utilities.getReactInstance(node), m => m && m.getPredicateSections, {walkable: ["return", "stateNode"]});