Compare commits

...

3 commits

Author SHA1 Message Date
ff196a64bb
Improve message sender rendering
All checks were successful
continuous-integration/drone/push Build is passing
- Refactor sender class into parts
- Sender name colour depends on mxid, like Element
  - (colours slightly modified for contrast)
- Display blank avatar if loading fails
- Remove # parts from mxc
- Don't replace member state if loaded state is older
2020-10-29 17:31:25 +13:00
a4c7f29ec9
Emacs files to gitignore 2020-10-29 17:27:38 +13:00
5bfe98bdf4
Stop scrollback at top of timeline 2020-10-29 17:26:34 +13:00
6 changed files with 141 additions and 41 deletions

4
.gitignore vendored
View file

@ -288,6 +288,10 @@ modules.xml
# End of https://www.toptal.com/developers/gitignore/api/node,vscode,webstorm,webstorm+all
# Emacs
*~
\#*#
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
/build/

View file

@ -42,7 +42,7 @@ class Chat extends ElemJS {
}
this.addSubscription("beforeChange", timeline, beforeChangeSubscription)
//Make sure after loading scrollback we don't move the scroll position
// Make sure after loading scrollback we don't move the scroll position
const beforeScrollbackLoadSubscription = () => {
const lastScrollHeight = chatMessages.scrollHeight;

View file

@ -1,7 +1,8 @@
const lsm = require("./lsm.js")
function resolveMxc(url, size, method) {
const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
let [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
id = id.replace(/#.*$/, "")
if (size && method) {
return `${lsm.get("domain")}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`
} else {

120
src/js/sender.js Normal file
View file

@ -0,0 +1,120 @@
const {ElemJS, ejs} = require("./basic.js")
const {store} = require("./store/store.js")
const {resolveMxc} = require("./functions.js")
function nameToColor(str) {
// code from element's react sdk
const colors = ["#55a7f0", "#da55ff", "#1bc47c", "#ea657e", "#fd8637", "#22cec6", "#8c8de3", "#71bf22"]
let hash = 0
let i
let chr
if (str.length === 0) {
return hash
}
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i)
hash = ((hash << 5) - hash) + chr
hash |= 0
}
hash = Math.abs(hash) % 8
return colors[hash]
}
class Avatar extends ElemJS {
constructor() {
super("div")
this.class("c-message-group__avatar")
this.mxc = undefined
this.image = null
this.update(null)
}
update(mxc) {
if (mxc === this.mxc) return
this.mxc = mxc
this.hasImage = !!mxc
if (this.hasImage) {
const size = 96
const url = resolveMxc(mxc, size, "crop")
this.image = ejs("img").class("c-message-group__icon").attribute("src", url).attribute("width", size).attribute("height", size)
this.image.on("error", this.onError.bind(this))
}
this.render()
}
onError() {
this.hasImage = false
this.render()
}
render() {
this.clearChildren()
if (this.hasImage) {
this.child(this.image)
} else {
this.child(
ejs("div").class("c-message-group__icon", "c-message-group__icon--no-icon")
)
}
}
}
/** Must update at least once to render. */
class Name extends ElemJS {
constructor() {
super("div")
this.class("c-message-group__name")
/**
* Keeps track of whether we have the proper display name or not.
* If we do, then we shoudn't override it with the mxid if the name becomes unavailable.
*/
this.hasName = false
this.name = ""
this.mxid = ""
}
update(event) {
this.mxid = event.state_key
if (event.content.displayname) {
this.hasName = true
this.name = event.content.displayname
} else if (!this.hasName) {
this.name = this.mxid
}
this.render()
}
render() {
// set text
this.text(this.name)
// set color
this.style("color", nameToColor(this.mxid))
}
}
class Sender {
constructor(roomID, mxid) {
this.sender = store.rooms.get(roomID).value().members.get(mxid)
this.name = new Name()
this.avatar = new Avatar()
this.sender.subscribe("changeSelf", this.update.bind(this))
this.update()
}
update() {
if (this.sender.exists()) {
// name
this.name.update(this.sender.value())
// avatar
this.avatar.update(this.sender.value().content.avatar_url)
}
}
}
module.exports = {
Sender
}

View file

@ -2,8 +2,8 @@ const {ElemJS, ejs} = require("./basic.js")
const {Subscribable} = require("./store/subscribable.js")
const {store} = require("./store/store.js")
const {Anchor} = require("./anchor.js")
const {Sender} = require("./sender.js")
const lsm = require("./lsm.js")
const {resolveMxc} = require("./functions.js")
let debug = false
@ -100,41 +100,6 @@ class Event extends ElemJS {
}
}
class Sender {
constructor(roomID, mxid) {
this.sender = store.rooms.get(roomID).value().members.get(mxid)
this.sender.subscribe("changeSelf", this.update.bind(this))
this.name = new ElemJS("div").class("c-message-group__name")
this.avatar = new ElemJS("div").class("c-message-group__avatar")
this.displayingGoodData = false
this.update()
}
update() {
if (this.sender.exists()) {
// name
if (this.sender.value().content.displayname) {
this.name.text(this.sender.value().content.displayname)
this.displayingGoodData = true
} else if (!this.displayingGoodData) {
this.name.text(this.sender.value().state_key)
}
// avatar
this.avatar.clearChildren()
if (this.sender.value().content.avatar_url) {
this.avatar.child(
ejs("img").class("c-message-group__icon").attribute("src", resolveMxc(this.sender.value().content.avatar_url, 96, "crop"))
)
} else {
this.avatar.child(
ejs("div").class("c-message-group__icon", "c-message-group__icon--no-icon")
)
}
}
}
}
class EventGroup extends ElemJS {
constructor(reactive, list) {
super("div")
@ -301,7 +266,11 @@ class Timeline extends Subscribable {
if (eventData.type === "m.room.member") {
// update members
if (eventData.membership !== "leave") {
this.room.members.get(eventData.state_key).set(eventData)
const member = this.room.members.get(eventData.state_key)
// only use the latest state
if (!member.exists() || eventData.origin_server_ts > member.data.origin_server_ts) {
member.set(eventData)
}
}
}
}
@ -396,7 +365,13 @@ class Timeline extends Subscribable {
this.from = root.end
// console.log(this.updateEvents, root.chunk)
if (root.state) this.updateStateEvents(root.state)
this.updateEvents(root.chunk)
if (root.chunk.length) {
// there are events to display
this.updateEvents(root.chunk)
} else {
// we reached the top of the scrollback
this.reactiveTimeline.loadMore.remove()
}
this.broadcast("afterScrollbackLoad")
}

View file

@ -23,7 +23,7 @@
border-radius: 50%
&--no-icon
background-color: #48d
background-color: #bbb
&__intro
display: flex