Files
overlord/Overlord-Server/public/assets/chat-client.js
T

180 lines
4.8 KiB
JavaScript
Raw Normal View History

2026-04-14 21:56:18 -05:00
const UNREAD_KEY = "overlord_chat_unread";
const MUTE_KEY = "overlord_chat_mute";
let ws = null;
let canWrite = false;
let currentUserId = 0;
let messageHandlers = new Set();
let unreadHandlers = new Set();
let statusHandlers = new Set();
let readyHandlers = new Set();
if (localStorage.getItem(UNREAD_KEY) === null) {
localStorage.setItem(UNREAD_KEY, "0");
}
if (localStorage.getItem(MUTE_KEY) === null) {
localStorage.setItem(MUTE_KEY, "none");
}
function emitStatus(status) {
for (const handler of statusHandlers) {
try { handler(status); } catch {}
}
}
function emitReady(history, writable, userId) {
canWrite = writable;
currentUserId = userId || 0;
for (const handler of readyHandlers) {
try { handler(history, writable); } catch {}
}
}
function emitMessage(msg) {
for (const handler of messageHandlers) {
try { handler(msg); } catch {}
}
}
export function getUnreadCount() {
return Number(localStorage.getItem(UNREAD_KEY) || "0");
}
export function setUnreadCount(value) {
const next = Math.max(0, Number(value) || 0);
localStorage.setItem(UNREAD_KEY, String(next));
for (const handler of unreadHandlers) {
try { handler(next); } catch {}
}
}
export function incrementUnread() {
setUnreadCount(getUnreadCount() + 1);
}
export function resetUnread() {
setUnreadCount(0);
}
export function getMuteMode() {
return localStorage.getItem(MUTE_KEY) || "none";
}
export function setMuteMode(mode) {
const valid = ["none", "mute", "mute_collapse", "hide"];
if (!valid.includes(mode)) return;
localStorage.setItem(MUTE_KEY, mode);
}
let audioCtx = null;
function playNotificationSound() {
const mute = getMuteMode();
if (mute !== "none") return;
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = "sine";
osc.frequency.setValueAtTime(880, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(440, audioCtx.currentTime + 0.15);
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.2);
osc.start(audioCtx.currentTime);
osc.stop(audioCtx.currentTime + 0.2);
} catch {}
}
export function onMessage(cb) { messageHandlers.add(cb); return () => messageHandlers.delete(cb); }
export function onUnreadChanged(cb) { unreadHandlers.add(cb); return () => unreadHandlers.delete(cb); }
export function onStatusChanged(cb) { statusHandlers.add(cb); return () => statusHandlers.delete(cb); }
export function onReady(cb) { readyHandlers.add(cb); return () => readyHandlers.delete(cb); }
export function getCanWrite() { return canWrite; }
export function sendMessage(text) {
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
if (!canWrite) return false;
const trimmed = (text || "").trim();
if (!trimmed || trimmed.length > 2000) return false;
ws.send(JSON.stringify({ type: "chat_send", message: trimmed }));
return true;
}
export async function fetchHistory(before, limit = 50) {
const params = new URLSearchParams();
if (before) params.set("before", String(before));
params.set("limit", String(limit));
try {
const res = await fetch(`/api/chat/history?${params.toString()}`);
if (!res.ok) return { messages: [], canWrite: false };
return await res.json();
} catch {
return { messages: [], canWrite: false };
}
}
function handleWsMessage(payload) {
if (!payload || typeof payload.type !== "string") return;
if (payload.type === "chat_ready") {
emitReady(payload.history || [], !!payload.canWrite, payload.userId);
return;
}
if (payload.type === "chat_message") {
emitMessage(payload);
const isOwnMessage = currentUserId && payload.userId === currentUserId;
if (!isOwnMessage) {
const mute = getMuteMode();
if (mute === "none") {
incrementUnread();
playNotificationSound();
}
}
return;
}
if (payload.type === "chat_error") {
console.warn("[chat] server error:", payload.error);
}
}
function connect() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/api/chat/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
emitStatus("connected");
};
ws.onmessage = (event) => {
if (typeof event.data === "string") {
try {
handleWsMessage(JSON.parse(event.data));
} catch {}
return;
}
};
ws.onerror = () => {
emitStatus("error");
};
ws.onclose = () => {
emitStatus("disconnected");
setTimeout(connect, 3000);
};
}
let started = false;
export function start() {
if (started) return;
started = true;
connect();
}