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-23 提交的版本,檢視 最新版本

// ==UserScript==
// @name         kick.com Fullscreen Chat Overlay
// @namespace    Violentmonkey Scripts
// @match        *://*.kick.com/*
// @grant        none
// @version      0.1.8
// @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 lastPositionPerRow = [];
const messageQueue = [];
const badgeCache = [];
const rowQueue = [];

let displayedMessages = {};

let observer, existingSocket;

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

const boundHandleChatMessageEvent = handleChatMessageEvent.bind(this);

function getMessageKey(key, value) {
	return key + "|" + value;
}

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 (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);
		}

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

	const queueLength = messageQueue.length;

	let wait = isVod ? (100 * (40 / queueLength)) : 2000 / queueLength;
	if (queueLength < 3) {
		wait = 1000;
	}

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

function selectRow(messageContainer, messageKey) {
	let selectedRow = 0;
	const messageWidth = messageContainer.clientWidth;

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

	newParentWidth = (parent.offsetWidth);
	if (parentWidth != newParentWidth) {
		messageQueue.length = 0;

		while (chatMessages.firstChild) {
			chatMessages.removeChild(chatMessages.firstChild);
		}

		displayedMessages = {};
		lastPositionPerRow.length = 0;
	}

	parentWidth = newParentWidth;

	if (lastPositionPerRow.length > 0) {
		for (let i = 0; i < lastPositionPerRow.length; i++) {
			const messageHeight = messageContainer.clientHeight;
			const topPosition = i * (messageHeight + 5) + 2;

			const lastMessage = lastPositionPerRow[i];
			if (topPosition <= parent.offsetHeight && lastMessage !== undefined) {

				if (rowQueue[i] === null || rowQueue[i] === undefined) {

					const timeNeeded = calculateTimeNeeded(messageWidth, lastMessage);

					if (timeNeeded === 0) {
						lastPositionPerRow[i] = messageContainer;

						startAnimation(topPosition, messageContainer, messageKey);
						return;
					}

					rowQueue[i] = messageContainer;

					messageContainer.style.display = 'none';

					setTimeout(() => {
						messageContainer.style.display = '';
						lastPositionPerRow[i] = messageContainer;
						rowQueue[i] = null;
						startAnimation(topPosition, messageContainer, messageKey);
					}, timeNeeded);

					return;
				}
			}
			else {
				try {
					chatMessages.removeChild(messageContainer);
					delete displayedMessages[messageKey];
				} finally {
					return;
				}
			}

			selectedRow = i + 1;
		}
	}

	lastPositionPerRow[selectedRow] = messageContainer;

	const topPosition = 2;
	startAnimation(topPosition, messageContainer, messageKey);
}

function calculateTimeNeeded(messageWidth, lastMessage) {
	const remainingSpace = parentWidth + messageWidth - (lastMessage.offsetLeft + lastMessage.clientWidth + 5);

	if (remainingSpace >= messageWidth) {
		return 0;
	}

	const scrollTime = Math.ceil((messageWidth - remainingSpace) / (parentWidth * 2) * 20000);
	return scrollTime;
}

function appendMessage(messageKey, messageContent) {
	if (displayedMessages[messageKey]) {
		return;
	}

	displayedMessages[messageKey] = true;

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

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

	selectRow(messageContainer, messageKey);
}

function startAnimation(topPosition, messageContainer, messageKey) {
	messageContainer.style.top = topPosition + 'px';
	messageContainer.style.animation = "slide-in 20s linear";
	messageContainer.style.marginRight = `-${messageContainer.clientWidth}px`;

	messageContainer.addEventListener("animationend", function () {
		chatMessages.removeChild(messageContainer);
		delete displayedMessages[messageKey];
	});
}

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(sender.id, reduced);

	const messageContentContainer = document.createElement("div");
	messageContentContainer.classList.add("chat-message-content");

	const badgeSpan = document.createElement("span");
	badgeSpan.classList.add("chat-overlay-badge");

	const badgeElements = getBadges(data);
	badgeElements.forEach(badgeElement => {
		badgeSpan.appendChild(badgeElement);
	});

	const usernameSpan = document.createElement("span");
	usernameSpan.style.color = color;
	usernameSpan.classList.add("chat-overlay-username");
	usernameSpan.style.verticalAlign = "middle";
	usernameSpan.textContent = username;

	const boldSpan = document.createElement("span");
	boldSpan.classList.add("font-bold", "text-white");
	boldSpan.style.verticalAlign = "middle";
	boldSpan.textContent = ": ";

	const contentSpan = document.createElement("span");
	contentSpan.style.color = "#ffffff";
	contentSpan.style.verticalAlign = "middle";
	contentSpan.classList.add("chat-overlay-content");

	const emoteRegex = /\[emote:(\d+):(\w+)\]/g;
	let lastIndex = 0;
	let match;

	while ((match = emoteRegex.exec(reduced)) !== null) {
		const textBeforeEmote = reduced.slice(lastIndex, match.index);
		contentSpan.appendChild(document.createTextNode(textBeforeEmote));

		const img = document.createElement("img");
		const [, id, name] = match;
		img.src = `https://files.kick.com/emotes/${id}/fullsize`;
		img.alt = name;
		img.classList.add("emote-image", "my-auto");
		contentSpan.appendChild(img);

		lastIndex = emoteRegex.lastIndex;
	}

	const textAfterLastEmote = reduced.slice(lastIndex);
	contentSpan.appendChild(document.createTextNode(textAfterLastEmote));

	messageContentContainer.append(badgeSpan, usernameSpan, boldSpan, contentSpan);
	appendMessage(messageKey, messageContentContainer);
}

