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/build/index.html b/build/index.html
index 1fb6d0f..a14a855 100644
--- a/build/index.html
+++ b/build/index.html
@@ -2,10 +2,10 @@
-
+
-
-
+
+
Carbon
diff --git a/build/static/Timeline.js b/build/static/Timeline.js
index f412366..7c5a508 100644
--- a/build/static/Timeline.js
+++ b/build/static/Timeline.js
@@ -1,13 +1,6 @@
import {ElemJS, ejs} from "./basic.js"
import {Subscribable} from "./store/Subscribable.js"
import {Anchor} from "./Anchor.js"
-import * as lsm from "./lsm.js"
-
-let sentIndex = 0
-
-function getTxnId() {
- return Date.now() + (sentIndex++)
-}
function eventSearch(list, event, min = 0, max = -1) {
if (list.length === 0) return {success: false, i: 0}
@@ -35,35 +28,23 @@ class Event extends ElemJS {
super("div")
this.class("c-message")
this.data = null
- this.group = null
this.update(data)
}
- setGroup(group) {
- this.group = group
- }
-
update(data) {
this.data = data
this.render()
}
- removeEvent() {
- if (this.group) this.group.removeEvent(this)
- else this.remove()
- }
-
render() {
- this.element.classList[this.data.pending ? "add" : "remove"]("c-message--pending")
- this.text(this.data.content.body)
+ this.child(this.data.content.body)
}
}
class EventGroup extends ElemJS {
- constructor(reactive, list) {
+ constructor(list) {
super("div")
this.class("c-message-group")
- this.reactive = reactive
this.list = list
this.data = {
sender: list[0].data.sender,
@@ -85,20 +66,9 @@ class EventGroup extends ElemJS {
addEvent(event) {
const index = eventSearch(this.list, event).i
- event.setGroup(this)
this.list.splice(index, 0, event)
this.messages.childAt(index + 1, event)
}
-
- removeEvent(event) {
- const search = eventSearch(this.list, event)
- if (!search.success) throw new Error(`Event ${event.data.event_id} not found in this group`)
- const index = search.i
- // actually remove the event
- this.list.splice(index, 1)
- event.remove() // should get everything else
- if (this.list.length === 0) this.reactive.removeGroup(this)
- }
}
class ReactiveTimeline extends ElemJS {
@@ -120,7 +90,7 @@ class ReactiveTimeline extends ElemJS {
const success = indices.some(i => {
if (!this.list[i]) {
// if (printed++ < 100) console.log("tryadd success, created group")
- const group = new EventGroup(this, [event])
+ const group = new EventGroup([event])
this.list.splice(i, 0, group)
this.childAt(i, group)
return true
@@ -133,12 +103,6 @@ class ReactiveTimeline extends ElemJS {
if (!success) console.log("tryadd failure", indices, this.list.map(l => l.data.sender), event.data)
}
- removeGroup(group) {
- const index = this.list.indexOf(group)
- this.list.splice(index, 1)
- group.remove() // should get everything else
- }
-
render() {
this.clearChildren()
this.list.forEach(group => this.child(group))
@@ -148,84 +112,36 @@ class ReactiveTimeline extends ElemJS {
}
class Timeline extends Subscribable {
- constructor(id) {
+ constructor() {
super()
Object.assign(this.events, {
- beforeChange: [],
- afterChange: []
+ beforeChange: []
})
Object.assign(this.eventDeps, {
- beforeChange: [],
- afterChange: []
+ beforeChange: []
})
- this.id = id
this.list = []
this.map = new Map()
this.reactiveTimeline = new ReactiveTimeline([])
this.latest = 0
- this.pending = new Set()
}
updateEvents(events) {
this.broadcast("beforeChange")
for (const eventData of events) {
this.latest = Math.max(this.latest, eventData.origin_server_ts)
- let id = eventData.event_id
- if (eventData.sender === lsm.get("mx_user_id") && eventData.content && this.pending.has(eventData.content["chat.carbon.message.pending_id"])) {
- id = eventData.content["chat.carbon.message.pending_id"]
- }
- if (this.map.has(id)) {
- this.map.get(id).update(eventData)
+ if (this.map.has(eventData.event_id)) {
+ this.map.get(eventData.event_id).update(eventData)
} else {
const event = new Event(eventData)
- this.map.set(id, event)
this.reactiveTimeline.addEvent(event)
}
}
- this.broadcast("afterChange")
- }
-
- removeEvent(id) {
- if (!this.map.has(id)) throw new Error(`Tried to delete event ID ${id} which does not exist`)
- this.map.get(id).removeEvent()
- this.map.delete(id)
}
getTimeline() {
return this.reactiveTimeline
}
-
- send(body) {
- const tx = getTxnId()
- const id = `pending$${tx}`
- this.pending.add(id)
- const content = {
- msgtype: "m.text",
- body,
- "chat.carbon.message.pending_id": id
- }
- const fakeEvent = {
- origin_server_ts: Date.now(),
- event_id: id,
- sender: lsm.get("mx_user_id"),
- content,
- pending: true
- }
- this.updateEvents([fakeEvent])
- return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${this.id}/send/m.room.message/${tx}?access_token=${lsm.get("access_token")}`, {
- method: "PUT",
- body: JSON.stringify(content),
- headers: {
- "Content-Type": "application/json"
- }
- })/*.then(() => {
- const subscription = () => {
- this.removeEvent(id)
- this.unsubscribe("afterChange", subscription)
- }
- this.subscribe("afterChange", subscription)
- })*/
- }
/*
getGroupedEvents() {
let currentSender = Symbol("N/A")
diff --git a/build/static/chat-input.js b/build/static/chat-input.js
index 08ee236..d8c0a35 100644
--- a/build/static/chat-input.js
+++ b/build/static/chat-input.js
@@ -3,6 +3,8 @@ import {store} from "./store/store.js"
import * as lsm from "./lsm.js"
import {chat} from "./chat.js"
+let sentIndex = 0
+
const input = q("#c-chat-textarea")
store.activeRoom.subscribe("changeSelf", () => {
@@ -31,7 +33,21 @@ function fixHeight() {
input.style.height = (input.scrollHeight + 1) + "px"
}
+function getTxnId() {
+ return Date.now() + (sentIndex++)
+}
+
function send(body) {
if (!store.activeRoom.exists()) return
- return store.activeRoom.value().timeline.send(body)
+ 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/build/static/main.css b/build/static/main.css
index a813b7e..af591b6 100644
--- a/build/static/main.css
+++ b/build/static/main.css
@@ -182,11 +182,6 @@ body {
.c-message {
margin-top: 4px;
- opacity: 1;
- transition: opacity 0.2s ease-out;
-}
-.c-message--pending {
- opacity: 0.5;
}
.c-message-event {
diff --git a/build/static/room-picker.js b/build/static/room-picker.js
index 3de2ab1..9fc99f5 100644
--- a/build/static/room-picker.js
+++ b/build/static/room-picker.js
@@ -68,7 +68,7 @@ class Room extends ElemJS {
this.id = id
this.data = data
- this.timeline = new Timeline(this.id)
+ this.timeline = new Timeline()
this.group = null
this.class("c-room")
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 f412366..da4505b 100644
--- a/src/js/Timeline.js
+++ b/src/js/Timeline.js
@@ -1,232 +1,171 @@
-import {ElemJS, ejs} from "./basic.js"
-import {Subscribable} from "./store/Subscribable.js"
-import {Anchor} from "./Anchor.js"
-import * as lsm from "./lsm.js"
-
-let sentIndex = 0
-
-function getTxnId() {
- return Date.now() + (sentIndex++)
-}
+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.group = null
- this.update(data)
- }
+ constructor(data) {
+ super("div");
+ this.class("c-message");
+ this.data = null;
+ this.update(data);
+ }
- setGroup(group) {
- this.group = group
- }
+ update(data) {
+ this.data = data;
+ this.render();
+ }
- update(data) {
- this.data = data
- this.render()
- }
-
- removeEvent() {
- if (this.group) this.group.removeEvent(this)
- else this.remove()
- }
-
- render() {
- this.element.classList[this.data.pending ? "add" : "remove"]("c-message--pending")
- this.text(this.data.content.body)
- }
+ render() {
+ this.child(this.data.content.body);
+ }
}
class EventGroup extends ElemJS {
- constructor(reactive, list) {
- super("div")
- this.class("c-message-group")
- this.reactive = reactive
- 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
- event.setGroup(this)
- this.list.splice(index, 0, event)
- this.messages.childAt(index + 1, event)
- }
-
- removeEvent(event) {
- const search = eventSearch(this.list, event)
- if (!search.success) throw new Error(`Event ${event.data.event_id} not found in this group`)
- const index = search.i
- // actually remove the event
- this.list.splice(index, 1)
- event.remove() // should get everything else
- if (this.list.length === 0) this.reactive.removeGroup(this)
- }
+ 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(this, [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
+ );
+ }
- removeGroup(group) {
- const index = this.list.indexOf(group)
- this.list.splice(index, 1)
- group.remove() // should get everything else
- }
-
- 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(id) {
- super()
- Object.assign(this.events, {
- beforeChange: [],
- afterChange: []
- })
- Object.assign(this.eventDeps, {
- beforeChange: [],
- afterChange: []
- })
- this.id = id
- this.list = []
- this.map = new Map()
- this.reactiveTimeline = new ReactiveTimeline([])
- this.latest = 0
- this.pending = new Set()
- }
+ 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)
- let id = eventData.event_id
- if (eventData.sender === lsm.get("mx_user_id") && eventData.content && this.pending.has(eventData.content["chat.carbon.message.pending_id"])) {
- id = eventData.content["chat.carbon.message.pending_id"]
- }
- if (this.map.has(id)) {
- this.map.get(id).update(eventData)
- } else {
- const event = new Event(eventData)
- this.map.set(id, event)
- this.reactiveTimeline.addEvent(event)
- }
- }
- this.broadcast("afterChange")
- }
+ 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);
+ }
+ }
+ }
- removeEvent(id) {
- if (!this.map.has(id)) throw new Error(`Tried to delete event ID ${id} which does not exist`)
- this.map.get(id).removeEvent()
- this.map.delete(id)
- }
-
- getTimeline() {
- return this.reactiveTimeline
- }
-
- send(body) {
- const tx = getTxnId()
- const id = `pending$${tx}`
- this.pending.add(id)
- const content = {
- msgtype: "m.text",
- body,
- "chat.carbon.message.pending_id": id
- }
- const fakeEvent = {
- origin_server_ts: Date.now(),
- event_id: id,
- sender: lsm.get("mx_user_id"),
- content,
- pending: true
- }
- this.updateEvents([fakeEvent])
- return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${this.id}/send/m.room.message/${tx}?access_token=${lsm.get("access_token")}`, {
- method: "PUT",
- body: JSON.stringify(content),
- headers: {
- "Content-Type": "application/json"
- }
- })/*.then(() => {
- const subscription = () => {
- this.removeEvent(id)
- this.unsubscribe("afterChange", subscription)
- }
- this.subscribe("afterChange", subscription)
- })*/
- }
-/*
+ getTimeline() {
+ return this.reactiveTimeline;
+ }
+ /*
getGroupedEvents() {
let currentSender = Symbol("N/A")
let groups = []
@@ -246,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 08ee236..6bf3e2f 100644
--- a/src/js/chat-input.js
+++ b/src/js/chat-input.js
@@ -1,37 +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";
-const input = q("#c-chat-textarea")
+let sentIndex = 0;
+
+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++;
}
function send(body) {
- if (!store.activeRoom.exists()) return
- return store.activeRoom.value().timeline.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",
+ },
+ }
+ );
}
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 3de2ab1..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.id)
- 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")
diff --git a/src/sass/components/messages.sass b/src/sass/components/messages.sass
index 6284254..779cd58 100644
--- a/src/sass/components/messages.sass
+++ b/src/sass/components/messages.sass
@@ -44,11 +44,6 @@
.c-message
margin-top: 4px
- opacity: 1
- transition: opacity 0.2s ease-out
-
- &--pending
- opacity: 0.5
.c-message-event
padding-top: 10px