type int = number; type float = number; var _conf: any = 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)}/${_conf["backend"]["path"]}`, { "method": "POST", "body": JSON.stringify({"action": action, "id": _model.connection_id, "data": data}), } ); if (response.ok) { return response.json(); } else { console.error(response.text()); return Promise.reject(new Error("backend call failed")); } } enum enum_state { offline = "offline", connecting = "connecting", online = "online", } type type_model = { state: enum_state; channel: (null | string); nickname: (null | string); connection_id: (null | string); usershash: (null | string); }; var _model: type_model = { "state": enum_state.offline, "channel": null, "nickname": null, "connection_id": null, "usershash": null, }; function update_state(): void { document.querySelector("body").setAttribute("class", _model.state); } function update_events(events): void { let dom_events: HTMLUListElement = document.querySelector("#events"); 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 "private_message": { { let dom_time: HTMLSpanElement = document.createElement("span"); dom_time.classList.add("event_time"); dom_time.textContent = timestring; dom_event.appendChild(dom_time); } { let dom_type: HTMLSpanElement = document.createElement("span"); dom_type.classList.add("event_type"); dom_type.textContent = 'private'; dom_event.appendChild(dom_type); } { let dom_sender: HTMLSpanElement = document.createElement("span"); 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: HTMLSpanElement = document.createElement("span"); dom_message.classList.add("event_message"); dom_message.textContent = event["data"]["message"]; dom_event.appendChild(dom_message); } break; } case "channel_message": { { let dom_time: HTMLSpanElement = document.createElement("span"); dom_time.classList.add("event_time"); dom_time.textContent = timestring; dom_event.appendChild(dom_time); } { let dom_sender: HTMLSpanElement = document.createElement("span"); 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: HTMLSpanElement = document.createElement("span"); dom_message.classList.add("event_message"); dom_message.textContent = event["data"]["message"]; dom_event.appendChild(dom_message); } break; } } dom_events.appendChild(dom_event); } if (events.length > 0) { dom_events.scrollTo(0, dom_events["scrollTopMax"]); } else { // do nothing } } 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_users.appendChild(dom_user); } } function set_state(state: enum_state): void { _model.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 (_model.state) { default: { throw (new Error("invalid state: " + _model.state)); break; } case enum_state.offline: { // do nothing break; } case enum_state.connecting: { const ready: boolean = await backend_call("check", null); if (ready) { set_state(enum_state.online); } else { // do nothing } break; } case enum_state.online: { const stuff: any = await backend_call("fetch", null); update_events(stuff["events"]); const usershash: string = btoa(JSON.stringify(stuff["users"])); if (_model.usershash !== usershash) { _model.usershash = usershash; update_users(stuff["users"]); } else { // do nothing } break; } } }, _conf["settings"]["poll_interval_in_milliseconds"] ); set_state(enum_state.offline); } function setup_control(): void { document.querySelector("#connect > form").addEventListener ( "submit", async (event) => { event.preventDefault(); 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 connection_id: string = await backend_call ( "connect", { "server": _conf["irc"]["server"], "channels": [channel], "nickname": nickname, } ); _model.connection_id = connection_id; _model.channel = channel; _model.nickname = nickname; set_state(enum_state.connecting); } ); document.querySelector("#disconnect").addEventListener ( "click", async (event) => { await backend_call ( "disconnect", null ); set_state(enum_state.offline); _model.connection_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": _model.nickname, "to": _model.channel, "message": message, } }; update_events([event_]); await backend_call ( "send", { "channel": _model.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();} ); }