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