(async function () { let DELAY = 0; const DISTR = 0.33; class User { constructor(name) { this.userWindow = document.querySelector(`#demo .user.${name}`); this.terminal = this.userWindow.querySelector(`.terminal`); this.input = this.terminal.querySelector(".input"); this.demoInput = this.terminal.querySelector("input"); this.resetInput(); this.setupDemo(); this.group = []; this.display = this.terminal.querySelector(".display"); this.setupMoveWindow(); this.name = name; } reset() { this.resetInput(); this.display.innerHTML = ""; } setGroup(groupName, users) { this.users = users; this.group = users.filter((u) => u !== this); this.groupName = groupName; } tryDemo() { this.reset(); show(this.demoInput); this.demoInput.value = ""; } async send(to, message, typeTo, paste, secret) { await this._sendMsg(`@${to.name}`, message, typeTo, paste, secret); await to.receive(this, toSecret(secret, message)); await delay(20); } async sendGroup(message, typeTo, paste) { await this._sendMsg(`#${this.groupName}`, message, typeTo, paste); await this.receiveGroup(message); await delay(10); } async _sendMsg(toStr, message, typeTo, paste, secret) { await this.type(`${toStr} `, !typeTo); if (secret) await this.type("#"); await this.type(message, paste); if (secret) await this.type("#"); await delay(10); this.resetInput(); this.show("sent", `${toStr} ${toSecret(secret, message)}`); } async type(str, paste) { if (paste) { await delay(10); this.input.insertAdjacentHTML("beforeend", str); } else { for (const char of str) { await delay(isAlpha(char) ? 1 : 2); this.input.insertAdjacentHTML("beforeend", char); } } await delay(2); } resetInput() { this.input.innerHTML = "> "; show(this.demoInput, false); } async receive(from, message, edit, group) { await delay(10); let g = group ? `#${this.groupName} ` : ""; this.show("received", `${g}${from.name}> ${message}`, edit); } async receiveGroup(message, edit) { await Promise.all(this.group.map((u) => u.receive(this, message, edit, true))); } show(mode, str, edit) { if (edit && this.lastMessage) { this.lastMessage.innerHTML = highlight(str); return; } this.display.insertAdjacentHTML("beforeend", `
${highlight(str)}
`); } setupDemo() { if (!this.demoInput) return; let editMode = false; on("keypress", this.demoInput, async ({ key }) => { if (key === "Enter") { const edit = editMode; editMode = false; const [to, ...words] = this.demoInput.value.trim().split(" "); const message = words.join(" "); switch (to[0]) { case undefined: if (message !== "") { this.show("error", "Message should start with @user or #group"); } break; case "@": await this.sendInput(to.slice(1), message, edit); break; case "#": await this.sendInputGroup(to.slice(1), message, edit); break; default: this.show("error", "Message should start with @user or #group"); } } else if (this.demoInput.value === "" && key !== "@" && key !== "#") { const channel = this.currentChannel(); if (channel) this.demoInput.value = channel + " "; } }); on("keydown", this.demoInput, async (e) => { switch (e.key) { case "ArrowUp": if (this.demoInput.value === "" && this.lastMessage) { const str = (this.demoInput.value = this.lastMessage.innerText); editMode = true; await delay(0); this.demoInput.selectionStart = str.length; } break; case "Tab": { e.preventDefault(); const userIndex = this.users.indexOf(this); const nextIndex = (userIndex + 1) % this.users.length; this.users[nextIndex].demoInput.focus(); } } }); } async sendInput(name, message, edit) { if (name === this.name) { this.show("error", "Can't send message to yourself"); return; } const recipient = this.group.find((u) => u.name === name); if (recipient === undefined) { const knownNames = this.group.map((u) => `@${u.name}`).join(", ") + ` or @${this.name}`; this.show("error", `Unknown recipient @${name} (try ${knownNames})`); return; } this.show("sent", `@${name} ${message}`, edit); this.demoInput.value = ""; await recipient.receive(this, message, edit); } async sendInputGroup(name, message, edit) { if (name !== this.groupName) { this.show("error", `Unknown group #${name} (try #team)`); return; } this.show("sent", `#${name} ${message}`, edit); this.demoInput.value = ""; await this.receiveGroup(message, edit); } get lastMessage() { const messages = this.display.childNodes; return messages[messages.length - 1]; } currentChannel() { return this.lastMessage && toContact(this.lastMessage.childNodes[0].innerHTML); } setupMoveWindow() { let moving = false; let startX, startY; const user = this.userWindow; const parent = user.parentNode; on("mousedown", this.terminal, (e) => { if (e.clientY - this.terminal.getBoundingClientRect().top > 20) return; moving = true; startX = user.offsetLeft - e.clientX; startY = user.offsetTop - e.clientY; parent.removeChild(user); parent.appendChild(user); }); on("mouseup", this.terminal, () => (moving = false)); on("mouseleave", this.terminal, () => (moving = false)); on("mousemove", this.terminal, (e) => { if (!moving) return; user.style.left = e.clientX + startX + "px"; user.style.top = e.clientY + startY + "px"; }); } } function toContact(str) { return str.endsWith(">") ? "@" + str.slice(0, -4) : str; } function setGroup(groupName, users) { users.forEach((u) => u.setGroup(groupName, users)); } const alice = new User("alice"); const bob = new User("bob"); const tom = new User("tom"); const team = [alice, bob, tom]; setGroup("team", team); async function chatDemo() { team.forEach((u) => u.reset()); await alice.sendGroup("please review my PR project/site#72", true); await tom.sendGroup("anybody got application key 🔑?"); await bob.sendGroup("looking at it now @alice 👀"); await alice.sendGroup("thanks @bob!"); await alice.sendGroup("will DM @tom"); await alice.send(tom, "w3@o6CewoZx#%$SQETXbWnus", true, true, true); await tom.send(alice, "you're the savior 🙏!"); await alice.send(bob, "please check the tests too", true); await bob.send(alice, "all looks good 👍"); await alice.send(bob, "thank you!"); DELAY = 80; } const invitation = "smp::example.com:5223#1XNE1m2E1m0lm92​WG​Ket9CL6+lO742Vy5​G6nsrkvgs8=::St9hPY+k6nfrbaXj::rsa:MII​BoTANBgkqhkiG9w0B​AQEFAAOCAY4AMIIBiQKCAQEA03XGpEqh3faDN​Gl06pPhaT=="; async function establishConnection() { team.forEach((u) => u.reset()); await alice.type("/add bob"); await delay(10); alice.resetInput(); // alice.show("/add bob"); alice.show("sent", "pass this invitation to your contact (via any channel):"); alice.show("sent", " "); alice.show("sent", invitation); alice.show("sent", " "); alice.show("sent", "and ask them to connect:"); alice.show("sent", "/c name_for_you invitation_above"); await delay(20); await bob.type("/connect alice "); await bob.type(invitation, true); await delay(20); bob.resetInput(); await bob.show("received", "/connect alice " + invitation); await delay(10); bob.show("received", "@alice connected"); await delay(2); alice.show("received", "@bob connected"); await alice.send(bob, "hello bob"); await bob.send(alice, "hi alice"); } await chatDemo(); const RUN_DEMO = "#demo .run-demo"; const RUN_FASTER = "#demo .run-faster"; const TRY_IT = "#demo .try-it"; onClick(RUN_DEMO, runChatDemo); // onClick(RUN_DEMO, establishConnection); onClick(RUN_FASTER, () => (DELAY /= 2)); onClick(TRY_IT, tryChatDemo); async function runChatDemo() { show(RUN_DEMO, false); show(RUN_FASTER); enable(TRY_IT, false); await chatDemo(); show(RUN_DEMO); show(RUN_FASTER, false); enable(TRY_IT); } function tryChatDemo() { team.forEach((u) => u.tryDemo()); alice.demoInput.focus(); } async function delay(units) { // delay is random with `1 +/- DISTR` range const ms = units * DELAY * (1 - DISTR + 2 * DISTR * Math.random()); return new Promise((resolve) => setTimeout(resolve, ms)); } function highlight(str) { return str .replace(/(@[a-z]+)([^0-9]|$)/gi, `$1$2`) .replace(/([a-z]+>)([^0-9]|$)/gi, `$1$2`) .replace(/(#[a-z]+)([^0-9]|$)/gi, `$1$2`) .replace(/#([^\s]+)#([\s]|$)/gi, `#$1#$2`); } function toSecret(secret, message) { return secret ? `#${message}#` : message; } function isAlpha(c) { c = c.toUpperCase(); return c >= "A" && c <= "Z"; } let flipper = setInterval(flipProblem, 10000); onClick("#problem .pagination", () => { clearInterval(flipper); flipper = setInterval(flipProblem, 20000); }); function flipProblem() { if (isElementInViewport(document.getElementById("problem"))) { window.location.hash = window.location.hash === "#problem-explained" ? "#problem-intro" : "#problem-explained"; } } function isElementInViewport(el) { if (!el) return false; const r = el.getBoundingClientRect(); return r.bottom >= 0 && r.top <= window.innerHeight; } function onClick(selector, handler, enable = true) { const el = document.querySelector(selector); if (el) on("click", el, handler, enable); } function on(event, el, handler, enable = true) { const method = enable ? "addEventListener" : "removeEventListener"; el[method](event, handler); } function show(selector, visible = true) { const el = typeof selector === "string" ? document.querySelector(selector) : selector; if (el) el.style.display = visible ? "block" : "none"; } function enable(selector, enabled = true) { const el = document.querySelector(selector); el.disabled = enabled ? "" : "true"; } })();