Merge pull request 'Rich message rendering' (#24) from rich-messages into princess
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			Reviewed-on: #24
This commit is contained in:
		
						commit
						f6b95b2ebd
					
				
					 17 changed files with 573 additions and 629 deletions
				
			
		
							
								
								
									
										10
									
								
								build.js
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								build.js
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -145,9 +145,11 @@ async function addJS(sourcePath, targetPath) {
 | 
			
		|||
	await fs.promises.writeFile(pj(buildDir, targetPath), content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function addBundle(sourcePath, targetPath) {
 | 
			
		||||
async function addBundle(sourcePath, targetPath, module = false) {
 | 
			
		||||
	let opts = {}
 | 
			
		||||
	if (module) opts.standalone = sourcePath
 | 
			
		||||
	const content = await new Promise(resolve => {
 | 
			
		||||
		browserify()
 | 
			
		||||
		browserify([], opts)
 | 
			
		||||
			.add(pj(".", sourcePath))
 | 
			
		||||
			.transform(file => {
 | 
			
		||||
				let content = ""
 | 
			
		||||
| 
						 | 
				
			
			@ -173,7 +175,6 @@ async function addBundle(sourcePath, targetPath) {
 | 
			
		|||
	})
 | 
			
		||||
	const writer = fs.promises.writeFile(pj(buildDir, targetPath), content)
 | 
			
		||||
	staticFiles.set(sourcePath, `${targetPath}?static=${hash(content)}`)
 | 
			
		||||
	runHint(sourcePath, content)
 | 
			
		||||
	await writer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -287,6 +288,9 @@ async function addBabel(sourcePath, targetPath) {
 | 
			
		|||
			await addPug(item.source, item.target)
 | 
			
		||||
		} else if (item.type === "bundle") {
 | 
			
		||||
			await addBundle(item.source, item.target)
 | 
			
		||||
		} else if (item.type === "module") {
 | 
			
		||||
			// Creates a standalone bundle that can be imported on runtime
 | 
			
		||||
			await addBundle(item.source, item.target, true)
 | 
			
		||||
		} else {
 | 
			
		||||
			throw new Error("Unknown item type: "+item.type)
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										721
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										721
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
				
			
			@ -11,16 +11,18 @@
 | 
			
		|||
  "keywords": [],
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
  "dependencies": {},
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@babel/core": "^7.11.1",
 | 
			
		||||
    "@babel/preset-env": "^7.11.0",
 | 
			
		||||
    "browserify": "^17.0.0",
 | 
			
		||||
    "chalk": "^4.1.0",
 | 
			
		||||
    "dompurify": "^2.2.0",
 | 
			
		||||
    "highlight.js": "^10.3.2",
 | 
			
		||||
    "http-server": "^0.12.3",
 | 
			
		||||
    "jshint": "^2.12.0",
 | 
			
		||||
    "node-fetch": "^2.6.0",
 | 
			
		||||
    "pug": "^3.0.0",
 | 
			
		||||
    "sass": "^1.26.10"
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								src/js/date-formatter.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/js/date-formatter.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
const dateFormatter = Intl.DateTimeFormat("default", {hour: "numeric", minute: "numeric", day: "numeric", month: "short", year: "numeric"})
 | 
			
		||||
 | 
			
		||||
module.exports = {dateFormatter}
 | 
			
		||||
							
								
								
									
										31
									
								
								src/js/events/components.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/js/events/components.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
const {ElemJS} = require("../basic")
 | 
			
		||||
const {lazyLoad} = require("../lazy-load-module")
 | 
			
		||||
 | 
			
		||||
class HighlightedCode extends ElemJS {
 | 
			
		||||
	constructor(element) {
 | 
			
		||||
		super(element)
 | 
			
		||||
		if (this.element.tagName === "PRE" && this.element.children.length === 1 && this.element.children[0].tagName === "CODE") {
 | 
			
		||||
			// we shouldn't nest code inside a pre. put the text in the pre directly.
 | 
			
		||||
			const code = this.element.children[0]
 | 
			
		||||
			this.clearChildren()
 | 
			
		||||
			for (const child of code.childNodes) {
 | 
			
		||||
				this.element.appendChild(child)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (this.element.textContent.length > 80) {
 | 
			
		||||
			/*
 | 
			
		||||
			  no need to highlight very short code blocks:
 | 
			
		||||
			  - content inside might not be code, some users still use code blocks
 | 
			
		||||
			    for plaintext quotes
 | 
			
		||||
			  - language detection will almost certainly be incorrect
 | 
			
		||||
			  - even if it's code and the language is detected, the user will
 | 
			
		||||
			    be able to mentally format small amounts of code themselves
 | 
			
		||||
 | 
			
		||||
			  feel free to change the threshold number
 | 
			
		||||
			*/
 | 
			
		||||
			lazyLoad("https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10/build/highlight.min.js").then(hljs => hljs.highlightBlock(this.element))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {HighlightedCode}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/js/events/encrypted.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/js/events/encrypted.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
const {MatrixEvent} = require("./event")
 | 
			
		||||
const {ejs} = require("../basic")
 | 
			
		||||
 | 
			
		||||
class EncryptedMessage extends MatrixEvent {
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		this.child(
 | 
			
		||||
			ejs("i").text("Carbon cannot render encrypted messages yet")
 | 
			
		||||
		)
 | 
			
		||||
		super.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static canRender(eventData) {
 | 
			
		||||
		return eventData.type === "m.room.encrypted"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	canGroup() {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = [EncryptedMessage]
 | 
			
		||||
							
								
								
									
										54
									
								
								src/js/events/event.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/js/events/event.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
const {ElemJS, ejs} = require("../basic")
 | 
			
		||||
const {dateFormatter} = require("../date-formatter")
 | 
			
		||||
 | 
			
		||||
class MatrixEvent extends ElemJS {
 | 
			
		||||
	constructor(data) {
 | 
			
		||||
		super("div")
 | 
			
		||||
		this.class("c-message")
 | 
			
		||||
		this.data = null
 | 
			
		||||
		this.group = null
 | 
			
		||||
		this.editedAt = null
 | 
			
		||||
		this.update(data)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// predicates
 | 
			
		||||
 | 
			
		||||
	canGroup() {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// operations
 | 
			
		||||
 | 
			
		||||
	setGroup(group) {
 | 
			
		||||
		this.group = group
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setEdited(time) {
 | 
			
		||||
		this.editedAt = time
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	update(data) {
 | 
			
		||||
		this.data = data
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	removeEvent() {
 | 
			
		||||
		if (this.group) this.group.removeEvent(this)
 | 
			
		||||
		else this.remove()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.element.classList[this.data.pending ? "add" : "remove"]("c-message--pending")
 | 
			
		||||
		if (this.editedAt) {
 | 
			
		||||
			this.child(ejs("span").class("c-message__edited").text("(edited)").attribute("title", "at " + dateFormatter.format(this.editedAt)))
 | 
			
		||||
		}
 | 
			
		||||
		return this
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static canRender(eventData) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {MatrixEvent}
 | 
			
		||||
							
								
								
									
										55
									
								
								src/js/events/membership.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/js/events/membership.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
const {MatrixEvent} = require("./event")
 | 
			
		||||
const {ejs} = require("../basic")
 | 
			
		||||
 | 
			
		||||
class MembershipEvent extends MatrixEvent {
 | 
			
		||||
	static canRender(eventData) {
 | 
			
		||||
		return eventData.type === "m.room.member"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	renderText(text) {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		this.child(
 | 
			
		||||
			ejs("i").text(text)
 | 
			
		||||
		)
 | 
			
		||||
		super.render()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class JoinedEvent extends MembershipEvent {
 | 
			
		||||
	static canRender(eventData) {
 | 
			
		||||
		return super.canRender(eventData) && eventData.content.membership === "join"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.renderText("joined the room")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class InvitedEvent extends MembershipEvent {
 | 
			
		||||
	static canRender(eventData) {
 | 
			
		||||
		return super.canRender(eventData) && eventData.content.membership === "invite"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.renderText(`invited ${this.data.content.displayname}`)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class LeaveEvent extends MembershipEvent {
 | 
			
		||||
	static canRender(eventData) {
 | 
			
		||||
		return super.canRender(eventData) && eventData.content.membership === "leave"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.renderText("left the room")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class UnknownMembership extends MembershipEvent {
 | 
			
		||||
	render() {
 | 
			
		||||
		this.renderText("unknown membership event")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = [JoinedEvent, InvitedEvent, LeaveEvent, UnknownMembership]
 | 
			
		||||
							
								
								
									
										140
									
								
								src/js/events/message.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/js/events/message.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,140 @@
 | 
			
		|||
const {ejs, ElemJS} = require("../basic")
 | 
			
		||||
const {HighlightedCode} = require("./components")
 | 
			
		||||
const DOMPurify = require("dompurify")
 | 
			
		||||
const {resolveMxc} = require("../functions")
 | 
			
		||||
const {MatrixEvent} = require("./event")
 | 
			
		||||
 | 
			
		||||
const purifier = DOMPurify()
 | 
			
		||||
 | 
			
		||||
purifier.addHook("uponSanitizeAttribute", (node, hookevent, config) => {
 | 
			
		||||
	// If purifier already rejected an attribute there is no point in checking it
 | 
			
		||||
	if (hookevent.keepAttr === false) return;
 | 
			
		||||
 | 
			
		||||
	const allowedElementAttributes = {
 | 
			
		||||
		"FONT": ["data-mx-bg-color", "data-mx-color", "color"],
 | 
			
		||||
		"SPAN": ["data-mx-bg-color", "data-mx-color"],
 | 
			
		||||
		"A": ["name", "target", "href"],
 | 
			
		||||
		"IMG": ["width", "height", "alt", "title", "src", "data-mx-emoticon"],
 | 
			
		||||
		"OL": ["start"],
 | 
			
		||||
		"CODE": ["class"],
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const allowedAttributes = allowedElementAttributes[node.tagName] || []
 | 
			
		||||
	hookevent.keepAttr = allowedAttributes.indexOf(hookevent.attrName) > -1
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
purifier.addHook("uponSanitizeElement", (node, hookevent, config) => {
 | 
			
		||||
	// Remove bad classes from our code element
 | 
			
		||||
	if (node.tagName === "CODE") {
 | 
			
		||||
		node.classList.forEach(c => {
 | 
			
		||||
			if (!c.startsWith("language-")) {
 | 
			
		||||
				node.classList.remove(c)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	if (node.tagName === "A") {
 | 
			
		||||
		node.setAttribute("rel", "noopener") // prevent the opening page from accessing carbon
 | 
			
		||||
		node.setAttribute("target", "_blank") // open in a new tab instead of replacing carbon
 | 
			
		||||
	}
 | 
			
		||||
	return node
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function cleanHTML(html) {
 | 
			
		||||
	const config = {
 | 
			
		||||
		ALLOWED_TAGS: [
 | 
			
		||||
			"font", "del", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p",
 | 
			
		||||
			"a", "ul", "ol", "sup", "sub", "li", "b", "i", "u", "strong", "em",
 | 
			
		||||
			"strike", "code", "hr", "br", "div", "table", "thead", "tbody", "tr",
 | 
			
		||||
			"th", "td", "caption", "pre", "span", "img",
 | 
			
		||||
			// matrix tags
 | 
			
		||||
			"mx-reply"
 | 
			
		||||
		],
 | 
			
		||||
 | 
			
		||||
		// In case we mess up in the uponSanitizeAttribute hook
 | 
			
		||||
		ALLOWED_ATTR: [
 | 
			
		||||
			"color", "name", "target", "href", "width", "height", "alt", "title",
 | 
			
		||||
			"src", "start", "class", "noreferrer", "noopener",
 | 
			
		||||
			// matrix attrs
 | 
			
		||||
			"data-mx-emoticon", "data-mx-bg-color", "data-mx-color"
 | 
			
		||||
		],
 | 
			
		||||
 | 
			
		||||
		// Return a DOM fragment instead of a string, avoids potential future mutation XSS
 | 
			
		||||
		// should also be faster than the browser parsing HTML twice
 | 
			
		||||
		// https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/
 | 
			
		||||
		RETURN_DOM_FRAGMENT: true,
 | 
			
		||||
		RETURN_DOM_IMPORT: true
 | 
			
		||||
	}
 | 
			
		||||
	return purifier.sanitize(html, config)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Here we put all the processing of the messages that isn't as likely to potentially lead to security issues
 | 
			
		||||
function postProcessElements(rootNode) {
 | 
			
		||||
	const element = rootNode.element
 | 
			
		||||
 | 
			
		||||
	element.querySelectorAll("pre").forEach(n => {
 | 
			
		||||
		new HighlightedCode(n)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	element.querySelectorAll("img").forEach(n => {
 | 
			
		||||
		let src = n.getAttribute("src")
 | 
			
		||||
		if (src) src = resolveMxc(src)
 | 
			
		||||
		n.setAttribute("src", src)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	element.querySelectorAll("font, span").forEach(n => {
 | 
			
		||||
		const color = n.getAttribute("data-mx-color") || n.getAttribute("color")
 | 
			
		||||
		const bgColor = n.getAttribute("data-mx-bg-color")
 | 
			
		||||
		if (color) n.style.color = color
 | 
			
		||||
		if (bgColor) n.style.backgroundColor = bgColor
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HTMLMessage extends MatrixEvent {
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
 | 
			
		||||
		let html = this.data.content.formatted_body
 | 
			
		||||
		const content = ejs("div")
 | 
			
		||||
 | 
			
		||||
		const fragment = cleanHTML(html)
 | 
			
		||||
		content.element.appendChild(fragment)
 | 
			
		||||
		postProcessElements(content)
 | 
			
		||||
 | 
			
		||||
		this.child(content)
 | 
			
		||||
 | 
			
		||||
		super.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static canRender(event) {
 | 
			
		||||
		const content = event.content
 | 
			
		||||
		return (
 | 
			
		||||
			event.type === "m.room.message"
 | 
			
		||||
				&& (content.msgtype === "m.text" || content.msgtype === "m.notice")
 | 
			
		||||
				&& content.format === "org.matrix.custom.html"
 | 
			
		||||
				&& content.formatted_body
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	canGroup() {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class TextMessage extends MatrixEvent {
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		this.text(this.data.content.body)
 | 
			
		||||
		super.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static canRender(event) {
 | 
			
		||||
		return event.type === "m.room.message"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	canGroup() {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = [HTMLMessage, TextMessage]
 | 
			
		||||
							
								
								
									
										19
									
								
								src/js/events/render-event.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/js/events/render-event.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
const messageEvent = require("./message")
 | 
			
		||||
const encryptedEvent = require("./encrypted")
 | 
			
		||||
const membershipEvent = require("./membership")
 | 
			
		||||
const unknownEvent = require("./unknown")
 | 
			
		||||
 | 
			
		||||
const events = [
 | 
			
		||||
	...messageEvent,
 | 
			
		||||
	...encryptedEvent,
 | 
			
		||||
	...membershipEvent,
 | 
			
		||||
	...unknownEvent,
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function renderEvent(eventData) {
 | 
			
		||||
	const constructor = events.find(e => e.canRender(eventData))
 | 
			
		||||
	return new constructor(eventData)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {renderEvent}
 | 
			
		||||
							
								
								
									
										19
									
								
								src/js/events/unknown.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/js/events/unknown.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
const {MatrixEvent} = require("./event")
 | 
			
		||||
const {ejs} = require("../basic")
 | 
			
		||||
 | 
			
		||||
class UnknownEvent extends MatrixEvent {
 | 
			
		||||
	static canRender() {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		this.child(
 | 
			
		||||
			ejs("i").text(`Unknown event of type ${this.data.type}`)
 | 
			
		||||
		)
 | 
			
		||||
		super.render()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = [UnknownEvent]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								src/js/lazy-load-module.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/js/lazy-load-module.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
// I hate this with passion
 | 
			
		||||
async function lazyLoad(url) {
 | 
			
		||||
	const cache = window.lazyLoadCache || new Map()
 | 
			
		||||
	window.lazyLoadCache = cache
 | 
			
		||||
	if (cache.get(url)) return cache.get(url)
 | 
			
		||||
 | 
			
		||||
	const module = loadModuleWithoutCache(url)
 | 
			
		||||
	cache.set(url, module)
 | 
			
		||||
	return module
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Loads the module without caching
 | 
			
		||||
async function loadModuleWithoutCache(url) {
 | 
			
		||||
	const src = await fetch(url).then(r => r.text())
 | 
			
		||||
	let module = {}
 | 
			
		||||
	eval(src)
 | 
			
		||||
	return module.exports
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {lazyLoad}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,13 +4,14 @@ 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")
 | 
			
		||||
const {renderEvent} = require("./events/render-event")
 | 
			
		||||
const {dateFormatter} = require("./date-formatter")
 | 
			
		||||
 | 
			
		||||
let debug = false
 | 
			
		||||
 | 
			
		||||
const NO_MAX = Symbol("NO_MAX")
 | 
			
		||||
 | 
			
		||||
const dateFormatter = Intl.DateTimeFormat("default", {hour: "numeric", minute: "numeric", day: "numeric", month: "short", year: "numeric"})
 | 
			
		||||
 | 
			
		||||
let sentIndex = 0
 | 
			
		||||
 | 
			
		||||
function getTxnId() {
 | 
			
		||||
| 
						 | 
				
			
			@ -38,67 +39,6 @@ function eventSearch(list, event, min = 0, max = NO_MAX) {
 | 
			
		|||
	else return eventSearch(list, event, mid + 1, max)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Event extends ElemJS {
 | 
			
		||||
	constructor(data) {
 | 
			
		||||
		super("div")
 | 
			
		||||
		this.class("c-message")
 | 
			
		||||
		this.data = null
 | 
			
		||||
		this.group = null
 | 
			
		||||
		this.editedAt = null
 | 
			
		||||
		this.update(data)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// predicates
 | 
			
		||||
 | 
			
		||||
	canGroup() {
 | 
			
		||||
		return this.data.type === "m.room.message"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// operations
 | 
			
		||||
 | 
			
		||||
	setGroup(group) {
 | 
			
		||||
		this.group = group
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setEdited(time) {
 | 
			
		||||
		this.editedAt = time
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	update(data) {
 | 
			
		||||
		this.data = data
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	removeEvent() {
 | 
			
		||||
		if (this.group) this.group.removeEvent(this)
 | 
			
		||||
		else this.remove()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.element.classList[this.data.pending ? "add" : "remove"]("c-message--pending")
 | 
			
		||||
		if (this.data.type === "m.room.message") {
 | 
			
		||||
			this.text(this.data.content.body)
 | 
			
		||||
		} else if (this.data.type === "m.room.member") {
 | 
			
		||||
			if (this.data.content.membership === "join") {
 | 
			
		||||
				this.child(ejs("i").text("joined the room"))
 | 
			
		||||
			} else if (this.data.content.membership === "invite") {
 | 
			
		||||
				this.child(ejs("i").text(`invited ${this.data.content.displayname} to the room`))
 | 
			
		||||
			} else if (this.data.content.membership === "leave") {
 | 
			
		||||
				this.child(ejs("i").text("left the room"))
 | 
			
		||||
			} else {
 | 
			
		||||
				this.child(ejs("i").text("unknown membership event"))
 | 
			
		||||
			}
 | 
			
		||||
		} else if (this.data.type === "m.room.encrypted") {
 | 
			
		||||
			this.child(ejs("i").text("Carbon does not yet support encrypted messages."))
 | 
			
		||||
		} else {
 | 
			
		||||
			this.child(ejs("i").text(`Unsupported event type ${this.data.type}`))
 | 
			
		||||
		}
 | 
			
		||||
		if (this.editedAt) {
 | 
			
		||||
			this.child(ejs("span").class("c-message__edited").text("(edited)").attribute("title", "at " + dateFormatter.format(this.editedAt)))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class EventGroup extends ElemJS {
 | 
			
		||||
	constructor(reactive, list) {
 | 
			
		||||
| 
						 | 
				
			
			@ -314,7 +254,7 @@ class Timeline extends Subscribable {
 | 
			
		|||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				// add new event
 | 
			
		||||
				const event = new Event(eventData)
 | 
			
		||||
				const event = renderEvent(eventData)
 | 
			
		||||
				this.map.set(id, event)
 | 
			
		||||
				this.reactiveTimeline.addEvent(event)
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,3 +5,4 @@ $mild: #393c42
 | 
			
		|||
$milder: #42454a
 | 
			
		||||
$divider: #4b4e54
 | 
			
		||||
$muted: #999
 | 
			
		||||
$link: #57bffd
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								src/sass/components/highlighted-code.sass
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/sass/components/highlighted-code.sass
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
@use "../../../node_modules/highlight.js/scss/obsidian"
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
@use "../colors" as c
 | 
			
		||||
 | 
			
		||||
.c-event-groups *
 | 
			
		||||
.c-event-groups > *
 | 
			
		||||
  overflow-anchor: none
 | 
			
		||||
 | 
			
		||||
.c-message-group, .c-message-event
 | 
			
		||||
| 
						 | 
				
			
			@ -67,6 +67,33 @@
 | 
			
		|||
    &:hover
 | 
			
		||||
      background-color: c.$darker
 | 
			
		||||
 | 
			
		||||
  // message formatting rules
 | 
			
		||||
 | 
			
		||||
  code, pre
 | 
			
		||||
    border-radius: 4px
 | 
			
		||||
    font-size: 0.9em
 | 
			
		||||
 | 
			
		||||
  pre
 | 
			
		||||
    background-color: c.$darkest
 | 
			
		||||
    padding: 8px
 | 
			
		||||
    border: 1px solid c.$divider
 | 
			
		||||
 | 
			
		||||
  code
 | 
			
		||||
    background-color: c.$darker
 | 
			
		||||
    padding: 2px 4px
 | 
			
		||||
 | 
			
		||||
  a
 | 
			
		||||
    color: c.$link
 | 
			
		||||
 | 
			
		||||
  p, pre
 | 
			
		||||
    margin: 16px 0px
 | 
			
		||||
 | 
			
		||||
    &:first-child
 | 
			
		||||
      margin-top: 0px
 | 
			
		||||
 | 
			
		||||
    &:last-child
 | 
			
		||||
      margin-bottom: 0px
 | 
			
		||||
 | 
			
		||||
.c-message-event
 | 
			
		||||
  padding-top: 10px
 | 
			
		||||
  padding-left: 6px
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,4 +5,5 @@
 | 
			
		|||
@use "./components/chat"
 | 
			
		||||
@use "./components/chat-input"
 | 
			
		||||
@use "./components/anchor"
 | 
			
		||||
@use "./components/highlighted-code"
 | 
			
		||||
@use "./loading"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue