gpg: Signature made Fri 04 Sep 2026 00:58:11 IST

gpg:                using EDDSA key 25BB975698147F3F76719E0C60B5F0386E3DDB7Egpg:                using EDDSA key 25BB975698147F3F76719E0C60B5F0386E3DDB7E
gpg: Good signature from "ari melody <ari@arimelody.space>" [ultimate]
gpg:                 aka "ari melody <ari@arimelody.me>" [ultimate]
initial design for new chat widget
This commit is contained in:
ari melody 2026-09-04 00:58:11 +01:00
parent 4d0efce2e0
commit eab3c09b8b
Signed by: ari
GPG key ID: 60B5F0386E3DDB7E
8 changed files with 359 additions and 17 deletions

View file

@ -1,4 +1,98 @@
const chatContainer = document.getElementById("chat");
const end = new URLSearchParams(location.search).get("end") !== null;
class ChatMessage {
get username() { return this._username; }
set username(username) {
this._username = username;
this._usernameEl.innerText = username;
}
get content() { return this._content; }
set content(content) {
this._content = content;
this._contentEl.innerText = content;
}
get colour() { return this._colour; }
set colour(colour) {
this._colour = colour;
if (colour != undefined)
this._container.style.setProperty("--colour", colour);
}
get element() { return this._container; }
destroy() {
this._container.remove();
}
constructor(message) {
this._headerEl = document.createElement('div');
this._headerEl.className = "header";
// TODO: assemble badges in header, before username
this._usernameEl = document.createElement('p');
this._usernameEl.className = "username";
this.username = message.username;
this._headerEl.appendChild(this._usernameEl);
this._contentEl = document.createElement('p');
this._contentEl.className = "content";
// TODO: build message content from fragments
// TODO: 7TV emotes (stretch goal: FFZ emotes)
this.content = message.text;
this._container = document.createElement("div");
this._container.className = "message";
this._container.dataset.id = message.id;
this._container.dataset.username = message.username;
// TODO: generate and cache random colour for users that do not have one
this.colour = message.colour;
this._container.appendChild(this._headerEl);
this._container.appendChild(this._contentEl);
}
}
class SystemMessage {
get text() { return this._text; }
set text(text) {
this._text = text;
this._textEl.innerText = text;
}
get type() { return this._type; }
set type(type) {
this._type = type;
this._container.className = ["message", "system", type].join(" ").trim();
}
get element() { return this._container; }
destroy() {
this._container.remove();
}
constructor(text, type) {
// TODO: actual icon designs for system messages
this._iconEl = document.createElement('div');
this._iconEl.className = "icon";
this._textEl = document.createElement('p');
this._textEl.className = "text";
this.text = text;
this._container = document.createElement("div");
this.type = type;
this._container.appendChild(this._iconEl);
this._container.appendChild(this._textEl);
}
}
async function sync() {
let eventSource;
@ -20,10 +114,16 @@ async function sync() {
});
eventSource.addEventListener("chat", event => {
console.log(`[${chat}] ${event.data}`);
const messageEl = document.createElement("p");
messageEl.innerText = event.data;
chatContainer.appendChild(messageEl);
handleChatMessage(JSON.parse(event.data));
});
eventSource.addEventListener("system", event => {
handleSystemMessage(JSON.parse(event.data));
});
eventSource.addEventListener("delete", event => {
const data = JSON.parse(event.data);
const username = data.username;
const message_id = data.id;
handleDeleteMessage(username, message_id);
});
eventSource.addEventListener("open", () => {
@ -31,8 +131,60 @@ async function sync() {
});
}
function twitchEmoteURL(emoteID) {
return `https://static-cdn.jtvnw.net/emoticons/v2/${emoteID}/default/dark/1.0#e=0`
function handleChatMessage(data) {
console.log(`[CHAT] ${data.username}: ${data.text}`);
const chatMessage = new ChatMessage(data);
addMessageAndTruncateChat(chatMessage.element);
}
function handleSystemMessage(data) {
if (data.type)
console.log(`[SYSTEM] [${data.type}] ${data.text}`);
else
console.log(`[SYSTEM] ${data.text}`);
const systemMessage = new SystemMessage(data.text, data.type || "");
addMessageAndTruncateChat(systemMessage.element);
}
function handleDeleteMessage(username, message_id) {
if (username == "*" && message_id == "*") {
chatContainer.innerHTML = "";
return;
}
if (username.length > 0 && message_id == "*") {
chatContainer.querySelectorAll(`.message[data-username=${username}]`)
.forEach(msg => {
msg.remove();
});
return;
}
if (username.length == 0 && message_id.length > 0) {
const msg = chatContainer.querySelector(`.message[data-id=${message_id}]`);
if (msg) msg.remove();
}
}
const CHAT_LIMIT = 50;
function addMessageAndTruncateChat(element) {
chatContainer.appendChild(element);
window.scrollTo(0, document.body.scrollHeight);
if (chatContainer.childElementCount <= CHAT_LIMIT) return;
chatContainer.children[0].remove();
}
function twitchEmoteURL(emoteID) {
return `https://static-cdn.jtvnw.net/emoticons/v2/${emoteID}/default/dark/4.0#e=0`
}
function twitchBadgeURL(badgeID) {
return `https://static-cdn.jtvnw.net/badges/v1/${badgeID}/3`
}
if (end)
chatContainer.classList.add("end")
sync();