From 5083a80ba2ddc954646c76cab7df51b3ed826f94 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Sun, 11 Mar 2018 17:58:09 +0200 Subject: [PATCH 01/21] Remove observers --- client/src/ui/automanip.js | 68 -------------------------------------- 1 file changed, 68 deletions(-) diff --git a/client/src/ui/automanip.js b/client/src/ui/automanip.js index 0cf85ae2..db529cb4 100644 --- a/client/src/ui/automanip.js +++ b/client/src/ui/automanip.js @@ -41,74 +41,6 @@ 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); - }); - } - bindings() { this.manipAll = this.manipAll.bind(this); this.markupInjector = this.markupInjector.bind(this); From 6a854ab070e7f3ed01dce5964ef2d920e7214ce1 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Mon, 12 Mar 2018 17:36:11 +0200 Subject: [PATCH 02/21] Patcher --- client/src/modules/globals.js | 3 + client/src/modules/patcher.js | 83 +++++++++++ client/src/modules/reactcomponents.js | 192 ++++++++++++++++++++++++++ client/src/ui/automanip.js | 9 +- client/src/ui/bdui.js | 4 +- core/src/main.js | 2 +- 6 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 client/src/modules/patcher.js create mode 100644 client/src/modules/reactcomponents.js 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/patcher.js b/client/src/modules/patcher.js new file mode 100644 index 00000000..f6dbf789 --- /dev/null +++ b/client/src/modules/patcher.js @@ -0,0 +1,83 @@ +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..abcbb243 --- /dev/null +++ b/client/src/modules/reactcomponents.js @@ -0,0 +1,192 @@ +import Patcher from './patcher'; + +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; + 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 {} + } +} + +class ReactComponent { + constructor(id, component, retVal) { + this._id = id; + this._component = component; + this._retVal = retVal; + } + + 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) { + 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); + if (!item) continue; + const content = fn.apply(this, [item]); + switch (method) { + case 'replace': + parent[key] = content; + break; + } + } + if (updateOthers) self.forceUpdateOthers(); + }); + } + + forceUpdateOthers() { + + } +} + +export default class ReactComponents { + static get components() { return this._components || (this._components = []) } + static get listeners() { return this._listeners || (this._listeners = []) } + + static push(component, retVal) { + if (!(component instanceof Function)) return null; + const { displayName } = component; + if (!displayName) return null; + 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) { + const have = this.components.find(c => c.id === name); + if (have) return have; + 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)); + }); + } + +} diff --git a/client/src/ui/automanip.js b/client/src/ui/automanip.js index db529cb4..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,6 +41,10 @@ class TempApi { export default class extends EventListener { + constructor(args) { + super(args); + } + bindings() { this.manipAll = this.manipAll.bind(this); this.markupInjector = this.markupInjector.bind(this); @@ -50,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 }, @@ -57,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/core/src/main.js b/core/src/main.js index ed7c39d5..28895fcb 100644 --- a/core/src/main.js +++ b/core/src/main.js @@ -150,7 +150,7 @@ class BetterDiscord { //this.windowUtils.webContents.on('did-finish-load', e => this.injectScripts(true)); this.windowUtils.events('did-get-response-details', () => this.ignite(this.windowUtils.window)); - this.windowUtils.events('did-finish-load', e => this.injectScripts(true)); + this.windowUtils.events('did-get-response-details', e => this.injectScripts(true)); this.windowUtils.events('did-navigate-in-page', (event, url, isMainFrame) => { this.windowUtils.send('did-navigate-in-page', { event, url, isMainFrame }); From 7116efe2073678093bde60f920bd6bc93cf9d4cd Mon Sep 17 00:00:00 2001 From: Jiiks Date: Mon, 12 Mar 2018 17:46:55 +0200 Subject: [PATCH 03/21] Replay exports --- client/src/modules/modules.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/modules/modules.js b/client/src/modules/modules.js index f13d3bb3..6d8b5427 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 { default as ReactComponents } from './reactcomponents'; From 67dddf53918aa8ac024ca7d0d05a73d42e916283 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Mon, 12 Mar 2018 17:48:29 +0200 Subject: [PATCH 04/21] Replay index --- client/src/index.js | 54 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/client/src/index.js b/client/src/index.js index b7bb9f3d..087edba0 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -8,17 +8,22 @@ * 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, DiscordApi } from 'modules'; +import { ClientLogger as Logger, ClientIPC, Utils } from 'common'; import { EmoteModule } from 'builtin'; -const ignoreExternal = false; +const ignoreExternal = true; class BetterDiscord { constructor() { window.discordApi = DiscordApi; + window.bdu = Utils; + window.ref = Reflection; + window.rc = ReactComponents; + + window.ReactComponents = ReactComponents; window.bddb = Database; window.bdglobals = Globals; window.ClientIPC = ClientIPC; @@ -34,7 +39,11 @@ class BetterDiscord { window.dom = DOM; 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(); + //this.globalReady = this.globalReady.bind(this); + //this.globalReady(); } async init() { @@ -68,5 +77,38 @@ class BetterDiscord { if (window.BetterDiscord) { Logger.log('main', 'Attempting to inject again?'); } else { - let bdInstance = new BetterDiscord(); + let instance = null; + function init() { + instance = new BetterDiscord(); + } + + window.Patcher = Patcher; + Events.on('react-ensure', init); + function ensureReact() { + if (!window.webpackJsonp || !WebpackModules.getModuleByName('React')) return setTimeout(ensureReact, 10); + ReactComponents.getComponent('Message').then(Message => { + Events.emit('react-ensure'); + Message.patchRender([{ + selector: '.message', + method: 'replace', + fn: function (item) { + if (!this.props || !this.props.message) return item; + const { message } = this.props; + const { id, colorString, bot, author, attachments, embeds } = message; + item.props['data-message-id'] = id; + item.props['data-colourstring'] = colorString; + if (bot || (author && author.bot)) item.props.className += ' bd-isBot'; + if (attachments && attachments.length) item.props.className += ' bd-hasAttachments'; + if (embeds && embeds.length) item.props.className += ' bd-hasEmbeds'; + if (author && author.id === '301511787814191105') item.props.className += ' bd-isCurrentUser'; + return item; + } + }]); + }); + Patcher.superpatch('React', 'createElement', function (component, retVal) { + if (!component.displayName) return; + ReactComponents.push(component, retVal); + }); + } + ensureReact(); } From d965e29867750e7178f97d792f07ed55ff07e22f Mon Sep 17 00:00:00 2001 From: Jiiks Date: Mon, 12 Mar 2018 17:50:09 +0200 Subject: [PATCH 05/21] Throw everything into _bd object --- client/src/index.js | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/client/src/index.js b/client/src/index.js index 087edba0..bf325cef 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -18,32 +18,34 @@ const ignoreExternal = true; class BetterDiscord { constructor() { - window.discordApi = DiscordApi; - window.bdu = Utils; - window.ref = Reflection; - window.rc = ReactComponents; - - window.ReactComponents = ReactComponents; - 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._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'); this.globalReady = this.globalReady.bind(this); Events.on('global-ready', this.globalReady); Globals.initg(); - //this.globalReady = this.globalReady.bind(this); - //this.globalReady(); } async init() { From 2c2ef33565395acbd941a7b73e14f9b4de387238 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Mon, 12 Mar 2018 17:50:43 +0200 Subject: [PATCH 06/21] Use DiscordApi to get current user --- client/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/index.js b/client/src/index.js index bf325cef..5874d725 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -102,7 +102,7 @@ if (window.BetterDiscord) { if (bot || (author && author.bot)) item.props.className += ' bd-isBot'; if (attachments && attachments.length) item.props.className += ' bd-hasAttachments'; if (embeds && embeds.length) item.props.className += ' bd-hasEmbeds'; - if (author && author.id === '301511787814191105') item.props.className += ' bd-isCurrentUser'; + if (author && author.id === DiscordApi.currentUser.id) item.props.className += ' bd-isCurrentUser'; return item; } }]); From 7599271f31fcbd1459e7ec8ba5b4698e0479e2a8 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Mon, 12 Mar 2018 17:52:24 +0200 Subject: [PATCH 07/21] add author id --- client/src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/index.js b/client/src/index.js index 5874d725..1e396304 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -99,6 +99,7 @@ if (window.BetterDiscord) { const { id, colorString, bot, author, attachments, embeds } = message; item.props['data-message-id'] = id; item.props['data-colourstring'] = colorString; + if (author && author.id) item.props['data-user-id'] = author.id; if (bot || (author && author.bot)) item.props.className += ' bd-isBot'; if (attachments && attachments.length) item.props.className += ' bd-hasAttachments'; if (embeds && embeds.length) item.props.className += ' bd-hasEmbeds'; From ae6b745e686ff2f30b0c84ce9eb1454b91105eb2 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 00:34:03 +0200 Subject: [PATCH 08/21] Partially fix autocomplete and use patches --- client/src/builtin/EmoteComponent.vue | 9 - client/src/builtin/EmoteModule.js | 138 ++++++------ client/src/index.js | 35 +-- client/src/modules/modules.js | 2 +- client/src/modules/reactcomponents.js | 210 +++++++++++++++++- client/src/styles/partials/emote.scss | 11 + client/src/styles/partials/index.scss | 1 + .../src/ui/components/common/Autocomplete.vue | 20 +- client/src/ui/reflection.js | 7 + 9 files changed, 320 insertions(+), 113 deletions(-) create mode 100644 client/src/styles/partials/emote.scss 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 1e396304..f3c65367 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -10,7 +10,7 @@ import { DOM, BdUI, Modals, Reflection } from 'ui'; import BdCss from './styles/index.scss'; -import { Patcher, Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database, ReactComponents, DiscordApi } from 'modules'; +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 = true; @@ -83,35 +83,6 @@ if (window.BetterDiscord) { function init() { instance = new BetterDiscord(); } - - window.Patcher = Patcher; - Events.on('react-ensure', init); - function ensureReact() { - if (!window.webpackJsonp || !WebpackModules.getModuleByName('React')) return setTimeout(ensureReact, 10); - ReactComponents.getComponent('Message').then(Message => { - Events.emit('react-ensure'); - Message.patchRender([{ - selector: '.message', - method: 'replace', - fn: function (item) { - if (!this.props || !this.props.message) return item; - const { message } = this.props; - const { id, colorString, bot, author, attachments, embeds } = message; - item.props['data-message-id'] = id; - item.props['data-colourstring'] = colorString; - if (author && author.id) item.props['data-user-id'] = author.id; - if (bot || (author && author.bot)) item.props.className += ' bd-isBot'; - if (attachments && attachments.length) item.props.className += ' bd-hasAttachments'; - if (embeds && embeds.length) item.props.className += ' bd-hasEmbeds'; - if (author && author.id === DiscordApi.currentUser.id) item.props.className += ' bd-isCurrentUser'; - return item; - } - }]); - }); - Patcher.superpatch('React', 'createElement', function (component, retVal) { - if (!component.displayName) return; - ReactComponents.push(component, retVal); - }); - } - ensureReact(); + Events.on('autopatcher', init); + ReactAutoPatcher.autoPatch().then(() => Events.emit('autopatcher')); } diff --git a/client/src/modules/modules.js b/client/src/modules/modules.js index 6d8b5427..c6311c53 100644 --- a/client/src/modules/modules.js +++ b/client/src/modules/modules.js @@ -16,4 +16,4 @@ 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 { default as ReactComponents } from './reactcomponents'; +export * from './reactcomponents'; diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index abcbb243..a20fb5ff 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -1,4 +1,36 @@ import Patcher from './patcher'; +import WebpackModules from './webpackmodules'; +import DiscordApi from './discordapi'; +import { EmoteModule } from 'builtin'; + +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() { @@ -105,6 +137,20 @@ class Helpers { 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 { @@ -112,6 +158,52 @@ class ReactComponent { 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() { @@ -129,15 +221,17 @@ class ReactComponent { unpatchRender() { } - + /* patchRender(actions, updateOthers) { const self = this; if (!(actions instanceof Array)) actions = [actions]; - Patcher.slavepatch(this.component.prototype, 'render', function(args, obj) { + 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) { @@ -149,20 +243,99 @@ class ReactComponent { if (updateOthers) self.forceUpdateOthers(); }); } - + */ forceUpdateOthers() { } } -export default class ReactComponents { +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 async patchem() { + this.patchMessage(); + this.patchMessageGroup(); + this.patchChannelMember(); + } + + static async patchMessage() { + this.Message.component = await ReactComponents.getComponent('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'); + 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['data-member-id'] = user.id; + if (user.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser'; + if (isOwner) retVal.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 null; + 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); @@ -189,4 +362,31 @@ export default class ReactComponents { }); } + 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(c); + return c; + } } 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/components/common/Autocomplete.vue b/client/src/ui/components/common/Autocomplete.vue index 4c248be8..13aae70d 100644 --- a/client/src/ui/components/common/Autocomplete.vue +++ b/client/src/ui/components/common/Autocomplete.vue @@ -19,7 +19,7 @@ -
+
@@ -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); }, @@ -91,21 +93,31 @@ const selected = this.emotes[this.selectedIndex]; if (!selected) return; this.inject(selected); + this.open = false; return; } + if (e.key === 'Tab' && !this.open) this.open = true; + if (!this.open) return; const se = e.target.selectionEnd; this.sterm = e.target.value.substr(0, se).split(' ').slice(-1).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; }, + 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; diff --git a/client/src/ui/reflection.js b/client/src/ui/reflection.js index 5c2903cb..c1597dc5 100644 --- a/client/src/ui/reflection.js +++ b/client/src/ui/reflection.js @@ -80,6 +80,10 @@ class Reflection { } return this.propIterator(curProp, propNames); } + + static getState(node) { + return this.reactInternalInstance(node).return.stateNode.state; + } } export default function (node) { @@ -91,6 +95,9 @@ export default function (node) { get props() { return 'not yet implemented'; } + get state() { + return Reflection.getState(this.node); + } get reactInternalInstance() { return Reflection.reactInternalInstance(this.node); } From 006a67ee011c87fdd670dcdbe0d51b26b21372c4 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 00:34:49 +0200 Subject: [PATCH 09/21] DO NOT USE THIS BRANCH YOU WILL DIE IF YOU DO --- core/src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main.js b/core/src/main.js index 28895fcb..f85f9095 100644 --- a/core/src/main.js +++ b/core/src/main.js @@ -7,7 +7,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - +/*DO NOT USE THIS BRANCH YOU WILL DIE IF YOU DO*/ const path = require('path'); const sass = require('node-sass'); From ce9a4c6b3d4b04b28ef0dc2ba73428a9033c0917 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 02:36:34 +0200 Subject: [PATCH 10/21] Fix channel member render --- client/src/modules/reactcomponents.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index a20fb5ff..01102992 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -301,9 +301,9 @@ export class ReactAutoPatcher { this.ChannelMember.component = await ReactComponents.getComponent('ChannelMember'); this.ChannelMember.component.on('render', ({ component, retVal, p }) => { const { user, isOwner } = component.props; - retVal.props['data-member-id'] = user.id; - if (user.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser'; - if (isOwner) retVal.props.className += ' bd-isOwner'; + 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'; }); } From 6473ddaf9faac7cdfe41256d4c47f82b445c4fe5 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 09:12:00 +0200 Subject: [PATCH 11/21] add important component scanner through reflection --- client/src/modules/reactcomponents.js | 26 ++++++++++++++++++++++++-- client/src/ui/reflection.js | 17 ++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index 01102992..babf0bb3 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -2,6 +2,7 @@ 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() { @@ -267,7 +268,7 @@ export class ReactAutoPatcher { } static async patchMessage() { - this.Message.component = await ReactComponents.getComponent('Message'); + this.Message.component = await ReactComponents.getComponent('Message', true, { selector: '.message', displayName: 'Message' }); this.Message.component.on('render', ({ component, retVal, p }) => { const { message } = component.props; const { id, colorString, bot, author, attachments, embeds } = message; @@ -349,9 +350,30 @@ export class ReactComponents { return c; } - static async getComponent(name) { + 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, diff --git a/client/src/ui/reflection.js b/client/src/ui/reflection.js index c1597dc5..f018be1b 100644 --- a/client/src/ui/reflection.js +++ b/client/src/ui/reflection.js @@ -82,7 +82,19 @@ class Reflection { } static getState(node) { - return this.reactInternalInstance(node).return.stateNode.state; + try { + return this.reactInternalInstance(node).return.stateNode.state; + } catch (err) { + return null; + } + } + + static getComponent(node) { + try { + return this.reactInternalInstance(node).return.type; + } catch (err) { + return null; + } } } @@ -101,6 +113,9 @@ export default function (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]); From bcc856bc6ba51a61f2a5851e52a2fa4847d758b0 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 09:17:58 +0200 Subject: [PATCH 12/21] Don't need separate displayName --- client/src/modules/reactcomponents.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index babf0bb3..cf755f27 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -268,7 +268,7 @@ export class ReactAutoPatcher { } static async patchMessage() { - this.Message.component = await ReactComponents.getComponent('Message', true, { selector: '.message', displayName: 'Message' }); + 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; From e73ac58ff39bd7f57000a9acbb78305dbbfef827 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 09:25:48 +0200 Subject: [PATCH 13/21] Go back to did-finish-load --- client/src/modules/reactcomponents.js | 4 ++-- core/src/main.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index cf755f27..f79844b0 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -261,7 +261,7 @@ export class ReactAutoPatcher { while (!window.webpackJsonp || !WebpackModules.getModuleByName('React')) await new Promise(resolve => setTimeout(resolve, 10)); return 1; } - static async patchem() { + static patchem() { this.patchMessage(); this.patchMessageGroup(); this.patchChannelMember(); @@ -290,7 +290,7 @@ export class ReactAutoPatcher { static async patchMessageGroup() { ReactComponents.setName('MessageGroup', this.MessageGroup.filter); - this.MessageGroup.component = await ReactComponents.getComponent('MessageGroup'); + 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; diff --git a/core/src/main.js b/core/src/main.js index f85f9095..2f40dff7 100644 --- a/core/src/main.js +++ b/core/src/main.js @@ -150,7 +150,7 @@ class BetterDiscord { //this.windowUtils.webContents.on('did-finish-load', e => this.injectScripts(true)); this.windowUtils.events('did-get-response-details', () => this.ignite(this.windowUtils.window)); - this.windowUtils.events('did-get-response-details', e => this.injectScripts(true)); + this.windowUtils.events('did-finish-load', e => this.injectScripts(true)); this.windowUtils.events('did-navigate-in-page', (event, url, isMainFrame) => { this.windowUtils.send('did-navigate-in-page', { event, url, isMainFrame }); From 5f7ddcb8ce5215e4c0fe80ca95a9e5e70d5c5367 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 09:43:03 +0200 Subject: [PATCH 14/21] add TODO notice --- client/src/ui/reflection.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/ui/reflection.js b/client/src/ui/reflection.js index f018be1b..9d6a0b58 100644 --- a/client/src/ui/reflection.js +++ b/client/src/ui/reflection.js @@ -90,6 +90,7 @@ class Reflection { } 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) { From 9e875f40392b5ad8cbecbafa7c9b62b553c1851e Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 10:58:40 +0200 Subject: [PATCH 15/21] Fix autocomplete handlers and keys and trigger it with tab --- .../src/ui/components/common/Autocomplete.vue | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/client/src/ui/components/common/Autocomplete.vue b/client/src/ui/components/common/Autocomplete.vue index 13aae70d..0f3815b6 100644 --- a/client/src/ui/components/common/Autocomplete.vue +++ b/client/src/ui/components/common/Autocomplete.vue @@ -82,6 +82,40 @@ return uri.replace(':id', value); }, searchEmotes(e) { + 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; + } + 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.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; + }, + searchEmotesOLD(e) { if (e.key === 'ArrowDown' && this.open && this.caret) { this.selectedIndex = (this.selectedIndex + 1) >= 10 ? 0 : this.selectedIndex + 1; return; @@ -99,7 +133,7 @@ if (e.key === 'Tab' && !this.open) this.open = true; if (!this.open) return; const se = e.target.selectionEnd; - this.sterm = e.target.value.substr(0, se).split(' ').slice(-1).pop(); + this.sterm = e.target.value.substr(0, se).split(/\s+/g).pop(); if (this.sterm.length < 3) { this.reset(); @@ -121,19 +155,13 @@ 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; } } } From 1e438764a084c01ac5a20fbca2ddc37397ab3a25 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 10:59:16 +0200 Subject: [PATCH 16/21] Reset and remove old --- .../src/ui/components/common/Autocomplete.vue | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/client/src/ui/components/common/Autocomplete.vue b/client/src/ui/components/common/Autocomplete.vue index 0f3815b6..9625e8d3 100644 --- a/client/src/ui/components/common/Autocomplete.vue +++ b/client/src/ui/components/common/Autocomplete.vue @@ -115,34 +115,6 @@ } return false; }, - searchEmotesOLD(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) { - const selected = this.emotes[this.selectedIndex]; - if (!selected) return; - this.inject(selected); - this.open = false; - return; - } - if (e.key === 'Tab' && !this.open) this.open = true; - if (!this.open) return; - const se = e.target.selectionEnd; - this.sterm = e.target.value.substr(0, se).split(/\s+/g).pop(); - - if (this.sterm.length < 3) { - this.reset(); - return; - } - this.title = this.sterm; - this.emotes = EmoteModule.filter(new RegExp(this.sterm, ''), 10); - this.open = this.emotes.length; - }, reset() { this.emotes = []; this.title = ''; @@ -162,6 +134,7 @@ DOM.manip.setText(substr + value.substr(selectionEnd, value.length), false); ta.selectionEnd = ta.selectionStart = selectionEnd + en.length - this.sterm.length; + this.reset(); } } } From b3ed0444089dd8092a7bbc619847bccb8956b4ac Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 11:01:10 +0200 Subject: [PATCH 17/21] add copyright headers --- client/src/modules/patcher.js | 10 ++++++++++ client/src/modules/reactcomponents.js | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/client/src/modules/patcher.js b/client/src/modules/patcher.js index f6dbf789..50206919 100644 --- a/client/src/modules/patcher.js +++ b/client/src/modules/patcher.js @@ -1,3 +1,13 @@ +/** + * 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 { diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index f79844b0..8a48c4bb 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -1,3 +1,14 @@ +/** + * 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'; From d6794e6de85abbc689356c803ed2b9824a4f86f5 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 11:02:52 +0200 Subject: [PATCH 18/21] Remove branch warning --- core/src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main.js b/core/src/main.js index 2f40dff7..ed7c39d5 100644 --- a/core/src/main.js +++ b/core/src/main.js @@ -7,7 +7,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -/*DO NOT USE THIS BRANCH YOU WILL DIE IF YOU DO*/ + const path = require('path'); const sass = require('node-sass'); From 81941bb14056684515b994dcc9f46669531cf813 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 11:06:54 +0200 Subject: [PATCH 19/21] Lint --- client/src/index.js | 1 + client/src/modules/reactcomponents.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/index.js b/client/src/index.js index f3c65367..9e109a42 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -80,6 +80,7 @@ if (window.BetterDiscord) { Logger.log('main', 'Attempting to inject again?'); } else { let instance = null; + // eslint-disable-next-line no-inner-declarations function init() { instance = new BetterDiscord(); } diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index 8a48c4bb..7eca4762 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -65,6 +65,7 @@ class Helpers { } 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); @@ -419,7 +420,7 @@ export class ReactComponents { } } if (have) return have; - this.unknownComponents.push(c); - return c; + this.unknownComponents.push(component); + return component; } } From 25044beaef9746630f0a8c66d45045bf6fb056e8 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 11:11:08 +0200 Subject: [PATCH 20/21] Don't --- client/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/index.js b/client/src/index.js index 9e109a42..795854a7 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -13,7 +13,7 @@ import BdCss from './styles/index.scss'; 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 = true; +const ignoreExternal = false; class BetterDiscord { From bf7919a8770ce7ab04444899057597620f927b68 Mon Sep 17 00:00:00 2001 From: Jiiks Date: Wed, 14 Mar 2018 11:13:18 +0200 Subject: [PATCH 21/21] add developer mode --- client/src/index.js | 48 ++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/client/src/index.js b/client/src/index.js index 795854a7..ef652907 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -14,32 +14,36 @@ import { Patcher, Events, CssEditor, Globals, ExtModuleManager, PluginManager, T import { ClientLogger as Logger, ClientIPC, Utils } from 'common'; import { EmoteModule } from 'builtin'; const ignoreExternal = false; +const DEV = true; class BetterDiscord { constructor() { - window._bd = { - DOM, - BdUI, - Modals, - Reflection, - Patcher, - Events, - CssEditor, - Globals, - ExtModuleManager, - PluginManager, - ThemeManager, - ModuleManager, - WebpackModules, - Settings, - Database, - ReactComponents, - DiscordApi, - Logger, - ClientIPC, - Utils, - EmoteModule + 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');