Partially fix autocomplete and use patches
This commit is contained in:
parent
7599271f31
commit
ae6b745e68
|
@ -19,12 +19,3 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.bd-emotewrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.bd-emotewrapper img {
|
||||
max-height: 32px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,84 +8,98 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import { FileUtils } from 'common';
|
||||
import { Events, Globals } from 'modules';
|
||||
import { Events, Globals, WebpackModules, ReactComponents } from 'modules';
|
||||
import { DOM, VueInjector } from 'ui';
|
||||
import EmoteComponent from './EmoteComponent.vue';
|
||||
let emotes = null;
|
||||
const emotesEnabled = true;
|
||||
|
||||
export default class {
|
||||
static get React() {
|
||||
return WebpackModules.getModuleByName('React');
|
||||
}
|
||||
static processMarkup(markup) {
|
||||
if (!emotesEnabled) return markup; // TODO Get it from setttings
|
||||
const newMarkup = [];
|
||||
for (const [ti, t] of markup.entries()) {
|
||||
if ('string' !== typeof t) {
|
||||
newMarkup.push(t);
|
||||
continue;
|
||||
}
|
||||
|
||||
const words = t.split(/([^\s]+)([\s]|$)/g);
|
||||
if (!words) continue;
|
||||
let text = null;
|
||||
for (const [wi, word] of words.entries()) {
|
||||
let isEmote = false;
|
||||
if (this.testWord(word)) {
|
||||
isEmote = true;
|
||||
}
|
||||
if (isEmote) {
|
||||
if (text !== null) {
|
||||
newMarkup.push(text);
|
||||
text = null;
|
||||
}
|
||||
newMarkup.push(this.React.createElement('span', { className: 'bd-emote-outer' }, word));
|
||||
continue;
|
||||
}
|
||||
if (text === null) {
|
||||
text = `${word}`;
|
||||
} else {
|
||||
text += `${word}`;
|
||||
}
|
||||
if (wi === words.length - 1) {
|
||||
newMarkup.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newMarkup;
|
||||
}
|
||||
|
||||
static testWord(word) {
|
||||
if (!/:[\w]+:/gmi.test(word)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static injectAll() {
|
||||
if (!emotesEnabled) return;
|
||||
const all = document.getElementsByClassName('bd-emote-outer');
|
||||
for (const ec of all) {
|
||||
if (ec.children.length) continue;
|
||||
this.injectEmote(ec);
|
||||
}
|
||||
}
|
||||
|
||||
static async observe() {
|
||||
const dataPath = Globals.getObject('paths').find(path => path.id === 'data').path;
|
||||
try {
|
||||
emotes = await FileUtils.readJsonFromFile(dataPath + '/emotes.json');
|
||||
Events.on('ui:mutable:.markup',
|
||||
markup => {
|
||||
if (!emotes) return;
|
||||
this.injectEmotes(markup);
|
||||
});
|
||||
const Message = await ReactComponents.getComponent('Message');
|
||||
Message.on('componentDidMount', ({ element }) => this.injectEmotes(element));
|
||||
Message.on('componentDidUpdate', ({ state, element }) => {
|
||||
if (!state.isEditing) this.injectEmotes(element);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
static injectEmotes(node) {
|
||||
if (!/:[\w]+:/gmi.test(node.textContent)) return node;
|
||||
const childNodes = [...node.childNodes];
|
||||
const newNode = document.createElement('div');
|
||||
newNode.className = node.className;
|
||||
newNode.classList.add('hasEmotes');
|
||||
static injectEmote(e) {
|
||||
if (!emotesEnabled) return;
|
||||
const isEmote = this.isEmote(e.textContent);
|
||||
if (!isEmote) return;
|
||||
VueInjector.inject(
|
||||
e,
|
||||
DOM.createElement('span'),
|
||||
{ EmoteComponent },
|
||||
`<EmoteComponent src="${isEmote.src}" name="${isEmote.name}"/>`
|
||||
);
|
||||
e.classList.add('bd-is-emote');
|
||||
}
|
||||
|
||||
for (const [cni, cn] of childNodes.entries()) {
|
||||
if (cn.nodeType !== Node.TEXT_NODE) {
|
||||
newNode.appendChild(cn);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { nodeValue } = cn;
|
||||
const words = nodeValue.split(/([^\s]+)([\s]|$)/g);
|
||||
|
||||
if (!words.some(word => word.startsWith(':') && word.endsWith(':'))) {
|
||||
newNode.appendChild(cn);
|
||||
continue;
|
||||
}
|
||||
let text = null;
|
||||
for (const [wi, word] of words.entries()) {
|
||||
let isEmote = null;
|
||||
if (word.startsWith(':') && word.endsWith(':')) {
|
||||
isEmote = this.isEmote(word);
|
||||
}
|
||||
|
||||
if (isEmote) {
|
||||
if (text !== null) {
|
||||
newNode.appendChild(document.createTextNode(text));
|
||||
text = null;
|
||||
}
|
||||
|
||||
const emoteRoot = document.createElement('span');
|
||||
newNode.appendChild(emoteRoot);
|
||||
VueInjector.inject(
|
||||
emoteRoot,
|
||||
DOM.createElement('span'),
|
||||
{ EmoteComponent },
|
||||
`<EmoteComponent src="${isEmote.src}" name="${isEmote.name}"/>`,
|
||||
true
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (text === null) {
|
||||
text = word;
|
||||
} else {
|
||||
text += word;
|
||||
}
|
||||
|
||||
if (wi === words.length - 1) {
|
||||
newNode.appendChild(document.createTextNode(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
node.replaceWith(newNode);
|
||||
static injectEmotes(element) {
|
||||
if (!emotesEnabled || !element) return;
|
||||
for (const beo of element.getElementsByClassName('bd-emote-outer')) this.injectEmote(beo);
|
||||
}
|
||||
|
||||
static isEmote(word) {
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
.bd-emote-outer.bd-is-emote {
|
||||
font-size: 0;
|
||||
}
|
||||
.bd-emotewrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
img {
|
||||
max-height: 32px;
|
||||
}
|
||||
}
|
|
@ -12,3 +12,4 @@
|
|||
@import './discordoverrides.scss';
|
||||
@import './helpers.scss';
|
||||
@import './misc.scss';
|
||||
@import './emote.scss';
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(emote, index) in emotes" class="bd-autocompleteRow" :key="emote.id">
|
||||
<div v-for="(emote, index) in emotes" class="bd-autocompleteRow" :key="index">
|
||||
<div class="bd-autocompleteSelector bd-selectable" :class="{'bd-selected': index === selectedIndex}" @mouseover="() => { selected = emote.id }" @click="() => inject(emote)">
|
||||
<div class="bd-autocompleteField">
|
||||
<img :src="getEmoteSrc(emote)"/>
|
||||
|
@ -54,12 +54,14 @@
|
|||
created() {
|
||||
window.addEventListener('keydown', this.prevents);
|
||||
const ta = document.querySelector('.chat textarea');
|
||||
if(!ta) return;
|
||||
ta.addEventListener('keydown', this.setCaret);
|
||||
ta.addEventListener('keyup', this.searchEmotes);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('keydown', this.prevents);
|
||||
const ta = document.querySelector('.chat textarea');
|
||||
if (!ta) return;
|
||||
ta.removeEventListener('keydown', this.setCaret);
|
||||
ta.removeEventListener('keyup', this.searchEmotes);
|
||||
},
|
||||
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue