|
| 1 | +<!DOCTYPE html> |
| 2 | +<!-- |
| 3 | +🔴 Neon Red Chat Overlay |
| 4 | +A customized Social Stream Ninja (SSN) overlay for live streaming chat, styled with a neon red theme. |
| 5 | +This overlay displays messages, avatars, usernames, badges, and donations in real-time with a stacked chat style — no disappearing messages. |
| 6 | +
|
| 7 | +✨ Features |
| 8 | +1. Neon red gradient theme with glowing effects |
| 9 | +2. Supports avatars, badges, and source icons (YouTube, Twitch, Kick, etc.) |
| 10 | +3. Smooth animations for new messages |
| 11 | +4. Messages stack permanently (no auto-fade or removal) |
| 12 | +5. Responsive and transparent overlay — perfect for OBS/XSplit |
| 13 | +
|
| 14 | +🛠 How It Works |
| 15 | +1. Built on Social Stream Ninja overlay logic |
| 16 | +2. Messages arrive via WebRTC or WebSocket from connected chat platforms |
| 17 | +3. CSS provides the neon red theme and styling |
| 18 | +4. JavaScript modified to disable fading/deletion (messages persist and scroll upward) |
| 19 | +--> |
| 20 | + |
| 21 | +<html lang="en"> |
| 22 | +<head> |
| 23 | + <meta charset="UTF-8"> |
| 24 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 25 | + <title>Chat Overlay</title> |
| 26 | + <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"> |
| 27 | + <script src="https://socialstream.ninja/libs/colours.js" type="text/javascript"></script> |
| 28 | + <!-- Links to Social Stream Ninja resources --> |
| 29 | + <link rel="canonical" href="https://socialstream.ninja/"> |
| 30 | + <meta name="generator" content="Social Stream Ninja - https://github.com/steveseguin/social_stream"> |
| 31 | +<style> |
| 32 | +@import url('https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap'); |
| 33 | + |
| 34 | +body, html { |
| 35 | + margin: 0; |
| 36 | + padding: 0; |
| 37 | + width: 100%; |
| 38 | + height: 100%; |
| 39 | + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; |
| 40 | + background-color: #0000; /* Transparent background for overlay */ |
| 41 | + overflow: hidden; /* Prevent body scrollbars */ |
| 42 | + box-sizing: border-box; |
| 43 | +} |
| 44 | + |
| 45 | +* { |
| 46 | + -webkit-font-smoothing: antialiased; |
| 47 | + -moz-osx-font-smoothing: grayscale; |
| 48 | + text-rendering: optimizeLegibility; |
| 49 | +} |
| 50 | + |
| 51 | +#chat-container { |
| 52 | + position: absolute; |
| 53 | + bottom: 0; |
| 54 | + left: 50%; |
| 55 | + transform: translateX(-50%); /* Center horizontally */ |
| 56 | + width: 100%; |
| 57 | + max-width: 350px; |
| 58 | + height: 100vh; /* Or your desired fixed height for the chat box */ |
| 59 | + overflow: hidden; /* Crucial: content exceeding this will be hidden */ |
| 60 | + padding: 0 25px; /* Horizontal padding only on the main container */ |
| 61 | + box-sizing: border-box; |
| 62 | +} |
| 63 | + |
| 64 | +#message-list-wrapper { |
| 65 | + width: 100%; |
| 66 | + transition: transform 0.5s ease-out; |
| 67 | + /* This element now starts at the top of chat-container by default. |
| 68 | + It grows downwards as messages are added. |
| 69 | + The transform will pull it upwards to keep newest messages visible. */ |
| 70 | +} |
| 71 | + |
| 72 | +.message { |
| 73 | + background: linear-gradient(145deg, #1a2a48, #162238); |
| 74 | + padding: 15px; |
| 75 | + margin-bottom: 15px; |
| 76 | + border-radius: 15px; |
| 77 | + border: 1px solid #2e3b55; |
| 78 | + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); |
| 79 | + color: #e4e6eb; |
| 80 | + opacity: 0; |
| 81 | + transform: translateY(30px); |
| 82 | + transition: opacity 0.6s ease-out, transform 0.6s ease-out, |
| 83 | + max-height 0.5s ease-in-out, margin-bottom 0.5s ease-in-out, padding-top 0.5s ease-in-out, padding-bottom 0.5s ease-in-out, border-width 0.5s ease-in-out; |
| 84 | + flex-shrink: 0; |
| 85 | + overflow: hidden; |
| 86 | + max-height: 500px; |
| 87 | + box-sizing: border-box; |
| 88 | + position: relative; |
| 89 | +} |
| 90 | + |
| 91 | +.message-with-edge { |
| 92 | + border-left: 4px solid; /* Width will be consistent but color will be set dynamically */ |
| 93 | + padding-left: 11px; /* Reduce left padding to account for the border */ |
| 94 | +} |
| 95 | + |
| 96 | +.message.visible { |
| 97 | + opacity: 1; |
| 98 | + transform: translateY(0); |
| 99 | +} |
| 100 | + |
| 101 | +.message.fading { |
| 102 | + opacity: 0; |
| 103 | + transition: opacity 1.5s ease-out; |
| 104 | +} |
| 105 | + |
| 106 | +.message.hidden { |
| 107 | + opacity: 0; |
| 108 | + transform: translateY(-30px); |
| 109 | + max-height: 0px !important; |
| 110 | + padding-top: 0px !important; |
| 111 | + padding-bottom: 0px !important; |
| 112 | + margin-bottom: 0px !important; /* This is important for accurate height calculation if we were animating out */ |
| 113 | + border-width: 0px !important; |
| 114 | +} |
| 115 | +.avatar-wrapper { |
| 116 | + width: 50px; |
| 117 | + height: 50px; |
| 118 | + border-radius: 50%; |
| 119 | + display: inline-block; |
| 120 | + margin-right: 10px; |
| 121 | + position: relative; |
| 122 | + overflow: hidden; |
| 123 | + background: linear-gradient(45deg, #4ab8f9, #4cd137); |
| 124 | + padding: 2px; |
| 125 | + vertical-align: middle; |
| 126 | + flex-shrink: 0; |
| 127 | +} |
| 128 | +#spacer { |
| 129 | + width: 100%; |
| 130 | + /* height is controlled by inline style & JS */ |
| 131 | + padding: 0; |
| 132 | + margin: 0; |
| 133 | + border: none; |
| 134 | + box-sizing: border-box; |
| 135 | + /* The spacer should not be visible or interactable */ |
| 136 | + background: transparent; |
| 137 | + opacity: 1; /* ensure it contributes to layout if needed, but it's transparent */ |
| 138 | + transform: none; |
| 139 | + transition: none; /* Height changes should be immediate, not animated */ |
| 140 | +} |
| 141 | +.name { color: white; font-size: 1.1em; font-weight: 700; max-width: 200px; overflow: hidden;} |
| 142 | +.text { font-size: 0.9em; line-height: 1.4; margin: 8px 0; word-wrap: break-word; } |
| 143 | +.text img { max-width:100px; max-height: 24px; vertical-align: bottom; } |
| 144 | +.donation { color: #4cd137; font-weight: 600; } |
| 145 | +.name-bg { background-color: #4a6fa5; padding: 4px 8px; border-radius: 5px; display: inline-block; vertical-align: middle; margin-right: 8px; } |
| 146 | +.membership-status { color: #ffd700; font-weight: 700; font-size: 0.85em; margin-bottom: 5px; } |
| 147 | +.large-image { max-width: 100%; height: auto; margin-top: 10px; border-radius: 10px; display: block; } |
| 148 | +.avatar { width: 100%; height: 100%; border-radius: 50%; background-size: cover; background-position: center; } |
| 149 | +.source-icon { width: 18px; height: 18px; vertical-align: middle; margin-left: 5px; } |
| 150 | +.badge { display: inline-block; height: 0.9em; margin-left: 4px; vertical-align: middle; } |
| 151 | + |
| 152 | +.message-content-wrapper { |
| 153 | + display: flex; |
| 154 | + align-items: flex-start; |
| 155 | + border-radius: 8px; |
| 156 | +} |
| 157 | + |
| 158 | +.message-text-meta { |
| 159 | + flex-grow: 1; |
| 160 | + min-width: 0; |
| 161 | +} |
| 162 | + |
| 163 | +</style> |
| 164 | +</head> |
| 165 | +<body> |
| 166 | +<div id="chat-container"> |
| 167 | + <div id="message-list-wrapper"> |
| 168 | + <div id="spacer" style="height: 2200px; width: 100%;"></div> |
| 169 | + </div> |
| 170 | +</div> |
| 171 | +<script> |
| 172 | + var urlParams = new URLSearchParams(window.location.search); |
| 173 | + var roomID = "iWWnKL28tQ"; |
| 174 | + if (urlParams.has("session")) { |
| 175 | + roomID = urlParams.get("session"); |
| 176 | + } |
| 177 | + var password = "false"; |
| 178 | + var featuredMode = false; |
| 179 | + |
| 180 | + const chatContainer = document.getElementById('chat-container'); |
| 181 | + const messageListWrapper = document.getElementById('message-list-wrapper'); |
| 182 | + // const MAX_MESSAGES = urlParams.has("limit") ? parseInt(urlParams.get("limit")) : 20; // Adjust as needed |
| 183 | + const topSpacer = document.getElementById('spacer'); |
| 184 | + const messageTimestamps = new Map(); |
| 185 | + const MAX_SPACER_HEIGHT_BEFORE_RESET = 18000000; |
| 186 | + |
| 187 | + function adjustMessageWrapperScroll() { |
| 188 | + requestAnimationFrame(() => { |
| 189 | + const wrapperScrollHeight = messageListWrapper.scrollHeight; |
| 190 | + const containerClientHeight = chatContainer.clientHeight; |
| 191 | + |
| 192 | + let translateY = 0; |
| 193 | + if (wrapperScrollHeight > containerClientHeight) { |
| 194 | + translateY = -(wrapperScrollHeight - containerClientHeight); |
| 195 | + } |
| 196 | + translateY = Math.min(0, translateY); |
| 197 | + |
| 198 | + messageListWrapper.style.transform = `translateY(${translateY}px)`; |
| 199 | + }); |
| 200 | + } |
| 201 | + |
| 202 | + // setInterval(() => { |
| 203 | + // const now = Date.now(); |
| 204 | + // const messages = Array.from(messageListWrapper.children).filter( |
| 205 | + // child => child.id !== 'spacer' && child.classList.contains('message') |
| 206 | + // ); |
| 207 | + |
| 208 | + // messages.forEach(message => { |
| 209 | + // const timestamp = messageTimestamps.get(message.id); |
| 210 | + // if (timestamp) { |
| 211 | + // const age = now - timestamp; |
| 212 | + |
| 213 | + // // After 30 seconds, start fading |
| 214 | + // if (age > 30000 && !message.classList.contains('fading')) { |
| 215 | + // message.classList.add('fading'); |
| 216 | + // } |
| 217 | + // } |
| 218 | + // }); |
| 219 | + // }, 1000); |
| 220 | + |
| 221 | + function addMessageToOverlay(data) { |
| 222 | + |
| 223 | + if (!data.chatname && !data.chatmessage && !data.hasDonation && !data.donation && !data.contentimg){ |
| 224 | + return; |
| 225 | + } |
| 226 | + |
| 227 | + const messageDiv = document.createElement('div'); |
| 228 | + messageDiv.classList.add('message'); |
| 229 | + |
| 230 | + // Generate unique ID and track timestamp for aging |
| 231 | + const messageId = data.mid || 'msg-' + Date.now(); |
| 232 | + messageDiv.id = messageId; |
| 233 | + messageTimestamps.set(messageId, Date.now()); |
| 234 | + |
| 235 | + var chatbadgesHtml = ""; |
| 236 | + if (data.chatbadges) { |
| 237 | + data.chatbadges.forEach(badge => { |
| 238 | + if (typeof badge === "object") { |
| 239 | + if (badge.type === "img" && badge.src) { |
| 240 | + chatbadgesHtml += `<img class='badge' src='${badge.src}' alt='badge'>`; |
| 241 | + } else if (badge.type === "svg" && badge.html) { |
| 242 | + chatbadgesHtml += `<span class='badge svg'>${badge.html}</span>`; |
| 243 | + } |
| 244 | + } else { |
| 245 | + chatbadgesHtml += `<img class='badge' src='${badge}' alt='badge'>`; |
| 246 | + } |
| 247 | + }); |
| 248 | + } |
| 249 | + |
| 250 | + let avatarBorderColor = "linear-gradient(45deg, #4ab8f9, #4cd137)"; // Default gradient |
| 251 | + if (data.nameColor) { |
| 252 | + avatarBorderColor = `linear-gradient(45deg, ${data.nameColor}, ${data.nameColor}88)`; |
| 253 | + } |
| 254 | + |
| 255 | + let leftEdgeColor = null; |
| 256 | + if (typeof getColorFromType === 'function' && data.type) { |
| 257 | + try { |
| 258 | + leftEdgeColor = getColorFromType(data.type); |
| 259 | + } catch (e) { |
| 260 | + console.error("Error getting color from type:", e); |
| 261 | + } |
| 262 | + } |
| 263 | + |
| 264 | + if (leftEdgeColor) { |
| 265 | + messageDiv.style.borderLeft = `4px solid ${leftEdgeColor}`; |
| 266 | + messageDiv.style.paddingLeft = '12px'; |
| 267 | + } |
| 268 | + |
| 269 | + const sourceIconHtml = data.type ? `<img src="https://socialstream.ninja/sources/images/${data.type}.png" alt="Source" class="source-icon">` : ''; |
| 270 | + const avatarHtml = data.chatimg ? |
| 271 | + `<div class="avatar-wrapper" style="background: ${avatarBorderColor}"><div class="avatar" style="background-image: url('${data.chatimg}');"></div></div>` : ''; |
| 272 | + const nameHtml = data.chatname ? `<div class="name-bg"><div class="name" ${data.nameColor ? `style="text-shadow: 0 0 3px ${data.nameColor}"` : ''}>${data.chatname}</div></div>` : ''; |
| 273 | + const membershipHtml = data.membership ? `<div class="membership-status">${data.membership}</div>` : ''; |
| 274 | + const messageTextHtml = `<div class="text" ${data.event ? 'style="font-style: italic;"' : ''}>${data.chatmessage ? data.chatmessage : ''}</div>`; |
| 275 | + const donationHtml = data.hasDonation ? `<div class="donation">${data.hasDonation}</div>` : ''; |
| 276 | + |
| 277 | + const contentImgHtml = data.contentimg ? |
| 278 | + `<div style="padding: 3px; border-radius: 12px; background: ${avatarBorderColor}; display: inline-block; margin-top: 10px;"> |
| 279 | + <img src="${data.contentimg}" alt="Content Image" class="large-image" style="margin-top: 0; border-radius: 8px;" onerror="this.parentNode.style.display='none';"> |
| 280 | + </div>` : ''; |
| 281 | + |
| 282 | + messageDiv.innerHTML = ` |
| 283 | + <div class="message-content-wrapper"> |
| 284 | + ${avatarHtml} |
| 285 | + <div class="message-text-meta"> |
| 286 | + <div> |
| 287 | + ${nameHtml} |
| 288 | + ${sourceIconHtml} |
| 289 | + ${chatbadgesHtml} |
| 290 | + </div> |
| 291 | + ${membershipHtml} |
| 292 | + ${messageTextHtml} |
| 293 | + ${donationHtml} |
| 294 | + </div> |
| 295 | + </div> |
| 296 | + ${contentImgHtml} |
| 297 | + `; |
| 298 | + |
| 299 | + messageListWrapper.appendChild(messageDiv); |
| 300 | + void messageDiv.offsetWidth; |
| 301 | + messageDiv.classList.add('visible'); |
| 302 | + |
| 303 | + const messages = Array.from(messageListWrapper.children).filter( |
| 304 | + child => child.id !== 'spacer' && child.classList.contains('message') |
| 305 | + ); |
| 306 | + |
| 307 | + // if (messages.length > MAX_MESSAGES) { |
| 308 | + // const oldestMessage = messages[0]; |
| 309 | + |
| 310 | + // if (oldestMessage) { |
| 311 | + // const messageStyle = getComputedStyle(oldestMessage); |
| 312 | + // const marginBottom = parseFloat(messageStyle.marginBottom) || 0; |
| 313 | + // const spaceOccupiedByOldestMessage = oldestMessage.offsetHeight + marginBottom; |
| 314 | + |
| 315 | + // const currentSpacerHeight = parseFloat(topSpacer.style.height) || 0; |
| 316 | + // let newPotentialSpacerHeight = currentSpacerHeight + spaceOccupiedByOldestMessage; |
| 317 | + |
| 318 | + // if (newPotentialSpacerHeight > MAX_SPACER_HEIGHT_BEFORE_RESET) { |
| 319 | + // console.log(`Spacer height (${newPotentialSpacerHeight}px) exceeded threshold (${MAX_SPACER_HEIGHT_BEFORE_RESET}px). Resetting to 0px.`); |
| 320 | + // newPotentialSpacerHeight = 0; // Reset the height |
| 321 | + // } |
| 322 | + |
| 323 | + // topSpacer.style.height = newPotentialSpacerHeight + 'px'; |
| 324 | + |
| 325 | + // messageTimestamps.delete(oldestMessage.id); |
| 326 | + // messageListWrapper.removeChild(oldestMessage); |
| 327 | + // } |
| 328 | + // } |
| 329 | + |
| 330 | + adjustMessageWrapperScroll(); |
| 331 | + } |
| 332 | + |
| 333 | + var iframe = document.createElement("iframe"); |
| 334 | + if (featuredMode){ |
| 335 | + iframe.src = `https://vdo.socialstream.ninja/?ln&password=${password}&salt=vdo.ninja&label=overlay&exclude=${roomID}&scene&novideo&noaudio&cleanoutput&room=${roomID}`; |
| 336 | + } else { |
| 337 | + iframe.src = "https://vdo.socialstream.ninja/?ln&salt=vdo.ninja&password="+password+"&push&label=dock&vd=0&ad=0&novideo&noaudio&autostart&cleanoutput&room="+roomID; |
| 338 | + } |
| 339 | + iframe.style.cssText = "width: 0px; height: 0px; position: fixed; left: -100px; top: -100px;"; |
| 340 | + document.body.appendChild(iframe); |
| 341 | + window.addEventListener("message", function (e) { |
| 342 | + if (e.source != iframe.contentWindow) return; |
| 343 | + if (e.data.dataReceived && e.data.dataReceived.overlayNinja) { |
| 344 | + addMessageToOverlay(e.data.dataReceived.overlayNinja); |
| 345 | + } |
| 346 | + }); |
| 347 | + |
| 348 | + var conCon = 1; |
| 349 | + var socketserver = false; |
| 350 | + var serverURL = urlParams.has("localserver") ? "ws://127.0.0.1:3000" : "wss://io.socialstream.ninja"; |
| 351 | + |
| 352 | + function setupSocket(){ |
| 353 | + socketserver.onclose = function (){ |
| 354 | + setTimeout(function(){ |
| 355 | + conCon+=1; |
| 356 | + socketserver = new WebSocket(serverURL); |
| 357 | + setupSocket(); |
| 358 | + },100*conCon); |
| 359 | + }; |
| 360 | + socketserver.onopen = function (){ |
| 361 | + conCon = 1; |
| 362 | + socketserver.send(JSON.stringify({"join":roomID, "out":3, "in":4})); |
| 363 | + }; |
| 364 | + socketserver.addEventListener('message', function (event) { |
| 365 | + if (event.data){ |
| 366 | + try { |
| 367 | + var data = JSON.parse(event.data); |
| 368 | + if (data) { |
| 369 | + addMessageToOverlay(data); |
| 370 | + } |
| 371 | + } catch (error) { |
| 372 | + console.error("Error processing WebSocket message:", error, "Data:", event.data); |
| 373 | + } |
| 374 | + } |
| 375 | + }); |
| 376 | + } |
| 377 | + |
| 378 | + if (urlParams.has("server") || urlParams.has("server2")){ |
| 379 | + serverURL = urlParams.get("server") || urlParams.get("server2") || serverURL; |
| 380 | + socketserver = new WebSocket(serverURL); |
| 381 | + setupSocket(); |
| 382 | + } |
| 383 | +</script> |
| 384 | +</body> |
| 385 | +</html> |
0 commit comments