diff --git a/src/js/chat.js b/src/js/chat.js index 050ff8b..6695c3a 100644 --- a/src/js/chat.js +++ b/src/js/chat.js @@ -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; diff --git a/src/js/functions.js b/src/js/functions.js index bf94921..da60793 100644 --- a/src/js/functions.js +++ b/src/js/functions.js @@ -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 { diff --git a/src/js/sender.js b/src/js/sender.js new file mode 100644 index 0000000..55943dc --- /dev/null +++ b/src/js/sender.js @@ -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 +} diff --git a/src/js/timeline.js b/src/js/timeline.js index 59e3120..c03c635 100644 --- a/src/js/timeline.js +++ b/src/js/timeline.js @@ -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) + } } } } diff --git a/src/sass/components/messages.sass b/src/sass/components/messages.sass index 8a33dc2..2a7665a 100644 --- a/src/sass/components/messages.sass +++ b/src/sass/components/messages.sass @@ -23,7 +23,7 @@ border-radius: 50% &--no-icon - background-color: #48d + background-color: #bbb &__intro display: flex