Merge pull request #1408 from BetterDiscord/electron17

Prepare for electron 17 and patch for swc
This commit is contained in:
Zack 2022-09-26 23:39:31 -04:00 committed by GitHub
commit b2aae545c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 6041 additions and 17045 deletions

View File

@ -1,7 +1,8 @@
{
"extends": "eslint:recommended",
"env": {
"node": true
"node": true,
"es2020": true
},
"parserOptions": {
"ecmaVersion": 2022,
@ -82,15 +83,7 @@
"yoda": "error"
},
"globals": {
"Proxy": "readonly",
"Set": "readonly",
"WeakMap": "readonly",
"WeakSet": "readonly",
"Map": "readonly",
"Promise": "readonly",
"Reflect": "readonly",
"DiscordNative": "readonly",
"__non_webpack_require__": "readonly",
"Symbol": "readonly"
"__non_webpack_require__": "readonly"
}
}

View File

@ -40,7 +40,7 @@ For normal users, installing via the installers makes the most sense. However wh
### Prerequisites
- [Git](https://git-scm.com)
- [Node.js](https://nodejs.org/en/) with `npm`.
- [Node.js](https://nodejs.org/en/) with [pnpm](https://pnpm.io/).
- Command line of your choice.
### 1: Clone the repository
@ -49,25 +49,25 @@ git clone https://github.com/BetterDiscord/BetterDiscord.git
```
### 2: Install dependencies
```ps
npm install
pnpm recursive install
```
### 3: Run Build Script
This will create a `injector.js`, `preload.js`, and `renderer.js` in the `dist` folder.
```ps
npm run build
pnpm run build
```
### 4: Inject into your Discord client
#### Install to Stable
```ps
npm run inject
pnpm run inject
```
#### Install to PTB
```ps
npm run inject ptb
pnpm run inject ptb
```
#### Install to Canary
```ps
npm run inject canary
pnpm run inject canary
```
## Additional Scripts
@ -75,7 +75,7 @@ npm run inject canary
### Compiling & Distribution
This will create a `betterdiscord.asar` file in the `dist` folder.
```ps
npm run dist
pnpm run dist
```
---

19
common/clone.js Normal file
View File

@ -0,0 +1,19 @@
export function getKeys(object) {
const keys = [];
for (const key in object) keys.push(key);
return keys;
}
export default function cloneObject(target, newObject = {}, keys) {
if (!Array.isArray(keys)) keys = getKeys(target);
return keys.reduce((clone, key) => {
if (typeof(target[key]) === "object" && !Array.isArray(target[key]) && target[key] !== null) clone[key] = cloneObject(target[key], {});
else if (typeof target[key] === "function") clone[key] = target[key].bind(target);
else clone[key] = target[key];
return clone;
}, newObject);
}

36
common/events.js Normal file
View File

@ -0,0 +1,36 @@
import Logger from "./logger";
export default class EventEmitter {
static get EventEmitter() {return EventEmitter;}
constructor() {
this.events = {};
}
setMaxListeners() {}
on(event, callback) {
if (!this.events[event]) this.events[event] = new Set();
this.events[event].add(callback);
}
emit(event, ...args) {
if (!this.events[event]) return;
for (const [index, listener] of this.events[event].entries()) {
try {
listener(...args);
}
catch (error) {
Logger.error("Emitter", `Cannot fire listener for event ${event} at position ${index}:`, error);
}
}
}
off(event, callback) {
if (!this.events[event]) return;
return this.events[event].delete(callback);
}
}

View File

@ -1,3 +1,3 @@
# BetterDiscord Injector
You're probably looking for the main app, [click here](https://github.com/rauenzi/BetterDiscordApp) to go there.
You're probably looking for the main app, [click here](https://github.com/BetterDiscord/BetterDiscord) to go there.

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "betterdiscord-injector",
"name": "@betterdiscord/injector",
"version": "0.6.2",
"description": "BetterDiscord injector module",
"main": "src/index.js",
@ -11,10 +11,6 @@
"lint": "eslint --ext .js src/"
},
"devDependencies": {
"circular-dependency-plugin": "^5.2.2",
"copy-webpack-plugin": "^8.0.0",
"eslint": "^7.21.0",
"webpack": "^5.24.2",
"webpack-cli": "^4.5.0"
"webpack": "^5.73.0"
}
}

View File

@ -65,7 +65,8 @@ export default class BetterDiscord {
try {
${content}
return true;
} catch {
} catch(error) {
console.error(error);
return false;
}
})();

View File

@ -1,59 +0,0 @@
const Module = require("module");
const path = require("path");
const electron = require("electron");
const NodeEvents = require("events");
const cloneObject = function (target, newObject = {}, keys) {
if (!Array.isArray(keys)) keys = Object.keys(Object.getOwnPropertyDescriptors(target));
return keys.reduce((clone, key) => {
if (typeof(target[key]) === "object" && !Array.isArray(target[key]) && target[key] !== null && !(target[key] instanceof NodeEvents)) clone[key] = cloneObject(target[key], {});
else clone[key] = target[key];
return clone;
}, newObject);
};
/* global window:false */
// const context = electron.webFrame.top.context;
Object.defineProperty(window, "webpackJsonp", {
get: () => electron.webFrame.top.context.webpackJsonp
});
electron.webFrame.top.context.global = electron.webFrame.top.context;
electron.webFrame.top.context.require = require;
electron.webFrame.top.context.Buffer = Buffer;
electron.webFrame.top.context.process = new class PatchedProcess extends NodeEvents {
get __ORIGINAL_PROCESS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__() {return process;}
constructor() {
super();
Object.assign(this,
cloneObject(process, {}, Object.keys(NodeEvents.prototype)),
cloneObject(process, {})
);
}
};
// Load Discord's original preload
const preload = process.env.DISCORD_PRELOAD;
if (preload) {
// Restore original preload for future windows
electron.ipcRenderer.send("bd-register-preload", preload);
// Run original preload
try {
const originalKill = process.kill;
process.kill = function() {};
require(preload);
process.kill = originalKill;
}
catch (e) {
// TODO bail out
}
}
Module.globalPaths.push(path.resolve(process.env.DISCORD_APP_PATH, "..", "app.asar", "node_modules"));

View File

@ -1,6 +1,4 @@
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const CircularDependencyPlugin = require("circular-dependency-plugin");
const TerserPlugin = require("terser-webpack-plugin");
module.exports = (env, argv) => ({
@ -29,20 +27,6 @@ module.exports = (env, argv) => ({
common: path.resolve(__dirname, "..", "common")
}
},
plugins: [
new CircularDependencyPlugin({
exclude: /node_modules/,
cwd: process.cwd(),
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "src", "preload.js"),
to: path.resolve(__dirname, "..", "dist", "preload.js")
},
],
})
],
optimization: {
minimizer: [
new TerserPlugin({

2060
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,31 @@
{
"name": "betterdiscord",
"version": "1.6.3",
"version": "1.7.0",
"description": "Enhances Discord by adding functionality and themes.",
"main": "src/index.js",
"scripts": {
"install": "cd injector && npm install && cd ../renderer && npm install",
"build": "npm run build-injector && npm run build-renderer",
"build-prod": "npm run build-prod --prefix injector && npm run build-prod --prefix renderer",
"build-injector": "npm run build --prefix injector",
"build-renderer": "npm run build --prefix renderer",
"build": "pnpm run build-injector && pnpm run build-preload && pnpm run build-renderer",
"build-prod": "pnpm --filter injector build-prod && pnpm --filter preload build-prod && pnpm --filter renderer build-prod",
"build-injector": "pnpm --filter injector build",
"build-renderer": "pnpm --filter renderer build",
"build-preload": "pnpm --filter preload build",
"pack-emotes": "node scripts/emotes.js",
"inject": "node scripts/inject.js",
"lint": "eslint --ext .js common/ && npm run lint --prefix injector && npm run lint --prefix renderer",
"lint": "eslint --ext .js common/ && pnpm --filter injector lint && pnpm --filter preload lint && pnpm --filter renderer lint-js",
"test": "mocha --require @babel/register --recursive \"./tests/renderer/*.js\"",
"dist": "npm run build-prod && node scripts/pack.js",
"dist": "pnpm run build-prod && node scripts/pack.js",
"api": "jsdoc -X renderer/src/modules/pluginapi.js > jsdoc-ast.json"
},
"devDependencies": {
"asar": "^3.0.3",
"eslint": "^7.12.0",
"eslint-plugin-react": "^7.21.5",
"jsdoc": "^3.6.11",
"mocha": "^10.0.0"
"asar": "^3.2.0",
"eslint": "^8.23.0",
"eslint-plugin-react": "^7.31.6",
"mocha": "^10.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
},
"engines": {
"node": ">=14",
"pnpm": ">=7"
}
}

4862
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,4 @@
packages:
- "injector/"
- "preload/"
- "renderer/"

3
preload/README.md Normal file
View File

@ -0,0 +1,3 @@
# BetterDiscord Preload
You're probably looking for the main app, [click here](https://github.com/BetterDiscord/BetterDiscord) to go there.

11
preload/jsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2020",
"allowSyntheticDefaultImports": false,
"baseUrl": "./",
"paths": {
"common": ["../common"]
}
},
"exclude": ["node_modules"]
}

16
preload/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "@betterdiscord/preload",
"version": "0.0.1",
"description": "BetterDiscord preload module",
"main": "src/index.js",
"private": true,
"scripts": {
"build": "webpack --progress --color",
"watch": "webpack --progress --color --watch",
"build-prod": "webpack --stats minimal --mode production",
"lint": "eslint --ext .js src/"
},
"devDependencies": {
"webpack": "^5.73.0"
}
}

28
preload/src/api/crypto.js Normal file
View File

@ -0,0 +1,28 @@
const crypto = (() => {
let cache = null;
return () => {
if (cache) return cache;
return cache = __non_webpack_require__("crypto");
};
})();
export function createHash(type) {
const hash = crypto().createHash(type);
const ctx = {
update(data) {
hash.update(data);
return ctx;
},
digest(encoding) {return hash.digest(encoding);}
};
return ctx;
}
export function randomBytes(length) {
return crypto().randomBytes(length);
}

View File

@ -0,0 +1,13 @@
import {ipcRenderer as IPC, shell} from "electron";
export const ipcRenderer = {
send: IPC.send.bind(IPC),
sendToHost: IPC.sendToHost.bind(IPC),
sendTo: IPC.sendTo.bind(IPC),
sendSync: IPC.sendSync.bind(IPC),
invoke: IPC.invoke.bind(IPC),
on: IPC.on.bind(IPC),
off: IPC.off.bind(IPC)
};
export {shell};

View File

@ -0,0 +1,77 @@
import * as fs from "fs";
import cloneObject from "common/clone";
import Logger from "common/logger";
export function readFile(path, options = "utf8") {
return fs.readFileSync(path, options);
}
export function writeFile(path, content, options) {
if (content instanceof Uint8Array) {
content = Buffer.from(content);
}
const doWriteFile = options?.originalFs ? __non_webpack_require__("original-fs").writeFileSync : fs.writeFileSync;
return doWriteFile(path, content, options);
}
export function readDirectory(path, options) {
return fs.readdirSync(path, options);
}
export function createDirectory(path, options) {
return fs.mkdirSync(path, options);
}
export function deleteDirectory(path, options) {
fs.rmdirSync(path, options);
}
export function exists(path) {
return fs.existsSync(path);
}
export function getRealPath(path, options) {
return fs.realpathSync(path, options);
}
export function rename(oldPath, newPath) {
return fs.renameSync(oldPath, newPath);
}
export function unlinkSync(fileToDelete) {
return fs.unlinkSync(fileToDelete);
}
export function createWriteStream(path, options) {
return cloneObject(fs.createWriteStream(path, options));
}
export function watch(path, options, callback) {
const watcher = fs.watch(path, options, (event, filename) => {
try {
callback(event, filename);
}
catch (error) {
Logger.stacktrace("filesystem", "Failed to watch path", error);
}
});
return {
close: () => {
watcher.close();
}
};
}
export function getStats(path, options) {
const stats = fs.statSync(path, options);
return {
...stats,
isFile: stats.isFile.bind(stats),
isDirectory: stats.isDirectory.bind(stats),
isSymbolicLink: stats.isSymbolicLink.bind(stats)
};
}

59
preload/src/api/https.js Normal file
View File

@ -0,0 +1,59 @@
import * as https from "https";
const methods = ["get", "put", "post", "delete"];
const headersToClone = ["statusCode", "statusMessage", "url", "headers", "method", "aborted", "complete", "rawHeaders", "end"];
const request = function (url, options, callback) {
let responseObject = undefined;
let pipe = undefined;
const req = https.request(url, Object.assign({method: "GET"}, options), res => {
const chunks = [];
let error = null;
responseObject = res;
if (pipe) {
res.pipe(pipe);
}
res.addListener("error", err => {error = err;});
res.addListener("data", chunk => {
chunks.push(chunk);
});
res.addListener("end", () => {
const headers = Object.fromEntries(headersToClone.map(h => [h, res[h]]));
callback(error, headers, Buffer.concat(chunks));
req.end();
});
});
req.end();
return {
end() {req.end();},
pipe(fsStream) {
if (!responseObject) {
pipe = fsStream;
} else {
responseObject.pipe(fsStream);
}
}
};
};
export default Object.assign({request},
Object.fromEntries(methods.map(method => [
method,
function () {
arguments[1] ??= {};
arguments[1].method ??= method.toUpperCase();
return Reflect.apply(request, this, arguments);
}
]))
);

44
preload/src/api/index.js Normal file
View File

@ -0,0 +1,44 @@
import fs from "fs";
import path from "path";
import Module from "module";
// const Module = require("module");
Module.globalPaths.push(path.resolve(process.env.DISCORD_APP_PATH, "..", "app.asar", "node_modules"));
// module.paths.push(path.resolve(process.env.DISCORD_APP_PATH, "..", "app.asar", "node_modules"));
Module._load = (load => (req, parent, isMain) => {
if (req.includes("./") || req.includes("..")) return load(req, parent, isMain);
const found = Module.globalPaths.find(m => fs.existsSync(path.resolve(m, req)));
return found ? load(path.resolve(found, req), parent, isMain) : load(req, parent, isMain);
})(Module._load);
// const originalLoad = Module.prototype.load;
// Module.prototype.load = function() {
// const returnValue = Reflect.apply(originalLoad, this, arguments);
// console.log(this, arguments, returnValue);
// return returnValue;
// };
// const nodeModulePaths = Module._nodeModulePaths;
// console.log(nodeModulePaths);
// Module._nodeModulePaths = (from) => {
// return nodeModulePaths(from).concat([path.resolve(process.env.DISCORD_APP_PATH, "..", "app.asar", "node_modules")]);
// };
// console.log(Module._nodeModulePaths, Module._nodeModulePaths("request"));
// console.dir(Module);
// console.log(Object.keys(Module));
// console.log(require("request"));
export * as filesystem from "./filesystem";
export {default as https} from "./https";
export * as electron from "./electron";
export * as crypto from "./crypto";
export * as vm from "./vm";
// We can expose that without any issues.
export * as path from "path";
export * as net from "net"; // TODO: evaluate need and create wrapper
export * as os from "os";

14
preload/src/api/vm.js Normal file
View File

@ -0,0 +1,14 @@
import vm from "vm";
export function compileFunction(code, params = [], options = {}) {
try {
return vm.compileFunction(code, params, options);
}
catch (error) {
return {
name: error.name,
message: error.message,
stack: error.stack
};
}
}

15
preload/src/index.js Normal file
View File

@ -0,0 +1,15 @@
import {contextBridge} from "electron";
import newProcess from "./process";
import * as BdApi from "./api";
import init from "./init";
let hasInitialized = false;
contextBridge.exposeInMainWorld("BetterDiscord", BdApi);
contextBridge.exposeInMainWorld("process", newProcess);
contextBridge.exposeInMainWorld("BetterDiscordPreload", () => {
if (hasInitialized) return null;
hasInitialized = true;
return BdApi;
});
init();

22
preload/src/init.js Normal file
View File

@ -0,0 +1,22 @@
import {ipcRenderer as IPC} from "electron";
import * as IPCEvents from "common/constants/ipcevents";
export default function() {
// Load Discord's original preload
const preload = process.env.DISCORD_PRELOAD;
if (preload) {
// Restore original preload for future windows
IPC.send(IPCEvents.REGISTER_PRELOAD, preload);
// Run original preload
try {
const originalKill = process.kill;
process.kill = function() {};
__non_webpack_require__(preload);
process.kill = originalKill;
}
catch (e) {
// TODO bail out
}
}
}

3
preload/src/process.js Normal file
View File

@ -0,0 +1,3 @@
import cloneObject, {getKeys} from "common/clone";
export default cloneObject(process, {}, getKeys(process).filter(p => p !== "config"));

42
preload/webpack.config.js Normal file
View File

@ -0,0 +1,42 @@
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
module.exports = (env, argv) => ({
mode: "development",
target: "node",
devtool: argv.mode === "production" ? undefined : "eval-source-map",
entry: "./src/index.js",
output: {
filename: "preload.js",
path: path.resolve(__dirname, "..", "dist")
},
externals: {
electron: `require("electron")`,
fs: `require("fs")`,
path: `require("path")`,
request: `require("request")`,
events: `require("events")`,
rimraf: `require("rimraf")`,
yauzl: `require("yauzl")`,
mkdirp: `require("mkdirp")`,
module: `require("module")`,
os: `require("os")`,
net: `require("net")`
},
resolve: {
extensions: [".js"],
alias: {
common: path.resolve(__dirname, "..", "common")
}
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {drop_debugger: false},
keep_classnames: true
}
})
]
}
});

12637
renderer/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "betterdiscord-renderer",
"name": "@betterdiscord/renderer",
"description": "Renderer portion of the BetterDiscord application.",
"private": true,
"main": "src/index.js",
@ -24,16 +24,13 @@
"babel-plugin-module-resolver": "^4.1.0",
"circular-dependency-plugin": "^5.2.2",
"css-loader": "^6.5.1",
"eslint": "^8.8.0",
"eslint-plugin-react": "^7.28.0",
"postcss": "^8.4.5",
"postcss-cli": "^9.1.0",
"postcss-csso": "^6.0.0",
"postcss-easy-import": "^4.0.0",
"postcss-loader": "^6.2.1",
"stylelint": "^14.3.0",
"stylelint-config-standard": "^24.0.0",
"webpack": "^5.67.0",
"webpack-cli": "^4.9.2"
"webpack": "^5.73.0",
"stylelint-config-standard": "^24.0.0"
}
}

View File

@ -6,8 +6,8 @@ export {default as PublicServers} from "./general/publicservers";
export {default as VoiceDisconnect} from "./general/voicedisconnect";
export {default as MediaKeys} from "./general/mediakeys";
export {default as EmoteModule} from "./emotes/emotes";
export {default as EmoteMenu} from "./emotes/emotemenu";
// export {default as EmoteModule} from "./emotes/emotes";
// export {default as EmoteMenu} from "./emotes/emotemenu";
// export {default as EmoteAutocaps} from "./emotes/emoteautocaps";
export {default as DevToolsListener} from "./developer/devtools";

View File

@ -1,8 +1,42 @@
import Builtin from "../../structs/builtin";
import {DiscordModules, WebpackModules, Strings, DOM} from "modules";
import {DiscordModules, WebpackModules, Strings, DOM, React} from "modules";
import PublicServersMenu from "../../ui/publicservers/menu";
import Globe from "../../ui/icons/globe";
const LayerStack = WebpackModules.getByProps("pushLayer");
const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {hasError: false};
}
componentDidCatch() {
this.setState({hasError: true});
}
render() {
if (this.state.hasError) return null;
return this.props.children;
}
}
export default new class PublicServers extends Builtin {
get name() {return "PublicServers";}
@ -10,17 +44,37 @@ export default new class PublicServers extends Builtin {
get id() {return "publicServers";}
enabled() {
this._appendButton();
const ListNavigators = WebpackModules.getByProps("ListNavigatorProvider");
this.after(ListNavigators, "ListNavigatorProvider", (_, __, returnValue) => {
if (returnValue.props.value.id !== "guildsnav") return;
this._appendButton();
});
// let target = null;
// WebpackModules.getModule((_, m) => {
// if (m.exports?.toString().includes("privateChannelIds")) {
// target = m.exports;
// }
// });
// if (!target || !target.Z) return;
// const PrivateChannelListComponents = WebpackModules.getByProps("LinkButton");
// this.after(target, "Z", (_, __, returnValue) => {
// const destination = returnValue?.props?.children?.props?.children;
// if (!destination || !Array.isArray(destination)) return;
// if (destination.find(b => b?.props?.children?.props?.id === "public-server-button")) return;
// destination.push(
// React.createElement(ErrorBoundary, null,
// React.createElement(PrivateChannelListComponents.LinkButton,
// {
// id: "public-server-button",
// onClick: () => this.openPublicServers(),
// text: "Public Servers",
// icon: () => React.createElement(Globe, {color: "currentColor"})
// }
// )
// )
// );
// });
}
disabled() {
this.unpatchAll();
DOM.query("#bd-pub-li").remove();
// this.unpatchAll();
// DOM.query("#bd-pub-li").remove();
}
async _appendButton() {
@ -36,7 +90,7 @@ export default new class PublicServers extends Builtin {
}
openPublicServers() {
LayerStack.pushLayer(() => DiscordModules.React.createElement(PublicServersMenu, {close: LayerStack.popLayer}));
LayerManager.pushLayer(() => DiscordModules.React.createElement(PublicServersMenu, {close: LayerManager.popLayer}));
}
get button() {

View File

@ -1,20 +1,23 @@
// fixed, improved, added, progress
export default {
description: "Discord is _still_ making a lot of internal changes!",
description: "BetterDiscord is alive! At least... _sorta_.",
changes: [
{
title: "Changes",
title: "Known Issues",
type: "improved",
items: [
"Plugin startup errors should be more descriptive for developers.",
"**Many many plugins are either completely broken or missing functionality.** Please refer to the respective developers for ETAs.",
"The Twitch Emote system is completely broken, and there is no ETA on being fixed.",
"The Public Servers module is also broken with no ETA for a fix.",
]
},
{
title: "Fixes",
title: "Important News!",
type: "fixed",
items: [
"Fixed an issue where custom css crashed Discord.",
"Fixed an issue where `waitForModule` returned a boolean instead of a module.",
"Due to recent and upcoming changes, BetterDiscord is going to go through a rewrite.",
"There is no ETA or timeline for this rewrite.",
"We will continue to try and __maintain__ this version of BetterDiscord without adding new features."
]
}
]

View File

@ -4,7 +4,7 @@ export default [
id: "general",
collapsible: true,
settings: [
{type: "switch", id: "emotes", value: true},
{type: "switch", id: "emotes", value: true, disabled: true},
{type: "switch", id: "publicServers", value: true},
{type: "switch", id: "voiceDisconnect", value: false},
{type: "switch", id: "showToasts", value: true},

View File

@ -1,3 +1,4 @@
import require from "./polyfill"; // eslint-disable-line no-unused-vars
import secure from "./secure";
import patchModuleLoad from "./moduleloader";
import LoadingIcon from "./loadingicon";
@ -8,6 +9,7 @@ import BdApi from "./modules/pluginapi";
secure();
patchModuleLoad();
window.BdApi = BdApi;
window.global = window;
// Add loading icon at the bottom right
LoadingIcon.show();

View File

@ -68,7 +68,9 @@ export default class AddonManager {
if (this.watcher) return Logger.err(this.name, `Already watching ${this.prefix} addons.`);
Logger.log(this.name, `Starting to watch ${this.prefix} addons.`);
this.watcher = fs.watch(this.addonFolder, {persistent: false}, async (eventType, filename) => {
// console.log("watcher", eventType, filename, !eventType || !filename, !filename.endsWith(this.extension));
if (!eventType || !filename) return;
// console.log(eventType, filename)
const absolutePath = path.resolve(this.addonFolder, filename);
if (!filename.endsWith(this.extension)) {
@ -93,10 +95,11 @@ export default class AddonManager {
Logger.err(this.name, `Could not rename file: ${filename} ${newFilename}`, error);
}
}
// console.log("watcher", "before promise");
await new Promise(r => setTimeout(r, 100));
try {
const stats = fs.statSync(absolutePath);
// console.log("watcher", stats);
if (!stats.isFile()) return;
if (!stats || !stats.mtime || !stats.mtime.getTime()) return;
if (typeof(stats.mtime.getTime()) !== "number") return;
@ -106,7 +109,10 @@ export default class AddonManager {
if (eventType == "change") this.reloadAddon(filename, true);
}
catch (err) {
if (err.code !== "ENOENT") return;
// window.watcherError = err;
// console.log("watcher", err);
// console.dir(err);
if (err.code !== "ENOENT" && !err?.message.startsWith("ENOENT")) return;
delete this.timeCache[filename];
this.unloadAddon(filename, true);
}
@ -207,6 +213,7 @@ export default class AddonManager {
unloadAddon(idOrFileOrAddon, shouldToast = true, isReload = false) {
const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon;
// console.log("watcher", "unloadAddon", idOrFileOrAddon, addon);
if (!addon) return false;
if (this.state[addon.id]) isReload ? this.stopAddon(addon) : this.disableAddon(addon);
@ -314,6 +321,7 @@ export default class AddonManager {
deleteAddon(idOrFileOrAddon) {
const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon;
// console.log(path.resolve(this.addonFolder, addon.filename), fs.unlinkSync)
return fs.unlinkSync(path.resolve(this.addonFolder, addon.filename));
}

View File

@ -8,8 +8,8 @@ import BDLogo from "../ui/icons/bdlogo";
import Logger from "common/logger";
const React = DiscordModules.React;
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
const Anchor = WebpackModules.getByDisplayName("Anchor");
const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
const Anchor = WebpackModules.getByProps("Link");
const Developers = [
/* Zerebos#7790 */
@ -21,7 +21,7 @@ const Developers = [
const DeveloperBadge = function DeveloperBadge({type, size = 16}) {
return React.createElement(Tooltip, {color: "primary", position: "top", text: "BetterDiscord Developer"},
props => React.createElement(Anchor, Object.assign({className: `bd-${type}-badge`, href: "https://github.com/BetterDiscord/BetterDiscord", title: "BetterDiscord", target: "_blank"}, props),
props => React.createElement(Anchor.Link, Object.assign({className: `bd-${type}-badge`, href: "https://github.com/BetterDiscord/BetterDiscord", title: "BetterDiscord", target: "_blank"}, props),
React.createElement(BDLogo, {size, className: "bd-logo"})
)
);
@ -33,17 +33,14 @@ export default new class ComponentPatcher {
debug(...message) {return Logger.debug("ComponentPatcher", ...message);}
initialize() {
Utilities.suppressErrors(this.patchSocial.bind(this), "BD Social Patch")();
Utilities.suppressErrors(this.patchGuildPills.bind(this), "BD Guild Pills Patch")();
Utilities.suppressErrors(this.patchGuildListItems.bind(this), "BD Guild List Items Patch")();
Utilities.suppressErrors(this.patchMessageHeader.bind(this), "BD Message Header Patch")();
Utilities.suppressErrors(this.patchMemberList.bind(this), "BD Member List Patch")();
Utilities.suppressErrors(this.patchProfile.bind(this), "BD Profile Badges Patch")();
// Utilities.suppressErrors(this.patchSocial.bind(this), "BD Social Patch")();
// Utilities.suppressErrors(this.patchMemberList.bind(this), "BD Member List Patch")();
// Utilities.suppressErrors(this.patchProfile.bind(this), "BD Profile Badges Patch")();
}
patchSocial() {
if (this.socialPatch) return;
const TabBar = WebpackModules.getByDisplayName("TabBar");
const TabBar = WebpackModules.getByProps("Types", "Looks", "Header");
if (!TabBar) return;
this.socialPatch = Patcher.after("ComponentPatcher", TabBar.prototype, "render", (thisObject, args, returnValue) => {
const children = returnValue.props.children;
@ -76,102 +73,6 @@ export default new class ComponentPatcher {
});
}
patchGuildListItems() {
if (this.guildListItemsPatch) return;
const ListNavigators = WebpackModules.getByProps("ListNavigatorProvider");
const GuildComponent = WebpackModules.find(m => m.type && m.type.toString().includes("guildNode") && m.type.toString().includes("treeitem"));
if (!GuildComponent || typeof(GuildComponent.type) !== "function") return this.warn("Failed to get Guild component.");
if (!ListNavigators || typeof(ListNavigators.ListNavigatorProvider) !== "function") return this.warn("Failed to get ListNavigatorProvider component.");
this.guildListItemsPatch = Patcher.after("ComponentPatcher", GuildComponent, "type", (_, [props], returnValue) => {
if (!returnValue || !returnValue.props) return;
try {
returnValue.props.className += " bd-guild";
if (props.unread) returnValue.props.className += " bd-unread";
if (props.selected) returnValue.props.className += " bd-selected";
if (props.mediaState.audio) returnValue.props.className += " bd-audio";
if (props.mediaState.video) returnValue.props.className += " bd-video";
if (props.badge) returnValue.props.className += " bd-badge";
if (props.animatable) returnValue.props.className += " bd-animatable";
if (props.unavailable) returnValue.props.className += " bd-unavailable";
if (props.mediaState.screenshare) returnValue.props.className += " bd-screenshare";
if (props.mediaState.liveStage) returnValue.props.className += " bd-live-stage";
if (props.muted) returnValue.props.className += " bd-muted";
}
catch (err) {
Logger.error("ComponentPatcher:Guilds", `Error inside BDGuild:`, err);
this.guildListItemsPatch();
}
});
const {useState} = DiscordModules.React;
function useForceUpdate() {
const [, setValue] = useState(false);
return () => setValue(v => !v); // update the state to force render
}
let hasForced = false;
this.cancelForceUpdate = Patcher.after("ComponentPatcher", ListNavigators, "ListNavigatorProvider", (_, __, returnValue) => {
if (returnValue.props.value.id !== "guildsnav") return;
const originalParent = Utilities.findInTree(returnValue, m => m?.props?.className, {walkable: ["children", "props"]});
if (!originalParent) return;
const original = originalParent.type;
originalParent.type = e => {
const forceUpdate = useForceUpdate();
if (!hasForced) {
hasForced = true;
setTimeout(() => {
forceUpdate();
this.cancelForceUpdate();
}, 1);
}
return Reflect.apply(original, null, [e]);
};
});
}
patchGuildPills() {
if (this.guildPillPatch) return;
const guildPill = WebpackModules.find(m => m?.default?.displayName === "AnimatedHalfPill");
if (!guildPill) return;
this.guildPillPatch = Patcher.after("ComponentPatcher", guildPill, "default", (_, args, returnValue) => {
const props = args[0];
if (props.unread) returnValue.props.className += " bd-unread";
if (props.selected) returnValue.props.className += " bd-selected";
if (props.hovered) returnValue.props.className += " bd-hovered";
return returnValue;
});
}
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;
// if (!returnValue?.type) return;
// const orig = returnValue.type;
// returnValue.type = function() {
// const retVal = Reflect.apply(orig, this, arguments);
// 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;
// };
// });
}
async patchMemberList() {
if (this.memberListPatch) return;
const memo = WebpackModules.find(m => m?.type?.toString().includes("useGlobalHasAvatarDecorations"));
@ -198,7 +99,7 @@ export default new class ComponentPatcher {
patchProfile() {
if (this.profilePatch) return;
const UserProfileBadgeLists = WebpackModules.getModule(m => m?.default?.displayName === "UserProfileBadgeList", {first: false});
const UserProfileBadgeLists = WebpackModules.getModule(m => m?.toString()?.includes("PROFILE_USER_BADGES"), {first: false});
for (const UserProfileBadgeList of UserProfileBadgeLists) {
this.profilePatch = Patcher.after("ComponentPatcher", UserProfileBadgeList, "default", (_, [{user}], res) => {
if (Developers.indexOf(user?.id) < 0) return;

View File

@ -3,6 +3,7 @@ import LocaleManager from "./localemanager";
import Logger from "common/logger";
import {Config, Changelog} from "data";
import WebpackModules from "./webpackmodules";
import DOMManager from "./dommanager";
import PluginManager from "./pluginmanager";
import ThemeManager from "./thememanager";
@ -18,7 +19,6 @@ import IPC from "./ipc";
import LoadingIcon from "../loadingicon";
import Styles from "../styles/index.css";
import Editor from "./editor";
import {WebpackModules} from "modules";
export default new class Core {
async startup() {
@ -48,6 +48,7 @@ export default new class Core {
Logger.log("Startup", "Initializing Settings");
Settings.initialize();
// SettingsRenderer.patchSections();
Logger.log("Startup", "Initializing DOMManager");
DOMManager.initialize();
@ -68,6 +69,7 @@ export default new class Core {
for (const module in Builtins) {
Builtins[module].initialize();
}
this.polyfillWebpack();
Logger.log("Startup", "Loading Plugins");
// const pluginErrors = [];
@ -86,9 +88,18 @@ export default new class Core {
const previousVersion = DataStore.getBDData("version");
if (Config.version > previousVersion) {
Modals.showChangelogModal(Changelog);
// Modals.showChangelogModal(Changelog);
const md = [Changelog.description];
for (const type of Changelog.changes) {
md.push(`**${type.title}**`);
for (const entry of type.items) {
md.push(` - ${entry}`);
}
}
Modals.showConfirmationModal(`BetterDiscord v${Config.version}`, md, {cancelText: ""});
DataStore.setBDData("version", Config.version);
}
// SettingsRenderer.patchSections();
}
polyfillWebpack() {

View File

@ -47,7 +47,7 @@ export default Utilities.memoizeObject({
get MentionStore() {return WebpackModules.getByProps("getMentions");},
/* User Stores and Utils */
get UserStore() {return WebpackModules.getByProps("getCurrentUser");},
get UserStore() {return WebpackModules.getByProps("getCurrentUser", "getUser");},
get UserStatusStore() {return WebpackModules.getByProps("getStatus", "getState");},
get UserTypingStore() {return WebpackModules.getByProps("isTyping");},
get UserActivityStore() {return WebpackModules.getByProps("getActivity");},
@ -133,14 +133,14 @@ export default Utilities.memoizeObject({
/* Electron & Other Internals with Utils*/
get ElectronModule() {return WebpackModules.getByProps("setBadge");},
get Dispatcher() {return WebpackModules.getByProps("dispatch", "subscribe");},
get Dispatcher() {return WebpackModules.getByProps("dispatch", "subscribe", "register");},
get PathUtils() {return WebpackModules.getByProps("hasBasename");},
get NotificationModule() {return WebpackModules.getByProps("showNotification");},
get RouterModule() {return WebpackModules.getByProps("Router");},
get APIModule() {return WebpackModules.getByProps("getAPIBaseURL");},
get AnalyticEvents() {return WebpackModules.getByProps("AnalyticEventConfigs");},
get KeyGenerator() {return WebpackModules.getByRegex(/"binary"/);},
get Buffers() {return WebpackModules.getByProps("Buffer", "kMaxLength");},
get Buffers() {return WebpackModules.getByProps("INSPECT_MAX_BYTES", "kMaxLength");},
get DeviceStore() {return WebpackModules.getByProps("getDevices");},
get SoftwareInfo() {return WebpackModules.getByProps("os");},
get CurrentContext() {return WebpackModules.getByProps("setTagsContext");},

View File

@ -16,10 +16,7 @@ export default new class LocaleManager {
initialize() {
this.setLocale(this.discordLocale);
Dispatcher.subscribe("USER_SETTINGS_UPDATE", ({settings}) => {
const newLocale = settings.locale;
if (newLocale && newLocale != this.locale) this.setLocale(newLocale);
});
Dispatcher.subscribe("USER_SETTINGS_UPDATE", (newLocale) => this.setLocale(newLocale));
}
setLocale(newLocale) {

View File

@ -117,7 +117,7 @@ export default new class PluginManager extends AddonManager {
try {
const module = {filename, exports: {}};
// Test if the code is valid gracefully
vm.compileFunction(addon.fileContent, ["require", "module", "exports", "__filename", "__dirname"]);
vm.compileFunction(addon.fileContent, ["require", "module", "exports", "__filename", "__dirname"], {filename: path.basename(filename)});
addon.fileContent += normalizeExports(addon.exports || addon.name);
addon.fileContent += `\n//# sourceURL=betterdiscord://plugins/${addon.filename}`;
const wrappedPlugin = new Function(["require", "module", "exports", "__filename", "__dirname"], addon.fileContent); // eslint-disable-line no-new-func

View File

@ -113,24 +113,6 @@ export class Filters {
}
}
const protect = theModule => {
if (theModule.remove && theModule.set && theModule.clear && theModule.get && !theModule.sort) return null;
if (!theModule.getToken && !theModule.getEmail && !theModule.showToken) return theModule;
const proxy = new Proxy(theModule, {
getOwnPropertyDescriptor: function(obj, prop) {
if (prop === "getToken" || prop === "getEmail" || prop === "showToken") return undefined;
return Object.getOwnPropertyDescriptor(obj, prop);
},
get: function(obj, func) {
if (func == "getToken") return () => "mfa.XCnbKzo0CLIqdJzBnL0D8PfDruqkJNHjwHXtr39UU3F8hHx43jojISyi5jdjO52e9_e9MjmafZFFpc-seOMa";
if (func == "getEmail") return () => "puppet11112@gmail.com";
if (func == "showToken") return () => true;
// if (func == "__proto__") return proxy;
return obj[func];
}
});
return proxy;
};
const hasThrown = new WeakSet();
@ -152,6 +134,10 @@ export default class WebpackModules {
const {first = true, defaultExport = true} = options;
const wrappedFilter = (exports, module, moduleId) => {
try {
if (exports?.default?.remove && exports?.default?.set && exports?.default?.clear && exports?.default?.get && !exports?.default?.sort) return false;
if (exports.remove && exports.set && exports.clear && exports.get && !exports.sort) return false;
if (exports?.default?.getToken || exports?.default?.getEmail || exports?.default?.showToken) return false;
if (exports.getToken || exports.getEmail || exports.showToken) return false;
return filter(exports, module, moduleId);
}
catch (err) {
@ -160,6 +146,7 @@ export default class WebpackModules {
return false;
}
};
const modules = this.getAllModules();
const rm = [];
const indices = Object.keys(modules);
@ -168,14 +155,42 @@ export default class WebpackModules {
if (!modules.hasOwnProperty(index)) continue;
const module = modules[index];
const {exports} = module;
if (exports === window) continue;
let foundModule = null;
if (typeof(exports) === "object") {
const wrappers = Object.getOwnPropertyDescriptors(exports);
const getters = Object.keys(wrappers).filter(k => wrappers[k].get);
if (getters.length) {
for (const getter of getters) {
const wrappedExport = exports[getter];
if (!wrappedExport) continue;
if (wrappedExport.__esModule && wrappedExport.default && wrappedFilter(wrappedExport.default, module, index)) foundModule = defaultExport ? wrappedExport.default : wrappedExport;
if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
}
}
else {
if (!exports) continue;
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports, module, index)) foundModule = exports;
if (!foundModule) continue;
if (first) return protect(foundModule);
rm.push(protect(foundModule));
if (first) return foundModule;
rm.push(foundModule);
}
}
else {
if (!exports) continue;
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports, module, index)) foundModule = exports;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
}
}
return first || rm.length == 0 ? undefined : rm;
@ -219,11 +234,36 @@ export default class WebpackModules {
};
let foundModule = null;
if (typeof(exports) === "object") {
const wrappers = Object.getOwnPropertyDescriptors(exports);
const getters = Object.keys(wrappers).filter(k => wrappers[k].get);
if (getters.length) {
for (const getter of getters) {
const wrappedExport = exports[getter];
if (!wrappedExport) continue;
if (wrappedExport.__esModule && wrappedExport.default && wrappedFilter(wrappedExport.default, module, index)) foundModule = defaultExport ? wrappedExport.default : wrappedExport;
if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport;
if (!foundModule) continue;
if (first) returnedModules[q] = foundModule;
else returnedModules[q].push(foundModule);
}
}
else {
if (!exports) continue;
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports, module, index)) foundModule = exports;
if (!foundModule) continue;
if (first) returnedModules[q] = protect(foundModule);
else returnedModules[q].push(protect(foundModule));
if (first) returnedModules[q] = foundModule;
else returnedModules[q].push(foundModule);
}
}
else {
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports, module, index)) foundModule = exports;
if (!foundModule) continue;
if (first) returnedModules[q] = foundModule;
else returnedModules[q].push(foundModule);
}
}
}
@ -341,12 +381,30 @@ export default class WebpackModules {
if (!exports) return;
let foundModule = null;
if (typeof(exports) === "object") {
const wrappers = Object.getOwnPropertyDescriptors(exports);
const getters = Object.keys(wrappers).filter(k => wrappers[k].get);
if (getters.length) {
for (const getter of getters) {
const wrappedExport = exports[getter];
if (!wrappedExport) continue;
if (wrappedExport.__esModule && wrappedExport.default && wrappedFilter(wrappedExport.default)) foundModule = defaultExport ? wrappedExport.default : wrappedExport;
if (wrappedFilter(wrappedExport)) foundModule = wrappedExport;
}
}
else {
if (exports.__esModule && exports.default && wrappedFilter(exports.default)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports)) foundModule = exports;
if (!foundModule) return;
}
}
else {
if (exports.__esModule && exports.default && wrappedFilter(exports.default)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports)) foundModule = exports;
}
if (!foundModule) return;
cancel();
resolve(protect(foundModule));
resolve(foundModule);
};
this.addListener(listener);

