diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4ded9c..116b1b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ This changelog starts with the restructured 1.0.0 release that happened after context isolation changes. The changelogs here should more-or-less mirror the ones that get shown in the client but probably with less formatting and pizzazz. +## 1.6.1 + +### Added + +### Removed + +### Changed + +### Fixed +- Fixed an issue where `waitForModule` would not return the found module. +- Fixed an issue where broken addon METAs could prevent BD from fully loading. +- Fixed an issue where developer badges stopped rendering. + ## 1.6.0 ### Added diff --git a/package.json b/package.json index 0dae6b0b..449e9466 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "betterdiscord", - "version": "1.6.0", + "version": "1.6.1", "description": "Enhances Discord by adding functionality and themes.", "main": "src/index.js", "scripts": { diff --git a/renderer/src/data/changelog.js b/renderer/src/data/changelog.js index e50e74da..ee4f6084 100644 --- a/renderer/src/data/changelog.js +++ b/renderer/src/data/changelog.js @@ -1,26 +1,14 @@ // fixed, improved, added, progress export default { - description: "Big things are coming soon, this is just the tip of the iceberg!", + description: "Big things are coming soon, please be patient!", changes: [ { - title: "What's New?", - type: "added", + title: "Fixes", + type: "fixed", items: [ - "Better handling and fallback when the editor fails to load. (Thanks Qb)", - "Now able to sort addons by whether they're enabled. (Thanks TheGreenPig)", - "New `Webpack` API added for plugin developers to take advantage of. Please see the docs for details!", - "New developer docs (still work-in-progress) available at https://docs.betterdiscord.app" - ] - }, - { - title: "Improvements", - type: "improved", - items: [ - "Addons should now load faster, use less memory, and be a bit more consistent!", - "Addon error modal should work a little better. (Thanks Qb)", - "Plugin and startup errors should make more sense now.", - "The crash dialog has more information and more buttons.", - "Minor speed and memory improvements." + "Fixed an issue where `waitForModule` would not return the found module.", + "Fixed an issue where broken addon METAs could prevent BD from fully loading.", + "Fixed an issue where developer badges stopped rendering." ] } ] diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js index 69de1acd..81c6386d 100644 --- a/renderer/src/modules/addonmanager.js +++ b/renderer/src/modules/addonmanager.js @@ -4,7 +4,6 @@ import Settings from "./settingsmanager"; import Events from "./emitter"; import DataStore from "./datastore"; import AddonError from "../structs/addonerror"; -import MetaError from "../structs/metaerror"; import Toasts from "../ui/toasts"; import DiscordModules from "./discordmodules"; import Strings from "./strings"; @@ -121,21 +120,21 @@ export default class AddonManager { Logger.log(this.name, `No longer watching ${this.prefix} addons.`); } - extractMeta(fileContent) { + extractMeta(fileContent, filename) { const firstLine = fileContent.split("\n")[0]; - const hasOldMeta = firstLine.includes("//META"); - if (hasOldMeta) return this.parseOldMeta(fileContent); + const hasOldMeta = firstLine.includes("//META") && firstLine.includes("*//"); + if (hasOldMeta) return this.parseOldMeta(fileContent, filename); const hasNewMeta = firstLine.includes("/**"); if (hasNewMeta) return this.parseNewMeta(fileContent); - throw new MetaError(Strings.Addons.metaNotFound); + throw new AddonError(filename, filename, Strings.Addons.metaNotFound, {message: "", stack: fileContent}, this.prefix); } - parseOldMeta(fileContent) { + parseOldMeta(fileContent, filename) { const meta = fileContent.split("\n")[0]; const metaData = meta.substring(meta.lastIndexOf("//META") + 6, meta.lastIndexOf("*//")); const parsed = Utilities.testJSON(metaData); - if (!parsed) throw new MetaError(Strings.Addons.metaError); - if (!parsed.name) throw new MetaError(Strings.Addons.missingNameData); + if (!parsed) throw new AddonError(filename, filename, Strings.Addons.metaError, {message: "", stack: meta}, this.prefix); + if (!parsed.name) throw new AddonError(filename, filename, Strings.Addons.missingNameData, {message: "", stack: meta}, this.prefix); parsed.format = "json"; return parsed; } @@ -168,12 +167,12 @@ export default class AddonManager { let fileContent = fs.readFileSync(filename, "utf8"); fileContent = stripBOM(fileContent); const stats = fs.statSync(filename); - const addon = this.extractMeta(fileContent); + const addon = this.extractMeta(fileContent, path.basename(filename)); if (!addon.author) addon.author = Strings.Addons.unknownAuthor; if (!addon.version) addon.version = "???"; if (!addon.description) addon.description = Strings.Addons.noDescription; // if (!addon.name || !addon.author || !addon.description || !addon.version) return new AddonError(addon.name || path.basename(filename), filename, "Addon is missing name, author, description, or version", {message: "Addon must provide name, author, description, and version.", stack: ""}, this.prefix); - addon.id = addon.name; + addon.id = addon.name || path.basename(filename); addon.slug = path.basename(filename).replace(this.extension, "").replace(/ /g, "-"); addon.filename = path.basename(filename); addon.added = stats.atimeMs; @@ -186,9 +185,13 @@ export default class AddonManager { // Subclasses should use the return (if not AddonError) and push to this.addonList loadAddon(filename, shouldToast = false) { if (typeof(filename) === "undefined") return; - - const addon = this.requireAddon(path.resolve(this.addonFolder, filename)); - if (addon instanceof AddonError) return addon; + let addon; + try { + addon = this.requireAddon(path.resolve(this.addonFolder, filename)); + } + catch (e) { + return e; + } if (this.addonList.find(c => c.id == addon.id)) return new AddonError(addon.name, filename, Strings.Addons.alreadyExists.format({type: this.prefix, name: addon.name}), this.prefix); const error = this.initializeAddon(addon); diff --git a/renderer/src/modules/componentpatcher.js b/renderer/src/modules/componentpatcher.js index 91b6fe4d..819b9cb9 100644 --- a/renderer/src/modules/componentpatcher.js +++ b/renderer/src/modules/componentpatcher.js @@ -148,29 +148,45 @@ export default new class ComponentPatcher { patchMessageHeader() { if (this.messageHeaderPatch) return; - const MessageTimestamp = WebpackModules.getModule(m => m?.default?.toString().indexOf("showTimestampOnHover") > -1); - this.messageHeaderPatch = Patcher.after("ComponentPatcher", MessageTimestamp, "default", (_, [{message}], returnValue) => { - const userId = Utilities.getNestedProp(message, "author.id"); - if (Developers.indexOf(userId) < 0) return; - const children = Utilities.getNestedProp(returnValue, "props.children.1.props.children"); - if (!Array.isArray(children)) return; + // const MessageTimestamp = WebpackModules.getModule(m => m?.default?.toString().indexOf("showTimestampOnHover") > -1); + // this.messageHeaderPatch = Patcher.after("ComponentPatcher", MessageTimestamp, "default", (_, [{message}], returnValue) => { + // const userId = Utilities.getNestedProp(message, "author.id"); + // if (Developers.indexOf(userId) < 0) return; + // if (!returnValue?.type) return; + // const orig = returnValue.type; + // returnValue.type = function() { + // const retVal = Reflect.apply(orig, this, arguments); - children.splice(2, 0, - React.createElement(DeveloperBadge, { - type: "chat" - }) - ); - }); + // const children = Utilities.getNestedProp(retVal, "props.children.1.props.children"); + // if (!Array.isArray(children)) return; + + // children.splice(3, 0, + // React.createElement(DeveloperBadge, { + // type: "chat" + // }) + // ); + + // return retVal; + // }; + + // }); } - patchMemberList() { + async patchMemberList() { if (this.memberListPatch) return; - const MemberListItem = WebpackModules.findByDisplayName("MemberListItem"); + const memo = WebpackModules.find(m => m?.type?.toString().includes("useGlobalHasAvatarDecorations")); + if (!memo?.type) return; + const MemberListItem = await new Promise(resolve => { + const cancelFindListItem = Patcher.after("ComponentPatcher", memo, "type", (_, props, returnValue) => { + cancelFindListItem(); + resolve(returnValue?.type); + }); + }); if (!MemberListItem?.prototype?.renderDecorators) return; this.memberListPatch = Patcher.after("ComponentPatcher", MemberListItem.prototype, "renderDecorators", (thisObject, args, returnValue) => { const user = Utilities.getNestedProp(thisObject, "props.user"); const children = Utilities.getNestedProp(returnValue, "props.children"); - if (!children || Developers.indexOf(user.id) < 0) return; + if (!children || Developers.indexOf(user?.id) < 0) return; if (!Array.isArray(children)) return; children.push( React.createElement(DeveloperBadge, { @@ -182,19 +198,21 @@ export default new class ComponentPatcher { patchProfile() { if (this.profilePatch) return; - const UserProfileBadgeList = WebpackModules.getModule(m => m?.default?.displayName === "UserProfileBadgeList"); - this.profilePatch = Patcher.after("ComponentPatcher", UserProfileBadgeList, "default", (_, [{user}], res) => { - if (Developers.indexOf(user?.id) < 0) return; - const children = Utilities.getNestedProp(res, "props.children"); - if (!Array.isArray(children)) return; + const UserProfileBadgeLists = WebpackModules.getModule(m => m?.default?.displayName === "UserProfileBadgeList", {first: false}); + for (const UserProfileBadgeList of UserProfileBadgeLists) { + this.profilePatch = Patcher.after("ComponentPatcher", UserProfileBadgeList, "default", (_, [{user}], res) => { + if (Developers.indexOf(user?.id) < 0) return; + const children = Utilities.getNestedProp(res, "props.children"); + if (!Array.isArray(children)) return; - children.unshift( - React.createElement(DeveloperBadge, { - type: "profile", - size: 18 - }) - ); - }); + children.unshift( + React.createElement(DeveloperBadge, { + type: "profile", + size: 18 + }) + ); + }); + } } }; \ No newline at end of file diff --git a/renderer/src/modules/utilities.js b/renderer/src/modules/utilities.js index b4e4cf15..ece4bfd3 100644 --- a/renderer/src/modules/utilities.js +++ b/renderer/src/modules/utilities.js @@ -1,6 +1,5 @@ import {Config} from "data"; import Logger from "common/logger"; -import DOM from "./domtools"; export default class Utilities { @@ -8,36 +7,6 @@ export default class Utilities { return `https://cdn.staticaly.com/gh/BetterDiscord/BetterDiscord/${Config.hash}/${path}`; } - /** - * Parses a string of HTML and returns the results. If the second parameter is true, - * the parsed HTML will be returned as a document fragment {@see https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment}. - * This is extremely useful if you have a list of elements at the top level, they can then be appended all at once to another node. - * - * If the second parameter is false, then the return value will be the list of parsed - * nodes and there were multiple top level nodes, otherwise the single node is returned. - * @param {string} html - HTML to be parsed - * @param {boolean} [fragment=false] - Whether or not the return should be the raw `DocumentFragment` - * @returns {(DocumentFragment|NodeList|HTMLElement)} - The result of HTML parsing - */ - static parseHTML(html, fragment = false) { - const template = document.createElement("template"); - template.innerHTML = html; - const node = template.content.cloneNode(true); - if (fragment) return node; - return node.childNodes.length > 1 ? node.childNodes : node.childNodes[0]; - } - - static getTextArea() { - return DOM.query(".channelTextArea-1LDbYG textarea"); - } - - static insertText(textarea, text) { - textarea.focus(); - textarea.selectionStart = 0; - textarea.selectionEnd = textarea.value.length; - document.execCommand("insertText", false, text); - } - static escape(s) { return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); } @@ -148,25 +117,6 @@ export default class Utilities { return proxy; } - /** - * Protects prototypes from external assignment. - * - * Needs some work before full usage - * @param {Class} Component - component with prototype to protect - */ - static protectPrototype(Component) { - const descriptors = Object.getOwnPropertyDescriptors(Component.prototype); - for (const name in descriptors) { - const descriptor = descriptors[name]; - descriptor.configurable = false; - descriptor.enumerable = false; - if (Object.prototype.hasOwnProperty.call(descriptor, "get")) descriptor.set = () => Logger.warn("protectPrototype", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins"); - if (Object.prototype.hasOwnProperty.call(descriptor, "value") && typeof(descriptor.value) === "function") descriptor.value.bind(Component.prototype); - if (Object.prototype.hasOwnProperty.call(descriptor, "writable")) descriptor.writable = false; - } - Object.defineProperties(Component.prototype, descriptors); - } - /** * Deep extends an object with a set of other objects. Objects later in the list * of `extenders` have priority, that is to say if one sets a key to be a primitive, diff --git a/renderer/src/modules/webpackmodules.js b/renderer/src/modules/webpackmodules.js index 0c085200..f40c2f74 100644 --- a/renderer/src/modules/webpackmodules.js +++ b/renderer/src/modules/webpackmodules.js @@ -142,8 +142,8 @@ export default class WebpackModules { /** * Finds a module using a filter function. - * @param {Function} filter A function to use to filter modules - * @param {object} [options] Whether to return only the first matching module + * @param {function} filter A function to use to filter modules + * @param {object} [options] Set of options to customize the search * @param {Boolean} [options.first=true] Whether to return only the first matching module * @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export * @return {Any} @@ -312,7 +312,7 @@ export default class WebpackModules { /** * Finds a module that lazily loaded. * @param {(m) => boolean} filter A function to use to filter modules. - * @param {object} [options] Whether to return only the first matching module + * @param {object} [options] Set of options to customize the search * @param {AbortSignal} [options.signal] AbortSignal of an AbortController to cancel the promise * @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export * @returns {Promise} @@ -320,25 +320,25 @@ export default class WebpackModules { static getLazy(filter, options = {}) { /** @type {AbortSignal} */ const abortSignal = options.signal; - const defaultExport = options.defaultExport; + const defaultExport = options.defaultExport ?? true; 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); + const listener = function (mod) { + const directMatch = filter(mod); if (directMatch) { cancel(); return resolve(directMatch); } - const defaultMatch = filter(m.default); + const defaultMatch = filter(mod.default); if (!defaultMatch) return; cancel(); - resolve(defaultExport ? m.default : defaultExport); + resolve(defaultExport ? mod.default : mod); }; this.addListener(listener); diff --git a/renderer/src/structs/metaerror.js b/renderer/src/structs/metaerror.js deleted file mode 100644 index 5be833a7..00000000 --- a/renderer/src/structs/metaerror.js +++ /dev/null @@ -1,6 +0,0 @@ -export default class MetaError extends Error { - constructor(message) { - super(message); - this.name = "MetaError"; - } -} \ No newline at end of file