type int = number; type float = number; var _conf: any = null; var _state: string = undefined; var _channel: string = undefined; var _nickname: string = undefined; var _userhash: string = undefined; var _id: (null | string) = null; function get_timestamp(): int { return Math.floor(Date.now()/1000); } function hash_string_to_unit(x: string): float { return (x.split("").reduce((x, y) => ((x + y.charCodeAt(0)) % 32), 0) / 32); } function get_usercolor(name: string): string { const hue: float = hash_string_to_unit(name); return `hsl(${(hue*360).toFixed(2)},50%,75%)`; } async function backend_call(action: string, data: any): Promise { const response = await fetch( `${_conf.backend.scheme}://${_conf.backend.host}:${_conf.backend.port.toFixed(0)}`, { "method": "POST", "body": JSON.stringify({"action": action, "id": _id, "data": data}), } ); if (response.ok) { return response.json(); } else { console.error(response.text()); return Promise.reject(new Error("backend call failed")); } } function update_state(): void { document.querySelector("body").setAttribute("class", _state); } function update_history(events): void { let dom_history: HTMLUListElement = document.querySelector("#history"); for (const event of events) { const timestring: string = (new Date(event["timestamp"]*1000)).toISOString().slice(11, 19); let dom_event: HTMLLIElement = document.createElement("li"); dom_event.classList.add("event"); switch (event["kind"]) { default: dom_event.textContent = ("-- unhandled event: " + JSON.stringify(event)); break; case "channel_message": { let dom_time: HTMLDivElement = document.createElement("div"); dom_time.classList.add("event_time"); dom_time.textContent = timestring; dom_event.appendChild(dom_time); } { let dom_sender: HTMLDivElement = document.createElement("div"); dom_sender.classList.add("event_sender"); dom_sender.style.color = get_usercolor(event["data"]["from"] ?? ""); dom_sender.textContent = event["data"]["from"]; dom_event.appendChild(dom_sender); } { let dom_message: HTMLDivElement = document.createElement("div"); dom_message.classList.add("event_message"); dom_message.textContent = event["data"]["message"]; dom_event.appendChild(dom_message); } break; } dom_history.appendChild(dom_event); } dom_history.scrollTo(0, dom_history["scrollTopMax"]); } function update_users(users: Array<{name: string; role: string;}>): void { let dom_users: HTMLUListElement = document.querySelector("#users"); dom_users.textContent = ""; const users_sorted: Array<{name: string; role: string;}> = users.sort( (x, y) => ( (x.role >= y.role) ? -1 : ( (x.role === y.role) ? ((x.name < y.name) ? -1 : +1) : +1 ) ) ); for (const user of users_sorted) { let dom_user: HTMLLIElement = document.createElement("li"); dom_user.classList.add("user"); { let dom_role: HTMLSpanElement = document.createElement("span"); dom_role.textContent = user.role; dom_user.appendChild(dom_role); } { let dom_name: HTMLSpanElement = document.createElement("span"); dom_name.textContent = user.name; dom_name.style.color = get_usercolor(user.name); dom_user.appendChild(dom_name); } // dom_user.textContent = `${user.role}${user.name}`; dom_users.appendChild(dom_user); } } function set_state(state: string): void { _state = state; update_state(); } function setup_view(): void { document.querySelector("#channel").value = _conf["irc"]["predefined_channel"]; document.querySelector("#nickname").value = (_conf["irc"]["predefined_nickname_prefix"] + (Math.random()*100).toFixed(0)); setInterval( async () => { switch (_state) { case "offline": // do nothing break; case "checking": const ready: boolean = await backend_call("check", null); if (ready) { set_state("online"); } break; case "online": const stuff: any = await backend_call("fetch", null); update_history(stuff["events"]); const userhash: string = btoa(JSON.stringify(stuff["users"])); if (_userhash !== userhash) { _userhash = userhash; update_users(stuff["users"]); } break; } }, _conf["settings"]["poll_interval_in_milliseconds"] ); set_state("offline"); } function setup_control(): void { document.querySelector("#connect > form").addEventListener( "submit", async (event) => { let dom_nickname: HTMLInputElement = document.querySelector("#nickname"); let dom_channel: HTMLInputElement = document.querySelector("#channel"); const nickname: string = dom_nickname.value; const channel: string = dom_channel.value; const id: string = await backend_call( "connect", { "server": _conf["irc"]["server"], "channels": [channel], "nickname": nickname, } ); _id = id; _channel = channel; _nickname = nickname; set_state("checking"); } ); document.querySelector("#disconnect").addEventListener( "click", async (event) => { await backend_call( "disconnect", null ); set_state("offline"); _id = null; } ); document.querySelector("#main > form").addEventListener( "submit", async (event) => { event.preventDefault(); let dom_message: HTMLInputElement = document.querySelector("#message"); const message: string = dom_message.value; dom_message.value = ""; dom_message.focus(); const event_: any = { "timestamp": get_timestamp(), "kind": "channel_message", "data": { "from": _nickname, "to": _channel, "message": message, } }; update_history([event_]); await backend_call( "say", { "channel": _channel, "message": message, } ); } ); } async function main(): Promise { _conf = await fetch("conf.json").then(x => x.json()); setup_view(); setup_control(); } function init(): void { document.addEventListener( "DOMContentLoaded", (event) => { main(); } ); }