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