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 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + 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 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + 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 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + 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