2018-03-07 00:37:14 +01:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2018-03-21 21:47:46 +01:00
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
import { Events, Settings, Globals, WebpackModules, ReactComponents, MonkeyPatch } from 'modules';
|
2018-03-17 22:35:09 +01:00
|
|
|
import { DOM, VueInjector, Reflection } from 'ui';
|
2018-03-31 02:17:42 +02:00
|
|
|
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
|
2018-03-22 03:13:32 +01:00
|
|
|
import path from 'path';
|
2018-03-07 00:37:14 +01:00
|
|
|
import EmoteComponent from './EmoteComponent.vue';
|
2018-03-21 21:47:46 +01:00
|
|
|
|
2018-03-10 04:05:12 +01:00
|
|
|
let emotes = null;
|
2018-03-13 23:34:03 +01:00
|
|
|
const emotesEnabled = true;
|
2018-03-31 02:17:42 +02:00
|
|
|
const enforceWrapperFrom = (new Date('2018-05-01')).valueOf();
|
2018-03-07 00:37:14 +01:00
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
export default new class EmoteModule {
|
2018-03-22 03:13:32 +01:00
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
constructor() {
|
|
|
|
this.favourite_emotes = [];
|
|
|
|
}
|
|
|
|
|
2018-03-31 04:26:42 +02:00
|
|
|
init() {
|
|
|
|
this.enabledSetting = Settings.getSetting('emotes', 'default', 'enable');
|
|
|
|
this.enabledSetting.on('setting-updated', event => {
|
|
|
|
// Rerender all messages (or if we're disabling emotes, those that have emotes)
|
|
|
|
for (const message of document.querySelectorAll(event.value ? '.message' : '.bd-emote-outer')) {
|
|
|
|
Reflection(event.value ? message : message.closest('.message')).forceUpdate();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return this.observe();
|
|
|
|
}
|
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
/**
|
|
|
|
* Sets an emote as favourite.
|
|
|
|
* @param {String} emote The name of the emote
|
|
|
|
* @param {Boolean} favourite The new favourite state
|
|
|
|
* @param {Boolean} save Whether to save settings
|
|
|
|
* @return {Promise}
|
|
|
|
*/
|
|
|
|
setFavourite(emote, favourite, save = true) {
|
|
|
|
if (favourite && !this.favourite_emotes.includes(emote)) this.favourite_emotes.push(emote);
|
|
|
|
if (!favourite) Utils.removeFromArray(this.favourite_emotes, emote);
|
|
|
|
if (save) return Settings.saveSettings();
|
|
|
|
}
|
|
|
|
|
|
|
|
addFavourite(emote, save = true) {
|
|
|
|
return this.setFavourite(emote, true, save);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeFavourite(emote, save = true) {
|
|
|
|
return this.setFavourite(emote, false, save);
|
|
|
|
}
|
|
|
|
|
|
|
|
isFavourite(emote) {
|
|
|
|
return this.favourite_emotes.includes(emote);
|
|
|
|
}
|
|
|
|
|
|
|
|
get searchCache() {
|
2018-03-15 19:11:46 +01:00
|
|
|
return this._searchCache || (this._searchCache = {});
|
|
|
|
}
|
2018-03-22 03:13:32 +01:00
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
get emoteDb() {
|
2018-03-15 18:44:41 +01:00
|
|
|
return emotes;
|
|
|
|
}
|
2018-03-22 03:13:32 +01:00
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
get React() {
|
2018-03-13 23:34:03 +01:00
|
|
|
return WebpackModules.getModuleByName('React');
|
2018-03-07 00:37:14 +01:00
|
|
|
}
|
2018-03-22 03:13:32 +01:00
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
get ReactDOM() {
|
2018-03-17 17:05:44 +01:00
|
|
|
return WebpackModules.getModuleByName('ReactDOM');
|
|
|
|
}
|
2018-03-22 03:13:32 +01:00
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
processMarkup(markup, timestamp) {
|
2018-03-31 04:26:42 +02:00
|
|
|
if (!this.enabledSetting.value) return markup;
|
2018-03-31 02:17:42 +02:00
|
|
|
|
|
|
|
timestamp = timestamp.valueOf();
|
|
|
|
const allowNoWrapper = timestamp < enforceWrapperFrom;
|
|
|
|
|
2018-03-13 23:34:03 +01:00
|
|
|
const newMarkup = [];
|
2018-03-15 18:07:16 +01:00
|
|
|
for (const child of markup) {
|
2018-03-31 02:17:42 +02:00
|
|
|
if (typeof child !== 'string') {
|
2018-03-15 18:07:16 +01:00
|
|
|
newMarkup.push(child);
|
2018-03-07 00:37:14 +01:00
|
|
|
continue;
|
|
|
|
}
|
2018-03-31 02:17:42 +02:00
|
|
|
if (!this.testWord(child) && !allowNoWrapper) {
|
2018-03-15 18:07:16 +01:00
|
|
|
newMarkup.push(child);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const words = child.split(/([^\s]+)([\s]|$)/g);
|
2018-03-13 23:34:03 +01:00
|
|
|
if (!words) continue;
|
2018-03-07 00:37:14 +01:00
|
|
|
let text = null;
|
2018-03-15 18:07:16 +01:00
|
|
|
for (const [wordIndex, word] of words.entries()) {
|
|
|
|
const isEmote = this.isEmote(word);
|
2018-03-07 00:37:14 +01:00
|
|
|
if (isEmote) {
|
|
|
|
if (text !== null) {
|
2018-03-13 23:34:03 +01:00
|
|
|
newMarkup.push(text);
|
2018-03-07 00:37:14 +01:00
|
|
|
text = null;
|
|
|
|
}
|
2018-03-31 02:17:42 +02:00
|
|
|
newMarkup.push(this.React.createElement('span', {
|
|
|
|
className: 'bd-emote-outer',
|
|
|
|
'data-bdemote-name': isEmote.name,
|
|
|
|
'data-bdemote-src': isEmote.src,
|
|
|
|
'data-has-wrapper': /;[\w]+;/gmi.test(word)
|
|
|
|
}));
|
2018-03-07 00:37:14 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (text === null) {
|
2018-03-15 18:07:16 +01:00
|
|
|
text = word;
|
2018-03-07 00:37:14 +01:00
|
|
|
} else {
|
2018-03-15 18:07:16 +01:00
|
|
|
text += word;
|
2018-03-07 00:37:14 +01:00
|
|
|
}
|
2018-03-15 18:07:16 +01:00
|
|
|
if (wordIndex === words.length - 1) {
|
2018-03-13 23:34:03 +01:00
|
|
|
newMarkup.push(text);
|
2018-03-07 00:37:14 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-03-13 23:34:03 +01:00
|
|
|
return newMarkup;
|
|
|
|
}
|
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
testWord(word) {
|
|
|
|
return !/;[\w]+;/gmi.test(word);
|
2018-03-13 23:34:03 +01:00
|
|
|
}
|
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
injectAll() {
|
2018-03-13 23:34:03 +01:00
|
|
|
if (!emotesEnabled) return;
|
|
|
|
const all = document.getElementsByClassName('bd-emote-outer');
|
|
|
|
for (const ec of all) {
|
|
|
|
if (ec.children.length) continue;
|
|
|
|
this.injectEmote(ec);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
findByProp(obj, what, value) {
|
2018-03-17 17:05:44 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
async observe() {
|
2018-03-22 03:13:32 +01:00
|
|
|
const dataPath = Globals.getPath('data');
|
|
|
|
try {
|
|
|
|
emotes = await FileUtils.readJsonFromFile(path.join(dataPath, 'emotes.json'));
|
|
|
|
} catch (err) {
|
|
|
|
Logger.err('EmoteModule', [`Failed to load emote data. Make sure you've downloaded the emote data and placed it in ${dataPath}:`, err]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-03-13 23:34:03 +01:00
|
|
|
try {
|
|
|
|
const Message = await ReactComponents.getComponent('Message');
|
2018-03-17 17:05:44 +01:00
|
|
|
this.unpatchRender = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('render', (component, args, retVal) => {
|
|
|
|
try {
|
2018-03-22 03:13:32 +01:00
|
|
|
// First child has all the actual text content, second is the edited timestamp
|
|
|
|
const markup = this.findByProp(retVal, 'className', 'markup');
|
2018-03-17 17:05:44 +01:00
|
|
|
if (!markup) return;
|
2018-03-31 02:17:42 +02:00
|
|
|
Logger.log('EmoteModule', ['Message :', retVal, component]);
|
|
|
|
markup.children[0] = this.processMarkup(markup.children[0], component.props.message.editedTimestamp || component.props.message.timestamp);
|
2018-03-17 17:05:44 +01:00
|
|
|
} catch (err) {
|
|
|
|
Logger.err('EmoteModule', err);
|
|
|
|
}
|
|
|
|
});
|
2018-03-17 22:35:09 +01:00
|
|
|
for (const message of document.querySelectorAll('.message')) {
|
|
|
|
Reflection(message).forceUpdate();
|
|
|
|
}
|
|
|
|
this.injectAll();
|
|
|
|
this.unpatchMount = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('componentDidMount', component => {
|
2018-03-17 17:05:44 +01:00
|
|
|
const element = this.ReactDOM.findDOMNode(component);
|
|
|
|
if (!element) return;
|
|
|
|
this.injectEmotes(element);
|
|
|
|
});
|
2018-03-17 22:35:09 +01:00
|
|
|
this.unpatchUpdate = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('componentDidUpdate', component => {
|
2018-03-17 17:05:44 +01:00
|
|
|
const element = this.ReactDOM.findDOMNode(component);
|
|
|
|
if (!element) return;
|
|
|
|
this.injectEmotes(element);
|
2018-03-13 23:34:03 +01:00
|
|
|
});
|
|
|
|
} catch (err) {
|
2018-03-17 17:18:56 +01:00
|
|
|
Logger.err('EmoteModule', err);
|
2018-03-13 23:34:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
injectEmote(root) {
|
2018-03-13 23:34:03 +01:00
|
|
|
if (!emotesEnabled) return;
|
2018-03-17 17:17:50 +01:00
|
|
|
while (root.firstChild) {
|
|
|
|
root.removeChild(root.firstChild);
|
|
|
|
}
|
2018-03-31 02:17:42 +02:00
|
|
|
const { bdemoteName, bdemoteSrc, hasWrapper } = root.dataset;
|
2018-03-15 18:07:16 +01:00
|
|
|
if (!bdemoteName || !bdemoteSrc) return;
|
2018-03-20 22:11:11 +01:00
|
|
|
VueInjector.inject(root, {
|
|
|
|
components: { EmoteComponent },
|
2018-03-31 02:17:42 +02:00
|
|
|
data: { src: bdemoteSrc, name: bdemoteName, hasWrapper },
|
|
|
|
template: '<EmoteComponent :src="src" :name="name" :hasWrapper="hasWrapper" />'
|
2018-03-20 22:11:11 +01:00
|
|
|
}, DOM.createElement('span'));
|
2018-03-15 18:07:16 +01:00
|
|
|
root.classList.add('bd-is-emote');
|
2018-03-13 23:34:03 +01:00
|
|
|
}
|
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
injectEmotes(element) {
|
2018-03-13 23:34:03 +01:00
|
|
|
if (!emotesEnabled || !element) return;
|
|
|
|
for (const beo of element.getElementsByClassName('bd-emote-outer')) this.injectEmote(beo);
|
2018-03-07 00:37:14 +01:00
|
|
|
}
|
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
isEmote(word) {
|
2018-03-10 04:05:12 +01:00
|
|
|
if (!emotes) return null;
|
2018-03-15 18:07:16 +01:00
|
|
|
const name = word.replace(/;/g, '');
|
2018-03-10 04:05:12 +01:00
|
|
|
const emote = emotes.find(emote => emote.id === name);
|
|
|
|
if (!emote) return null;
|
|
|
|
let { id, value } = emote;
|
|
|
|
if (value.id) value = value.id;
|
|
|
|
const uri = emote.type === 2 ? 'https://cdn.betterttv.net/emote/:id/1x' : emote.type === 1 ? 'https://cdn.frankerfacez.com/emoticon/:id/1' : 'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0';
|
|
|
|
return { name, src: uri.replace(':id', value) };
|
|
|
|
}
|
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
filterTest() {
|
2018-03-10 04:05:12 +01:00
|
|
|
const re = new RegExp('Kappa', 'i');
|
|
|
|
const filtered = emotes.filter(emote => re.test(emote.id));
|
|
|
|
return filtered.slice(0, 10);
|
2018-03-07 00:37:14 +01:00
|
|
|
}
|
2018-03-10 04:29:04 +01:00
|
|
|
|
2018-03-31 02:17:42 +02:00
|
|
|
filter(regex, limit, start = 0) {
|
2018-03-15 19:11:46 +01:00
|
|
|
const key = `${regex}:${limit}:${start}`;
|
|
|
|
if (this.searchCache.hasOwnProperty(key)) return this.searchCache[key];
|
2018-03-10 04:29:04 +01:00
|
|
|
let index = 0;
|
2018-03-15 18:44:41 +01:00
|
|
|
let startIndex = 0;
|
2018-03-15 19:11:46 +01:00
|
|
|
return this.searchCache[key] = emotes.filter(emote => {
|
2018-03-10 04:29:04 +01:00
|
|
|
if (index >= limit) return false;
|
|
|
|
if (regex.test(emote.id)) {
|
2018-03-15 18:44:41 +01:00
|
|
|
if (startIndex < start) {
|
|
|
|
startIndex++;
|
|
|
|
return false;
|
|
|
|
}
|
2018-03-10 04:29:04 +01:00
|
|
|
index++;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2018-03-22 03:13:32 +01:00
|
|
|
|
2018-03-07 00:37:14 +01:00
|
|
|
}
|