Sync with matrix and populate rooms list

This commit is contained in:
Cadence Ember 2020-10-15 22:06:41 +13:00
parent dd0b14720e
commit ac6320c12c
Signed by: cadence
GPG key ID: BC1C2C61CF521B17
18 changed files with 465 additions and 47 deletions

View file

@ -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}`)
}

View file

@ -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 &quot;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 &quot;channels&quot; 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 (&quot;groups v1&quot;) 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
View 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}

View file

@ -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;

View file

@ -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())

View file

@ -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)
}

View 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}

View file

@ -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
View 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
View file

@ -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",

View file

@ -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
View 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}

View file

@ -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())

View file

@ -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)
}

View 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}

View file

@ -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
View 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()

View file

@ -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