Merge pull request #161 from JsSucks/emote-module

Emote module
This commit is contained in:
Alexei Stukov 2018-03-07 11:43:20 +02:00 committed by GitHub
commit 67747f85eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 34416 additions and 9 deletions

View File

@ -0,0 +1,8 @@
<template>
<span class="edited" v-tooltip="ets">(edited)</span>
</template>
<script>
export default {
props: ['ets']
}
</script>

View File

@ -0,0 +1,30 @@
<template>
<span class="bd-emotewrapper" v-tooltip="name">
<img class="bd-emote" :src="src" :alt="name"/>
</span>
</template>
<script>
export default {
data() {
return {
favourite: false
}
},
props: ['src', 'name'],
methods: {
},
beforeMount() {
// Check favourite state
}
}
</script>
<style>
.bd-emotewrapper {
position: relative;
display: inline-block;
}
.bd-emotewrapper img {
max-height: 32px;
}
</style>

View File

@ -0,0 +1,166 @@
/**
* BetterDiscord Emote Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* 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 { Events } from 'modules';
import { DOM, VueInjector } from 'ui';
import EditedTimeStamp from './EditedTimeStamp.vue';
import EmoteComponent from './EmoteComponent.vue';
import TwitchEmotes from '../data/twitch_emotes.json';
export default class {
static observe() {
Events.on('server-switch', this.injectAll.bind(this));
Events.on('channel-switch', this.injectAll.bind(this));
Events.on('discord:MESSAGE_CREATE', e => {
// Assume that it's the last one for now since the event doesn't give the element
const query = document.querySelectorAll('.markup:not(.mutable)');
if (!query) return;
this.injectMarkup(query[query.length - 1], true);
}); // TODO
}
static injectAll() {
for (const el of document.querySelectorAll('.markup:not(.mutable)')) {
this.injectMarkup(el, this.cloneMarkup(el), false);
}
}
static cloneMarkup(node) {
const childNodes = [...node.childNodes];
const clone = document.createElement('div');
clone.className = 'markup mutable';
const ets = this.getEts(node);
for (const [cni, cn] of childNodes.entries()) {
if (cn.nodeType !== Node.TEXT_NODE) {
if (cn.className.includes('edited')) continue;
}
clone.appendChild(cn.cloneNode(true));
}
return { clone, ets }
}
static getEts(node) {
try {
const reh = Object.keys(node).find(k => k.startsWith('__reactInternalInstance'));
return node[reh].memoizedProps.children[node[reh].memoizedProps.children.length - 1].props.text;
} catch (err) {
return null;
}
}
static injectMarkup(sibling, markup, reinject) {
if (sibling.className && sibling.className.includes('mutable')) return; // Ignore trying to make mutable mutable again
let cc = null;
for (const cn of sibling.parentElement.childNodes) {
if (cn.className && cn.className.includes('mutable')) cc = cn;
}
if (cc) sibling.parentElement.removeChild(cc);
if (markup === true) markup = this.cloneMarkup(sibling);
markup.clone = this.injectEmotes(markup.clone);
sibling.parentElement.insertBefore(markup.clone, sibling);
sibling.classList.add('shadow');
sibling.style.display = 'none';
if (markup.ets) {
const etsRoot = document.createElement('span');
markup.clone.appendChild(etsRoot);
VueInjector.inject(
etsRoot,
DOM.createElement('span', null, 'test'),
{ EditedTimeStamp },
`<EditedTimeStamp ets="${markup.ets}"/>`,
true
);
}
if (reinject) return;
new MutationObserver(() => {
this.injectMarkup(sibling, this.cloneMarkup(sibling), true);
}).observe(sibling, { characterData: false, attributes: false, childList: true, subtree: false });
new MutationObserver(() => {
this.injectMarkup(sibling, this.cloneMarkup(sibling), true);
}).observe(sibling, { characterData: true, attributes: false, childList: false, subtree: true });
}
static makeMutable(node) {
if (node.classList && node.classList.contains('shadow')) return;
this.injectMarkup(node, this.cloneMarkup(node));
}
static injectEmotes(node) {
if (!/:[\w]+:/gmi.test(node.textContent)) return node;
const childNodes = [...node.childNodes];
const newNode = document.createElement('div');
newNode.className = 'markup mutable hasEmotes';
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', null, 'emotetest'),
{ 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));
}
}
}
return newNode;
}
static isEmote(word) {
const name = word.replace(/:/g, '');
if (TwitchEmotes.hasOwnProperty(name)) {
const src = `https://static-cdn.jtvnw.net/emoticons/v1/${TwitchEmotes[name]}/1.0`;
return { name, src };
}
return null;
}
}

View File

@ -0,0 +1 @@
export { default as EmoteModule } from './EmoteModule';

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ import { DOM, BdUI, Modals } from 'ui';
import BdCss from './styles/index.scss';
import { Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database } from 'modules';
import { ClientLogger as Logger, ClientIPC } from 'common';
import { EmoteModule } from 'builtin';
class BetterDiscord {
@ -27,7 +28,8 @@ class BetterDiscord {
window.bdsettings = Settings;
window.bdmodals = Modals;
window.bdlogs = Logger;
window.emotes = EmoteModule;
EmoteModule.observe();
DOM.injectStyle(BdCss, 'bdmain');
Events.on('global-ready', this.globalReady.bind(this));
}

View File

@ -11,6 +11,7 @@
import EventListener from './eventlistener';
import { Utils } from 'common';
import Events from './events';
import WebpackModules from './webpackmodules';
import {
MESSAGE_CREATE
@ -23,6 +24,10 @@ import {
*/
export default class extends EventListener {
init() {
this.hook();
}
bindings() {
this.hook = this.hook.bind(this);
}
@ -33,9 +38,19 @@ export default class extends EventListener {
];
}
hook() {}
hook() {
const self = this;
const orig = this.eventsModule.prototype.emit;
this.eventsModule.prototype.emit = function (...args) {
orig.call(this, ...args);
self.wsc = this;
self.emit(...args);
}
}
get eventsModule() {}
get eventsModule() {
return WebpackModules.getModuleByPrototypes(['setMaxListeners', 'emit']);
}
/**
* Discord emit overload
@ -58,7 +73,7 @@ export default class extends EventListener {
dispatch(e, d) {
Events.emit('raw-event', { type: e, data: d });
let evt = null;
switch (e) {
case this.actions.READ:
Events.emit('discord-ready');
@ -74,7 +89,7 @@ export default class extends EventListener {
});
break;
case this.actions.MESSAGE_CREATE:
Events.emit('discord-event', { type: e, data: new MESSAGE_CREATE(d) });
evt = { type: e, data: new MESSAGE_CREATE(d) };
break;
case 'k':
Events.emit('discord-event', {
@ -86,6 +101,7 @@ export default class extends EventListener {
break;
}
if (evt !== null) Events.emit(`discord:${evt.type}`, evt);
}
/**

View File

@ -27,6 +27,40 @@ export default class {
data: { userid }
}));
}, 100);
// This is temporary
const locationInterval = setInterval(() => {
try {
const path = window.location.pathname.match(/\d+/g);
if (!path) {
this.prevLocation = null;
Events.emit('server-switch');
return;
}
const ids = path.reduce((obj, el, index) => {
obj[index === 0 ? 'guildId' : 'channelId'] = el;
return obj;
},
{});
if (Object.keys(ids).length === 1) {
ids.isDm = true;
ids.dmId = ids.guildId
}
if (!this.prevLocation) {
Events.emit('server-switch');
} else {
if (this.prevLocation.channelId !== ids.channelId) {
Events.emit('channel-switch');
} else if (this.prevLocation.guildId !== ids.guildId) {
Events.emit('server-switch');
}
}
this.prevLocation = ids;
} catch (err) {
console.log(err);
}
}, 100);
}
static get profilePopupModule() {

View File

@ -12,11 +12,11 @@ import Vue from './vue';
export default class {
static inject(root, bdnode, components, template) {
bdnode.appendTo(root);
static inject(root, bdnode, components, template, replaceRoot) {
if(!replaceRoot) bdnode.appendTo(root);
return new Vue({
el: bdnode.element,
el: replaceRoot ? root : bdnode.element,
components,
template
});

View File

@ -46,7 +46,8 @@ module.exports = {
path.resolve('src', 'modules'),
path.resolve('src', 'ui'),
path.resolve('src', 'plugins'),
path.resolve('src', 'structs')
path.resolve('src', 'structs'),
path.resolve('src', 'builtin')
]
}
/* resolve: {