Compare commits

..

2 commits

Author SHA1 Message Date
229e6903fd
Display unread/notification counters on rooms
Some checks failed
continuous-integration/drone/push Build is failing
2020-11-25 01:28:04 +13:00
e90a2c7da8
Rename property to message namespace 2020-11-24 20:00:45 +13:00
8 changed files with 172 additions and 33 deletions

View file

@ -103,7 +103,7 @@ function send(body) {
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
body, body,
formatted_body: toHTML(body), formatted_body: toHTML(body),
"chat.carbon.input_body": body "chat.carbon.message.input_body": body
} }
return store.activeRoom.value().timeline.send("m.room.message", content) return store.activeRoom.value().timeline.send("m.room.message", content)
} }

View file

@ -56,12 +56,65 @@ class Group extends ElemJS {
} }
} }
class RoomNotifier extends ElemJS {
constructor() {
super("div")
this.classes = [
"notifications",
"unreads",
"none"
]
this.class("c-room__number")
this.state = {
notifications: 0,
unreads: 0
}
this.render()
}
/**
* @param {object} state
* @param {number} [state.notifications]
* @param {number} [state.unreads]
*/
update(state) {
Object.assign(this.state, state)
this.render()
}
render() {
const display = {
number: this.state.notifications || this.state.unreads,
kind: this.state.notifications ? "notifications" : "unreads"
}
// set number
if (display.number) {
this.text(display.number)
} else {
this.text("")
display.kind = "none"
}
// set class
this.classes.forEach(c => {
const name = "c-room__number--" + c
if (c === display.kind) {
this.class(name)
} else {
this.removeClass(name)
}
})
}
}
class Room extends ElemJS { class Room extends ElemJS {
constructor(id, data) { constructor(id, data) {
super("div") super("div")
this.id = id this.id = id
this.data = data this.data = data
this.number = new RoomNotifier()
this.timeline = new Timeline(this) this.timeline = new Timeline(this)
this.group = null this.group = null
this.members = new SubscribeMapList(SubscribeValue) this.members = new SubscribeMapList(SubscribeValue)
@ -75,22 +128,21 @@ class Room extends ElemJS {
} }
get order() { get order() {
if (this.group) { let string = ""
let chars = 36 if (this.number.state.notifications) {
let total = 0 string += "N"
const name = this.getName() } else if (this.number.state.unreads) {
for (let i = 0; i < name.length; i++) { string += "U"
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 { } else {
return -this.timeline.latest string += "_"
} }
if (this.group) {
string += this.name
} else {
string += (4000000000000 - this.timeline.latest) // good until 2065 :)
}
console.log(string)
return string
} }
getMemberName(mxid) { getMemberName(mxid) {
@ -174,6 +226,7 @@ class Room extends ElemJS {
this.child(ejs("div").class("c-room__icon", "c-room__icon--no-icon")) this.child(ejs("div").class("c-room__icon", "c-room__icon--no-icon"))
} }
this.child(ejs("div").class("c-room__name").text(this.getName())) this.child(ejs("div").class("c-room__name").text(this.getName()))
this.child(this.number)
// active // 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")
@ -193,6 +246,7 @@ class Rooms extends ElemJS {
store.activeGroup.subscribe("changeSelf", this.render.bind(this)) store.activeGroup.subscribe("changeSelf", this.render.bind(this))
store.directs.subscribe("changeItem", this.render.bind(this)) store.directs.subscribe("changeItem", this.render.bind(this))
store.newEvents.subscribe("changeSelf", this.sort.bind(this)) store.newEvents.subscribe("changeSelf", this.sort.bind(this))
store.notificationsChange.subscribe("changeSelf", this.sort.bind(this))
this.render() this.render()
} }

View file

@ -9,7 +9,8 @@ const store = {
directs: new SubscribeSet(), directs: new SubscribeSet(),
activeGroup: new SubscribeValue(), activeGroup: new SubscribeValue(),
activeRoom: new SubscribeValue(), activeRoom: new SubscribeValue(),
newEvents: new Subscribable() newEvents: new Subscribable(),
notificationsChange: new Subscribable()
} }
window.store = store window.store = store

View file

@ -1,40 +1,54 @@
const {Subscribable} = require("./subscribable.js") const {Subscribable} = require("./subscribable.js")
const {SubscribeValue} = require("./subscribe_value.js")
class SubscribeMap extends Subscribable { class SubscribeMap extends Subscribable {
constructor() { constructor() {
super() super()
Object.assign(this.events, { Object.assign(this.events, {
addItem: [], addItem: [],
editItem: [],
deleteItem: [],
changeItem: [], changeItem: [],
removeItem: [] askSet: []
}) })
this.map = new Map() Object.assign(this.eventDeps, {
addItem: ["changeItem"],
editItem: ["changeItem"],
deleteItem: ["changeItem"],
changeItem: [],
askSet: []
})
this.backing = new Map()
} }
has(key) { has(key) {
return this.map.has(key) && this.map.get(key).exists() return this.backing.has(key)
} }
get(key) { forEach(f) {
if (this.map.has(key)) { for (const key of this.backing.keys()) {
return this.map.get(key) f(key, this.backing.get(key))
} else {
this.map.set(key, new SubscribeValue())
} }
} }
askSet(key, value) {
this.broadcast("askSet", key, value)
}
set(key, value) { set(key, value) {
let s const existed = this.backing.has(key)
if (this.map.has(key)) { this.backing.set(key, value)
s = this.map.get(key).set(value) if (existed) {
this.broadcast("changeItem", key)
} else {
s = new SubscribeValue().set(value)
this.map.set(key, s)
this.broadcast("addItem", key) this.broadcast("addItem", key)
} else {
this.broadcast("editItem", key)
}
}
delete(key) {
if (this.backing.has(key)) {
this.backing.delete(key)
this.broadcast("deleteItem", key)
} }
return s
} }
} }

View file

@ -54,6 +54,15 @@ class SubscribeMapList extends Subscribable {
} }
sort() { sort() {
const key = this.list[0]
if (typeof this.map.get(key).value().order === "number") {
this.sortByNumber()
} else {
this.sortByString()
}
}
sortByNumber() {
this.list.sort((a, b) => { this.list.sort((a, b) => {
const orderA = this.map.get(a).value().order const orderA = this.map.get(a).value().order
const orderB = this.map.get(b).value().order const orderB = this.map.get(b).value().order
@ -62,6 +71,20 @@ class SubscribeMapList extends Subscribable {
this.broadcast("changeItem") this.broadcast("changeItem")
} }
sortByString() {
this.list.sort((a, b) => {
let r
const orderA = this.map.get(a).value().order
const orderB = this.map.get(b).value().order
if (orderA < orderB) r = -1
else if (orderA > orderB) r = 1
else r = 0
console.log("comparing", orderA, orderB, r)
return r
})
this.broadcast("changeItem")
}
_add(key, value, start) { _add(key, value, start) {
let s let s
if (this.map.has(key)) { if (this.map.has(key)) {

View file

@ -37,6 +37,7 @@ function sync() {
function manageSync(root) { function manageSync(root) {
try { try {
let newEvents = false let newEvents = false
let notificationsChange = false
// set up directs // set up directs
if (root.account_data) { if (root.account_data) {
@ -66,6 +67,14 @@ function manageSync(root) {
} }
} }
if (data.ephemeral) timeline.updateEphemeral(data.ephemeral.events) if (data.ephemeral) timeline.updateEphemeral(data.ephemeral.events)
if (data.unread_notifications) {
timeline.updateNotificationCount(data.unread_notifications.notification_count)
notificationsChange = true
}
if (data["org.matrix.msc2654.unread_count"] != undefined) {
timeline.updateUnreadCount(data["org.matrix.msc2654.unread_count"])
notificationsChange = true
}
}) })
} }
} }
@ -109,6 +118,7 @@ function manageSync(root) {
} }
if (newEvents) store.newEvents.broadcast("changeSelf") if (newEvents) store.newEvents.broadcast("changeSelf")
if (notificationsChange) store.notificationsChange.broadcast("changeSelf")
} catch (e) { } catch (e) {
console.error(root) console.error(root)
throw e throw e

View file

@ -1,6 +1,7 @@
const {ElemJS, ejs} = require("./basic.js") const {ElemJS, ejs} = require("./basic.js")
const {Subscribable} = require("./store/subscribable.js") const {Subscribable} = require("./store/subscribable.js")
const {SubscribeValue} = require("./store/subscribe_value.js") const {SubscribeValue} = require("./store/subscribe_value.js")
const {SubscribeMap} = require("./store/subscribe_map.js")
const {store} = require("./store/store.js") const {store} = require("./store/store.js")
const {Anchor} = require("./anchor.js") const {Anchor} = require("./anchor.js")
const {Sender} = require("./sender.js") const {Sender} = require("./sender.js")
@ -197,6 +198,7 @@ class Timeline extends Subscribable {
this.pending = new Set() this.pending = new Set()
this.pendingEdits = [] this.pendingEdits = []
this.typing = new SubscribeValue().set([]) this.typing = new SubscribeValue().set([])
this.userReads = new SubscribeMap()
this.from = null this.from = null
} }
@ -280,9 +282,25 @@ class Timeline extends Subscribable {
if (eventData.type === "m.typing") { if (eventData.type === "m.typing") {
this.typing.set(eventData.content.user_ids) this.typing.set(eventData.content.user_ids)
} }
if (eventData.type === "m.receipt") {
for (const eventID of Object.keys(eventData.content)) {
for (const user of Object.keys(eventData.content[eventID]["m.read"])) {
this.userReads.set(user, eventID)
}
}
// console.log("Updated read receipts:", this.userReads)
}
} }
} }
updateUnreadCount(count) {
this.room.number.update({unreads: count})
}
updateNotificationCount(count) {
this.room.number.update({notifications: count})
}
removeEvent(id) { removeEvent(id) {
if (!this.map.has(id)) throw new Error(`Tried to delete event ID ${id} which does not exist`) if (!this.map.has(id)) throw new Error(`Tried to delete event ID ${id} which does not exist`)
this.map.get(id).removeEvent() this.map.get(id).removeEvent()

View file

@ -43,3 +43,22 @@ $icon-padding: 8px
white-space: nowrap white-space: nowrap
overflow: hidden overflow: hidden
text-overflow: ellipsis text-overflow: ellipsis
flex: 1
&__number
flex-shrink: 0
line-height: 1
padding: 4px 5px
border-radius: 5px
font-size: 14px
&--none
display: none
&--unreads
background-color: #ddd
color: #111
&--notifications
background-color: #ffac4b
color: #000