function createUserBanMessage(data) {
	const bannedUser = data.user.username;
	const messageKey = getMessageKey('-ban-', bannedUser);

	const banMessageContent = document.createElement("div");
	banMessageContent.classList.add("chat-message-content");

	const banMessageSpan = document.createElement("span");
	banMessageSpan.style.color = "#FF0000";
	banMessageSpan.textContent = `${bannedUser} \uD83D\uDEAB banned by \uD83D\uDEAB ${data.banned_by.username}`;

	banMessageContent.appendChild(banMessageSpan);

	appendMessage(messageKey, banMessageContent);
}

function createSubMessage(data) {
	const username = data.username;
	const months = data.months;
	const messageKey = getMessageKey('-sub-', username);

	const subscriptionMessageContent = document.createElement("div");
	subscriptionMessageContent.classList.add("chat-message-content");

	const subscriptionMessageSpan = document.createElement("span");
	subscriptionMessageSpan.style.color = "#00FF00";
	subscriptionMessageSpan.textContent = `\uD83C\uDF89 ${username} subscribed for ${months} month(s)`;

	subscriptionMessageContent.appendChild(subscriptionMessageSpan);

	appendMessage(messageKey, subscriptionMessageContent);
}

function createHostMessage(data) {
	const hostUsername = data.host_username;
	const viewersCount = data.number_viewers;
	const messageKey = getMessageKey('-host-', hostUsername);

	const hostMessageContent = document.createElement("div");
	hostMessageContent.classList.add("chat-message-content");

	const hostMessageSpan = document.createElement("span");
	hostMessageSpan.style.color = "#00FF00";
	hostMessageSpan.textContent = `\uD83C\uDF89 ${hostUsername} hosted with ${viewersCount} viewers`;

	hostMessageContent.appendChild(hostMessageSpan);

	appendMessage(messageKey, hostMessageContent);
}

function createGiftedMessage(data) {
	const gifterUsername = data.gifter_username;
	const giftedUsernames = data.gifted_usernames;
	const messageKey = getMessageKey('-gift-', gifterUsername + giftedUsernames[0]);

	const giftedContent = document.createElement("div");
	giftedContent.classList.add("chat-message-content");

	const giftedSpan = document.createElement("span");
	giftedSpan.style.color = "#00FF00";
	giftedSpan.textContent = `\uD83C\uDF89 ${giftedUsernames.length} Subscriptions Gifted by ${gifterUsername}`;

	giftedContent.appendChild(giftedSpan);

	appendMessage(messageKey, giftedContent);
}

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 = document.createElement("div");
		messageContent.classList.add("chat-message-content");

		const followersMessageSpan = document.createElement("span");
		followersMessageSpan.textContent = `\uD83C\uDF89 ${followersDiff} new follower(s)`;

		messageContent.appendChild(followersMessageSpan);

		appendMessage(messageKey, messageContent);
	}

	lastFollowersCount = followersCount;
}

function reduceRepeatedSentences(input) {
	const regexSentence = /(\b.+?\b)\1+/g;
	const sentence = input.replace(regexSentence, '$1');
	const regexChar = /(.)(\1{10,})/g;
	return sentence.replace(regexChar, '$1$1$1$1$1$1$1$1$1$1');
}

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

	let firstChatIdentity = document.querySelector(`.chat-entry-username[data-chat-entry-user-id="${data.sender.id}"]`);

	if (firstChatIdentity !== null) {
		let identity = firstChatIdentity.closest('.chat-message-identity');
		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) {
				badgeElements.push(new DOMParser().parseFromString(cachedBadge.html, 'text/html').body.firstChild);
				return;
			}

			const imgElement = baseBadge.querySelector(`img`);
			if (imgElement) {
				const imgUrl = imgElement.src;
				const newImg = document.createElement('img');
				newImg.src = imgUrl;
				newImg.classList.add('badge-overlay');
				badgeCache.push({
					type: badgeText,
					html: newImg.outerHTML
				});
				badgeElements.push(newImg);
				return;
			}

			const svgElement = baseBadge.querySelector(`svg`);
			if (svgElement) {
				svgElement.classList.add('badge-overlay');

				badgeCache.push({
					type: badgeText,
					html: svgElement.outerHTML
				});

				badgeElements.push(svgElement);
				return;
			}

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

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

	let badgeArray = [];
	let badgeCount = 0;

	if (badges.length === 0) return badgeArray;

	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) {
			badgeArray.push(new DOMParser().parseFromString(cachedBadge.html, 'text/html').body.firstChild)
			badgeCount++;
			return;
		}
	});

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

	return badgeArray;
}

