# Building Custom Overlays for Chat All In One
## 1\. Introduction
Chat All In One (SSN) is a versatile application for consolidating and managing live social media streams. While it offers several built-in overlay pages (like `dock.html`, `featured.html`, `events.html`, etc.), users and developers can create their own custom HTML/CSS/JavaScript-based overlays to tailor the visual experience and functionality to specific needs.
This guide explains how to build such custom pages, focusing on connecting to the SSN backend, receiving and processing messages, and displaying them, all *without* modifying the core `background.js` application.
## 2\. Prerequisites
- Basic understanding of HTML, CSS, and JavaScript.
- Familiarity with JSON data structures.
- A running instance of the Chat All In One extension or standalone application.
## 3\. Core Concepts Recap
Before diving into custom page creation, let's revisit some core SSN concepts from the "Understanding Chat All In One for AI Integration" document:
- **`streamID` (Session ID):** Your unique session identifier. This is crucial and will be needed to connect your custom page. It's typically found in the SSN extension's settings.
- **`password` (Optional):** If your SSN session is password-protected, you'll need this.
- **Message Structure:** Incoming data will be JSON objects. Refer to the "Message Structure" section in the AI integration document for a detailed list of common fields (e.g., `id`, `type`, `chatname`, `chatmessage`, `hasDonation`, `event`).
## 4\. Connection Methods for Custom Overlays
Custom overlay pages primarily connect to the Chat All In One backend to receive messages. There are two main ways:
### 4.1. VDO.Ninja Iframe (Recommended for Overlays)
This is the most common and straightforward method for overlay pages. Your custom HTML page will embed a VDO.Ninja iframe. This iframe acts as a bridge, receiving data from the SSN `background.js` (via VDO.Ninja's WebRTC or WebSocket infrastructure) and then passing it to your parent HTML page (your custom overlay) using `window.postMessage()`.
**How it works:**
1. The SSN `background.js` sends messages to a VDO.Ninja room associated with your `streamID`.
2. Your custom overlay page includes an iframe whose `src` is a VDO.Ninja URL pointing to the *same* `streamID`. This iframe is configured typically as a "view-only" or specific-label client.
3. When the iframe receives data from SSN, it uses `parent.postMessage()` to send the data to your custom HTML page.
4. Your custom page listens for these messages using `window.addEventListener('message', callbackFunction)`.
**Example Iframe Setup:**
```html
```
**Key Iframe URL Parameters for VDO.Ninja:**
- `&room=STREAM_ID`: Specifies the main VDO.Ninja room to connect to.
- `&view=STREAM_ID`: Makes this client a viewer in the specified room, receiving data sent to that room.
- `&label=YOUR_LABEL`: Assigns a unique label to this iframe instance. This is crucial if you want `background.js` or `dock.html` to send targeted messages specifically to this overlay.
- `&password=PASSWORD`: If your SSN session is password protected.
- `&novideo`, `&noaudio`: Ensures no accidental camera/mic activation.
- `&cleanoutput`: Simplifies the VDO.Ninja interface within the iframe.
- `&ln`: (Light Ninja) a more performant version of VDO.Ninja, for viewing.
See `dock.html`, `featured.html`, `events.html` etc. for more examples of iframe setups. They often use `label=dock`, `label=overlay`, `label=actions` respectively.
### 4.2. WebSocket API (Advanced)
For more direct control or server-side integrations, you can connect to the SSN WebSocket server (`wss://io.socialstream.ninja`). This bypasses the need for an iframe but requires handling the WebSocket connection and message parsing directly in your JavaScript.
**Connection:**
```javascript
const urlParams = new URLSearchParams(window.location.search);
const roomID = urlParams.get("session") || "test";
// Choose appropriate IN_CHANNEL and OUT_CHANNEL based on your needs.
// For a simple overlay listening to general messages, IN_CHANNEL 3 or 4 might be suitable.
const inChannel = urlParams.get("in_channel") || "4"; // Channel to listen on
const outChannel = urlParams.get("out_channel") || "3"; // Channel to send to (if needed)
const socketServerURL = urlParams.get("server") || "wss://io.socialstream.ninja";
const socket = new WebSocket(socketServerURL);
socket.onopen = function() {
console.log("WebSocket Connected!");
const joinMessage = {
join: roomID,
in: parseInt(inChannel), // Channel(s) this client wants to receive messages from
out: parseInt(outChannel) // Default channel this client will send messages to
};
socket.send(JSON.stringify(joinMessage));
};
socket.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
// Messages from SSN are often wrapped.
if (data.overlayNinja) {
processIncomingSSNMessage(data.overlayNinja);
} else if (data.action && data.value) { // Simple command format
// Less common for overlays to receive this directly unless targeted
console.log("Received command:", data);
} else {
// Potentially direct message data if not wrapped
processIncomingSSNMessage(data);
}
} catch (e) {
console.error("Error parsing WebSocket message:", e);
}
};
socket.onerror = function(error) {
console.error("WebSocket Error:", error);
};
socket.onclose = function() {
console.log("WebSocket Disconnected. Attempting to reconnect...");
// Implement reconnection logic if needed
};
function processIncomingSSNMessage(data) {
console.log("Received SSN Data via WebSocket:", data);
// Your custom logic
}
```
This method is more common for tools that interact with SSN rather than purely visual overlays, but it's an option. The `dock.html` uses a similar WebSocket connection for its primary communication.
## 5\. Receiving and Processing Messages
Once connected, your custom page will receive message objects.
### Message Structure (Recap)
A typical message object might look like this (fields vary based on source and event):
```json
{
"id": 1678886400000,
"type": "twitch",
"chatname": "StreamFan123",
"chatmessage": "Great stream! 🎉",
"chatimg": "url_to_avatar.png",
"timestamp": 1678886400000,
"hasDonation": null, // Or "$5.00"
"membership": null, // Or "Tier 1 Subscriber"
"event": null, // Or "follow"
"userid": "123456789",
"nameColor": "#FF00FF",
"chatbadges": ["url_to_badge.png"],
"contentimg": null, // Or "url_to_image_in_chat.gif"
"karma": 0.85,
"bot": false,
"mod": false,
"host": false,
"vip": true,
"tid": 101 // Browser tab ID
}
```
Refer to `about.md` for more details on these fields.
> **Note:** Reserve the `event` field for true system notifications (follows, raids, /me actions, etc.). Regular chat messages should leave `event` unset/false so they are never mistaken for events.
### Client-Side Filtering and Logic
Your JavaScript code will be responsible for deciding what to do with each incoming message.
```javascript
function processIncomingSSNMessage(data) {
// --- Basic Filtering Examples ---
// 1. Filter by message type (source platform)
if (data.type === 'youtube') {
// console.log("This is a YouTube message:", data.chatname, data.chatmessage);
// displayYouTubeMessage(data);
} else if (data.type === 'twitch') {
// console.log("This is a Twitch message:", data.chatname, data.chatmessage);
// displayTwitchMessage(data);
}
// 2. Filter by event type
if (data.event === 'follow') {
// console.log("New Follower:", data.chatname, "on", data.type);
// displayFollowAlert(data);
} else if (data.event === 'subscriber') {
// console.log("New Subscriber:", data.chatname, "on", data.type);
// displaySubscriberAlert(data);
}
// 3. Filter for donations
if (data.hasDonation) {
// console.log("Donation Received:", data.chatname, "donated", data.hasDonation, "on", data.type);
// displayDonation(data);
}
// 4. Filter for messages from VIPs
if (data.vip) {
// console.log("VIP Message from", data.chatname, ":", data.chatmessage);
// highlightVIPMessage(data);
}
// 5. Filter out messages from bots (if identified)
if (data.bot) {
// console.log("Ignoring bot message from", data.chatname);
return; // Don't process further
}
// --- More Complex Filtering ---
const urlParams = new URLSearchParams(window.location.search);
const onlyShowType = urlParams.get('onlytype'); // e.g., &onlytype=twitch
const hideType = urlParams.get('hidetype'); // e.g., &hidetype=youtube
const showEventsOnly = urlParams.has('eventsonly'); // &eventsonly
const donationsOnly = urlParams.has('donationsonly'); // &donationsonly
if (onlyShowType && data.type !== onlyShowType) {
return;
}
if (hideType && data.type === hideType) {
return;
}
if (showEventsOnly && !data.event) {
return;
}
if (donationsOnly && !data.hasDonation) {
return;
}
// --- Fallback: Display all other messages or specific content ---
displayGenericMessage(data);
}
function displayGenericMessage(data) {
const messageList = document.getElementById('message-list'); // Assuming you have this element
if (!messageList) return;
const messageElement = document.createElement('div');
messageElement.className = `message-item message-type-${data.type || 'unknown'}`;
if (data.event) {
messageElement.classList.add(`event-${data.event}`);
}
let content = '';
if (data.chatimg) {
content += ` `;
}
content += `${data.chatname || 'Anonymous'}: `;
content += `${data.chatmessage || ''}`;
if (data.hasDonation) {
content += ` ❤️ ${data.hasDonation}`;
}
if (data.membership) {
content += ` ⭐ ${data.membership}`;
}
if (data.contentimg) {
content += `