Merge pull request #168 from JsSucks/optimize

Optimize - Requires Core Rebuild(?)
This commit is contained in:
Alexei Stukov 2018-03-14 11:16:11 +02:00 committed by GitHub
commit d1dc8140e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 723 additions and 186 deletions

View File

@ -19,12 +19,3 @@
}
}
</script>
<style>
.bd-emotewrapper {
position: relative;
display: inline-block;
}
.bd-emotewrapper img {
max-height: 32px;
}
</style>

View File

@ -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) {

View File

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

View File

@ -16,6 +16,9 @@ export default new class extends Module {
constructor(args) {
super(args);
}
initg() {
this.first();
}

View File

@ -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';

View File

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

View File

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

View File

@ -0,0 +1,11 @@
.bd-emote-outer.bd-is-emote {
font-size: 0;
}
.bd-emotewrapper {
position: relative;
display: inline-block;
img {
max-height: 32px;
}
}

View File

@ -12,3 +12,4 @@
@import './discordoverrides.scss';
@import './helpers.scss';
@import './misc.scss';
@import './emote.scss';

View File

@ -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() {

View File

@ -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(() => {

View File

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

View File

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