Kick.com Fullscreen Chat Overlay

Enhances the Kick.com viewing experience by providing a fullscreen chat overlay. Messages will flow from right to left, allowing for a seamless chat experience while watching content.

目前為 2023-10-19 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Kick.com Fullscreen Chat Overlay
// @namespace    Violentmonkey Scripts
// @match        *://*.kick.com/*
// @grant        none
// @version      0.1.7
// @author       spaghetto.be
// @description  Enhances the Kick.com viewing experience by providing a fullscreen chat overlay. Messages will flow from right to left, allowing for a seamless chat experience while watching content.
// @icon         https://s2.googleusercontent.com/s2/favicons?domain=kick.com&sz=32
// @license      MIT
// ==/UserScript==

window.onload = function() {
	const displayedMessages = {}; // Keeps track of displayed messages to avoid duplicates
	const lastPositionPerRow = []; // Keeps track of the last position of messages in each row
	const messageQueue = [];

	let observer, existingSocket;

	let loading = true,
		isVod = true,
		isProcessing = false,
		lastFollowersCount = null;

	const chatMessagesElement = document.getElementById("chat-messages");

	// Bind handleChatMessageEvent to the current context
	const boundHandleChatMessageEvent = handleChatMessageEvent.bind(this);

	/**
	 * Generates a unique key based on sender ID and content.
	 * @param {string} key - The base key.
	 * @param {string} value - The value to append to the key.
	 * @returns {string} The generated message key.
	 */
	function getMessageKey(key, value) {
		return key + "|" + value;
	}

	/**
	 * Processes the message queue.
	 */
	function processMessageQueue() {

		if (isProcessing || messageQueue.length === 0) {
			return;
		}

		isProcessing = true;

		const data = messageQueue.shift();
		const eventType = data.event ?? "";

		try {
			if (eventType === "App\\Events\\ChatMessageEvent") {
				createMessage(data.data);
			} else if (data.type === "message") {
				createMessage(data);
			} else if (data.type === "reply") {
				// if (!isVod) console.log('reply'); Should be added
			} else if (eventType === "App\\Events\\UserBannedEvent") {
				createUserBanMessage(data.data);
			} else if (eventType === "App\\Events\\GiftedSubscriptionsEvent") {
				createGiftedMessage(data.data);
			} else if (eventType === "App\\Events\\FollowersUpdated") {
				createFollowersMessage(data.data);
			} else if (eventType === "App\\Events\\StreamHostEvent") {
				createHostMessage(data.data);
			} else if (eventType === "App\\Events\\SubscriptionEvent") {
				createSubMessage(data.data);
			} else if (eventType.includes("pusher_internal")) {
				// Do nothing for internal events
			} else {
				// console.warn(`Unknown event type: ${eventType}`);
				// console.log(data); Other events like MessagePinned
			}

		} catch (error) {
			console.error("Error parsing message data: ", error);
		}

		// Dynamic
		let wait = isVod ? (100 * (40 / messageQueue.length)) : (100 * (40 / messageQueue.length));
		if (messageQueue.length < 3) {
			wait = 2000;
		}

		setTimeout(function() {
			isProcessing = false;
			processMessageQueue();
		}, wait);
	}

	/**
	 * Selects the appropriate row for a new message.
	 * @param {Element} messageContainer - The message container element.
	 * @param {number} messageWidth - The width of the message.
	 * @returns {number} The selected row index.
	 */
	function selectRow(messageContainer, messageWidth) {
		let selectedRow = 0;
		const parent = document.getElementById("chat-messages");
		if (lastPositionPerRow.length > 0) {
			for (let i = 0; i < lastPositionPerRow.length; i++) {
				const lastMessage = lastPositionPerRow[i];
				if ((parent.offsetWidth * 2) - (lastMessage.offsetLeft + lastMessage.clientWidth + 10) >= messageWidth) {
					selectedRow = i;
					break;
				}
				selectedRow = i + 1;
			}
		}

		lastPositionPerRow[selectedRow] = messageContainer;
		return selectedRow;

	}

	/**
	 * Appends a message to the chat overlay.
	 * @param {string} messageKey - The unique message key.
	 * @param {string} messageContent - The content of the message.
	 */
	function appendMessage(messageKey, messageContent) {
		if (displayedMessages[messageKey]) {
			return; // Ignore duplicate message
		}

		displayedMessages[messageKey] = true;

		const messageContainer = document.createElement("div");
		messageContainer.classList.add("chat-message", "chat-entry");

		messageContainer.innerHTML = messageContent;
		chatMessages.appendChild(messageContainer);

		const messageWidth = messageContainer.clientWidth;
		const messageHeight = messageContainer.clientHeight;

		const parent = document.getElementById("chat-messages");
		if (parent === null) return;

		let selectedRow = selectRow(messageContainer, messageWidth);
		const topPosition = selectedRow * (messageHeight + 5);


		if (topPosition <= parent.offsetHeight) {
			messageContainer.style.top = topPosition + 'px';
			messageContainer.style.animation = "slide 20s linear";

			messageContainer.addEventListener("animationend", function() {
				chatMessages.removeChild(messageContainer);
				delete displayedMessages[messageKey];
			});
		} else {
			// Handle case where message doesn't fit in the available height
			// You may add custom logic or display an error message here
			chatMessages.removeChild(messageContainer);
			delete displayedMessages[messageKey];
		}
	}

	/**
	 * Appends a ban message to the chat overlay.
	 * @param {object} data - Data containing information about the ban.
	 */
	function createUserBanMessage(data) {
		const bannedUser = data.user.username;
		const messageKey = getMessageKey('-ban-', bannedUser);

		const banMessageContent = `
                <div class="chat-message-content">
                    <span style="color:#FF0000">${bannedUser} 🚫 banned by 🚫 ${data.banned_by.username}</span>
                </div>
            `;

		appendMessage(messageKey, banMessageContent);
	}

	/**
	 * Appends a subscription message to the chat overlay.
	 * @param {object} data - Data containing information about the subscription.
	 */
	function createSubMessage(data) {
		const username = data.username;
		const months = data.months;
		const messageKey = getMessageKey('-sub-', username);

		const subscriptionMessageContent = `
                <div class="chat-message-content">
                    <span style="color:#00FF00">🎉 ${username} subscribed for ${months} month(s)</span>
                </div>
            `;

		appendMessage(messageKey, subscriptionMessageContent);
	}

	/**
	 * Appends a host message to the chat overlay.
	 * @param {object} data - Data containing information about the host.
	 */
	function createHostMessage(data) {
		const hostUsername = data.host_username;
		const viewersCount = data.number_viewers;
		const messageKey = getMessageKey('-host-', hostUsername);

		const hostMessageContent = `
                <div class="chat-message-content">
                    <span style="color:#00FF00">🎉 ${hostUsername} hosted with ${viewersCount} viewers</span>
                </div>
            `;

		appendMessage(messageKey, hostMessageContent);
	}

	/**
	 * Appends a gifted message to the chat overlay.
	 * @param {object} data - Data containing information about the gifted subscription.
	 */
	function createGiftedMessage(data) {
		const gifterUsername = data.gifter_username;
		const giftedUsernames = data.gifted_usernames;
		const messageKey = getMessageKey('-gift-', gifterUsername + giftedUsernames[0]);

		const giftedContent = `
                <div class="chat-message-content">
                    <span style="color:#00FF00">🎉 ${giftedUsernames.length} Subscriptions Gifted by ${gifterUsername}</span>
                </div>
            `;

		appendMessage(messageKey, giftedContent);
	}

	/**
	 * Appends a followers message to the chat overlay.
	 * @param {object} data - Data containing information about the followers count.
	 */
	function createFollowersMessage(data) {
		const followersCount = data.followersCount;
		const messageKey = getMessageKey('-followers-', followersCount);

		if (lastFollowersCount !== null) {
			const followersDiff = followersCount - lastFollowersCount;
			if (followersDiff === 0) {
				return;
			}
			const messageKey = getMessageKey('-followers-', followersCount);
			const messageContent = `
                    <div class="chat-message-content">
                        🎉 Followers gained: ${followersDiff}
                    </div>
                `;
			appendMessage(messageKey, messageContent);
		}

		lastFollowersCount = followersCount;
	}

	/**
	 * Reduces repeated sentences in a message.
	 * @param {string} input - The input message content.
	 * @returns {string} The message content with repeated sentences reduced.
	 */
	function reduceRepeatedSentences(input) {
		const regexSentence = /(\b.+?\b)\1+/g; // Regex for repeated sentences
		const sentence = input.replace(regexSentence, '$1');
		const regexChar = /(.)(\1{10,})/g; // Regex for repeated characters
		return sentence.replace(regexChar, '$1$1$1$1$1$1$1$1$1$1');
	}

	const badgeCache = [];

	function checkForBadges(data) {
		const sender = data.sender;
		const badges = sender.identity.badges || [];

		let firstChatIdentity = document.querySelector(`.chat-entry-username[data-chat-entry-user="${sender.username.toLowerCase()}"]`);
		if (firstChatIdentity === null) {
     			firstChatIdentity = document.querySelector(`.chat-entry-username[data-chat-entry-user="${sender.username.toLowerCase().replace(/_/g, '-')}"]`);
      		if (firstChatIdentity === null) {
      		setTimeout(function() {
				    console.info('checkForBadges: ' + sender.username);
				    messageQueue.push(data);
				    processMessageQueue();
			}, 2000);
			return;
		  }
    }
		const identity = firstChatIdentity.closest('.chat-message-identity');

		// Check for badges
		identity.querySelectorAll('.badge-tooltip').forEach(function(baseBadge, index) {
			let badge = badges[index];
			if (badge === undefined) return;
			let badgeText = badge.text;

			if (badge.count) {
				badgeText = `${badge.type}-${badge.count}`;
			}
			const cachedBadge = badgeCache.find(badgeCache => badgeCache.type === badgeText);
			if (cachedBadge) {
				return; // Badge found in cache, return HTML
			}

			const imgElement = baseBadge.querySelector(`img`);
			if (imgElement) {
				const imgUrl = imgElement.src;
				const newImg = document.createElement('img');
				newImg.src = imgUrl;
				newImg.classList.add('badge-overlay'); // Add your specific class here
				badgeCache.push({
					type: badgeText,
					html: newImg.outerHTML
				});
				messageQueue.push(data);
				return;
			}

			const svgElement = baseBadge.querySelector(`svg`);
			if (svgElement) {
				// Add the badge-overlay class to the parent div
				const divElement = document.createElement('div');
				divElement.classList.add('chat-overlay-badge');
				svgElement.classList.add('badge-overlay');

				divElement.appendChild(svgElement);

				badgeCache.push({
					type: badgeText,
					html: divElement.outerHTML
				});
				messageQueue.push(data);

				return;
			}

			console.warn('badge not found: ' + badgeText);
		});
	}

	/**
	 * Check if the provided HTML contains an image with alt like badge.
	 * If found, extract img tag and add it before username with other badges.
	 * Also, update the badgeArray with the badge text and content.
	 * @param {string} badgeText - The text associated with the badge.
	 * @param {Array} badgeArray - Array to store badges and content.
	 * @returns {string} The badge element in HTML.
	 */
	function getBadges(data) {
		const sender = data.sender;
		const badges = sender.identity.badges || [];

		let badgeString = '';
		let badgeCount = 0;

		// Check for badges
		badges.forEach(badge => {
			let badgeText = badge.text;
			if (badge.count) {
				badgeText = `${badge.type}-${badge.count}`;
			}
			const cachedBadge = badgeCache.find(badgeCache => badgeCache.type === badgeText);
			if (cachedBadge) {
				badgeString += ` ${cachedBadge.html}`;
				badgeCount++;
				return; // Badge found in cache, return HTML
			}
		});

		if (badgeCount !== badges.length) {
			setTimeout(checkForBadges(data), 1000);
			return;
		}

		return badgeString; // Badge not found, return text in HTML
	}

	/**
	 * Creates a message and handles emotes.
	 * @param {object} data - Data containing information about the message.
	 */
	function createMessage(data) {
		const sender = data.sender;
		const username = sender.username;
		const color = sender.identity.color;
		const content = data.content;
		const reduced = reduceRepeatedSentences(content);
		const messageKey = getMessageKey(data.sender.id, reduced);

		const replacedContent = reduced.replace(
			/\[emote:(\d+):(\w+)\]/g,
			(match, id, name) => {
				return `<img src="https://files.kick.com/emotes/${id}/fullsize" alt="${name}" class="emote-image my-auto" />`;
			}
		);

		const badges = getBadges(data);

		const messageContent = `
  <div class="chat-message-content">
    <span class="chat-overlay-badge">${badges ? badges + ' ' : ''}</span>
    <span style="color:${color}; vertical-align: middle;" class="chat-overlay-username">${username}</span>
    <span class="font-bold text-white" style="vertical-align: middle;">: </span>
    <span style="color:#ffffff; vertical-align: middle;" class="chat-overlay-content">${replacedContent}</span>
  </div>
`;

		appendMessage(messageKey, messageContent);
	}

	/**
	 * Initializes the chat overlay.
	 * @param {boolean} self - Indicates if the chat overlay is being initialized on page load.
	 */
	function initializeChat(self) {
		if (chatMessagesElement !== null && (loading || !self)) return;

		loading = true;
		resetConnection();

		if (document.querySelector("video") !== null) {
			createChat();
			existingSocket.connection.bind("message", boundHandleChatMessageEvent);
			return;
		}

		observer = new MutationObserver(function(mutations) {
			mutations.forEach(function(mutation) {
				if (mutation.addedNodes) {
					mutation.addedNodes.forEach(function(node) {
						if (node.nodeName.toLowerCase() === "video") {
							observer.disconnect();
							createChat();
						}
					});
				}
			});
		});

		observer.observe(document.body, {
			childList: true,
			subtree: true
		});

		setTimeout(function() {
			observer.disconnect();
			existingSocket.connection.bind("message", boundHandleChatMessageEvent);
			interceptChatRequests();
		}, 2000);
	}

	/**
	 * Resets the chat connection and clears message data.
	 */
	function resetConnection() {
		existingSocket = window.Echo.connector.pusher;
		existingSocket.connection.unbind("message", boundHandleChatMessageEvent);

		for (const key in displayedMessages) {
			if (displayedMessages.hasOwnProperty(key)) {
				delete displayedMessages[key];
			}
		}

		lastPositionPerRow.length = 0;
		messageQueue.length = 0;

		isVod = window.location.href.includes('/video/');
	}

	/**
	 * Handles incoming chat messages over pusher handler.
	 * @param {object} data - Data containing information about the incoming message.
	 */
	function handleChatMessageEvent(data) {
		if (isVod) return;
		if (document.getElementById("chat-messages") !== null) {
			messageQueue.push(data);
			processMessageQueue();
			return;
		}
		initializeChat(false);
	}

	/**
	 * Creates the chat overlay and appends it to the page.
	 */
	function createChat() {
		if (chatMessagesElement !== null) return;

		const chatOverlay = document.createElement("div");
		chatOverlay.id = "chat-overlay";
		chatOverlay.innerHTML = `
                <div id="chat-messages"></div>
            `;

		const videoPlayer = document.querySelector("video");
		videoPlayer.parentNode.insertBefore(chatOverlay, videoPlayer);

		const chatOverlayStyles = document.createElement("style");
		chatOverlayStyles.textContent = `
                #chat-overlay {
                    position: absolute;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    pointer-events: none;
                    overflow: hidden;
                    z-index: 9999;
                }

                #chat-messages {
                    display: flex;
                    flex-direction: column-reverse;
                    align-items: flex-end;
                    height: 100%;
                    overflow-y: auto;
                }

                .chat-overlay-username {
                    font-weight: 700;
                }

                .chat-message .font-bold {
                    margin-left: -4px; /* Adjust the value as needed */
                }

                .chat-overlay-username,
                .chat-overlay-content {
                    vertical-align: middle;
                }

                .chat-message {
                    position: absolute;
                    background-color: rgba(34, 34, 34, 0.6);
                    border-radius: 10px;
                    white-space: nowrap;
                    max-width: calc(100% - 20px);
                    overflow: hidden;
                    text-overflow: ellipsis;
                    max-height: 1rem;
                    display: flex;
                    align-items: center;
                }

                .chat-overlay-badge {
                    display: inline !important;
                }

                .badge-overlay {
                    display: inline !important;
                    max-width: 1rem;
                    max-height: 1rem;
                }

                .emote-image {
                    display: inline !important;
                    margin-right: 3px;
                    max-width: 1.5rem;
                    max-height: 1.5rem;
                }

                @keyframes slide {
                    0% {
                        right: -100%;
                    }

                    100% {
                        right: 100%;
                    }
                }
            `;

		document.head.appendChild(chatOverlayStyles);
		chatMessages = document.getElementById("chat-messages");

		loading = false;
		console.info('Chat Overlay Created: ' + window.location.href);
	}


	/**
	 * Intercepts chat requests to process incoming messages.
	 */
	function interceptChatRequests() {
		let open = window.XMLHttpRequest.prototype.open;
		window.XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
			open.apply(this, arguments); // Open the XMLHttpRequest immediately

			if (url.includes("/api/v2/channels/") && url.includes("/messages")) {
				this.addEventListener("load", function() {
					let self = this; // Store a reference to this

					setTimeout(function() {
						const response = JSON.parse(self.responseText);
						if (isVod && response.data && response.data.messages && document.getElementById("chat-messages") !== null) {
							response.data.messages.forEach(function(message) {
								messageQueue.push(message);
								processMessageQueue();
							});
						} else {
							setTimeout(function() {
								initializeChat(false);
							}, 1000);
						}
					}, 0);
				}, false);
			}
		};
	}

	// Call initializeChat to initialize chat on page load
	initializeChat(false);
};

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址