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:
parent
4d0efce2e0
commit
eab3c09b8b
8 changed files with 359 additions and 17 deletions
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
@font-face {
|
||||
font-family: 'Darumadrop One';
|
||||
src: url('/public/fonts/darumadrop-one/DarumadropOne-Regular.ttf') format('truetype');
|
||||
}
|
||||
*/
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Darumadrop+One&display=swap');
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
background: hsl(0 0 50%);
|
||||
}
|
||||
|
||||
#chat {
|
||||
position: relative;
|
||||
top: .33em;
|
||||
min-height: calc(100vh - .66em);
|
||||
max-width: 600px;
|
||||
margin: .33em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
gap: .25em;
|
||||
|
||||
font-family: "Darumadrop One", sans-serif;
|
||||
font-size: 32px;
|
||||
line-height: 1em;
|
||||
|
||||
word-wrap: break-word;
|
||||
|
||||
&.end {
|
||||
text-align: end;
|
||||
align-items: end;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
width: 100%;
|
||||
|
||||
--colour: hsl(0 0 75%);
|
||||
|
||||
color: hsl(0 0 100%);
|
||||
text-shadow: .05em .05em 0 hsl(0 0 0 / 75%);
|
||||
|
||||
&.system {
|
||||
width: 420px;
|
||||
margin: 0 1em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
--colour: hsl(0 0 100%);
|
||||
color: var(--colour);
|
||||
|
||||
&.follow { --hue: 128; }
|
||||
&.subscribe { --hue: 284; }
|
||||
&.cheer { --hue: 222; }
|
||||
&.raid { --hue: 28; }
|
||||
&.poll { --hue: 269; }
|
||||
&.shoutout { --hue: 104; }
|
||||
&.point-redeem { --hue: 338; }
|
||||
|
||||
&.follow, &.subscribe, &.cheer, &.raid, &.poll, &.shoutout, &.point-redeem {
|
||||
--colour: oklch(80% 80% var(--hue) / 100%);
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: .5em;
|
||||
margin-top: .3em;
|
||||
margin-right: .2em;
|
||||
transform: translateY(.15em);
|
||||
aspect-ratio: 1;
|
||||
|
||||
background-color: var(--colour);
|
||||
border-radius: 100%;
|
||||
box-shadow: .05em .05em 0 hsl(0 0 0 / 75%);
|
||||
}
|
||||
}
|
||||
|
||||
p { margin: 0; }
|
||||
|
||||
.header {
|
||||
color: var(--colour);
|
||||
}
|
||||
|
||||
.content {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue