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, {
|
hint(source, {
|
||||||
esversion: 9,
|
esversion: 9,
|
||||||
undef: true,
|
undef: true,
|
||||||
unused: true,
|
// unused: true,
|
||||||
loopfunc: true,
|
loopfunc: true,
|
||||||
globals: ["require", "console", "URLSearchParams", "L"],
|
globals: ["console", "URLSearchParams"],
|
||||||
strict: "global",
|
browser: true,
|
||||||
browser: true
|
asi: true,
|
||||||
})
|
})
|
||||||
const result = hint.data()
|
const result = hint.data()
|
||||||
if (result.errors && result.errors.length) {
|
let problems = 0
|
||||||
|
if (result.errors) {
|
||||||
for (const error of result.errors) {
|
for (const error of result.errors) {
|
||||||
if (error.evidence) {
|
if (error.evidence) {
|
||||||
const text = error.evidence.replace(/\t/g, " ")
|
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")
|
let type = error.code.startsWith("W") ? chalk.yellow("warning") : chalk.red("error")
|
||||||
console.log(`hint: ${type} in ${filename}`)
|
console.log(`hint: ${type} in ${filename}`)
|
||||||
console.log(` ${error.line}:${error.character}: ${error.reason} (${error.code})`)
|
console.log(` ${error.line}:${error.character}: ${error.reason} (${error.code})`)
|
||||||
console.log(chalk.gray(
|
console.log(chalk.gray(
|
||||||
" "
|
" "
|
||||||
+ text.slice(0, error.character)
|
+ text.slice(0, error.character)
|
||||||
+ chalk.inverse(text.substr(error.character, 1))
|
+ chalk.inverse(text.substr(error.character, 1))
|
||||||
+ text.slice(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 {
|
} else {
|
||||||
console.log(`hint: ${chalk.green("ok")} for ${filename}`)
|
console.log(`hint: ${chalk.green("ok")} for ${filename}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<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/groups.js?static=2cc7f0daf8"></script>
|
||||||
<script type="module" src="static/chat-input.js?static=a90499fdac"></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>
|
<title>Carbon</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -32,7 +33,7 @@
|
||||||
<div class="c-message-group__name">Cadence</div>
|
<div class="c-message-group__name">Cadence</div>
|
||||||
<div class="c-message-group__date">at 4:20 pm</div>
|
<div class="c-message-group__date">at 4:20 pm</div>
|
||||||
</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">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 class="c-message">so yeah, press the second button, you see all the ungrouped channels</div>
|
||||||
</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;
|
background-color: #2f3135;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
width: 240px;
|
width: 240px;
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
@ -54,11 +54,13 @@ body {
|
||||||
.c-room__icon {
|
.c-room__icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
background-color: #bbb;
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.c-room__icon--no-icon {
|
||||||
|
background-color: #bbb;
|
||||||
|
}
|
||||||
.c-room__name {
|
.c-room__name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
import {q, ElemJS, ejs} from "./basic.js"
|
import {q, ElemJS, ejs} from "./basic.js"
|
||||||
import {store} from "./store/store.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 {
|
class ActiveGroupMarker extends ElemJS {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -51,19 +61,47 @@ class Group extends ElemJS {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Room extends ElemJS {
|
class Room extends ElemJS {
|
||||||
constructor(key, data) {
|
constructor(id, data) {
|
||||||
super("div")
|
super("div")
|
||||||
|
|
||||||
|
this.id = id
|
||||||
this.data = data
|
this.data = data
|
||||||
|
|
||||||
this.class("c-room")
|
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))
|
this.on("click", this.onClick.bind(this))
|
||||||
store.activeRoom.subscribe("changeSelf", this.render.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() {
|
onClick() {
|
||||||
|
@ -71,6 +109,16 @@ class Room extends ElemJS {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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
|
const active = store.activeRoom.value() === this
|
||||||
this.element.classList[active ? "add" : "remove"]("c-room--active")
|
this.element.classList[active ? "add" : "remove"]("c-room--active")
|
||||||
}
|
}
|
||||||
|
@ -84,8 +132,10 @@ class Rooms extends ElemJS {
|
||||||
this.rooms = []
|
this.rooms = []
|
||||||
|
|
||||||
store.rooms.subscribe("askAdd", this.askAdd.bind(this))
|
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.activeGroup.subscribe("changeSelf", this.render.bind(this))
|
||||||
|
store.directs.subscribe("changeItem", this.render.bind(this))
|
||||||
|
|
||||||
this.render()
|
this.render()
|
||||||
}
|
}
|
||||||
|
@ -95,18 +145,25 @@ class Rooms extends ElemJS {
|
||||||
store.rooms.addEnd(key, room)
|
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() {
|
render() {
|
||||||
this.clearChildren()
|
this.clearChildren()
|
||||||
let first = null
|
let first = null
|
||||||
// set room list
|
// set room list
|
||||||
store.rooms.forEach((id, room) => {
|
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()
|
if (!first) first = room.value()
|
||||||
this.child(room.value())
|
this.child(room.value())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// if needed, change the active room to be an item in the room list
|
// 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) {
|
if (first) {
|
||||||
store.activeRoom.set(first)
|
store.activeRoom.set(first)
|
||||||
} else {
|
} else {
|
||||||
|
@ -136,6 +193,7 @@ class Groups extends ElemJS {
|
||||||
}
|
}
|
||||||
const groups = new Groups()
|
const groups = new Groups()
|
||||||
|
|
||||||
|
|
||||||
;[
|
;[
|
||||||
{
|
{
|
||||||
id: "directs",
|
id: "directs",
|
||||||
|
@ -146,7 +204,7 @@ const groups = new Groups()
|
||||||
id: "channels",
|
id: "channels",
|
||||||
name: "Channels",
|
name: "Channels",
|
||||||
icon: "/static/channels.svg"
|
icon: "/static/channels.svg"
|
||||||
},
|
}/*,
|
||||||
{
|
{
|
||||||
id: "123",
|
id: "123",
|
||||||
name: "Fediverse Drama Museum"
|
name: "Fediverse Drama Museum"
|
||||||
|
@ -158,9 +216,10 @@ const groups = new Groups()
|
||||||
{
|
{
|
||||||
id: "789",
|
id: "789",
|
||||||
name: "Invidious"
|
name: "Invidious"
|
||||||
}
|
}*/
|
||||||
].forEach(data => store.groups.askAdd(data.id, data))
|
].forEach(data => store.groups.askAdd(data.id, data))
|
||||||
|
|
||||||
|
/*
|
||||||
;[
|
;[
|
||||||
{id: "001", name: "riley", group: store.groups.get("directs").value()},
|
{id: "001", name: "riley", group: store.groups.get("directs").value()},
|
||||||
{id: "002", name: "BadAtNames", 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: "024", name: "osu", group: store.groups.get("456").value()},
|
||||||
{id: "025", name: "covid", group: store.groups.get("456").value()}
|
{id: "025", name: "covid", group: store.groups.get("456").value()}
|
||||||
].forEach(data => store.rooms.askAdd(data.id, data))
|
].forEach(data => store.rooms.askAdd(data.id, data))
|
||||||
|
*/
|
||||||
|
|
||||||
store.activeGroup.set(store.groups.get("directs").value())
|
store.activeGroup.set(store.groups.get("directs").value())
|
||||||
|
|
|
@ -7,14 +7,14 @@ class SubscribeMapList extends Subscribable {
|
||||||
this.inner = inner
|
this.inner = inner
|
||||||
Object.assign(this.events, {
|
Object.assign(this.events, {
|
||||||
addItem: [],
|
addItem: [],
|
||||||
removeItem: [],
|
deleteItem: [],
|
||||||
editItem: [],
|
editItem: [],
|
||||||
changeItem: [],
|
changeItem: [],
|
||||||
askAdd: []
|
askAdd: []
|
||||||
})
|
})
|
||||||
Object.assign(this.eventDeps, {
|
Object.assign(this.eventDeps, {
|
||||||
addItem: ["changeItem"],
|
addItem: ["changeItem"],
|
||||||
removeItem: ["changeItem"],
|
deleteItem: ["changeItem"],
|
||||||
editItem: ["changeItem"],
|
editItem: ["changeItem"],
|
||||||
changeItem: [],
|
changeItem: [],
|
||||||
askAdd: []
|
askAdd: []
|
||||||
|
@ -59,7 +59,7 @@ class SubscribeMapList extends Subscribable {
|
||||||
const exists = this.map.get(key).exists()
|
const exists = this.map.get(key).exists()
|
||||||
s = this.map.get(key).set(value)
|
s = this.map.get(key).set(value)
|
||||||
if (exists) {
|
if (exists) {
|
||||||
this.broadcast("changeItem", key)
|
this.broadcast("editItem", key)
|
||||||
} else {
|
} else {
|
||||||
this.broadcast("addItem", key)
|
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 {SubscribeMapList} from "./SubscribeMapList.js"
|
||||||
|
import {SubscribeSet} from "./SubscribeSet.js"
|
||||||
import {SubscribeValue} from "./SubscribeValue.js"
|
import {SubscribeValue} from "./SubscribeValue.js"
|
||||||
|
|
||||||
const store = {
|
const store = {
|
||||||
groups: new SubscribeMapList(SubscribeValue),
|
groups: new SubscribeMapList(SubscribeValue),
|
||||||
rooms: new SubscribeMapList(SubscribeValue),
|
rooms: new SubscribeMapList(SubscribeValue),
|
||||||
|
directs: new SubscribeSet(),
|
||||||
activeGroup: new SubscribeValue(),
|
activeGroup: new SubscribeValue(),
|
||||||
activeRoom: 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"
|
target: "/static/whitney-400.woff"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "file",
|
type: "js",
|
||||||
source: "/js/basic.js",
|
source: "/js/basic.js",
|
||||||
target: "/static/basic.js"
|
target: "/static/basic.js"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "file",
|
type: "js",
|
||||||
source: "/js/groups.js",
|
source: "/js/groups.js",
|
||||||
target: "/static/groups.js"
|
target: "/static/groups.js"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "file",
|
type: "js",
|
||||||
source: "/js/chat-input.js",
|
source: "/js/chat-input.js",
|
||||||
target: "/static/chat-input.js"
|
target: "/static/chat-input.js"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "file",
|
type: "js",
|
||||||
source: "/js/room-picker.js",
|
source: "/js/room-picker.js",
|
||||||
target: "/static/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",
|
type: "file",
|
||||||
source: "/assets/fonts/whitney-500.woff",
|
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/groups.js"))
|
||||||
script(type="module" src=getStatic("/js/chat-input.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/room-picker.js"))
|
||||||
|
script(type="module" src=getStatic("/js/sync/sync.js"))
|
||||||
title Carbon
|
title Carbon
|
||||||
body
|
body
|
||||||
main.main
|
main.main
|
||||||
|
@ -50,7 +51,7 @@ html
|
||||||
.c-chat__inner
|
.c-chat__inner
|
||||||
+message-notice("You've reached the start of the conversation.")
|
+message-notice("You've reached the start of the conversation.")
|
||||||
+message("Cadence", [
|
+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`,
|
`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`
|
`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 {q, ElemJS, ejs} from "./basic.js"
|
||||||
import {store} from "./store/store.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 {
|
class ActiveGroupMarker extends ElemJS {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -51,19 +61,47 @@ class Group extends ElemJS {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Room extends ElemJS {
|
class Room extends ElemJS {
|
||||||
constructor(key, data) {
|
constructor(id, data) {
|
||||||
super("div")
|
super("div")
|
||||||
|
|
||||||
|
this.id = id
|
||||||
this.data = data
|
this.data = data
|
||||||
|
|
||||||
this.class("c-room")
|
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))
|
this.on("click", this.onClick.bind(this))
|
||||||
store.activeRoom.subscribe("changeSelf", this.render.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() {
|
onClick() {
|
||||||
|
@ -71,6 +109,16 @@ class Room extends ElemJS {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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
|
const active = store.activeRoom.value() === this
|
||||||
this.element.classList[active ? "add" : "remove"]("c-room--active")
|
this.element.classList[active ? "add" : "remove"]("c-room--active")
|
||||||
}
|
}
|
||||||
|
@ -84,8 +132,10 @@ class Rooms extends ElemJS {
|
||||||
this.rooms = []
|
this.rooms = []
|
||||||
|
|
||||||
store.rooms.subscribe("askAdd", this.askAdd.bind(this))
|
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.activeGroup.subscribe("changeSelf", this.render.bind(this))
|
||||||
|
store.directs.subscribe("changeItem", this.render.bind(this))
|
||||||
|
|
||||||
this.render()
|
this.render()
|
||||||
}
|
}
|
||||||
|
@ -95,18 +145,25 @@ class Rooms extends ElemJS {
|
||||||
store.rooms.addEnd(key, room)
|
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() {
|
render() {
|
||||||
this.clearChildren()
|
this.clearChildren()
|
||||||
let first = null
|
let first = null
|
||||||
// set room list
|
// set room list
|
||||||
store.rooms.forEach((id, room) => {
|
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()
|
if (!first) first = room.value()
|
||||||
this.child(room.value())
|
this.child(room.value())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// if needed, change the active room to be an item in the room list
|
// 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) {
|
if (first) {
|
||||||
store.activeRoom.set(first)
|
store.activeRoom.set(first)
|
||||||
} else {
|
} else {
|
||||||
|
@ -136,6 +193,7 @@ class Groups extends ElemJS {
|
||||||
}
|
}
|
||||||
const groups = new Groups()
|
const groups = new Groups()
|
||||||
|
|
||||||
|
|
||||||
;[
|
;[
|
||||||
{
|
{
|
||||||
id: "directs",
|
id: "directs",
|
||||||
|
@ -146,7 +204,7 @@ const groups = new Groups()
|
||||||
id: "channels",
|
id: "channels",
|
||||||
name: "Channels",
|
name: "Channels",
|
||||||
icon: "/static/channels.svg"
|
icon: "/static/channels.svg"
|
||||||
},
|
}/*,
|
||||||
{
|
{
|
||||||
id: "123",
|
id: "123",
|
||||||
name: "Fediverse Drama Museum"
|
name: "Fediverse Drama Museum"
|
||||||
|
@ -158,9 +216,10 @@ const groups = new Groups()
|
||||||
{
|
{
|
||||||
id: "789",
|
id: "789",
|
||||||
name: "Invidious"
|
name: "Invidious"
|
||||||
}
|
}*/
|
||||||
].forEach(data => store.groups.askAdd(data.id, data))
|
].forEach(data => store.groups.askAdd(data.id, data))
|
||||||
|
|
||||||
|
/*
|
||||||
;[
|
;[
|
||||||
{id: "001", name: "riley", group: store.groups.get("directs").value()},
|
{id: "001", name: "riley", group: store.groups.get("directs").value()},
|
||||||
{id: "002", name: "BadAtNames", 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: "024", name: "osu", group: store.groups.get("456").value()},
|
||||||
{id: "025", name: "covid", group: store.groups.get("456").value()}
|
{id: "025", name: "covid", group: store.groups.get("456").value()}
|
||||||
].forEach(data => store.rooms.askAdd(data.id, data))
|
].forEach(data => store.rooms.askAdd(data.id, data))
|
||||||
|
*/
|
||||||
|
|
||||||
store.activeGroup.set(store.groups.get("directs").value())
|
store.activeGroup.set(store.groups.get("directs").value())
|
||||||
|
|
|
@ -7,14 +7,14 @@ class SubscribeMapList extends Subscribable {
|
||||||
this.inner = inner
|
this.inner = inner
|
||||||
Object.assign(this.events, {
|
Object.assign(this.events, {
|
||||||
addItem: [],
|
addItem: [],
|
||||||
removeItem: [],
|
deleteItem: [],
|
||||||
editItem: [],
|
editItem: [],
|
||||||
changeItem: [],
|
changeItem: [],
|
||||||
askAdd: []
|
askAdd: []
|
||||||
})
|
})
|
||||||
Object.assign(this.eventDeps, {
|
Object.assign(this.eventDeps, {
|
||||||
addItem: ["changeItem"],
|
addItem: ["changeItem"],
|
||||||
removeItem: ["changeItem"],
|
deleteItem: ["changeItem"],
|
||||||
editItem: ["changeItem"],
|
editItem: ["changeItem"],
|
||||||
changeItem: [],
|
changeItem: [],
|
||||||
askAdd: []
|
askAdd: []
|
||||||
|
@ -59,7 +59,7 @@ class SubscribeMapList extends Subscribable {
|
||||||
const exists = this.map.get(key).exists()
|
const exists = this.map.get(key).exists()
|
||||||
s = this.map.get(key).set(value)
|
s = this.map.get(key).set(value)
|
||||||
if (exists) {
|
if (exists) {
|
||||||
this.broadcast("changeItem", key)
|
this.broadcast("editItem", key)
|
||||||
} else {
|
} else {
|
||||||
this.broadcast("addItem", key)
|
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 {SubscribeMapList} from "./SubscribeMapList.js"
|
||||||
|
import {SubscribeSet} from "./SubscribeSet.js"
|
||||||
import {SubscribeValue} from "./SubscribeValue.js"
|
import {SubscribeValue} from "./SubscribeValue.js"
|
||||||
|
|
||||||
const store = {
|
const store = {
|
||||||
groups: new SubscribeMapList(SubscribeValue),
|
groups: new SubscribeMapList(SubscribeValue),
|
||||||
rooms: new SubscribeMapList(SubscribeValue),
|
rooms: new SubscribeMapList(SubscribeValue),
|
||||||
|
directs: new SubscribeSet(),
|
||||||
activeGroup: new SubscribeValue(),
|
activeGroup: new SubscribeValue(),
|
||||||
activeRoom: 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
|
background-color: c.$darker
|
||||||
padding: $icon-padding
|
padding: $icon-padding
|
||||||
width: $list-width
|
width: $list-width
|
||||||
font-size: 20px
|
font-size: 18px
|
||||||
font-weight: 500
|
font-weight: 500
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
scrollbar-width: thin
|
scrollbar-width: thin
|
||||||
|
@ -32,11 +32,13 @@ $icon-padding: 8px
|
||||||
&__icon
|
&__icon
|
||||||
width: $icon-size
|
width: $icon-size
|
||||||
height: $icon-size
|
height: $icon-size
|
||||||
background-color: #bbb
|
|
||||||
margin-right: $icon-padding
|
margin-right: $icon-padding
|
||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
flex-shrink: 0
|
flex-shrink: 0
|
||||||
|
|
||||||
|
&--no-icon
|
||||||
|
background-color: #bbb
|
||||||
|
|
||||||
&__name
|
&__name
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
|
|
Loading…
Reference in a new issue