forked from cadence/Carbon
		
	
							parent
							
								
									d7454ddc1c
								
							
						
					
					
						commit
						f9662e31a2
					
				
					 24 changed files with 1251 additions and 1392 deletions
				
			
		| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
.gitignore
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
{}
 | 
					 | 
				
			||||||
							
								
								
									
										451
									
								
								build.js
									
										
									
									
									
								
							
							
						
						
									
										451
									
								
								build.js
									
										
									
									
									
								
							| 
						 | 
					@ -1,280 +1,245 @@
 | 
				
			||||||
const pug = require("pug");
 | 
					const pug = require("pug")
 | 
				
			||||||
const sass = require("sass");
 | 
					const sass = require("sass")
 | 
				
			||||||
const fs = require("fs").promises;
 | 
					const fs = require("fs").promises
 | 
				
			||||||
const os = require("os");
 | 
					const os = require("os")
 | 
				
			||||||
const crypto = require("crypto");
 | 
					const crypto = require("crypto")
 | 
				
			||||||
const path = require("path");
 | 
					const path = require("path")
 | 
				
			||||||
const pj = path.join;
 | 
					const pj = path.join
 | 
				
			||||||
const babel = require("@babel/core");
 | 
					const babel = require("@babel/core")
 | 
				
			||||||
const fetch = require("node-fetch");
 | 
					const fetch = require("node-fetch")
 | 
				
			||||||
const chalk = require("chalk");
 | 
					const chalk = require("chalk")
 | 
				
			||||||
const hint = require("jshint").JSHINT;
 | 
					const hint = require("jshint").JSHINT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
