diff --git a/css/demo.css b/css/demo.css
new file mode 100644
index 0000000000..4b1a2b1fc1
--- /dev/null
+++ b/css/demo.css
@@ -0,0 +1,227 @@
+#demo {
+ padding-top: 3.5rem;
+ padding-bottom: 2.5rem;
+ text-align: center;
+}
+
+#demo .all-users {
+ position: relative;
+ height: 640px;
+}
+
+#demo .user {
+ position: absolute;
+}
+
+#demo h3 {
+ position: absolute;
+ top: -60px;
+ text-align: left;
+ padding: 0 0 5px 7px;
+ color: #ffffbb;
+ mix-blend-mode: difference;
+ font-size: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+#demo .user .terminal {
+ text-align: left;
+ position: relative;
+ background-color: rgba(0, 0, 0, 0.8);
+ border: 1px solid white;
+ border-radius: 10px;
+ box-sizing: border-box;
+ padding: 10px 10px;
+ color: white;
+ overflow-y: hidden;
+}
+
+#demo .user .terminal:before {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: 20px;
+ background: #666 url(/img/topbar.svg) no-repeat 8px center;
+ border-radius: 10px 10px 0 0;
+ z-index: 2;
+}
+
+#demo .user .terminal input,
+#demo .user .terminal .input,
+#demo .user .terminal .display {
+ font-family: Menlo, "Lucida Console", Monaco, monospace;
+ font-size: 14px;
+ letter-spacing: 0.27px;
+ line-height: 28px;
+ position: absolute;
+ left: 10px;
+ right: 10px;
+}
+
+#demo .user .terminal input,
+#demo .user .terminal .input {
+ color: white;
+ display: block;
+ height: 30px;
+ bottom: 0;
+}
+
+#demo .user .terminal input {
+ background-color: transparent;
+ display: none;
+ left: 25px;
+ width: calc(100% - 30px);
+}
+
+#demo .user .terminal .display {
+ color: white;
+ text-align: left;
+ bottom: 30px;
+ z-index: 1;
+ overflow-x: hidden;
+ word-wrap: break-word;
+}
+
+#demo .user .terminal .display .info {
+ color: white;
+}
+
+#demo .user .terminal .display .error {
+ color: #ff6347;
+}
+
+#demo .user .terminal .display .sent span.group {
+ color: cyan;
+}
+
+#demo .user .terminal .display .received span.group {
+ color: yellow;
+}
+
+#demo .user .terminal .display span {
+ font-family: Menlo, Monaco, "Lucida Console", monospace;
+ text-align: left;
+ margin: 0 0;
+}
+
+#demo .user .terminal .display span.recipient {
+ color: cyan;
+}
+
+#demo .user .terminal .display span.sender {
+ color: yellow;
+}
+
+#demo .user .terminal .display span.secret {
+ color: rgba(0, 0, 0, 0.4);
+ background-color: rgba(0, 0, 0, 0.4);
+}
+
+#demo .alice {
+ left: -30px;
+ top: 102px;
+}
+
+#demo .bob {
+ left: 425px;
+ top: 0px;
+}
+
+#demo .tom {
+ left: 425px;
+ top: 378px;
+}
+
+#demo .all-users .terminal {
+ width: 410px;
+}
+
+#demo .alice .terminal {
+ height: 340px;
+}
+
+#demo .bob .terminal {
+ height: 305px;
+}
+
+#demo .tom .terminal {
+ height: 280px;
+}
+
+
+/* Demo buttons */
+#demo button {
+ position: absolute;
+ width: 140px;
+ height: 40px;
+ bottom: 85px;
+ font-size: 16px;
+ border-radius: 34px;
+ color: white;
+ font-weight: 500;
+ letter-spacing: 0.02em;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ padding-left: 1.25rem;
+ padding-right: 1.25rem;
+ background-color: #0053d0;
+}
+
+.dark #demo button {
+ color: black;
+ background-color: #70f0f9;
+}
+
+#demo button:disabled {
+ filter: brightness(75%);
+}
+
+#demo .run-demo {
+ left: 20px;
+}
+
+#demo .run-faster {
+ left: 20px;
+ display: none;
+}
+
+#demo .try-it {
+ left: 180px;
+}
+
+@media (max-width: 1240px) {
+ #demo .all-users {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ #demo .alice {
+ position: relative !important;
+ top: 0;
+ left: 0;
+ }
+
+ #demo button,
+ #demo .bob,
+ #demo .tom {
+ display: none;
+ }
+}
+
+@media (max-width: 570px) {
+ #demo .alice .terminal {
+ width: 400px;
+ }
+}
+
+@media (max-width: 440px) {
+ #demo .alice .terminal {
+ width: 330px;
+ height: 420px;
+ }
+}
\ No newline at end of file
diff --git a/docs/themes.html b/docs/themes.html
index f8c85fd3ee..19386e6933 100644
--- a/docs/themes.html
+++ b/docs/themes.html
@@ -669,7 +669,7 @@ window.addEventListener('scroll',changeHeaderBg);
Once you have configured your theme in the app, export it to a file and give it a descriptive name – e.g., example.theme
-Export your app database, and import a sample chat database.
+Export your app database, and import a sample chat database - the passphrase is passphrase.
Make three screenshots - the list of conversations with opened profile picker, conversation and privacy settings.
diff --git a/js/demo.js b/js/demo.js
new file mode 100644
index 0000000000..21beec97f4
--- /dev/null
+++ b/js/demo.js
@@ -0,0 +1,347 @@
+(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#1XNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8=::St9hPY+k6nfrbaXj::rsa:MIIBoTANBgkqhkiG9w0BAQEFAAOCAY4AMIIBiQKCAQEA03XGpEqh3faDNGl06pPhaT==";
+
+ 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";
+ }
+})();
diff --git a/js/docs.js b/js/docs.js
index 4535fb5ea9..137923482c 100644
--- a/js/docs.js
+++ b/js/docs.js
@@ -1,4 +1,51 @@
document.addEventListener("DOMContentLoaded", function () {
+ if (window.location.pathname.endsWith('cli.html')) {
+ const cliHeader = document.querySelector('h1')
+ const demoSection = document.createElement('section')
+ demoSection.id = 'demo'
+ demoSection.innerHTML = `
+
+
+
+
+
+
+
+
+ `
+ cliHeader.parentNode.insertBefore(demoSection, cliHeader.nextSibling)
+
+ const demoScript = document.createElement('script')
+ demoScript.src = '/js/demo.js'
+ document.body.appendChild(demoScript)
+
+ const demoStyles = document.createElement('link')
+ demoStyles.rel = 'stylesheet'
+ demoStyles.href = '/css/demo.css'
+ document.head.appendChild(demoStyles)
+ }
+
const imgs = document.querySelectorAll('p img')
imgs.forEach(img => {
console.log(img.height)