Compare commits
	
		
			11 commits
		
	
	
		
			a5309a81b1
			...
			6224cde132
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6224cde132 | |||
| ff196a64bb | |||
| a4c7f29ec9 | |||
| 5bfe98bdf4 | |||
| f4b368ea3e | |||
| 6da9f41519 | |||
| df47c8a88a | |||
| 5ab182e615 | |||
| 08a0990bc8 | |||
| c9dffc9d4a | |||
| 6227f6fa84 | 
					 13 changed files with 539 additions and 269 deletions
				
			
		
							
								
								
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -288,6 +288,10 @@ modules.xml
 | 
			
		|||
 | 
			
		||||
# 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)
 | 
			
		||||
 | 
			
		||||
/build/
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										65
									
								
								build.js
									
										
									
									
									
								
							
							
						
						
									
										65
									
								
								build.js
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -9,7 +9,8 @@ const babel = require("@babel/core")
 | 
			
		|||
const fetch = require("node-fetch")
 | 
			
		||||
const chalk = require("chalk")
 | 
			
		||||
const hint = require("jshint").JSHINT
 | 
			
		||||
const browserify = require('browserify')
 | 
			
		||||
const browserify = require("browserify")
 | 
			
		||||
const {Transform} = require("stream")
 | 
			
		||||
 | 
			
		||||
process.chdir(pj(__dirname, "src"))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -17,10 +18,10 @@ const buildDir = "../build"
 | 
			
		|||
 | 
			
		||||
const validationQueue = []
 | 
			
		||||
const validationHost = os.hostname() === "future" ? "http://localhost:8888/" : "http://validator.w3.org/nu/"
 | 
			
		||||
const static_files = new Map()
 | 
			
		||||
const staticFiles = new Map()
 | 
			
		||||
const links = new Map()
 | 
			
		||||
const sources = new Map()
 | 
			
		||||
const pugLocals = {static: static_files, links}
 | 
			
		||||
const pugLocals = {static: staticFiles, links}
 | 
			
		||||
 | 
			
		||||
const spec = require("./spec.js")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -95,6 +96,7 @@ function runHint(filename, source) {
 | 
			
		|||
		globals: ["console", "URLSearchParams", "staticFiles"],
 | 
			
		||||
		browser: true,
 | 
			
		||||
		asi: true,
 | 
			
		||||
		node: true
 | 
			
		||||
	})
 | 
			
		||||
	const result = hint.data()
 | 
			
		||||
	let problems = 0
 | 
			
		||||
| 
						 | 
				
			
			@ -127,33 +129,52 @@ function runHint(filename, source) {
 | 
			
		|||
 | 
			
		||||
async function addFile(sourcePath, targetPath) {
 | 
			
		||||
	const contents = await fs.promises.readFile(pj(".", sourcePath), {encoding: null});
 | 
			
		||||
	static_files.set(sourcePath, `${targetPath}?static=${hash(contents)}`)
 | 
			
		||||
	staticFiles.set(sourcePath, `${targetPath}?static=${hash(contents)}`)
 | 
			
		||||
	await fs.promises.writeFile(pj(buildDir, targetPath), contents)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadJS(sourcePath, targetPath) {
 | 
			
		||||
	let content = await fs.promises.readFile(pj(".", sourcePath), {encoding: "utf8"})
 | 
			
		||||
	sources.set(sourcePath, content);
 | 
			
		||||
	static_files.set(sourcePath, `${targetPath}?static=${hash(content)}`)
 | 
			
		||||
	sources.set(sourcePath, content)
 | 
			
		||||
	staticFiles.set(sourcePath, `${targetPath}?static=${hash(content)}`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function addJS(sourcePath, targetPath) {
 | 
			
		||||
	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)
 | 
			
		||||
	await fs.promises.writeFile(pj(buildDir, targetPath), content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function addBundle(sourcePath, targetPath) {
 | 
			
		||||
	await browserify()
 | 
			
		||||
	const content = await new Promise(resolve => {
 | 
			
		||||
		browserify()
 | 
			
		||||
			.add(pj(".", sourcePath))
 | 
			
		||||
		.bundle()
 | 
			
		||||
		.pipe(fs.createWriteStream(pj(buildDir, targetPath)));
 | 
			
		||||
	static_files.set(sourcePath, targetPath)
 | 
			
		||||
			.transform(file => {
 | 
			
		||||
				let content = ""
 | 
			
		||||
				const transform = new Transform({
 | 
			
		||||
					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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -167,7 +188,7 @@ async function addSass(sourcePath, targetPath) {
 | 
			
		|||
				if (!(name instanceof sass.types.String)) {
 | 
			
		||||
					throw "$name: expected a string"
 | 
			
		||||
				}
 | 
			
		||||
				const result = getRelative(targetPath, static_files.get(name.getValue()))
 | 
			
		||||
				const result = getRelative(targetPath, staticFiles.get(name.getValue()))
 | 
			
		||||
				if (typeof result === "string") {
 | 
			
		||||
					return new sass.types.String(result)
 | 
			
		||||
				} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -176,8 +197,8 @@ async function addSass(sourcePath, targetPath) {
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}).css;
 | 
			
		||||
	static_files.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`)
 | 
			
		||||
	await validate(sourcePath, renderedCSS, "css")
 | 
			
		||||
	staticFiles.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`)
 | 
			
		||||
	validate(sourcePath, renderedCSS, "css")
 | 
			
		||||
	await fs.promises.writeFile(pj(buildDir, targetPath), renderedCSS)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -186,17 +207,17 @@ async function addPug(sourcePath, targetPath) {
 | 
			
		|||
		return getRelative(targetPath, staticTarget)
 | 
			
		||||
	}
 | 
			
		||||
	function getStatic(target) {
 | 
			
		||||
		return getRelativeHere(static_files.get(target))
 | 
			
		||||
		return getRelativeHere(staticFiles.get(target))
 | 
			
		||||
	}
 | 
			
		||||
	function getStaticName(target) {
 | 
			
		||||
		return getRelativeHere(static_files.get(target)).replace(/\?.*$/, "")
 | 
			
		||||
		return getRelativeHere(staticFiles.get(target)).replace(/\?.*$/, "")
 | 
			
		||||
	}
 | 
			
		||||
	function getLink(target) {
 | 
			
		||||
		return getRelativeHere(links.get(target))
 | 
			
		||||
	}
 | 
			
		||||
	const renderedHTML = pug.compileFile(pj(".", sourcePath), {pretty: true})({getStatic, getStaticName, getLink, ...pugLocals})
 | 
			
		||||
	let renderedWithoutPHP = renderedHTML.replace(/<\?(?:php|=).*?\?>/gsm, "")
 | 
			
		||||
	await validate(sourcePath, renderedWithoutPHP, "html")
 | 
			
		||||
	validate(sourcePath, renderedWithoutPHP, "html")
 | 
			
		||||
	await fs.promises.writeFile(pj(buildDir, targetPath), renderedHTML)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -224,7 +245,7 @@ async function addBabel(sourcePath, targetPath) {
 | 
			
		|||
 | 
			
		||||
	const filenameWithQuery = `${targetPath}?static=${hash(compiled.code)}`;
 | 
			
		||||
 | 
			
		||||
	static_files.set(sourcePath, filenameWithQuery)
 | 
			
		||||
	staticFiles.set(sourcePath, filenameWithQuery)
 | 
			
		||||
 | 
			
		||||
	await Promise.all([
 | 
			
		||||
		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,18 +11,17 @@
 | 
			
		|||
  "keywords": [],
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "browserify": "^17.0.0",
 | 
			
		||||
    "tippy.js": "^6.2.7"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {},
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/core": "^7.11.1",
 | 
			
		||||
    "@babel/preset-env": "^7.11.0",
 | 
			
		||||
    "browserify": "^17.0.0",
 | 
			
		||||
    "chalk": "^4.1.0",
 | 
			
		||||
    "http-server": "^0.12.3",
 | 
			
		||||
    "jshint": "^2.12.0",
 | 
			
		||||
    "node-fetch": "^2.6.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,86 +19,6 @@ module.exports = [
 | 
			
		|||
		source: "/js/main.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",
 | 
			
		||||
		source: "/assets/fonts/whitney-500.woff",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,7 +27,7 @@ class Chat extends ElemJS {
 | 
			
		|||
		// connect to the new room's timeline updater
 | 
			
		||||
		if (store.activeRoom.exists()) {
 | 
			
		||||
			const timeline = store.activeRoom.value().timeline
 | 
			
		||||
			const subscription = () => {
 | 
			
		||||
			const beforeChangeSubscription = () => {
 | 
			
		||||
				// scroll anchor does not work if the timeline is scrolled to the top.
 | 
			
		||||
				// at the start, when there are not enough messages for a full screen, this is the case.
 | 
			
		||||
				// once there are enough messages that scrolling is necessary, we initiate a scroll down to activate the scroll anchor.
 | 
			
		||||
| 
						 | 
				
			
			@ -40,12 +40,29 @@ class Chat extends ElemJS {
 | 
			
		|||
					}
 | 
			
		||||
				}, 0)
 | 
			
		||||
			}
 | 
			
		||||
			const name = "beforeChange"
 | 
			
		||||
			this.removableSubscriptions.push({name, target: timeline, subscription})
 | 
			
		||||
			timeline.subscribe(name, subscription)
 | 
			
		||||
			this.addSubscription("beforeChange", timeline, beforeChangeSubscription)
 | 
			
		||||
 | 
			
		||||
			// Make sure after loading scrollback we don't move the scroll position
 | 
			
		||||
			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()
 | 
			
		||||
	}
 | 
			
		||||
	addSubscription(name, target, subscription) {
 | 
			
		||||
		this.removableSubscriptions.push({name, target, subscription})
 | 
			
		||||
		target.subscribe(name, subscription)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
const lsm = require("./lsm.js")
 | 
			
		||||
 | 
			
		||||
function resolveMxc(url, size, method) {
 | 
			
		||||
	const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
 | 
			
		||||
	let [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
 | 
			
		||||
	id = id.replace(/#.*$/, "")
 | 
			
		||||
	if (size && method) {
 | 
			
		||||
		return `${lsm.get("domain")}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`
 | 
			
		||||
	} else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										120
									
								
								src/js/sender.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/js/sender.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,120 @@
 | 
			
		|||
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 {store} = require("./store/store.js")
 | 
			
		||||
const {Anchor} = require("./anchor.js")
 | 
			
		||||
const {Sender} = require("./sender.js")
 | 
			
		||||
const lsm = require("./lsm.js")
 | 
			
		||||
const {resolveMxc} = require("./functions.js")
 | 
			
		||||
 | 
			
		||||
let debug = false
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -100,41 +100,6 @@ 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 {
 | 
			
		||||
	constructor(reactive, list) {
 | 
			
		||||
		super("div")
 | 
			
		||||
| 
						 | 
				
			
			@ -176,16 +141,43 @@ 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 {
 | 
			
		||||
	constructor(id, list) {
 | 
			
		||||
		super("div")
 | 
			
		||||
		this.class("c-event-groups")
 | 
			
		||||
		this.id = id
 | 
			
		||||
		this.list = list
 | 
			
		||||
		this.loadMore = new LoadMore(this.id)
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addEvent(event) {
 | 
			
		||||
		this.loadMore.remove()
 | 
			
		||||
		// if (debug) console.log("running search", this.list, event)
 | 
			
		||||
		// if (debug) debugger;
 | 
			
		||||
		const search = eventSearch(this.list, event)
 | 
			
		||||
| 
						 | 
				
			
			@ -201,6 +193,8 @@ class ReactiveTimeline extends ElemJS {
 | 
			
		|||
		} else {
 | 
			
		||||
			this.tryAddGroups(event, [search.i])
 | 
			
		||||
		}
 | 
			
		||||
		this.loadMore = new LoadMore(this.id)
 | 
			
		||||
		this.childAt(0, this.loadMore)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tryAddGroups(event, indices) {
 | 
			
		||||
| 
						 | 
				
			
			@ -233,6 +227,7 @@ class ReactiveTimeline extends ElemJS {
 | 
			
		|||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		this.child(this.loadMore)
 | 
			
		||||
		this.list.forEach(group => this.child(group))
 | 
			
		||||
		this.anchor = new Anchor()
 | 
			
		||||
		this.child(this.anchor)
 | 
			
		||||
| 
						 | 
				
			
			@ -244,11 +239,15 @@ class Timeline extends Subscribable {
 | 
			
		|||
		super()
 | 
			
		||||
		Object.assign(this.events, {
 | 
			
		||||
			beforeChange: [],
 | 
			
		||||
			afterChange: []
 | 
			
		||||
			afterChange: [],
 | 
			
		||||
			beforeScrollbackLoad: [],
 | 
			
		||||
			afterScrollbackLoad: [],
 | 
			
		||||
		})
 | 
			
		||||
		Object.assign(this.eventDeps, {
 | 
			
		||||
			beforeChange: [],
 | 
			
		||||
			afterChange: []
 | 
			
		||||
			afterChange: [],
 | 
			
		||||
			beforeScrollbackLoad: [],
 | 
			
		||||
			afterScrollbackLoad: [],
 | 
			
		||||
		})
 | 
			
		||||
		this.room = room
 | 
			
		||||
		this.id = this.room.id
 | 
			
		||||
| 
						 | 
				
			
			@ -267,7 +266,11 @@ class Timeline extends Subscribable {
 | 
			
		|||
			if (eventData.type === "m.room.member") {
 | 
			
		||||
				// update members
 | 
			
		||||
				if (eventData.membership !== "leave") {
 | 
			
		||||
					this.room.members.get(eventData.state_key).set(eventData)
 | 
			
		||||
					const member = this.room.members.get(eventData.state_key)
 | 
			
		||||
					// only use the latest state
 | 
			
		||||
					if (!member.exists() || eventData.origin_server_ts > member.data.origin_server_ts) {
 | 
			
		||||
						member.set(eventData)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -349,16 +352,27 @@ class Timeline extends Subscribable {
 | 
			
		|||
		url.searchParams.set("access_token", lsm.get("access_token"))
 | 
			
		||||
		url.searchParams.set("from", this.from)
 | 
			
		||||
		url.searchParams.set("dir", "b")
 | 
			
		||||
		url.searchParams.set("limit", 10)
 | 
			
		||||
		url.searchParams.set("limit", "20")
 | 
			
		||||
		const filter = {
 | 
			
		||||
			lazy_load_members: true
 | 
			
		||||
		}
 | 
			
		||||
		url.searchParams.set("filter", JSON.stringify(filter))
 | 
			
		||||
 | 
			
		||||
		const root = await fetch(url.toString()).then(res => res.json())
 | 
			
		||||
 | 
			
		||||
		this.broadcast("beforeScrollbackLoad")
 | 
			
		||||
 | 
			
		||||
		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.chunk.length) {
 | 
			
		||||
			// there are events to display
 | 
			
		||||
			this.updateEvents(root.chunk)
 | 
			
		||||
		} else {
 | 
			
		||||
			// we reached the top of the scrollback
 | 
			
		||||
			this.reactiveTimeline.loadMore.remove()
 | 
			
		||||
		}
 | 
			
		||||
		this.broadcast("afterScrollbackLoad")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	send(body) {
 | 
			
		||||
| 
						 | 
				
			
			@ -385,32 +399,8 @@ class Timeline extends Subscribable {
 | 
			
		|||
			headers: {
 | 
			
		||||
				"Content-Type": "application/json"
 | 
			
		||||
			}
 | 
			
		||||
		})/*.then(() => {
 | 
			
		||||
			const subscription = () => {
 | 
			
		||||
				this.removeEvent(id)
 | 
			
		||||
				this.unsubscribe("afterChange", subscription)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
			this.subscribe("afterChange", subscription)
 | 
			
		||||
		})*/
 | 
			
		||||
	}
 | 
			
		||||
/*
 | 
			
		||||
	getGroupedEvents() {
 | 
			
		||||
		let currentSender = Symbol("N/A")
 | 
			
		||||
		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}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@
 | 
			
		|||
    border-radius: 50%
 | 
			
		||||
 | 
			
		||||
    &--no-icon
 | 
			
		||||
      background-color: #48d
 | 
			
		||||
      background-color: #bbb
 | 
			
		||||
 | 
			
		||||
  &__intro
 | 
			
		||||
    display: flex
 | 
			
		||||
| 
						 | 
				
			
			@ -46,6 +46,7 @@
 | 
			
		|||
 | 
			
		||||
.c-message
 | 
			
		||||
  margin-top: 4px
 | 
			
		||||
  overflow-wrap: anywhere
 | 
			
		||||
  opacity: 1
 | 
			
		||||
  transition: opacity 0.2s ease-out
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										13
									
								
								src/sass/loading.sass
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/sass/loading.sass
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
@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,7 +1,9 @@
 | 
			
		|||
@use "./base"
 | 
			
		||||
@use "./loading"
 | 
			
		||||
@use "./colors" as c
 | 
			
		||||
@use "./tippy"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.main
 | 
			
		||||
  justify-content: center
 | 
			
		||||
  align-items: center
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,3 +5,4 @@
 | 
			
		|||
@use "./components/chat"
 | 
			
		||||
@use "./components/chat-input"
 | 
			
		||||
@use "./components/anchor"
 | 
			
		||||
@use "./loading"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue