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