diff --git a/spec.js b/spec.js
index 1babec8..d1429c2 100644
--- a/spec.js
+++ b/spec.js
@@ -39,6 +39,21 @@ module.exports = [
source: "/assets/icons/join-event.svg",
target: "/static/join-event.svg",
},
+ {
+ type: "file",
+ source: "/assets/icons/leave-event.svg",
+ target: "/static/leave-event.svg",
+ },
+ {
+ type: "file",
+ source: "/assets/icons/invite-event.svg",
+ target: "/static/invite-event.svg",
+ },
+ {
+ type: "file",
+ source: "/assets/icons/profile-event.svg",
+ target: "/static/profile-event.svg",
+ },
{
type: "sass",
source: "/sass/main.sass",
diff --git a/src/assets/icons/invite-event.svg b/src/assets/icons/invite-event.svg
new file mode 100644
index 0000000..fa44732
--- /dev/null
+++ b/src/assets/icons/invite-event.svg
@@ -0,0 +1,81 @@
+
+
+
+
diff --git a/src/assets/icons/join-event.svg b/src/assets/icons/join-event.svg
index 042e3bd..2b6b901 100644
--- a/src/assets/icons/join-event.svg
+++ b/src/assets/icons/join-event.svg
@@ -25,9 +25,9 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
- inkscape:zoom="1"
- inkscape:cx="15.649008"
- inkscape:cy="8.3751893"
+ inkscape:zoom="11.313708"
+ inkscape:cx="-4.2728481"
+ inkscape:cy="-2.1951295"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
diff --git a/src/assets/icons/leave-event.svg b/src/assets/icons/leave-event.svg
new file mode 100644
index 0000000..f836616
--- /dev/null
+++ b/src/assets/icons/leave-event.svg
@@ -0,0 +1,80 @@
+
+
+
+
diff --git a/src/assets/icons/profile-event.svg b/src/assets/icons/profile-event.svg
new file mode 100644
index 0000000..6fcdadf
--- /dev/null
+++ b/src/assets/icons/profile-event.svg
@@ -0,0 +1,83 @@
+
+
+
+
diff --git a/src/js/events/encrypted.js b/src/js/events/encrypted.js
index f73d4b9..efc3ccf 100644
--- a/src/js/events/encrypted.js
+++ b/src/js/events/encrypted.js
@@ -1,7 +1,7 @@
-const {MatrixEvent} = require("./event")
+const {GroupableEvent} = require("./event")
const {ejs} = require("../basic")
-class EncryptedMessage extends MatrixEvent {
+class EncryptedMessage extends GroupableEvent {
render() {
this.clearChildren()
this.child(
@@ -13,10 +13,6 @@ class EncryptedMessage extends MatrixEvent {
static canRender(eventData) {
return eventData.type === "m.room.encrypted"
}
-
- canGroup() {
- return true
- }
}
module.exports = [EncryptedMessage]
diff --git a/src/js/events/event.js b/src/js/events/event.js
index 189d6e2..040d94e 100644
--- a/src/js/events/event.js
+++ b/src/js/events/event.js
@@ -5,7 +5,6 @@ const {SubscribeSet} = require("../store/subscribe_set.js")
class MatrixEvent extends ElemJS {
constructor(data) {
super("div")
- this.class("c-message")
this.data = null
this.group = null
this.editedAt = null
@@ -53,4 +52,21 @@ class MatrixEvent extends ElemJS {
}
}
-module.exports = {MatrixEvent}
+class GroupableEvent extends MatrixEvent {
+ constructor(data) {
+ super(data)
+ this.class("c-message")
+ }
+
+ canGroup() {
+ return true
+ }
+}
+
+class UngroupableEvent extends MatrixEvent {
+}
+
+module.exports = {
+ GroupableEvent,
+ UngroupableEvent
+}
diff --git a/src/js/events/image.js b/src/js/events/image.js
index ec5a9d4..6d8d771 100644
--- a/src/js/events/image.js
+++ b/src/js/events/image.js
@@ -1,8 +1,8 @@
const {ejs, ElemJS} = require("../basic")
const {resolveMxc} = require("../functions")
-const {MatrixEvent} = require("./event")
+const {GroupableEvent} = require("./event")
-class Image extends MatrixEvent {
+class Image extends GroupableEvent {
render() {
this.clearChildren()
this.class("c-message--media")
diff --git a/src/js/events/membership.js b/src/js/events/membership.js
index 2e85bae..0f10b04 100644
--- a/src/js/events/membership.js
+++ b/src/js/events/membership.js
@@ -1,15 +1,35 @@
-const {MatrixEvent} = require("./event")
+const {UngroupableEvent} = require("./event")
const {ejs} = require("../basic")
+const {extractDisplayName, resolveMxc, extractLocalpart} = require("../functions")
+
+class MembershipEvent extends UngroupableEvent {
+ constructor(data) {
+ super(data)
+ this.class("c-message-event")
+ this.senderName = extractDisplayName(data)
+ if (data.content.avatar_url) {
+ this.smallAvatar = ejs("img")
+ .attribute("width", "32")
+ .attribute("height", "32")
+ .attribute("src", resolveMxc(data.content.avatar_url, 32, "crop"))
+ .class("c-message-event__avatar")
+ } else {
+ this.smallAvatar = ""
+ }
+ this.render()
+ }
-class MembershipEvent extends MatrixEvent {
static canRender(eventData) {
return eventData.type === "m.room.member"
}
- renderText(text) {
+ renderInner(iconURL, elements) {
this.clearChildren()
this.child(
- ejs("i").text(text)
+ ejs("div").class("c-message-event__inner").child(
+ iconURL ? ejs("img").class("c-message-event__icon").attribute("width", "20").attribute("height", "20").attribute("src", iconURL) : "",
+ ...elements
+ )
)
super.render()
}
@@ -22,7 +42,30 @@ class JoinedEvent extends MembershipEvent {
}
render() {
- this.renderText("joined the room")
+ const changes = []
+ const prev = this.data.unsigned.prev_content
+ if (prev && prev.membership === "join") {
+ if (prev.avatar_url !== this.data.content.avatar_url) {
+ changes.push("changed their avatar")
+ }
+ if (prev.displayname !== this.data.content.displayname) {
+ changes.push(`changed their display name (was ${this.data.unsigned.prev_content.displayname})`)
+ }
+ }
+ let message
+ let iconURL
+ if (changes.length) {
+ message = " " + changes.join(", ")
+ iconURL = "static/profile-event.svg"
+ } else {
+ message = " joined the room"
+ iconURL = "static/join-event.svg"
+ }
+ this.renderInner(iconURL, [
+ this.smallAvatar,
+ this.senderName,
+ message
+ ])
}
}
@@ -32,7 +75,10 @@ class InvitedEvent extends MembershipEvent {
}
render() {
- this.renderText(`invited ${this.data.content.displayname}`)
+ this.renderInner("static/invite-event.svg", [
+ this.smallAvatar,
+ `${extractLocalpart(this.data.sender)} invited ${this.data.state_key}` // full mxid for clarity
+ ])
}
}
@@ -42,13 +88,21 @@ class LeaveEvent extends MembershipEvent {
}
render() {
- this.renderText("left the room")
+ this.renderInner("static/leave-event.svg", [
+ this.smallAvatar,
+ this.senderName,
+ " left the room"
+ ])
}
}
class UnknownMembership extends MembershipEvent {
render() {
- this.renderText("unknown membership event")
+ this.renderInner("", [
+ this.smallAvatar,
+ this.senderName,
+ ejs("i").text(" unknown membership event")
+ ])
}
}
diff --git a/src/js/events/message.js b/src/js/events/message.js
index 9528abe..50cd3b5 100644
--- a/src/js/events/message.js
+++ b/src/js/events/message.js
@@ -2,7 +2,7 @@ const {ejs, ElemJS} = require("../basic")
const {HighlightedCode} = require("./components")
const DOMPurify = require("dompurify")
const {resolveMxc} = require("../functions")
-const {MatrixEvent} = require("./event")
+const {GroupableEvent} = require("./event")
const purifier = DOMPurify()
@@ -88,7 +88,7 @@ function postProcessElements(element) {
}
-class HTMLMessage extends MatrixEvent {
+class HTMLMessage extends GroupableEvent {
render() {
this.clearChildren()
@@ -111,10 +111,6 @@ class HTMLMessage extends MatrixEvent {
&& content.formatted_body
)
}
-
- canGroup() {
- return true
- }
}
function autoLinkText(text) {
@@ -139,7 +135,7 @@ function autoLinkText(text) {
return fragment
}
-class TextMessage extends MatrixEvent {
+class TextMessage extends GroupableEvent {
render() {
this.clearChildren()
this.class("c-message--plain")
@@ -151,10 +147,6 @@ class TextMessage extends MatrixEvent {
static canRender(event) {
return event.type === "m.room.message"
}
-
- canGroup() {
- return true
- }
}
module.exports = [HTMLMessage, TextMessage]
diff --git a/src/js/events/unknown.js b/src/js/events/unknown.js
index c3f10dd..5133aa8 100644
--- a/src/js/events/unknown.js
+++ b/src/js/events/unknown.js
@@ -1,7 +1,7 @@
-const {MatrixEvent} = require("./event")
+const {GroupableEvent} = require("./event")
const {ejs} = require("../basic")
-class UnknownEvent extends MatrixEvent {
+class UnknownEvent extends GroupableEvent {
static canRender() {
return true
}
diff --git a/src/js/timeline.js b/src/js/timeline.js
index cd3ef05..3ef646b 100644
--- a/src/js/timeline.js
+++ b/src/js/timeline.js
@@ -65,6 +65,11 @@ class EventGroup extends ElemJS {
)
}
+ canGroup() {
+ if (this.list.length) return this.list[0].canGroup()
+ else return true
+ }
+
addEvent(event) {
const index = eventSearch(this.list, event).i
event.setGroup(this)
@@ -142,16 +147,21 @@ class ReactiveTimeline extends ElemJS {
const success = indices.some(i => {
if (!this.list[i]) {
// if (printed++ < 100) console.log("tryadd success, created group")
- const group = new EventGroup(this, [event])
if (i === -1) {
// here, -1 means at the start, before the first group
i = 0 // jank but it does the trick
}
- this.list.splice(i, 0, group)
- this.childAt(i, group)
- event.setGroup(group)
+ if (event.canGroup()) {
+ const group = new EventGroup(this, [event])
+ this.list.splice(i, 0, group)
+ this.childAt(i, group)
+ event.setGroup(group)
+ } else {
+ this.list.splice(i, 0, event)
+ this.childAt(i, event)
+ }
return true
- } else if (this.list[i] && this.list[i].data.sender === event.data.sender) {
+ } else if (event.canGroup() && this.list[i] && this.list[i].canGroup() && this.list[i].data.sender === event.data.sender) {
// if (printed++ < 100) console.log("tryadd success, using existing group")
this.list[i].addEvent(event)
return true
diff --git a/src/sass/components/messages.sass b/src/sass/components/messages.sass
index 170ae71..4b61003 100644
--- a/src/sass/components/messages.sass
+++ b/src/sass/components/messages.sass
@@ -116,17 +116,30 @@
margin-bottom: 0px
.c-message-event
- padding-top: 10px
+ // closer spacing than normal messages
+ padding-top: 2px
padding-left: 6px
+ margin-bottom: -4px
+ line-height: 1.2
&__inner
- display: flex
- align-items: center
+ text-indent: -36px
+ margin-left: 36px
+
+ img
+ // let me know if there's a smarter way to line this shit up
+ position: relative
+ top: -5px
+ transform: translateY(50%)
&__icon
margin-right: 8px
- position: relative
- top: 1px
+
+ &__avatar
+ width: 16px
+ height: 16px
+ border-radius: 50%
+ margin: 0px 6px
.c-message-notice
padding: 12px