Support Element's groups and list sorting

This commit is contained in:
Cadence Ember 2020-10-19 20:20:08 +13:00
parent 4b0b5c4b39
commit 1f9462b89d
Signed by untrusted user: cadence
GPG key ID: BC1C2C61CF521B17
18 changed files with 272 additions and 145 deletions

View file

@ -49,7 +49,7 @@ function validate(filename, body, type) {
let match
if (match = message.message.match(/Property “([\w-]+)” doesn't exist.$/)) {
// allow these properties specifically
if (["scrollbar-width", "scrollbar-color"].includes(match[1])) {
if (["scrollbar-width", "scrollbar-color", "overflow-anchor"].includes(match[1])) {
continue
}
}

View file

@ -2,11 +2,11 @@
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="static/main.css?static=da499eb39c">
<link rel="stylesheet" type="text/css" href="static/main.css?static=9aad8398d2">
<script type="module" src="static/groups.js?static=2cc7f0daf8"></script>
<script type="module" src="static/chat-input.js?static=e8b21037fa"></script>
<script type="module" src="static/room-picker.js?static=1d38378110"></script>
<script type="module" src="static/sync/sync.js?static=9e31c8a727"></script>
<script type="module" src="static/room-picker.js?static=7bc94b38d3"></script>
<script type="module" src="static/sync/sync.js?static=56e374b23d"></script>
<script type="module" src="static/chat.js?static=8a04bee48d"></script>
<title>Carbon</title>
</head>

View file

@ -7,7 +7,7 @@ class Anchor extends ElemJS {
}
scroll() {
console.log("anchor scrolled")
// console.log("anchor scrolled")
this.element.scrollIntoView({block: "start"})
}
}

View file

@ -57,7 +57,7 @@ class EventGroup extends ElemJS {
this.messages = ejs("div").class("c-message-group__messages").child(
ejs("div").class("c-message-group__intro").child(
ejs("div").class("c-message-group__name").text(this.data.sender),
ejs("div").class("c-message-group__date").text("at 4:20 pm")
ejs("div").class("c-message-group__date").text(this.data.origin_server_ts)
),
...this.list
)
@ -123,11 +123,13 @@ class Timeline extends Subscribable {
this.list = []
this.map = new Map()
this.reactiveTimeline = new ReactiveTimeline([])
this.latest = 0
}
updateEvents(events) {
this.broadcast("beforeChange")
for (const eventData of events) {
this.latest = Math.max(this.latest, eventData.origin_server_ts)
if (this.map.has(eventData.event_id)) {
this.map.get(eventData.event_id).update(eventData)
} else {

View file

@ -71,7 +71,7 @@ body {
position: relative;
width: 80px;
flex-shrink: 0;
font-size: 24px;
font-size: 22px;
font-weight: 500;
}
.c-groups__display {

View file

@ -36,6 +36,7 @@ class Group extends ElemJS {
super("div")
this.data = data
this.order = this.data.order
this.class("c-group")
this.child(
@ -68,6 +69,7 @@ class Room extends ElemJS {
this.id = id
this.data = data
this.timeline = new Timeline()
this.group = null
this.class("c-room")
@ -77,6 +79,25 @@ class Room extends ElemJS {
this.render()
}
get order() {
if (this.group) {
let chars = 36
let total = 0
const name = this.getName()
for (let i = 0; i < name.length; i++) {
const c = name[i]
let d = 0
if (c >= "A" && c <= "Z") d = c.charCodeAt(0) - 65 + 10
else if (c >= "a" && c <= "z") d = c.charCodeAt(0) - 97 + 10
else if (c >= "0" && c <= "9") d = +c
total += d * chars ** (-i)
}
return total
} else {
return -this.timeline.latest
}
}
getName() {
let name = this.data.state.events.find(e => e.type === "m.room.name")
if (name) {
@ -102,9 +123,17 @@ class Room extends ElemJS {
return store.directs.has(this.id)
}
setGroup(id) {
this.group = id
}
getGroup() {
if (this.group) {
return store.groups.get(this.group).value()
} else {
return this.isDirect() ? store.groups.get("directs").value() : store.groups.get("channels").value()
}
}
onClick() {
store.activeRoom.set(this)
@ -138,10 +167,16 @@ class Rooms extends ElemJS {
// store.rooms.subscribe("changeItem", this.render.bind(this))
store.activeGroup.subscribe("changeSelf", this.render.bind(this))
store.directs.subscribe("changeItem", this.render.bind(this))
store.newEvents.subscribe("changeSelf", this.sort.bind(this))
this.render()
}
sort() {
store.rooms.sort()
this.render()
}
askAdd(event, {key, data}) {
const room = new Room(key, data)
store.rooms.addEnd(key, room)
@ -181,74 +216,20 @@ class Groups extends ElemJS {
super(q("#c-groups-list"))
store.groups.subscribe("askAdd", this.askAdd.bind(this))
store.groups.subscribe("addItem", this.addItem.bind(this))
store.groups.subscribe("changeItem", this.render.bind(this))
}
askAdd(event, {key, data}) {
const group = new Group(key, data)
store.groups.addEnd(key, group)
store.groups.sort()
}
addItem(event, key) {
this.child(store.groups.get(key).value())
render() {
this.clearChildren()
store.groups.forEach((key, item) => {
this.child(item.value())
})
}
}
const groups = new Groups()
;[
{
id: "directs",
name: "Directs",
icon: "/static/directs.svg"
},
{
id: "channels",
name: "Channels",
icon: "/static/channels.svg"
}/*,
{
id: "123",
name: "Fediverse Drama Museum"
},
{
id: "456",
name: "Epicord"
},
{
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()},
{id: "003", name: "lynxano", group: store.groups.get("directs").value()},
{id: "004", name: "quarky", group: store.groups.get("directs").value()},
{id: "005", name: "lepton", group: store.groups.get("directs").value()},
{id: "006", name: "ash", group: store.groups.get("directs").value()},
{id: "007", name: "mewmew", group: store.groups.get("directs").value()},
{id: "008", name: "Toniob", group: store.groups.get("directs").value()},
{id: "009", name: "cockandball", group: store.groups.get("directs").value()},
{id: "010", name: "Carbon brainstorming", group: store.groups.get("channels").value()},
{id: "011", name: "Bibliogram", group: store.groups.get("channels").value()},
{id: "012", name: "Monsters Inc Debate Hall", group: store.groups.get("channels").value()},
{id: "013", name: "DRB clan", group: store.groups.get("channels").value()},
{id: "014", name: "mettaton simp zone", group: store.groups.get("channels").value()},
{id: "015", name: "witches", group: store.groups.get("123").value()},
{id: "016", name: "snouts", group: store.groups.get("123").value()},
{id: "017", name: "monads", group: store.groups.get("123").value()},
{id: "018", name: "radical", group: store.groups.get("123").value()},
{id: "019", name: "blobcat", group: store.groups.get("123").value()},
{id: "020", name: "main", group: store.groups.get("456").value()},
{id: "021", name: "gaming", group: store.groups.get("456").value()},
{id: "022", name: "inhalers", group: store.groups.get("456").value()},
{id: "023", name: "minecraft", 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()}
].forEach(data => store.rooms.askAdd(data.id, data))
*/
store.activeGroup.set(store.groups.get("directs").value())

View file

@ -53,6 +53,16 @@ class SubscribeMapList extends Subscribable {
this._add(key, value, false)
}
sort() {
console.log("sorting")
this.list.sort((a, b) => {
const orderA = this.map.get(a).value().order
const orderB = this.map.get(b).value().order
return orderA - orderB
})
this.broadcast("changeItem")
}
_add(key, value, start) {
let s
if (this.map.has(key)) {

View file

@ -1,3 +1,4 @@
import {Subscribable} from "./Subscribable.js"
import {SubscribeMapList} from "./SubscribeMapList.js"
import {SubscribeSet} from "./SubscribeSet.js"
import {SubscribeValue} from "./SubscribeValue.js"
@ -7,7 +8,8 @@ const store = {
rooms: new SubscribeMapList(SubscribeValue),
directs: new SubscribeSet(),
activeGroup: new SubscribeValue(),
activeRoom: new SubscribeValue()
activeRoom: new SubscribeValue(),
newEvents: new Subscribable()
}
window.store = store

View file

@ -3,6 +3,16 @@ import * as lsm from "../lsm.js"
let lastBatch = null
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}`
}
}
function sync() {
const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/sync`)
url.searchParams.append("access_token", lsm.get("access_token"))
@ -35,6 +45,8 @@ function sync() {
function manageSync(root) {
try {
let newEvents = false
// set up directs
const directs = root.account_data.events.find(e => e.type === "m.direct")
if (directs) {
@ -49,8 +61,45 @@ function manageSync(root) {
store.rooms.askAdd(id, room)
}
const timeline = store.rooms.get(id).value().timeline
if (room.timeline.events.length) newEvents = true
timeline.updateEvents(room.timeline.events)
})
// set up groups
Promise.all(
Object.keys(root.groups.join).map(id => {
if (!store.groups.has(id)) {
return Promise.all(["profile", "rooms"].map(path => {
const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/groups/${id}/${path}`)
url.searchParams.append("access_token", lsm.get("access_token"))
return fetch(url.toString()).then(res => res.json())
})).then(([profile, rooms]) => {
rooms = rooms.chunk
let order = 999
let orderEvent = root.account_data.events.find(e => e.type === "im.vector.web.tag_ordering")
if (orderEvent) {
if (orderEvent.content.tags.includes(id)) {
order = orderEvent.content.tags.indexOf(id)
}
}
const data = {
name: profile.name,
icon: resolveMxc(profile.avatar_url, 96, "crop"),
order
}
store.groups.askAdd(id, data)
rooms.forEach(groupRoom => {
if (store.rooms.has(groupRoom.room_id)) {
store.rooms.get(groupRoom.room_id).value().setGroup(id)
}
})
})
}
})
).then(() => {
store.rooms.sort()
})
if (newEvents) store.newEvents.broadcast("changeSelf")
} catch (e) {
console.error(root)
throw e
@ -61,4 +110,21 @@ function syncLoop() {
return sync().then(manageSync).then(syncLoop)
}
;[
{
id: "directs",
name: "Directs",
icon: "/static/directs.svg",
order: -2
},
{
id: "channels",
name: "Channels",
icon: "/static/channels.svg",
order: -1
}
].forEach(data => store.groups.askAdd(data.id, data))
store.activeGroup.set(store.groups.get("directs").value())
syncLoop()

View file

@ -69,6 +69,11 @@ module.exports = [
source: "/js/Timeline.js",
target: "/static/Timeline.js"
},
{
type: "js",
source: "/js/Anchor.js",
target: "/static/Anchor.js"
},
{
type: "js",
source: "/js/chat.js",

View file

@ -7,7 +7,7 @@ class Anchor extends ElemJS {
}
scroll() {
console.log("anchor scrolled")
// console.log("anchor scrolled")
this.element.scrollIntoView({block: "start"})
}
}

View file

@ -57,7 +57,7 @@ class EventGroup extends ElemJS {
this.messages = ejs("div").class("c-message-group__messages").child(
ejs("div").class("c-message-group__intro").child(
ejs("div").class("c-message-group__name").text(this.data.sender),
ejs("div").class("c-message-group__date").text("at 4:20 pm")
ejs("div").class("c-message-group__date").text(this.data.origin_server_ts)
),
...this.list
)
@ -123,11 +123,13 @@ class Timeline extends Subscribable {
this.list = []
this.map = new Map()
this.reactiveTimeline = new ReactiveTimeline([])
this.latest = 0
}
updateEvents(events) {
this.broadcast("beforeChange")
for (const eventData of events) {
this.latest = Math.max(this.latest, eventData.origin_server_ts)
if (this.map.has(eventData.event_id)) {
this.map.get(eventData.event_id).update(eventData)
} else {

View file

@ -1,29 +1,30 @@
import {q} from "./basic.js"
import {store} from "./store/store.js"
import * as lsm from "./lsm.js"
import {chat} from "./chat.js"
let sentIndex = 0
const chat = q("#c-chat-textarea")
const input = q("#c-chat-textarea")
chat.addEventListener("keydown", event => {
input.addEventListener("keydown", event => {
if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey) {
event.preventDefault()
const body = chat.value
send(chat.value)
chat.value = ""
const body = input.value
send(input.value)
input.value = ""
fixHeight()
}
})
chat.addEventListener("input", () => {
input.addEventListener("input", () => {
fixHeight()
})
function fixHeight() {
chat.style.height = "0px"
console.log(chat.clientHeight, chat.scrollHeight)
chat.style.height = (chat.scrollHeight + 1) + "px"
input.style.height = "0px"
console.log(input.clientHeight, input.scrollHeight)
input.style.height = (input.scrollHeight + 1) + "px"
}
function getTxnId() {

View file

@ -36,6 +36,7 @@ class Group extends ElemJS {
super("div")
this.data = data
this.order = this.data.order
this.class("c-group")
this.child(
@ -68,6 +69,7 @@ class Room extends ElemJS {
this.id = id
this.data = data
this.timeline = new Timeline()
this.group = null
this.class("c-room")
@ -77,6 +79,25 @@ class Room extends ElemJS {
this.render()
}
get order() {
if (this.group) {
let chars = 36
let total = 0
const name = this.getName()
for (let i = 0; i < name.length; i++) {
const c = name[i]
let d = 0
if (c >= "A" && c <= "Z") d = c.charCodeAt(0) - 65 + 10
else if (c >= "a" && c <= "z") d = c.charCodeAt(0) - 97 + 10
else if (c >= "0" && c <= "9") d = +c
total += d * chars ** (-i)
}
return total
} else {
return -this.timeline.latest
}
}
getName() {
let name = this.data.state.events.find(e => e.type === "m.room.name")
if (name) {
@ -102,9 +123,17 @@ class Room extends ElemJS {
return store.directs.has(this.id)
}
setGroup(id) {
this.group = id
}
getGroup() {
if (this.group) {
return store.groups.get(this.group).value()
} else {
return this.isDirect() ? store.groups.get("directs").value() : store.groups.get("channels").value()
}
}
onClick() {
store.activeRoom.set(this)
@ -138,10 +167,16 @@ class Rooms extends ElemJS {
// store.rooms.subscribe("changeItem", this.render.bind(this))
store.activeGroup.subscribe("changeSelf", this.render.bind(this))
store.directs.subscribe("changeItem", this.render.bind(this))
store.newEvents.subscribe("changeSelf", this.sort.bind(this))
this.render()
}
sort() {
store.rooms.sort()
this.render()
}
askAdd(event, {key, data}) {
const room = new Room(key, data)
store.rooms.addEnd(key, room)
@ -181,74 +216,20 @@ class Groups extends ElemJS {
super(q("#c-groups-list"))
store.groups.subscribe("askAdd", this.askAdd.bind(this))
store.groups.subscribe("addItem", this.addItem.bind(this))
store.groups.subscribe("changeItem", this.render.bind(this))
}
askAdd(event, {key, data}) {
const group = new Group(key, data)
store.groups.addEnd(key, group)
store.groups.sort()
}
addItem(event, key) {
this.child(store.groups.get(key).value())
render() {
this.clearChildren()
store.groups.forEach((key, item) => {
this.child(item.value())
})
}
}
const groups = new Groups()
;[
{
id: "directs",
name: "Directs",
icon: "/static/directs.svg"
},
{
id: "channels",
name: "Channels",
icon: "/static/channels.svg"
}/*,
{
id: "123",
name: "Fediverse Drama Museum"
},
{
id: "456",
name: "Epicord"
},
{
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()},
{id: "003", name: "lynxano", group: store.groups.get("directs").value()},
{id: "004", name: "quarky", group: store.groups.get("directs").value()},
{id: "005", name: "lepton", group: store.groups.get("directs").value()},
{id: "006", name: "ash", group: store.groups.get("directs").value()},
{id: "007", name: "mewmew", group: store.groups.get("directs").value()},
{id: "008", name: "Toniob", group: store.groups.get("directs").value()},
{id: "009", name: "cockandball", group: store.groups.get("directs").value()},
{id: "010", name: "Carbon brainstorming", group: store.groups.get("channels").value()},
{id: "011", name: "Bibliogram", group: store.groups.get("channels").value()},
{id: "012", name: "Monsters Inc Debate Hall", group: store.groups.get("channels").value()},
{id: "013", name: "DRB clan", group: store.groups.get("channels").value()},
{id: "014", name: "mettaton simp zone", group: store.groups.get("channels").value()},
{id: "015", name: "witches", group: store.groups.get("123").value()},
{id: "016", name: "snouts", group: store.groups.get("123").value()},
{id: "017", name: "monads", group: store.groups.get("123").value()},
{id: "018", name: "radical", group: store.groups.get("123").value()},
{id: "019", name: "blobcat", group: store.groups.get("123").value()},
{id: "020", name: "main", group: store.groups.get("456").value()},
{id: "021", name: "gaming", group: store.groups.get("456").value()},
{id: "022", name: "inhalers", group: store.groups.get("456").value()},
{id: "023", name: "minecraft", 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()}
].forEach(data => store.rooms.askAdd(data.id, data))
*/
store.activeGroup.set(store.groups.get("directs").value())

View file

@ -53,6 +53,15 @@ class SubscribeMapList extends Subscribable {
this._add(key, value, false)
}
sort() {
this.list.sort((a, b) => {
const orderA = this.map.get(a).value().order
const orderB = this.map.get(b).value().order
return orderA - orderB
})
this.broadcast("changeItem")
}
_add(key, value, start) {
let s
if (this.map.has(key)) {

View file

@ -1,3 +1,4 @@
import {Subscribable} from "./Subscribable.js"
import {SubscribeMapList} from "./SubscribeMapList.js"
import {SubscribeSet} from "./SubscribeSet.js"
import {SubscribeValue} from "./SubscribeValue.js"
@ -7,7 +8,8 @@ const store = {
rooms: new SubscribeMapList(SubscribeValue),
directs: new SubscribeSet(),
activeGroup: new SubscribeValue(),
activeRoom: new SubscribeValue()
activeRoom: new SubscribeValue(),
newEvents: new Subscribable()
}
window.store = store

View file

@ -3,6 +3,16 @@ import * as lsm from "../lsm.js"
let lastBatch = null
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}`
}
}
function sync() {
const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/sync`)
url.searchParams.append("access_token", lsm.get("access_token"))
@ -35,6 +45,8 @@ function sync() {
function manageSync(root) {
try {
let newEvents = false
// set up directs
const directs = root.account_data.events.find(e => e.type === "m.direct")
if (directs) {
@ -49,8 +61,45 @@ function manageSync(root) {
store.rooms.askAdd(id, room)
}
const timeline = store.rooms.get(id).value().timeline
if (room.timeline.events.length) newEvents = true
timeline.updateEvents(room.timeline.events)
})
// set up groups
Promise.all(
Object.keys(root.groups.join).map(id => {
if (!store.groups.has(id)) {
return Promise.all(["profile", "rooms"].map(path => {
const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/groups/${id}/${path}`)
url.searchParams.append("access_token", lsm.get("access_token"))
return fetch(url.toString()).then(res => res.json())
})).then(([profile, rooms]) => {
rooms = rooms.chunk
let order = 999
let orderEvent = root.account_data.events.find(e => e.type === "im.vector.web.tag_ordering")
if (orderEvent) {
if (orderEvent.content.tags.includes(id)) {
order = orderEvent.content.tags.indexOf(id)
}
}
const data = {
name: profile.name,
icon: resolveMxc(profile.avatar_url, 96, "crop"),
order
}
store.groups.askAdd(id, data)
rooms.forEach(groupRoom => {
if (store.rooms.has(groupRoom.room_id)) {
store.rooms.get(groupRoom.room_id).value().setGroup(id)
}
})
})
}
})
).then(() => {
store.rooms.sort()
})
if (newEvents) store.newEvents.broadcast("changeSelf")
} catch (e) {
console.error(root)
throw e
@ -61,4 +110,21 @@ function syncLoop() {
return sync().then(manageSync).then(syncLoop)
}
;[
{
id: "directs",
name: "Directs",
icon: "/static/directs.svg",
order: -2
},
{
id: "channels",
name: "Channels",
icon: "/static/channels.svg",
order: -1
}
].forEach(data => store.groups.askAdd(data.id, data))
store.activeGroup.set(store.groups.get("directs").value())
syncLoop()

View file

@ -10,7 +10,7 @@ $out-width: $base-width + rooms.$list-width
position: relative
width: $base-width
flex-shrink: 0
font-size: 24px
font-size: 22px
font-weight: 500
&__display