Skip to content

Commit 530c4c9

Browse files
authored
Merge pull request #626 from Makz09/patch-1
Thank you; it should be live in a few minutes. > Create notimeoutmessages.html
2 parents 627b51e + 571346a commit 530c4c9

1 file changed

Lines changed: 385 additions & 0 deletions

File tree

themes/notimeoutmessages.html

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
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

Comments
 (0)