Merge remote-tracking branch 'upstream/main' into addon-store

This commit is contained in:
Tropical 2022-08-31 23:10:18 -05:00
commit 89fa89f797
17 changed files with 219 additions and 196 deletions

View File

@ -4,7 +4,7 @@
"node": true
},
"parserOptions": {
"ecmaVersion": 2020,
"ecmaVersion": 2022,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true

View File

@ -2,6 +2,60 @@
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.3
### Added
### Removed
### Changed
- Plugin startup errors should be more descriptive for developers.
### Fixed
- Fixed an issue where custom css crashed Discord.
- Fixed an issue where `waitForModule` returned a boolean instead of a module.
## 1.6.2
### Added
### Removed
### Changed
### Fixed
- Fixed non-loading issue due to changed UserSettingsStore
## 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
- 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.
### Removed
### Changed
- Addon loading no longer uses `require`
- Addon error modal updated (Thanks Qb)
- Fixed plugin error display on the modal
### Fixed
- Fixed dispatcher changes by Discord
## 1.5.3
### Added

View File

@ -16,6 +16,7 @@ The following is a set of guidelines for contributing to BetterDiscord. These ar
* [Suggesting Enhancements](#suggesting-enhancements)
* [Your First Code Contribution](#your-first-code-contribution)
* [Pull Requests](#pull-requests)
* [Translations](#translations)
[Styleguides](#styleguides)
* [Git Commit Messages](#git-commit-messages)
@ -126,6 +127,13 @@ Please follow these steps to have your contribution considered by the maintainer
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
### Translations
BetterDiscord supports a number of languages thanks to translations provided by the community. Translations for the BetterDiscord project should be submitted via [POEditor](https://poeditor.com/join/project?hash=nRljcnV0ET).
* Do not submit translations generated with a translation such as Google Translate, DeepL, or anything of the sorts,
* Only submit translations for languages you are at the very least fluent in, better yet if it's your first language.
## Styleguides
### Git Commit Messages

View File

@ -44,10 +44,6 @@
"name": "Show Addon Errors",
"note": "Shows a modal with plugin/theme errors"
},
"autoReload": {
"name": "Automatic Loading",
"note": "Automatically loads, reloads, and unloads plugins and themes"
},
"editAction": {
"name": "Edit Action",
"note": "Where plugins & themes appear when editing",

View File

@ -1,6 +1,6 @@
{
"name": "betterdiscord",
"version": "1.5.3",
"version": "1.6.3",
"description": "Enhances Discord by adding functionality and themes.",
"main": "src/index.js",
"scripts": {

View File

@ -1,11 +1,20 @@
// fixed, improved, added, progress
export default {
description: "Improvements coming soon!",
description: "Discord is _still_ making a lot of internal changes!",
changes: [
{
title: "Changes",
type: "improved",
items: [
"Plugin startup errors should be more descriptive for developers.",
]
},
{
title: "Fixes",
type: "fixed",
items: [
"Injection on Canary is fixed!"
"Fixed an issue where custom css crashed Discord.",
"Fixed an issue where `waitForModule` returned a boolean instead of a module.",
]
}
]

View File

@ -20,7 +20,6 @@ export default [
{type: "switch", id: "store", value: true},
{type: "switch", id: "autoEnable", value: false},
{type: "switch", id: "addonErrors", value: true},
{type: "switch", id: "autoReload", value: true},
{type: "dropdown", id: "editAction", value: "detached", options: [{value: "detached"}, {value: "system"}]}
]
},

View File

@ -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";
@ -37,9 +36,6 @@ export default class AddonManager {
get addonFolder() {return "";}
get language() {return "";}
get prefix() {return "addon";}
get collection() {return "settings";}
get category() {return "addons";}
get id() {return "autoReload";}
emit(event, ...args) {return Events.emit(`${this.prefix}-${event}`, ...args);}
constructor() {
@ -50,10 +46,6 @@ export default class AddonManager {
}
initialize() {
Settings.on(this.collection, this.category, this.id, (enabled) => {
if (enabled) this.watchAddons();
else this.unwatchAddons();
});
return this.loadAllAddons();
}
@ -129,21 +121,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;
}
@ -176,12 +168,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;
@ -194,9 +186,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);
@ -328,7 +324,7 @@ export default class AddonManager {
}
this.saveState();
if (Settings.get(this.collection, this.category, this.id)) this.watchAddons();
this.watchAddons();
return errors;
}

View File

@ -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
})
);
});
}
}
};

View File

@ -36,7 +36,8 @@ export default Utilities.memoizeObject({
/* Current User Info, State and Settings */
get UserInfoStore() {return WebpackModules.getByProps("getToken");},
get UserSettingsStore() {return WebpackModules.getByProps("guildPositions");},
get LocaleStore() {return WebpackModules.getByProps("locale", "initialize");},
get ThemeStore() {return WebpackModules.getByProps("theme", "initialize");},
get AccountManager() {return WebpackModules.getByProps("register", "login");},
get UserSettingsUpdater() {return WebpackModules.getByProps("updateRemoteSettings");},
get OnlineWatcher() {return WebpackModules.getByProps("isOnline");},

View File

@ -3,10 +3,10 @@ import DiscordModules from "./discordmodules";
import Utilities from "./utilities";
import Events from "./emitter";
const {Dispatcher, UserSettingsStore} = DiscordModules;
const {Dispatcher, LocaleStore} = DiscordModules;
export default new class LocaleManager {
get discordLocale() {return UserSettingsStore.locale;}
get discordLocale() {return LocaleStore?.locale ?? this.defaultLocale;}
get defaultLocale() {return "en-US";}
constructor() {

View File

@ -135,13 +135,13 @@ BdApi.showConfirmationModal = function (title, content, options = {}) {
};
/**
* This shows a toast similar to android towards the bottom of the screen.
* Shows a toast similar to android towards the bottom of the screen.
*
* @param {string} content The string to show in the toast.
* @param {object} options Options object. Optional parameter.
* @param {string} [options.type=""] Changes the type of the toast stylistically and semantically. Choices: "", "info", "success", "danger"/"error", "warning"/"warn". Default: ""
* @param {boolean} [options.icon=true] Determines whether the icon should show corresponding to the type. A toast without type will always have no icon. Default: true
* @param {number} [options.timeout=3000] Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: 3000
* @param {boolean} [options.icon=true] Determines whether the icon should show corresponding to the type. A toast without type will always have no icon. Default: `true`
* @param {number} [options.timeout=3000] Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: `3000`
* @param {boolean} [options.forceShow=false] Whether to force showing the toast and ignore the bd setting
*/
BdApi.showToast = function(content, options = {}) {
@ -149,24 +149,24 @@ BdApi.showToast = function(content, options = {}) {
};
/**
* Show a notice above discord's chat layer.
* Shows a notice above Discord's chat layer.
*
* @param {string|Node} content Content of the notice
* @param {object} options Options for the notice.
* @param {string} [options.type="info" | "error" | "warning" | "success"] Type for the notice. Will affect the color.
* @param {Array<{label: string, onClick: function}>} [options.buttons] Buttons that should be added next to the notice text.
* @param {number} [options.timeout=10000] Timeout until the notice is closed. Won't fire if it's set to 0;
* @returns {function}
* @returns {function} A callback for closing the notice. Passing `true` as first parameter closes immediately without transitioning out.
*/
BdApi.showNotice = function (content, options = {}) {
return Notices.show(content, options);
};
/**
* Finds a webpack module using a filter
* Finds a webpack module using a filter.
*
* @deprecated
* @param {function} filter A filter given the exports, module, and moduleId. Returns true if the module matches.
* @param {function} filter A filter given the exports, module, and moduleId. Returns `true` if the module matches.
* @returns {any} Either the matching module or `undefined`
*/
BdApi.findModule = function(filter) {
@ -174,10 +174,10 @@ BdApi.findModule = function(filter) {
};
/**
* Finds multple webpack modules using a filter
* Finds multiple webpack modules using a filter.
*
* @deprecated
* @param {function} filter A filter given the exports, module, and moduleId. Returns true if the module matches.
* @param {function} filter A filter given the exports, module, and moduleId. Returns `true` if the module matches.
* @returns {Array} Either an array of matching modules or an empty array
*/
BdApi.findAllModules = function(filter) {
@ -185,7 +185,7 @@ BdApi.findAllModules = function(filter) {
};
/**
* Finds a webpack module by own properties
* Finds a webpack module by own properties.
*
* @deprecated
* @param {...string} props Any desired properties
@ -197,7 +197,7 @@ BdApi.findModuleByProps = function(...props) {
/**
* Finds a webpack module by own prototypes
* Finds a webpack module by own prototypes.
*
* @deprecated
* @param {...string} protos Any desired prototype properties
@ -208,10 +208,10 @@ BdApi.findModuleByPrototypes = function(...protos) {
};
/**
* Finds a webpack module by displayName property
* Finds a webpack module by `displayName` property.
*
* @deprecated
* @param {string} name Desired displayName property
* @param {string} name Desired `displayName` property
* @returns {any} Either the matching module or `undefined`
*/
BdApi.findModuleByDisplayName = function(name) {
@ -219,7 +219,7 @@ BdApi.findModuleByDisplayName = function(name) {
};
/**
* Get the internal react data of a specified node
* Gets the internal react data of a specified node.
*
* @param {HTMLElement} node Node to get the react data from
* @returns {object|undefined} Either the found data or `undefined`
@ -268,7 +268,7 @@ BdApi.deleteData = function(pluginName, key) {
};
/**
* This function monkey-patches a method on an object. The patching callback may be run before, after or instead of target method.
* Monkey-patches a method on an object. The patching callback may be run before, after or instead of target method.
*
* - Be careful when monkey-patching. Think not only about original functionality of target method and your changes, but also about developers of other plugins, who may also patch this method before or after you. Try to change target method behaviour as little as possible, and avoid changing method signatures.
* - Display name of patched method is changed, so you can see if a function has been patched (and how many times) while debugging or in the stack trace. Also, patched methods have property `__monkeyPatched` set to `true`, in case you want to check something programmatically.
@ -343,7 +343,7 @@ BdApi.testJSON = function(data) {
};
/**
* Gets a specific setting's status from BD
* Gets a specific setting's status from BD.
*
* @deprecated
* @param {string} [collection="settings"] Collection ID
@ -356,7 +356,7 @@ BdApi.isSettingEnabled = function(collection, category, id) {
};
/**
* Enable a BetterDiscord setting by ids.
* Enables a BetterDiscord setting by ids.
*
* @deprecated
* @param {string} [collection="settings"] Collection ID
@ -380,7 +380,7 @@ BdApi.disableSetting = function(collection, category, id) {
};
/**
* Toggle a BetterDiscord setting by ids.
* Toggles a BetterDiscord setting by ids.
*
* @deprecated
* @param {string} [collection="settings"] Collection ID
@ -395,7 +395,7 @@ BdApi.toggleSetting = function(collection, category, id) {
* Gets some data in BetterDiscord's misc data.
*
* @deprecated
* @param {string} key Key of the data to load.
* @param {string} key Key of the data to load
* @returns {any} The stored data
*/
BdApi.getBDData = function(key) {
@ -403,10 +403,10 @@ BdApi.getBDData = function(key) {
};
/**
* Gets some data in BetterDiscord's misc data.
* Sets some data in BetterDiscord's misc data.
*
* @deprecated
* @param {string} key Key of the data to load.
* @param {string} key Key of the data to store
* @returns {any} The stored data
*/
BdApi.setBDData = function(key, data) {
@ -443,57 +443,59 @@ BdApi.openDialog = async function (options) {
* `AddonAPI` is a utility class for working with plugins and themes. Instances are accessible through the {@link BdApi}.
*/
class AddonAPI {
constructor(manager) {this.manager = manager;}
#manager;
constructor(manager) {this.#manager = manager;}
/**
* The path to the addon folder.
* @type string
*/
get folder() {return this.manager.addonFolder;}
get folder() {return this.#manager.addonFolder;}
/**
* Determines if a particular adon is enabled.
* @param {string} idOrFile Addon id or filename.
* @returns {boolean}
*/
isEnabled(idOrFile) {return this.manager.isEnabled(idOrFile);}
isEnabled(idOrFile) {return this.#manager.isEnabled(idOrFile);}
/**
* Enables the given addon.
* @param {string} idOrFile Addon id or filename.
*/
enable(idOrAddon) {return this.manager.enableAddon(idOrAddon);}
enable(idOrAddon) {return this.#manager.enableAddon(idOrAddon);}
/**
* Disables the given addon.
* @param {string} idOrFile Addon id or filename.
*/
disable(idOrAddon) {return this.manager.disableAddon(idOrAddon);}
disable(idOrAddon) {return this.#manager.disableAddon(idOrAddon);}
/**
* Toggles if a particular addon is enabled.
* @param {string} idOrFile Addon id or filename.
*/
toggle(idOrAddon) {return this.manager.toggleAddon(idOrAddon);}
toggle(idOrAddon) {return this.#manager.toggleAddon(idOrAddon);}
/**
* Reloads if a particular addon is enabled.
* @param {string} idOrFile Addon id or filename.
*/
reload(idOrFileOrAddon) {return this.manager.reloadAddon(idOrFileOrAddon);}
reload(idOrFileOrAddon) {return this.#manager.reloadAddon(idOrFileOrAddon);}
/**
* Gets a particular addon.
* @param {string} idOrFile Addon id or filename.
* @returns {object} Addon instance
*/
get(idOrFile) {return this.manager.getAddon(idOrFile);}
get(idOrFile) {return this.#manager.getAddon(idOrFile);}
/**
* Gets all addons of this type.
* @returns {Array<object>} Array of all addon instances
*/
getAll() {return this.manager.addonList.map(a => this.manager.getAddon(a.id));}
getAll() {return this.#manager.addonList.map(a => this.#manager.getAddon(a.id));}
}
/**
@ -516,25 +518,9 @@ BdApi.Themes = new AddonAPI(ThemeManager);
* @summary {@link Patcher} is a utility class for modifying existing functions.
*/
BdApi.Patcher = {
/**
* This function creates a version of itself that binds all `caller` parameters to your ID.
* @param {string} id ID to use for all subsequent calls
* @returns {Patcher} An instance of this patcher with all functions bound to your ID
*/
bind(id) {
return {
patch: BdApi.Patcher.patch.bind(BdApi.Patcher, id),
before: BdApi.Patcher.before.bind(BdApi.Patcher, id),
instead: BdApi.Patcher.instead.bind(BdApi.Patcher, id),
after: BdApi.Patcher.after.bind(BdApi.Patcher, id),
getPatchesByCaller: BdApi.Patcher.getPatchesByCaller.bind(BdApi.Patcher, id),
unpatchAll: BdApi.Patcher.unpatchAll.bind(BdApi.Patcher, id),
};
},
/**
* This method patches onto another function, allowing your code to run beforehand.
* Using this, you are also able to modify the incoming arguments before the original method is run.
* Using this, you are able to modify the incoming arguments before the original method is run.
* @param {string} caller Name of the caller of the patch function.
* @param {object} moduleToPatch Object with the function to be patched. Can also be an object's prototype.
* @param {string} functionName Name of the function to be patched.
@ -547,7 +533,7 @@ BdApi.Patcher = {
/**
* This method patches onto another function, allowing your code to run instead.
* Using this, you are also able to modify the return value, using the return of your code instead.
* Using this, you are able to replace the original completely. You can still call the original manually if needed.
* @param {string} caller Name of the caller of the patch function.
* @param {object} moduleToPatch Object with the function to be patched. Can also be an object's prototype.
* @param {string} functionName Name of the function to be patched.
@ -559,8 +545,8 @@ BdApi.Patcher = {
},
/**
* This method patches onto another function, allowing your code to run instead.
* Using this, you are also able to modify the return value, using the return of your code instead.
* This method patches onto another function, allowing your code to run afterwards.
* Using this, you are able to modify the return value after the original method is run.
* @param {string} caller Name of the caller of the patch function.
* @param {object} moduleToPatch Object with the function to be patched. Can also be an object's prototype.
* @param {string} functionName Name of the function to be patched.
@ -634,9 +620,9 @@ BdApi.Webpack = {
byStrings(...strings) {return Filters.byStrings(strings);},
/**
* Generates a function that filters by a set of properties.
* Generates a function that filters by the `displayName` property.
* @param {string} name Name the module should have
* @returns {function} A filter that checks for a set of properties
* @returns {function} A filter that checks for a `displayName` match
*/
byDisplayName(name) {return Filters.byDisplayName(name);},
@ -650,7 +636,7 @@ BdApi.Webpack = {
/**
* Finds a module using a filter function.
* @param {function} filter A function to use to filter modules. It is given exports, module, and moduleID. Return true to signify match.
* @param {function} filter A function to use to filter modules. It is given exports, module, and moduleID. Return `true` to signify match.
* @param {object} [options] Whether to return only the first matching module
* @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
@ -674,8 +660,8 @@ BdApi.Webpack = {
getBulk(...queries) {return WebpackModules.getBulk(...queries);},
/**
* Finds a module that lazily loaded.
* @param {function} filter A function to use to filter modules. It is given exports. Return true to signify match.
* Finds a module that is lazily loaded.
* @param {function} filter A function to use to filter modules. It is given exports. Return `true` to signify match.
* @param {object} [options] Whether to return only the first matching module
* @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

View File

@ -129,7 +129,7 @@ export default new class PluginManager extends AddonManager {
return addon;
}
catch (err) {
return new AddonError(addon.name || addon.filename, module.filename, "Plugin could not be compiled", {message: err.message, stack: err.stack}, this.prefix);
throw new AddonError(addon.name || addon.filename, module.filename, "Plugin could not be compiled", {message: err.message, stack: err.stack}, this.prefix);
}
}

View File

@ -1,7 +1,5 @@
import {Config} from "data";
import Logger from "common/logger";
import DOM from "./domtools";
import ClassName from "../structs/classname";
export default class Utilities {
@ -9,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, "\\$&");
}
@ -164,25 +132,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,

View File

@ -132,6 +132,8 @@ const protect = theModule => {
return proxy;
};
const hasThrown = new WeakSet();
export default class WebpackModules {
static find(filter, first = true) {return this.getModule(filter, {first});}
static findAll(filter) {return this.getModule(filter, {first: false});}
@ -140,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}
@ -153,7 +155,8 @@ export default class WebpackModules {
return filter(exports, module, moduleId);
}
catch (err) {
Logger.warn("WebpackModules~getModule", "Module filter threw an exception.", filter, err);
if (!hasThrown.has(filter)) Logger.warn("WebpackModules~getModule", "Module filter threw an exception.", filter, err);
hasThrown.add(filter);
return false;
}
};
@ -209,7 +212,8 @@ export default class WebpackModules {
return filter(ex, mod, moduleId);
}
catch (err) {
Logger.warn("WebpackModules~getModule", "Module filter threw an exception.", filter, err);
if (!hasThrown.has(filter)) Logger.warn("WebpackModules~getBulk", "Module filter threw an exception.", filter, err);
hasThrown.add(filter);
return false;
}
};
@ -308,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<any>}
@ -316,25 +320,33 @@ 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);
const wrappedFilter = (exports) => {
try {
return filter(exports);
}
catch (err) {
if (!hasThrown.has(filter)) Logger.warn("WebpackModules~getModule", "Module filter threw an exception.", filter, err);
hasThrown.add(filter);
return false;
}
};
return new Promise((resolve) => {
const cancel = () => {this.removeListener(listener);};
const listener = function (m) {
const directMatch = filter(m);
const cancel = () => this.removeListener(listener);
const listener = function(exports) {
if (!exports) return;
let foundModule = null;
if (exports.__esModule && exports.default && wrappedFilter(exports.default)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports)) foundModule = exports;
if (!foundModule) return;
if (directMatch) {
cancel();
return resolve(directMatch);
}
const defaultMatch = filter(m.default);
if (!defaultMatch) return;
cancel();
resolve(defaultExport ? m.default : defaultExport);
resolve(protect(foundModule));
};
this.addListener(listener);

View File

@ -1,6 +0,0 @@
export default class MetaError extends Error {
constructor(message) {
super(message);
this.name = "MetaError";
}
}

View File

@ -3,6 +3,7 @@ import {React, WebpackModules, DiscordModules, Settings} from "modules";
import Checkbox from "./checkbox";
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
const ThemeStore = DiscordModules.ThemeStore;
const languages = ["abap", "abc", "actionscript", "ada", "apache_conf", "asciidoc", "assembly_x86", "autohotkey", "batchfile", "bro", "c_cpp", "c9search", "cirru", "clojure", "cobol", "coffee", "coldfusion", "csharp", "csound_document", "csound_orchestra", "csound_score", "css", "curly", "d", "dart", "diff", "dockerfile", "dot", "drools", "dummy", "dummysyntax", "eiffel", "ejs", "elixir", "elm", "erlang", "forth", "fortran", "ftl", "gcode", "gherkin", "gitignore", "glsl", "gobstones", "golang", "graphqlschema", "groovy", "haml", "handlebars", "haskell", "haskell_cabal", "haxe", "hjson", "html", "html_elixir", "html_ruby", "ini", "io", "jack", "jade", "java", "javascript", "json", "jsoniq", "jsp", "jssm", "jsx", "julia", "kotlin", "latex", "less", "liquid", "lisp", "livescript", "logiql", "lsl", "lua", "luapage", "lucene", "makefile", "markdown", "mask", "matlab", "maze", "mel", "mushcode", "mysql", "nix", "nsis", "objectivec", "ocaml", "pascal", "perl", "pgsql", "php", "pig", "powershell", "praat", "prolog", "properties", "protobuf", "python", "r", "razor", "rdoc", "red", "rhtml", "rst", "ruby", "rust", "sass", "scad", "scala", "scheme", "scss", "sh", "sjs", "smarty", "snippets", "soy_template", "space", "sql", "sqlserver", "stylus", "svg", "swift", "tcl", "tex", "text", "textile", "toml", "tsx", "twig", "typescript", "vala", "vbscript", "velocity", "verilog", "vhdl", "wollok", "xml", "xquery", "yaml", "django"];
@ -12,7 +13,7 @@ export default class CodeEditor extends React.Component {
constructor(props) {
super(props);
this.props.theme = DiscordModules.UserSettingsStore && DiscordModules.UserSettingsStore.theme === "light" ? "vs" : "vs-dark";
this.props.theme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
this.props.language = this.props.language.toLowerCase().replace(/ /g, "_");
if (!languages.includes(this.props.language)) this.props.language = CodeEditor.defaultProps.language;
@ -36,7 +37,7 @@ export default class CodeEditor extends React.Component {
this.editor = window.monaco.editor.create(document.getElementById(this.props.id), {
value: this.props.value,
language: this.props.language,
theme: DiscordModules.UserSettingsStore.theme == "light" ? "vs" : "vs-dark",
theme: ThemeStore?.theme == "light" ? "vs" : "vs-dark",
fontSize: Settings.get("settings", "editor", "fontSize"),
lineNumbers: Settings.get("settings", "editor", "lineNumbers"),
minimap: {enabled: Settings.get("settings", "editor", "minimap")},
@ -69,19 +70,19 @@ export default class CodeEditor extends React.Component {
document.getElementById(this.props.id).appendChild(textarea);
}
if (DiscordModules.UserSettingsStore) DiscordModules.UserSettingsStore.addChangeListener(this.onThemeChange);
ThemeStore?.addChangeListener?.(this.onThemeChange);
window.addEventListener("resize", this.resize);
}
componentWillUnmount() {
window.removeEventListener("resize", this.resize);
if (DiscordModules.UserSettingsStore) DiscordModules.UserSettingsStore.removeChangeListener(this.onThemeChange);
ThemeStore?.removeChangeListener?.(this.onThemeChange);
for (const binding of this.bindings) binding.dispose();
this.editor.dispose();
}
onThemeChange() {
const newTheme = DiscordModules.UserSettingsStore.theme === "light" ? "vs" : "vs-dark";
const newTheme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
if (newTheme === this.props.theme) return;
this.props.theme = newTheme;
if (window.monaco?.editor) window.monaco.editor.setTheme(this.props.theme);