Files
locutus/src/locutus.ts

288 lines
8.8 KiB
TypeScript

"use strict";
import * as USIMP from "usimp";
export class App {
account: USIMP.Account | null;
defaultLocation: string;
main: HTMLElement;
windows: HTMLElement;
session: USIMP.Session | null;
constructor() {
this.defaultLocation = '/welcome';
this.account = null;
this.session = null;
const main = document.getElementsByTagName("main")[0];
if (!main) throw new Error("Element <main> not found");
this.main = main;
const windows = document.getElementById("windows");
if (!windows) throw new Error("Element #windows not found");
this.windows = windows;
window.addEventListener("hashchange", event => {
this.handleUrl(event.newURL);
});
this.handleUrl(document.URL);
}
close() {
if (this.session) this.session.close();
}
sleep() {
if (this.session) this.session.sleep();
}
async wakeup() {
if (this.session) await this.session.wakeup();
}
setHash(hash: string) {
const url = new URL(document.URL);
url.hash = hash;
location.href = url.toString();
}
handleUrl(url: string) {
this.handleHash(new URL(url).hash);
}
handleHash(hash: string) {
if (hash[0] === '#') hash = hash.substr(1);
const defaultCase = () => {
history.replaceState(null, document.title, `#${this.defaultLocation}`);
this.handleHash(this.defaultLocation);
}
switch (hash) {
case "/":
if (!this.session) {
return defaultCase();
}
this.hideWindows();
this.showMain();
this.removeAllWindows();
this.setupMain();
break;
case "/welcome":
this.hideMain();
this.showWindows();
this.removeAllWindows();
this.addWelcomeWindow();
break;
case "/login":
if (this.session) {
return defaultCase();
}
this.hideMain();
this.showWindows();
this.removeAllWindows();
this.addLoginWindow();
break;
default:
console.warn(`Invalid url hash #${hash}`);
return defaultCase();
}
}
async login(accountName: string, domainName: string, password: string) {
let domain;
try {
domain = await USIMP.Domain.fromName(domainName);
} catch (error) {
if (error instanceof DOMException) {
throw new Error("Connection timed out");
} else {
throw new Error("Invalid USIMP domain");
}
}
const session = new USIMP.Session(domain);
const rtt = await session.chooseDomainServer();
console.log(rtt);
const response = await session.authenticate(accountName, password);
if (response.status === "success") {
this.session = session;
this.defaultLocation = "/";
this.setHash("/");
} else {
console.error(response.error);
throw new Error(response.error.message || response.error.code);
}
}
hideMain() {
this.main.style.visibility = "hidden";
}
showMain() {
this.main.style.visibility = "visible";
}
hideWindows() {
this.windows.style.visibility = "hidden";
}
showWindows() {
this.windows.style.visibility = "visible";
}
removeAllWindows() {
while (this.windows.lastChild) this.windows.removeChild(this.windows.lastChild);
}
addWelcomeWindow() {
const win = document.createElement("div");
win.classList.add("window-welcome");
win.innerHTML = `
<h1>Welcome to Locutus!</h1>
<a href="#/login" class="button">Login</a>`;
this.windows.appendChild(win);
}
addMessage(id: string, message: string, delivered: boolean = true) {
const test = document.getElementById(`msg-${id}`);
if (test) return;
const msg = document.createElement("div");
msg.id = `msg-${id}`;
msg.classList.add("message");
if (delivered) {
msg.classList.add("delivered");
} else {
msg.classList.add("pending");
}
msg.innerText = message;
const chat = this.main.getElementsByClassName("chat-history")[0];
if (!chat) throw new Error("Element .chat-history not found");
chat.appendChild(msg);
chat.scrollTop = chat.scrollHeight;
}
confirmMessage(id: string) {
const msg = document.getElementById(`msg-${id}`);
if (!msg) return;
msg.classList.remove("pending");
msg.classList.add("delivered");
}
addLoginWindow() {
const win = document.createElement("div");
win.classList.add("window-login");
win.innerHTML = `
<h1>Login to USIMP Account</h1>
<form>
<input name="account" placeholder="Account name" type="text" required/>
<input name="domain" placeholder="Domain" type="text" pattern="([a-zA-Z0-9_-]+\\.)+[a-zA-Z]{2,}" required/>
<input name="password" placeholder="Password" type="password" required/>
<button type="submit">Login</button>
</form>`;
const form = win.getElementsByTagName("form")[0];
if (!form) throw new Error("Element <form> not found");
form.addEventListener("submit", async event => {
event.preventDefault();
for (const e of form.getElementsByTagName("button")) e.disabled = false;
for (const e of form.getElementsByTagName("input")) {
e.disabled = true;
e.removeAttribute("invalid");
}
try {
try {
await this.login(form['account'].value, form['domain'].value, form['password'].value);
} finally {
for (const d of form.getElementsByTagName("div")) form.removeChild(d);
for (const e of form.getElementsByTagName("input")) e.disabled = false;
for (const e of form.getElementsByTagName("button")) e.disabled = false;
}
} catch (error: any) {
if (error.toString() === "Error: unable to authenticate") {
const account = document.getElementsByName("account")[0];
if (account) account.setAttribute("invalid", "invalid");
} else {
const domain = document.getElementsByName("domain")[0];
if (domain) domain.setAttribute("invalid", "invalid");
}
const div = document.createElement("div");
div.classList.add("error");
div.innerText = error.toString();
form.appendChild(div);
}
});
this.windows.appendChild(win);
const domain = document.getElementsByName("domain")[0];
if (!domain) throw new Error("Element name=domain not found");
domain.addEventListener("input", (event) => {
domain.removeAttribute("invalid");
});
const account = document.getElementsByName("account")[0];
if (!account) throw new Error("Element name=account not found");
account.addEventListener("input", (event) => {
account.removeAttribute("invalid");
});
account.focus();
}
setupMain() {
if (!this.session) throw new Error("Invalid state");
this.main.innerHTML = `
<div class="chat">
<div class="chat-history"></div>
<div class="chat-input-wrapper">
<input name="message" type="text" autocomplete="off"/>
</div>
</div>`;
const input = document.getElementsByTagName("input")[0];
if (!input) throw new Error("Element <input> not found");
input.addEventListener("keydown", event => {
if (event.key === "Enter" && input.value.length > 0) {
if (!this.session) throw new Error("No session found");
const id = crypto.randomUUID();
const val = input.value;
this.addMessage(id, input.value, false);
this.session.newEvent(
"24595934-4540-4333-ac2b-78796ac3f25f",
{message: val},
id
).then(() => {
this.confirmMessage(id);
});
input.value = "";
}
});
this.session.subscribe(event => {
this.addMessage(event.id, event.data.message);
}).then();
}
}