From 333fbe28c7086f9ea070c5bdd2fc4f5eb5d7fa06 Mon Sep 17 00:00:00 2001 From: Mitchdev Date: Sat, 28 Oct 2023 19:34:17 +1300 Subject: [PATCH] Update emotes without having to refresh chat. --- assets/chat/css/style.scss | 1 + assets/chat/js/autocomplete.js | 10 +++ assets/chat/js/chat.js | 90 ++++++++++++++++--- assets/chat/js/emotes.js | 11 ++- .../js/messages/ChatGenericEventMessage.js | 50 +++++++++++ assets/chat/js/messages/ChatMessage.js | 5 ++ assets/chat/js/messages/MessageBuilder.js | 5 ++ assets/chat/js/messages/MessageTypes.js | 1 + assets/chat/js/messages/index.js | 1 + assets/chat/js/window.js | 6 +- assets/views/templates.html | 2 +- 11 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 assets/chat/js/messages/ChatGenericEventMessage.js diff --git a/assets/chat/css/style.scss b/assets/chat/css/style.scss index 00834d9c..a4f01a02 100644 --- a/assets/chat/css/style.scss +++ b/assets/chat/css/style.scss @@ -541,6 +541,7 @@ hr { } } +.msg-genevent, .msg-donation, .msg-subscription, .msg-giftsub, diff --git a/assets/chat/js/autocomplete.js b/assets/chat/js/autocomplete.js index 3ff9cd3f..6a213c89 100644 --- a/assets/chat/js/autocomplete.js +++ b/assets/chat/js/autocomplete.js @@ -232,6 +232,16 @@ class ChatAutoComplete { } } + removeEmotes() { + this.buckets.forEach((bucket, key) => { + if (key !== '/') { + bucket.forEach((ac, str) => { + if (ac.isemote) bucket.delete(str); + }); + } + }); + } + select(index) { this.selected = Math.min(index, this.results.length - 1); const result = this.results[this.selected]; diff --git a/assets/chat/js/chat.js b/assets/chat/js/chat.js index 7f5c2078..601932be 100644 --- a/assets/chat/js/chat.js +++ b/assets/chat/js/chat.js @@ -53,6 +53,7 @@ import makeSafeForRegex, { import { HashLinkConverter, MISSING_ARG_ERROR } from './hashlinkconverter'; import ChatCommands from './commands'; import MessageTemplateHTML from '../../views/templates.html'; +import { EmoteFormatter } from './formatters'; class Chat { constructor(config) { @@ -85,6 +86,7 @@ class Chat { this.flairs = []; this.flairsMap = new Map(); this.emoteService = new EmoteService(); + this.emoteformatter = new EmoteFormatter(); this.user = new ChatUser(); this.users = new Map(); @@ -141,6 +143,10 @@ class Chat { this.source.on('MASSGIFT', (data) => this.onMASSGIFT(data)); this.source.on('DONATION', (data) => this.onDONATION(data)); this.source.on('UPDATEUSER', (data) => this.onUPDATEUSER(data)); + this.source.on('THEMECHANGED', (data) => this.onTHEMECHANGED(data)); + this.source.on('EMOTECHANGED', (data) => this.onEMOTECHANGED(data)); + this.source.on('EMOTEADDED', (data) => this.onEMOTEADDED(data)); + this.source.on('EMOTEREMOVED', (data) => this.onEMOTEREMOVED(data)); this.control.on('SEND', (data) => this.cmdSEND(data)); this.control.on('HINT', (data) => this.cmdHINT(data)); @@ -503,18 +509,35 @@ class Chat { await this.loadFlairs(); } - async loadEmotes() { + async reloadEmotes(cacheKey, updateMessages = true) { + this.config.cacheKey = cacheKey; + await this.loadEmotes(); + + if (updateMessages) { + for (const window of this.windows.values()) { + window.updateMessages(this, true); + } + } + } + + async loadEmotes(cssOnly = false) { + const emotecss = document.getElementById('emotescss'); + if (emotecss) emotecss.remove(); Chat.loadCss( `${this.config.cdn.base}/emotes/emotes.css?_=${this.config.cacheKey}`, + 'emotescss', ); - return fetch( - `${this.config.cdn.base}/emotes/emotes.json?_=${this.config.cacheKey}`, - ) - .then((res) => res.json()) - .then((json) => { - this.setEmotes(json); - }) - .catch(() => {}); + if (!cssOnly) { + return fetch( + `${this.config.cdn.base}/emotes/emotes.json?_=${this.config.cacheKey}`, + ) + .then((res) => res.json()) + .then((json) => { + this.setEmotes(json); + }) + .catch(() => {}); + } + return null; } async loadFlairs() { @@ -561,6 +584,7 @@ class Chat { setEmotes(emotes) { this.emoteService.setEmotes(emotes); + this.autocomplete.removeEmotes(); this.emoteService .emotesForUser(this.user) .map((e) => e.prefix) @@ -1399,6 +1423,49 @@ class Chat { } } + onTHEMECHANGED(data) { + this.reloadEmotes(data.cacheKey); + + MessageBuilder.event({ + infoHtml: `Theme changed to ${data.label}`, + borderColor: data.color, + }).into(this); + } + + async onEMOTECHANGED(data) { + await this.reloadEmotes(data.cacheKey); + + // Only send a message if prefix changed. + if (data.oldEmote.prefix !== data.newEmote.prefix) { + MessageBuilder.event({ + infoHtml: `${data.oldEmote.prefix} was changed to ${ + data.newEmote.prefix + } ${this.emoteformatter.format(this, data.newEmote.prefix)}`, + }).into(this); + } + } + + async onEMOTEADDED(data) { + await this.reloadEmotes(data.cacheKey, false); + + MessageBuilder.event({ + infoHtml: `${data.emote.prefix} was added ${this.emoteformatter.format( + this, + data.emote.prefix, + )}`, + borderColor: 'success', + }).into(this); + } + + onEMOTEREMOVED(data) { + this.reloadEmotes(data.cacheKey); + + MessageBuilder.event({ + infoHtml: `${data.prefix} was removed`, + borderColor: 'fail', + }).into(this); + } + cmdSHOWPOLL() { if (this.chatpoll.poll) { this.chatpoll.show(); @@ -2303,7 +2370,7 @@ class Chat { } static removeSlashCmdFromText(msg) { - return msg.replace(regexslashcmd, '').trim(); + return msg ? msg.replace(regexslashcmd, '').trim() : ''; } static extractNicks(text) { @@ -2368,12 +2435,13 @@ class Chat { return nanoseconds; } - static loadCss(url) { + static loadCss(url, id) { const link = document.createElement('link'); link.href = url; link.type = 'text/css'; link.rel = 'stylesheet'; link.media = 'screen'; + if (id) link.id = id; document.getElementsByTagName('head')[0].appendChild(link); return link; } diff --git a/assets/chat/js/emotes.js b/assets/chat/js/emotes.js index 0b9aeaa9..aa64ebea 100644 --- a/assets/chat/js/emotes.js +++ b/assets/chat/js/emotes.js @@ -1,5 +1,5 @@ export default class EmoteService { - tiers = new Set(); + emoteTiers = new Set(); emotesMapped = new Map(); @@ -41,17 +41,22 @@ export default class EmoteService { return this.emotes.filter((e) => e.twitch).map((e) => e.prefix); } + get tiers() { + return Array.from(this.emoteTiers).sort((a, b) => a - b); + } + getEmote(emote) { return this.emotesMapped.get(emote); } setEmotes(emotes) { + this.emoteTiers = new Set(); + this.emotesMapped = new Map(); this.emotes = emotes; emotes.forEach((e) => { - this.tiers.add(e.minimumSubTier); + this.emoteTiers.add(e.minimumSubTier); this.emotesMapped.set(e.prefix, e); }); - this.tiers = Array.from(this.tiers).sort((a, b) => a - b); } emotePrefixesForTier(tier) { diff --git a/assets/chat/js/messages/ChatGenericEventMessage.js b/assets/chat/js/messages/ChatGenericEventMessage.js new file mode 100644 index 00000000..dbcf4411 --- /dev/null +++ b/assets/chat/js/messages/ChatGenericEventMessage.js @@ -0,0 +1,50 @@ +import ChatEventMessage from './ChatEventMessage'; +import MessageTypes from './MessageTypes'; + +/* + + UPDATE TO TYPESCRIPT + + options { + infoHtml: string, + bottomText: string, + borderColor: 'success' | 'fail' | 'info' | 'css color' + } + +*/ + +function getBorderColor(color) { + switch (color) { + case 'success': + return '#61bd4f'; // $color-green + case 'danger': + case 'fail': + return '#eb5a46'; // $color-red + case 'info': + return '#ffab4a'; // $color-orange + default: + return color; + } +} + +export default class ChatGenericEventMessage extends ChatEventMessage { + constructor(options) { + super(options.bottomText, options.timestamp); + this.type = MessageTypes.GENEVENT; + this.options = options; + } + + html(chat = null) { + const eventTemplate = super.html(chat); + + eventTemplate.querySelector('.event-info').innerHTML = + this.options.infoHtml ?? ''; + + const attr = {}; + if (this.options.borderColor) { + attr.style = `border-color: ${getBorderColor(this.options.borderColor)}`; + } + + return this.wrap(eventTemplate.innerHTML, [], attr); + } +} diff --git a/assets/chat/js/messages/ChatMessage.js b/assets/chat/js/messages/ChatMessage.js index f5a2d5ff..3f7b509a 100644 --- a/assets/chat/js/messages/ChatMessage.js +++ b/assets/chat/js/messages/ChatMessage.js @@ -53,6 +53,11 @@ export default class ChatMessage extends ChatUIMessage { ); } + rebuildTxt(chat) { + const text = this.buildMessageTxt(chat); + this.ui.getElementsByClassName('text')[0].outerHTML = text; + } + buildMessageTxt(chat) { // TODO we strip off the `/me ` of every message -- must be a better way to do this let msg = diff --git a/assets/chat/js/messages/MessageBuilder.js b/assets/chat/js/messages/MessageBuilder.js index 4baf7f93..9baa19ed 100644 --- a/assets/chat/js/messages/MessageBuilder.js +++ b/assets/chat/js/messages/MessageBuilder.js @@ -9,6 +9,7 @@ import ChatDonationMessage from './ChatDonationMessage'; import ChatRegularSubscriptionMessage from './subscriptions/ChatRegularSubscriptionMessage'; import ChatGiftedSubscriptionMessage from './subscriptions/ChatGiftedSubscriptionMessage'; import ChatMassSubscriptionMessage from './subscriptions/ChatMassSubscriptionMessage'; +import ChatGenericEventMessage from './ChatGenericEventMessage'; export default class MessageBuilder { static element(message, classes = []) { @@ -60,6 +61,10 @@ export default class MessageBuilder { return new PinnedMessage(message, user, timestamp, uuid); } + static event(options) { + return new ChatGenericEventMessage(options); + } + static subscription(data) { return new ChatRegularSubscriptionMessage( data.data, diff --git a/assets/chat/js/messages/MessageTypes.js b/assets/chat/js/messages/MessageTypes.js index 312a0008..5adefe54 100644 --- a/assets/chat/js/messages/MessageTypes.js +++ b/assets/chat/js/messages/MessageTypes.js @@ -13,4 +13,5 @@ export default { GIFTSUB: 'GIFTSUB', MASSGIFT: 'MASSGIFT', DONATION: 'DONATION', + GENEVENT: 'GENEVENT', }; diff --git a/assets/chat/js/messages/index.js b/assets/chat/js/messages/index.js index 4732353c..9804645a 100644 --- a/assets/chat/js/messages/index.js +++ b/assets/chat/js/messages/index.js @@ -8,6 +8,7 @@ export { default as ChatUserMessage } from './ChatUserMessage'; export { default as ChatGiftedSubscriptionMessage } from './subscriptions/ChatGiftedSubscriptionMessage'; export { default as ChatMassSubscriptionMessage } from './subscriptions/ChatMassSubscriptionMessage'; export { default as ChatRegularSubscriptionMessage } from './subscriptions/ChatRegularSubscriptionMessage'; +export { default as ChatGenericEventMessage } from './ChatGenericEventMessage'; export { default as PinnedMessage, checkIfPinWasDismissed, diff --git a/assets/chat/js/window.js b/assets/chat/js/window.js index c47de9b0..05c8b181 100644 --- a/assets/chat/js/window.js +++ b/assets/chat/js/window.js @@ -121,7 +121,7 @@ class ChatWindow extends EventEmitter { * Use chat state (settings and authentication data) to update the messages in * this window. */ - updateMessages(chat) { + updateMessages(chat, rebuildTxt = false) { for (const message of this.messages) { if (message.type !== MessageTypes.UI) { message.updateTimeFormat(); @@ -136,6 +136,10 @@ class ChatWindow extends EventEmitter { message.setTag(chat.taggednicks.get(username)); message.setTagTitle(chat.taggednotes.get(username)); + if (rebuildTxt) { + message.rebuildTxt(chat); + } + if (message.moderated) { message.censor(parseInt(chat.settings.get('showremoved') || '1', 10)); } diff --git a/assets/views/templates.html b/assets/views/templates.html index 22d7d42d..be214055 100644 --- a/assets/views/templates.html +++ b/assets/views/templates.html @@ -2,7 +2,7 @@
- +