
div.sender-avatar {
width: 1.5rem;
@@ -104,6 +103,10 @@ div.timeline-event {
> div.event-time-only {
display: flex;
}
+
+ + div.timeline-event.hidden-event {
+ margin-top: 0;
+ }
}
}
@@ -111,11 +114,6 @@ div.hidden-event > div.sender-avatar, blockquote.reply-body > div.reply-sender >
margin-top: 0;
display: flex;
align-items: center;
-
- > img {
- width: 1rem;
- height: 1rem;
- }
}
div.date-separator {
diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx
index c3bdaaa..62d8dc2 100644
--- a/web/src/ui/timeline/TimelineEvent.tsx
+++ b/web/src/ui/timeline/TimelineEvent.tsx
@@ -14,13 +14,15 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
.
import React, { use, useCallback } from "react"
-import { getAvatarURL } from "../../api/media.ts"
-import { RoomStateStore } from "../../api/statestore.ts"
-import { MemDBEvent, MemberEventContent } from "../../api/types"
+import { getAvatarURL } from "@/api/media.ts"
+import { RoomStateStore } from "@/api/statestore.ts"
+import { MemDBEvent, MemberEventContent } from "@/api/types"
import { ClientContext } from "../ClientContext.ts"
+import { LightboxContext } from "../Lightbox.tsx"
import ReplyBody from "./ReplyBody.tsx"
import EncryptedBody from "./content/EncryptedBody.tsx"
import HiddenEvent from "./content/HiddenEvent.tsx"
+import MemberBody from "./content/MemberBody.tsx"
import { MediaMessageBody, TextMessageBody, UnknownMessageBody } from "./content/MessageBody.tsx"
import RedactedBody from "./content/RedactedBody.tsx"
import { EventContentProps } from "./content/props.ts"
@@ -71,6 +73,8 @@ function getBodyType(evt: MemDBEvent): React.FunctionComponent
{
}
}
+function isSmallEvent(bodyType: React.FunctionComponent): boolean {
+ return bodyType === HiddenEvent || bodyType === MemberBody
+}
+
const TimelineEvent = ({ room, evt, prevEvt, setReplyTo }: TimelineEventProps) => {
const wrappedSetReplyTo = useCallback(() => setReplyTo(evt), [evt, setReplyTo])
const client = use(ClientContext)!
@@ -109,12 +117,15 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyTo }: TimelineEventProps) =
const eventTS = new Date(evt.timestamp)
const editEventTS = evt.last_edit ? new Date(evt.last_edit.timestamp) : null
const wrapperClassNames = ["timeline-event"]
- if (BodyType === HiddenEvent) {
+ let smallAvatar = false
+ if (isSmallEvent(BodyType)) {
wrapperClassNames.push("hidden-event")
+ smallAvatar = true
} else if (prevEvt?.sender === evt.sender &&
prevEvt.timestamp + 15 * 60 * 1000 > evt.timestamp &&
- getBodyType(prevEvt) !== HiddenEvent) {
+ !isSmallEvent(getBodyType(prevEvt))) {
wrapperClassNames.push("same-sender")
+ smallAvatar = true
}
const fullTime = fullTimeFormatter.format(eventTS)
const shortTime = formatShortTime(eventTS)
@@ -123,9 +134,10 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyTo }: TimelineEventProps) =
const mainEvent =
@@ -142,7 +154,7 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyTo }: TimelineEventProps) =
{typeof replyTo === "string" && BodyType !== HiddenEvent
? : null}
-
+
{evt.reactions ? : null}
{evt.sender === client.userID && evt.transaction_id ?
: null}
diff --git a/web/src/ui/timeline/content/MemberBody.tsx b/web/src/ui/timeline/content/MemberBody.tsx
new file mode 100644
index 0000000..fb06294
--- /dev/null
+++ b/web/src/ui/timeline/content/MemberBody.tsx
@@ -0,0 +1,87 @@
+// gomuks - A Matrix client written in Go.
+// Copyright (C) 2024 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import React, { use } from "react"
+import { getAvatarURL } from "@/api/media.ts"
+import { MemberEventContent, UserID } from "@/api/types"
+import { LightboxContext } from "../../Lightbox.tsx"
+import { EventContentProps } from "./props.ts"
+
+function useChangeDescription(
+ sender: UserID, target: UserID, content: MemberEventContent, prevContent?: MemberEventContent,
+): string | React.ReactElement {
+ const targetAvatar =
+ if (content.membership === prevContent?.membership) {
+ if (content.displayname !== prevContent.displayname) {
+ if (content.avatar_url !== prevContent.avatar_url) {
+ return "changed their displayname and avatar"
+ } else if (!content.displayname) {
+ return "removed their displayname"
+ } else if (!prevContent.displayname) {
+ return `set their displayname to ${content.displayname}`
+ }
+ return `changed their displayname from ${prevContent.displayname} to ${content.displayname}`
+ } else if (content.avatar_url !== prevContent.avatar_url) {
+ if (!content.avatar_url) {
+ return "removed their avatar"
+ } else if (!prevContent.avatar_url) {
+ return <>set their avatar to {targetAvatar}>
+ }
+ return <>
+ changed their avatar from
to {targetAvatar}
+ >
+ }
+ return "made no change"
+ } else if (content.membership === "join") {
+ return "joined the room"
+ } else if (content.membership === "invite") {
+ return <>invited {content.avatar_url && targetAvatar} {content.displayname ?? target}>
+ } else if (content.membership === "ban") {
+ return <>banned {content.avatar_url && targetAvatar} {content.displayname ?? target}>
+ } else if (content.membership === "knock") {
+ return "knocked on the room"
+ } else if (content.membership === "leave") {
+ if (sender === target) {
+ return "left the room"
+ }
+ return <>kicked {content.displayname}>
+ }
+ return "made an unknown membership change"
+}
+
+const MemberBody = ({ event, sender }: EventContentProps) => {
+ const content = event.content as MemberEventContent
+ const prevContent = event.unsigned.prev_content as MemberEventContent | undefined
+ return
+ {sender?.content.displayname ?? event.sender} {
+ useChangeDescription(event.sender, event.state_key as UserID, content, prevContent)
+ }
+
+}
+
+export default MemberBody
diff --git a/web/src/ui/timeline/content/props.ts b/web/src/ui/timeline/content/props.ts
index 91c394f..2321702 100644
--- a/web/src/ui/timeline/content/props.ts
+++ b/web/src/ui/timeline/content/props.ts
@@ -19,4 +19,5 @@ import { MemDBEvent } from "../../../api/types"
export interface EventContentProps {
room: RoomStateStore
event: MemDBEvent
+ sender?: MemDBEvent
}