From 38cd241122a12a1b08136c68602073ec0a25c6d8 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Sat, 8 Nov 2025 05:43:49 +1100 Subject: [PATCH 1/5] Added support for Twitch Watch Streaks --- horizontal-chat/script.js | 18 +++++++++ horizontal-chat/settings/settings.json | 8 ++++ multichat-overlay/script.js | 47 +++++++++++++++++++++++ multichat-overlay/settings/settings.json | 8 ++++ multistream-alerts/script.js | 31 +++++++++++++++ multistream-alerts/settings/settings.json | 17 ++++++++ 6 files changed, 129 insertions(+) diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index 43358f7..f5cd335 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -46,6 +46,7 @@ const showTwitchFollows = GetBooleanParam("showTwitchFollows", false); const showTwitchSubs = GetBooleanParam("showTwitchSubs", true); const showTwitchChannelPointRedemptions = GetBooleanParam("showTwitchChannelPointRedemptions", true); const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); +const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); const showTwitchSharedChat = GetBooleanParam("showTwitchSharedChat", true); const kickUsername = urlParams.get("kickUsername") || ""; @@ -181,6 +182,11 @@ client.on('Twitch.Raid', (response) => { TwitchRaid(response.data); }) +client.on('Twitch.WatchStreak', (response) => { + console.debug(response.data); + TwitchWatchStreak(response.data); +}) + client.on('Twitch.ChatMessageDeleted', (response) => { console.debug(response.data); TwitchChatMessageDeleted(response.data); @@ -789,6 +795,18 @@ async function TwitchRaid(data) { ShowAlert(message, 'twitch'); } +async function TwitchWatchStreak(data) { + if (!showTwitchWatchStreaks) + return; + + const displayName = data.displayName; + const watchStreak = data.watchStreak; + + let message = `${displayName} is currently on a ${watchStreak}-stream streak!`; + + ShowAlert(message, 'twitch'); +} + function TwitchChatMessageDeleted(data) { const messageList = document.getElementById("messageList"); diff --git a/horizontal-chat/settings/settings.json b/horizontal-chat/settings/settings.json index 0222140..b1eed16 100644 --- a/horizontal-chat/settings/settings.json +++ b/horizontal-chat/settings/settings.json @@ -175,6 +175,14 @@ "defaultValue": true, "group": "Which Twitch messages do you want to see?" }, + { + "id": "showTwitchWatchStreaks", + "label": "Watch Streaks", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch messages do you want to see?" + }, { "id": "showTwitchSharedChat", "label": "Shared Chat Messages", diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index abe2055..4448375 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -51,6 +51,7 @@ const showTwitchFollows = GetBooleanParam("showTwitchFollows", false); const showTwitchSubs = GetBooleanParam("showTwitchSubs", true); const showTwitchChannelPointRedemptions = GetBooleanParam("showTwitchChannelPointRedemptions", true); const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); +const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); const showTwitchSharedChat = GetIntParam("showTwitchSharedChat", 2); const kickUsername = urlParams.get("kickUsername") || ""; @@ -200,6 +201,11 @@ client.on('Twitch.Raid', (response) => { TwitchRaid(response.data); }) +client.on('Twitch.WatchStreak', (response) => { + console.debug(response.data); + TwitchWatchStreak(response.data); +}) + client.on('Twitch.ChatMessageDeleted', (response) => { console.debug(response.data); TwitchChatMessageDeleted(response.data); @@ -1160,6 +1166,47 @@ async function TwitchRaid(data) { AddMessageItem(instance, data.messageId); } +async function TwitchWatchStreak(data) { + if (!showTwitchWatchStreaks) + return; + + // Get a reference to the template + const template = document.getElementById('cardTemplate'); + + // Create a new instance of the template + const instance = template.content.cloneNode(true); + + // Get divs + const cardDiv = instance.querySelector("#card"); + const headerDiv = instance.querySelector("#header"); + const avatarDiv = instance.querySelector("#avatar"); + const iconDiv = instance.querySelector("#icon"); + const titleDiv = instance.querySelector("#title"); + const contentDiv = instance.querySelector("#content"); + + // Set the card background colors + cardDiv.classList.add('twitch'); + + if (showAvatar) { + // Render avatars + const username = data.userName; + const avatarURL = await GetAvatar(username, 'twitch'); + const avatar = new Image(); + avatar.src = avatarURL; + avatar.classList.add("avatar"); + avatarDiv.appendChild(avatar); + } + + const displayName = data.displayName; + const watchStreak = data.watchStreak; + const message = data.message; + + titleDiv.innerText = `${displayName} is currently on a ${watchStreak}-stream streak! `; + contentDiv.innerText = `${message}`; + + AddMessageItem(instance, data.messageId); +} + function TwitchChatMessageDeleted(data) { const messageList = document.getElementById("messageList"); diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json index dbf67a4..65ecd5a 100644 --- a/multichat-overlay/settings/settings.json +++ b/multichat-overlay/settings/settings.json @@ -282,6 +282,14 @@ "defaultValue": true, "group": "Which Twitch messages do you want to see?" }, + { + "id": "showTwitchWatchStreaks", + "label": "Watch Streaks", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch messages do you want to see?" + }, { "id": "showTwitchSharedChat", "label": "Shared Chat Messages", diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index 3abfb05..a2a6ec9 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -67,6 +67,8 @@ const showTwitchCheers = GetBooleanParam("showTwitchCheers", true); const twitchCheerAction = urlParams.get("twitchCheerAction") || ""; const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); const twitchRaidAction = urlParams.get("twitchRaidAction") || ""; +const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); +const twitchWatchStreaksAction = urlParams.get("twitchWatchStreaksAction") || ""; // Which Kick alerts do you want to see? const kickUsername = urlParams.get("kickUsername") || ""; @@ -211,6 +213,11 @@ client.on('Twitch.Raid', (response) => { TwitchRaid(response.data); }) +client.on('Twitch.WatchStreak', (response) => { + console.debug(response.data); + TwitchWatchStreak(response.data); +}) + client.on('YouTube.SuperChat', (response) => { console.debug(response.data); YouTubeSuperChat(response.data); @@ -721,6 +728,30 @@ async function TwitchRaid(data) { ); } +async function TwitchWatchStreak(data) { + if (!showTwitchWatchStreaks) + return; + + // Render avatars + const avatarURL = await GetAvatar(data.userName, 'twitch'); + + // Set the text + const username = data.displayName; + const watchStreak = data.watchStreak; + + UpdateAlertBox( + 'twitch', + avatarURL, + `${username}`, + `is currently on a ${watchStreak}-stream streak!`, + '', + username, + '', + twitchWatchStreaksAction, + data + ); +} + function YouTubeSuperChat(data) { if (!showYouTubeSuperChats) return; diff --git a/multistream-alerts/settings/settings.json b/multistream-alerts/settings/settings.json index 89768e0..b59bc74 100644 --- a/multistream-alerts/settings/settings.json +++ b/multistream-alerts/settings/settings.json @@ -312,6 +312,23 @@ "showIf": "showTwitchRaids", "group": "Which Twitch alerts do you want to see?" }, + { + "id": "showTwitchWatchStreaks", + "label": "Watch Streaks", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch alerts do you want to see?" + }, + { + "id": "twitchWatchStreaksAction", + "label": "", + "description": "Also run this Streamer.bot Action", + "type": "sb-actions", + "defaultValue": "", + "showIf": "showTwitchWatchStreaks", + "group": "Which Twitch alerts do you want to see?" + }, { "id": "showYouTubeSuperChats", "label": "Super Chats", From 9d891bc2c9d6a5021b822f388aa12dcd4792414f Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Mon, 17 Nov 2025 23:11:14 +1100 Subject: [PATCH 2/5] Fixed error with StreamElements tips would not show --- multistream-alerts/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index a2a6ec9..ab5d0d0 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -867,7 +867,7 @@ async function StreamElementsTip(data) { UpdateAlertBox( 'streamelements', - '' + '', `${donater}`, `donated ${currency}${formattedAmount}`, ``, From 080c9f6bc8cb86982020294e6c48a9749d27a0e0 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Mon, 17 Nov 2025 23:25:00 +1100 Subject: [PATCH 3/5] Updated StringToHex -> RandomHex --- .common/utils/helpers.js | 41 ++++++++++++++++++++++++++++--------- horizontal-chat/script.js | 2 +- multichat-overlay/script.js | 2 +- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/.common/utils/helpers.js b/.common/utils/helpers.js index 8dde251..298a2bf 100644 --- a/.common/utils/helpers.js +++ b/.common/utils/helpers.js @@ -222,21 +222,42 @@ function EscapeRegExp(string) { } // Given a string, return a random hex code. The same input always results in the same output -function StringToHex(str) { - // Simple hash function to convert string to a number +function RandomHex(str) { + // Simple hash to convert string to a number let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); - hash |= 0; // Convert to 32bit integer + hash |= 0; } - // Convert hash to a hex color - let color = '#'; - for (let i = 0; i < 3; i++) { - // Extract each byte and convert to 2-digit hex - const value = (hash >> (i * 8)) & 0xFF; - color += value.toString(16).padStart(2, '0'); + // Use the hash to generate a hue (0–360) + const hue = Math.abs(hash) % 360; + + // Force super saturation + readable brightness + const saturation = 100; // maximum saturation + const lightness = 55; // tweak 50–60 depending on your background + + // Convert HSL → RGB → hex + function hslToHex(h, s, l) { + s /= 100; + l /= 100; + + const k = n => (n + h / 30) % 12; + const a = s * Math.min(l, 1 - l); + const f = n => + l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); + + const r = Math.round(255 * f(0)); + const g = Math.round(255 * f(8)); + const b = Math.round(255 * f(4)); + + return ( + "#" + + r.toString(16).padStart(2, "0") + + g.toString(16).padStart(2, "0") + + b.toString(16).padStart(2, "0") + ); } - return color; + return hslToHex(hue, saturation, lightness); } \ No newline at end of file diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index f5cd335..95f7c98 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -896,7 +896,7 @@ function YouTubeMessage(data) { if (showUsername) { usernameDiv.innerText = data.user.name; if (randomYouTubeColors) - usernameDiv.style.color = StringToHex(data.user.name); + usernameDiv.style.color = RandomHex(data.user.name); else usernameDiv.style.color = youtubeColor; // YouTube users do not have colors, so just set it to red } diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 4448375..e72246a 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1312,7 +1312,7 @@ async function YouTubeMessage(data) { if (showUsername) { usernameDiv.innerText = data.user.name; if (randomYouTubeColors) - usernameDiv.style.color = StringToHex(data.user.name); + usernameDiv.style.color = RandomHex(data.user.name); else usernameDiv.style.color = youtubeColor; // YouTube users do not have colors, so just set it to red } From 8c8460d6d3cd01f8b12ce4729d39a2274cb19d78 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Mon, 17 Nov 2025 23:32:45 +1100 Subject: [PATCH 4/5] Added emote support to Watch Streaks --- horizontal-chat/script.js | 2 +- multichat-overlay/script.js | 60 ++++++++++++++++++++++++++++++++++-- multistream-alerts/script.js | 2 +- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index 95f7c98..709a42b 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -802,7 +802,7 @@ async function TwitchWatchStreak(data) { const displayName = data.displayName; const watchStreak = data.watchStreak; - let message = `${displayName} is currently on a ${watchStreak}-stream streak!`; + let message = `${displayName} is currently on a ${watchStreak} stream streak!`; ShowAlert(message, 'twitch'); } diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index e72246a..05c2c69 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1201,9 +1201,29 @@ async function TwitchWatchStreak(data) { const watchStreak = data.watchStreak; const message = data.message; - titleDiv.innerText = `${displayName} is currently on a ${watchStreak}-stream streak! `; + titleDiv.innerText = `${displayName} is currently on a ${watchStreak} stream streak! `; contentDiv.innerText = `${message}`; + // Render emotes + for (i in data.emotes) { + const emoteElement = ``; + const emoteName = EscapeRegExp(data.emotes[i].name); + + let regexPattern = emoteName; + + // Check if the emote name consists only of word characters (alphanumeric and underscore) + if (/^\w+$/.test(emoteName)) { + regexPattern = `\\b${emoteName}\\b`; + } + else { + // For non-word emotes, ensure they are surrounded by non-word characters or boundaries + regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`; + } + + const regex = new RegExp(regexPattern, 'g'); + contentDiv.innerHTML = contentDiv.innerHTML.replace(regex, emoteElement); + } + AddMessageItem(instance, data.messageId); } @@ -3250,4 +3270,40 @@ async function GetYouTubeVideoData(videoId) { console.error('Error fetching YouTube video data:', error); return null; } -} \ No newline at end of file +} + +let data = { + "msgId": "617ba873-519d-4e28-9d72-e25c13af8022", + "subscriber": false, + "message": "exeldr1Good", + "systemMessage": "Exeldro watched 5 consecutive streams and sparked a watch streak!", + "emotes": [ + { + "id": "emotesv2_ae32a0ba44b347479250e76759d34b8a", + "type": "Twitch", + "name": "exeldr1Good", + "startIndex": 0, + "endIndex": 10, + "imageUrl": "https://static-cdn.jtvnw.net/emoticons/v2/emotesv2_ae32a0ba44b347479250e76759d34b8a/default/dark/2.0" + } + ], + "badges": [ + { + "name": "moderator", + "version": "1", + "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/3", + "info": "" + } + ], + "monthsSubscribed": 0, + "watchStreak": 5, + "copoReward": 450, + "string": "77656c7f-33ef-4d8d-981c-5be7fec88030", + "userId": "416052011", + "userName": "exeldro", + "displayName": "Exeldro", + "role": 3, + "isTest": false + } + + TwitchWatchStreak(data); \ No newline at end of file diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index ab5d0d0..e628056 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -743,7 +743,7 @@ async function TwitchWatchStreak(data) { 'twitch', avatarURL, `${username}`, - `is currently on a ${watchStreak}-stream streak!`, + `is currently on a ${watchStreak} stream streak!`, '', username, '', From 52b5b021770e6fdca436fd54c885f2de460bad0c Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Mon, 17 Nov 2025 23:38:47 +1100 Subject: [PATCH 5/5] Oops lol --- multichat-overlay/script.js | 38 +------------------------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 05c2c69..dc1a20d 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -3270,40 +3270,4 @@ async function GetYouTubeVideoData(videoId) { console.error('Error fetching YouTube video data:', error); return null; } -} - -let data = { - "msgId": "617ba873-519d-4e28-9d72-e25c13af8022", - "subscriber": false, - "message": "exeldr1Good", - "systemMessage": "Exeldro watched 5 consecutive streams and sparked a watch streak!", - "emotes": [ - { - "id": "emotesv2_ae32a0ba44b347479250e76759d34b8a", - "type": "Twitch", - "name": "exeldr1Good", - "startIndex": 0, - "endIndex": 10, - "imageUrl": "https://static-cdn.jtvnw.net/emoticons/v2/emotesv2_ae32a0ba44b347479250e76759d34b8a/default/dark/2.0" - } - ], - "badges": [ - { - "name": "moderator", - "version": "1", - "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/3", - "info": "" - } - ], - "monthsSubscribed": 0, - "watchStreak": 5, - "copoReward": 450, - "string": "77656c7f-33ef-4d8d-981c-5be7fec88030", - "userId": "416052011", - "userName": "exeldro", - "displayName": "Exeldro", - "role": 3, - "isTest": false - } - - TwitchWatchStreak(data); \ No newline at end of file +} \ No newline at end of file