View File

@ -0,0 +1,17 @@
import WebpackModules from "../modules/webpackmodules";
Object.defineProperty(window, "Buffer", {
get() {return Buffer.getBuffer().Buffer;},
configurable: true,
enumerable: false
});
export default class Buffer {
static getBuffer() {
if (this.cached) return this.cached;
this.cached = WebpackModules.getByProps("INSPECT_MAX_BYTES");
return this.cached;
}
}

View File

@ -0,0 +1,9 @@
import Remote from "./remote";
export default {
...Remote.crypto,
// Wrap it in Buffer
randomBytes(length) {
return Buffer.from(Remote.crypto.randomBytes(length));
}
};

172
renderer/src/polyfill/fs.js Normal file
View File

@ -0,0 +1,172 @@
import Remote from "./remote";
export const readFileSync = function (path, options = "utf8") {
return Remote.filesystem.readFile(path, options);
};
export const readFile = function (path, options = "utf8", callback) {
try {
const contents = Remote.filesystem.readFile(path, options);
callback(null, contents);
}
catch (error) {
callback(error, null);
}
};
export const writeFile = function (path, data, options = "utf8", callback) {
if (typeof(options) === "function") {
callback = options;
if (!["object", "string"].includes(typeof(options))) options = undefined;
}
try {
Remote.filesystem.writeFile(path, data, options);
callback(null);
}
catch (error) {
callback(error);
}
};
export const writeFileSync = function (path, data, options) {
Remote.filesystem.writeFile(path, data, options);
};
export const readdir = function (path, options, callback) {
try {
const result = Remote.filesystem.readDirectory(path, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const readdirSync = function (path, options) {
return Remote.filesystem.readDirectory(path, options);
};
export const mkdir = function (path, options, callback) {
try {
const result = Remote.filesystem.createDirectory(path, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const mkdirSync = function (path, options) {
Remote.filesystem.createDirectory(path, options);
};
export const rmdir = function (path, options, callback) {
try {
const result = Remote.filesystem.deleteDirectory(path, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const rmdirSync = function (path, options) {
Remote.filesystem.deleteDirectory(path, options);
};
export const exists = function (path, options, callback) {
try {
const result = Remote.filesystem.exists(path, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const existsSync = function (path, options) {
return Remote.filesystem.exists(path, options);
};
export const stat = function (path, options, callback) {
try {
const result = Remote.filesystem.getStats(path, options);
callback(null, result);
}
catch (error) {
callback(error);
}
};
export const statSync = function (path, options) {
return Remote.filesystem.getStats(path, options);
};
export const lstat = stat;
export const lstatSync = statSync;
export const rename = function (oldPath, newPath, options, callback) {
try {
const result = Remote.filesystem.rename(oldPath, newPath, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const renameSync = function (oldPath, newPath, options) {
return Remote.filesystem.renameSync(oldPath, newPath, options);
};
export const realpath = function (path, options, callback) {
try {
const result = Remote.filesystem.getStats(path, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const realpathSync = function (path, options) {
return Remote.filesystem.getRealPath(path, options);
};
export const watch = (path, options, callback) => {
return Remote.filesystem.watch(path, options, callback);
};
export const createWriteStream = (path, options) => {
return Remote.filesystem.createWriteStream(path, options);
};
export const unlinkSync = (path) => Remote.filesystem.unlinkSync(path);
export const unlink = (path) => Remote.filesystem.unlinkSync(path);
export default {
readFile,
exists,
existsSync,
lstat,
lstatSync,
mkdir,
mkdirSync,
readFileSync,
readdir,
readdirSync,
realpath,
realpathSync,
rename,
renameSync,
rmdir,
rmdirSync,
unlink,
unlinkSync,
watch,
writeFile,
writeFileSync,
createWriteStream
};

View File

@ -0,0 +1,23 @@
import EventEmitter from "common/events";
import Remote from "./remote";
export function get(url, options = {}, callback) {
if (typeof(options) === "function") {
callback = options;
options = null;
}
const emitter = new EventEmitter();
callback(emitter);
Remote.https.get(url, options, (error, res, body) => {
if (error) return emitter.emit("error", error);
emitter.emit("data", body);
emitter.emit("end", res);
});
return emitter;
}
export default {get};

View File

@ -0,0 +1,45 @@
import Module from "./module";
import * as vm from "./vm";
import * as fs from "./fs";
import request from "./request";
import EventEmitter from "common/events";
import * as https from "./https";
import Buffer from "./buffer";
import crypto from "./crypto";
import Remote from "./remote";
const originalFs = Object.assign({}, fs);
originalFs.writeFileSync = (path, data, options) => fs.writeFileSync(path, data, Object.assign({}, options, {originalFs: true}));
originalFs.writeFile = (path, data, options) => fs.writeFile(path, data, Object.assign({}, options, {originalFs: true}));
export const createRequire = function (path) {
return mod => {
switch (mod) {
case "request": return request;
case "https": return https;
case "original-fs": return originalFs;
case "fs": return fs;
case "path": return Remote.path;
case "events": return EventEmitter;
case "electron": return Remote.electron;
case "process": return window.process;
case "vm": return vm;
case "module": return Module;
case "buffer": return Buffer.getBuffer();
case "crypto": return crypto;
default:
return Module._load(mod, path, createRequire);
}
};
};
const require = window.require = createRequire(".");
require.cache = {};
require.resolve = (path) => {
for (const key of Object.keys(require.cache)) {
if (key.startsWith(path)) return require.cache[key];
}
};
export default require;

View File

@ -0,0 +1,103 @@
import Logger from "common/logger";
import {compileFunction} from "./vm";
import Remote from "./remote";
import fs from "./fs";
const path = Remote.path;
export const RequireExtensions = {
".js": (module, filename) => {
const fileContent = Remote.filesystem.readFile(filename, "utf8");
module.fileContent = fileContent;
module._compile(fileContent);
return module.exports;
},
".json": (module, filename) => {
const fileContent = Remote.filesystem.readFile(filename, "utf8");
module.fileContent = fileContent;
module.exports = JSON.parse(fileContent);
return module.exports;
}
};
export default class Module {
static resolveMainFile(mod, basePath) {
const parent = path.extname(basePath) ? path.dirname(basePath) : basePath;
const files = Remote.filesystem.readDirectory(parent);
if (!Array.isArray(files)) return null;
for (const file of files) {
const ext = path.extname(file);
if (file === "package.json") {
const pkg = require(path.resolve(parent, file));
if (!Reflect.has(pkg, "main")) continue;
return path.resolve(parent, pkg.main);
}
if (path.slice(0, -ext.length) == "index" && RequireExtensions[ext]) return mod;
}
}
static getExtension(mod) {
return path.extname(mod) || Reflect.ownKeys(RequireExtensions).find(e => Remote.filesystem.exists(mod + e));
}
static getFilePath(basePath, mod) {
if (!path.isAbsolute(mod)) mod = path.resolve(basePath, mod);
const defaultExtension = path.extname(mod);
if (!defaultExtension) {
const ext = Reflect.ownKeys(RequireExtensions).find(e => Remote.filesystem.exists(mod + e));
if (ext) {
mod = mod + ext;
}
}
return fs.realpathSync(mod);
}
static _load(mod, basePath, createRequire) {
const originalReq = mod;
if (!path.isAbsolute(mod)) mod = path.resolve(basePath, mod);
const filePath = this.getFilePath(basePath, mod);
if (!Remote.filesystem.exists(filePath)) throw new Error(`Cannot find module ${mod}`);
if (window.require.cache[filePath]) return window.require.cache[filePath].exports;
const stats = Remote.filesystem.getStats(filePath);
if (stats.isDirectory()) mod = this.resolveMainFile(mod, basePath);
const ext = this.getExtension(filePath);
const loader = RequireExtensions[ext];
if (!loader) throw new Error(`Cannot find module ${originalReq}`);
const module = window.require.cache[mod] = new Module(filePath, internalModule, createRequire(mod));
loader(module, filePath);
return module.exports;
}
static get Module() {return Module;}
static get createRequire() {return Logger.warn("ContextModule", "Module.createRequire not implemented yet.");}
static get _extensions() {return RequireExtensions;}
constructor(id, parent, require) {
this.id = id;
this.path = Remote.path.dirname(id);
this.exports = {};
this.parent = parent;
this.filename = id;
this.loaded = false;
this.children = [];
this.require = require;
if (parent) parent.children.push(this);
}
_compile(code) {
const wrapped = compileFunction(code, ["require", "module", "exports", "__filename", "__dirname", "global"], this.filename);
wrapped(this.require, this, this.exports, this.filename, this.path, window);
}
}
const internalModule = new Module(".", null);

View File

@ -0,0 +1,3 @@
/** @type {import("../../../preload/src/api/index")} */
const RemoteAPI = window.BetterDiscordPreload(); // eslint-disable-line new-cap
export default RemoteAPI;

View File

@ -0,0 +1,68 @@
import Remote from "./remote";
const methods = ["get", "put", "post", "delete", "head"];
const aliases = {del: "delete"};
function parseArguments() {
let url, options, callback;
for (const arg of arguments) {
switch (typeof arg) {
case (arg !== null && "object"):
options = arg;
if ("url" in options) {
url = options.url;
}
break;
case (!url && "string"):
url = arg;
break;
case (!callback && "function"):
callback = arg;
break;
}
}
return {url, options, callback};
}
function validOptions(url, callback) {
return typeof url === "string" && typeof callback === "function";
}
function fixBuffer(options, callback) {
return (error, res, body) => {
if ("Content-Type" in Object(options.headers) && options.headers["Content-Type"] !== "text/plain") {
body = Buffer.from(body);
}
else {
body = Buffer.from(body).toString();
}
callback(error, res, body);
};
}
export default function request() {
const {url, options = {}, callback} = parseArguments.apply(this, arguments);
if (!validOptions(url, callback)) return null;
if ("method" in options && methods.indexOf(options.method.toLowerCase()) >= 0) {
return Remote.https[options.method](url, options, fixBuffer(options, callback));
}
return Remote.https.request(url, options, fixBuffer(options, callback));
}
Object.assign(request, Object.fromEntries(
methods.concat(Object.keys(aliases)).map(method => [method, function () {
const {url, options = {}, callback} = parseArguments.apply(this, arguments);
if (!validOptions(url, callback)) return null;
return Remote.https[aliases[method] || method](url, options, fixBuffer(options, callback));
}])
));

View File

@ -0,0 +1,9 @@
import Remote from "./remote";
export const compileFunction = function(code, params = [], options = {}) {
const returned = Remote.vm.compileFunction(code, params, options);
if (typeof(returned) === "function") return returned;
const syntaxError = new SyntaxError(returned.message);
syntaxError.stack = returned.stack;
throw syntaxError;
};

View File

@ -2,7 +2,7 @@ import {React, WebpackModules, DiscordModules, Settings} from "modules";
import Checkbox from "./checkbox";
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
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"];

View File

@ -1,6 +1,6 @@
import {Settings, React, WebpackModules, Events, Strings} from "modules";
const TooltipWrapper = WebpackModules.getByDisplayName("Tooltip");
const TooltipWrapper = WebpackModules.getByPrototypes("renderTooltip");
export default class BDEmote extends React.Component {
constructor(props) {

View File

@ -2,9 +2,9 @@ import {React, WebpackModules} from "modules";
import EmoteModule from "../builtins/emotes/emotes";
const ContextMenuActions = WebpackModules.getByProps("openContextMenu");
const {MenuItem, MenuGroup} = WebpackModules.find(m => m.MenuRadioItem && !m.default);
const ContextMenu = WebpackModules.getByProps("default", "MenuStyle").default;
const {ComponentDispatch} = WebpackModules.getByProps("ComponentDispatch");
const {MenuItem, MenuGroup} = WebpackModules.find(m => m.MenuRadioItem && !m.default) ?? {MenuItem: () => null, MenuGroup: () => null};
const ContextMenu = WebpackModules.getByProps("default", "MenuStyle")?.default;
const {ComponentDispatch} = WebpackModules.getByProps("ComponentDispatch") ?? {ComponentDispatch: () => null};
export default class EmoteIcon extends React.Component {
render() {

View File

@ -1,5 +1,5 @@
import {React, WebpackModules} from "modules";
const {ScrollerAuto: Scroller} = WebpackModules.getByProps("ScrollerAuto");
const {ScrollerAuto: Scroller} = WebpackModules.getByProps("ScrollerAuto") ?? {ScrollerAuto: () => null};
export default class EmoteMenuCard extends React.Component {
render() {
return <div className={`bd-emote-menu`}>

View File

@ -3,14 +3,14 @@ import FloatingWindowContainer from "./floating/container";
/* eslint-disable new-cap */
const LayerProviders = WebpackModules.getByProps("AppReferencePositionLayer");
const AppLayerProvider = WebpackModules.getByDisplayName("AppLayerProvider");
export default class FloatingWindows {
static initialize() {
const containerRef = React.createRef();
const container = <FloatingWindowContainer ref={containerRef} />;
const wrapped = LayerProviders
? React.createElement(LayerProviders.AppLayerProvider().props.layerContext.Provider, {value: [document.querySelector("#app-mount > .layerContainer-yqaFcK")]}, container) // eslint-disable-line new-cap
const wrapped = AppLayerProvider
? React.createElement(AppLayerProvider().props.layerContext.Provider, {value: [document.querySelector("#app-mount > .layerContainer-2v_Sit")]}, container) // eslint-disable-line new-cap
: container;
const div = DOM.createElement(`<div id="floating-windows-layer">`);
DOMManager.bdBody.append(div);

View File

@ -3,7 +3,8 @@ import {React} from "modules";
export default class Globe extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
const color = this.props.color || "#FFFFFF";
return <svg viewBox="2 2 20 20" fill={color} style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"/>
</svg>;

View File

@ -5,21 +5,27 @@ import FormattableString from "../structs/string";
import AddonErrorModal from "./addonerrormodal";
import ErrorBoundary from "./errorboundary";
export default class Modals {
static get shouldShowAddonErrors() {return Settings.get("settings", "addons", "addonErrors");}
static get ModalActions() {return WebpackModules.getByProps("openModal", "updateModal");}
static get ModalActions() {
return {
openModal: WebpackModules.getModule(m => m?.toString().includes("onCloseCallback") && m?.toString().includes("Layer")),
closeModal: WebpackModules.getModule(m => m?.toString().includes("onCloseCallback()"))
};
}
static get ModalStack() {return WebpackModules.getByProps("push", "update", "pop", "popWithKey");}
static get ModalComponents() {return WebpackModules.getByProps("ModalRoot");}
static get ModalComponents() {return WebpackModules.getByProps("Header", "Footer");}
static get ModalRoot() {return WebpackModules.getModule(m => m?.toString().includes("ENTERING"));}
static get ModalClasses() {return WebpackModules.getByProps("modal", "content");}
static get AlertModal() {return WebpackModules.getByPrototypes("handleCancel", "handleSubmit", "handleMinorConfirm");}
static get FlexElements() {return WebpackModules.getByProps("Child", "Align");}
static get FormTitle() {return WebpackModules.findByDisplayName("FormTitle");}
static get TextElement() {return WebpackModules.getByProps("Sizes", "Weights");}
static get ConfirmationModal() {return WebpackModules.findByDisplayName("ConfirmModal");}
static get Markdown() {return WebpackModules.find(m => m.displayName === "Markdown" && m.rules);}
static get Buttons() {return WebpackModules.getByProps("ButtonSizes");}
static get FormTitle() {return WebpackModules.getByProps("Tags", "Sizes");}
static get TextElement() {return WebpackModules.getModule(m => m?.Sizes?.SIZE_32 && m.Colors);}
static get ConfirmationModal() {return WebpackModules.getModule(m => m?.toString()?.includes("confirmText"));}
static get Markdown() {return WebpackModules.find(m => m?.prototype?.render && m.rules);}
static get Buttons() {return WebpackModules.getByProps("BorderColors");}
static default(title, content) {
const modal = DOM.createElement(`<div class="bd-modal-wrapper theme-dark">
@ -86,7 +92,7 @@ export default class Modals {
return ModalActions.openModal(props => {
return React.createElement(ConfirmationModal, Object.assign({
header: title,
confirmButtonColor: danger ? this.Buttons.ButtonColors.RED : this.Buttons.ButtonColors.BRAND,
confirmButtonColor: danger ? this.Buttons.Colors.RED : this.Buttons.Colors.BRAND,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: onConfirm,
@ -104,7 +110,7 @@ export default class Modals {
}
this.addonErrorsRef = React.createRef();
this.ModalActions.openModal(props => React.createElement(this.ModalComponents.ModalRoot, Object.assign(props, {
this.ModalActions.openModal(props => React.createElement(this.ModalRoot, Object.assign(props, {
size: "medium",
className: "bd-error-modal",
children: [
@ -114,9 +120,9 @@ export default class Modals {
themeErrors: Array.isArray(themeErrors) ? themeErrors : [],
onClose: props.onClose
}),
React.createElement(this.ModalComponents.ModalFooter, {
React.createElement(this.ModalComponents.Footer, {
className: "bd-error-modal-footer",
}, React.createElement(this.Buttons.default, {
}, React.createElement(this.Buttons, {
onClick: props.onClose,
className: "bd-button"
}, Strings.Modals.okay))
@ -221,17 +227,17 @@ export default class Modals {
const mc = this.ModalComponents;
const modal = props => {
return React.createElement(mc.ModalRoot, Object.assign({size: mc.ModalSize.MEDIUM, className: "bd-addon-modal"}, props),
React.createElement(mc.ModalHeader, {separator: false, className: "bd-addon-modal-header"},
return React.createElement(ErrorBoundary, {}, React.createElement(this.ModalRoot, Object.assign({size: mc.Sizes.MEDIUM, className: "bd-addon-modal" + " " + mc.Sizes.MEDIUM}, props),
React.createElement(mc.Header, {separator: false, className: "bd-addon-modal-header"},
React.createElement(this.FormTitle, {tag: "h4"}, `${name} Settings`)
),
React.createElement(mc.ModalContent, {className: "bd-addon-modal-settings"},
React.createElement(mc.Content, {className: "bd-addon-modal-settings"},
React.createElement(ErrorBoundary, {}, child)
),
React.createElement(mc.ModalFooter, {className: "bd-addon-modal-footer"},
React.createElement(this.Buttons.default, {onClick: props.onClose, className: "bd-button"}, Strings.Modals.done)
React.createElement(mc.Footer, {className: "bd-addon-modal-footer"},
React.createElement(this.Buttons, {onClick: props.onClose, className: "bd-button"}, Strings.Modals.done)
)
);
));
};
return this.ModalActions.openModal(props => {

View File

@ -1,4 +1,4 @@
import {React, WebpackModules, Strings} from "modules";
import {React, WebpackModules, Strings, DiscordModules} from "modules";
import Modals from "../modals";
import SettingsTitle from "../settings/title";
import ServerCard from "./card";
@ -8,9 +8,26 @@ import Search from "../settings/components/search";
import Previous from "../icons/previous";
import Next from "../icons/next";
const SettingsView = WebpackModules.getByDisplayName("SettingsView");
const SettingsView = WebpackModules.getByPrototypes("renderSidebar");
const GuildActions = WebpackModules.getByProps("transitionToGuildSync");
const LayerManager = WebpackModules.getByProps("popLayer");
const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
const EMPTY_RESULTS = {
servers: [],

View File

@ -62,7 +62,7 @@ export default new class SettingsRenderer {
}
async patchSections() {
const UserSettings = await WebpackModules.getLazy(Filters.byDisplayName("SettingsView"));
const UserSettings = await WebpackModules.getLazy(Filters.byPrototypeFields(["getPredicateSections"]));
Patcher.after("SettingsManager", UserSettings.prototype, "getPredicateSections", (thisObject, args, returnValue) => {
let location = returnValue.findIndex(s => s.section.toLowerCase() == "changelog") - 1;

View File

@ -1,7 +1,6 @@
import Logger from "common/logger";
import {React, Strings, WebpackModules, DiscordModules} from "modules";
import SimpleMarkdown from "../../structs/markdown";
import ReloadIcon from "../icons/reload";
import EditIcon from "../icons/edit";
import DeleteIcon from "../icons/delete";
import CogIcon from "../icons/cog";
@ -25,8 +24,25 @@ const LinkIcons = {
patreon: PatreonIcon
};
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
const LayerStack = WebpackModules.getByProps("popLayer");
const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
const UserStore = WebpackModules.getByProps("getCurrentUser");
const ChannelStore = WebpackModules.getByProps("getDMFromUserId");
const PrivateChannelActions = WebpackModules.getByProps("openPrivateChannel");
@ -41,7 +57,6 @@ export default class AddonCard extends React.Component {
this.panelRef = React.createRef();
this.onChange = this.onChange.bind(this);
this.reload = this.reload.bind(this);
this.showSettings = this.showSettings.bind(this);
this.messageAuthor = this.messageAuthor.bind(this);
}
@ -58,12 +73,6 @@ export default class AddonCard extends React.Component {
}
}
reload() {
if (!this.props.reload) return;
this.props.addon = this.props.reload(this.props.addon.id);
this.forceUpdate();
}
getString(value) {return typeof value == "string" ? value : value.toString();}
onChange() {
@ -74,7 +83,7 @@ export default class AddonCard extends React.Component {
messageAuthor() {
if (!this.props.addon.authorId) return;
if (LayerStack) LayerStack.popLayer();
if (LayerManager) LayerManager.popLayer();
if (!UserStore || !ChannelActions || !ChannelStore || !PrivateChannelActions) return;
const selfId = UserStore.getCurrentUser().id;
if (selfId == this.props.addon.authorId) return;
@ -114,7 +123,7 @@ export default class AddonCard extends React.Component {
let code = url;
const tester = /\.gg\/(.*)$/;
if (tester.test(code)) code = code.match(tester)[1];
DiscordModules.LayerStack.popLayer();
LayerManager.popLayer();
DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel(code);
};
}
@ -124,7 +133,6 @@ export default class AddonCard extends React.Component {
get controls() { // {this.props.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-addon-settings" disabled={!this.props.enabled}>{Strings.Addons.addonSettings}</button>}
return <div className="bd-controls">
{this.props.hasSettings && this.makeControlButton(Strings.Addons.addonSettings, <CogIcon size={"20px"} />, this.showSettings, {disabled: !this.props.enabled})}
{this.props.showReloadIcon && this.makeControlButton(Strings.Addons.reload, <ReloadIcon size={"20px"} />, this.reload)}
{this.props.editAddon && this.makeControlButton(Strings.Addons.editAddon, <EditIcon size={"20px"} />, this.props.editAddon)}
{this.props.deleteAddon && this.makeControlButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, this.props.deleteAddon, {danger: true})}
</div>;

View File

@ -1,9 +1,8 @@
import Logger from "common/logger";
import {React, Settings, Strings, Events, WebpackModules, DataStore} from "modules";
import {React, Strings, Events, WebpackModules, DataStore} from "modules";
import Modals from "../modals";
import SettingsTitle from "./title";
import ReloadIcon from "../icons/reload";
import AddonCard from "./addoncard";
import Dropdown from "./components/dropdown";
import Search from "./components/search";
@ -14,7 +13,7 @@ import GridIcon from "../icons/grid";
import NoResults from "../blankslates/noresults";
import EmptyImage from "../blankslates/emptyimage";
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
export default class AddonList extends React.Component {
@ -125,7 +124,6 @@ export default class AddonList extends React.Component {
render() {
const {title, folder, addonList, addonState, onChange, reload} = this.props;
const showReloadIcon = !Settings.get("settings", "addons", "autoReload");
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: this.openFolder} : null;
let sortedAddons = addonList.sort((a, b) => {
const sortByEnabled = this.state.sort === "isEnabled";
@ -152,7 +150,7 @@ export default class AddonList extends React.Component {
const renderedCards = sortedAddons.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard type={this.props.type} editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} showReloadIcon={showReloadIcon} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
return <ErrorBoundary><AddonCard type={this.props.type} editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
});
const hasAddonsInstalled = this.props.addonList.length !== 0;
@ -160,7 +158,7 @@ export default class AddonList extends React.Component {
const hasResults = sortedAddons.length !== 0;
return [
<SettingsTitle key="title" text={title} button={button} otherChildren={showReloadIcon && <ReloadIcon className="bd-reload" onClick={this.reload.bind(this)} />} />,
<SettingsTitle key="title" text={title} button={button} />,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={this.search} placeholder={`${Strings.Addons.search.format({type: this.props.title})}...`} />
<div className="bd-controls-advanced">

View File

@ -4,7 +4,7 @@ import HistoryIcon from "../icons/history";
import Modals from "../modals";
const SidebarComponents = WebpackModules.getModule(m => m.Header && m.Separator && m.Item);
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
export default class SettingsTitle extends React.Component {
render() {