forked from cadence/Carbon
		
	Sync with matrix and populate rooms list
This commit is contained in:
		
							parent
							
								
									dd0b14720e
								
							
						
					
					
						commit
						ac6320c12c
					
				
					 18 changed files with 465 additions and 47 deletions
				
			
		
							
								
								
									
										25
									
								
								build.js
									
										
									
									
									
								
							
							
						
						
									
										25
									
								
								build.js
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -80,29 +80,36 @@ function runHint(filename, source) {
 | 
			
		|||
	hint(source, {
 | 
			
		||||
		esversion: 9,
 | 
			
		||||
		undef: true,
 | 
			
		||||
		unused: true,
 | 
			
		||||
		// unused: true,
 | 
			
		||||
		loopfunc: true,
 | 
			
		||||
		globals: ["require", "console", "URLSearchParams", "L"],
 | 
			
		||||
		strict: "global",
 | 
			
		||||
		browser: true
 | 
			
		||||
		globals: ["console", "URLSearchParams"],
 | 
			
		||||
		browser: true,
 | 
			
		||||
		asi: true,
 | 
			
		||||
	})
 | 
			
		||||
	const result = hint.data()
 | 
			
		||||
	if (result.errors && result.errors.length) {
 | 
			
		||||
	let problems = 0
 | 
			
		||||
	if (result.errors) {
 | 
			
		||||
		for (const error of result.errors) {
 | 
			
		||||
			if (error.evidence) {
 | 
			
		||||
				const text = error.evidence.replace(/\t/g, "    ")
 | 
			
		||||
				if ([
 | 
			
		||||
					"W014"
 | 
			
		||||
				].includes(error.code)) continue
 | 
			
		||||
				let type = error.code.startsWith("W") ? chalk.yellow("warning") : chalk.red("error")
 | 
			
		||||
				console.log(`hint: ${type} in ${filename}`)
 | 
			
		||||
				console.log(`    ${error.line}:${error.character}: ${error.reason} (${error.code})`)
 | 
			
		||||
				console.log(chalk.gray(
 | 
			
		||||
					"    "
 | 
			
		||||
					+ text.slice(0, error.character)
 | 
			
		||||
					+ chalk.inverse(text.substr(error.character, 1))
 | 
			
		||||
					+ text.slice(error.character+1)
 | 
			
		||||
						+ text.slice(0, error.character)
 | 
			
		||||
						+ chalk.inverse(text.substr(error.character, 1))
 | 
			
		||||
						+ text.slice(error.character+1)
 | 
			
		||||
				))
 | 
			
		||||
				problems++
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		console.log(`hint: ${chalk.cyan(result.errors.length+" problems")} in ${filename}`)
 | 
			
		||||
	}
 | 
			
		||||
	if (problems) {
 | 
			
		||||
		console.log(`hint: ${chalk.cyan(problems+" problems")} in ${filename}`)
 | 
			
		||||
	} else {
 | 
			
		||||
		console.log(`hint: ${chalk.green("ok")} for ${filename}`)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,10 +2,11 @@
 | 
			
		|||
<html>
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <link rel="stylesheet" type="text/css" href="static/main.css?static=79b6afceb8">
 | 
			
		||||
    <link rel="stylesheet" type="text/css" href="static/main.css?static=b0aba41b0b">
 | 
			
		||||
    <script type="module" src="static/groups.js?static=2cc7f0daf8"></script>
 | 
			
		||||
    <script type="module" src="static/chat-input.js?static=a90499fdac"></script>
 | 
			
		||||
    <script type="module" src="static/room-picker.js?static=596e719ff8"></script>
 | 
			
		||||
    <script type="module" src="static/room-picker.js?static=d92adfaaca"></script>
 | 
			
		||||
    <script type="module" src="static/sync/sync.js?static=232a64285c"></script>
 | 
			
		||||
    <title>Carbon</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +33,7 @@
 | 
			
		|||
                  <div class="c-message-group__name">Cadence</div>
 | 
			
		||||
                  <div class="c-message-group__date">at 4:20 pm</div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="c-message">the second button is for rooms (gonna call them "channels to make discord users happy) that are not in a group (which will be most rooms - few people set up groups because they're so annoying, and many communities of people only need a single chatroom)</div>
 | 
			
		||||
                <div class="c-message">the second button is for rooms (gonna call them "channels" to make discord users happy) that are not in a group (which will be most rooms - few people set up groups because they're so annoying, and many communities of people only need a single chatroom)</div>
 | 
			
		||||
                <div class="c-message">for now, please assume that current groups ("groups v1") will not be recognised by this client at all</div>
 | 
			
		||||
                <div class="c-message">so yeah, press the second button, you see all the ungrouped channels</div>
 | 
			
		||||
              </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										11
									
								
								build/static/lsm.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								build/static/lsm.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
function get(name) {
 | 
			
		||||
	return localStorage.getItem(name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function set(name, value) {
 | 
			
		||||
	return localStorage.setItem(name, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.lsm = {get, set}
 | 
			
		||||
 | 
			
		||||
export {get, set}
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ body {
 | 
			
		|||
	background-color: #2f3135;
 | 
			
		||||
	padding: 8px;
 | 
			
		||||
	width: 240px;
 | 
			
		||||
	font-size: 20px;
 | 
			
		||||
	font-size: 18px;
 | 
			
		||||
	font-weight: 500;
 | 
			
		||||
	overflow-y: auto;
 | 
			
		||||
	scrollbar-width: thin;
 | 
			
		||||
| 
						 | 
				
			
			@ -54,11 +54,13 @@ body {
 | 
			
		|||
.c-room__icon {
 | 
			
		||||
	width: 32px;
 | 
			
		||||
	height: 32px;
 | 
			
		||||
	background-color: #bbb;
 | 
			
		||||
	margin-right: 8px;
 | 
			
		||||
	border-radius: 50%;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
}
 | 
			
		||||
.c-room__icon--no-icon {
 | 
			
		||||
	background-color: #bbb;
 | 
			
		||||
}
 | 
			
		||||
.c-room__name {
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,15 @@
 | 
			
		|||
import {q, ElemJS, ejs} from "./basic.js"
 | 
			
		||||
import {store} from "./store/store.js"
 | 
			
		||||
import * as lsm from "./lsm.js"
 | 
			
		||||
 | 
			
		||||
function resolveMxc(url, size, method) {
 | 
			
		||||
	const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
 | 
			
		||||
	if (size && method) {
 | 
			
		||||
		return `${lsm.get("domain")}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`
 | 
			
		||||
	} else {
 | 
			
		||||
		return `${lsm.get("domain")}/_matrix/media/r0/download/${server}/${id}`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ActiveGroupMarker extends ElemJS {
 | 
			
		||||
	constructor() {
 | 
			
		||||
| 
						 | 
				
			
			@ -51,19 +61,47 @@ class Group extends ElemJS {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
class Room extends ElemJS {
 | 
			
		||||
	constructor(key, data) {
 | 
			
		||||
	constructor(id, data) {
 | 
			
		||||
		super("div")
 | 
			
		||||
 | 
			
		||||
		this.id = id
 | 
			
		||||
		this.data = data
 | 
			
		||||
 | 
			
		||||
		this.class("c-room")
 | 
			
		||||
		this.child(
 | 
			
		||||
			ejs("div").class("c-room__icon"),
 | 
			
		||||
			ejs("div").class("c-room__name").text(this.data.name)
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		this.on("click", this.onClick.bind(this))
 | 
			
		||||
		store.activeRoom.subscribe("changeSelf", this.render.bind(this))
 | 
			
		||||
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getName() {
 | 
			
		||||
		let name = this.data.state.events.find(e => e.type === "m.room.name")
 | 
			
		||||
		if (name) {
 | 
			
		||||
			name = name.content.name
 | 
			
		||||
		} else {
 | 
			
		||||
			const users = this.data.summary["m.heroes"]
 | 
			
		||||
			const usernames = users.map(u => (u.match(/^@([^:]+):/) || [])[1] || u)
 | 
			
		||||
			name = usernames.join(", ")
 | 
			
		||||
		}
 | 
			
		||||
		return name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getIcon() {
 | 
			
		||||
		const avatar = this.data.state.events.find(e => e.type === "m.room.avatar")
 | 
			
		||||
		if (avatar) {
 | 
			
		||||
			return resolveMxc(avatar.content.url || avatar.content.avatar_url, 32, "crop")
 | 
			
		||||
		} else {
 | 
			
		||||
			return null
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	isDirect() {
 | 
			
		||||
		return store.directs.has(this.id)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getGroup() {
 | 
			
		||||
		return this.isDirect() ? store.groups.get("directs").value() : store.groups.get("channels").value()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onClick() {
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +109,16 @@ class Room extends ElemJS {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		// data
 | 
			
		||||
		const icon = this.getIcon()
 | 
			
		||||
		if (icon) {
 | 
			
		||||
			this.child(ejs("img").class("c-room__icon").attribute("src", icon))
 | 
			
		||||
		} else {
 | 
			
		||||
			this.child(ejs("div").class("c-room__icon", "c-room__icon--no-icon"))
 | 
			
		||||
		}
 | 
			
		||||
		this.child(ejs("div").class("c-room__name").text(this.getName()))
 | 
			
		||||
		// active
 | 
			
		||||
		const active = store.activeRoom.value() === this
 | 
			
		||||
		this.element.classList[active ? "add" : "remove"]("c-room--active")
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -84,8 +132,10 @@ class Rooms extends ElemJS {
 | 
			
		|||
		this.rooms = []
 | 
			
		||||
 | 
			
		||||
		store.rooms.subscribe("askAdd", this.askAdd.bind(this))
 | 
			
		||||
		store.rooms.subscribe("changeItem", this.render.bind(this))
 | 
			
		||||
		store.rooms.subscribe("addItem", this.addItem.bind(this))
 | 
			
		||||
		// store.rooms.subscribe("changeItem", this.render.bind(this))
 | 
			
		||||
		store.activeGroup.subscribe("changeSelf", this.render.bind(this))
 | 
			
		||||
		store.directs.subscribe("changeItem", this.render.bind(this))
 | 
			
		||||
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -95,18 +145,25 @@ class Rooms extends ElemJS {
 | 
			
		|||
		store.rooms.addEnd(key, room)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addItem(event, key) {
 | 
			
		||||
		const room = store.rooms.get(key).value()
 | 
			
		||||
		if (room.getGroup() === store.activeGroup.value()) {
 | 
			
		||||
			this.child(room)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		let first = null
 | 
			
		||||
		// set room list
 | 
			
		||||
		store.rooms.forEach((id, room) => {
 | 
			
		||||
			if (room.value().data.group === store.activeGroup.value()) {
 | 
			
		||||
			if (room.value().getGroup() === store.activeGroup.value()) {
 | 
			
		||||
				if (!first) first = room.value()
 | 
			
		||||
				this.child(room.value())
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
		// if needed, change the active room to be an item in the room list
 | 
			
		||||
		if (!store.activeRoom.exists() || store.activeRoom.value().data.group !== store.activeGroup.value()) {
 | 
			
		||||
		if (!store.activeRoom.exists() || store.activeRoom.value().getGroup() !== store.activeGroup.value()) {
 | 
			
		||||
			if (first) {
 | 
			
		||||
				store.activeRoom.set(first)
 | 
			
		||||
			} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -136,6 +193,7 @@ class Groups extends ElemJS {
 | 
			
		|||
}
 | 
			
		||||
const groups = new Groups()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
;[
 | 
			
		||||
	{
 | 
			
		||||
		id: "directs",
 | 
			
		||||
| 
						 | 
				
			
			@ -146,7 +204,7 @@ const groups = new Groups()
 | 
			
		|||
		id: "channels",
 | 
			
		||||
		name: "Channels",
 | 
			
		||||
		icon: "/static/channels.svg"
 | 
			
		||||
	},
 | 
			
		||||
	}/*,
 | 
			
		||||
	{
 | 
			
		||||
		id: "123",
 | 
			
		||||
		name: "Fediverse Drama Museum"
 | 
			
		||||
| 
						 | 
				
			
			@ -158,9 +216,10 @@ const groups = new Groups()
 | 
			
		|||
	{
 | 
			
		||||
		id: "789",
 | 
			
		||||
		name: "Invidious"
 | 
			
		||||
	}
 | 
			
		||||
	}*/
 | 
			
		||||
].forEach(data => store.groups.askAdd(data.id, data))
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
;[
 | 
			
		||||
	{id: "001", name: "riley", group: store.groups.get("directs").value()},
 | 
			
		||||
	{id: "002", name: "BadAtNames", group: store.groups.get("directs").value()},
 | 
			
		||||
| 
						 | 
				
			
			@ -188,5 +247,6 @@ const groups = new Groups()
 | 
			
		|||
	{id: "024", name: "osu", group: store.groups.get("456").value()},
 | 
			
		||||
	{id: "025", name: "covid", group: store.groups.get("456").value()}
 | 
			
		||||
].forEach(data => store.rooms.askAdd(data.id, data))
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
store.activeGroup.set(store.groups.get("directs").value())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,14 +7,14 @@ class SubscribeMapList extends Subscribable {
 | 
			
		|||
		this.inner = inner
 | 
			
		||||
		Object.assign(this.events, {
 | 
			
		||||
			addItem: [],
 | 
			
		||||
			removeItem: [],
 | 
			
		||||
			deleteItem: [],
 | 
			
		||||
			editItem: [],
 | 
			
		||||
			changeItem: [],
 | 
			
		||||
			askAdd: []
 | 
			
		||||
		})
 | 
			
		||||
		Object.assign(this.eventDeps, {
 | 
			
		||||
			addItem: ["changeItem"],
 | 
			
		||||
			removeItem: ["changeItem"],
 | 
			
		||||
			deleteItem: ["changeItem"],
 | 
			
		||||
			editItem: ["changeItem"],
 | 
			
		||||
			changeItem: [],
 | 
			
		||||
			askAdd: []
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +59,7 @@ class SubscribeMapList extends Subscribable {
 | 
			
		|||
			const exists = this.map.get(key).exists()
 | 
			
		||||
			s = this.map.get(key).set(value)
 | 
			
		||||
			if (exists) {
 | 
			
		||||
				this.broadcast("changeItem", key)
 | 
			
		||||
				this.broadcast("editItem", key)
 | 
			
		||||
			} else {
 | 
			
		||||
				this.broadcast("addItem", key)
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										50
									
								
								build/static/store/SubscribeSet.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								build/static/store/SubscribeSet.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
import {Subscribable} from "./Subscribable.js"
 | 
			
		||||
 | 
			
		||||
class SubscribeSet extends Subscribable {
 | 
			
		||||
	constructor() {
 | 
			
		||||
		super()
 | 
			
		||||
		Object.assign(this.events, {
 | 
			
		||||
			addItem: [],
 | 
			
		||||
			deleteItem: [],
 | 
			
		||||
			changeItem: [],
 | 
			
		||||
			askAdd: []
 | 
			
		||||
		})
 | 
			
		||||
		Object.assign(this.eventDeps, {
 | 
			
		||||
			addItem: ["changeItem"],
 | 
			
		||||
			deleteItem: ["changeItem"],
 | 
			
		||||
			changeItem: [],
 | 
			
		||||
			askAdd: []
 | 
			
		||||
		})
 | 
			
		||||
		this.set = new Set()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	has(key) {
 | 
			
		||||
		return this.set.has(key)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	forEach(f) {
 | 
			
		||||
		for (const key of this.set.keys()) {
 | 
			
		||||
			f(key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	askAdd(key) {
 | 
			
		||||
		this.broadcast("askAdd", key)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	add(key) {
 | 
			
		||||
		if (!this.set.has(key)) {
 | 
			
		||||
			this.set.add(key)
 | 
			
		||||
			this.broadcast("addItem", key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	delete(key) {
 | 
			
		||||
		if (this.set.has(key)) {
 | 
			
		||||
			this.set.delete(key)
 | 
			
		||||
			this.broadcast("deleteItem", key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {SubscribeSet}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,11 @@
 | 
			
		|||
import {SubscribeMapList} from "./SubscribeMapList.js"
 | 
			
		||||
import {SubscribeSet} from "./SubscribeSet.js"
 | 
			
		||||
import {SubscribeValue} from "./SubscribeValue.js"
 | 
			
		||||
 | 
			
		||||
const store = {
 | 
			
		||||
	groups: new SubscribeMapList(SubscribeValue),
 | 
			
		||||
	rooms: new SubscribeMapList(SubscribeValue),
 | 
			
		||||
	directs: new SubscribeSet(),
 | 
			
		||||
	activeGroup: new SubscribeValue(),
 | 
			
		||||
	activeRoom: new SubscribeValue()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										62
									
								
								build/static/sync/sync.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								build/static/sync/sync.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
import {store} from "../store/store.js"
 | 
			
		||||
import * as lsm from "../lsm.js"
 | 
			
		||||
 | 
			
		||||
let lastBatch = null
 | 
			
		||||
 | 
			
		||||
function sync() {
 | 
			
		||||
	const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/sync`)
 | 
			
		||||
	url.searchParams.append("access_token", lsm.get("access_token"))
 | 
			
		||||
	const filter = {
 | 
			
		||||
		room: {
 | 
			
		||||
			// pulling more from the timeline massively increases download size
 | 
			
		||||
			timeline: {
 | 
			
		||||
				limit: 5
 | 
			
		||||
			},
 | 
			
		||||
			// members are not currently needed
 | 
			
		||||
			state: {
 | 
			
		||||
				lazy_load_members: true
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		presence: {
 | 
			
		||||
			// presence is not implemented, ignore it
 | 
			
		||||
			types: []
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	url.searchParams.append("filter", JSON.stringify(filter))
 | 
			
		||||
	url.searchParams.append("timeout", 20000)
 | 
			
		||||
	if (lastBatch) {
 | 
			
		||||
		url.searchParams.append("since", lastBatch)
 | 
			
		||||
	}
 | 
			
		||||
	return fetch(url.toString()).then(res => res.json()).then(root => {
 | 
			
		||||
		lastBatch = root.next_batch
 | 
			
		||||
		return root
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function manageSync(root) {
 | 
			
		||||
	try {
 | 
			
		||||
		// set up directs
 | 
			
		||||
		const directs = root.account_data.events.find(e => e.type === "m.direct")
 | 
			
		||||
		if (directs) {
 | 
			
		||||
			Object.values(directs.content).forEach(ids => {
 | 
			
		||||
				ids.forEach(id => store.directs.add(id))
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// set up rooms
 | 
			
		||||
		Object.entries(root.rooms.join).forEach(([id, room]) => {
 | 
			
		||||
			if (!store.rooms.has(id)) {
 | 
			
		||||
				store.rooms.askAdd(id, room)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		console.error(root)
 | 
			
		||||
		throw e
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function syncLoop() {
 | 
			
		||||
	return sync().then(manageSync).then(syncLoop)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
syncLoop()
 | 
			
		||||
							
								
								
									
										43
									
								
								spec.js
									
										
									
									
									
								
							
							
						
						
									
										43
									
								
								spec.js
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -10,25 +10,60 @@ module.exports = [
 | 
			
		|||
		target: "/static/whitney-400.woff"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "file",
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/basic.js",
 | 
			
		||||
		target: "/static/basic.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "file",
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/groups.js",
 | 
			
		||||
		target: "/static/groups.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "file",
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/chat-input.js",
 | 
			
		||||
		target: "/static/chat-input.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "file",
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/room-picker.js",
 | 
			
		||||
		target: "/static/room-picker.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/store/store.js",
 | 
			
		||||
		target: "/static/store/store.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/store/Subscribable.js",
 | 
			
		||||
		target: "/static/store/Subscribable.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/store/SubscribeValue.js",
 | 
			
		||||
		target: "/static/store/SubscribeValue.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/store/SubscribeMapList.js",
 | 
			
		||||
		target: "/static/store/SubscribeMapList.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/store/SubscribeSet.js",
 | 
			
		||||
		target: "/static/store/SubscribeSet.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/sync/sync.js",
 | 
			
		||||
		target: "/static/sync/sync.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/lsm.js",
 | 
			
		||||
		target: "/static/lsm.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "file",
 | 
			
		||||
		source: "/assets/fonts/whitney-500.woff",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,6 +37,7 @@ html
 | 
			
		|||
    script(type="module" src=getStatic("/js/groups.js"))
 | 
			
		||||
    script(type="module" src=getStatic("/js/chat-input.js"))
 | 
			
		||||
    script(type="module" src=getStatic("/js/room-picker.js"))
 | 
			
		||||
    script(type="module" src=getStatic("/js/sync/sync.js"))
 | 
			
		||||
    title Carbon
 | 
			
		||||
  body
 | 
			
		||||
    main.main
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +51,7 @@ html
 | 
			
		|||
          .c-chat__inner
 | 
			
		||||
            +message-notice("You've reached the start of the conversation.")
 | 
			
		||||
            +message("Cadence", [
 | 
			
		||||
              `the second button is for rooms (gonna call them "channels to make discord users happy) that are not in a group (which will be most rooms - few people set up groups because they're so annoying, and many communities of people only need a single chatroom)`,
 | 
			
		||||
              `the second button is for rooms (gonna call them "channels" to make discord users happy) that are not in a group (which will be most rooms - few people set up groups because they're so annoying, and many communities of people only need a single chatroom)`,
 | 
			
		||||
              `for now, please assume that current groups ("groups v1") will not be recognised by this client at all`,
 | 
			
		||||
              `so yeah, press the second button, you see all the ungrouped channels`
 | 
			
		||||
            ])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										11
									
								
								src/js/lsm.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/js/lsm.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
function get(name) {
 | 
			
		||||
	return localStorage.getItem(name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function set(name, value) {
 | 
			
		||||
	return localStorage.setItem(name, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.lsm = {get, set}
 | 
			
		||||
 | 
			
		||||
export {get, set}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,15 @@
 | 
			
		|||
import {q, ElemJS, ejs} from "./basic.js"
 | 
			
		||||
import {store} from "./store/store.js"
 | 
			
		||||
import * as lsm from "./lsm.js"
 | 
			
		||||
 | 
			
		||||
function resolveMxc(url, size, method) {
 | 
			
		||||
	const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
 | 
			
		||||
	if (size && method) {
 | 
			
		||||
		return `${lsm.get("domain")}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`
 | 
			
		||||
	} else {
 | 
			
		||||
		return `${lsm.get("domain")}/_matrix/media/r0/download/${server}/${id}`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ActiveGroupMarker extends ElemJS {
 | 
			
		||||
	constructor() {
 | 
			
		||||
| 
						 | 
				
			
			@ -51,19 +61,47 @@ class Group extends ElemJS {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
class Room extends ElemJS {
 | 
			
		||||
	constructor(key, data) {
 | 
			
		||||
	constructor(id, data) {
 | 
			
		||||
		super("div")
 | 
			
		||||
 | 
			
		||||
		this.id = id
 | 
			
		||||
		this.data = data
 | 
			
		||||
 | 
			
		||||
		this.class("c-room")
 | 
			
		||||
		this.child(
 | 
			
		||||
			ejs("div").class("c-room__icon"),
 | 
			
		||||
			ejs("div").class("c-room__name").text(this.data.name)
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		this.on("click", this.onClick.bind(this))
 | 
			
		||||
		store.activeRoom.subscribe("changeSelf", this.render.bind(this))
 | 
			
		||||
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getName() {
 | 
			
		||||
		let name = this.data.state.events.find(e => e.type === "m.room.name")
 | 
			
		||||
		if (name) {
 | 
			
		||||
			name = name.content.name
 | 
			
		||||
		} else {
 | 
			
		||||
			const users = this.data.summary["m.heroes"]
 | 
			
		||||
			const usernames = users.map(u => (u.match(/^@([^:]+):/) || [])[1] || u)
 | 
			
		||||
			name = usernames.join(", ")
 | 
			
		||||
		}
 | 
			
		||||
		return name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getIcon() {
 | 
			
		||||
		const avatar = this.data.state.events.find(e => e.type === "m.room.avatar")
 | 
			
		||||
		if (avatar) {
 | 
			
		||||
			return resolveMxc(avatar.content.url || avatar.content.avatar_url, 32, "crop")
 | 
			
		||||
		} else {
 | 
			
		||||
			return null
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	isDirect() {
 | 
			
		||||
		return store.directs.has(this.id)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getGroup() {
 | 
			
		||||
		return this.isDirect() ? store.groups.get("directs").value() : store.groups.get("channels").value()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onClick() {
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +109,16 @@ class Room extends ElemJS {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		// data
 | 
			
		||||
		const icon = this.getIcon()
 | 
			
		||||
		if (icon) {
 | 
			
		||||
			this.child(ejs("img").class("c-room__icon").attribute("src", icon))
 | 
			
		||||
		} else {
 | 
			
		||||
			this.child(ejs("div").class("c-room__icon", "c-room__icon--no-icon"))
 | 
			
		||||
		}
 | 
			
		||||
		this.child(ejs("div").class("c-room__name").text(this.getName()))
 | 
			
		||||
		// active
 | 
			
		||||
		const active = store.activeRoom.value() === this
 | 
			
		||||
		this.element.classList[active ? "add" : "remove"]("c-room--active")
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -84,8 +132,10 @@ class Rooms extends ElemJS {
 | 
			
		|||
		this.rooms = []
 | 
			
		||||
 | 
			
		||||
		store.rooms.subscribe("askAdd", this.askAdd.bind(this))
 | 
			
		||||
		store.rooms.subscribe("changeItem", this.render.bind(this))
 | 
			
		||||
		store.rooms.subscribe("addItem", this.addItem.bind(this))
 | 
			
		||||
		// store.rooms.subscribe("changeItem", this.render.bind(this))
 | 
			
		||||
		store.activeGroup.subscribe("changeSelf", this.render.bind(this))
 | 
			
		||||
		store.directs.subscribe("changeItem", this.render.bind(this))
 | 
			
		||||
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -95,18 +145,25 @@ class Rooms extends ElemJS {
 | 
			
		|||
		store.rooms.addEnd(key, room)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addItem(event, key) {
 | 
			
		||||
		const room = store.rooms.get(key).value()
 | 
			
		||||
		if (room.getGroup() === store.activeGroup.value()) {
 | 
			
		||||
			this.child(room)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		let first = null
 | 
			
		||||
		// set room list
 | 
			
		||||
		store.rooms.forEach((id, room) => {
 | 
			
		||||
			if (room.value().data.group === store.activeGroup.value()) {
 | 
			
		||||
			if (room.value().getGroup() === store.activeGroup.value()) {
 | 
			
		||||
				if (!first) first = room.value()
 | 
			
		||||
				this.child(room.value())
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
		// if needed, change the active room to be an item in the room list
 | 
			
		||||
		if (!store.activeRoom.exists() || store.activeRoom.value().data.group !== store.activeGroup.value()) {
 | 
			
		||||
		if (!store.activeRoom.exists() || store.activeRoom.value().getGroup() !== store.activeGroup.value()) {
 | 
			
		||||
			if (first) {
 | 
			
		||||
				store.activeRoom.set(first)
 | 
			
		||||
			} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -136,6 +193,7 @@ class Groups extends ElemJS {
 | 
			
		|||
}
 | 
			
		||||
const groups = new Groups()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
;[
 | 
			
		||||
	{
 | 
			
		||||
		id: "directs",
 | 
			
		||||
| 
						 | 
				
			
			@ -146,7 +204,7 @@ const groups = new Groups()
 | 
			
		|||
		id: "channels",
 | 
			
		||||
		name: "Channels",
 | 
			
		||||
		icon: "/static/channels.svg"
 | 
			
		||||
	},
 | 
			
		||||
	}/*,
 | 
			
		||||
	{
 | 
			
		||||
		id: "123",
 | 
			
		||||
		name: "Fediverse Drama Museum"
 | 
			
		||||
| 
						 | 
				
			
			@ -158,9 +216,10 @@ const groups = new Groups()
 | 
			
		|||
	{
 | 
			
		||||
		id: "789",
 | 
			
		||||
		name: "Invidious"
 | 
			
		||||
	}
 | 
			
		||||
	}*/
 | 
			
		||||
].forEach(data => store.groups.askAdd(data.id, data))
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
;[
 | 
			
		||||
	{id: "001", name: "riley", group: store.groups.get("directs").value()},
 | 
			
		||||
	{id: "002", name: "BadAtNames", group: store.groups.get("directs").value()},
 | 
			
		||||
| 
						 | 
				
			
			@ -188,5 +247,6 @@ const groups = new Groups()
 | 
			
		|||
	{id: "024", name: "osu", group: store.groups.get("456").value()},
 | 
			
		||||
	{id: "025", name: "covid", group: store.groups.get("456").value()}
 | 
			
		||||
].forEach(data => store.rooms.askAdd(data.id, data))
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
store.activeGroup.set(store.groups.get("directs").value())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,14 +7,14 @@ class SubscribeMapList extends Subscribable {
 | 
			
		|||
		this.inner = inner
 | 
			
		||||
		Object.assign(this.events, {
 | 
			
		||||
			addItem: [],
 | 
			
		||||
			removeItem: [],
 | 
			
		||||
			deleteItem: [],
 | 
			
		||||
			editItem: [],
 | 
			
		||||
			changeItem: [],
 | 
			
		||||
			askAdd: []
 | 
			
		||||
		})
 | 
			
		||||
		Object.assign(this.eventDeps, {
 | 
			
		||||
			addItem: ["changeItem"],
 | 
			
		||||
			removeItem: ["changeItem"],
 | 
			
		||||
			deleteItem: ["changeItem"],
 | 
			
		||||
			editItem: ["changeItem"],
 | 
			
		||||
			changeItem: [],
 | 
			
		||||
			askAdd: []
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +59,7 @@ class SubscribeMapList extends Subscribable {
 | 
			
		|||
			const exists = this.map.get(key).exists()
 | 
			
		||||
			s = this.map.get(key).set(value)
 | 
			
		||||
			if (exists) {
 | 
			
		||||
				this.broadcast("changeItem", key)
 | 
			
		||||
				this.broadcast("editItem", key)
 | 
			
		||||
			} else {
 | 
			
		||||
				this.broadcast("addItem", key)
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										50
									
								
								src/js/store/SubscribeSet.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/js/store/SubscribeSet.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
import {Subscribable} from "./Subscribable.js"
 | 
			
		||||
 | 
			
		||||
class SubscribeSet extends Subscribable {
 | 
			
		||||
	constructor() {
 | 
			
		||||
		super()
 | 
			
		||||
		Object.assign(this.events, {
 | 
			
		||||
			addItem: [],
 | 
			
		||||
			deleteItem: [],
 | 
			
		||||
			changeItem: [],
 | 
			
		||||
			askAdd: []
 | 
			
		||||
		})
 | 
			
		||||
		Object.assign(this.eventDeps, {
 | 
			
		||||
			addItem: ["changeItem"],
 | 
			
		||||
			deleteItem: ["changeItem"],
 | 
			
		||||
			changeItem: [],
 | 
			
		||||
			askAdd: []
 | 
			
		||||
		})
 | 
			
		||||
		this.set = new Set()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	has(key) {
 | 
			
		||||
		return this.set.has(key)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	forEach(f) {
 | 
			
		||||
		for (const key of this.set.keys()) {
 | 
			
		||||
			f(key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	askAdd(key) {
 | 
			
		||||
		this.broadcast("askAdd", key)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	add(key) {
 | 
			
		||||
		if (!this.set.has(key)) {
 | 
			
		||||
			this.set.add(key)
 | 
			
		||||
			this.broadcast("addItem", key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	delete(key) {
 | 
			
		||||
		if (this.set.has(key)) {
 | 
			
		||||
			this.set.delete(key)
 | 
			
		||||
			this.broadcast("deleteItem", key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {SubscribeSet}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,11 @@
 | 
			
		|||
import {SubscribeMapList} from "./SubscribeMapList.js"
 | 
			
		||||
import {SubscribeSet} from "./SubscribeSet.js"
 | 
			
		||||
import {SubscribeValue} from "./SubscribeValue.js"
 | 
			
		||||
 | 
			
		||||
const store = {
 | 
			
		||||
	groups: new SubscribeMapList(SubscribeValue),
 | 
			
		||||
	rooms: new SubscribeMapList(SubscribeValue),
 | 
			
		||||
	directs: new SubscribeSet(),
 | 
			
		||||
	activeGroup: new SubscribeValue(),
 | 
			
		||||
	activeRoom: new SubscribeValue()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										62
									
								
								src/js/sync/sync.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/js/sync/sync.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
import {store} from "../store/store.js"
 | 
			
		||||
import * as lsm from "../lsm.js"
 | 
			
		||||
 | 
			
		||||
let lastBatch = null
 | 
			
		||||
 | 
			
		||||
function sync() {
 | 
			
		||||
	const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/sync`)
 | 
			
		||||
	url.searchParams.append("access_token", lsm.get("access_token"))
 | 
			
		||||
	const filter = {
 | 
			
		||||
		room: {
 | 
			
		||||
			// pulling more from the timeline massively increases download size
 | 
			
		||||
			timeline: {
 | 
			
		||||
				limit: 5
 | 
			
		||||
			},
 | 
			
		||||
			// members are not currently needed
 | 
			
		||||
			state: {
 | 
			
		||||
				lazy_load_members: true
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		presence: {
 | 
			
		||||
			// presence is not implemented, ignore it
 | 
			
		||||
			types: []
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	url.searchParams.append("filter", JSON.stringify(filter))
 | 
			
		||||
	url.searchParams.append("timeout", 20000)
 | 
			
		||||
	if (lastBatch) {
 | 
			
		||||
		url.searchParams.append("since", lastBatch)
 | 
			
		||||
	}
 | 
			
		||||
	return fetch(url.toString()).then(res => res.json()).then(root => {
 | 
			
		||||
		lastBatch = root.next_batch
 | 
			
		||||
		return root
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function manageSync(root) {
 | 
			
		||||
	try {
 | 
			
		||||
		// set up directs
 | 
			
		||||
		const directs = root.account_data.events.find(e => e.type === "m.direct")
 | 
			
		||||
		if (directs) {
 | 
			
		||||
			Object.values(directs.content).forEach(ids => {
 | 
			
		||||
				ids.forEach(id => store.directs.add(id))
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// set up rooms
 | 
			
		||||
		Object.entries(root.rooms.join).forEach(([id, room]) => {
 | 
			
		||||
			if (!store.rooms.has(id)) {
 | 
			
		||||
				store.rooms.askAdd(id, room)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		console.error(root)
 | 
			
		||||
		throw e
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function syncLoop() {
 | 
			
		||||
	return sync().then(manageSync).then(syncLoop)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
syncLoop()
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ $icon-padding: 8px
 | 
			
		|||
  background-color: c.$darker
 | 
			
		||||
  padding: $icon-padding
 | 
			
		||||
  width: $list-width
 | 
			
		||||
  font-size: 20px
 | 
			
		||||
  font-size: 18px
 | 
			
		||||
  font-weight: 500
 | 
			
		||||
  overflow-y: auto
 | 
			
		||||
  scrollbar-width: thin
 | 
			
		||||
| 
						 | 
				
			
			@ -32,11 +32,13 @@ $icon-padding: 8px
 | 
			
		|||
  &__icon
 | 
			
		||||
    width: $icon-size
 | 
			
		||||
    height: $icon-size
 | 
			
		||||
    background-color: #bbb
 | 
			
		||||
    margin-right: $icon-padding
 | 
			
		||||
    border-radius: 50%
 | 
			
		||||
    flex-shrink: 0
 | 
			
		||||
 | 
			
		||||
    &--no-icon
 | 
			
		||||
      background-color: #bbb
 | 
			
		||||
 | 
			
		||||
  &__name
 | 
			
		||||
    white-space: nowrap
 | 
			
		||||
    overflow: hidden
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue