first reactcomponents + usage

This commit is contained in:
Zack Rauen 2019-06-22 00:37:19 -04:00
parent 7d3403d79a
commit 6bb5808063
13 changed files with 179 additions and 210 deletions

File diff suppressed because one or more lines are too long

View File

@ -31,9 +31,9 @@ const favoritesHTML = `<div id="bda-qem-favourite-container">
const makeEmote = (emote, url, options = {}) => {
const {onContextMenu, onClick} = options;
const emoteContainer = $(`<div class="emote-container">
const emoteContainer = Utilities.parseHTML(`<div class="emote-container">
<img class="emote-icon" alt="${emote}" src="${url}" title="${emote}">
</div>`)[0];
</div>`);
if (onContextMenu) emoteContainer.addEventListener("contextmenu", onContextMenu);
emoteContainer.addEventListener("click", onClick);
return emoteContainer;
@ -52,13 +52,13 @@ export default new class EmoteMenu extends Builtin {
this.lastTab = "bda-qem-emojis";
this.favoriteEmotes = {};
this.qmeHeader = $(headerHTML)[0];
this.qmeHeader = Utilities.parseHTML(headerHTML);
for (const button of this.qmeHeader.getElementsByTagName("button")) button.addEventListener("click", this.switchMenu.bind(this));
this.teContainer = $(twitchEmoteHTML)[0];
this.teContainer = Utilities.parseHTML(twitchEmoteHTML);
this.teContainerInner = this.teContainer.querySelector(".emote-menu-inner");
this.faContainer = $(favoritesHTML)[0];
this.faContainer = Utilities.parseHTML(favoritesHTML);
this.faContainerInner = this.faContainer.querySelector(".emote-menu-inner");
this.observer = new MutationObserver(mutations => {for (const mutation of mutations) this.observe(mutation);});
@ -94,11 +94,13 @@ export default new class EmoteMenu extends Builtin {
}
enableHideEmojis() {
$(".emojiPicker-3m1S-j").addClass("bda-qme-hidden");
const picker = document.querySelector(".emojiPicker-3m1S-j");
if (picker) picker.classList.add("bda-qme-hidden");
}
disableHideEmojis() {
$(".emojiPicker-3m1S-j").removeClass("bda-qme-hidden");
const picker = document.querySelector(".emojiPicker-3m1S-j");
if (picker) picker.classList.remove("bda-qme-hidden");
}
insertEmote(emote) {
@ -131,7 +133,7 @@ export default new class EmoteMenu extends Builtin {
}
switchMenu(e) {
let id = typeof(e) == "string" ? e : $(e.target).attr("id");
let id = typeof(e) == "string" ? e : e.target.id;
if (id == "bda-qem-emojis" && this.hideEmojis) id = "bda-qem-favourite";
const twitch = $("#bda-qem-twitch");
const fav = $("#bda-qem-favourite");

View File

@ -1,5 +1,4 @@
import Builtin from "../structs/builtin";
import PSConnection from "./publicservers/connection";
import {BDV2, DiscordModules, WebpackModules} from "modules";
import {PublicServersMenu} from "ui";
@ -21,7 +20,7 @@ export default new class PublicServers extends Builtin {
}
openPublicServers() {
LayerStack.pushLayer(() => DiscordModules.React.createElement(PublicServersMenu, {close: LayerStack.popLayer, connection: PSConnection}));
LayerStack.pushLayer(() => DiscordModules.React.createElement(PublicServersMenu, {close: LayerStack.popLayer}));
}
get button() {

View File

@ -3,13 +3,13 @@ import Core from "./modules/core";
import BdApi from "./modules/pluginapi";
import PluginManager from "./modules/pluginmanager";
import ThemeManager from "./modules/thememanager";
import {bdPluginStorage} from "./modules/oldstorage";
import Events from "./modules/emitter";
import Settings from "./modules/settingsmanager";
import DataStore from "./modules/datastore";
import EmoteModule from "./builtins/emotes";
import DomManager from "./modules/dommanager";
import Utilities from "./modules/utilities";
import ReactComponents from "./modules/reactcomponents";
// Perform some setup
// proxyLocalStorage();
@ -30,13 +30,14 @@ window.themeModule = ThemeManager;
// window.bdplugins = Plugins;
window.bdEmotes = EmoteModule.Emotes;
window.bemotes = EmoteModule.blacklist;
window.bdPluginStorage = bdPluginStorage;
// window.bdPluginStorage = bdPluginStorage;
window.settingsModule = Settings;
window.DataStore = DataStore;
window.DomManager = DomManager;
window.utils = Utilities;
window.Components = ReactComponents;
window.BDEvents = Events;
window.bdConfig = Config;

View File

@ -1,96 +0,0 @@
import WebpackModules from "./webpackmodules";
import DiscordModules from "./discordmodules";
import Utilities from "./utilities";
import BDLogo from "../ui/icons/bdlogo";
const React = DiscordModules.React;
export default new class {
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.patchGuildSeparator.bind(this), "BD Guild Separator Patch")();
}
patchSocial() {
if (this.socialPatch) return;
const TabBar = WebpackModules.getModule(m => m.displayName == "TabBar");
const Anchor = WebpackModules.getModule(m => m.displayName == "Anchor");
if (!TabBar || !Anchor) return;
this.socialPatch = Utilities.monkeyPatch(TabBar.prototype, "render", {after: (data) => {
const children = data.returnValue.props.children;
if (!children || !children.length) return;
if (children[children.length - 2].type.displayName !== "Separator") return;
if (!children[children.length - 1].type.toString().includes("socialLinks")) return;
const original = children[children.length - 1].type;
const newOne = function() {
const returnVal = original(...arguments);
returnVal.props.children.push(React.createElement(Anchor, {className: "bd-social-link", href: "https://github.com/rauenzi/BetterDiscordApp", rel: "author", title: "BandagedBD", target: "_blank"},
React.createElement(BDLogo, {size: "16px", className: "bd-social-logo"})
));
return returnVal;
};
children[children.length - 1].type = newOne;
}});
}
patchGuildListItems() {
if (this.guildListItemsPatch) return;
const listItemClass = this.guildClasses.listItem.split(" ")[0];
const blobClass = this.guildClasses.blobContainer.split(" ")[0];
const reactInstance = Utilities.getReactInstance(document.querySelector(`.${listItemClass} .${blobClass}`).parentElement);
const GuildComponent = reactInstance.return.type;
if (!GuildComponent) return;
this.guildListItemsPatch = Utilities.monkeyPatch(GuildComponent.prototype, "render", {after: (data) => {
const returnValue = data.returnValue;
const guildData = data.thisObject.props;
returnValue.props.className += " bd-guild";
if (guildData.unread) returnValue.props.className += " bd-unread";
if (guildData.selected) returnValue.props.className += " bd-selected";
if (guildData.audio) returnValue.props.className += " bd-audio";
if (guildData.video) returnValue.props.className += " bd-video";
if (guildData.badge) returnValue.props.className += " bd-badge";
if (guildData.animatable) returnValue.props.className += " bd-animatable";
return returnValue;
}});
}
patchGuildPills() {
if (this.guildPillPatch) return;
const guildPill = WebpackModules.getModule(m => m.default && m.default.toString && m.default.toString().includes("translate3d"));
if (!guildPill) return;
this.guildPillPatch = Utilities.monkeyPatch(guildPill, "default", {after: (data) => {
const props = data.methodArguments[0];
if (props.unread) data.returnValue.props.className += " bd-unread";
if (props.selected) data.returnValue.props.className += " bd-selected";
if (props.hovered) data.returnValue.props.className += " bd-hovered";
return data.returnValue;
}});
}
patchGuildSeparator() {
if (this.guildSeparatorPatch) return;
const Guilds = WebpackModules.getByDisplayName("Guilds");
const guildComponents = WebpackModules.getByProps("renderListItem");
if (!guildComponents || !Guilds) return;
const GuildSeparator = function() {
const returnValue = guildComponents.Separator(...arguments);
returnValue.props.className += " bd-guild-separator";
return returnValue;
};
this.guildSeparatorPatch = Utilities.monkeyPatch(Guilds.prototype, "render", {after: (data) => {
data.returnValue.props.children[1].props.children[3].type = GuildSeparator;
}});
}
};
// lc = WebpackModules.getByDisplayName("FluxContainer(Layers)")
// Patcher.after(lc.prototype, "render", (t,a,r) => {console.log(t,a,r);})
// return.type
// Patcher.after(temp3.prototype, "renderLayers", (t,a,r) => {
// console.log(t,a,r);
// if (t.props.layers.includes("USER_SETTINGS")) r[1].props.className = "user-settings-prop";
// })

View File

@ -9,6 +9,7 @@ import ThemeManager from "./thememanager";
import Settings from "./settingsmanager";
import * as Builtins from "builtins";
import {Modals} from "ui";
import ReactComponents from "./reactcomponents";
function Core() {
}
@ -22,7 +23,7 @@ Core.prototype.init = async function() {
Modals.alert("Not Supported", "BetterDiscord v" + Config.version + " (your version)" + " is not supported by the latest js (" + Config.bbdVersion + ").<br><br> Please download the latest version from <a href='https://github.com/rauenzi/BetterDiscordApp/releases/latest' target='_blank'>GitHub</a>");
return;
}
ReactComponents.initialize();
const latestLocalVersion = Config.updater ? Config.updater.LatestVersion : Config.latestVersion;
if (latestLocalVersion > Config.version) {
Modals.alert("Update Available", `

View File

@ -1,32 +0,0 @@
import Utilities from "./utilities";
import DataStore from "./datastore";
export class bdStorage {
static get(key) {
Utilities.warn("Deprecation Notice", "Please use BdApi.getBDData(). bdStorage may be removed in future versions.");
return DataStore.getBDData(key);
}
static set(key, data) {
Utilities.warn("Deprecation Notice", "Please use BdApi.setBDData(). bdStorage may be removed in future versions.");
DataStore.setBDData(key, data);
}
}
export class bdPluginStorage {
static get(pluginName, key) {
Utilities.warn("Deprecation Notice", `${pluginName}, please use BdApi.loadData() or BdApi.getData(). bdPluginStorage may be removed in future versions.`);
return DataStore.getPluginData(pluginName, key) || null;
}
static set(pluginName, key, data) {
Utilities.warn("Deprecation Notice", `${pluginName}, please use BdApi.saveData() or BdApi.setData(). bdPluginStorage may be removed in future versions.`);
if (typeof(data) === "undefined") return Utilities.warn("Deprecation Notice", "Trying to set undefined value in plugin " + pluginName);
DataStore.setPluginData(pluginName, key, data);
}
static delete(pluginName, key) {
Utilities.warn("Deprecation Notice", `${pluginName}, please use BdApi.deleteData(). bdPluginStorage may be removed in future versions.`);
DataStore.deletePluginData(pluginName, key);
}
}

