diff --git a/client/src/builtin/EmoteComponent.vue b/client/src/builtin/EmoteComponent.vue
index 3eba95ac..8ee88531 100644
--- a/client/src/builtin/EmoteComponent.vue
+++ b/client/src/builtin/EmoteComponent.vue
@@ -19,12 +19,3 @@
}
}
-
diff --git a/client/src/builtin/EmoteModule.js b/client/src/builtin/EmoteModule.js
index dd66bfce..b3ef75f7 100644
--- a/client/src/builtin/EmoteModule.js
+++ b/client/src/builtin/EmoteModule.js
@@ -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 },
+ ``
+ );
+ 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 },
- ``,
- 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) {
diff --git a/client/src/index.js b/client/src/index.js
index b7bb9f3d..ef652907 100644
--- a/client/src/index.js
+++ b/client/src/index.js
@@ -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'));
}
diff --git a/client/src/modules/globals.js b/client/src/modules/globals.js
index adc1cc71..f355f051 100644
--- a/client/src/modules/globals.js
+++ b/client/src/modules/globals.js
@@ -16,6 +16,9 @@ export default new class extends Module {
constructor(args) {
super(args);
+ }
+
+ initg() {
this.first();
}
diff --git a/client/src/modules/modules.js b/client/src/modules/modules.js
index f13d3bb3..c6311c53 100644
--- a/client/src/modules/modules.js
+++ b/client/src/modules/modules.js
@@ -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';
diff --git a/client/src/modules/patcher.js b/client/src/modules/patcher.js
new file mode 100644
index 00000000..50206919
--- /dev/null
+++ b/client/src/modules/patcher.js
@@ -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;
+ }
+}
diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js
new file mode 100644
index 00000000..7eca4762
--- /dev/null
+++ b/client/src/modules/reactcomponents.js
@@ -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;
+ }
+}
diff --git a/client/src/styles/partials/emote.scss b/client/src/styles/partials/emote.scss
new file mode 100644
index 00000000..d3251f83
--- /dev/null
+++ b/client/src/styles/partials/emote.scss
@@ -0,0 +1,11 @@
+.bd-emote-outer.bd-is-emote {
+ font-size: 0;
+}
+.bd-emotewrapper {
+ position: relative;
+ display: inline-block;
+
+ img {
+ max-height: 32px;
+ }
+}
diff --git a/client/src/styles/partials/index.scss b/client/src/styles/partials/index.scss
index aeb1d353..671cece8 100644
--- a/client/src/styles/partials/index.scss
+++ b/client/src/styles/partials/index.scss
@@ -12,3 +12,4 @@
@import './discordoverrides.scss';
@import './helpers.scss';
@import './misc.scss';
+@import './emote.scss';
diff --git a/client/src/ui/automanip.js b/client/src/ui/automanip.js
index 0cf85ae2..0eb69629 100644
--- a/client/src/ui/automanip.js
+++ b/client/src/ui/automanip.js
@@ -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() {
diff --git a/client/src/ui/bdui.js b/client/src/ui/bdui.js
index 091193a2..009a2905 100644
--- a/client/src/ui/bdui.js
+++ b/client/src/ui/bdui.js
@@ -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(() => {
diff --git a/client/src/ui/components/common/Autocomplete.vue b/client/src/ui/components/common/Autocomplete.vue
index 4c248be8..9625e8d3 100644
--- a/client/src/ui/components/common/Autocomplete.vue
+++ b/client/src/ui/components/common/Autocomplete.vue
@@ -19,7 +19,7 @@
-
+
{ selected = emote.id }" @click="() => inject(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();
}
}
}
diff --git a/client/src/ui/reflection.js b/client/src/ui/reflection.js
index 5c2903cb..9d6a0b58 100644
--- a/client/src/ui/reflection.js
+++ b/client/src/ui/reflection.js
@@ -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]);