function initializeChat(self) {
	const chatMessagesElement = document.getElementById("chat-messages");
	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);
}

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;
	badgeCache.length = 0;

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

function handleChatMessageEvent(data) {
	if (isVod) return;
	if (document.getElementById("chat-messages") !== null && chatEnabled) {
		messageQueue.push(data);
		processMessageQueue();
		return;
	}

	initializeChat(false);
}

function createChat() {
	const chatMessagesElement = document.getElementById("chat-messages");
	if (chatMessagesElement !== null) return;

	const chatOverlay = document.createElement("div");
	chatOverlay.id = "chat-overlay";

	const chatMessagesContainer = document.createElement("div");
	chatMessagesContainer.id = "chat-messages";

	chatOverlay.appendChild(chatMessagesContainer);

	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: -1px; /* 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: 0.9rem;
				max-height: 0.9rem;
				margin-right: 4px;
			}

			.svg-toggle {
				fill: rgb(83, 252, 24) !important;
			}

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

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

	document.head.appendChild(chatOverlayStyles);

	createToggle();

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

function createToggle() {
	chatMessages = document.getElementById("chat-messages");

	const toggleButton = document.createElement('button');
	toggleButton.className = 'vjs-control vjs-button';

	const spanIconPlaceholder = document.createElement('span');
	spanIconPlaceholder.className = 'vjs-icon-placeholder';
	spanIconPlaceholder.setAttribute('aria-hidden', 'true');

	const svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
	svgElement.setAttribute('width', '65%');
	svgElement.setAttribute('viewBox', '0 0 16 16');
	svgElement.setAttribute('fill', 'none');
	svgElement.classList.add('mx-auto');
	svgElement.id = 'toggle-icon';

	const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
	pathElement.setAttribute('d', 'M12.8191 7.99813C12.8191 7.64949 12.7816 7.30834 12.7104 6.97844L13.8913 6.29616L12.3918 3.69822L11.2071 4.38051C10.7048 3.9269 10.105 3.57076 9.44517 3.35708V2H6.44611V3.36082C5.78632 3.57451 5.19025 3.9269 4.68416 4.38426L3.49953 3.70197L2 6.29616L3.18088 6.97844C3.10965 7.30834 3.07217 7.64949 3.07217 7.99813C3.07217 8.34677 3.10965 8.68791 3.18088 9.01781L2 9.70009L3.49953 12.298L4.68416 11.6157C5.1865 12.0694 5.78632 12.4255 6.44611 12.6392V14H9.44517V12.6392C10.105 12.4255 10.701 12.0731 11.2071 11.6157L12.3918 12.298L13.8913 9.70009L12.7104 9.01781C12.7816 8.68791 12.8191 8.34677 12.8191 7.99813ZM9.82006 9.87254H6.07123V6.12371H9.82006V9.87254Z');
	pathElement.setAttribute('fill', 'currentColor');
	pathElement.classList.add('svg-toggle');

	const spanControlText = document.createElement('span');
	spanControlText.className = 'vjs-control-text';
	spanControlText.setAttribute('aria-live', 'polite');
	spanControlText.textContent = 'Toggle Chat';

	svgElement.append(pathElement);
	toggleButton.append(spanIconPlaceholder, svgElement, spanControlText);

	const existingButton = document.querySelector('.vjs-fullscreen-control');
	existingButton.parentNode.insertBefore(toggleButton, existingButton.nextSibling);

	chatEnabled = true;

	toggleButton.addEventListener('click', function () {
		chatEnabled = !chatEnabled;
		messageQueue.length = 0;
		while (chatMessages.firstChild) {
			chatMessages.removeChild(chatMessages.firstChild);
		}

		displayedMessages = {};
		lastPositionPerRow.length = 0;

		if (chatEnabled) {
			pathElement.classList.add('svg-toggle');
		} else {
			pathElement.classList.remove('svg-toggle');
		}
	});
}

function interceptChatRequests() {
	let open = window.XMLHttpRequest.prototype.open;
	window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
		open.apply(this, arguments);
		if (url.includes("/api/v2/channels/") && url.includes("/messages")) {
			this.addEventListener("load", function () {
				let self = this;
				const response = JSON.parse(self.responseText);
				if (isVod && response.data && response.data.messages && document.getElementById("chat-messages") !== null && chatEnabled) {
					response.data.messages.forEach(function (message) {
						messageQueue.push(message);
						processMessageQueue();
					});
				} else {
					setTimeout(function () {
						initializeChat(false);
					}, 1000);
				}

			}, false);
		}
	};
}

initializeChat(false);
};

QingJ © 2025

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