View File

@ -0,0 +1,82 @@
import DiscordModules from "./discordmodules";
import Patcher from "./patcher";
const React = DiscordModules.React;
const components = {};
const unknownComponents = new Set();
const listeners = new Set();
export default new class ReactComponents {
get named() {return components;}
get unknown() {return unknownComponents;}
get listeners() {return listeners;}
initialize() {
this.walkReactTree(document.querySelector("#app-mount")._reactRootContainer._internalRoot.current);
Patcher.after("ReactComponents", React, "createElement", (_, __, returnValue) => {
this.walkRenderTree(returnValue);
});
Patcher.instead("ReactComponents", React.Component.prototype, "componentWillMount", (thisObject) => {
this.addComponent(thisObject.constructor);
});
Patcher.instead("ReactComponents", React.Component.prototype, "UNSAFE_componentWillMount", (thisObject) => {
this.addComponent(thisObject.constructor);
});
}
get(name, filter) {
return new Promise(resolve => {
if (components[name]) return resolve(components[name]);
listeners.add({name, filter, resolve});
if (!filter) return;
for (const component of unknownComponents) {
if (!filter(component)) continue;
component.displayName = name;
unknownComponents.delete(component);
this.addNamedComponent(component);
}
});
}
addNamedComponent(component) {
const name = component.displayName;
if (!components[name]) {
components[name] = component;
for (const listener of listeners) {
if (listener.name !== name) continue;
listener.resolve(component);
listeners.delete(listener);
}
}
}
addUnknownComponent(component) {
if (unknownComponents.has(component)) return;
for (const listener of listeners) {
if (!listener.filter || !listener.filter(component)) continue;
component.displayName = listener.name;
this.addNamedComponent(component);
}
if (!component.displayName) unknownComponents.add(component);
}
addComponent(component) {
if (component.displayName) return this.addNamedComponent(component);
return this.addUnknownComponent(component);
}
walkRenderTree(tree) {
if (!tree) return;
if (typeof(tree.type) == "function") this.addComponent(tree.type);
if (Array.isArray(tree)) for (const value of tree) this.walkRenderTree(value);
if (tree.props && tree.props.children) this.walkRenderTree(tree.props.children);
}
walkReactTree(tree) {
if (!tree) return;
if (typeof(tree.type) == "function") this.addComponent(tree.type);
if (tree.child) this.walkReactTree(tree.child);
if (tree.sibling) this.walkReactTree(tree.sibling);
}
};

View File

@ -4,6 +4,8 @@ import DataStore from "./datastore";
import Events from "./emitter";
import WebpackModules from "./webpackmodules";
import DiscordModules from "./discordmodules";
import Patcher from "./patcher";
import ReactComponents from "./reactcomponents";
import {SettingsPanel as SettingsRenderer} from "ui";
import Utilities from "./utilities";
@ -102,17 +104,17 @@ export default new class SettingsManager {
}
async patchSections() {
Utilities.monkeyPatch(WebpackModules.getByDisplayName("FluxContainer(GuildSettings)").prototype, "render", {after: (data) => {
data.thisObject._reactInternalFiber.return.return.return.return.return.return.memoizedProps.id = "guild-settings";
}});
const UserSettings = await this.getUserSettings();
Utilities.monkeyPatch(UserSettings.prototype, "render", {after: (data) => {
data.thisObject._reactInternalFiber.return.return.return.return.return.return.return.memoizedProps.id = "user-settings";
}});
Utilities.monkeyPatch(UserSettings.prototype, "generateSections", {after: (data) => {
let location = data.returnValue.findIndex(s => s.section.toLowerCase() == "linux") + 1;
Patcher.after("SettingsManager", WebpackModules.getByDisplayName("FluxContainer(GuildSettings)").prototype, "render", (thisObject) => {
thisObject._reactInternalFiber.return.return.return.return.return.return.memoizedProps.id = "guild-settings";
});
const UserSettings = await ReactComponents.get("UserSettings", m => m.prototype && m.prototype.generateSections);
Patcher.after("SettingsManager", UserSettings.prototype, "render", (thisObject) => {
thisObject._reactInternalFiber.return.return.return.return.return.return.return.memoizedProps.id = "user-settings";
});
Patcher.after("SettingsManager", UserSettings.prototype, "generateSections", (thisObject, args, returnValue) => {
let location = returnValue.findIndex(s => s.section.toLowerCase() == "linux") + 1;
const insert = (section) => {
data.returnValue.splice(location, 0, section);
returnValue.splice(location, 0, section);
location++;
};
insert({section: "DIVIDER"});
@ -126,11 +128,11 @@ export default new class SettingsManager {
});
}
for (const panel of this.panels.sort((a,b) => a.order > b.order)) {
if (panel.clickListener) panel.onClick = (event) => panel.clickListener(data.thisObject, event, data.returnValue);
if (panel.clickListener) panel.onClick = (event) => panel.clickListener(thisObject, event, returnValue);
insert(panel);
}
insert({section: "CUSTOM", element: () => SettingsRenderer.attribution});
}});
});
this.forceUpdate();
}
@ -140,16 +142,6 @@ export default new class SettingsManager {
Utilities.getReactInstance(node).return.return.return.return.return.return.stateNode.forceUpdate();
}
getUserSettings() {
return new Promise(resolve => {
const cancel = Utilities.monkeyPatch(WebpackModules.getByProps("getUserSettingsSections").default.prototype, "render", {after: (data) => {
resolve(data.returnValue.type);
data.thisObject.forceUpdate();
cancel();
}});
});
}
saveSettings() {
DataStore.setData("settings", this.state);
}

View File

@ -2,6 +2,25 @@ import Logger from "./logger";
export default class Utilities {
/**
* 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 $(".channelTextArea-1LDbYG textarea");
}

View File

@ -1,4 +1,4 @@
import {Logger, WebpackModules, React, Settings} from "modules";
import {Logger, WebpackModules, Utilities, React, Settings} from "modules";
export default class Modals {
@ -10,7 +10,7 @@ export default class Modals {
static get ConfirmationModal() {return WebpackModules.getModule(m => m.defaultProps && m.key && m.key() == "confirm-modal");}
static default(title, content) {
const modal = $(`<div class="bd-modal-wrapper theme-dark">
const modal = Utilities.parseHTML(`<div class="bd-modal-wrapper theme-dark">
<div class="bd-backdrop backdrop-1wrmKB"></div>
<div class="bd-modal modal-1UGdnR">
<div class="bd-modal-inner inner-1JeGVc">
@ -30,15 +30,15 @@ export default class Modals {
</div>
</div>
</div>`);
modal.find(".footer button").on("click", () => {
modal.addClass("closing");
setTimeout(() => { modal.remove(); }, 300);
modal.querySelector(".footer button").addEventListener("click", () => {
modal.addClass("closing");
setTimeout(() => { modal.remove(); }, 300);
});
modal.find(".bd-backdrop").on("click", () => {
modal.addClass("closing");
setTimeout(() => { modal.remove(); }, 300);
modal.querySelector(".bd-backdrop").addEventListener("click", () => {
modal.addClass("closing");
setTimeout(() => { modal.remove(); }, 300);
});
modal.appendTo("#app-mount");
document.querySelector("#app-mount").append(modal);
}
static alert(title, content) {

View File

@ -1,6 +1,7 @@
import {React, WebpackModules} from "modules";
import SettingsTitle from "../settings/title";
import ServerCard from "./card";
import Connection from "../../structs/psconnection";
const SettingsView = WebpackModules.getByDisplayName("SettingsView");
@ -37,7 +38,7 @@ export default class PublicServers extends React.Component {
}
async checkConnection() {
const userData = await this.props.connection.checkConnection();
const userData = await Connection.checkConnection();
if (!userData) {
return this.setState({loading: true, user: null});
}
@ -46,7 +47,7 @@ export default class PublicServers extends React.Component {
}
async connect() {
await this.props.connection.connect();
await Connection.connect();
this.checkConnection();
}
@ -57,7 +58,7 @@ export default class PublicServers extends React.Component {
async search(term = "", from = 0) {
this.setState({query: term, loading: true});
const results = await this.props.connection.search({term, category: this.state.category == "All" ? "" : this.state.category, from});
const results = await Connection.search({term, category: this.state.category == "All" ? "" : this.state.category, from});
if (!results) {
return this.setState({results: {
servers: [],
@ -82,7 +83,7 @@ export default class PublicServers extends React.Component {
}
async join(id, native = false) {
return await this.props.connection.join(id, native);
return await Connection.join(id, native);
}
get searchBox() {
@ -104,7 +105,7 @@ export default class PublicServers extends React.Component {
const connectButton = this.state.user ? null : {title: "Connect", onClick: this.connect};
const pinned = this.state.category == "All" || !this.state.user ? this.bdServer : null;
const servers = this.state.results.servers.map((server) => {
return React.createElement(ServerCard, {key: server.identifier, server: server, joined: this.props.connection.hasJoined(server.identifier), defaultAvatar: this.props.connection.getDefaultAvatar});
return React.createElement(ServerCard, {key: server.identifier, server: server, joined: Connection.hasJoined(server.identifier), defaultAvatar: Connection.getDefaultAvatar});
});
return [React.createElement(SettingsTitle, {text: this.title, button: connectButton}),
pinned,
@ -139,7 +140,7 @@ export default class PublicServers extends React.Component {
invite_code: "0Tmfo5ZbORCRqbAd",
pinned: true
};
return React.createElement(ServerCard, {server: server, pinned: true, joined: this.props.connection.hasJoined(server.identifier), defaultAvatar: this.props.connection.getDefaultAvatar});
return React.createElement(ServerCard, {server: server, pinned: true, joined: Connection.hasJoined(server.identifier), defaultAvatar: Connection.getDefaultAvatar});
}
render() {