process.chdir(pj(__dirname, "src"));
 | 
					process.chdir(pj(__dirname, "src"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const buildDir = "../build";
 | 
					const buildDir = "../build"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const validationQueue = [];
 | 
					const validationQueue = []
 | 
				
			||||||
const validationHost =
 | 
					const validationHost = os.hostname() === "future" ? "http://localhost:8888/" : "http://validator.w3.org/nu/"
 | 
				
			||||||
  os.hostname() === "future"
 | 
					const static = new Map()
 | 
				
			||||||
    ? "http://localhost:8888/"
 | 
					const links = new Map()
 | 
				
			||||||
    : "http://validator.w3.org/nu/";
 | 
					const pugLocals = {static, links}
 | 
				
			||||||
const static = new Map();
 | 
					 | 
				
			||||||
const links = new Map();
 | 
					 | 
				
			||||||
const pugLocals = { static, links };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const spec = require("./spec.js");
 | 
					const spec = require("./spec.js")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function hash(buffer) {
 | 
					function hash(buffer) {
 | 
				
			||||||
  return crypto.createHash("sha256").update(buffer).digest("hex").slice(0, 10);
 | 
						return crypto.createHash("sha256").update(buffer).digest("hex").slice(0, 10)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function validate(filename, body, type) {
 | 
					function validate(filename, body, type) {
 | 
				
			||||||
  const promise = fetch(validationHost + "?out=json", {
 | 
						const promise = fetch(validationHost+"?out=json", {
 | 
				
			||||||
    method: "POST",
 | 
							method: "POST",
 | 
				
			||||||
    body,
 | 
							body,
 | 
				
			||||||
    headers: {
 | 
							headers: {
 | 
				
			||||||
      "content-type": `text/${type}; charset=UTF-8`,
 | 
								"content-type": `text/${type}; charset=UTF-8`
 | 
				
			||||||
    },
 | 
							}
 | 
				
			||||||
  })
 | 
						}).then(res => res.json()).then(root => {
 | 
				
			||||||
    .then((res) => res.json())
 | 
							return function cont() {
 | 
				
			||||||
    .then((root) => {
 | 
								let concerningMessages = 0
 | 
				
			||||||
      return function cont() {
 | 
								for (const message of root.messages) {
 | 
				
			||||||
        let concerningMessages = 0;
 | 
									if (message.hiliteStart) {
 | 
				
			||||||
        for (const message of root.messages) {
 | 
										let type = message.type
 | 
				
			||||||
          if (message.hiliteStart) {
 | 
										if (message.type === "error") {
 | 
				
			||||||
            let type = message.type;
 | 
											type = chalk.red("error")
 | 
				
			||||||
            if (message.type === "error") {
 | 
										} else if (message.type === "warning") {
 | 
				
			||||||
              type = chalk.red("error");
 | 
											type = chalk.yellow("warning")
 | 
				
			||||||
            } else if (message.type === "warning") {
 | 
										} else {
 | 
				
			||||||
              type = chalk.yellow("warning");
 | 
											continue // don't care about info
 | 
				
			||||||
            } else {
 | 
										}
 | 
				
			||||||
              continue; // don't care about info
 | 
										let match
 | 
				
			||||||
            }
 | 
										if (match = message.message.match(/Property “([\w-]+)” doesn't exist.$/)) {
 | 
				
			||||||
            let match;
 | 
											// allow these properties specifically
 | 
				
			||||||
            if (
 | 
											if (["scrollbar-width", "scrollbar-color", "overflow-anchor"].includes(match[1])) {
 | 
				
			||||||
              (match = message.message.match(
 | 
												continue
 | 
				
			||||||
                /Property “([\w-]+)” doesn't exist.$/
 | 
											}
 | 
				
			||||||
              ))
 | 
										}
 | 
				
			||||||
            ) {
 | 
										concerningMessages++
 | 
				
			||||||
              // allow these properties specifically
 | 
										console.log(`validation: ${type} in ${filename}`)
 | 
				
			||||||
              if (
 | 
										console.log(`    ${message.message}`)
 | 
				
			||||||
                [
 | 
										const text = message.extract.replace(/\n/g, "⏎").replace(/\t/g, " ")
 | 
				
			||||||
                  "scrollbar-width",
 | 
										console.log(chalk.grey(
 | 
				
			||||||
                  "scrollbar-color",
 | 
											"    "
 | 
				
			||||||
                  "overflow-anchor",
 | 
												+ text.slice(0, message.hiliteStart)
 | 
				
			||||||
                ].includes(match[1])
 | 
												+ chalk.inverse(text.substr(message.hiliteStart, message.hiliteLength))
 | 
				
			||||||
              ) {
 | 
												+ text.slice(message.hiliteStart+message.hiliteLength)
 | 
				
			||||||
                continue;
 | 
										))
 | 
				
			||||||
              }
 | 
									} else {
 | 
				
			||||||
            }
 | 
										console.log(message)
 | 
				
			||||||
            concerningMessages++;
 | 
									}
 | 
				
			||||||
            console.log(`validation: ${type} in ${filename}`);
 | 
								}
 | 
				
			||||||
            console.log(`    ${message.message}`);
 | 
								if (!concerningMessages) {
 | 
				
			||||||
            const text = message.extract
 | 
									console.log(`validation: ${chalk.green("ok")} for ${filename}`)
 | 
				
			||||||
              .replace(/\n/g, "⏎")
 | 
								}
 | 
				
			||||||
              .replace(/\t/g, " ");
 | 
							}
 | 
				
			||||||
            console.log(
 | 
						})
 | 
				
			||||||
              chalk.grey(
 | 
						validationQueue.push(promise)
 | 
				
			||||||
                "    " +
 | 
						return promise
 | 
				
			||||||
                  text.slice(0, message.hiliteStart) +
 | 
					 | 
				
			||||||
                  chalk.inverse(
 | 
					 | 
				
			||||||
                    text.substr(message.hiliteStart, message.hiliteLength)
 | 
					 | 
				
			||||||
                  ) +
 | 
					 | 
				
			||||||
                  text.slice(message.hiliteStart + message.hiliteLength)
 | 
					 | 
				
			||||||
              )
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            console.log(message);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (!concerningMessages) {
 | 
					 | 
				
			||||||
          console.log(`validation: ${chalk.green("ok")} for ${filename}`);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  validationQueue.push(promise);
 | 
					 | 
				
			||||||
  return promise;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function runHint(filename, source) {
 | 
					function runHint(filename, source) {
 | 
				
			||||||
  hint(source, {
 | 
						hint(source, {
 | 
				
			||||||
    esversion: 9,
 | 
							esversion: 9,
 | 
				
			||||||
    undef: true,
 | 
							undef: true,
 | 
				
			||||||
    // unused: true,
 | 
							// unused: true,
 | 
				
			||||||
    loopfunc: true,
 | 
							loopfunc: true,
 | 
				
			||||||
    globals: ["console", "URLSearchParams"],
 | 
							globals: ["console", "URLSearchParams"],
 | 
				
			||||||
    browser: true,
 | 
							browser: true,
 | 
				
			||||||
    asi: true,
 | 
							asi: true,
 | 
				
			||||||
  });
 | 
						})
 | 
				
			||||||
  const result = hint.data();
 | 
						const result = hint.data()
 | 
				
			||||||
  let problems = 0;
 | 
						let problems = 0
 | 
				
			||||||
  if (result.errors) {
 | 
						if (result.errors) {
 | 
				
			||||||
    for (const error of result.errors) {
 | 
							for (const error of result.errors) {
 | 
				
			||||||
      if (error.evidence) {
 | 
								if (error.evidence) {
 | 
				
			||||||
        const text = error.evidence.replace(/\t/g, "    ");
 | 
									const text = error.evidence.replace(/\t/g, "    ")
 | 
				
			||||||
        if (["W014"].includes(error.code)) continue;
 | 
									if ([
 | 
				
			||||||
        let type = error.code.startsWith("W")
 | 
										"W014"
 | 
				
			||||||
          ? chalk.yellow("warning")
 | 
									].includes(error.code)) continue
 | 
				
			||||||
          : chalk.red("error");
 | 
									let type = error.code.startsWith("W") ? chalk.yellow("warning") : chalk.red("error")
 | 
				
			||||||
        console.log(`hint: ${type} in ${filename}`);
 | 
									console.log(`hint: ${type} in ${filename}`)
 | 
				
			||||||
        console.log(
 | 
									console.log(`    ${error.line}:${error.character}: ${error.reason} (${error.code})`)
 | 
				
			||||||
          `    ${error.line}:${error.character}: ${error.reason} (${error.code})`
 | 
									console.log(chalk.gray(
 | 
				
			||||||
        );
 | 
										"    "
 | 
				
			||||||
        console.log(
 | 
											+ text.slice(0, error.character)
 | 
				
			||||||
          chalk.gray(
 | 
											+ chalk.inverse(text.substr(error.character, 1))
 | 
				
			||||||
            "    " +
 | 
											+ text.slice(error.character+1)
 | 
				
			||||||
              text.slice(0, error.character) +
 | 
									))
 | 
				
			||||||
              chalk.inverse(text.substr(error.character, 1)) +
 | 
									problems++
 | 
				
			||||||
              text.slice(error.character + 1)
 | 
								}
 | 
				
			||||||
          )
 | 
							}
 | 
				
			||||||
        );
 | 
						}
 | 
				
			||||||
        problems++;
 | 
						if (problems) {
 | 
				
			||||||
      }
 | 
							console.log(`hint: ${chalk.cyan(problems+" problems")} in ${filename}`)
 | 
				
			||||||
    }
 | 
						} else {
 | 
				
			||||||
  }
 | 
							console.log(`hint: ${chalk.green("ok")} for ${filename}`)
 | 
				
			||||||
  if (problems) {
 | 
						}
 | 
				
			||||||
    console.log(`hint: ${chalk.cyan(problems + " problems")} in ${filename}`);
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    console.log(`hint: ${chalk.green("ok")} for ${filename}`);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function addFile(sourcePath, targetPath) {
 | 
					async function addFile(sourcePath, targetPath) {
 | 
				
			||||||
  const contents = await fs.readFile(pj(".", sourcePath), { encoding: null });
 | 
						const contents = await fs.readFile(pj(".", sourcePath), {encoding: null})
 | 
				
			||||||
  static.set(sourcePath, `${targetPath}?static=${hash(contents)}`);
 | 
						static.set(sourcePath, `${targetPath}?static=${hash(contents)}`)
 | 
				
			||||||
  fs.writeFile(pj(buildDir, targetPath), contents);
 | 
						fs.writeFile(pj(buildDir, targetPath), contents)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function addJS(sourcePath, targetPath) {
 | 
					async function addJS(sourcePath, targetPath) {
 | 
				
			||||||
  const source = await fs.readFile(pj(".", sourcePath), { encoding: "utf8" });
 | 
						const source = await fs.readFile(pj(".", sourcePath), {encoding: "utf8"})
 | 
				
			||||||
  static.set(sourcePath, `${targetPath}?static=${hash(source)}`);
 | 
						static.set(sourcePath, `${targetPath}?static=${hash(source)}`)
 | 
				
			||||||
  runHint(sourcePath, source);
 | 
						runHint(sourcePath, source);
 | 
				
			||||||
  fs.writeFile(pj(buildDir, targetPath), source);
 | 
						fs.writeFile(pj(buildDir, targetPath), source)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function addSass(sourcePath, targetPath) {
 | 
					async function addSass(sourcePath, targetPath) {
 | 
				
			||||||
  const renderedCSS = sass.renderSync({
 | 
						const renderedCSS = sass.renderSync({
 | 
				
			||||||
    file: pj(".", sourcePath),
 | 
							file: pj(".", sourcePath),
 | 
				
			||||||
    outputStyle: "expanded",
 | 
							outputStyle: "expanded",
 | 
				
			||||||
    indentType: "tab",
 | 
							indentType: "tab",
 | 
				
			||||||
    indentWidth: 1,
 | 
							indentWidth: 1,
 | 
				
			||||||
    functions: {
 | 
							functions: {
 | 
				
			||||||
      "static($name)": function (name) {
 | 
								"static($name)": function(name) {
 | 
				
			||||||
        if (!(name instanceof sass.types.String)) {
 | 
									if (!(name instanceof sass.types.String)) {
 | 
				
			||||||
          throw "$name: expected a string";
 | 
										throw "$name: expected a string"
 | 
				
			||||||
        }
 | 
									}
 | 
				
			||||||
        const result = static.get(name.getValue());
 | 
									const result = static.get(name.getValue())
 | 
				
			||||||
        if (typeof result === "string") {
 | 
									if (typeof result === "string") {
 | 
				
			||||||
          return new sass.types.String(result);
 | 
										return new sass.types.String(result)
 | 
				
			||||||
        } else {
 | 
									} else {
 | 
				
			||||||
          throw new Error(
 | 
										throw new Error("static file '"+name.getValue()+"' does not exist")
 | 
				
			||||||
            "static file '" + name.getValue() + "' does not exist"
 | 
									}
 | 
				
			||||||
          );
 | 
								}
 | 
				
			||||||
        }
 | 
							}
 | 
				
			||||||
      },
 | 
						}).css
 | 
				
			||||||
    },
 | 
						static.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`)
 | 
				
			||||||
  }).css;
 | 
						validate(sourcePath, renderedCSS, "css")
 | 
				
			||||||
  static.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`);
 | 
						await fs.writeFile(pj(buildDir, targetPath), renderedCSS)
 | 
				
			||||||
  validate(sourcePath, renderedCSS, "css");
 | 
					 | 
				
			||||||
  await fs.writeFile(pj(buildDir, targetPath), renderedCSS);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function addPug(sourcePath, targetPath) {
 | 
					async function addPug(sourcePath, targetPath) {
 | 
				
			||||||
  function getRelative(staticTarget) {
 | 
						function getRelative(staticTarget) {
 | 
				
			||||||
    const pathLayer = (
 | 
							const pathLayer = (path.dirname(targetPath).replace(/\/$/, "").match(/\//g) || []).length
 | 
				
			||||||
      path.dirname(targetPath).replace(/\/$/, "").match(/\//g) || []
 | 
							const prefix = Array(pathLayer).fill("../").join("")
 | 
				
			||||||
    ).length;
 | 
							const result = prefix + staticTarget.replace(/^\//, "")
 | 
				
			||||||
    const prefix = Array(pathLayer).fill("../").join("");
 | 
							if (result) return result
 | 
				
			||||||
    const result = prefix + staticTarget.replace(/^\//, "");
 | 
							else return "./"
 | 
				
			||||||
    if (result) return result;
 | 
						}
 | 
				
			||||||
    else return "./";
 | 
						function getStatic(target) {
 | 
				
			||||||
  }
 | 
							return getRelative(static.get(target))
 | 
				
			||||||
  function getStatic(target) {
 | 
						}
 | 
				
			||||||
    return getRelative(static.get(target));
 | 
						function getStaticName(target) {
 | 
				
			||||||
  }
 | 
							return getRelative(static.get(target)).replace(/\?.*$/, "")
 | 
				
			||||||
  function getStaticName(target) {
 | 
						}
 | 
				
			||||||
    return getRelative(static.get(target)).replace(/\?.*$/, "");
 | 
						function getLink(target) {
 | 
				
			||||||
  }
 | 
							return getRelative(links.get(target))
 | 
				
			||||||
  function getLink(target) {
 | 
						}
 | 
				
			||||||
    return getRelative(links.get(target));
 | 
						const renderedHTML = pug.compileFile(pj(".", sourcePath), {pretty: true})({getStatic, getStaticName, getLink, ...pugLocals})
 | 
				
			||||||
  }
 | 
						let renderedWithoutPHP = renderedHTML.replace(/<\?(?:php|=).*?\?>/gsm, "")
 | 
				
			||||||
  const renderedHTML = pug.compileFile(pj(".", sourcePath), { pretty: true })({
 | 
						validate(sourcePath, renderedWithoutPHP, "html")
 | 
				
			||||||
    getStatic,
 | 
						await fs.writeFile(pj(buildDir, targetPath), renderedHTML)
 | 
				
			||||||
    getStaticName,
 | 
					 | 
				
			||||||
    getLink,
 | 
					 | 
				
			||||||
    ...pugLocals,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  let renderedWithoutPHP = renderedHTML.replace(/<\?(?:php|=).*?\?>/gms, "");
 | 
					 | 
				
			||||||
  validate(sourcePath, renderedWithoutPHP, "html");
 | 
					 | 
				
			||||||
  await fs.writeFile(pj(buildDir, targetPath), renderedHTML);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function addBabel(sourcePath, targetPath) {
 | 
					async function addBabel(sourcePath, targetPath) {
 | 
				
			||||||
  const originalCode = await fs.readFile(pj(".", sourcePath), "utf8");
 | 
						const originalCode = await fs.readFile(pj(".", sourcePath), "utf8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const compiled = babel.transformSync(originalCode, {
 | 
						const compiled = babel.transformSync(originalCode, {
 | 
				
			||||||
    sourceMaps: false,
 | 
							sourceMaps: false,
 | 
				
			||||||
    sourceType: "script",
 | 
							sourceType: "script",
 | 
				
			||||||
    presets: [
 | 
							presets: [
 | 
				
			||||||
      [
 | 
								[
 | 
				
			||||||
        "@babel/env",
 | 
									"@babel/env", {
 | 
				
			||||||
        {
 | 
										targets: {
 | 
				
			||||||
          targets: {
 | 
											"ie": 11
 | 
				
			||||||
            ie: 11,
 | 
										}
 | 
				
			||||||
          },
 | 
									}
 | 
				
			||||||
        },
 | 
								]
 | 
				
			||||||
      ],
 | 
							],
 | 
				
			||||||
    ],
 | 
							generatorOpts: {
 | 
				
			||||||
    generatorOpts: {
 | 
								comments: false,
 | 
				
			||||||
      comments: false,
 | 
								minified: false,
 | 
				
			||||||
      minified: false,
 | 
								sourceMaps: false,
 | 
				
			||||||
      sourceMaps: false,
 | 
							}
 | 
				
			||||||
    },
 | 
						})
 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const filenameWithQuery = `${targetPath}?static=${hash(compiled.code)}`;
 | 
						const filenameWithQuery = `${targetPath}?static=${hash(compiled.code)}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static.set(sourcePath, filenameWithQuery);
 | 
						static.set(sourcePath, filenameWithQuery)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await Promise.all([
 | 
						await Promise.all([
 | 
				
			||||||
    fs.writeFile(pj(buildDir, targetPath), originalCode),
 | 
							fs.writeFile(pj(buildDir, targetPath), originalCode),
 | 
				
			||||||
    fs.writeFile(pj(buildDir, minFilename), compiled.code),
 | 
							fs.writeFile(pj(buildDir, minFilename), compiled.code),
 | 
				
			||||||
    fs.writeFile(pj(buildDir, mapFilename), JSON.stringify(compiled.map)),
 | 
							fs.writeFile(pj(buildDir, mapFilename), JSON.stringify(compiled.map))
 | 
				
			||||||
  ]);
 | 
						])
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
(async () => {
 | 
					;(async () => {
 | 
				
			||||||
  // Stage 1: Register
 | 
						// Stage 1: Register
 | 
				
			||||||
  for (const item of spec) {
 | 
						for (const item of spec) {
 | 
				
			||||||
    if (item.type === "pug") {
 | 
							if (item.type === "pug") {
 | 
				
			||||||
      links.set(item.source, item.target.replace(/index.html$/, ""));
 | 
								links.set(item.source, item.target.replace(/index.html$/, ""))
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Stage 2: Build
 | 
						// Stage 2: Build
 | 
				
			||||||
  for (const item of spec) {
 | 
						for (const item of spec) {
 | 
				
			||||||
    if (item.type === "file") {
 | 
							if (item.type === "file") {
 | 
				
			||||||
      await addFile(item.source, item.target);
 | 
								await addFile(item.source, item.target)
 | 
				
			||||||
    } else if (item.type === "js") {
 | 
							} else if (item.type === "js") {
 | 
				
			||||||
      await addJS(item.source, item.target);
 | 
								await addJS(item.source, item.target)
 | 
				
			||||||
    } else if (item.type === "sass") {
 | 
							} else if (item.type === "sass") {
 | 
				
			||||||
      await addSass(item.source, item.target);
 | 
								await addSass(item.source, item.target)
 | 
				
			||||||
    } else if (item.type === "babel") {
 | 
							} else if (item.type === "babel") {
 | 
				
			||||||
      await addBabel(item.source, item.target);
 | 
								await addBabel(item.source, item.target)
 | 
				
			||||||
    } else if (item.type === "pug") {
 | 
							} else if (item.type === "pug") {
 | 
				
			||||||
      await addPug(item.source, item.target);
 | 
								await addPug(item.source, item.target)
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      throw new Error("Unknown item type: " + item.type);
 | 
								throw new Error("Unknown item type: "+item.type)
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log(chalk.green("All files emitted."));
 | 
						console.log(chalk.green("All files emitted."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await Promise.all(validationQueue).then((v) => {
 | 
						await Promise.all(validationQueue).then(v => {
 | 
				
			||||||
    console.log(`validation: using host ${chalk.cyan(validationHost)}`);
 | 
							console.log(`validation: using host ${chalk.cyan(validationHost)}`)
 | 
				
			||||||
    v.forEach((cont) => cont());
 | 
							v.forEach(cont => cont())
 | 
				
			||||||
  });
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log(chalk.green("Build complete.") + "\n\n------------\n");
 | 
						console.log(chalk.green("Build complete.") + "\n\n------------\n")
 | 
				
			||||||
})();
 | 
					})()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  "compilerOptions": {
 | 
						"compilerOptions": {
 | 
				
			||||||
    "target": "esnext",
 | 
							"target": "esnext",
 | 
				
			||||||
    "module": "esnext",
 | 
							"module": "esnext",
 | 
				
			||||||
    "checkJs": true,
 | 
							"checkJs": true,
 | 
				
			||||||
    "moduleResolution": "node",
 | 
							"moduleResolution": "node",
 | 
				
			||||||
    "allowSyntheticDefaultImports": true
 | 
							"allowSyntheticDefaultImports": true
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  "name": "carbon",
 | 
					  "name": "cosc212-assignment-1",
 | 
				
			||||||
  "version": "1.0.0",
 | 
					  "version": "1.0.0",
 | 
				
			||||||
  "lockfileVersion": 1,
 | 
					  "lockfileVersion": 1,
 | 
				
			||||||
  "requires": true,
 | 
					  "requires": true,
 | 
				
			||||||
| 
						 | 
					@ -1095,15 +1095,6 @@
 | 
				
			||||||
        "to-fast-properties": "^2.0.0"
 | 
					        "to-fast-properties": "^2.0.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@prettier/plugin-pug": {
 | 
					 | 
				
			||||||
      "version": "1.9.0",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@prettier/plugin-pug/-/plugin-pug-1.9.0.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-doLga3EPMPiUgO98aUWXoq8YuPLIwUWX0YbwqnSg2URQ7hKGjxlyEeVlAmrERVI3mm9zbwpEEZ02jw0ROd+5+g==",
 | 
					 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "requires": {
 | 
					 | 
				
			||||||
        "pug-lexer": "^5.0.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "@types/color-name": {
 | 
					    "@types/color-name": {
 | 
				
			||||||
      "version": "1.1.1",
 | 
					      "version": "1.1.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
 | 
				
			||||||
| 
						 | 
					@ -1913,12 +1904,6 @@
 | 
				
			||||||
        "mkdirp": "^0.5.5"
 | 
					        "mkdirp": "^0.5.5"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "prettier": {
 | 
					 | 
				
			||||||
      "version": "2.1.2",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
 | 
					 | 
				
			||||||
      "dev": true
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "promise": {
 | 
					    "promise": {
 | 
				
			||||||
      "version": "7.3.1",
 | 
					      "version": "7.3.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,12 +15,10 @@
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@babel/core": "^7.11.1",
 | 
					    "@babel/core": "^7.11.1",
 | 
				
			||||||
    "@babel/preset-env": "^7.11.0",
 | 
					    "@babel/preset-env": "^7.11.0",
 | 
				
			||||||
    "@prettier/plugin-pug": "^1.9.0",
 | 
					 | 
				
			||||||
    "chalk": "^4.1.0",
 | 
					    "chalk": "^4.1.0",
 | 
				
			||||||
    "http-server": "^0.12.3",
 | 
					    "http-server": "^0.12.3",
 | 
				
			||||||
    "jshint": "^2.12.0",
 | 
					    "jshint": "^2.12.0",
 | 
				
			||||||
    "node-fetch": "^2.6.0",
 | 
					    "node-fetch": "^2.6.0",
 | 
				
			||||||
    "prettier": "^2.1.2",
 | 
					 | 
				
			||||||
    "pug": "^3.0.0",
 | 
					    "pug": "^3.0.0",
 | 
				
			||||||
    "sass": "^1.26.10"
 | 
					    "sass": "^1.26.10"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										232
									
								
								spec.js
									
										
									
									
									
								
							
							
						
						
									
										232
									
								
								spec.js
									
										
									
									
									
								
							| 
						 | 
					@ -1,117 +1,117 @@
 | 
				
			||||||
module.exports = [
 | 
					module.exports = [
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "file",
 | 
							type: "file",
 | 
				
			||||||
    source: "/assets/fonts/whitney-500.woff",
 | 
							source: "/assets/fonts/whitney-500.woff",
 | 
				
			||||||
    target: "/static/whitney-500.woff",
 | 
							target: "/static/whitney-500.woff"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "file",
 | 
							type: "file",
 | 
				
			||||||
    source: "/assets/fonts/whitney-400.woff",
 | 
							source: "/assets/fonts/whitney-400.woff",
 | 
				
			||||||
    target: "/static/whitney-400.woff",
 | 
							target: "/static/whitney-400.woff"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/basic.js",
 | 
							source: "/js/basic.js",
 | 
				
			||||||
    target: "/static/basic.js",
 | 
							target: "/static/basic.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/groups.js",
 | 
							source: "/js/groups.js",
 | 
				
			||||||
    target: "/static/groups.js",
 | 
							target: "/static/groups.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/chat-input.js",
 | 
							source: "/js/chat-input.js",
 | 
				
			||||||
    target: "/static/chat-input.js",
 | 
							target: "/static/chat-input.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/room-picker.js",
 | 
							source: "/js/room-picker.js",
 | 
				
			||||||
    target: "/static/room-picker.js",
 | 
							target: "/static/room-picker.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/store/store.js",
 | 
							source: "/js/store/store.js",
 | 
				
			||||||
    target: "/static/store/store.js",
 | 
							target: "/static/store/store.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/store/Subscribable.js",
 | 
							source: "/js/store/Subscribable.js",
 | 
				
			||||||
    target: "/static/store/Subscribable.js",
 | 
							target: "/static/store/Subscribable.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/store/SubscribeValue.js",
 | 
							source: "/js/store/SubscribeValue.js",
 | 
				
			||||||
    target: "/static/store/SubscribeValue.js",
 | 
							target: "/static/store/SubscribeValue.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/store/SubscribeMapList.js",
 | 
							source: "/js/store/SubscribeMapList.js",
 | 
				
			||||||
    target: "/static/store/SubscribeMapList.js",
 | 
							target: "/static/store/SubscribeMapList.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/store/SubscribeSet.js",
 | 
							source: "/js/store/SubscribeSet.js",
 | 
				
			||||||
    target: "/static/store/SubscribeSet.js",
 | 
							target: "/static/store/SubscribeSet.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/sync/sync.js",
 | 
							source: "/js/sync/sync.js",
 | 
				
			||||||
    target: "/static/sync/sync.js",
 | 
							target: "/static/sync/sync.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/lsm.js",
 | 
							source: "/js/lsm.js",
 | 
				
			||||||
    target: "/static/lsm.js",
 | 
							target: "/static/lsm.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/Timeline.js",
 | 
							source: "/js/Timeline.js",
 | 
				
			||||||
    target: "/static/Timeline.js",
 | 
							target: "/static/Timeline.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/Anchor.js",
 | 
							source: "/js/Anchor.js",
 | 
				
			||||||
    target: "/static/Anchor.js",
 | 
							target: "/static/Anchor.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "js",
 | 
							type: "js",
 | 
				
			||||||
    source: "/js/chat.js",
 | 
							source: "/js/chat.js",
 | 
				
			||||||
    target: "/static/chat.js",
 | 
							target: "/static/chat.js"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "file",
 | 
							type: "file",
 | 
				
			||||||
    source: "/assets/fonts/whitney-500.woff",
 | 
							source: "/assets/fonts/whitney-500.woff",
 | 
				
			||||||
    target: "/static/whitney-500.woff",
 | 
							target: "/static/whitney-500.woff"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "file",
 | 
							type: "file",
 | 
				
			||||||
    source: "/assets/icons/directs.svg",
 | 
							source: "/assets/icons/directs.svg",
 | 
				
			||||||
    target: "/static/directs.svg",
 | 
							target: "/static/directs.svg"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "file",
 | 
							type: "file",
 | 
				
			||||||
    source: "/assets/icons/channels.svg",
 | 
							source: "/assets/icons/channels.svg",
 | 
				
			||||||
    target: "/static/channels.svg",
 | 
							target: "/static/channels.svg"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "file",
 | 
							type: "file",
 | 
				
			||||||
    source: "/assets/icons/join-event.svg",
 | 
							source: "/assets/icons/join-event.svg",
 | 
				
			||||||
    target: "/static/join-event.svg",
 | 
							target: "/static/join-event.svg"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "sass",
 | 
							type: "sass",
 | 
				
			||||||
    source: "/sass/main.sass",
 | 
							source: "/sass/main.sass",
 | 
				
			||||||
    target: "/static/main.css",
 | 
							target: "/static/main.css"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "pug",
 | 
							type: "pug",
 | 
				
			||||||
    source: "/home.pug",
 | 
							source: "/home.pug",
 | 
				
			||||||
    target: "/index.html",
 | 
							target: "/index.html"
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    type: "pug",
 | 
							type: "pug",
 | 
				
			||||||
    source: "/login.pug",
 | 
							source: "/login.pug",
 | 
				
			||||||
    target: "/login.html",
 | 
							target: "/login.html"
 | 
				
			||||||
  },
 | 
						}
 | 
				
			||||||
];
 | 
					]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										31
									
								
								src/home.pug
									
										
									
									
									
								
							
							
						
						
									
										31
									
								
								src/home.pug
									
										
									
									
									
								
							| 
						 | 
					@ -26,32 +26,29 @@ mixin message-notice(content)
 | 
				
			||||||
mixin message-event(icon, content)
 | 
					mixin message-event(icon, content)
 | 
				
			||||||
  .c-message-event
 | 
					  .c-message-event
 | 
				
			||||||
    .c-message-event__inner
 | 
					    .c-message-event__inner
 | 
				
			||||||
      img.c-message-event__icon(src=icon, alt="")
 | 
					      img(src=icon alt="").c-message-event__icon
 | 
				
			||||||
      = content
 | 
					      = content
 | 
				
			||||||
 | 
					
 | 
				
			||||||
doctype html
 | 
					doctype html
 | 
				
			||||||
html
 | 
					html
 | 
				
			||||||
  head
 | 
					  head
 | 
				
			||||||
    meta(charset="utf-8")
 | 
					    meta(charset="utf-8")
 | 
				
			||||||
    link(rel="stylesheet", type="text/css", href=getStatic('/sass/main.sass'))
 | 
					    link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass"))
 | 
				
			||||||
    script(type="module", src=getStatic('/js/groups.js'))
 | 
					    script(type="module" src=getStatic("/js/groups.js"))
 | 
				
			||||||
    script(type="module", src=getStatic('/js/chat-input.js'))
 | 
					    script(type="module" src=getStatic("/js/chat-input.js"))
 | 
				
			||||||
    script(type="module", src=getStatic('/js/room-picker.js'))
 | 
					    script(type="module" src=getStatic("/js/room-picker.js"))
 | 
				
			||||||
    script(type="module", src=getStatic('/js/sync/sync.js'))
 | 
					    script(type="module" src=getStatic("/js/sync/sync.js"))
 | 
				
			||||||
    script(type="module", src=getStatic('/js/chat.js'))
 | 
					    script(type="module" src=getStatic("/js/chat.js"))
 | 
				
			||||||
    title Carbon
 | 
					    title Carbon
 | 
				
			||||||
  body
 | 
					  body
 | 
				
			||||||
    main.main
 | 
					    main.main
 | 
				
			||||||
      .c-groups
 | 
					      .c-groups
 | 
				
			||||||
        #c-groups-display.c-groups__display
 | 
					        .c-groups__display#c-groups-display
 | 
				
			||||||
          #c-group-marker.c-group-marker
 | 
					          .c-group-marker#c-group-marker
 | 
				
			||||||
          #c-groups-list.c-groups__container
 | 
					          .c-groups__container#c-groups-list
 | 
				
			||||||
      #c-rooms.c-rooms
 | 
					      .c-rooms#c-rooms
 | 
				
			||||||
      .c-chat
 | 
					      .c-chat
 | 
				
			||||||
        #c-chat-messages.c-chat__messages
 | 
					        .c-chat__messages#c-chat-messages
 | 
				
			||||||
          #c-chat.c-chat__inner
 | 
					          .c-chat__inner#c-chat
 | 
				
			||||||
        .c-chat-input
 | 
					        .c-chat-input
 | 
				
			||||||
          textarea#c-chat-textarea.c-chat-input__textarea(
 | 
					          textarea(placeholder="Send a message..." autocomplete="off").c-chat-input__textarea#c-chat-textarea
 | 
				
			||||||
            placeholder="Send a message...",
 | 
					 | 
				
			||||||
            autocomplete="off"
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,15 @@
 | 
				
			||||||
import { ElemJS } from "./basic.js";
 | 
					import {ElemJS} from "./basic.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Anchor extends ElemJS {
 | 
					class Anchor extends ElemJS {
 | 
				
			||||||
  constructor() {
 | 
						constructor() {
 | 
				
			||||||
    super("div");
 | 
							super("div")
 | 
				
			||||||
    this.class("c-anchor");
 | 
							this.class("c-anchor")
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  scroll() {
 | 
						scroll() {
 | 
				
			||||||
    // console.log("anchor scrolled")
 | 
							// console.log("anchor scrolled")
 | 
				
			||||||
    this.element.scrollIntoView({ block: "start" });
 | 
							this.element.scrollIntoView({block: "start"})
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { Anchor };
 | 
					export {Anchor}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,171 +1,148 @@
 | 
				
			||||||
import { ElemJS, ejs } from "./basic.js";
 | 
					import {ElemJS, ejs} from "./basic.js"
 | 
				
			||||||
import { Subscribable } from "./store/Subscribable.js";
 | 
					import {Subscribable} from "./store/Subscribable.js"
 | 
				
			||||||
import { Anchor } from "./Anchor.js";
 | 
					import {Anchor} from "./Anchor.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function eventSearch(list, event, min = 0, max = -1) {
 | 
					function eventSearch(list, event, min = 0, max = -1) {
 | 
				
			||||||
  if (list.length === 0) return { success: false, i: 0 };
 | 
						if (list.length === 0) return {success: false, i: 0}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (max === -1) max = list.length - 1;
 | 
						if (max === -1) max = list.length - 1
 | 
				
			||||||
  let mid = Math.floor((max + min) / 2);
 | 
						let mid = Math.floor((max + min) / 2)
 | 
				
			||||||
  // success condition
 | 
						// success condition
 | 
				
			||||||
  if (list[mid] && list[mid].data.event_id === event.data.event_id)
 | 
						if (list[mid] && list[mid].data.event_id === event.data.event_id) return {success: true, i: mid}
 | 
				
			||||||
    return { success: true, i: mid };
 | 
						// failed condition
 | 
				
			||||||
  // failed condition
 | 
						if (min >= max) {
 | 
				
			||||||
  if (min >= max) {
 | 
							while (mid !== -1 && (!list[mid] || list[mid].data.origin_server_ts > event.data.origin_server_ts)) mid--
 | 
				
			||||||
    while (
 | 
							return {
 | 
				
			||||||
      mid !== -1 &&
 | 
								success: false,
 | 
				
			||||||
      (!list[mid] ||
 | 
								i: mid + 1
 | 
				
			||||||
        list[mid].data.origin_server_ts > event.data.origin_server_ts)
 | 
							}
 | 
				
			||||||
    )
 | 
						}
 | 
				
			||||||
      mid--;
 | 
						// recurse (below)
 | 
				
			||||||
    return {
 | 
						if (list[mid].data.origin_server_ts > event.data.origin_server_ts) return eventSearch(list, event, min, mid-1)
 | 
				
			||||||
      success: false,
 | 
						// recurse (above)
 | 
				
			||||||
      i: mid + 1,
 | 
						else return eventSearch(list, event, mid+1, max)
 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  // recurse (below)
 | 
					 | 
				
			||||||
  if (list[mid].data.origin_server_ts > event.data.origin_server_ts)
 | 
					 | 
				
			||||||
    return eventSearch(list, event, min, mid - 1);
 | 
					 | 
				
			||||||
  // recurse (above)
 | 
					 | 
				
			||||||
  else return eventSearch(list, event, mid + 1, max);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Event extends ElemJS {
 | 
					class Event extends ElemJS {
 | 
				
			||||||
  constructor(data) {
 | 
						constructor(data) {
 | 
				
			||||||
    super("div");
 | 
							super("div")
 | 
				
			||||||
    this.class("c-message");
 | 
							this.class("c-message")
 | 
				
			||||||
    this.data = null;
 | 
							this.data = null
 | 
				
			||||||
    this.update(data);
 | 
							this.update(data)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update(data) {
 | 
						update(data) {
 | 
				
			||||||
    this.data = data;
 | 
							this.data = data
 | 
				
			||||||
    this.render();
 | 
							this.render()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
						render() {
 | 
				
			||||||
    this.child(this.data.content.body);
 | 
							this.child(this.data.content.body)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EventGroup extends ElemJS {
 | 
					class EventGroup extends ElemJS {
 | 
				
			||||||
  constructor(list) {
 | 
						constructor(list) {
 | 
				
			||||||
    super("div");
 | 
							super("div")
 | 
				
			||||||
    this.class("c-message-group");
 | 
							this.class("c-message-group")
 | 
				
			||||||
    this.list = list;
 | 
							this.list = list
 | 
				
			||||||
    this.data = {
 | 
							this.data = {
 | 
				
			||||||
      sender: list[0].data.sender,
 | 
								sender: list[0].data.sender,
 | 
				
			||||||
      origin_server_ts: list[0].data.origin_server_ts,
 | 
								origin_server_ts: list[0].data.origin_server_ts
 | 
				
			||||||
    };
 | 
							}
 | 
				
			||||||
    this.child(
 | 
							this.child(
 | 
				
			||||||
      ejs("div")
 | 
								ejs("div").class("c-message-group__avatar").child(
 | 
				
			||||||
        .class("c-message-group__avatar")
 | 
									ejs("div").class("c-message-group__icon")
 | 
				
			||||||
        .child(ejs("div").class("c-message-group__icon")),
 | 
								),
 | 
				
			||||||
      (this.messages = ejs("div")
 | 
								this.messages = ejs("div").class("c-message-group__messages").child(
 | 
				
			||||||
        .class("c-message-group__messages")
 | 
									ejs("div").class("c-message-group__intro").child(
 | 
				
			||||||
        .child(
 | 
										ejs("div").class("c-message-group__name").text(this.data.sender),
 | 
				
			||||||
          ejs("div")
 | 
										ejs("div").class("c-message-group__date").text(this.data.origin_server_ts)
 | 
				
			||||||
            .class("c-message-group__intro")
 | 
									),
 | 
				
			||||||
            .child(
 | 
									...this.list
 | 
				
			||||||
              ejs("div").class("c-message-group__name").text(this.data.sender),
 | 
								)
 | 
				
			||||||
              ejs("div")
 | 
							)
 | 
				
			||||||
                .class("c-message-group__date")
 | 
						}
 | 
				
			||||||
                .text(this.data.origin_server_ts)
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ...this.list
 | 
					 | 
				
			||||||
        ))
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addEvent(event) {
 | 
						addEvent(event) {
 | 
				
			||||||
    const index = eventSearch(this.list, event).i;
 | 
							const index = eventSearch(this.list, event).i
 | 
				
			||||||
    this.list.splice(index, 0, event);
 | 
							this.list.splice(index, 0, event)
 | 
				
			||||||
    this.messages.childAt(index + 1, event);
 | 
							this.messages.childAt(index + 1, event)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReactiveTimeline extends ElemJS {
 | 
					class ReactiveTimeline extends ElemJS {
 | 
				
			||||||
  constructor(list) {
 | 
						constructor(list) {
 | 
				
			||||||
    super("div");
 | 
							super("div")
 | 
				
			||||||
    this.class("c-event-groups");
 | 
							this.class("c-event-groups")
 | 
				
			||||||
    this.list = list;
 | 
							this.list = list
 | 
				
			||||||
    this.render();
 | 
							this.render()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addEvent(event) {
 | 
						addEvent(event) {
 | 
				
			||||||
    const search = eventSearch(this.list, event);
 | 
							const search = eventSearch(this.list, event)
 | 
				
			||||||
    // console.log(search, this.list.map(l => l.data.sender), event.data)
 | 
							// console.log(search, this.list.map(l => l.data.sender), event.data)
 | 
				
			||||||
    if (!search.success && search.i >= 1)
 | 
							if (!search.success && search.i >= 1) this.tryAddGroups(event, [search.i-1, search.i])
 | 
				
			||||||
      this.tryAddGroups(event, [search.i - 1, search.i]);
 | 
							else this.tryAddGroups(event, [search.i])
 | 
				
			||||||
    else this.tryAddGroups(event, [search.i]);
 | 
						}
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  tryAddGroups(event, indices) {
 | 
						tryAddGroups(event, indices) {
 | 
				
			||||||
    const success = indices.some((i) => {
 | 
							const success = indices.some(i => {
 | 
				
			||||||
      if (!this.list[i]) {
 | 
								if (!this.list[i]) {
 | 
				
			||||||
        // if (printed++ < 100) console.log("tryadd success, created group")
 | 
									// if (printed++ < 100) console.log("tryadd success, created group")
 | 
				
			||||||
        const group = new EventGroup([event]);
 | 
									const group = new EventGroup([event])
 | 
				
			||||||
        this.list.splice(i, 0, group);
 | 
									this.list.splice(i, 0, group)
 | 
				
			||||||
        this.childAt(i, group);
 | 
									this.childAt(i, group)
 | 
				
			||||||
        return true;
 | 
									return true
 | 
				
			||||||
      } else if (
 | 
								} else if (this.list[i] && this.list[i].data.sender === event.data.sender) {
 | 
				
			||||||
        this.list[i] &&
 | 
									// if (printed++ < 100) console.log("tryadd success, using existing group")
 | 
				
			||||||
        this.list[i].data.sender === event.data.sender
 | 
									this.list[i].addEvent(event)
 | 
				
			||||||
      ) {
 | 
									return true
 | 
				
			||||||
        // if (printed++ < 100) console.log("tryadd success, using existing group")
 | 
								}
 | 
				
			||||||
        this.list[i].addEvent(event);
 | 
							})
 | 
				
			||||||
        return true;
 | 
							if (!success) console.log("tryadd failure", indices, this.list.map(l => l.data.sender), event.data)
 | 
				
			||||||
      }
 | 
						}
 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    if (!success)
 | 
					 | 
				
			||||||
      console.log(
 | 
					 | 
				
			||||||
        "tryadd failure",
 | 
					 | 
				
			||||||
        indices,
 | 
					 | 
				
			||||||
        this.list.map((l) => l.data.sender),
 | 
					 | 
				
			||||||
        event.data
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
						render() {
 | 
				
			||||||
    this.clearChildren();
 | 
							this.clearChildren()
 | 
				
			||||||
    this.list.forEach((group) => this.child(group));
 | 
							this.list.forEach(group => this.child(group))
 | 
				
			||||||
    this.anchor = new Anchor();
 | 
							this.anchor = new Anchor()
 | 
				
			||||||
    this.child(this.anchor);
 | 
							this.child(this.anchor)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Timeline extends Subscribable {
 | 
					class Timeline extends Subscribable {
 | 
				
			||||||
  constructor() {
 | 
						constructor() {
 | 
				
			||||||
    super();
 | 
							super()
 | 
				
			||||||
    Object.assign(this.events, {
 | 
							Object.assign(this.events, {
 | 
				
			||||||
      beforeChange: [],
 | 
								beforeChange: []
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
    Object.assign(this.eventDeps, {
 | 
							Object.assign(this.eventDeps, {
 | 
				
			||||||
      beforeChange: [],
 | 
								beforeChange: []
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
    this.list = [];
 | 
							this.list = []
 | 
				
			||||||
    this.map = new Map();
 | 
							this.map = new Map()
 | 
				
			||||||
    this.reactiveTimeline = new ReactiveTimeline([]);
 | 
							this.reactiveTimeline = new ReactiveTimeline([])
 | 
				
			||||||
    this.latest = 0;
 | 
							this.latest = 0
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateEvents(events) {
 | 
						updateEvents(events) {
 | 
				
			||||||
    this.broadcast("beforeChange");
 | 
							this.broadcast("beforeChange")
 | 
				
			||||||
    for (const eventData of events) {
 | 
							for (const eventData of events) {
 | 
				
			||||||
      this.latest = Math.max(this.latest, eventData.origin_server_ts);
 | 
								this.latest = Math.max(this.latest, eventData.origin_server_ts)
 | 
				
			||||||
      if (this.map.has(eventData.event_id)) {
 | 
								if (this.map.has(eventData.event_id)) {
 | 
				
			||||||
        this.map.get(eventData.event_id).update(eventData);
 | 
									this.map.get(eventData.event_id).update(eventData)
 | 
				
			||||||
      } else {
 | 
								} else {
 | 
				
			||||||
        const event = new Event(eventData);
 | 
									const event = new Event(eventData)
 | 
				
			||||||
        this.reactiveTimeline.addEvent(event);
 | 
									this.reactiveTimeline.addEvent(event)
 | 
				
			||||||
      }
 | 
								}
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getTimeline() {
 | 
						getTimeline() {
 | 
				
			||||||
    return this.reactiveTimeline;
 | 
							return this.reactiveTimeline
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
  /*
 | 
					/*
 | 
				
			||||||
	getGroupedEvents() {
 | 
						getGroupedEvents() {
 | 
				
			||||||
		let currentSender = Symbol("N/A")
 | 
							let currentSender = Symbol("N/A")
 | 
				
			||||||
		let groups = []
 | 
							let groups = []
 | 
				
			||||||
| 
						 | 
					@ -185,4 +162,4 @@ class Timeline extends Subscribable {
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { Timeline };
 | 
					export {Timeline}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										246
									
								
								src/js/basic.js
									
										
									
									
									
								
							
							
						
						
									
										246
									
								
								src/js/basic.js
									
										
									
									
									
								
							| 
						 | 
					@ -3,13 +3,13 @@
 | 
				
			||||||
 * @template {HTMLElement} T
 | 
					 * @template {HTMLElement} T
 | 
				
			||||||
 * @returns {T}
 | 
					 * @returns {T}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const q = (s) => document.querySelector(s);
 | 
					const q = s => document.querySelector(s);
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Shortcut for querySelectorAll.
 | 
					 * Shortcut for querySelectorAll.
 | 
				
			||||||
 * @template {HTMLElement} T
 | 
					 * @template {HTMLElement} T
 | 
				
			||||||
 * @returns {T[]}
 | 
					 * @returns {T[]}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const qa = (s) => document.querySelectorAll(s);
 | 
					const qa = s => document.querySelectorAll(s);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * An easier, chainable, object-oriented way to create and update elements
 | 
					 * An easier, chainable, object-oriented way to create and update elements
 | 
				
			||||||
| 
						 | 
					@ -18,147 +18,143 @@ const qa = (s) => document.querySelectorAll(s);
 | 
				
			||||||
 * Created by Cadence Ember in 2018.
 | 
					 * Created by Cadence Ember in 2018.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class ElemJS {
 | 
					class ElemJS {
 | 
				
			||||||
  constructor(type) {
 | 
						constructor(type) {
 | 
				
			||||||
    if (type instanceof HTMLElement) {
 | 
							if (type instanceof HTMLElement) {
 | 
				
			||||||
      // If passed an existing element, bind to it
 | 
								// If passed an existing element, bind to it
 | 
				
			||||||
      this.bind(type);
 | 
								this.bind(type);
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      // Otherwise, create a new detached element to bind to
 | 
								// Otherwise, create a new detached element to bind to
 | 
				
			||||||
      this.bind(document.createElement(type));
 | 
								this.bind(document.createElement(type));
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
    this.children = [];
 | 
							this.children = [];
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Bind this construct to an existing element on the page. */
 | 
						/** Bind this construct to an existing element on the page. */
 | 
				
			||||||
  bind(element) {
 | 
						bind(element) {
 | 
				
			||||||
    this.element = element;
 | 
							this.element = element;
 | 
				
			||||||
    this.element.js = this;
 | 
							this.element.js = this;
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Add a class. */
 | 
						/** Add a class. */
 | 
				
			||||||
  class() {
 | 
						class() {
 | 
				
			||||||
    for (let name of arguments) if (name) this.element.classList.add(name);
 | 
							for (let name of arguments) if (name) this.element.classList.add(name);
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Remove a class. */
 | 
						/** Remove a class. */
 | 
				
			||||||
  removeClass() {
 | 
						removeClass() {
 | 
				
			||||||
    for (let name of arguments) if (name) this.element.classList.remove(name);
 | 
							for (let name of arguments) if (name) this.element.classList.remove(name);
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Set a JS property on the element. */
 | 
						/** Set a JS property on the element. */
 | 
				
			||||||
  direct(name, value) {
 | 
						direct(name, value) {
 | 
				
			||||||
    if (name) this.element[name] = value;
 | 
							if (name) this.element[name] = value;
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Set an attribute on the element. */
 | 
						/** Set an attribute on the element. */
 | 
				
			||||||
  attribute(name, value) {
 | 
						attribute(name, value) {
 | 
				
			||||||
    if (name) this.element.setAttribute(name, value != undefined ? value : "");
 | 
							if (name) this.element.setAttribute(name, value != undefined ? value : "");
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Set a style on the element. */
 | 
						/** Set a style on the element. */
 | 
				
			||||||
  style(name, value) {
 | 
						style(name, value) {
 | 
				
			||||||
    if (name) this.element.style[name] = value;
 | 
							if (name) this.element.style[name] = value;
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Set the element's ID. */
 | 
						/** Set the element's ID. */
 | 
				
			||||||
  id(name) {
 | 
						id(name) {
 | 
				
			||||||
    if (name) this.element.id = name;
 | 
							if (name) this.element.id = name;
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Attach a callback function to an event on the element. */
 | 
						/** Attach a callback function to an event on the element. */
 | 
				
			||||||
  on(name, callback) {
 | 
						on(name, callback) {
 | 
				
			||||||
    this.element.addEventListener(name, callback);
 | 
							this.element.addEventListener(name, callback);
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Set the element's text. */
 | 
						/** Set the element's text. */
 | 
				
			||||||
  text(name) {
 | 
						text(name) {
 | 
				
			||||||
    this.element.innerText = name;
 | 
							this.element.innerText = name;
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Create a text node and add it to the element. */
 | 
						/** Create a text node and add it to the element. */
 | 
				
			||||||
  addText(name) {
 | 
						addText(name) {
 | 
				
			||||||
    const node = document.createTextNode(name);
 | 
							const node = document.createTextNode(name);
 | 
				
			||||||
    this.element.appendChild(node);
 | 
							this.element.appendChild(node);
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Set the element's HTML content. */
 | 
						/** Set the element's HTML content. */
 | 
				
			||||||
  html(name) {
 | 
						html(name) {
 | 
				
			||||||
    this.element.innerHTML = name;
 | 
							this.element.innerHTML = name;
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
						/**
 | 
				
			||||||
   * Add children to the element.
 | 
						 * Add children to the element.
 | 
				
			||||||
   * Children can either be an instance of ElemJS, in
 | 
						 * Children can either be an instance of ElemJS, in
 | 
				
			||||||
   * which case the element will be appended as a child,
 | 
						 * which case the element will be appended as a child,
 | 
				
			||||||
   * or a string, in which case the string will be added as a text node.
 | 
						 * or a string, in which case the string will be added as a text node.
 | 
				
			||||||
   * Each child should be a parameter to this method.
 | 
						 * Each child should be a parameter to this method.
 | 
				
			||||||
   */
 | 
						 */
 | 
				
			||||||
  child(...children) {
 | 
						child(...children) {
 | 
				
			||||||
    for (const toAdd of children) {
 | 
							for (const toAdd of children) {
 | 
				
			||||||
      if (typeof toAdd === "object" && toAdd !== null) {
 | 
								if (typeof toAdd === "object" && toAdd !== null) {
 | 
				
			||||||
        // Should be an instance of ElemJS, so append as child
 | 
									// Should be an instance of ElemJS, so append as child
 | 
				
			||||||
        toAdd.parent = this;
 | 
									toAdd.parent = this;
 | 
				
			||||||
        this.element.appendChild(toAdd.element);
 | 
									this.element.appendChild(toAdd.element);
 | 
				
			||||||
        this.children.push(toAdd);
 | 
									this.children.push(toAdd);
 | 
				
			||||||
      } else if (typeof toAdd === "string") {
 | 
								} else if (typeof toAdd === "string") {
 | 
				
			||||||
        // Is a string, so add as text node
 | 
									// Is a string, so add as text node
 | 
				
			||||||
        this.addText(toAdd);
 | 
									this.addText(toAdd);
 | 
				
			||||||
      }
 | 
								}
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
    return this;
 | 
							return this;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  childAt(index, toAdd) {
 | 
						childAt(index, toAdd) {
 | 
				
			||||||
    if (typeof toAdd === "object" && toAdd !== null) {
 | 
							if (typeof toAdd === "object" && toAdd !== null) {
 | 
				
			||||||
      toAdd.parent = this;
 | 
								toAdd.parent = this;
 | 
				
			||||||
      this.children.splice(index, 0, toAdd);
 | 
								this.children.splice(index, 0, toAdd);
 | 
				
			||||||
      if (index >= this.element.childNodes.length) {
 | 
								if (index >= this.element.childNodes.length) {
 | 
				
			||||||
        this.element.appendChild(toAdd.element);
 | 
									this.element.appendChild(toAdd.element)
 | 
				
			||||||
      } else {
 | 
								} else {
 | 
				
			||||||
        this.element.childNodes[index].insertAdjacentElement(
 | 
									this.element.childNodes[index].insertAdjacentElement("beforebegin", toAdd.element)
 | 
				
			||||||
          "beforebegin",
 | 
								}
 | 
				
			||||||
          toAdd.element
 | 
							}
 | 
				
			||||||
        );
 | 
						}
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
						/**
 | 
				
			||||||
   * Remove all children from the element.
 | 
						 * Remove all children from the element.
 | 
				
			||||||
   */
 | 
						 */
 | 
				
			||||||
  clearChildren() {
 | 
						clearChildren() {
 | 
				
			||||||
    this.children.length = 0;
 | 
							this.children.length = 0;
 | 
				
			||||||
    while (this.element.lastChild)
 | 
							while (this.element.lastChild) this.element.removeChild(this.element.lastChild);
 | 
				
			||||||
      this.element.removeChild(this.element.lastChild);
 | 
						}
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
						/**
 | 
				
			||||||
   * Remove this element.
 | 
						 * Remove this element.
 | 
				
			||||||
   */
 | 
						 */
 | 
				
			||||||
  remove() {
 | 
						remove() {
 | 
				
			||||||
    let index;
 | 
							let index;
 | 
				
			||||||
    if (this.parent && (index = this.parent.children.indexOf(this)) !== -1) {
 | 
							if (this.parent && (index = this.parent.children.indexOf(this)) !== -1) {
 | 
				
			||||||
      this.parent.children.splice(index, 1);
 | 
								this.parent.children.splice(index, 1);
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
    this.parent = null;
 | 
							this.parent = null;
 | 
				
			||||||
    this.element.remove();
 | 
							this.element.remove();
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Shortcut for `new ElemJS`. */
 | 
					/** Shortcut for `new ElemJS`. */
 | 
				
			||||||
function ejs(tag) {
 | 
					function ejs(tag) {
 | 
				
			||||||
  return new ElemJS(tag);
 | 
						return new ElemJS(tag);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { q, qa, ElemJS, ejs };
 | 
					export {q, qa, ElemJS, ejs}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,60 +1,53 @@
 | 
				
			||||||
import { q } from "./basic.js";
 | 
					import {q} from "./basic.js"
 | 
				
			||||||
import { store } from "./store/store.js";
 | 
					import {store} from "./store/store.js"
 | 
				
			||||||
import * as lsm from "./lsm.js";
 | 
					import * as lsm from "./lsm.js"
 | 
				
			||||||
import { chat } from "./chat.js";
 | 
					import {chat} from "./chat.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let sentIndex = 0;
 | 
					let sentIndex = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const input = q("#c-chat-textarea");
 | 
					const input = q("#c-chat-textarea")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
store.activeRoom.subscribe("changeSelf", () => {
 | 
					store.activeRoom.subscribe("changeSelf", () => {
 | 
				
			||||||
  if (store.activeRoom.exists()) {
 | 
						if (store.activeRoom.exists()) {
 | 
				
			||||||
    input.focus();
 | 
							input.focus()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
});
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
input.addEventListener("keydown", (event) => {
 | 
					input.addEventListener("keydown", event => {
 | 
				
			||||||
  if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey) {
 | 
						if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey) {
 | 
				
			||||||
    event.preventDefault();
 | 
							event.preventDefault()
 | 
				
			||||||
    const body = input.value;
 | 
							const body = input.value
 | 
				
			||||||
    send(input.value);
 | 
							send(input.value)
 | 
				
			||||||
    input.value = "";
 | 
							input.value = ""
 | 
				
			||||||
    fixHeight();
 | 
							fixHeight()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
});
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
input.addEventListener("input", () => {
 | 
					input.addEventListener("input", () => {
 | 
				
			||||||
  fixHeight();
 | 
						fixHeight()
 | 
				
			||||||
});
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function fixHeight() {
 | 
					function fixHeight() {
 | 
				
			||||||
  input.style.height = "0px";
 | 
						input.style.height = "0px"
 | 
				
			||||||
  // console.log(input.clientHeight, input.scrollHeight)
 | 
						// console.log(input.clientHeight, input.scrollHeight)
 | 
				
			||||||
  input.style.height = input.scrollHeight + 1 + "px";
 | 
						input.style.height = (input.scrollHeight + 1) + "px"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getTxnId() {
 | 
					function getTxnId() {
 | 
				
			||||||
  return Date.now() + sentIndex++;
 | 
						return Date.now() + (sentIndex++)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function send(body) {
 | 
					function send(body) {
 | 
				
			||||||
  if (!store.activeRoom.exists()) return;
 | 
						if (!store.activeRoom.exists()) return
 | 
				
			||||||
  const id = store.activeRoom.value().id;
 | 
						const id = store.activeRoom.value().id
 | 
				
			||||||
  return fetch(
 | 
						return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${id}/send/m.room.message/${getTxnId()}?access_token=${lsm.get("access_token")}`, {
 | 
				
			||||||
    `${lsm.get(
 | 
							method: "PUT",
 | 
				
			||||||
      "domain"
 | 
							body: JSON.stringify({
 | 
				
			||||||
    )}/_matrix/client/r0/rooms/${id}/send/m.room.message/${getTxnId()}?access_token=${lsm.get(
 | 
								msgtype: "m.text",
 | 
				
			||||||
      "access_token"
 | 
								body
 | 
				
			||||||
    )}`,
 | 
							}),
 | 
				
			||||||
    {
 | 
							headers: {
 | 
				
			||||||
      method: "PUT",
 | 
								"Content-Type": "application/json"
 | 
				
			||||||
      body: JSON.stringify({
 | 
							}
 | 
				
			||||||
        msgtype: "m.text",
 | 
						})
 | 
				
			||||||
        body,
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										113
									
								
								src/js/chat.js
									
										
									
									
									
								
							
							
						
						
									
										113
									
								
								src/js/chat.js
									
										
									
									
									
								
							| 
						 | 
					@ -1,72 +1,65 @@
 | 
				
			||||||
import { ElemJS, q, ejs } from "./basic.js";
 | 
					import {ElemJS, q, ejs} from "./basic.js"
 | 
				
			||||||
import { store } from "./store/store.js";
 | 
					import {store} from "./store/store.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const chatMessages = q("#c-chat-messages");
 | 
					const chatMessages = q("#c-chat-messages")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Chat extends ElemJS {
 | 
					class Chat extends ElemJS {
 | 
				
			||||||
  constructor() {
 | 
						constructor() {
 | 
				
			||||||
    super(q("#c-chat"));
 | 
							super(q("#c-chat"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.removableSubscriptions = [];
 | 
							this.removableSubscriptions = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    store.activeRoom.subscribe("changeSelf", this.changeRoom.bind(this));
 | 
							store.activeRoom.subscribe("changeSelf", this.changeRoom.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.render();
 | 
							this.render()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  unsubscribe() {
 | 
						unsubscribe() {
 | 
				
			||||||
    this.removableSubscriptions.forEach(({ name, target, subscription }) => {
 | 
							this.removableSubscriptions.forEach(({name, target, subscription}) => {
 | 
				
			||||||
      target.unsubscribe(name, subscription);
 | 
								target.unsubscribe(name, subscription)
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
    this.removableSubscriptions.length = 0;
 | 
							this.removableSubscriptions.length = 0
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  changeRoom() {
 | 
						changeRoom() {
 | 
				
			||||||
    // disconnect from the previous room
 | 
							// disconnect from the previous room
 | 
				
			||||||
    this.unsubscribe();
 | 
							this.unsubscribe()
 | 
				
			||||||
    // connect to the new room's timeline updater
 | 
							// connect to the new room's timeline updater
 | 
				
			||||||
    if (store.activeRoom.exists()) {
 | 
							if (store.activeRoom.exists()) {
 | 
				
			||||||
      const timeline = store.activeRoom.value().timeline;
 | 
								const timeline = store.activeRoom.value().timeline
 | 
				
			||||||
      const subscription = () => {
 | 
								const subscription = () => {
 | 
				
			||||||
        // scroll anchor does not work if the timeline is scrolled to the top.
 | 
									// scroll anchor does not work if the timeline is scrolled to the top.
 | 
				
			||||||
        // at the start, when there are not enough messages for a full screen, this is the case.
 | 
									// at the start, when there are not enough messages for a full screen, this is the case.
 | 
				
			||||||
        // once there are enough messages that scrolling is necessary, we initiate a scroll down to activate the scroll anchor.
 | 
									// once there are enough messages that scrolling is necessary, we initiate a scroll down to activate the scroll anchor.
 | 
				
			||||||
        let oldDifference =
 | 
									let oldDifference = chatMessages.scrollHeight - chatMessages.clientHeight
 | 
				
			||||||
          chatMessages.scrollHeight - chatMessages.clientHeight;
 | 
									setTimeout(() => {
 | 
				
			||||||
        setTimeout(() => {
 | 
										let newDifference = chatMessages.scrollHeight - chatMessages.clientHeight
 | 
				
			||||||
          let newDifference =
 | 
										// console.log("height difference", oldDifference, newDifference)
 | 
				
			||||||
            chatMessages.scrollHeight - chatMessages.clientHeight;
 | 
										if (oldDifference < 24) { // this is jank
 | 
				
			||||||
          // console.log("height difference", oldDifference, newDifference)
 | 
											this.element.parentElement.scrollBy(0, 1000)
 | 
				
			||||||
          if (oldDifference < 24) {
 | 
										}
 | 
				
			||||||
            // this is jank
 | 
									}, 0)
 | 
				
			||||||
            this.element.parentElement.scrollBy(0, 1000);
 | 
								}
 | 
				
			||||||
          }
 | 
								const name = "beforeChange"
 | 
				
			||||||
        }, 0);
 | 
								this.removableSubscriptions.push({name, target: timeline, subscription})
 | 
				
			||||||
      };
 | 
								timeline.subscribe(name, subscription)
 | 
				
			||||||
      const name = "beforeChange";
 | 
							}
 | 
				
			||||||
      this.removableSubscriptions.push({
 | 
							this.render()
 | 
				
			||||||
        name,
 | 
						}
 | 
				
			||||||
        target: timeline,
 | 
					 | 
				
			||||||
        subscription,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      timeline.subscribe(name, subscription);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this.render();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
						render() {
 | 
				
			||||||
    this.clearChildren();
 | 
							this.clearChildren()
 | 
				
			||||||
    if (store.activeRoom.exists()) {
 | 
							if (store.activeRoom.exists()) {
 | 
				
			||||||
      const reactiveTimeline = store.activeRoom.value().timeline.getTimeline();
 | 
								const reactiveTimeline = store.activeRoom.value().timeline.getTimeline()
 | 
				
			||||||
      this.child(reactiveTimeline);
 | 
								this.child(reactiveTimeline)
 | 
				
			||||||
      setTimeout(() => {
 | 
								setTimeout(() => {
 | 
				
			||||||
        this.element.parentElement.scrollBy(0, 1);
 | 
									this.element.parentElement.scrollBy(0, 1)
 | 
				
			||||||
        reactiveTimeline.anchor.scroll();
 | 
									reactiveTimeline.anchor.scroll()
 | 
				
			||||||
      }, 0);
 | 
								}, 0)
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const chat = new Chat();
 | 
					const chat = new Chat()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { chat };
 | 
					export {chat}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,14 @@
 | 
				
			||||||
import { q } from "./basic.js";
 | 
					import {q} from "./basic.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let state = "CLOSED";
 | 
					let state = "CLOSED"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const groups = q("#c-groups-display");
 | 
					const groups = q("#c-groups-display")
 | 
				
			||||||
const rooms = q("#c-rooms");
 | 
					const rooms = q("#c-rooms")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
groups.addEventListener("click", () => {
 | 
					groups.addEventListener("click", () => {
 | 
				
			||||||
  groups.classList.add("c-groups__display--closed");
 | 
						groups.classList.add("c-groups__display--closed")
 | 
				
			||||||
});
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
rooms.addEventListener("mouseout", () => {
 | 
					rooms.addEventListener("mouseout", () => {
 | 
				
			||||||
  groups.classList.remove("c-groups__display--closed");
 | 
						groups.classList.remove("c-groups__display--closed")
 | 
				
			||||||
});
 | 
					})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,11 @@
 | 
				
			||||||
function get(name) {
 | 
					function get(name) {
 | 
				
			||||||
  return localStorage.getItem(name);
 | 
						return localStorage.getItem(name)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function set(name, value) {
 | 
					function set(name, value) {
 | 
				
			||||||
  return localStorage.setItem(name, value);
 | 
						return localStorage.setItem(name, value)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
window.lsm = { get, set };
 | 
					window.lsm = {get, set}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { get, set };
 | 
					export {get, set}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,247 +1,235 @@
 | 
				
			||||||
import { q, ElemJS, ejs } from "./basic.js";
 | 
					import {q, ElemJS, ejs} from "./basic.js"
 | 
				
			||||||
import { store } from "./store/store.js";
 | 
					import {store} from "./store/store.js"
 | 
				
			||||||
import { Timeline } from "./Timeline.js";
 | 
					import {Timeline} from "./Timeline.js"
 | 
				
			||||||
import * as lsm from "./lsm.js";
 | 
					import * as lsm from "./lsm.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function resolveMxc(url, size, method) {
 | 
					function resolveMxc(url, size, method) {
 | 
				
			||||||
  const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1);
 | 
						const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
 | 
				
			||||||
  if (size && method) {
 | 
						if (size && method) {
 | 
				
			||||||
    return `${lsm.get(
 | 
							return `${lsm.get("domain")}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`
 | 
				
			||||||
      "domain"
 | 
						} else {
 | 
				
			||||||
    )}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`;
 | 
							return `${lsm.get("domain")}/_matrix/media/r0/download/${server}/${id}`
 | 
				
			||||||
  } else {
 | 
						}
 | 
				
			||||||
    return `${lsm.get("domain")}/_matrix/media/r0/download/${server}/${id}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ActiveGroupMarker extends ElemJS {
 | 
					class ActiveGroupMarker extends ElemJS {
 | 
				
			||||||
  constructor() {
 | 
						constructor() {
 | 
				
			||||||
    super(q("#c-group-marker"));
 | 
							super(q("#c-group-marker"))
 | 
				
			||||||
    store.activeGroup.subscribe("changeSelf", this.render.bind(this));
 | 
							store.activeGroup.subscribe("changeSelf", this.render.bind(this))
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
						render() {
 | 
				
			||||||
    if (store.activeGroup.exists()) {
 | 
							if (store.activeGroup.exists()) {
 | 
				
			||||||
      const group = store.activeGroup.value();
 | 
								const group = store.activeGroup.value()
 | 
				
			||||||
      this.style("opacity", 1);
 | 
								this.style("opacity", 1)
 | 
				
			||||||
      this.style("transform", `translateY(${group.element.offsetTop}px)`);
 | 
								this.style("transform", `translateY(${group.element.offsetTop}px)`)
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      this.style("opacity", 0);
 | 
								this.style("opacity", 0)
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const activeGroupMarker = new ActiveGroupMarker();
 | 
					const activeGroupMarker = new ActiveGroupMarker()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Group extends ElemJS {
 | 
					class Group extends ElemJS {
 | 
				
			||||||
  constructor(key, data) {
 | 
						constructor(key, data) {
 | 
				
			||||||
    super("div");
 | 
							super("div")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.data = data;
 | 
							this.data = data
 | 
				
			||||||
    this.order = this.data.order;
 | 
							this.order = this.data.order
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.class("c-group");
 | 
							this.class("c-group")
 | 
				
			||||||
    this.child(
 | 
							this.child(
 | 
				
			||||||
      this.data.icon
 | 
								(this.data.icon
 | 
				
			||||||
        ? ejs("img").class("c-group__icon").attribute("src", this.data.icon)
 | 
								 ? ejs("img").class("c-group__icon").attribute("src", this.data.icon)
 | 
				
			||||||
        : ejs("div").class("c-group__icon"),
 | 
								 : ejs("div").class("c-group__icon")
 | 
				
			||||||
      ejs("div").class("c-group__name").text(this.data.name)
 | 
								),
 | 
				
			||||||
    );
 | 
								ejs("div").class("c-group__name").text(this.data.name)
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.on("click", this.onClick.bind(this));
 | 
							this.on("click", this.onClick.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    store.activeGroup.subscribe("changeSelf", this.render.bind(this));
 | 
							store.activeGroup.subscribe("changeSelf", this.render.bind(this))
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
						render() {
 | 
				
			||||||
    const active = store.activeGroup.value() === this;
 | 
							const active = store.activeGroup.value() === this
 | 
				
			||||||
    this.element.classList[active ? "add" : "remove"]("c-group--active");
 | 
							this.element.classList[active ? "add" : "remove"]("c-group--active")
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClick() {
 | 
						onClick() {
 | 
				
			||||||
    store.activeGroup.set(this);
 | 
							store.activeGroup.set(this)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Room extends ElemJS {
 | 
					class Room extends ElemJS {
 | 
				
			||||||
  constructor(id, data) {
 | 
						constructor(id, data) {
 | 
				
			||||||
    super("div");
 | 
							super("div")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.id = id;
 | 
							this.id = id
 | 
				
			||||||
    this.data = data;
 | 
							this.data = data
 | 
				
			||||||
    this.timeline = new Timeline();
 | 
							this.timeline = new Timeline()
 | 
				
			||||||
    this.group = null;
 | 
							this.group = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.class("c-room");
 | 
							this.class("c-room")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.on("click", this.onClick.bind(this));
 | 
							this.on("click", this.onClick.bind(this))
 | 
				
			||||||
    store.activeRoom.subscribe("changeSelf", this.render.bind(this));
 | 
							store.activeRoom.subscribe("changeSelf", this.render.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.render();
 | 
							this.render()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get order() {
 | 
						get order() {
 | 
				
			||||||
    if (this.group) {
 | 
							if (this.group) {
 | 
				
			||||||
      let chars = 36;
 | 
								let chars = 36
 | 
				
			||||||
      let total = 0;
 | 
								let total = 0
 | 
				
			||||||
      const name = this.getName();
 | 
								const name = this.getName()
 | 
				
			||||||
      for (let i = 0; i < name.length; i++) {
 | 
								for (let i = 0; i < name.length; i++) {
 | 
				
			||||||
        const c = name[i];
 | 
									const c = name[i]
 | 
				
			||||||
        let d = 0;
 | 
									let d = 0
 | 
				
			||||||
        if (c >= "A" && c <= "Z") d = c.charCodeAt(0) - 65 + 10;
 | 
									if (c >= "A" && c <= "Z") d = c.charCodeAt(0) - 65 + 10
 | 
				
			||||||
        else if (c >= "a" && c <= "z") d = c.charCodeAt(0) - 97 + 10;
 | 
									else if (c >= "a" && c <= "z") d = c.charCodeAt(0) - 97 + 10
 | 
				
			||||||
        else if (c >= "0" && c <= "9") d = +c;
 | 
									else if (c >= "0" && c <= "9") d = +c
 | 
				
			||||||
        total += d * chars ** -i;
 | 
									total += d * chars ** (-i)
 | 
				
			||||||
      }
 | 
								}
 | 
				
			||||||
      return total;
 | 
								return total
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      return -this.timeline.latest;
 | 
								return -this.timeline.latest
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getName() {
 | 
						getName() {
 | 
				
			||||||
    let name = this.data.state.events.find((e) => e.type === "m.room.name");
 | 
							let name = this.data.state.events.find(e => e.type === "m.room.name")
 | 
				
			||||||
    if (name) {
 | 
							if (name) {
 | 
				
			||||||
      name = name.content.name;
 | 
								name = name.content.name
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      const users = this.data.summary["m.heroes"];
 | 
								const users = this.data.summary["m.heroes"]
 | 
				
			||||||
      const usernames = users.map((u) => (u.match(/^@([^:]+):/) || [])[1] || u);
 | 
								const usernames = users.map(u => (u.match(/^@([^:]+):/) || [])[1] || u)
 | 
				
			||||||
      name = usernames.join(", ");
 | 
								name = usernames.join(", ")
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
    return name;
 | 
							return name
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getIcon() {
 | 
						getIcon() {
 | 
				
			||||||
    const avatar = this.data.state.events.find(
 | 
							const avatar = this.data.state.events.find(e => e.type === "m.room.avatar")
 | 
				
			||||||
      (e) => e.type === "m.room.avatar"
 | 
							if (avatar) {
 | 
				
			||||||
    );
 | 
								return resolveMxc(avatar.content.url || avatar.content.avatar_url, 32, "crop")
 | 
				
			||||||
    if (avatar) {
 | 
							} else {
 | 
				
			||||||
      return resolveMxc(
 | 
								return null
 | 
				
			||||||
        avatar.content.url || avatar.content.avatar_url,
 | 
							}
 | 
				
			||||||
        32,
 | 
						}
 | 
				
			||||||
        "crop"
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isDirect() {
 | 
						isDirect() {
 | 
				
			||||||
    return store.directs.has(this.id);
 | 
							return store.directs.has(this.id)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setGroup(id) {
 | 
						setGroup(id) {
 | 
				
			||||||
    this.group = id;
 | 
							this.group = id
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getGroup() {
 | 
						getGroup() {
 | 
				
			||||||
    if (this.group) {
 | 
							if (this.group) {
 | 
				
			||||||
      return store.groups.get(this.group).value();
 | 
								return store.groups.get(this.group).value()
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      return this.isDirect()
 | 
								return this.isDirect() ? store.groups.get("directs").value() : store.groups.get("channels").value()
 | 
				
			||||||
        ? store.groups.get("directs").value()
 | 
							}
 | 
				
			||||||
        : store.groups.get("channels").value();
 | 
						}
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClick() {
 | 
						onClick() {
 | 
				
			||||||
    store.activeRoom.set(this);
 | 
							store.activeRoom.set(this)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
						render() {
 | 
				
			||||||
    this.clearChildren();
 | 
							this.clearChildren()
 | 
				
			||||||
    // data
 | 
							// data
 | 
				
			||||||
    const icon = this.getIcon();
 | 
							const icon = this.getIcon()
 | 
				
			||||||
    if (icon) {
 | 
							if (icon) {
 | 
				
			||||||
      this.child(ejs("img").class("c-room__icon").attribute("src", icon));
 | 
								this.child(ejs("img").class("c-room__icon").attribute("src", icon))
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      this.child(ejs("div").class("c-room__icon", "c-room__icon--no-icon"));
 | 
								this.child(ejs("div").class("c-room__icon", "c-room__icon--no-icon"))
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
    this.child(ejs("div").class("c-room__name").text(this.getName()));
 | 
							this.child(ejs("div").class("c-room__name").text(this.getName()))
 | 
				
			||||||
    // active
 | 
							// active
 | 
				
			||||||
    const active = store.activeRoom.value() === this;
 | 
							const active = store.activeRoom.value() === this
 | 
				
			||||||
    this.element.classList[active ? "add" : "remove"]("c-room--active");
 | 
							this.element.classList[active ? "add" : "remove"]("c-room--active")
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Rooms extends ElemJS {
 | 
					class Rooms extends ElemJS {
 | 
				
			||||||
  constructor() {
 | 
						constructor() {
 | 
				
			||||||
    super(q("#c-rooms"));
 | 
							super(q("#c-rooms"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.roomData = [];
 | 
							this.roomData = []
 | 
				
			||||||
    this.rooms = [];
 | 
							this.rooms = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    store.rooms.subscribe("askAdd", this.askAdd.bind(this));
 | 
							store.rooms.subscribe("askAdd", this.askAdd.bind(this))
 | 
				
			||||||
    store.rooms.subscribe("addItem", this.addItem.bind(this));
 | 
							store.rooms.subscribe("addItem", this.addItem.bind(this))
 | 
				
			||||||
    // store.rooms.subscribe("changeItem", this.render.bind(this))
 | 
							// store.rooms.subscribe("changeItem", this.render.bind(this))
 | 
				
			||||||
    store.activeGroup.subscribe("changeSelf", this.render.bind(this));
 | 
							store.activeGroup.subscribe("changeSelf", this.render.bind(this))
 | 
				
			||||||
    store.directs.subscribe("changeItem", this.render.bind(this));
 | 
							store.directs.subscribe("changeItem", this.render.bind(this))
 | 
				
			||||||
    store.newEvents.subscribe("changeSelf", this.sort.bind(this));
 | 
							store.newEvents.subscribe("changeSelf", this.sort.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.render();
 | 
							this.render()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sort() {
 | 
						sort() {
 | 
				
			||||||
    store.rooms.sort();
 | 
							store.rooms.sort()
 | 
				
			||||||
    this.render();
 | 
							this.render()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  askAdd(event, { key, data }) {
 | 
						askAdd(event, {key, data}) {
 | 
				
			||||||
    const room = new Room(key, data);
 | 
							const room = new Room(key, data)
 | 
				
			||||||
    store.rooms.addEnd(key, room);
 | 
							store.rooms.addEnd(key, room)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addItem(event, key) {
 | 
						addItem(event, key) {
 | 
				
			||||||
    const room = store.rooms.get(key).value();
 | 
							const room = store.rooms.get(key).value()
 | 
				
			||||||
    if (room.getGroup() === store.activeGroup.value()) {
 | 
							if (room.getGroup() === store.activeGroup.value()) {
 | 
				
			||||||
      this.child(room);
 | 
								this.child(room)
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
						render() {
 | 
				
			||||||
    this.clearChildren();
 | 
							this.clearChildren()
 | 
				
			||||||
    let first = null;
 | 
							let first = null
 | 
				
			||||||
    // set room list
 | 
							// set room list
 | 
				
			||||||
    store.rooms.forEach((id, room) => {
 | 
							store.rooms.forEach((id, room) => {
 | 
				
			||||||
      if (room.value().getGroup() === store.activeGroup.value()) {
 | 
								if (room.value().getGroup() === store.activeGroup.value()) {
 | 
				
			||||||
        if (!first) first = room.value();
 | 
									if (!first) first = room.value()
 | 
				
			||||||
        this.child(room.value());
 | 
									this.child(room.value())
 | 
				
			||||||
      }
 | 
								}
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
    // if needed, change the active room to be an item in the room list
 | 
							// if needed, change the active room to be an item in the room list
 | 
				
			||||||
    if (
 | 
							if (!store.activeRoom.exists() || store.activeRoom.value().getGroup() !== store.activeGroup.value()) {
 | 
				
			||||||
      !store.activeRoom.exists() ||
 | 
								if (first) {
 | 
				
			||||||
      store.activeRoom.value().getGroup() !== store.activeGroup.value()
 | 
									store.activeRoom.set(first)
 | 
				
			||||||
    ) {
 | 
								} else {
 | 
				
			||||||
      if (first) {
 | 
									store.activeRoom.delete()
 | 
				
			||||||
        store.activeRoom.set(first);
 | 
								}
 | 
				
			||||||
      } else {
 | 
							}
 | 
				
			||||||
        store.activeRoom.delete();
 | 
						}
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
const rooms = new Rooms();
 | 
					const rooms = new Rooms()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Groups extends ElemJS {
 | 
					class Groups extends ElemJS {
 | 
				
			||||||
  constructor() {
 | 
						constructor() {
 | 
				
			||||||
    super(q("#c-groups-list"));
 | 
							super(q("#c-groups-list"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    store.groups.subscribe("askAdd", this.askAdd.bind(this));
 | 
							store.groups.subscribe("askAdd", this.askAdd.bind(this))
 | 
				
			||||||
    store.groups.subscribe("changeItem", this.render.bind(this));
 | 
							store.groups.subscribe("changeItem", this.render.bind(this))
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  askAdd(event, { key, data }) {
 | 
						askAdd(event, {key, data}) {
 | 
				
			||||||
    const group = new Group(key, data);
 | 
							const group = new Group(key, data)
 | 
				
			||||||
    store.groups.addEnd(key, group);
 | 
							store.groups.addEnd(key, group)
 | 
				
			||||||
    store.groups.sort();
 | 
							store.groups.sort()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
						render() {
 | 
				
			||||||
    this.clearChildren();
 | 
							this.clearChildren()
 | 
				
			||||||
    store.groups.forEach((key, item) => {
 | 
							store.groups.forEach((key, item) => {
 | 
				
			||||||
      this.child(item.value());
 | 
								this.child(item.value())
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
const groups = new Groups();
 | 
					const groups = new Groups()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,45 +1,38 @@
 | 
				
			||||||
class Subscribable {
 | 
					class Subscribable {
 | 
				
			||||||
  constructor() {
 | 
						constructor() {
 | 
				
			||||||
    this.events = {
 | 
							this.events = {
 | 
				
			||||||
      addSelf: [],
 | 
								addSelf: [],
 | 
				
			||||||
      editSelf: [],
 | 
								editSelf: [],
 | 
				
			||||||
      removeSelf: [],
 | 
								removeSelf: [],
 | 
				
			||||||
      changeSelf: [],
 | 
								changeSelf: []
 | 
				
			||||||
    };
 | 
							}
 | 
				
			||||||
    this.eventDeps = {
 | 
							this.eventDeps = {
 | 
				
			||||||
      addSelf: ["changeSelf"],
 | 
								addSelf: ["changeSelf"],
 | 
				
			||||||
      editSelf: ["changeSelf"],
 | 
								editSelf: ["changeSelf"],
 | 
				
			||||||
      removeSelf: ["changeSelf"],
 | 
								removeSelf: ["changeSelf"],
 | 
				
			||||||
      changeSelf: [],
 | 
								changeSelf: []
 | 
				
			||||||
    };
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  subscribe(event, callback) {
 | 
						subscribe(event, callback) {
 | 
				
			||||||
    if (this.events[event]) {
 | 
							if (this.events[event]) {
 | 
				
			||||||
      this.events[event].push(callback);
 | 
								this.events[event].push(callback)
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      throw new Error(
 | 
								throw new Error(`Cannot subscribe to non-existent event ${event}, available events are: ${Object.keys(this.events).join(", ")}`)
 | 
				
			||||||
        `Cannot subscribe to non-existent event ${event}, available events are: ${Object.keys(
 | 
							}
 | 
				
			||||||
          this.events
 | 
						}
 | 
				
			||||||
        ).join(", ")}`
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  unsubscribe(event, callback) {
 | 
						unsubscribe(event, callback) {
 | 
				
			||||||
    const index = this.events[event].indexOf(callback);
 | 
							const index = this.events[event].indexOf(callback)
 | 
				
			||||||
    if (index === -1)
 | 
							if (index === -1) throw new Error(`Tried to remove a nonexisting subscription from event ${event}`)
 | 
				
			||||||
      throw new Error(
 | 
							this.events[event].splice(index, 1)
 | 
				
			||||||
        `Tried to remove a nonexisting subscription from event ${event}`
 | 
						}
 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    this.events[event].splice(index, 1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  broadcast(event, data) {
 | 
						broadcast(event, data) {
 | 
				
			||||||
    this.eventDeps[event].concat(event).forEach((eventName) => {
 | 
							this.eventDeps[event].concat(event).forEach(eventName => {
 | 
				
			||||||
      this.events[eventName].forEach((f) => f(event, data));
 | 
								this.events[eventName].forEach(f => f(event, data))
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { Subscribable };
 | 
					export {Subscribable}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,41 +1,41 @@
 | 
				
			||||||
import { Subscribable } from "./Subscribable.js";
 | 
					import {Subscribable} from "./Subscribable.js"
 | 
				
			||||||
import { SubscribeValue } from "./SubscribeValue.js";
 | 
					import {SubscribeValue} from "./SubscribeValue.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SubscribeMap extends Subscribable {
 | 
					class SubscribeMap extends Subscribable {
 | 
				
			||||||
  constructor() {
 | 
						constructor() {
 | 
				
			||||||
    super();
 | 
							super()
 | 
				
			||||||
    Object.assign(this.events, {
 | 
							Object.assign(this.events, {
 | 
				
			||||||
      addItem: [],
 | 
								addItem: [],
 | 
				
			||||||
      changeItem: [],
 | 
								changeItem: [],
 | 
				
			||||||
      removeItem: [],
 | 
								removeItem: []
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
    this.map = new Map();
 | 
							this.map = new Map()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has(key) {
 | 
						has(key) {
 | 
				
			||||||
    return this.map.has(key) && this.map.get(key).exists();
 | 
							return this.map.has(key) && this.map.get(key).exists()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get(key) {
 | 
						get(key) {
 | 
				
			||||||
    if (this.map.has(key)) {
 | 
							if (this.map.has(key)) {
 | 
				
			||||||
      return this.map.get(key);
 | 
								return this.map.get(key)
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      this.map.set(key, new SubscribeValue());
 | 
								this.map.set(key, new SubscribeValue())
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  set(key, value) {
 | 
						set(key, value) {
 | 
				
			||||||
    let s;
 | 
							let s
 | 
				
			||||||
    if (this.map.has(key)) {
 | 
							if (this.map.has(key)) {
 | 
				
			||||||
      s = this.map.get(key).set(value);
 | 
								s = this.map.get(key).set(value)
 | 
				
			||||||
      this.broadcast("changeItem", key);
 | 
								this.broadcast("changeItem", key)
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      s = new SubscribeValue().set(value);
 | 
								s = new SubscribeValue().set(value)
 | 
				
			||||||
      this.map.set(key, s);
 | 
								this.map.set(key, s)
 | 
				
			||||||
      this.broadcast("addItem", key);
 | 
								this.broadcast("addItem", key)
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
    return s;
 | 
							return s
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { SubscribeMap };
 | 
					export {SubscribeMap}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,86 +1,86 @@
 | 
				
			||||||
import { Subscribable } from "./Subscribable.js";
 | 
					import {Subscribable} from "./Subscribable.js"
 | 
				
			||||||
import { SubscribeValue } from "./SubscribeValue.js";
 | 
					import {SubscribeValue} from "./SubscribeValue.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SubscribeMapList extends Subscribable {
 | 
					class SubscribeMapList extends Subscribable {
 | 
				
			||||||
  constructor(inner) {
 | 
						constructor(inner) {
 | 
				
			||||||
    super();
 | 
							super()
 | 
				
			||||||
    this.inner = inner;
 | 
							this.inner = inner
 | 
				
			||||||
    Object.assign(this.events, {
 | 
							Object.assign(this.events, {
 | 
				
			||||||
      addItem: [],
 | 
								addItem: [],
 | 
				
			||||||
      deleteItem: [],
 | 
								deleteItem: [],
 | 
				
			||||||
      editItem: [],
 | 
								editItem: [],
 | 
				
			||||||
      changeItem: [],
 | 
								changeItem: [],
 | 
				
			||||||
      askAdd: [],
 | 
								askAdd: []
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
    Object.assign(this.eventDeps, {
 | 
							Object.assign(this.eventDeps, {
 | 
				
			||||||
      addItem: ["changeItem"],
 | 
								addItem: ["changeItem"],
 | 
				
			||||||
      deleteItem: ["changeItem"],
 | 
								deleteItem: ["changeItem"],
 | 
				
			||||||
      editItem: ["changeItem"],
 | 
								editItem: ["changeItem"],
 | 
				
			||||||
      changeItem: [],
 | 
								changeItem: [],
 | 
				
			||||||
      askAdd: [],
 | 
								askAdd: []
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
    this.map = new Map();
 | 
							this.map = new Map()
 | 
				
			||||||
    this.list = [];
 | 
							this.list = []
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has(key) {
 | 
						has(key) {
 | 
				
			||||||
    return this.map.has(key) && this.map.get(key).exists();
 | 
							return this.map.has(key) && this.map.get(key).exists()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get(key) {
 | 
						get(key) {
 | 
				
			||||||
    if (this.map.has(key)) {
 | 
							if (this.map.has(key)) {
 | 
				
			||||||
      return this.map.get(key);
 | 
								return this.map.get(key)
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      const item = new this.inner();
 | 
								const item = new this.inner()
 | 
				
			||||||
      this.map.set(key, item);
 | 
								this.map.set(key, item)
 | 
				
			||||||
      return item;
 | 
								return item
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  forEach(f) {
 | 
						forEach(f) {
 | 
				
			||||||
    this.list.forEach((key) => f(key, this.get(key)));
 | 
							this.list.forEach(key => f(key, this.get(key)))
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  askAdd(key, data) {
 | 
						askAdd(key, data) {
 | 
				
			||||||
    this.broadcast("askAdd", { key, data });
 | 
							this.broadcast("askAdd", {key, data})
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addStart(key, value) {
 | 
						addStart(key, value) {
 | 
				
			||||||
    this._add(key, value, true);
 | 
							this._add(key, value, true)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addEnd(key, value) {
 | 
						addEnd(key, value) {
 | 
				
			||||||
    this._add(key, value, false);
 | 
							this._add(key, value, false)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sort() {
 | 
						sort() {
 | 
				
			||||||
    this.list.sort((a, b) => {
 | 
							this.list.sort((a, b) => {
 | 
				
			||||||
      const orderA = this.map.get(a).value().order;
 | 
								const orderA = this.map.get(a).value().order
 | 
				
			||||||
      const orderB = this.map.get(b).value().order;
 | 
								const orderB = this.map.get(b).value().order
 | 
				
			||||||
      return orderA - orderB;
 | 
								return orderA - orderB
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
    this.broadcast("changeItem");
 | 
							this.broadcast("changeItem")
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _add(key, value, start) {
 | 
						_add(key, value, start) {
 | 
				
			||||||
    let s;
 | 
							let s
 | 
				
			||||||
    if (this.map.has(key)) {
 | 
							if (this.map.has(key)) {
 | 
				
			||||||
      const exists = this.map.get(key).exists();
 | 
								const exists = this.map.get(key).exists()
 | 
				
			||||||
      s = this.map.get(key).set(value);
 | 
								s = this.map.get(key).set(value)
 | 
				
			||||||
      if (exists) {
 | 
								if (exists) {
 | 
				
			||||||
        this.broadcast("editItem", key);
 | 
									this.broadcast("editItem", key)
 | 
				
			||||||
      } else {
 | 
								} else {
 | 
				
			||||||
        this.broadcast("addItem", key);
 | 
									this.broadcast("addItem", key)
 | 
				
			||||||
      }
 | 
								}
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      s = new this.inner().set(value);
 | 
								s = new this.inner().set(value)
 | 
				
			||||||
      this.map.set(key, s);
 | 
								this.map.set(key, s)
 | 
				
			||||||
      if (start) this.list.unshift(key);
 | 
								if (start) this.list.unshift(key)
 | 
				
			||||||
      else this.list.push(key);
 | 
								else this.list.push(key)
 | 
				
			||||||
      this.broadcast("addItem", key);
 | 
								this.broadcast("addItem", key)
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
    return s;
 | 
							return s
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { SubscribeMapList };
 | 
					export {SubscribeMapList}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,50 +1,50 @@
 | 
				
			||||||
import { Subscribable } from "./Subscribable.js";
 | 
					import {Subscribable} from "./Subscribable.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SubscribeSet extends Subscribable {
 | 
					class SubscribeSet extends Subscribable {
 | 
				
			||||||
  constructor() {
 | 
						constructor() {
 | 
				
			||||||
    super();
 | 
							super()
 | 
				
			||||||
    Object.assign(this.events, {
 | 
							Object.assign(this.events, {
 | 
				
			||||||
      addItem: [],
 | 
								addItem: [],
 | 
				
			||||||
      deleteItem: [],
 | 
								deleteItem: [],
 | 
				
			||||||
      changeItem: [],
 | 
								changeItem: [],
 | 
				
			||||||
      askAdd: [],
 | 
								askAdd: []
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
    Object.assign(this.eventDeps, {
 | 
							Object.assign(this.eventDeps, {
 | 
				
			||||||
      addItem: ["changeItem"],
 | 
								addItem: ["changeItem"],
 | 
				
			||||||
      deleteItem: ["changeItem"],
 | 
								deleteItem: ["changeItem"],
 | 
				
			||||||
      changeItem: [],
 | 
								changeItem: [],
 | 
				
			||||||
      askAdd: [],
 | 
								askAdd: []
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
    this.set = new Set();
 | 
							this.set = new Set()
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has(key) {
 | 
						has(key) {
 | 
				
			||||||
    return this.set.has(key);
 | 
							return this.set.has(key)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  forEach(f) {
 | 
						forEach(f) {
 | 
				
			||||||
    for (const key of this.set.keys()) {
 | 
							for (const key of this.set.keys()) {
 | 
				
			||||||
      f(key);
 | 
								f(key)
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  askAdd(key) {
 | 
						askAdd(key) {
 | 
				
			||||||
    this.broadcast("askAdd", key);
 | 
							this.broadcast("askAdd", key)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  add(key) {
 | 
						add(key) {
 | 
				
			||||||
    if (!this.set.has(key)) {
 | 
							if (!this.set.has(key)) {
 | 
				
			||||||
      this.set.add(key);
 | 
								this.set.add(key)
 | 
				
			||||||
      this.broadcast("addItem", key);
 | 
								this.broadcast("addItem", key)
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  delete(key) {
 | 
						delete(key) {
 | 
				
			||||||
    if (this.set.has(key)) {
 | 
							if (this.set.has(key)) {
 | 
				
			||||||
      this.set.delete(key);
 | 
								this.set.delete(key)
 | 
				
			||||||
      this.broadcast("deleteItem", key);
 | 
								this.broadcast("deleteItem", key)
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { SubscribeSet };
 | 
					export {SubscribeSet}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,47 +1,47 @@
 | 
				
			||||||
import { Subscribable } from "./Subscribable.js";
 | 
					import {Subscribable} from "./Subscribable.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SubscribeValue extends Subscribable {
 | 
					class SubscribeValue extends Subscribable {
 | 
				
			||||||
  constructor() {
 | 
						constructor() {
 | 
				
			||||||
    super();
 | 
							super()
 | 
				
			||||||
    this.hasData = false;
 | 
							this.hasData = false
 | 
				
			||||||
    this.data = null;
 | 
							this.data = null
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  exists() {
 | 
						exists() {
 | 
				
			||||||
    return this.hasData;
 | 
							return this.hasData
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  value() {
 | 
						value() {
 | 
				
			||||||
    if (this.hasData) return this.data;
 | 
							if (this.hasData) return this.data
 | 
				
			||||||
    else return null;
 | 
							else return null
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  set(data) {
 | 
						set(data) {
 | 
				
			||||||
    const exists = this.exists();
 | 
							const exists = this.exists()
 | 
				
			||||||
    this.data = data;
 | 
							this.data = data
 | 
				
			||||||
    this.hasData = true;
 | 
							this.hasData = true
 | 
				
			||||||
    if (exists) {
 | 
							if (exists) {
 | 
				
			||||||
      this.broadcast("editSelf", this.data);
 | 
								this.broadcast("editSelf", this.data)
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      this.broadcast("addSelf", this.data);
 | 
								this.broadcast("addSelf", this.data)
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
    return this;
 | 
							return this
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  edit(f) {
 | 
						edit(f) {
 | 
				
			||||||
    if (this.exists()) {
 | 
							if (this.exists()) {
 | 
				
			||||||
      f(this.data);
 | 
								f(this.data)
 | 
				
			||||||
      this.set(this.data);
 | 
								this.set(this.data)
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      throw new Error("Tried to edit a SubscribeValue that had no value");
 | 
								throw new Error("Tried to edit a SubscribeValue that had no value")
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  delete() {
 | 
						delete() {
 | 
				
			||||||
    this.hasData = false;
 | 
							this.hasData = false
 | 
				
			||||||
    this.broadcast("removeSelf");
 | 
							this.broadcast("removeSelf")
 | 
				
			||||||
    return this;
 | 
							return this
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { SubscribeValue };
 | 
					export {SubscribeValue}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,17 +1,17 @@
 | 
				
			||||||
import { Subscribable } from "./Subscribable.js";
 | 
					import {Subscribable} from "./Subscribable.js"
 | 
				
			||||||
import { SubscribeMapList } from "./SubscribeMapList.js";
 | 
					import {SubscribeMapList} from "./SubscribeMapList.js"
 | 
				
			||||||
import { SubscribeSet } from "./SubscribeSet.js";
 | 
					import {SubscribeSet} from "./SubscribeSet.js"
 | 
				
			||||||
import { SubscribeValue } from "./SubscribeValue.js";
 | 
					import {SubscribeValue} from "./SubscribeValue.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const store = {
 | 
					const store = {
 | 
				
			||||||
  groups: new SubscribeMapList(SubscribeValue),
 | 
						groups: new SubscribeMapList(SubscribeValue),
 | 
				
			||||||
  rooms: new SubscribeMapList(SubscribeValue),
 | 
						rooms: new SubscribeMapList(SubscribeValue),
 | 
				
			||||||
  directs: new SubscribeSet(),
 | 
						directs: new SubscribeSet(),
 | 
				
			||||||
  activeGroup: new SubscribeValue(),
 | 
						activeGroup: new SubscribeValue(),
 | 
				
			||||||
  activeRoom: new SubscribeValue(),
 | 
						activeRoom: new SubscribeValue(),
 | 
				
			||||||
  newEvents: new Subscribable(),
 | 
						newEvents: new Subscribable()
 | 
				
			||||||
};
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
window.store = store;
 | 
					window.store = store
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { store };
 | 
					export {store}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,139 +1,130 @@
 | 
				
			||||||
import { store } from "../store/store.js";
 | 
					import {store} from "../store/store.js"
 | 
				
			||||||
import * as lsm from "../lsm.js";
 | 
					import * as lsm from "../lsm.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let lastBatch = null;
 | 
					let lastBatch = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function resolveMxc(url, size, method) {
 | 
					function resolveMxc(url, size, method) {
 | 
				
			||||||
  const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1);
 | 
						const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
 | 
				
			||||||
  if (size && method) {
 | 
						if (size && method) {
 | 
				
			||||||
    return `${lsm.get(
 | 
							return `${lsm.get("domain")}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`
 | 
				
			||||||
      "domain"
 | 
						} else {
 | 
				
			||||||
    )}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`;
 | 
							return `${lsm.get("domain")}/_matrix/media/r0/download/${server}/${id}`
 | 
				
			||||||
  } else {
 | 
						}
 | 
				
			||||||
    return `${lsm.get("domain")}/_matrix/media/r0/download/${server}/${id}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function sync() {
 | 
					function sync() {
 | 
				
			||||||
  const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/sync`);
 | 
						const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/sync`)
 | 
				
			||||||
  url.searchParams.append("access_token", lsm.get("access_token"));
 | 
						url.searchParams.append("access_token", lsm.get("access_token"))
 | 
				
			||||||
  const filter = {
 | 
						const filter = {
 | 
				
			||||||
    room: {
 | 
							room: {
 | 
				
			||||||
      // pulling more from the timeline massively increases download size
 | 
								// pulling more from the timeline massively increases download size
 | 
				
			||||||
      timeline: {
 | 
								timeline: {
 | 
				
			||||||
        limit: 5,
 | 
									limit: 5
 | 
				
			||||||
      },
 | 
								},
 | 
				
			||||||
      // members are not currently needed
 | 
								// members are not currently needed
 | 
				
			||||||
      state: {
 | 
								state: {
 | 
				
			||||||
        lazy_load_members: true,
 | 
									lazy_load_members: true
 | 
				
			||||||
      },
 | 
								}
 | 
				
			||||||
    },
 | 
							},
 | 
				
			||||||
    presence: {
 | 
							presence: {
 | 
				
			||||||
      // presence is not implemented, ignore it
 | 
								// presence is not implemented, ignore it
 | 
				
			||||||
      types: [],
 | 
								types: []
 | 
				
			||||||
    },
 | 
							}
 | 
				
			||||||
  };
 | 
						}
 | 
				
			||||||
  url.searchParams.append("filter", JSON.stringify(filter));
 | 
						url.searchParams.append("filter", JSON.stringify(filter))
 | 
				
			||||||
  url.searchParams.append("timeout", 20000);
 | 
						url.searchParams.append("timeout", 20000)
 | 
				
			||||||
  if (lastBatch) {
 | 
						if (lastBatch) {
 | 
				
			||||||
    url.searchParams.append("since", lastBatch);
 | 
							url.searchParams.append("since", lastBatch)
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
  return fetch(url.toString())
 | 
						return fetch(url.toString()).then(res => res.json()).then(root => {
 | 
				
			||||||
    .then((res) => res.json())
 | 
							lastBatch = root.next_batch
 | 
				
			||||||
    .then((root) => {
 | 
							return root
 | 
				
			||||||
      lastBatch = root.next_batch;
 | 
						})
 | 
				
			||||||
      return root;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function manageSync(root) {
 | 
					function manageSync(root) {
 | 
				
			||||||
  try {
 | 
						try {
 | 
				
			||||||
    let newEvents = false;
 | 
							let newEvents = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // set up directs
 | 
							// set up directs
 | 
				
			||||||
    const directs = root.account_data.events.find((e) => e.type === "m.direct");
 | 
							const directs = root.account_data.events.find(e => e.type === "m.direct")
 | 
				
			||||||
    if (directs) {
 | 
							if (directs) {
 | 
				
			||||||
      Object.values(directs.content).forEach((ids) => {
 | 
								Object.values(directs.content).forEach(ids => {
 | 
				
			||||||
        ids.forEach((id) => store.directs.add(id));
 | 
									ids.forEach(id => store.directs.add(id))
 | 
				
			||||||
      });
 | 
								})
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // set up rooms
 | 
							// set up rooms
 | 
				
			||||||
    Object.entries(root.rooms.join).forEach(([id, room]) => {
 | 
							Object.entries(root.rooms.join).forEach(([id, room]) => {
 | 
				
			||||||
      if (!store.rooms.has(id)) {
 | 
								if (!store.rooms.has(id)) {
 | 
				
			||||||
        store.rooms.askAdd(id, room);
 | 
									store.rooms.askAdd(id, room)
 | 
				
			||||||
      }
 | 
								}
 | 
				
			||||||
      const timeline = store.rooms.get(id).value().timeline;
 | 
								const timeline = store.rooms.get(id).value().timeline
 | 
				
			||||||
      if (room.timeline.events.length) newEvents = true;
 | 
								if (room.timeline.events.length) newEvents = true
 | 
				
			||||||
      timeline.updateEvents(room.timeline.events);
 | 
								timeline.updateEvents(room.timeline.events)
 | 
				
			||||||
    });
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // set up groups
 | 
							// set up groups
 | 
				
			||||||
    Promise.all(
 | 
							Promise.all(
 | 
				
			||||||
      Object.keys(root.groups.join).map((id) => {
 | 
								Object.keys(root.groups.join).map(id => {
 | 
				
			||||||
        if (!store.groups.has(id)) {
 | 
									if (!store.groups.has(id)) {
 | 
				
			||||||
          return Promise.all(
 | 
										return Promise.all(["profile", "rooms"].map(path => {
 | 
				
			||||||
            ["profile", "rooms"].map((path) => {
 | 
											const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/groups/${id}/${path}`)
 | 
				
			||||||
              const url = new URL(
 | 
											url.searchParams.append("access_token", lsm.get("access_token"))
 | 
				
			||||||
                `${lsm.get("domain")}/_matrix/client/r0/groups/${id}/${path}`
 | 
											return fetch(url.toString()).then(res => res.json())
 | 
				
			||||||
              );
 | 
										})).then(([profile, rooms]) => {
 | 
				
			||||||
              url.searchParams.append("access_token", lsm.get("access_token"));
 | 
											rooms = rooms.chunk
 | 
				
			||||||
              return fetch(url.toString()).then((res) => res.json());
 | 
											let order = 999
 | 
				
			||||||
            })
 | 
											let orderEvent = root.account_data.events.find(e => e.type === "im.vector.web.tag_ordering")
 | 
				
			||||||
          ).then(([profile, rooms]) => {
 | 
											if (orderEvent) {
 | 
				
			||||||
            rooms = rooms.chunk;
 | 
												if (orderEvent.content.tags.includes(id)) {
 | 
				
			||||||
            let order = 999;
 | 
													order = orderEvent.content.tags.indexOf(id)
 | 
				
			||||||
            let orderEvent = root.account_data.events.find(
 | 
												}
 | 
				
			||||||
              (e) => e.type === "im.vector.web.tag_ordering"
 | 
											}
 | 
				
			||||||
            );
 | 
											const data = {
 | 
				
			||||||
            if (orderEvent) {
 | 
												name: profile.name,
 | 
				
			||||||
              if (orderEvent.content.tags.includes(id)) {
 | 
												icon: resolveMxc(profile.avatar_url, 96, "crop"),
 | 
				
			||||||
                order = orderEvent.content.tags.indexOf(id);
 | 
												order
 | 
				
			||||||
              }
 | 
											}
 | 
				
			||||||
            }
 | 
											store.groups.askAdd(id, data)
 | 
				
			||||||
            const data = {
 | 
											rooms.forEach(groupRoom => {
 | 
				
			||||||
              name: profile.name,
 | 
												if (store.rooms.has(groupRoom.room_id)) {
 | 
				
			||||||
              icon: resolveMxc(profile.avatar_url, 96, "crop"),
 | 
													store.rooms.get(groupRoom.room_id).value().setGroup(id)
 | 
				
			||||||
              order,
 | 
												}
 | 
				
			||||||
            };
 | 
											})
 | 
				
			||||||
            store.groups.askAdd(id, data);
 | 
										})
 | 
				
			||||||
            rooms.forEach((groupRoom) => {
 | 
									}
 | 
				
			||||||
              if (store.rooms.has(groupRoom.room_id)) {
 | 
								})
 | 
				
			||||||
                store.rooms.get(groupRoom.room_id).value().setGroup(id);
 | 
							).then(() => {
 | 
				
			||||||
              }
 | 
								store.rooms.sort()
 | 
				
			||||||
            });
 | 
							})
 | 
				
			||||||
          });
 | 
							if (newEvents) store.newEvents.broadcast("changeSelf")
 | 
				
			||||||
        }
 | 
						} catch (e) {
 | 
				
			||||||
      })
 | 
							console.error(root)
 | 
				
			||||||
    ).then(() => {
 | 
							throw e
 | 
				
			||||||
      store.rooms.sort();
 | 
						}
 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    if (newEvents) store.newEvents.broadcast("changeSelf");
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error(root);
 | 
					 | 
				
			||||||
    throw e;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function syncLoop() {
 | 
					function syncLoop() {
 | 
				
			||||||
  return sync().then(manageSync).then(syncLoop);
 | 
						return sync().then(manageSync).then(syncLoop)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[
 | 
					;[
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    id: "directs",
 | 
							id: "directs",
 | 
				
			||||||
    name: "Directs",
 | 
							name: "Directs",
 | 
				
			||||||
    icon: "/static/directs.svg",
 | 
							icon: "/static/directs.svg",
 | 
				
			||||||
    order: -2,
 | 
							order: -2
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    id: "channels",
 | 
							id: "channels",
 | 
				
			||||||
    name: "Channels",
 | 
							name: "Channels",
 | 
				
			||||||
    icon: "/static/channels.svg",
 | 
							icon: "/static/channels.svg",
 | 
				
			||||||
    order: -1,
 | 
							order: -1
 | 
				
			||||||
  },
 | 
						}
 | 
				
			||||||
].forEach((data) => store.groups.askAdd(data.id, data));
 | 
					].forEach(data => store.groups.askAdd(data.id, data))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
store.activeGroup.set(store.groups.get("directs").value());
 | 
					store.activeGroup.set(store.groups.get("directs").value())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
syncLoop();
 | 
					syncLoop()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,36 +1,21 @@
 | 
				
			||||||
doctype html
 | 
					doctype html
 | 
				
			||||||
html
 | 
					html
 | 
				
			||||||
  head
 | 
						head
 | 
				
			||||||
    meta(charset="utf-8")
 | 
							meta(charset="utf-8")
 | 
				
			||||||
    link(rel="stylesheet", type="text/css", href=getStatic('/sass/main.sass'))
 | 
							link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass"))
 | 
				
			||||||
    title Carbon
 | 
							title Carbon
 | 
				
			||||||
  body
 | 
						body
 | 
				
			||||||
    main.main
 | 
							main.main
 | 
				
			||||||
      form
 | 
								form
 | 
				
			||||||
        div
 | 
									div
 | 
				
			||||||
          label(for="login") Username
 | 
										label(for="login") Username
 | 
				
			||||||
          input#login(
 | 
										input(type="text" name="login" autocomplete="username" placeholder="example:matrix.org" required)#login
 | 
				
			||||||
            type="text",
 | 
									div
 | 
				
			||||||
            name="login",
 | 
										label(for="password") Password
 | 
				
			||||||
            autocomplete="username",
 | 
										input(type="text" name="password" autocomplete="current-password" required)#password
 | 
				
			||||||
            placeholder="example:matrix.org",
 | 
									div
 | 
				
			||||||
            required
 | 
					
 | 
				
			||||||
          )
 | 
										label(for="homeserver") Homeserver
 | 
				
			||||||
        div
 | 
										input(type="text" name="homeserver" value="matrix.org" required)#homeserver
 | 
				
			||||||
          label(for="password") Password
 | 
									div
 | 
				
			||||||
          input#password(
 | 
										input(type="submit" value="Login")
 | 
				
			||||||
            type="text",
 | 
					 | 
				
			||||||
            name="password",
 | 
					 | 
				
			||||||
            autocomplete="current-password",
 | 
					 | 
				
			||||||
            required
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
        div
 | 
					 | 
				
			||||||
          label(for="homeserver") Homeserver
 | 
					 | 
				
			||||||
          input#homeserver(
 | 
					 | 
				
			||||||
            type="text",
 | 
					 | 
				
			||||||
            name="homeserver",
 | 
					 | 
				
			||||||
            value="matrix.org",
 | 
					 | 
				
			||||||
            required
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
        div
 | 
					 | 
				
			||||||
          input(type="submit", value="Login")
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue