From c712b76ef79a620e6e9a5da60eec6838eb88c759 Mon Sep 17 00:00:00 2001 From: vyneer Date: Mon, 11 Mar 2024 20:51:52 +0300 Subject: [PATCH 01/95] feat: add additional data to paid events --- assets/chat/js/messages/ChatDonationMessage.js | 5 +++-- assets/chat/js/messages/ChatEventMessage.js | 3 ++- assets/chat/js/messages/MessageBuilder.js | 8 ++++++++ .../ChatGiftedSubscriptionMessage.js | 16 ++++++++++++++-- .../subscriptions/ChatMassSubscriptionMessage.js | 14 ++++++++++++-- .../ChatRegularSubscriptionMessage.js | 14 ++++++++++++-- .../subscriptions/ChatSubscriptionMessage.js | 6 ++++-- 7 files changed, 55 insertions(+), 11 deletions(-) diff --git a/assets/chat/js/messages/ChatDonationMessage.js b/assets/chat/js/messages/ChatDonationMessage.js index f3de6e76..715cd740 100644 --- a/assets/chat/js/messages/ChatDonationMessage.js +++ b/assets/chat/js/messages/ChatDonationMessage.js @@ -5,11 +5,12 @@ import MessageTypes from './MessageTypes'; const DONATION_TIERS = [0, 5, 10, 25, 50, 100]; export default class ChatDonationMessage extends ChatEventMessage { - constructor(message, user, amount, timestamp) { - super(message, timestamp); + constructor(message, user, amount, timestamp, expiry, uuid) { + super(message, timestamp, uuid); this.user = user; this.type = MessageTypes.DONATION; this.amount = amount; + this.expiry = expiry; } /** diff --git a/assets/chat/js/messages/ChatEventMessage.js b/assets/chat/js/messages/ChatEventMessage.js index 06834cad..c41f9f31 100644 --- a/assets/chat/js/messages/ChatEventMessage.js +++ b/assets/chat/js/messages/ChatEventMessage.js @@ -1,13 +1,14 @@ import ChatMessage from './ChatMessage'; export default class ChatEventMessage extends ChatMessage { - constructor(message, timestamp) { + constructor(message, timestamp, uuid) { super(message, timestamp); this.tag = null; this.title = ''; this.slashme = false; this.isown = false; this.mentioned = []; + this.uuid = uuid; } html(chat = null) { diff --git a/assets/chat/js/messages/MessageBuilder.js b/assets/chat/js/messages/MessageBuilder.js index 53739c20..11ce36e9 100644 --- a/assets/chat/js/messages/MessageBuilder.js +++ b/assets/chat/js/messages/MessageBuilder.js @@ -68,8 +68,10 @@ export default class MessageBuilder { new ChatUser(data.user), data.tier, data.tierLabel, + data.amount, data.streak, data.timestamp, + data.expiry, data.uuid, ); } @@ -80,8 +82,11 @@ export default class MessageBuilder { new ChatUser(data.user), data.tier, data.tierLabel, + data.amount, new ChatUser(data.recipient), + data.fromMassGift, data.timestamp, + data.expiry, data.uuid, ); } @@ -92,8 +97,10 @@ export default class MessageBuilder { new ChatUser(data.user), data.tier, data.tierLabel, + data.amount, data.quantity, data.timestamp, + data.expiry, data.uuid, ); } @@ -104,6 +111,7 @@ export default class MessageBuilder { new ChatUser(data.user), data.amount, data.timestamp, + data.expiry, data.uuid, ); } diff --git a/assets/chat/js/messages/subscriptions/ChatGiftedSubscriptionMessage.js b/assets/chat/js/messages/subscriptions/ChatGiftedSubscriptionMessage.js index b0e39ea6..96a0166c 100644 --- a/assets/chat/js/messages/subscriptions/ChatGiftedSubscriptionMessage.js +++ b/assets/chat/js/messages/subscriptions/ChatGiftedSubscriptionMessage.js @@ -3,10 +3,22 @@ import ChatSubscriptionMessage from './ChatSubscriptionMessage'; import MessageTypes from '../MessageTypes'; export default class ChatGiftedSubscriptionMessage extends ChatSubscriptionMessage { - constructor(message, user, tier, tierLabel, giftee, timestamp) { - super(message, user, tier, tierLabel, timestamp); + constructor( + message, + user, + tier, + tierLabel, + amount, + giftee, + fromMassGift, + timestamp, + expiry, + uuid, + ) { + super(message, user, tier, tierLabel, amount, timestamp, expiry, uuid); this.type = MessageTypes.GIFTSUB; this.giftee = giftee; + this.fromMassGift = fromMassGift; } html(chat = null) { diff --git a/assets/chat/js/messages/subscriptions/ChatMassSubscriptionMessage.js b/assets/chat/js/messages/subscriptions/ChatMassSubscriptionMessage.js index a2e9ed6b..9bb82712 100644 --- a/assets/chat/js/messages/subscriptions/ChatMassSubscriptionMessage.js +++ b/assets/chat/js/messages/subscriptions/ChatMassSubscriptionMessage.js @@ -2,8 +2,18 @@ import ChatSubscriptionMessage from './ChatSubscriptionMessage'; import MessageTypes from '../MessageTypes'; export default class ChatMassSubscriptionMessage extends ChatSubscriptionMessage { - constructor(message, user, tier, tierLabel, quantity, timestamp) { - super(message, user, tier, tierLabel, timestamp); + constructor( + message, + user, + tier, + tierLabel, + amount, + quantity, + timestamp, + expiry, + uuid, + ) { + super(message, user, tier, tierLabel, amount, timestamp, expiry, uuid); this.type = MessageTypes.MASSGIFT; this.quantity = quantity; } diff --git a/assets/chat/js/messages/subscriptions/ChatRegularSubscriptionMessage.js b/assets/chat/js/messages/subscriptions/ChatRegularSubscriptionMessage.js index 797a4f98..ba29a5db 100644 --- a/assets/chat/js/messages/subscriptions/ChatRegularSubscriptionMessage.js +++ b/assets/chat/js/messages/subscriptions/ChatRegularSubscriptionMessage.js @@ -2,8 +2,18 @@ import ChatSubscriptionMessage from './ChatSubscriptionMessage'; import MessageTypes from '../MessageTypes'; export default class ChatRegularSubscriptionMessage extends ChatSubscriptionMessage { - constructor(message, user, tier, tierLabel, streak, timestamp) { - super(message, user, tier, tierLabel, timestamp); + constructor( + message, + user, + tier, + tierLabel, + amount, + streak, + timestamp, + expiry, + uuid, + ) { + super(message, user, tier, tierLabel, amount, timestamp, expiry, uuid); this.type = MessageTypes.SUBSCRIPTION; this.streak = streak; } diff --git a/assets/chat/js/messages/subscriptions/ChatSubscriptionMessage.js b/assets/chat/js/messages/subscriptions/ChatSubscriptionMessage.js index 7a076d3b..b7353a16 100644 --- a/assets/chat/js/messages/subscriptions/ChatSubscriptionMessage.js +++ b/assets/chat/js/messages/subscriptions/ChatSubscriptionMessage.js @@ -3,11 +3,13 @@ import ChatEventMessage from '../ChatEventMessage'; import features from '../../features'; export default class ChatSubscriptionMessage extends ChatEventMessage { - constructor(message, user, tier, tierLabel, timestamp) { - super(message, timestamp); + constructor(message, user, tier, tierLabel, amount, timestamp, expiry, uuid) { + super(message, timestamp, uuid); this.user = user; this.tier = tier; this.tierLabel = tierLabel; + this.amount = amount; + this.expiry = expiry; } getTierStyles(chat = null) { From 83d5ecabadd2d916b01548dbd443a0da6e96e581 Mon Sep 17 00:00:00 2001 From: vyneer Date: Mon, 11 Mar 2024 20:53:01 +0300 Subject: [PATCH 02/95] feat: add a function to completely hide the pinned message --- assets/chat/js/messages/PinnedMessage.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/assets/chat/js/messages/PinnedMessage.js b/assets/chat/js/messages/PinnedMessage.js index cd83c2a1..a1fe72b5 100644 --- a/assets/chat/js/messages/PinnedMessage.js +++ b/assets/chat/js/messages/PinnedMessage.js @@ -45,15 +45,23 @@ export default class PinnedMessage extends ChatUserMessage { /** * Shows/hides the current message. * @param {boolean} state - * @returns {null} null */ - set visible(state) { + set displayed(state) { this.ui.classList.toggle('hidden', !state); document .getElementById('chat-pinned-show-btn') ?.classList.toggle('active', !state); } + /** + * Shows/hides the full pinned message frame. + * @param {boolean} state + */ + set hidden(state) { + const frame = document.getElementById('chat-pinned-frame'); + frame.classList.toggle('active', !state); + } + /** * Unpins the current message. * @returns {null} null @@ -61,8 +69,8 @@ export default class PinnedMessage extends ChatUserMessage { unpin() { dismissPin(this.uuid); + this.hidden = true; const frame = document.getElementById('chat-pinned-frame'); - frame.classList.toggle('active', false); frame.replaceChildren(); return null; @@ -77,7 +85,7 @@ export default class PinnedMessage extends ChatUserMessage { pin(chat = null, visibility = true) { this.ui.id = 'msg-pinned'; this.ui.classList.toggle('msg-pinned', true); - this.visible = visibility; + this.displayed = visibility; this.ui.querySelector('span.features').classList.toggle('hidden', true); chat.mainwindow.update(); @@ -108,7 +116,7 @@ export default class PinnedMessage extends ChatUserMessage { showPin.title = 'Show Pinned Message'; showPin.addEventListener('click', () => { - this.visible = true; + this.displayed = true; }); const closePin = document.createElement('a'); @@ -122,7 +130,7 @@ export default class PinnedMessage extends ChatUserMessage { closePin.addEventListener('click', () => { dismissPin(this.uuid); - this.visible = false; + this.displayed = false; }); this.ui.prepend(closePin); From bf100a8caa30f28235ab31b6c8f3eafe7029a328 Mon Sep 17 00:00:00 2001 From: vyneer Date: Tue, 12 Mar 2024 18:54:51 +0300 Subject: [PATCH 03/95] feat: add a youtube-like paid event bar --- assets/chat/css/style.scss | 123 +++++++++++++++++++- assets/chat/js/chat.js | 45 +++++++- assets/chat/js/focus.js | 5 +- assets/chat/js/menus/ChatEventBar.js | 165 +++++++++++++++++++++++++++ assets/chat/js/menus/index.js | 1 + assets/chat/js/source.js | 13 ++- assets/views/embed.html | 7 +- assets/views/stream.html | 6 +- 8 files changed, 352 insertions(+), 13 deletions(-) create mode 100644 assets/chat/js/menus/ChatEventBar.js diff --git a/assets/chat/css/style.scss b/assets/chat/css/style.scss index 9b100c4e..22ea6e27 100644 --- a/assets/chat/css/style.scss +++ b/assets/chat/css/style.scss @@ -765,13 +765,121 @@ hr { background-color: $color-chat-highlight; } -/* Pinned Messages */ -#chat-pinned-frame { - display: none; +/* Event bar */ +#chat-event-bar { + &:empty { + display: none !important; + } + + display: inline-flex; + overflow-x: scroll !important; + background: #151515; + z-index: 6; + + // width = height (lmao firefox) + scrollbar-width: none; + --ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } + + .msg-event-bar-wrapper { + cursor: pointer; + transition: + transform 500ms, + opacity 500ms; + opacity: 0; + transform: scale(0.1); + + &.active { + transform: unset; + opacity: 1; + } + + &:hover { + transition: transform 100ms; + transform: scale(1.05) !important; + } + + &.removed { + transform: scale(0.1) !important; + } + } + + .msg-chat { + margin: $gutter-sm $gutter-sm $gutter-sm $gutter-sm; + border-width: 2px; + } + + .user { + &.scrolling { + display: inline-block; + animation: scrolling-event-username 12s linear 3s infinite; + word-wrap: normal; + } + + &::before { + content: unset; + } + + &:hover { + text-decoration: none; + } + } + + .event-top { + padding: 0 $gutter-xs 0 0.4em; + } + + .event-info { + padding-right: $gutter-xs; + + &.scrolling { + overflow: hidden; + width: 74px; + } + } + + .event-icon { + width: 2em; + height: 2em; + } + + .rainbow-border::before { + margin: -2px; + } +} + +@keyframes scrolling-event-username { + 25%, + 50% { + transform: translateX(calc(-100% + 74px)); + } + + 75%, + 100% { + transform: translateX(0%); + } +} + +/* Hovering frame containing highlighted event from event bar and pinned message */ +#chat-hover-frame { padding: 4px; position: absolute; width: 100%; z-index: 210; +} + +/* Event highlight frame */ +#chat-event-highlight-frame { + .msg-event-bar { + margin: $gutter-sm $gutter-sm $gutter-sm $gutter-sm; + } +} + +/* Pinned Messages */ +#chat-pinned-frame { + display: none; &.active { display: block; @@ -878,6 +986,10 @@ hr { &.msg-pinned { opacity: 1; } + + &.msg-event-bar { + opacity: 1; + } } /* Emotes and combo */ @@ -1890,7 +2002,10 @@ button.btn { #chat-output-frame { margin: 0; } - #chat-pinned-frame { + #chat-event-bar { + display: none !important; + } + #chat-hover-frame { display: none !important; } diff --git a/assets/chat/js/chat.js b/assets/chat/js/chat.js index 2e6740b0..73a66edf 100644 --- a/assets/chat/js/chat.js +++ b/assets/chat/js/chat.js @@ -31,6 +31,7 @@ import { ChatEmoteTooltip, ChatSettingsMenu, ChatUserInfoMenu, + ChatEventBar, } from './menus'; import ChatAutoComplete from './autocomplete'; import ChatInputHistory from './history'; @@ -145,6 +146,7 @@ class Chat { this.source.on('DONATION', (data) => this.onDONATION(data)); this.source.on('UPDATEUSER', (data) => this.onUPDATEUSER(data)); this.source.on('DEATH', (data) => this.onDEATH(data)); + this.source.on('PAIDEVENTS', (data) => this.onPAIDEVENTS(data)); this.control.on('SEND', (data) => this.cmdSEND(data)); this.control.on('HINT', (data) => this.cmdHINT(data)); @@ -294,6 +296,7 @@ class Chat { this.mainwindow = new ChatWindow('main').into(this); this.mutedtimer = new MutedTimer(this); this.chatpoll = new ChatPoll(this); + this.eventBar = new ChatEventBar(this); this.pinnedMessage = null; this.windowToFront('main'); @@ -1284,19 +1287,53 @@ class Chat { } onSUBSCRIPTION(data) { - MessageBuilder.subscription(data).into(this); + const event = MessageBuilder.subscription(data); + event.into(this); + this.eventBar.add(event); } onGIFTSUB(data) { - MessageBuilder.gift(data).into(this); + const event = MessageBuilder.gift(data); + event.into(this); + this.eventBar.add(event); } onMASSGIFT(data) { - MessageBuilder.massgift(data).into(this); + const event = MessageBuilder.massgift(data); + event.into(this); + this.eventBar.add(event); } onDONATION(data) { - MessageBuilder.donation(data).into(this); + const event = MessageBuilder.donation(data); + event.into(this); + this.eventBar.add(event); + } + + onPAIDEVENTS(lines) { + lines.forEach((line) => { + const { eventname, data } = this.source.parse({ data: line }); + switch (eventname) { + case 'SUBSCRIPTION': { + this.eventBar.add(MessageBuilder.subscription(data)); + break; + } + case 'GIFTSUB': { + this.eventBar.add(MessageBuilder.gift(data)); + break; + } + case 'MASSGIFT': { + this.eventBar.add(MessageBuilder.massgift(data)); + break; + } + case 'DONATION': { + this.eventBar.add(MessageBuilder.donation(data)); + break; + } + default: + break; + } + }); } onPRIVMSGSENT() { diff --git a/assets/chat/js/focus.js b/assets/chat/js/focus.js index f00997c9..6806a5ea 100644 --- a/assets/chat/js/focus.js +++ b/assets/chat/js/focus.js @@ -9,7 +9,10 @@ class ChatUserFocus { this.chat = chat; this.css = css; this.focused = []; - this.chat.output.on('click', (e) => this.toggleElement(e.target)); + this.chat.output.on('click', (e) => { + this.toggleElement(e.target); + this.chat.eventBar.unhighlight(); + }); } toggleElement(target) { diff --git a/assets/chat/js/menus/ChatEventBar.js b/assets/chat/js/menus/ChatEventBar.js new file mode 100644 index 00000000..0bb9b5bd --- /dev/null +++ b/assets/chat/js/menus/ChatEventBar.js @@ -0,0 +1,165 @@ +export default class ChatEventBar { + constructor(chat) { + this.chat = chat; + + /** @type HTMLDivElement */ + this.eventBarUI = document.getElementById('chat-event-bar'); + /** @type HTMLDivElement */ + this.eventHighlightUI = document.getElementById( + 'chat-event-highlight-frame', + ); + + this.eventBarUI.addEventListener('wheel', (event) => { + if (event.deltaX === 0) { + event.preventDefault(); + this.eventBarUI.scrollBy({ + left: event.deltaY > 0 ? 30 : -30, + }); + } + }); + } + + /** + * Adds the event to the event bar. + * @param {ChatEventMessage} event + */ + add(event) { + if (!this.isValidEvent(event)) { + return; + } + + const eventMessageWrapper = document.createElement('div'); + eventMessageWrapper.classList.add('msg-event-bar-wrapper'); + eventMessageWrapper.dataset.uuid = event.uuid; + eventMessageWrapper.addEventListener('click', () => { + this.highlight(event); + this.chat.userfocus.toggleFocus('', false, true); + }); + + /** @type HTMLDivElement */ + const eventMessageUI = event.html(this.chat); + eventMessageUI.classList.add('msg-event-bar'); + eventMessageUI.querySelector('.event-bottom')?.remove(); + const eventInfoUI = eventMessageUI.querySelector('.event-info'); + const user = eventMessageUI.querySelector('.event-info a'); + eventInfoUI.replaceChildren(user); + + if (user.textContent.length >= 12) { + eventInfoUI.classList.add('scrolling'); + user.classList.add('scrolling'); + } + + eventMessageWrapper.append(eventMessageUI); + + this.eventBarUI.prepend(eventMessageWrapper); + setTimeout(() => { + eventMessageWrapper.classList.add('active'); + }, 1); + + // Update chat window to fix the scroll position + this.chat.mainwindow.update(); + + let percentageLeft = this.calculateExpiryPercentage(event); + this.setExpiryPercentage(eventMessageWrapper, percentageLeft); + + const intervalID = setInterval(() => { + percentageLeft = this.calculateExpiryPercentage(event); + + if (percentageLeft <= 0) { + eventMessageWrapper.addEventListener('transitionend', () => { + eventMessageWrapper.remove(); + clearInterval(intervalID); + }); + eventMessageWrapper.classList.replace('active', 'removed'); + return; + } + + this.setExpiryPercentage(eventMessageWrapper, percentageLeft); + }, 1000); + } + + /** + * Unhighlights the currently highlighted event. + */ + unhighlight() { + if (this.eventHighlightUI.hasChildNodes()) { + this.eventHighlightUI.replaceChildren(); + // Unhide pinned message interface + if (this.chat.pinnedMessage) this.chat.pinnedMessage.hidden = false; + } + } + + /** + * Highlights the specified event. + * @param {ChatEventMessage} event + */ + highlight(event) { + /** @type HTMLDivElement */ + const clonedMessageUI = event.html(this.chat); + clonedMessageUI.classList.add('msg-event-bar'); + + this.eventHighlightUI.replaceChildren(); + this.eventHighlightUI.append(clonedMessageUI); + + // Hide full pinned message interface to make everything look nice + if (this.chat.pinnedMessage) this.chat.pinnedMessage.hidden = true; + } + + /** + * Checks if the specified event is already in the event bar. + * @param {ChatEventMessage} event + * @returns {boolean} + */ + contains(event) { + return !!this.eventBarUI.querySelector( + `.msg-event-bar-wrapper[data-uuid="${event.uuid}"]`, + ); + } + + /** + * Checks if the specified event should appear in the event bar. + * @param {ChatEventMessage} event + * @returns {boolean} + * @private + */ + isValidEvent(event) { + if (this.contains(event)) { + return false; + } + + if (event.expiry - Date.now() < 0) { + return false; + } + + if (event.fromMassGift) { + return false; + } + + return true; + } + + /** + * Calculates percentage left until the specified event expires. + * @param {ChatEventMessage} event + * @returns {number} + * @private + */ + calculateExpiryPercentage(event) { + return ( + ((event.expiry - Date.now()) * 100) / (event.expiry - event.timestamp) + ); + } + + /** + * Sets the progress gradient of the specified event. + * @param {HTMLDivElement} eventWrapper + * @param {number} percentageLeft + * @private + */ + setExpiryPercentage(eventWrapper, percentageLeft) { + eventWrapper.dataset.percentageLeft = percentageLeft; + eventWrapper.querySelector( + '.event-top', + ).style.background = `linear-gradient(90deg, #282828, #282828 ${percentageLeft}%, #151515 ${percentageLeft}%, #151515)`; + } +} diff --git a/assets/chat/js/menus/index.js b/assets/chat/js/menus/index.js index 27fe072a..589736d6 100644 --- a/assets/chat/js/menus/index.js +++ b/assets/chat/js/menus/index.js @@ -5,3 +5,4 @@ export { default as ChatEmoteMenu } from './ChatEmoteMenu'; export { default as ChatEmoteTooltip } from './ChatEmoteTooltip'; export { default as ChatWhisperUsers } from './ChatWhisperUsers'; export { default as ChatUserInfoMenu } from './ChatUserInfoMenu'; +export { default as ChatEventBar } from './ChatEventBar'; diff --git a/assets/chat/js/source.js b/assets/chat/js/source.js index 8f815e5e..e49cde90 100644 --- a/assets/chat/js/source.js +++ b/assets/chat/js/source.js @@ -97,6 +97,12 @@ class ChatSource extends EventEmitter { } parseAndDispatch(event) { + const { eventname, data } = this.parse(event); + this.emit('DISPATCH', { data, event: eventname }); // Event is used to hook into all dispatched events + this.emit(eventname, data); + } + + parse(event) { const eventname = event.data.split(' ', 1)[0].toUpperCase(); const payload = event.data.substring(eventname.length + 1); let data = null; @@ -105,8 +111,11 @@ class ChatSource extends EventEmitter { } catch (ignored) { data = payload; } - this.emit('DISPATCH', { data, event: eventname }); // Event is used to hook into all dispatched events - this.emit(eventname, data); + + return { + eventname, + data, + }; } send(eventname, data) { diff --git a/assets/views/embed.html b/assets/views/embed.html index 9665edd7..1f9509c4 100644 --- a/assets/views/embed.html +++ b/assets/views/embed.html @@ -1,6 +1,8 @@
+
+
@@ -24,7 +26,10 @@
-
+
+
+
+
diff --git a/assets/views/stream.html b/assets/views/stream.html index 0adad494..2704ec37 100644 --- a/assets/views/stream.html +++ b/assets/views/stream.html @@ -1,6 +1,10 @@
+
-
+
+
+
+
From 2aca02539d5d3af9fd263384aafb5db3e69f2b1b Mon Sep 17 00:00:00 2001 From: vyneer Date: Thu, 28 Mar 2024 15:58:41 +0300 Subject: [PATCH 04/95] fix: sort events on `PAIDEVENTS` received --- assets/chat/js/chat.js | 1 + assets/chat/js/menus/ChatEventBar.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/assets/chat/js/chat.js b/assets/chat/js/chat.js index 73a66edf..d96089e1 100644 --- a/assets/chat/js/chat.js +++ b/assets/chat/js/chat.js @@ -1334,6 +1334,7 @@ class Chat { break; } }); + this.eventBar.sort(); } onPRIVMSGSENT() { diff --git a/assets/chat/js/menus/ChatEventBar.js b/assets/chat/js/menus/ChatEventBar.js index 0bb9b5bd..67180248 100644 --- a/assets/chat/js/menus/ChatEventBar.js +++ b/assets/chat/js/menus/ChatEventBar.js @@ -31,6 +31,7 @@ export default class ChatEventBar { const eventMessageWrapper = document.createElement('div'); eventMessageWrapper.classList.add('msg-event-bar-wrapper'); eventMessageWrapper.dataset.uuid = event.uuid; + eventMessageWrapper.dataset.unixtimestamp = event.timestamp.valueOf(); eventMessageWrapper.addEventListener('click', () => { this.highlight(event); this.chat.userfocus.toggleFocus('', false, true); @@ -116,6 +117,19 @@ export default class ChatEventBar { ); } + /** + * Sorts the events in the event bar in descending order by time received. + */ + sort() { + [...this.eventBarUI.children] + .sort((a, b) => + Number(a.dataset.unixtimestamp) < Number(b.dataset.unixtimestamp) + ? 1 + : -1, + ) + .forEach((node) => this.eventBarUI.appendChild(node)); + } + /** * Checks if the specified event should appear in the event bar. * @param {ChatEventMessage} event From acf1a47da2183596f32ad78531e0254656f0a4e0 Mon Sep 17 00:00:00 2001 From: vyneer Date: Thu, 28 Mar 2024 18:09:44 +0300 Subject: [PATCH 05/95] chore: fix formatting in ChatEventBar --- assets/chat/js/menus/ChatEventBar.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/chat/js/menus/ChatEventBar.js b/assets/chat/js/menus/ChatEventBar.js index 67180248..6f63800c 100644 --- a/assets/chat/js/menus/ChatEventBar.js +++ b/assets/chat/js/menus/ChatEventBar.js @@ -172,8 +172,7 @@ export default class ChatEventBar { */ setExpiryPercentage(eventWrapper, percentageLeft) { eventWrapper.dataset.percentageLeft = percentageLeft; - eventWrapper.querySelector( - '.event-top', - ).style.background = `linear-gradient(90deg, #282828, #282828 ${percentageLeft}%, #151515 ${percentageLeft}%, #151515)`; + eventWrapper.querySelector('.event-top').style.background = + `linear-gradient(90deg, #282828, #282828 ${percentageLeft}%, #151515 ${percentageLeft}%, #151515)`; } } From 9f36223a047691485c84a7edeef3b4d83483d75d Mon Sep 17 00:00:00 2001 From: vyneer Date: Fri, 26 Jul 2024 15:51:42 +0300 Subject: [PATCH 06/95] chore: rename `expiry` to `expirationTimestamp` --- assets/chat/js/menus/ChatEventBar.js | 20 ++++++++++++------- .../chat/js/messages/ChatDonationMessage.js | 4 ++-- assets/chat/js/messages/MessageBuilder.js | 8 ++++---- .../subscriptions/ChatSubscriptionMessage.js | 13 ++++++++++-- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/assets/chat/js/menus/ChatEventBar.js b/assets/chat/js/menus/ChatEventBar.js index 6f63800c..7e57bc62 100644 --- a/assets/chat/js/menus/ChatEventBar.js +++ b/assets/chat/js/menus/ChatEventBar.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('../messages/ChatEventMessage').default & {expirationTimestamp: number}} ExpiringEvent + */ + export default class ChatEventBar { constructor(chat) { this.chat = chat; @@ -21,7 +25,7 @@ export default class ChatEventBar { /** * Adds the event to the event bar. - * @param {ChatEventMessage} event + * @param {ExpiringEvent} event */ add(event) { if (!this.isValidEvent(event)) { @@ -92,7 +96,7 @@ export default class ChatEventBar { /** * Highlights the specified event. - * @param {ChatEventMessage} event + * @param {ExpiringEvent} event */ highlight(event) { /** @type HTMLDivElement */ @@ -108,7 +112,7 @@ export default class ChatEventBar { /** * Checks if the specified event is already in the event bar. - * @param {ChatEventMessage} event + * @param {ExpiringEvent} event * @returns {boolean} */ contains(event) { @@ -132,7 +136,7 @@ export default class ChatEventBar { /** * Checks if the specified event should appear in the event bar. - * @param {ChatEventMessage} event + * @param {ExpiringEvent} event * @returns {boolean} * @private */ @@ -141,7 +145,8 @@ export default class ChatEventBar { return false; } - if (event.expiry - Date.now() < 0) { + const currentTimestamp = Date.now(); + if (event.expirationTimestamp - currentTimestamp < 0) { return false; } @@ -154,13 +159,14 @@ export default class ChatEventBar { /** * Calculates percentage left until the specified event expires. - * @param {ChatEventMessage} event + * @param {ExpiringEvent} event * @returns {number} * @private */ calculateExpiryPercentage(event) { return ( - ((event.expiry - Date.now()) * 100) / (event.expiry - event.timestamp) + ((event.expirationTimestamp - Date.now()) * 100) / + (event.expirationTimestamp - event.timestamp) ); } diff --git a/assets/chat/js/messages/ChatDonationMessage.js b/assets/chat/js/messages/ChatDonationMessage.js index 715cd740..41db9d5d 100644 --- a/assets/chat/js/messages/ChatDonationMessage.js +++ b/assets/chat/js/messages/ChatDonationMessage.js @@ -5,12 +5,12 @@ import MessageTypes from './MessageTypes'; const DONATION_TIERS = [0, 5, 10, 25, 50, 100]; export default class ChatDonationMessage extends ChatEventMessage { - constructor(message, user, amount, timestamp, expiry, uuid) { + constructor(message, user, amount, timestamp, expirationTimestamp, uuid) { super(message, timestamp, uuid); this.user = user; this.type = MessageTypes.DONATION; this.amount = amount; - this.expiry = expiry; + this.expirationTimestamp = expirationTimestamp; } /** diff --git a/assets/chat/js/messages/MessageBuilder.js b/assets/chat/js/messages/MessageBuilder.js index 11ce36e9..5349d760 100644 --- a/assets/chat/js/messages/MessageBuilder.js +++ b/assets/chat/js/messages/MessageBuilder.js @@ -71,7 +71,7 @@ export default class MessageBuilder { data.amount, data.streak, data.timestamp, - data.expiry, + data.expirationTimestamp, data.uuid, ); } @@ -86,7 +86,7 @@ export default class MessageBuilder { new ChatUser(data.recipient), data.fromMassGift, data.timestamp, - data.expiry, + data.expirationTimestamp, data.uuid, ); } @@ -100,7 +100,7 @@ export default class MessageBuilder { data.amount, data.quantity, data.timestamp, - data.expiry, + data.expirationTimestamp, data.uuid, ); } @@ -111,7 +111,7 @@ export default class MessageBuilder { new ChatUser(data.user), data.amount, data.timestamp, - data.expiry, + data.expirationTimestamp, data.uuid, ); } diff --git a/assets/chat/js/messages/subscriptions/ChatSubscriptionMessage.js b/assets/chat/js/messages/subscriptions/ChatSubscriptionMessage.js index b7353a16..26dee8c1 100644 --- a/assets/chat/js/messages/subscriptions/ChatSubscriptionMessage.js +++ b/assets/chat/js/messages/subscriptions/ChatSubscriptionMessage.js @@ -3,13 +3,22 @@ import ChatEventMessage from '../ChatEventMessage'; import features from '../../features'; export default class ChatSubscriptionMessage extends ChatEventMessage { - constructor(message, user, tier, tierLabel, amount, timestamp, expiry, uuid) { + constructor( + message, + user, + tier, + tierLabel, + amount, + timestamp, + expirationTimestamp, + uuid, + ) { super(message, timestamp, uuid); this.user = user; this.tier = tier; this.tierLabel = tierLabel; this.amount = amount; - this.expiry = expiry; + this.expirationTimestamp = expirationTimestamp; } getTierStyles(chat = null) { From e9371265569dc0f02cb103deb56b6ac710513b65 Mon Sep 17 00:00:00 2001 From: vyneer Date: Fri, 26 Jul 2024 15:56:47 +0300 Subject: [PATCH 07/95] refactor: break up `calculateExpiryPercentage` func --- assets/chat/js/menus/ChatEventBar.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/assets/chat/js/menus/ChatEventBar.js b/assets/chat/js/menus/ChatEventBar.js index 7e57bc62..441f8f81 100644 --- a/assets/chat/js/menus/ChatEventBar.js +++ b/assets/chat/js/menus/ChatEventBar.js @@ -164,10 +164,11 @@ export default class ChatEventBar { * @private */ calculateExpiryPercentage(event) { - return ( - ((event.expirationTimestamp - Date.now()) * 100) / - (event.expirationTimestamp - event.timestamp) - ); + const currentTimestamp = Date.now(); + const eventTimeLeft = event.expirationTimestamp - currentTimestamp; + const eventFullDuration = event.expirationTimestamp - event.timestamp; + + return (eventTimeLeft * 100) / eventFullDuration; } /** From 008ce3af165c171686410c3df9ef25668961314a Mon Sep 17 00:00:00 2001 From: vyneer Date: Fri, 26 Jul 2024 16:47:06 +0300 Subject: [PATCH 08/95] chore: rename event-bar stuff --- assets/chat/css/style.scss | 14 ++--- assets/chat/js/menus/ChatEventBar.js | 65 +++++++++++------------- assets/chat/js/messages/PinnedMessage.js | 6 +-- assets/views/embed.html | 6 +-- assets/views/stream.html | 6 +-- assets/views/templates.html | 2 +- 6 files changed, 48 insertions(+), 51 deletions(-) diff --git a/assets/chat/css/style.scss b/assets/chat/css/style.scss index 22ea6e27..4f9c26a1 100644 --- a/assets/chat/css/style.scss +++ b/assets/chat/css/style.scss @@ -783,7 +783,7 @@ hr { display: none; } - .msg-event-bar-wrapper { + .event-bar-button { cursor: pointer; transition: transform 500ms, @@ -863,7 +863,7 @@ hr { } /* Hovering frame containing highlighted event from event bar and pinned message */ -#chat-hover-frame { +#chat-hover-container { padding: 4px; position: absolute; width: 100%; @@ -871,14 +871,14 @@ hr { } /* Event highlight frame */ -#chat-event-highlight-frame { - .msg-event-bar { +#chat-event-selected { + .event-bar-selected { margin: $gutter-sm $gutter-sm $gutter-sm $gutter-sm; } } /* Pinned Messages */ -#chat-pinned-frame { +#chat-pinned-message { display: none; &.active { @@ -987,7 +987,7 @@ hr { opacity: 1; } - &.msg-event-bar { + &.event-bar-selected { opacity: 1; } } @@ -2005,7 +2005,7 @@ button.btn { #chat-event-bar { display: none !important; } - #chat-hover-frame { + #chat-hover-container { display: none !important; } diff --git a/assets/chat/js/menus/ChatEventBar.js b/assets/chat/js/menus/ChatEventBar.js index 441f8f81..8f196659 100644 --- a/assets/chat/js/menus/ChatEventBar.js +++ b/assets/chat/js/menus/ChatEventBar.js @@ -9,9 +9,7 @@ export default class ChatEventBar { /** @type HTMLDivElement */ this.eventBarUI = document.getElementById('chat-event-bar'); /** @type HTMLDivElement */ - this.eventHighlightUI = document.getElementById( - 'chat-event-highlight-frame', - ); + this.eventSelectUI = document.getElementById('chat-event-selected'); this.eventBarUI.addEventListener('wheel', (event) => { if (event.deltaX === 0) { @@ -28,22 +26,21 @@ export default class ChatEventBar { * @param {ExpiringEvent} event */ add(event) { - if (!this.isValidEvent(event)) { + if (!this.shouldEventBeDisplayed(event)) { return; } - const eventMessageWrapper = document.createElement('div'); - eventMessageWrapper.classList.add('msg-event-bar-wrapper'); - eventMessageWrapper.dataset.uuid = event.uuid; - eventMessageWrapper.dataset.unixtimestamp = event.timestamp.valueOf(); - eventMessageWrapper.addEventListener('click', () => { - this.highlight(event); + const eventButton = document.createElement('div'); + eventButton.classList.add('event-bar-button'); + eventButton.dataset.uuid = event.uuid; + eventButton.dataset.unixtimestamp = event.timestamp.valueOf(); + eventButton.addEventListener('click', () => { + this.select(event); this.chat.userfocus.toggleFocus('', false, true); }); /** @type HTMLDivElement */ const eventMessageUI = event.html(this.chat); - eventMessageUI.classList.add('msg-event-bar'); eventMessageUI.querySelector('.event-bottom')?.remove(); const eventInfoUI = eventMessageUI.querySelector('.event-info'); const user = eventMessageUI.querySelector('.event-info a'); @@ -54,57 +51,57 @@ export default class ChatEventBar { user.classList.add('scrolling'); } - eventMessageWrapper.append(eventMessageUI); + eventButton.append(eventMessageUI); - this.eventBarUI.prepend(eventMessageWrapper); + this.eventBarUI.prepend(eventButton); setTimeout(() => { - eventMessageWrapper.classList.add('active'); + eventButton.classList.add('active'); }, 1); // Update chat window to fix the scroll position this.chat.mainwindow.update(); let percentageLeft = this.calculateExpiryPercentage(event); - this.setExpiryPercentage(eventMessageWrapper, percentageLeft); + this.setExpiryPercentage(eventButton, percentageLeft); const intervalID = setInterval(() => { percentageLeft = this.calculateExpiryPercentage(event); if (percentageLeft <= 0) { - eventMessageWrapper.addEventListener('transitionend', () => { - eventMessageWrapper.remove(); + eventButton.addEventListener('transitionend', () => { + eventButton.remove(); clearInterval(intervalID); }); - eventMessageWrapper.classList.replace('active', 'removed'); + eventButton.classList.replace('active', 'removed'); return; } - this.setExpiryPercentage(eventMessageWrapper, percentageLeft); + this.setExpiryPercentage(eventButton, percentageLeft); }, 1000); } /** - * Unhighlights the currently highlighted event. + * Unselects the currently highlighted event. */ - unhighlight() { - if (this.eventHighlightUI.hasChildNodes()) { - this.eventHighlightUI.replaceChildren(); + unselect() { + if (this.eventSelectUI.hasChildNodes()) { + this.eventSelectUI.replaceChildren(); // Unhide pinned message interface if (this.chat.pinnedMessage) this.chat.pinnedMessage.hidden = false; } } /** - * Highlights the specified event. + * Selects the specified event. * @param {ExpiringEvent} event */ - highlight(event) { + select(event) { /** @type HTMLDivElement */ const clonedMessageUI = event.html(this.chat); - clonedMessageUI.classList.add('msg-event-bar'); + clonedMessageUI.classList.add('event-bar-selected'); - this.eventHighlightUI.replaceChildren(); - this.eventHighlightUI.append(clonedMessageUI); + this.eventSelectUI.replaceChildren(); + this.eventSelectUI.append(clonedMessageUI); // Hide full pinned message interface to make everything look nice if (this.chat.pinnedMessage) this.chat.pinnedMessage.hidden = true; @@ -117,7 +114,7 @@ export default class ChatEventBar { */ contains(event) { return !!this.eventBarUI.querySelector( - `.msg-event-bar-wrapper[data-uuid="${event.uuid}"]`, + `.event-bar-button[data-uuid="${event.uuid}"]`, ); } @@ -140,7 +137,7 @@ export default class ChatEventBar { * @returns {boolean} * @private */ - isValidEvent(event) { + shouldEventBeDisplayed(event) { if (this.contains(event)) { return false; } @@ -173,13 +170,13 @@ export default class ChatEventBar { /** * Sets the progress gradient of the specified event. - * @param {HTMLDivElement} eventWrapper + * @param {HTMLDivElement} eventButton * @param {number} percentageLeft * @private */ - setExpiryPercentage(eventWrapper, percentageLeft) { - eventWrapper.dataset.percentageLeft = percentageLeft; - eventWrapper.querySelector('.event-top').style.background = + setExpiryPercentage(eventButton, percentageLeft) { + eventButton.dataset.percentageLeft = percentageLeft; + eventButton.querySelector('.event-top').style.background = `linear-gradient(90deg, #282828, #282828 ${percentageLeft}%, #151515 ${percentageLeft}%, #151515)`; } } diff --git a/assets/chat/js/messages/PinnedMessage.js b/assets/chat/js/messages/PinnedMessage.js index a1fe72b5..707dcd31 100644 --- a/assets/chat/js/messages/PinnedMessage.js +++ b/assets/chat/js/messages/PinnedMessage.js @@ -58,7 +58,7 @@ export default class PinnedMessage extends ChatUserMessage { * @param {boolean} state */ set hidden(state) { - const frame = document.getElementById('chat-pinned-frame'); + const frame = document.getElementById('chat-pinned-message'); frame.classList.toggle('active', !state); } @@ -70,7 +70,7 @@ export default class PinnedMessage extends ChatUserMessage { dismissPin(this.uuid); this.hidden = true; - const frame = document.getElementById('chat-pinned-frame'); + const frame = document.getElementById('chat-pinned-message'); frame.replaceChildren(); return null; @@ -135,7 +135,7 @@ export default class PinnedMessage extends ChatUserMessage { this.ui.prepend(closePin); - const pinnedFrame = document.getElementById('chat-pinned-frame'); + const pinnedFrame = document.getElementById('chat-pinned-message'); pinnedFrame.classList.toggle('active', true); pinnedFrame.prepend(this.ui); pinnedFrame.prepend(showPin); diff --git a/assets/views/embed.html b/assets/views/embed.html index 1f9509c4..6f8e4e4f 100644 --- a/assets/views/embed.html +++ b/assets/views/embed.html @@ -26,9 +26,9 @@
-
-
-
+
+
+
diff --git a/assets/views/stream.html b/assets/views/stream.html index 2704ec37..57199c97 100644 --- a/assets/views/stream.html +++ b/assets/views/stream.html @@ -2,9 +2,9 @@
-
-
-
+
+
+
diff --git a/assets/views/templates.html b/assets/views/templates.html index 22d7d42d..ff3785b1 100644 --- a/assets/views/templates.html +++ b/assets/views/templates.html @@ -1,5 +1,5 @@