Compare commits
No commits in common. "6224cde1327d8cf3a7d67497eac7524fc4ebea57" and "a5309a81b1f84b5cf81070b7fd6a68a463ca8ae8" have entirely different histories.
6224cde132
...
a5309a81b1
13 changed files with 269 additions and 539 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -288,10 +288,6 @@ modules.xml
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/node,vscode,webstorm,webstorm+all
|
# End of https://www.toptal.com/developers/gitignore/api/node,vscode,webstorm,webstorm+all
|
||||||
|
|
||||||
# Emacs
|
|
||||||
*~
|
|
||||||
\#*#
|
|
||||||
|
|
||||||
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
|
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
|
||||||
|
|
||||||
/build/
|
/build/
|
||||||
|
|
65
build.js
65
build.js
|
@ -9,8 +9,7 @@ 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
|
||||||
const browserify = require("browserify")
|
const browserify = require('browserify')
|
||||||
const {Transform} = require("stream")
|
|
||||||
|
|
||||||
process.chdir(pj(__dirname, "src"))
|
process.chdir(pj(__dirname, "src"))
|
||||||
|
|
||||||
|
@ -18,10 +17,10 @@ const buildDir = "../build"
|
||||||
|
|
||||||
const validationQueue = []
|
const validationQueue = []
|
||||||
const validationHost = os.hostname() === "future" ? "http://localhost:8888/" : "http://validator.w3.org/nu/"
|
const validationHost = os.hostname() === "future" ? "http://localhost:8888/" : "http://validator.w3.org/nu/"
|
||||||
const staticFiles = new Map()
|
const static_files = new Map()
|
||||||
const links = new Map()
|
const links = new Map()
|
||||||
const sources = new Map()
|
const sources = new Map()
|
||||||
const pugLocals = {static: staticFiles, links}
|
const pugLocals = {static: static_files, links}
|
||||||
|
|
||||||
const spec = require("./spec.js")
|
const spec = require("./spec.js")
|
||||||
|
|
||||||
|
@ -96,7 +95,6 @@ function runHint(filename, source) {
|
||||||
globals: ["console", "URLSearchParams", "staticFiles"],
|
globals: ["console", "URLSearchParams", "staticFiles"],
|
||||||
browser: true,
|
browser: true,
|
||||||
asi: true,
|
asi: true,
|
||||||
node: true
|
|
||||||
})
|
})
|
||||||
const result = hint.data()
|
const result = hint.data()
|
||||||
let problems = 0
|
let problems = 0
|
||||||
|
@ -129,52 +127,33 @@ function runHint(filename, source) {
|
||||||
|
|
||||||
async function addFile(sourcePath, targetPath) {
|
async function addFile(sourcePath, targetPath) {
|
||||||
const contents = await fs.promises.readFile(pj(".", sourcePath), {encoding: null});
|
const contents = await fs.promises.readFile(pj(".", sourcePath), {encoding: null});
|
||||||
staticFiles.set(sourcePath, `${targetPath}?static=${hash(contents)}`)
|
static_files.set(sourcePath, `${targetPath}?static=${hash(contents)}`)
|
||||||
await fs.promises.writeFile(pj(buildDir, targetPath), contents)
|
await fs.promises.writeFile(pj(buildDir, targetPath), contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadJS(sourcePath, targetPath) {
|
async function loadJS(sourcePath, targetPath) {
|
||||||
let content = await fs.promises.readFile(pj(".", sourcePath), {encoding: "utf8"})
|
let content = await fs.promises.readFile(pj(".", sourcePath), {encoding: "utf8"})
|
||||||
sources.set(sourcePath, content)
|
sources.set(sourcePath, content);
|
||||||
staticFiles.set(sourcePath, `${targetPath}?static=${hash(content)}`)
|
static_files.set(sourcePath, `${targetPath}?static=${hash(content)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addJS(sourcePath, targetPath) {
|
async function addJS(sourcePath, targetPath) {
|
||||||
let content = sources.get(sourcePath)
|
let content = sources.get(sourcePath)
|
||||||
|
// resolve imports to hashed paths
|
||||||
|
content = content.replace(/\$to_relative "([^"]+)"/g, function(_, file) {
|
||||||
|
if (!static_files.get(file)) throw new Error(`Tried to relative import ${file} from ${sourcePath}, but import not found`)
|
||||||
|
return '"' + getRelative(targetPath, static_files.get(file)) + '"'
|
||||||
|
})
|
||||||
runHint(sourcePath, content)
|
runHint(sourcePath, content)
|
||||||
await fs.promises.writeFile(pj(buildDir, targetPath), content)
|
await fs.promises.writeFile(pj(buildDir, targetPath), content)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addBundle(sourcePath, targetPath) {
|
async function addBundle(sourcePath, targetPath) {
|
||||||
const content = await new Promise(resolve => {
|
await browserify()
|
||||||
browserify()
|
|
||||||
.add(pj(".", sourcePath))
|
.add(pj(".", sourcePath))
|
||||||
.transform(file => {
|
.bundle()
|
||||||
let content = ""
|
.pipe(fs.createWriteStream(pj(buildDir, targetPath)));
|
||||||
const transform = new Transform({
|
static_files.set(sourcePath, targetPath)
|
||||||
transform(chunk, encoding, callback) {
|
|
||||||
content += chunk.toString()
|
|
||||||
callback(null, chunk)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
transform.on("finish", () => {
|
|
||||||
const relativePath = path.relative(process.cwd(), file).replace(/^\/*/, "/")
|
|
||||||
runHint(relativePath, content)
|
|
||||||
})
|
|
||||||
return transform
|
|
||||||
})
|
|
||||||
.bundle((err, res) => {
|
|
||||||
if (err) {
|
|
||||||
delete err.stream
|
|
||||||
throw err // Quit; problem parsing file to bundle
|
|
||||||
}
|
|
||||||
resolve(res)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const writer = fs.promises.writeFile(pj(buildDir, targetPath), content)
|
|
||||||
staticFiles.set(sourcePath, `${targetPath}?static=${hash(content)}`)
|
|
||||||
runHint(sourcePath, content)
|
|
||||||
await writer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addSass(sourcePath, targetPath) {
|
async function addSass(sourcePath, targetPath) {
|
||||||
|
@ -188,7 +167,7 @@ async function addSass(sourcePath, targetPath) {
|
||||||
if (!(name instanceof sass.types.String)) {
|
if (!(name instanceof sass.types.String)) {
|
||||||
throw "$name: expected a string"
|
throw "$name: expected a string"
|
||||||
}
|
}
|
||||||
const result = getRelative(targetPath, staticFiles.get(name.getValue()))
|
const result = getRelative(targetPath, static_files.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 {
|
||||||
|
@ -197,8 +176,8 @@ async function addSass(sourcePath, targetPath) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).css;
|
}).css;
|
||||||
staticFiles.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`)
|
static_files.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`)
|
||||||
validate(sourcePath, renderedCSS, "css")
|
await validate(sourcePath, renderedCSS, "css")
|
||||||
await fs.promises.writeFile(pj(buildDir, targetPath), renderedCSS)
|
await fs.promises.writeFile(pj(buildDir, targetPath), renderedCSS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,17 +186,17 @@ async function addPug(sourcePath, targetPath) {
|
||||||
return getRelative(targetPath, staticTarget)
|
return getRelative(targetPath, staticTarget)
|
||||||
}
|
}
|
||||||
function getStatic(target) {
|
function getStatic(target) {
|
||||||
return getRelativeHere(staticFiles.get(target))
|
return getRelativeHere(static_files.get(target))
|
||||||
}
|
}
|
||||||
function getStaticName(target) {
|
function getStaticName(target) {
|
||||||
return getRelativeHere(staticFiles.get(target)).replace(/\?.*$/, "")
|
return getRelativeHere(static_files.get(target)).replace(/\?.*$/, "")
|
||||||
}
|
}
|
||||||
function getLink(target) {
|
function getLink(target) {
|
||||||
return getRelativeHere(links.get(target))
|
return getRelativeHere(links.get(target))
|
||||||
}
|
}
|
||||||
const renderedHTML = pug.compileFile(pj(".", sourcePath), {pretty: true})({getStatic, getStaticName, getLink, ...pugLocals})
|
const renderedHTML = pug.compileFile(pj(".", sourcePath), {pretty: true})({getStatic, getStaticName, getLink, ...pugLocals})
|
||||||
let renderedWithoutPHP = renderedHTML.replace(/<\?(?:php|=).*?\?>/gsm, "")
|
let renderedWithoutPHP = renderedHTML.replace(/<\?(?:php|=).*?\?>/gsm, "")
|
||||||
validate(sourcePath, renderedWithoutPHP, "html")
|
await validate(sourcePath, renderedWithoutPHP, "html")
|
||||||
await fs.promises.writeFile(pj(buildDir, targetPath), renderedHTML)
|
await fs.promises.writeFile(pj(buildDir, targetPath), renderedHTML)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,7 +224,7 @@ async function addBabel(sourcePath, targetPath) {
|
||||||
|
|
||||||
const filenameWithQuery = `${targetPath}?static=${hash(compiled.code)}`;
|
const filenameWithQuery = `${targetPath}?static=${hash(compiled.code)}`;
|
||||||
|
|
||||||
staticFiles.set(sourcePath, filenameWithQuery)
|
static_files.set(sourcePath, filenameWithQuery)
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fs.promises.writeFile(pj(buildDir, targetPath), originalCode),
|
fs.promises.writeFile(pj(buildDir, targetPath), originalCode),
|
||||||
|
|
351
package-lock.json
generated
351
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -11,17 +11,18 @@
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
"browserify": "^17.0.0",
|
||||||
|
"tippy.js": "^6.2.7"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.11.1",
|
"@babel/core": "^7.11.1",
|
||||||
"@babel/preset-env": "^7.11.0",
|
"@babel/preset-env": "^7.11.0",
|
||||||
"browserify": "^17.0.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",
|
||||||
"pug": "^3.0.0",
|
"pug": "^3.0.0",
|
||||||
"sass": "^1.26.10",
|
"sass": "^1.26.10"
|
||||||
"tippy.js": "^6.2.7"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
80
spec.js
80
spec.js
|
@ -19,6 +19,86 @@ module.exports = [
|
||||||
source: "/js/main.js",
|
source: "/js/main.js",
|
||||||
target: "/static/bundle.js"
|
target: "/static/bundle.js"
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/main.js",
|
||||||
|
// target: "/static/main.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/basic.js",
|
||||||
|
// target: "/static/basic.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/groups.js",
|
||||||
|
// target: "/static/groups.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/chat-input.js",
|
||||||
|
// target: "/static/chat-input.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/room-picker.js",
|
||||||
|
// target: "/static/room-picker.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/store/store.js",
|
||||||
|
// target: "/static/store/store.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/store/subscribable.js",
|
||||||
|
// target: "/static/store/subscribable.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/store/subscribe_value.js",
|
||||||
|
// target: "/static/store/subscribe_value.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/store/subscribe_map_list.js",
|
||||||
|
// target: "/static/store/subscribe_map_list.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/store/subscribe_set.js",
|
||||||
|
// target: "/static/store/subscribe_set.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/sync/sync.js",
|
||||||
|
// target: "/static/sync/sync.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/lsm.js",
|
||||||
|
// target: "/static/lsm.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/timeline.js",
|
||||||
|
// target: "/static/timeline.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/anchor.js",
|
||||||
|
// target: "/static/anchor.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/chat.js",
|
||||||
|
// target: "/static/chat.js",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: "js",
|
||||||
|
// source: "/js/functions.js",
|
||||||
|
// target: "/static/functions.js",
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
type: "file",
|
type: "file",
|
||||||
source: "/assets/fonts/whitney-500.woff",
|
source: "/assets/fonts/whitney-500.woff",
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Chat extends ElemJS {
|
||||||
// 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 beforeChangeSubscription = () => {
|
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.
|
||||||
|
@ -40,29 +40,12 @@ class Chat extends ElemJS {
|
||||||
}
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
this.addSubscription("beforeChange", timeline, beforeChangeSubscription)
|
const name = "beforeChange"
|
||||||
|
this.removableSubscriptions.push({name, target: timeline, subscription})
|
||||||
// Make sure after loading scrollback we don't move the scroll position
|
timeline.subscribe(name, subscription)
|
||||||
const beforeScrollbackLoadSubscription = () => {
|
|
||||||
const lastScrollHeight = chatMessages.scrollHeight;
|
|
||||||
|
|
||||||
const afterScrollbackLoadSub = () => {
|
|
||||||
const scrollDiff = chatMessages.scrollHeight - lastScrollHeight;
|
|
||||||
chatMessages.scrollTop += scrollDiff;
|
|
||||||
|
|
||||||
timeline.unsubscribe("afterScrollbackLoad", afterScrollbackLoadSub)
|
|
||||||
}
|
|
||||||
|
|
||||||
timeline.subscribe("afterScrollbackLoad", afterScrollbackLoadSub)
|
|
||||||
}
|
|
||||||
this.addSubscription("beforeScrollbackLoad", timeline, beforeScrollbackLoadSubscription)
|
|
||||||
}
|
}
|
||||||
this.render()
|
this.render()
|
||||||
}
|
}
|
||||||
addSubscription(name, target, subscription) {
|
|
||||||
this.removableSubscriptions.push({name, target, subscription})
|
|
||||||
target.subscribe(name, subscription)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.clearChildren()
|
this.clearChildren()
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
const lsm = require("./lsm.js")
|
const lsm = require("./lsm.js")
|
||||||
|
|
||||||
function resolveMxc(url, size, method) {
|
function resolveMxc(url, size, method) {
|
||||||
let [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
|
const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
|
||||||
id = id.replace(/#.*$/, "")
|
|
||||||
if (size && method) {
|
if (size && method) {
|
||||||
return `${lsm.get("domain")}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`
|
return `${lsm.get("domain")}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`
|
||||||
} else {
|
} else {
|
||||||
|
|
120
src/js/sender.js
120
src/js/sender.js
|
@ -1,120 +0,0 @@
|
||||||
const {ElemJS, ejs} = require("./basic.js")
|
|
||||||
const {store} = require("./store/store.js")
|
|
||||||
const {resolveMxc} = require("./functions.js")
|
|
||||||
|
|
||||||
function nameToColor(str) {
|
|
||||||
// code from element's react sdk
|
|
||||||
const colors = ["#55a7f0", "#da55ff", "#1bc47c", "#ea657e", "#fd8637", "#22cec6", "#8c8de3", "#71bf22"]
|
|
||||||
let hash = 0
|
|
||||||
let i
|
|
||||||
let chr
|
|
||||||
if (str.length === 0) {
|
|
||||||
return hash
|
|
||||||
}
|
|
||||||
for (i = 0; i < str.length; i++) {
|
|
||||||
chr = str.charCodeAt(i)
|
|
||||||
hash = ((hash << 5) - hash) + chr
|
|
||||||
hash |= 0
|
|
||||||
}
|
|
||||||
hash = Math.abs(hash) % 8
|
|
||||||
return colors[hash]
|
|
||||||
}
|
|
||||||
|
|
||||||
class Avatar extends ElemJS {
|
|
||||||
constructor() {
|
|
||||||
super("div")
|
|
||||||
this.class("c-message-group__avatar")
|
|
||||||
|
|
||||||
this.mxc = undefined
|
|
||||||
this.image = null
|
|
||||||
|
|
||||||
this.update(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
update(mxc) {
|
|
||||||
if (mxc === this.mxc) return
|
|
||||||
this.mxc = mxc
|
|
||||||
this.hasImage = !!mxc
|
|
||||||
if (this.hasImage) {
|
|
||||||
const size = 96
|
|
||||||
const url = resolveMxc(mxc, size, "crop")
|
|
||||||
this.image = ejs("img").class("c-message-group__icon").attribute("src", url).attribute("width", size).attribute("height", size)
|
|
||||||
this.image.on("error", this.onError.bind(this))
|
|
||||||
}
|
|
||||||
this.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
onError() {
|
|
||||||
this.hasImage = false
|
|
||||||
this.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.clearChildren()
|
|
||||||
if (this.hasImage) {
|
|
||||||
this.child(this.image)
|
|
||||||
} else {
|
|
||||||
this.child(
|
|
||||||
ejs("div").class("c-message-group__icon", "c-message-group__icon--no-icon")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Must update at least once to render. */
|
|
||||||
class Name extends ElemJS {
|
|
||||||
constructor() {
|
|
||||||
super("div")
|
|
||||||
this.class("c-message-group__name")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keeps track of whether we have the proper display name or not.
|
|
||||||
* If we do, then we shoudn't override it with the mxid if the name becomes unavailable.
|
|
||||||
*/
|
|
||||||
this.hasName = false
|
|
||||||
this.name = ""
|
|
||||||
this.mxid = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
update(event) {
|
|
||||||
this.mxid = event.state_key
|
|
||||||
if (event.content.displayname) {
|
|
||||||
this.hasName = true
|
|
||||||
this.name = event.content.displayname
|
|
||||||
} else if (!this.hasName) {
|
|
||||||
this.name = this.mxid
|
|
||||||
}
|
|
||||||
this.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
// set text
|
|
||||||
this.text(this.name)
|
|
||||||
// set color
|
|
||||||
this.style("color", nameToColor(this.mxid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Sender {
|
|
||||||
constructor(roomID, mxid) {
|
|
||||||
this.sender = store.rooms.get(roomID).value().members.get(mxid)
|
|
||||||
this.name = new Name()
|
|
||||||
this.avatar = new Avatar()
|
|
||||||
this.sender.subscribe("changeSelf", this.update.bind(this))
|
|
||||||
this.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (this.sender.exists()) {
|
|
||||||
// name
|
|
||||||
this.name.update(this.sender.value())
|
|
||||||
|
|
||||||
// avatar
|
|
||||||
this.avatar.update(this.sender.value().content.avatar_url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Sender
|
|
||||||
}
|
|
|
@ -2,8 +2,8 @@ const {ElemJS, ejs} = require("./basic.js")
|
||||||
const {Subscribable} = require("./store/subscribable.js")
|
const {Subscribable} = require("./store/subscribable.js")
|
||||||
const {store} = require("./store/store.js")
|
const {store} = require("./store/store.js")
|
||||||
const {Anchor} = require("./anchor.js")
|
const {Anchor} = require("./anchor.js")
|
||||||
const {Sender} = require("./sender.js")
|
|
||||||
const lsm = require("./lsm.js")
|
const lsm = require("./lsm.js")
|
||||||
|
const {resolveMxc} = require("./functions.js")
|
||||||
|
|
||||||
let debug = false
|
let debug = false
|
||||||
|
|
||||||
|
@ -100,6 +100,41 @@ class Event extends ElemJS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Sender {
|
||||||
|
constructor(roomID, mxid) {
|
||||||
|
this.sender = store.rooms.get(roomID).value().members.get(mxid)
|
||||||
|
this.sender.subscribe("changeSelf", this.update.bind(this))
|
||||||
|
this.name = new ElemJS("div").class("c-message-group__name")
|
||||||
|
this.avatar = new ElemJS("div").class("c-message-group__avatar")
|
||||||
|
this.displayingGoodData = false
|
||||||
|
this.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.sender.exists()) {
|
||||||
|
// name
|
||||||
|
if (this.sender.value().content.displayname) {
|
||||||
|
this.name.text(this.sender.value().content.displayname)
|
||||||
|
this.displayingGoodData = true
|
||||||
|
} else if (!this.displayingGoodData) {
|
||||||
|
this.name.text(this.sender.value().state_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// avatar
|
||||||
|
this.avatar.clearChildren()
|
||||||
|
if (this.sender.value().content.avatar_url) {
|
||||||
|
this.avatar.child(
|
||||||
|
ejs("img").class("c-message-group__icon").attribute("src", resolveMxc(this.sender.value().content.avatar_url, 96, "crop"))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.avatar.child(
|
||||||
|
ejs("div").class("c-message-group__icon", "c-message-group__icon--no-icon")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class EventGroup extends ElemJS {
|
class EventGroup extends ElemJS {
|
||||||
constructor(reactive, list) {
|
constructor(reactive, list) {
|
||||||
super("div")
|
super("div")
|
||||||
|
@ -141,43 +176,16 @@ class EventGroup extends ElemJS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Displays a spinner and creates an event to notify timeline to load more messages */
|
|
||||||
class LoadMore extends ElemJS {
|
|
||||||
constructor(id) {
|
|
||||||
super("div")
|
|
||||||
this.class("c-message-notice")
|
|
||||||
this.id = id
|
|
||||||
|
|
||||||
this.child(
|
|
||||||
ejs("div").class("c-message-notice__inner").child(
|
|
||||||
ejs("span").class("loading-icon"),
|
|
||||||
ejs("span").text("Loading more...")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
const intersection_observer = new IntersectionObserver(e => this.intersectionHandler(e))
|
|
||||||
intersection_observer.observe(this.element)
|
|
||||||
}
|
|
||||||
|
|
||||||
intersectionHandler(e) {
|
|
||||||
if (e.some(e => e.isIntersecting)) {
|
|
||||||
store.rooms.get(this.id).value().timeline.loadScrollback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReactiveTimeline extends ElemJS {
|
class ReactiveTimeline extends ElemJS {
|
||||||
constructor(id, list) {
|
constructor(id, list) {
|
||||||
super("div")
|
super("div")
|
||||||
this.class("c-event-groups")
|
this.class("c-event-groups")
|
||||||
this.id = id
|
this.id = id
|
||||||
this.list = list
|
this.list = list
|
||||||
this.loadMore = new LoadMore(this.id)
|
|
||||||
this.render()
|
this.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
addEvent(event) {
|
addEvent(event) {
|
||||||
this.loadMore.remove()
|
|
||||||
// if (debug) console.log("running search", this.list, event)
|
// if (debug) console.log("running search", this.list, event)
|
||||||
// if (debug) debugger;
|
// if (debug) debugger;
|
||||||
const search = eventSearch(this.list, event)
|
const search = eventSearch(this.list, event)
|
||||||
|
@ -193,8 +201,6 @@ class ReactiveTimeline extends ElemJS {
|
||||||
} else {
|
} else {
|
||||||
this.tryAddGroups(event, [search.i])
|
this.tryAddGroups(event, [search.i])
|
||||||
}
|
}
|
||||||
this.loadMore = new LoadMore(this.id)
|
|
||||||
this.childAt(0, this.loadMore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tryAddGroups(event, indices) {
|
tryAddGroups(event, indices) {
|
||||||
|
@ -227,7 +233,6 @@ class ReactiveTimeline extends ElemJS {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.clearChildren()
|
this.clearChildren()
|
||||||
this.child(this.loadMore)
|
|
||||||
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)
|
||||||
|
@ -239,15 +244,11 @@ class Timeline extends Subscribable {
|
||||||
super()
|
super()
|
||||||
Object.assign(this.events, {
|
Object.assign(this.events, {
|
||||||
beforeChange: [],
|
beforeChange: [],
|
||||||
afterChange: [],
|
afterChange: []
|
||||||
beforeScrollbackLoad: [],
|
|
||||||
afterScrollbackLoad: [],
|
|
||||||
})
|
})
|
||||||
Object.assign(this.eventDeps, {
|
Object.assign(this.eventDeps, {
|
||||||
beforeChange: [],
|
beforeChange: [],
|
||||||
afterChange: [],
|
afterChange: []
|
||||||
beforeScrollbackLoad: [],
|
|
||||||
afterScrollbackLoad: [],
|
|
||||||
})
|
})
|
||||||
this.room = room
|
this.room = room
|
||||||
this.id = this.room.id
|
this.id = this.room.id
|
||||||
|
@ -266,11 +267,7 @@ class Timeline extends Subscribable {
|
||||||
if (eventData.type === "m.room.member") {
|
if (eventData.type === "m.room.member") {
|
||||||
// update members
|
// update members
|
||||||
if (eventData.membership !== "leave") {
|
if (eventData.membership !== "leave") {
|
||||||
const member = this.room.members.get(eventData.state_key)
|
this.room.members.get(eventData.state_key).set(eventData)
|
||||||
// only use the latest state
|
|
||||||
if (!member.exists() || eventData.origin_server_ts > member.data.origin_server_ts) {
|
|
||||||
member.set(eventData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -352,27 +349,16 @@ class Timeline extends Subscribable {
|
||||||
url.searchParams.set("access_token", lsm.get("access_token"))
|
url.searchParams.set("access_token", lsm.get("access_token"))
|
||||||
url.searchParams.set("from", this.from)
|
url.searchParams.set("from", this.from)
|
||||||
url.searchParams.set("dir", "b")
|
url.searchParams.set("dir", "b")
|
||||||
url.searchParams.set("limit", "20")
|
url.searchParams.set("limit", 10)
|
||||||
const filter = {
|
const filter = {
|
||||||
lazy_load_members: true
|
lazy_load_members: true
|
||||||
}
|
}
|
||||||
url.searchParams.set("filter", JSON.stringify(filter))
|
url.searchParams.set("filter", JSON.stringify(filter))
|
||||||
|
|
||||||
const root = await fetch(url.toString()).then(res => res.json())
|
const root = await fetch(url.toString()).then(res => res.json())
|
||||||
|
|
||||||
this.broadcast("beforeScrollbackLoad")
|
|
||||||
|
|
||||||
this.from = root.end
|
this.from = root.end
|
||||||
// console.log(this.updateEvents, root.chunk)
|
console.log(this.updateEvents, root.chunk)
|
||||||
if (root.state) this.updateStateEvents(root.state)
|
if (root.state) this.updateStateEvents(root.state)
|
||||||
if (root.chunk.length) {
|
|
||||||
// there are events to display
|
|
||||||
this.updateEvents(root.chunk)
|
this.updateEvents(root.chunk)
|
||||||
} else {
|
|
||||||
// we reached the top of the scrollback
|
|
||||||
this.reactiveTimeline.loadMore.remove()
|
|
||||||
}
|
|
||||||
this.broadcast("afterScrollbackLoad")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send(body) {
|
send(body) {
|
||||||
|
@ -399,8 +385,32 @@ class Timeline extends Subscribable {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
})
|
})/*.then(() => {
|
||||||
|
const subscription = () => {
|
||||||
|
this.removeEvent(id)
|
||||||
|
this.unsubscribe("afterChange", subscription)
|
||||||
}
|
}
|
||||||
|
this.subscribe("afterChange", subscription)
|
||||||
|
})*/
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
getGroupedEvents() {
|
||||||
|
let currentSender = Symbol("N/A")
|
||||||
|
let groups = []
|
||||||
|
let currentGroup = []
|
||||||
|
for (const event of this.list) {
|
||||||
|
if (event.sender === currentSender) {
|
||||||
|
currentGroup.push(event)
|
||||||
|
} else {
|
||||||
|
if (currentGroup.length) groups.push(currentGroup)
|
||||||
|
currentGroup = [event]
|
||||||
|
currentSender = event.sender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentGroup.length) groups.push(currentGroup)
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {Timeline}
|
module.exports = {Timeline}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
|
|
||||||
&--no-icon
|
&--no-icon
|
||||||
background-color: #bbb
|
background-color: #48d
|
||||||
|
|
||||||
&__intro
|
&__intro
|
||||||
display: flex
|
display: flex
|
||||||
|
@ -46,7 +46,6 @@
|
||||||
|
|
||||||
.c-message
|
.c-message
|
||||||
margin-top: 4px
|
margin-top: 4px
|
||||||
overflow-wrap: anywhere
|
|
||||||
opacity: 1
|
opacity: 1
|
||||||
transition: opacity 0.2s ease-out
|
transition: opacity 0.2s ease-out
|
||||||
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
@keyframes spin
|
|
||||||
0%
|
|
||||||
transform: rotate(0deg)
|
|
||||||
100%
|
|
||||||
transform: rotate(180deg)
|
|
||||||
|
|
||||||
.loading-icon
|
|
||||||
display: inline-block
|
|
||||||
background-color: #ccc
|
|
||||||
width: 12px
|
|
||||||
height: 12px
|
|
||||||
margin-right: 6px
|
|
||||||
animation: spin 0.7s infinite
|
|
|
@ -1,9 +1,7 @@
|
||||||
@use "./base"
|
@use "./base"
|
||||||
@use "./loading"
|
|
||||||
@use "./colors" as c
|
@use "./colors" as c
|
||||||
@use "./tippy"
|
@use "./tippy"
|
||||||
|
|
||||||
|
|
||||||
.main
|
.main
|
||||||
justify-content: center
|
justify-content: center
|
||||||
align-items: center
|
align-items: center
|
||||||
|
|
|
@ -5,4 +5,3 @@
|
||||||
@use "./components/chat"
|
@use "./components/chat"
|
||||||
@use "./components/chat-input"
|
@use "./components/chat-input"
|
||||||
@use "./components/anchor"
|
@use "./components/anchor"
|
||||||
@use "./loading"
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue