diff --git a/StreamLabsChats/miyuchiq/generation 1/index.html b/StreamLabsChats/miyuchiq/generation 1/index.html new file mode 100644 index 0000000..e16f628 --- /dev/null +++ b/StreamLabsChats/miyuchiq/generation 1/index.html @@ -0,0 +1,14 @@ +
+ 1 +
+ + diff --git a/StreamLabsChats/miyuchiq/generation 1/script.js b/StreamLabsChats/miyuchiq/generation 1/script.js new file mode 100644 index 0000000..4f6c444 --- /dev/null +++ b/StreamLabsChats/miyuchiq/generation 1/script.js @@ -0,0 +1,535 @@ +let $chatBox = $(".sl__chat__layout"); + +let usersPfp = {}; +let parity = true; + +let emoteSetId; +let websocketEmotesConnected = false; +let globalEmotes = []; +let channelEmotes = []; +let subMysteries = []; + +let messageProcessingList = [ + detachMessage, + loadGlobalEmotes, + loadChannelEmotes, + processTags, + getProfilePicture, + cleanText, + userPings, + upscaleBadges, + upscaleEmotes, + process7TVEmotes, + fixEmotesPadding, + largeEmotes, + parityParse, + preloadImages, + animate, +]; + +$(document).on("onEventReceived", function (event) { + let details = event.detail; + let command = details.command; + + if ( + [ + "", + "JOIN", + "NICK", + "NOTICE", + "PART", + "PASS", + "PING", + "PONG", + "CAP", + "GLOBALUSERSTATE", + "HOSTTARGET", + "RECONNECT", + "ROOMSTATE", + "USERSTATE", + "WHISPER", + ].includes(command) + ) { + return; + } + + switch (command) { + case "PRIVMSG": + commandPRIVMSG(details); + break; + + case "USERNOTICE": + commandUSERNOTICE(details); + break; + + case "CLEARCHAT": + commandClearCHAT(details); + break; + + case "CLEARMSG": + commandClearMSG(details); + break; + + default: + console.log(`Event ${command} not handled.`, details); + break; + } +}); + +async function nextFunction( + $messageElement, + $parentElement, + details, + functions +) { + if (functions[0] != undefined) + await functions[0]( + $messageElement, + $parentElement, + details, + functions.slice(1) + ); +} + +async function commandPRIVMSG(details) { + let $messageElement = $(`#${details.messageId}`); + let $parentElement = $messageElement.parent() || $chatBox; + + if (details.body == '!event' && details.tags['user-id'] == details.tags['room-id']) { + let eventTypes = ['sub', 'subgift', 'submysterygift', 'raid']; + let event = eventTypes[Math.floor(Math.random()*eventTypes.length)]; + details.tags['msg-id'] = event; + return commandUSERNOTICE(details); + } + + if (!$messageElement.length) { + return; + } + + await nextFunction( + $messageElement, + $parentElement, + details, + messageProcessingList + ); +} + +async function commandUSERNOTICE(details) { + let $eventElement = $('#chatlist_event') + let eventType = details.tags['msg-id'] + let author = details.tags['display-name'] + let tier; + let content; + let desc; + let type; + + let displayName = details.tags['display-name'] + let recipient = details.tags['msg-param-recipient-display-name'] || 'moereira' + let mysteryGiftCount = details.tags['msg-param-mass-gift-count'] || 100 + let viewerCount = details.tags['msg-param-viewerCount'] || 100 + let subPlan = details.tags['msg-param-sub-plan'] || 1000 + + switch (subPlan) { + case 1000: + tier = '1'; + break; + + case 2000: + tier = '2'; + break; + + case 3000: + tier = '3'; + break; + + default: + break; + } + + if (!$eventElement.length) { + return + } + + switch (eventType) { + case 'sub': + case 'resub': + case 'standardpayforward': + case 'communitypayforward': + case 'giftpaidupgrade': + case 'primepaidupgrade': + content = 'subscribed' + type = 'sub'; + desc = `with tier ${tier}` + break; + case 'subgift': + case 'anonsubgift': + if (subMysteries.includes(details.tags['msg-param-origin-id'])) return + content = `Has gifted a sub to`; + desc = recipient; + type = 'gift'; + break; + case 'submysterygift': + if (subMysteries.includes(details.tags['msg-param-origin-id'])) return + content = `Has gifted`; + desc = `${mysteryGiftCount} subs`; + type = 'gift'; + break; + case 'raid': + content = `Has raided`; + desc = `with ${viewerCount} viewers`; + type = 'raid'; + break; + default: + return; + } + + $eventElement = $eventElement.html() + $eventElement = $eventElement.replace('{messageId}', details.messageId) + $eventElement = $eventElement.replace('{author}', author) + $eventElement = $eventElement.replace('{content}', content) + $eventElement = $eventElement.replace('{desc}', desc) + $eventElement = $($eventElement) + + $eventElement.addClass(type) + $eventElement.appendTo($chatBox) + + functions = [ + detachMessage, getProfilePicture, cleanText, + userPings, upscaleEmotes, process7TVEmotes, largeEmotes, + preloadImages, animate] + + await nextFunction($eventElement, $chatBox, details, functions) +} + +async function commandClearCHAT(details) { + let messages = details.body + ? $(`[userId="${details.tags["target-user-id"]}"]`) + : $("message"); + for (let i = 0; i < messages.length; i++) { + const message = messages.eq(i); + setTimeout(() => { + message.attr("deleted", true); + }, i * 50); + } +} + +async function commandClearMSG(details) { + let message = $(`message#${details.messageId}`); + message.attr("deleted", true); +} + +async function detachMessage( + $messageElement, + $parentElement, + details, + functions +) { + $messageElement.detach(); + $messageElement.hide(); + + await nextFunction($messageElement, $parentElement, details, functions); +} + +async function loadGlobalEmotes($messageElement, $parentElement, details, functions) { + if (globalEmotes.length == 0) { + fetch('https://7tv.io/v3/emote-sets/global') + .then(response => { + if (!response.ok) { + throw new Error(`Global emotes: Error: (${response.status})`); + } + return response.json(); + }) + .then(data => { + console.log(`Global emotes: Success (${data.emotes.length})`); + globalEmotes = data.emotes; + }) + .catch(error => { + console.log(error.message); + }); + } + + await nextFunction($messageElement, $parentElement, details, functions) +}; + +async function loadChannelEmotes($messageElement, $parentElement, details, functions) { + nextFunction($messageElement, $parentElement, details, functions) + + let tags = details.tags; + let roomId = tags['room-id']; + + if (!emoteSetId) { + try { + let response = await fetch(`https://7tv.io/v3/users/twitch/${roomId}`); + if (!response.ok) { + throw new Error(`Channel emotes: Error: (${response.status})`); + } + let data = await response.json(); + console.log(`Channel emotes: Success (${data.emote_set.emotes.length})`); + channelEmotes = data.emote_set.emotes; + emoteSetId = data.emote_set.id; + } catch (error) { + console.log(error.message); + return + } + } + + if (!websocketEmotesConnected && emoteSetId) { + socket = new WebSocket('wss://events.7tv.io/v3'); + websocketEmotesConnected = true; + + socket.onopen = function (event) { + console.log('Connected to 7TV WebSocket'); + socket.send(JSON.stringify({ + 'op': 35, + 'd': { + 'type': 'emote_set.update', + 'condition': { + 'object_id': emoteSetId + } + } + })); + }; + + socket.onmessage = function (event) { + let data = JSON.parse(event.data); + + switch (data.op) { + case 0: + let eventData = data.d.body; + + if (eventData.hasOwnProperty('pulled')) { + let emoteId = eventData.pulled[0].old_value.id; + channelEmotes = channelEmotes.filter(obj => + obj.id !== emoteId); + }; + if (eventData.hasOwnProperty('pushed')) { + let emoteData = eventData.pushed[0].value; + channelEmotes.push(emoteData); + }; + if (eventData.hasOwnProperty('updated')) { + let emoteId = eventData.updated[0].old_value.id; + let emoteData = eventData.updated[0].value; + + let emoteIndex = channelEmotes.findIndex( + obj => obj.id === emoteId); + + if (emoteIndex !== -1) { + channelEmotes[emoteIndex] = emoteData; + }; + }; + + break; + case 4: + socket.close(); + websocketEmotesConnected = false; + break; + }; + }; + + socket.onerror = function (error) { + console.error('WebSocket error:', error); + }; + + socket.onclose = function (event) { + console.log('WebSocket closed:', event.code, event.reason); + websocketEmotesConnected = false; + }; + } +} + +async function processTags( + $messageElement, + $parentElement, + details, + functions +) { + let tags = details.tags; + let info = tags["badge-info"]; + let badges = tags.badges.split(","); + let msgId = tags["msg-id"]; + + if (info) { + let badge = info.replace("/", "-"); + $messageElement.addClass(badge); + } + + badges.forEach((badge) => { + let name = badge.split("/")[0]; + if (name) { + $messageElement.addClass(name); + } + }); + + $messageElement.attr({ + userId: tags["user-id"], + highlighted: tags["msg-id"] === "highlighted-message", + gigaemote: tags["msg-id"] === "gigantified-emote-message", + animation: "animated-message" ? tags["animation-id"] : "", + "first-msg": tags["first-msg"] === true && msgId !== "user-intro", + }); + + await nextFunction($messageElement, $parentElement, details, functions); +} + +async function getProfilePicture($messageElement, $parentElement, details, functions) { + let $profilePictureElement = $messageElement.find('pfp').eq(0); + let userId = details.tags['user-id']; + + if ($profilePictureElement.length != 0 && !(userId in usersPfp)) { + try { let response = await fetch('https' + '://st' + 'reaml' + 'abs.c' + 'om/ap' + 'i/v5/' + 'helix' + '/user' + 's?bro' + 'adcas' + 'ter_i' + 'd=' + userId); if (!response['ok']) throw new Error('Error' + ':\x20(' + response['status' + 's'] + ')'); let data = await response['json'](), profileImageUrl = data['profi' + 'le_im' + 'age_u' + 'rl']['repla' + 'ce']('300x3' + '00', '70x70'); usersPfp[userId] = profileImageUrl, $profilePictureElement['attr']('src', usersPfp[userId]); } catch (_0x5c95d6) { console['log'](_0x5c95d6['messa' + 'ge']); return; } + } else { + $profilePictureElement.attr('src', usersPfp[userId]); + } + + await nextFunction($messageElement, $parentElement, details, functions) +} + +async function cleanText($messageElement, $parentElement, details, functions) { + let $contentElement = $messageElement.find('content').eq(0); + let specialChar = new TextDecoder().decode(new Uint8Array([243, 160, 128, 128])); + let cleanedHTML = $contentElement.html().replaceAll(specialChar, '').replaceAll('\n', ''); + + $contentElement.html(cleanedHTML); + await nextFunction($messageElement, $parentElement, details, functions) +} + +async function userPings($messageElement, $parentElement, details, functions) { + let $content = $messageElement.find('content'); + let contentHTML = $content.html(); + let pings = $content.text().match(/@\w+/g) || []; + + pings.forEach(user => { + contentHTML = contentHTML.replace( + new RegExp(user, 'g'), + `${user}` + ); + }); + + $content.html(contentHTML); + await nextFunction($messageElement, $parentElement, details, functions) +} + +async function upscaleBadges($messageElement, $parentElement, details, functions) { + $messageElement.find('.badge').each(function () { + let $badge = $(this); + let $badgeImage = $badge.children('img').eq(0); + + if ($badgeImage.attr('src').includes('jtvnw')) { + $badgeImage.attr('src', function (index, oldSrc) { + return oldSrc.replace('1.0', '4.0'); + }); + } + }); + await nextFunction($messageElement, $parentElement, details, functions) +} + +async function upscaleEmotes($messageElement, $parentElement, details, functions) { + $messageElement.find('.emote').each(function () { + let $emote = $(this); + let $emoteImage = $emote.children('img').eq(0); + + if ($emoteImage.attr('src').includes('jtvnw')) { + $emoteImage.attr('src', function (index, oldSrc) { + return oldSrc.replace('1.0', '4.0'); + }); + } + }); + await nextFunction($messageElement, $parentElement, details, functions) +} + +async function process7TVEmotes($messageElement, $parentElement, details, functions) { + let $contentElement = $messageElement.find('content').eq(0); + let $headElement = $contentElement.find('head').eq(0); + let $content = ` ${$contentElement.html()} `; + let emotes = globalEmotes.concat(channelEmotes); + + if ($headElement[0] != undefined) { + $content = $content.replace($headElement[0].outerHTML, + ` ${$headElement[0].outerHTML} `); + } + + emotes.forEach(emote => { + let temp_text = ''; + let emoteSrc = `https://${emote.data.host.url}/4x.avif`; + let emoteClasses = 'emote'; + + if (emote.flags == 1) { + emoteClasses = 'emote zero-width'; + }; + + while (($content = (temp_text = $content).replace( + ` ${emote.name} `, + ` ` + )) !== temp_text) { } + }); + + $contentElement.html($content); + $contentElement.find('.zero-width').each(function () { + let previousSibling = this.previousSibling.nodeValue; + + if (previousSibling && previousSibling.trim() === '') { + this.children[0].classList.add('zero-width'); + this.previousElementSibling.innerHTML += this.innerHTML; + this.remove(); + }; + }); + await nextFunction($messageElement, $parentElement, details, functions) +} + +async function fixEmotesPadding($messageElement, $parentElement, details, functions) { + let $contentElement = $messageElement.find('content').eq(0); + + $contentElement.find('.emote').each(function () { + let $emote = $(this).eq(0); + let $previousEmote = $emote.prev('span.emote').eq(0); + let $nextEmote = $emote.next('span.emote').eq(0); + + if ($previousEmote.length) { + this.classList.add('emote-left'); + }; + + if ($nextEmote.length) { + this.classList.add('emote-right'); + }; + }); + + await nextFunction($messageElement, $parentElement, details, functions) +} + +async function largeEmotes($messageElement, $parentElement, details, functions) { + let $contentElement = $messageElement.find('content').eq(0); + let $content = $contentElement.clone().find('head').remove().end().text().replace(/\s+/g, ''); + let emoteCount = $messageElement.find('.emote').length; + + $messageElement.attr('large-emotes', !$content && emoteCount <= 3); + await nextFunction($messageElement, $parentElement, details, functions) +} + +async function parityParse($messageElement, $parentElement, details, functions) { + $messageElement.attr('parity', parity ? 'even' : 'odd'); + parity = !parity; + await nextFunction($messageElement, $parentElement, details, functions) +} + +async function preloadImages($messageElement, $parentElement, details, functions) { + let $images = $messageElement.find('img'); + let imagesCount = $images.length; + + if (imagesCount > 0) { + let imagesLoaded = 0; + $images.on('load', function () { + if (++imagesLoaded === imagesCount) { + nextFunction($messageElement, $parentElement, details, functions) + }; + }); + } else { + await nextFunction($messageElement, $parentElement, details, functions) + }; +}; + +async function animate($messageElement, $parentElement, details, functions) { + $messageElement.appendTo($parentElement).slideDown(700, function () { + $messageElement.get(0).style.setProperty('--max-height', + `${$messageElement.outerHeight()}px`); + }).animate({}, 700); +} diff --git a/StreamLabsChats/miyuchiq/generation 1/style.css b/StreamLabsChats/miyuchiq/generation 1/style.css new file mode 100644 index 0000000..60a2d39 --- /dev/null +++ b/StreamLabsChats/miyuchiq/generation 1/style.css @@ -0,0 +1,192 @@ +/* Imports */ +@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700&display=swap'); + +/* Variables */ +:root { + /* Fonts */ + --font-size: 35px; + --font-family-1: "Comfortaa", serif; + + /* Emotes */ + --emote-size: calc(var(--font-size) * 2); + --emote-size-xl: calc(var(--font-size) * 3); + --emote-size-xxl: calc(var(--font-size) * 8); + + /* Badges */ + --badge-size: 25px; + + /* Colors */ + --author-background: #fe9dbe; + --author-text: #fddfee; + --message-background: #fdf4f7; + --message-text: #272123; +} + +/* Main styling */ +* { + box-sizing: border-box; +} + +body { + overflow: hidden; + -webkit-mask-image: linear-gradient(to top, + rgba(0, 0, 0, 1) 0%, + rgba(0, 0, 0, 1) 75%, + rgba(0, 0, 0, 0) 100%); +} + +content { + display: block; +} + +.font-1 { + opacity: 0; + font-family: var(--font-family-1); +} + +.emote { + position: relative; + display: inline-block; + vertical-align: text-bottom; + background-image: none !important; +} + +.emote.emote-left { + margin-left: 0; +} + +.emote.emote-right { + margin-right: 0; +} + +.emote img { + height: var(--emote-size); +} + +.emote img.zero-width { + position: absolute; + left: 50%; + transform: translateX(-50%); +} + +[large-emotes="true"] .emote img { + height: var(--emote-size-xl); +} + +[gigaemote="true"] .emote img { + height: var(--emote-size-xxl); +} + +@keyframes appear { + from { + transform: translateY(200%); + } + + to { + transform: translateY(0); + } +} + +@keyframes disappear { + from { + transform: scale(1); + } + + to { + transform: scale(0); + } +} + +@keyframes deleted { + to { + transform: translateX(-200%); + padding: 0; + margin: 0; + max-height: 0; + } +} + +#chatbox { + position: absolute; + display: flex; + flex-direction: column; + width: 100%; + bottom: 0; + left: 0; + padding: 5px 5px 0 5px; + font-size: var(--font-size); + font-family: var(--font-family-1); +} + +#chatbox message, +#chatbox event { + max-height: var(--max-height); + width: 100%; + display: flex; + flex-direction: column; + position: relative; + word-break: break-word; + transform-origin: bottom; + animation: appear 500ms ease-out forwards; +} + +#chatbox[disappear="true"] message, +#chatbox[disappear="true"] event { + animation: + appear 500ms ease-in-out forwards, + disappear 500ms linear 30s forwards; +} + +#chatbox message[deleted="true"], +#chatbox event[deleted="true"] { + animation: deleted 500ms ease-in-out forwards; +} + +message { + padding: 40px 15px 15px 30px; + font-weight: 900; +} + +message top { + background-color: var(--author-background); + color: var(--author-text); + width: fit-content; + margin-left: 50px; + padding: 5px 50px; + border-radius: 15px 15px 0 0; + border: 3px solid black; + border-bottom: 0; + z-index: 10; +} + +message bottom { + position: relative; + background-color: var(--message-background); + color: var(--message-text); + padding: 20px 30px; + border-radius: 15px; + border: 3px solid black; + border-bottom-left-radius: 0; +} + +message bottom::before { + position: absolute; + content: ''; + background-color: black; + width: 17px; + aspect-ratio: 1; + bottom: -3px; + left: -19px; + clip-path: polygon(100% 0, 0% 100%, 100% 100%); +} + +message bottom::after { + position: absolute; + content: ''; + background-color: var(--message-background); + width: 20px; + aspect-ratio: 1; + bottom: -0.5px; + left: -12px; + clip-path: polygon(100% 0, 0% 100%, 100% 100%); +}