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 BdCss from './styles/index.scss';
|
||||||
import { Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database } from 'modules';
|
import { Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database } from 'modules';
|
||||||
import { ClientLogger as Logger, ClientIPC } from 'common';
|
import { ClientLogger as Logger, ClientIPC } from 'common';
|
||||||
|
import { EmoteModule } from 'builtin';
|
||||||
|
|
||||||
class BetterDiscord {
|
class BetterDiscord {
|
||||||
|
|
||||||
|
@ -27,7 +28,8 @@ class BetterDiscord {
|
||||||
window.bdsettings = Settings;
|
window.bdsettings = Settings;
|
||||||
window.bdmodals = Modals;
|
window.bdmodals = Modals;
|
||||||
window.bdlogs = Logger;
|
window.bdlogs = Logger;
|
||||||
|
window.emotes = EmoteModule;
|
||||||
|
EmoteModule.observe();
|
||||||
DOM.injectStyle(BdCss, 'bdmain');
|
DOM.injectStyle(BdCss, 'bdmain');
|
||||||
Events.on('global-ready', this.globalReady.bind(this));
|
Events.on('global-ready', this.globalReady.bind(this));
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
import EventListener from './eventlistener';
|
import EventListener from './eventlistener';
|
||||||
import { Utils } from 'common';
|
import { Utils } from 'common';
|
||||||
import Events from './events';
|
import Events from './events';
|
||||||
|
import WebpackModules from './webpackmodules';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MESSAGE_CREATE
|
MESSAGE_CREATE
|
||||||
|
@ -23,6 +24,10 @@ import {
|
||||||
*/
|
*/
|
||||||
export default class extends EventListener {
|
export default class extends EventListener {
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.hook();
|
||||||
|
}
|
||||||
|
|
||||||
bindings() {
|
bindings() {
|
||||||
this.hook = this.hook.bind(this);
|
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
|
* Discord emit overload
|
||||||
|
@ -58,7 +73,7 @@ export default class extends EventListener {
|
||||||
dispatch(e, d) {
|
dispatch(e, d) {
|
||||||
|
|
||||||
Events.emit('raw-event', { type: e, data: d });
|
Events.emit('raw-event', { type: e, data: d });
|
||||||
|
let evt = null;
|
||||||
switch (e) {
|
switch (e) {
|
||||||
case this.actions.READ:
|
case this.actions.READ:
|
||||||
Events.emit('discord-ready');
|
Events.emit('discord-ready');
|
||||||
|
@ -74,7 +89,7 @@ export default class extends EventListener {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case this.actions.MESSAGE_CREATE:
|
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;
|
break;
|
||||||
case 'k':
|
case 'k':
|
||||||
Events.emit('discord-event', {
|
Events.emit('discord-event', {
|
||||||
|
@ -86,6 +101,7 @@ export default class extends EventListener {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
if (evt !== null) Events.emit(`discord:${evt.type}`, evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,6 +27,40 @@ export default class {
|
||||||
data: { userid }
|
data: { userid }
|
||||||
}));
|
}));
|
||||||
}, 100);
|
}, 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() {
|
static get profilePopupModule() {
|
||||||
|
|
|
@ -12,11 +12,11 @@ import Vue from './vue';
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
|
|
||||||
static inject(root, bdnode, components, template) {
|
static inject(root, bdnode, components, template, replaceRoot) {
|
||||||
bdnode.appendTo(root);
|
if(!replaceRoot) bdnode.appendTo(root);
|
||||||
|
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el: bdnode.element,
|
el: replaceRoot ? root : bdnode.element,
|
||||||
components,
|
components,
|
||||||
template
|
template
|
||||||
});
|
});
|
||||||
|
|
|
@ -46,7 +46,8 @@ module.exports = {
|
||||||
path.resolve('src', 'modules'),
|
path.resolve('src', 'modules'),
|
||||||
path.resolve('src', 'ui'),
|
path.resolve('src', 'ui'),
|
||||||
path.resolve('src', 'plugins'),
|
path.resolve('src', 'plugins'),
|
||||||
path.resolve('src', 'structs')
|
path.resolve('src', 'structs'),
|
||||||
|
path.resolve('src', 'builtin')
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
/* resolve: {
|
/* resolve: {
|
||||||
|
|
Loading…
Reference in New Issue