Merge pull request #168 from JsSucks/optimize
Optimize - Requires Core Rebuild(?)
This commit is contained in:
commit
d1dc8140e4
|
@ -19,12 +19,3 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.bd-emotewrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.bd-emotewrapper img {
|
||||
max-height: 32px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,84 +8,98 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import { FileUtils } from 'common';
|
||||
import { Events, Globals } from 'modules';
|
||||
import { Events, Globals, WebpackModules, ReactComponents } from 'modules';
|
||||
import { DOM, VueInjector } from 'ui';
|
||||
import EmoteComponent from './EmoteComponent.vue';
|
||||
let emotes = null;
|
||||
const emotesEnabled = true;
|
||||
|
||||
export default class {
|
||||
static get React() {
|
||||
return WebpackModules.getModuleByName('React');
|
||||
}
|
||||
static processMarkup(markup) {
|
||||
if (!emotesEnabled) return markup; // TODO Get it from setttings
|
||||
const newMarkup = [];
|
||||
for (const [ti, t] of markup.entries()) {
|
||||
if ('string' !== typeof t) {
|
||||
newMarkup.push(t);
|
||||
continue;
|
||||
}
|
||||
|
||||
const words = t.split(/([^\s]+)([\s]|$)/g);
|
||||
if (!words) continue;
|
||||
let text = null;
|
||||
for (const [wi, word] of words.entries()) {
|
||||
let isEmote = false;
|
||||
if (this.testWord(word)) {
|
||||
isEmote = true;
|
||||
}
|
||||
if (isEmote) {
|
||||
if (text !== null) {
|
||||
newMarkup.push(text);
|
||||
text = null;
|
||||
}
|
||||
newMarkup.push(this.React.createElement('span', { className: 'bd-emote-outer' }, word));
|
||||
continue;
|
||||
}
|
||||
if (text === null) {
|
||||
text = `${word}`;
|
||||
} else {
|
||||
text += `${word}`;
|
||||
}
|
||||
if (wi === words.length - 1) {
|
||||
newMarkup.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newMarkup;
|
||||
}
|
||||
|
||||
static testWord(word) {
|
||||
if (!/:[\w]+:/gmi.test(word)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static injectAll() {
|
||||
if (!emotesEnabled) return;
|
||||
const all = document.getElementsByClassName('bd-emote-outer');
|
||||
for (const ec of all) {
|
||||
if (ec.children.length) continue;
|
||||
this.injectEmote(ec);
|
||||
}
|
||||
}
|
||||
|
||||
static async observe() {
|
||||
const dataPath = Globals.getObject('paths').find(path => path.id === 'data').path;
|
||||
try {
|
||||
emotes = await FileUtils.readJsonFromFile(dataPath + '/emotes.json');
|
||||
Events.on('ui:mutable:.markup',
|
||||
markup => {
|
||||
if (!emotes) return;
|
||||
this.injectEmotes(markup);
|
||||
});
|
||||
const Message = await ReactComponents.getComponent('Message');
|
||||
Message.on('componentDidMount', ({ element }) => this.injectEmotes(element));
|
||||
Message.on('componentDidUpdate', ({ state, element }) => {
|
||||
if (!state.isEditing) this.injectEmotes(element);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
static injectEmotes(node) {
|
||||
if (!/:[\w]+:/gmi.test(node.textContent)) return node;
|
||||
const childNodes = [...node.childNodes];
|
||||
const newNode = document.createElement('div');
|
||||
newNode.className = node.className;
|
||||
newNode.classList.add('hasEmotes');
|
||||
static injectEmote(e) {
|
||||
if (!emotesEnabled) return;
|
||||
const isEmote = this.isEmote(e.textContent);
|
||||
if (!isEmote) return;
|
||||
VueInjector.inject(
|
||||
e,
|
||||
DOM.createElement('span'),
|
||||
{ EmoteComponent },
|
||||
`<EmoteComponent src="${isEmote.src}" name="${isEmote.name}"/>`
|
||||
);
|
||||
e.classList.add('bd-is-emote');
|
||||
}
|
||||
|
||||
for (const [cni, cn] of childNodes.entries()) {
|
||||
if (cn.nodeType !== Node.TEXT_NODE) {
|
||||
newNode.appendChild(cn);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { nodeValue } = cn;
|
||||
const words = nodeValue.split(/([^\s]+)([\s]|$)/g);
|
||||
|
||||
if (!words.some(word => word.startsWith(':') && word.endsWith(':'))) {
|
||||
newNode.appendChild(cn);
|
||||
continue;
|
||||
}
|
||||
let text = null;
|
||||
for (const [wi, word] of words.entries()) {
|
||||
let isEmote = null;
|
||||
if (word.startsWith(':') && word.endsWith(':')) {
|
||||
isEmote = this.isEmote(word);
|
||||
}
|
||||
|
||||
if (isEmote) {
|
||||
if (text !== null) {
|
||||
newNode.appendChild(document.createTextNode(text));
|
||||
text = null;
|
||||
}
|
||||
|
||||
const emoteRoot = document.createElement('span');
|
||||
newNode.appendChild(emoteRoot);
|
||||
VueInjector.inject(
|
||||
emoteRoot,
|
||||
DOM.createElement('span'),
|
||||
{ EmoteComponent },
|
||||
`<EmoteComponent src="${isEmote.src}" name="${isEmote.name}"/>`,
|
||||
true
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (text === null) {
|
||||
text = word;
|
||||
} else {
|
||||
text += word;
|
||||
}
|
||||
|
||||
if (wi === words.length - 1) {
|
||||
newNode.appendChild(document.createTextNode(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
node.replaceWith(newNode);
|
||||
static injectEmotes(element) {
|
||||
if (!emotesEnabled || !element) return;
|
||||
for (const beo of element.getElementsByClassName('bd-emote-outer')) this.injectEmote(beo);
|
||||
}
|
||||
|
||||
static isEmote(word) {
|
||||
|
|
|
@ -8,33 +8,48 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { DOM, BdUI, Modals } from 'ui';
|
||||
import { DOM, BdUI, Modals, Reflection } from 'ui';
|
||||
import BdCss from './styles/index.scss';
|
||||
import { Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database, DiscordApi } from 'modules';
|
||||
import { ClientLogger as Logger, ClientIPC } from 'common';
|
||||
import { Patcher, Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database, ReactComponents, ReactAutoPatcher, DiscordApi } from 'modules';
|
||||
import { ClientLogger as Logger, ClientIPC, Utils } from 'common';
|
||||
import { EmoteModule } from 'builtin';
|
||||
const ignoreExternal = false;
|
||||
const DEV = true;
|
||||
|
||||
class BetterDiscord {
|
||||
|
||||
constructor() {
|
||||
window.discordApi = DiscordApi;
|
||||
window.bddb = Database;
|
||||
window.bdglobals = Globals;
|
||||
window.ClientIPC = ClientIPC;
|
||||
window.css = CssEditor;
|
||||
window.pm = PluginManager;
|
||||
window.tm = ThemeManager;
|
||||
window.events = Events;
|
||||
window.wpm = WebpackModules;
|
||||
window.bdsettings = Settings;
|
||||
window.bdmodals = Modals;
|
||||
window.bdlogs = Logger;
|
||||
window.emotes = EmoteModule;
|
||||
window.dom = DOM;
|
||||
window.BDDEVMODE = function () {
|
||||
if (!DEV) return;
|
||||
window._bd = {
|
||||
DOM,
|
||||
BdUI,
|
||||
Modals,
|
||||
Reflection,
|
||||
Patcher,
|
||||
Events,
|
||||
CssEditor,
|
||||
Globals,
|
||||
ExtModuleManager,
|
||||
PluginManager,
|
||||
ThemeManager,
|
||||
ModuleManager,
|
||||
WebpackModules,
|
||||
Settings,
|
||||
Database,
|
||||
ReactComponents,
|
||||
DiscordApi,
|
||||
Logger,
|
||||
ClientIPC,
|
||||
Utils,
|
||||
EmoteModule
|
||||
}
|
||||
}
|
||||
|
||||
DOM.injectStyle(BdCss, 'bdmain');
|
||||
Events.on('global-ready', this.globalReady.bind(this));
|
||||
this.globalReady = this.globalReady.bind(this);
|
||||
Events.on('global-ready', this.globalReady);
|
||||
Globals.initg();
|
||||
}
|
||||
|
||||
async init() {
|
||||
|
@ -68,5 +83,11 @@ class BetterDiscord {
|
|||
if (window.BetterDiscord) {
|
||||
Logger.log('main', 'Attempting to inject again?');
|
||||
} else {
|
||||
let bdInstance = new BetterDiscord();
|
||||
let instance = null;
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function init() {
|
||||
instance = new BetterDiscord();
|
||||
}
|
||||
Events.on('autopatcher', init);
|
||||
ReactAutoPatcher.autoPatch().then(() => Events.emit('autopatcher'));
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@ export default new class extends Module {
|
|||
|
||||
constructor(args) {
|
||||
super(args);
|
||||
}
|
||||
|
||||
initg() {
|
||||
this.first();
|
||||
}
|
||||
|
||||
|
|
|
@ -15,3 +15,5 @@ export { default as Permissions } from './permissionmanager';
|
|||
export { default as Database } from './database';
|
||||
export { default as EventsWrapper } from './eventswrapper';
|
||||
export { default as DiscordApi } from './discordapi';
|
||||
export { default as Patcher } from './patcher';
|
||||
export * from './reactcomponents';
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* BetterDiscord Component Patcher
|
||||
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
|
||||
* All rights reserved.
|
||||
* https://github.com/JsSucks - https://betterdiscord.net
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import WebpackModules from './webpackmodules';
|
||||
|
||||
export default class Patcher {
|
||||
static get patches() { return this._patches || (this._patches = {}) }
|
||||
static resolveModule(mn) {
|
||||
if (mn instanceof Function || (mn instanceof Object && !(mn instanceof Array))) return mn;
|
||||
if ('string' === typeof mn) return WebpackModules.getModuleByName(mn);
|
||||
if (mn instanceof Array) return WebpackModules.getModuleByProps(mn);
|
||||
return null;
|
||||
}
|
||||
static overrideFn(patch) {
|
||||
return function () {
|
||||
for (const s of patch.supers) {
|
||||
try {
|
||||
s.fn.apply(this, arguments);
|
||||
} catch (err) { }
|
||||
}
|
||||
const retVal = patch.ofn.apply(this, arguments);
|
||||
for (const s of patch.slaves) {
|
||||
try {
|
||||
s.fn.apply(this, [arguments, { patch, retVal }]);
|
||||
} catch (err) { }
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
}
|
||||
|
||||
static rePatch(po) {
|
||||
po.patch = po.module[po.fnn] = this.overrideFn(po);
|
||||
}
|
||||
|
||||
static pushPatch(id, module, fnn) {
|
||||
const patch = {
|
||||
module,
|
||||
fnn,
|
||||
ofn: module[fnn],
|
||||
revert: () => {
|
||||
patch.module[patch.fnn] = patch.ofn;
|
||||
patch.patch = null;
|
||||
patch.slaves = patch.supers = [];
|
||||
},
|
||||
supers: [],
|
||||
slaves: [],
|
||||
patch: null
|
||||
};
|
||||
patch.patch = module[fnn] = this.overrideFn(patch);
|
||||
return this.patches[id] = patch;
|
||||
}
|
||||
|
||||
static superpatch(mn, fnn, cb, dn) {
|
||||
const module = this.resolveModule(mn);
|
||||
if (!module || !module[fnn] || !(module[fnn] instanceof Function)) return null;
|
||||
const displayName = 'string' === typeof mn ? mn : dn || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
|
||||
const patchId = `${displayName}:${fnn}`;
|
||||
const patchObject = this.patches[patchId] || this.pushPatch(patchId, module, fnn);
|
||||
if (!patchObject.patch) this.rePatch(patchObject);
|
||||
const id = patchObject.supers.length + 1;
|
||||
const patch = {
|
||||
id,
|
||||
fn: cb,
|
||||
unpatch: () => patchObject.supers.splice(patchObject.supers.findIndex(slave => slave.id === id), 1)
|
||||
};
|
||||
patchObject.supers.push(patch);
|
||||
return patch;
|
||||
}
|
||||
|
||||
static slavepatch(mn, fnn, cb, dn) {
|
||||
const module = this.resolveModule(mn);
|
||||
if (!module || !module[fnn] || !(module[fnn] instanceof Function)) return null;
|
||||
const displayName = 'string' === typeof mn ? mn : dn || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
|
||||
const patchId = `${displayName}:${fnn}`;
|
||||
const patchObject = this.patches[patchId] || this.pushPatch(patchId, module, fnn);
|
||||
if (!patchObject.patch) this.rePatch(patchObject);
|
||||
const id = patchObject.slaves.length + 1;
|
||||
const patch = {
|
||||
id,
|
||||
fn: cb,
|
||||
unpatch: () => patchObject.slaves.splice(patchObject.slaves.findIndex(slave => slave.id === id), 1)
|
||||
};
|
||||
patchObject.slaves.push(patch);
|
||||
return patch;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,426 @@
|
|||
/**
|
||||
* BetterDiscord React Component Manipulations
|
||||
* original concept and some code by samogot - https://github.com/samogot / https://github.com/samogot/betterdiscord-plugins/tree/master/v2/1Lib%20Discord%20Internals
|
||||
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
|
||||
* All rights reserved.
|
||||
* https://github.com/JsSucks - https://betterdiscord.net
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import Patcher from './patcher';
|
||||
import WebpackModules from './webpackmodules';
|
||||
import DiscordApi from './discordapi';
|
||||
import { EmoteModule } from 'builtin';
|
||||
import { Reflection } from 'ui';
|
||||
|
||||
class Filters {
|
||||
static get byPrototypeFields() {
|
||||
return (fields, selector = x => x) => (module) => {
|
||||
const component = selector(module);
|
||||
if (!component) return false;
|
||||
if (!component.prototype) return false;
|
||||
for (const field of fields) {
|
||||
if (!component.prototype[field]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
static get byCode() {
|
||||
return (search, selector = x => x) => (module) => {
|
||||
const method = selector(module);
|
||||
if (!method) return false;
|
||||
return method.toString().search(search) !== -1;
|
||||
}
|
||||
}
|
||||
static get and() {
|
||||
return (...filters) => (module) => {
|
||||
for (const filter of filters) {
|
||||
if (!filter(module)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Helpers {
|
||||
static get plannedActions() {
|
||||
return this._plannedActions || (this._plannedActions = new Map());
|
||||
}
|
||||
static recursiveArray(parent, key, count = 1) {
|
||||
let index = 0;
|
||||
function* innerCall(parent, key) {
|
||||
const item = parent[key];
|
||||
if (item instanceof Array) {
|
||||
for (const subKey of item.keys()) {
|
||||
yield* innerCall(item, subKey)
|
||||
}
|
||||
return;
|
||||
}
|
||||
yield { item, parent, key, index: index++, count };
|
||||
}
|
||||
|
||||
return innerCall(parent, key);
|
||||
}
|
||||
static recursiveArrayCount(parent, key) {
|
||||
let count = 0;
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
for (let { } of this.recursiveArray(parent, key))
|
||||
++count;
|
||||
return this.recursiveArray(parent, key, count);
|
||||
}
|
||||
static get recursiveChildren() {
|
||||
return function*(parent, key, index = 0, count = 1) {
|
||||
const item = parent[key];
|
||||
yield { item, parent, key, index, count };
|
||||
if (item && item.props && item.props.children) {
|
||||
for (let { parent, key, index, count } of this.recursiveArrayCount(item.props, 'children')) {
|
||||
yield* this.recursiveChildren(parent, key, index, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static returnFirst(iterator, process) {
|
||||
for (let child of iterator) {
|
||||
const retVal = process(child);
|
||||
if (retVal !== undefined) return retVal;
|
||||
}
|
||||
}
|
||||
static getFirstChild(rootParent, rootKey, selector) {
|
||||
const getDirectChild = (item, selector) => {
|
||||
if (item && item.props && item.props.children) {
|
||||
return this.returnFirst(this.recursiveArrayCount(item.props, 'children'), checkFilter.bind(null, selector));
|
||||
}
|
||||
};
|
||||
const checkFilter = (selector, { item, parent, key, count, index }) => {
|
||||
let match = true;
|
||||
if (match && selector.type)
|
||||
match = item && selector.type === item.type;
|
||||
if (match && selector.tag)
|
||||
match = item && typeof item.type === 'string' && selector.tag === item.type;
|
||||
if (match && selector.className) {
|
||||
match = item && item.props && typeof item.props.className === 'string';
|
||||
if (match) {
|
||||
const classes = item.props.className.split(' ');
|
||||
if (selector.className === true)
|
||||
match = !!classes[0];
|
||||
else if (typeof selector.className === 'string')
|
||||
match = classes.includes(selector.className);
|
||||
else if (selector.className instanceof RegExp)
|
||||
match = !!classes.find(cls => selector.className.test(cls));
|
||||
else match = false;
|
||||
}
|
||||
}
|
||||
if (match && selector.text) {
|
||||
if (selector.text === true)
|
||||
match = typeof item === 'string';
|
||||
else if (typeof selector.text === 'string')
|
||||
match = item === selector.text;
|
||||
else if (selector.text instanceof RegExp)
|
||||
match = typeof item === 'string' && selector.text.test(item);
|
||||
else match = false;
|
||||
}
|
||||
if (match && selector.nthChild)
|
||||
match = index === (selector.nthChild < 0 ? count + selector.nthChild : selector.nthChild);
|
||||
if (match && selector.hasChild)
|
||||
match = getDirectChild(item, selector.hasChild);
|
||||
if (match && selector.hasSuccessor)
|
||||
match = item && !!this.getFirstChild(parent, key, selector.hasSuccessor).item;
|
||||
if (match && selector.eq) {
|
||||
--selector.eq;
|
||||
return;
|
||||
}
|
||||
if (match) {
|
||||
if (selector.child) {
|
||||
return getDirectChild(item, selector.child);
|
||||
}
|
||||
else if (selector.successor) {
|
||||
return this.getFirstChild(parent, key, selector.successor);
|
||||
}
|
||||
else {
|
||||
return { item, parent, key };
|
||||
}
|
||||
}
|
||||
};
|
||||
return this.returnFirst(this.recursiveChildren(rootParent, rootKey), checkFilter.bind(null, selector)) || {};
|
||||
}
|
||||
static parseSelector(selector) {
|
||||
if (selector.startsWith('.')) return { className: selector.substr(1) }
|
||||
if (selector.startsWith('#')) return { id: selector.substr(1) }
|
||||
return {}
|
||||
}
|
||||
static findByProp(obj, what, value) {
|
||||
if (obj.hasOwnProperty(what) && obj[what] === value) return obj;
|
||||
if (obj.props && !obj.children) return this.findByProp(obj.props, what, value);
|
||||
if (!obj.children || !obj.children.length) return null;
|
||||
for (const child of obj.children) {
|
||||
if (!child) continue;
|
||||
const findInChild = this.findByProp(child, what, value);
|
||||
if (findInChild) return findInChild;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
static get ReactDOM() {
|
||||
return WebpackModules.getModuleByName('ReactDOM');
|
||||
}
|
||||
}
|
||||
|
||||
class ReactComponent {
|
||||
constructor(id, component, retVal) {
|
||||
this._id = id;
|
||||
this._component = component;
|
||||
this._retVal = retVal;
|
||||
const self = this;
|
||||
Patcher.slavepatch(this.component.prototype, 'componentDidMount', function (a, parv) {
|
||||
self.eventCallback('componentDidMount', {
|
||||
props: this.props,
|
||||
state: this.state,
|
||||
element: Helpers.ReactDOM.findDOMNode(this),
|
||||
retVal: parv.retVal
|
||||
});
|
||||
});
|
||||
Patcher.slavepatch(this.component.prototype, 'componentDidUpdate', function(a, parv) {
|
||||
self.eventCallback('componentDidUpdate', {
|
||||
prevProps: a[0],
|
||||
prevState: a[1],
|
||||
props: this.props,
|
||||
state: this.state,
|
||||
element: Helpers.ReactDOM.findDOMNode(this),
|
||||
retVal: parv.retVal
|
||||
});
|
||||
});
|
||||
Patcher.slavepatch(this.component.prototype, 'render', function (a, parv) {
|
||||
self.eventCallback('render', {
|
||||
component: this,
|
||||
retVal: parv.retVal,
|
||||
p: parv
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
eventCallback(event, eventData) {
|
||||
for (const listener of this.events.find(e => e.id === event).listeners) {
|
||||
listener(eventData);
|
||||
}
|
||||
}
|
||||
|
||||
get events() {
|
||||
return this._events || (this._events = [
|
||||
{ id: 'componentDidMount', listeners: [] },
|
||||
{ id: 'componentDidUpdate', listeners: [] },
|
||||
{ id: 'render', listeners: [] }
|
||||
]);
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
const have = this.events.find(e => e.id === event);
|
||||
if (!have) return;
|
||||
have.listeners.push(callback);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get component() {
|
||||
return this._component;
|
||||
}
|
||||
|
||||
get retVal() {
|
||||
return this._retVal;
|
||||
}
|
||||
|
||||
unpatchRender() {
|
||||
|
||||
}
|
||||
/*
|
||||
patchRender(actions, updateOthers) {
|
||||
const self = this;
|
||||
if (!(actions instanceof Array)) actions = [actions];
|
||||
Patcher.slavepatch(this.component.prototype, 'render', function (args, obj) {
|
||||
console.log('obj', obj);
|
||||
for (const action of actions) {
|
||||
let { selector, method, fn } = action;
|
||||
if ('string' === typeof selector) selector = Helpers.parseSelector(selector);
|
||||
const { item, parent, key } = Helpers.getFirstChild(obj, 'retVal', selector);
|
||||
console.log('item2', item);
|
||||
if (!item) continue;
|
||||
const content = fn.apply(this, [item]);
|
||||
switch (method) {
|
||||
case 'replace':
|
||||
parent[key] = content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (updateOthers) self.forceUpdateOthers();
|
||||
});
|
||||
}
|
||||
*/
|
||||
forceUpdateOthers() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class ReactAutoPatcher {
|
||||
static async autoPatch() {
|
||||
await this.ensureReact();
|
||||
Patcher.superpatch('React', 'createElement', (component, retVal) => ReactComponents.push(component, retVal));
|
||||
this.patchem();
|
||||
return 1;
|
||||
}
|
||||
static async ensureReact() {
|
||||
while (!window.webpackJsonp || !WebpackModules.getModuleByName('React')) await new Promise(resolve => setTimeout(resolve, 10));
|
||||
return 1;
|
||||
}
|
||||
static patchem() {
|
||||
this.patchMessage();
|
||||
this.patchMessageGroup();
|
||||
this.patchChannelMember();
|
||||
}
|
||||
|
||||
static async patchMessage() {
|
||||
this.Message.component = await ReactComponents.getComponent('Message', true, { selector: '.message' });
|
||||
this.Message.component.on('render', ({ component, retVal, p }) => {
|
||||
const { message } = component.props;
|
||||
const { id, colorString, bot, author, attachments, embeds } = message;
|
||||
retVal.props['data-message-id'] = id;
|
||||
retVal.props['data-colourstring'] = colorString;
|
||||
if (author && author.id) retVal.props['data-user-id'] = author.id;
|
||||
if (bot || (author && author.bot)) retVal.props.className += ' bd-isBot';
|
||||
if (attachments && attachments.length) retVal.props.className += ' bd-hasAttachments';
|
||||
if (embeds && embeds.length) retVal.props.className += ' bd-hasEmbeds';
|
||||
if (author && author.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
|
||||
try {
|
||||
const markup = Helpers.findByProp(retVal, 'className', 'markup').children; // First child has all the actual text content, second is the edited timestamp
|
||||
markup[0] = EmoteModule.processMarkup(markup[0]);
|
||||
} catch (err) {
|
||||
console.error('MARKUP PARSER ERROR', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async patchMessageGroup() {
|
||||
ReactComponents.setName('MessageGroup', this.MessageGroup.filter);
|
||||
this.MessageGroup.component = await ReactComponents.getComponent('MessageGroup', true, { selector: '.message-group' });
|
||||
this.MessageGroup.component.on('render', ({ component, retVal, p }) => {
|
||||
const authorid = component.props.messages[0].author.id;
|
||||
retVal.props['data-author-id'] = authorid;
|
||||
if (authorid === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
|
||||
});
|
||||
}
|
||||
|
||||
static async patchChannelMember() {
|
||||
this.ChannelMember.component = await ReactComponents.getComponent('ChannelMember');
|
||||
this.ChannelMember.component.on('render', ({ component, retVal, p }) => {
|
||||
const { user, isOwner } = component.props;
|
||||
retVal.props.children.props['data-member-id'] = user.id;
|
||||
if (user.id === DiscordApi.currentUser.id) retVal.props.children.props.className += ' bd-isCurrentUser';
|
||||
if (isOwner) retVal.props.children.props.className += ' bd-isOwner';
|
||||
});
|
||||
}
|
||||
|
||||
static get MessageGroup() {
|
||||
return this._messageGroup || (
|
||||
this._messageGroup = {
|
||||
filter: Filters.byCode(/"message-group"[\s\S]*"has-divider"[\s\S]*"hide-overflow"[\s\S]*"is-local-bot-message"/, c => c.prototype && c.prototype.render)
|
||||
});
|
||||
}
|
||||
|
||||
static get Message() {
|
||||
return this._message || (this._message = {});
|
||||
}
|
||||
|
||||
static get ChannelMember() {
|
||||
return this._channelMember || (
|
||||
this._channelMember = {});
|
||||
}
|
||||
}
|
||||
|
||||
export class ReactComponents {
|
||||
static get components() { return this._components || (this._components = []) }
|
||||
static get unknownComponents() { return this._unknownComponents || (this._unknownComponents = [])}
|
||||
static get listeners() { return this._listeners || (this._listeners = []) }
|
||||
static get nameSetters() { return this._nameSetters || (this._nameSetters =[])}
|
||||
|
||||
static push(component, retVal) {
|
||||
if (!(component instanceof Function)) return null;
|
||||
const { displayName } = component;
|
||||
if (!displayName) {
|
||||
return this.processUnknown(component, retVal);
|
||||
}
|
||||
const have = this.components.find(comp => comp.id === displayName);
|
||||
if (have) return component;
|
||||
const c = new ReactComponent(displayName, component, retVal);
|
||||
this.components.push(c);
|
||||
const listener = this.listeners.find(listener => listener.id === displayName);
|
||||
if (!listener) return c;
|
||||
for (const l of listener.listeners) {
|
||||
l(c);
|
||||
}
|
||||
this.listeners.splice(this.listeners.findIndex(listener => listener.id === displayName), 1);
|
||||
return c;
|
||||
}
|
||||
|
||||
static async getComponent(name, important, importantArgs) {
|
||||
const have = this.components.find(c => c.id === name);
|
||||
if (have) return have;
|
||||
if (important) {
|
||||
const importantInterval = setInterval(() => {
|
||||
if (this.components.find(c => c.id === name)) {
|
||||
console.info(`Important component ${name} already found`);
|
||||
clearInterval(importantInterval);
|
||||
return;
|
||||
}
|
||||
const select = document.querySelector(importantArgs.selector);
|
||||
if (!select) return;
|
||||
const reflect = Reflection(select);
|
||||
if (!reflect.component) {
|
||||
clearInterval(important);
|
||||
console.error(`FAILED TO GET IMPORTANT COMPONENT ${name} WITH REFLECTION FROM`, select);
|
||||
return;
|
||||
}
|
||||
if (!reflect.component.displayName) reflect.component.displayName = name;
|
||||
console.info(`Found important component ${name} with reflection.`);
|
||||
this.push(reflect.component);
|
||||
clearInterval(importantInterval);
|
||||
}, 50);
|
||||
}
|
||||
const listener = this.listeners.find(l => l.id === name);
|
||||
if (!listener) this.listeners.push({
|
||||
id: name,
|
||||
listeners: []
|
||||
});
|
||||
return new Promise(resolve => {
|
||||
this.listeners.find(l => l.id === name).listeners.push(c => resolve(c));
|
||||
});
|
||||
}
|
||||
|
||||
static setName(name, filter, callback) {
|
||||
const have = this.components.find(c => c.id === name);
|
||||
if (have) return have;
|
||||
|
||||
for (const [rci, rc] of this.unknownComponents.entries()) {
|
||||
if (filter(rc.component)) {
|
||||
rc.component.displayName = name;
|
||||
this.unknownComponents.splice(rci, 1);
|
||||
return this.push(rc.component);
|
||||
}
|
||||
}
|
||||
return this.nameSetters.push({ name, filter });
|
||||
}
|
||||
|
||||
static processUnknown(component, retVal) {
|
||||
const have = this.unknownComponents.find(c => c.component === component);
|
||||
for (const [fi, filter] of this.nameSetters.entries()) {
|
||||
if (filter.filter(component)) {
|
||||
component.displayName = filter.name;
|
||||
this.nameSetters.splice(fi, 1);
|
||||
return this.push(component, retVal);
|
||||
}
|
||||
}
|
||||
if (have) return have;
|
||||
this.unknownComponents.push(component);
|
||||
return component;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
.bd-emote-outer.bd-is-emote {
|
||||
font-size: 0;
|
||||
}
|
||||
.bd-emotewrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
img {
|
||||
max-height: 32px;
|
||||
}
|
||||
}
|
|
@ -12,3 +12,4 @@
|
|||
@import './discordoverrides.scss';
|
||||
@import './helpers.scss';
|
||||
@import './misc.scss';
|
||||
@import './emote.scss';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { Events, WebpackModules, EventListener } from 'modules';
|
||||
import { Events, WebpackModules, EventListener, ReactComponents, Renderer } from 'modules';
|
||||
import Reflection from './reflection';
|
||||
import DOM from './dom';
|
||||
import VueInjector from './vueinjector';
|
||||
|
@ -41,72 +41,8 @@ class TempApi {
|
|||
|
||||
export default class extends EventListener {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const messageFilter = function (m) {
|
||||
return m.addedNodes && m.addedNodes.length && m.addedNodes[0].classList && m.addedNodes[0].classList.contains('message-group');
|
||||
}
|
||||
|
||||
DOM.observer.subscribe('loading-more-manip', messageFilter, mutations => {
|
||||
this.setIds();
|
||||
this.makeMutable();
|
||||
Events.emit('ui:laodedmore', mutations.map(m => m.addedNodes[0]));
|
||||
}, 'filter');
|
||||
|
||||
const userFilter = function (m) {
|
||||
return m.addedNodes && m.addedNodes.length && m.addedNodes[0].classList && m.addedNodes[0].classList.contains('member');
|
||||
}
|
||||
|
||||
DOM.observer.subscribe('loading-more-users-manip', userFilter, mutations => {
|
||||
this.setUserIds();
|
||||
Events.emit('ui:loadedmoreusers', mutations.map(m => m.addedNodes[0]));
|
||||
}, 'filter');
|
||||
|
||||
const channelFilter = function(m) {
|
||||
return m.addedNodes &&
|
||||
m.addedNodes.length &&
|
||||
m.addedNodes[0].className &&
|
||||
m.addedNodes[0].className.includes('container');
|
||||
}
|
||||
|
||||
DOM.observer.subscribe('loading-more-channels-manip', channelFilter, mutations => {
|
||||
this.setChannelIds();
|
||||
Events.emit('ui:loadedmorechannels', mutations.map(m => m.addedNodes[0]));
|
||||
}, 'filter');
|
||||
|
||||
const popoutFilter = function(m) {
|
||||
return m.addedNodes &&
|
||||
m.addedNodes.length &&
|
||||
m.addedNodes[0].className &&
|
||||
m.addedNodes[0].className.includes('popout');
|
||||
}
|
||||
|
||||
DOM.observer.subscribe('userpopout-manip', popoutFilter, mutations => {
|
||||
const userPopout = document.querySelector('[class*=userPopout]');
|
||||
if (!userPopout) return;
|
||||
const user = Reflection(userPopout).prop('user');
|
||||
if (!user) return;
|
||||
userPopout.setAttribute('data-user-id', user.id);
|
||||
if (user.id === TempApi.currentUserId) userPopout.setAttribute('data-currentuser', true);
|
||||
}, 'filter');
|
||||
|
||||
const modalFilter = function(m) {
|
||||
return m.addedNodes &&
|
||||
m.addedNodes.length &&
|
||||
m.addedNodes[0].className &&
|
||||
m.addedNodes[0].className.includes('modal');
|
||||
}
|
||||
|
||||
DOM.observer.subscribe('modal-manip', modalFilter, mutations => {
|
||||
const userModal = document.querySelector('[class*=modal] > [class*=inner]');
|
||||
if (!userModal) return;
|
||||
const user = Reflection(userModal).prop('user');
|
||||
if (!user) return;
|
||||
const modal = userModal.closest('[class*=modal]');
|
||||
if (!modal) return;
|
||||
modal.setAttribute('data-user-id', user.id);
|
||||
if (user.id === TempApi.currentUserId) modal.setAttribute('data-currentuser', true);
|
||||
});
|
||||
constructor(args) {
|
||||
super(args);
|
||||
}
|
||||
|
||||
bindings() {
|
||||
|
@ -118,6 +54,8 @@ export default class extends EventListener {
|
|||
}
|
||||
|
||||
get eventBindings() {
|
||||
return [{ id: 'gkh:keyup', callback: this.injectAutocomplete }];
|
||||
/*
|
||||
return [
|
||||
{ id: 'server-switch', callback: this.manipAll },
|
||||
{ id: 'channel-switch', callback: this.manipAll },
|
||||
|
@ -125,6 +63,7 @@ export default class extends EventListener {
|
|||
{ id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector },
|
||||
{ id: 'gkh:keyup', callback: this.injectAutocomplete }
|
||||
];
|
||||
*/
|
||||
}
|
||||
|
||||
manipAll() {
|
||||
|
|
|
@ -57,10 +57,10 @@ export default class {
|
|||
if (!this.profilePopupModule) return;
|
||||
clearInterval(defer);
|
||||
|
||||
Utils.monkeyPatch(this.profilePopupModule, 'open', 'after', (data, userid) => Events.emit('ui-event', {
|
||||
/*Utils.monkeyPatch(this.profilePopupModule, 'open', 'after', (data, userid) => Events.emit('ui-event', {
|
||||
event: 'profile-popup-open',
|
||||
data: { userid }
|
||||
}));
|
||||
}));*/
|
||||
}, 100);
|
||||
|
||||
const ehookInterval = setInterval(() => {
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(emote, index) in emotes" class="bd-autocompleteRow" :key="emote.id">
|
||||
<div v-for="(emote, index) in emotes" class="bd-autocompleteRow" :key="index">
|
||||
<div class="bd-autocompleteSelector bd-selectable" :class="{'bd-selected': index === selectedIndex}" @mouseover="() => { selected = emote.id }" @click="() => inject(emote)">
|
||||
<div class="bd-autocompleteField">
|
||||
<img :src="getEmoteSrc(emote)"/>
|
||||
|
@ -54,12 +54,14 @@
|
|||
created() {
|
||||
window.addEventListener('keydown', this.prevents);
|
||||
const ta = document.querySelector('.chat textarea');
|
||||
if(!ta) return;
|
||||
ta.addEventListener('keydown', this.setCaret);
|
||||
ta.addEventListener('keyup', this.searchEmotes);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('keydown', this.prevents);
|
||||
const ta = document.querySelector('.chat textarea');
|
||||
if (!ta) return;
|
||||
ta.removeEventListener('keydown', this.setCaret);
|
||||
ta.removeEventListener('keyup', this.searchEmotes);
|
||||
},
|
||||
|
@ -80,48 +82,59 @@
|
|||
return uri.replace(':id', value);
|
||||
},
|
||||
searchEmotes(e) {
|
||||
if (e.key === 'ArrowDown' && this.open && this.caret) {
|
||||
this.selectedIndex = (this.selectedIndex + 1) >= 10 ? 0 : this.selectedIndex + 1;
|
||||
return;
|
||||
} else if (e.key === 'ArrowUp' && this.open && this.caret) {
|
||||
this.selectedIndex = (this.selectedIndex - 1) < 0 ? 9 : this.selectedIndex - 1;
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Tab' && this.open && this.caret) {
|
||||
if (this.traverse(e)) return;
|
||||
if (e.key === 'Tab' && this.open) {
|
||||
const selected = this.emotes[this.selectedIndex];
|
||||
if (!selected) return;
|
||||
this.inject(selected);
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
const se = e.target.selectionEnd;
|
||||
this.sterm = e.target.value.substr(0, se).split(' ').slice(-1).pop();
|
||||
if (e.key === 'Tab' && !this.open) this.open = true;
|
||||
if (!this.open) return;
|
||||
const { selectionEnd, value } = e.target;
|
||||
this.sterm = value.substr(0, selectionEnd).split(/\s+/g).pop();
|
||||
|
||||
if (this.sterm.length < 3) {
|
||||
this.emotes = [];
|
||||
this.selected = '';
|
||||
this.selectedIndex = 0;
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
this.title = this.sterm;
|
||||
this.emotes = EmoteModule.filter(new RegExp(this.sterm, ''), 10);
|
||||
this.open = this.emotes.length;
|
||||
},
|
||||
traverse(e) {
|
||||
if (!this.open) return false;
|
||||
if (e.key === 'ArrowUp') {
|
||||
this.selectedIndex = (this.selectedIndex - 1) < 0 ? 9 : this.selectedIndex - 1;
|
||||
return true;
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
this.selectedIndex = (this.selectedIndex + 1) >= 10 ? 0 : this.selectedIndex + 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
reset() {
|
||||
this.emotes = [];
|
||||
this.title = '';
|
||||
this.selIndex = 0;
|
||||
this.selected = '';
|
||||
this.open = false;
|
||||
this.selectedIndex = 0;
|
||||
this.sterm = '';
|
||||
},
|
||||
inject(emote) {
|
||||
const ta = document.querySelector('.chat textarea');
|
||||
if (!ta) return;
|
||||
const currentText = document.querySelector('.chat textarea').value;
|
||||
const se = ta.selectionEnd;
|
||||
const split = currentText.substr(0, se).split(' ');
|
||||
split.pop();
|
||||
split.push(`:${emote.id}:`);
|
||||
const join = split.join(' ');
|
||||
const rest = currentText.substr(se, currentText.length);
|
||||
DOM.manip.setText(join + ' ' + rest, false);
|
||||
this.emotes = [];
|
||||
this.open = false;
|
||||
this.selectedIndex = 0;
|
||||
this.selected = '';
|
||||
ta.selectionEnd = ta.selectionStart = se + `:${emote.id}:`.length - this.title.length;
|
||||
const { selectionEnd, value } = ta;
|
||||
const en = `:${emote.id}:`;
|
||||
let substr = value.substr(0, selectionEnd);
|
||||
substr = substr.replace(new RegExp(this.sterm + '$'), en);
|
||||
|
||||
DOM.manip.setText(substr + value.substr(selectionEnd, value.length), false);
|
||||
ta.selectionEnd = ta.selectionStart = selectionEnd + en.length - this.sterm.length;
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,23 @@ class Reflection {
|
|||
}
|
||||
return this.propIterator(curProp, propNames);
|
||||
}
|
||||
|
||||
static getState(node) {
|
||||
try {
|
||||
return this.reactInternalInstance(node).return.stateNode.state;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static getComponent(node) {
|
||||
// IMPORTANT TODO Currently only checks the first found component. For example channel-member will not return the correct component
|
||||
try {
|
||||
return this.reactInternalInstance(node).return.type;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function (node) {
|
||||
|
@ -91,9 +108,15 @@ export default function (node) {
|
|||
get props() {
|
||||
return 'not yet implemented';
|
||||
}
|
||||
get state() {
|
||||
return Reflection.getState(this.node);
|
||||
}
|
||||
get reactInternalInstance() {
|
||||
return Reflection.reactInternalInstance(this.node);
|
||||
}
|
||||
get component() {
|
||||
return Reflection.getComponent(this.node);
|
||||
}
|
||||
prop(propName) {
|
||||
const split = propName.split('.');
|
||||
const first = Reflection.findProp(this.node, split[0]);
|
||||
|
|
Loading…
Reference in New Issue