Rich message rendering #24
6 changed files with 141 additions and 40 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -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/
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
120
src/js/sender.js
Normal 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
|
||||
}
|
|
@ -2,6 +2,7 @@ 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")
|
||||
const {renderEvent} = require("./events/renderEvent")
|
||||
|
@ -39,41 +40,6 @@ function eventSearch(list, event, min = 0, max = NO_MAX) {
|
|||
}
|
||||
|
||||
|
||||
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")
|
||||
|
@ -240,7 +206,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -335,7 +305,13 @@ class Timeline extends Subscribable {
|
|||
this.from = root.end
|
||||
// console.log(this.updateEvents, root.chunk)
|
||||
if (root.state) this.updateStateEvents(root.state)
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
border-radius: 50%
|
||||
|
||||
&--no-icon
|
||||
background-color: #48d
|
||||
background-color: #bbb
|
||||
|
||||
&__intro
|
||||
display: flex
|
||||
|
|
Loading…
Reference in a new issue