commit
67747f85eb
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<span class="edited" v-tooltip="ets">(edited)</span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['ets']
|
||||
}
|
||||
</script>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default as EmoteModule } from './EmoteModule';
|
File diff suppressed because it is too large
Load Diff
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue