diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 0e29f29..97f1893 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -73,6 +73,18 @@ export interface MemberEventContent extends UserProfile { reason?: string } +export interface RoomAvatarEventContent { + url?: ContentURI +} + +export interface RoomNameEventContent { + name?: string +} + +export interface RoomTopicEventContent { + topic?: string +} + export interface ACLEventContent { allow?: string[] allow_ip_literals?: boolean diff --git a/web/src/ui/timeline/content/RoomAvatarBody.tsx b/web/src/ui/timeline/content/RoomAvatarBody.tsx new file mode 100644 index 0000000..d996bba --- /dev/null +++ b/web/src/ui/timeline/content/RoomAvatarBody.tsx @@ -0,0 +1,52 @@ +// 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 { JSX, use } from "react" +import { getRoomAvatarURL } from "@/api/media.ts" +import { ContentURI, RoomAvatarEventContent } from "@/api/types" +import { ensureString } from "@/util/validation.ts" +import { LightboxContext } from "../../modal/Lightbox.tsx" +import EventContentProps from "./props.ts" + +const RoomAvatarBody = ({ event, sender, room }: EventContentProps) => { + const content = event.content as RoomAvatarEventContent + const prevContent = event.unsigned.prev_content as RoomAvatarEventContent | undefined + let changeDescription: JSX.Element | string + const oldURL = ensureString(prevContent?.url) + const newURL = ensureString(content.url) + const openLightbox = use(LightboxContext)! + const makeAvatar = (url: ContentURI) => + if (oldURL === newURL) { + changeDescription = "sent a room avatar event with no change" + } else if (oldURL && newURL) { + changeDescription = <>changed the room avatar from {makeAvatar(oldURL)} to {makeAvatar(newURL)} + } else if (!oldURL) { + changeDescription = <>set the room avatar to {makeAvatar(newURL)} + } else { + changeDescription = "removed the room avatar" + } + return
+ {sender?.content.displayname ?? event.sender} {changeDescription} +
+} + +export default RoomAvatarBody diff --git a/web/src/ui/timeline/content/RoomNameBody.tsx b/web/src/ui/timeline/content/RoomNameBody.tsx new file mode 100644 index 0000000..721d676 --- /dev/null +++ b/web/src/ui/timeline/content/RoomNameBody.tsx @@ -0,0 +1,45 @@ +// 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 { JSX } from "react" +import { RoomNameEventContent } from "@/api/types" +import { ensureString } from "@/util/validation.ts" +import EventContentProps from "./props.ts" + +function bidiIsolate(str: string): JSX.Element { + return {str} +} + +const RoomNameBody = ({ event, sender }: EventContentProps) => { + const content = event.content as RoomNameEventContent + const prevContent = event.unsigned.prev_content as RoomNameEventContent | undefined + let changeDescription: JSX.Element | string + const oldName = ensureString(prevContent?.name) + const newName = ensureString(content.name) + if (oldName === newName) { + changeDescription = "sent a room name event with no change" + } else if (oldName && newName) { + changeDescription = <>changed the room name from {bidiIsolate(oldName)} to {bidiIsolate(newName)} + } else if (!oldName) { + changeDescription = <>set the room name to {bidiIsolate(newName)} + } else { + changeDescription = "removed the room name" + } + return
+ {sender?.content.displayname ?? event.sender} {changeDescription} +
+} + +export default RoomNameBody diff --git a/web/src/ui/timeline/content/index.css b/web/src/ui/timeline/content/index.css index 15c5a83..ac4506e 100644 --- a/web/src/ui/timeline/content/index.css +++ b/web/src/ui/timeline/content/index.css @@ -44,6 +44,12 @@ div.member-body { } } +div.room-avatar-body { + display: flex; + align-items: center; + gap: .25rem; +} + div.message-text { &.plaintext-body { white-space: pre-wrap; diff --git a/web/src/ui/timeline/content/index.ts b/web/src/ui/timeline/content/index.ts index 05927ea..b2f666a 100644 --- a/web/src/ui/timeline/content/index.ts +++ b/web/src/ui/timeline/content/index.ts @@ -8,6 +8,8 @@ import MemberBody from "./MemberBody.tsx" import PinnedEventsBody from "./PinnedEventsBody.tsx" import PowerLevelBody from "./PowerLevelBody.tsx" import RedactedBody from "./RedactedBody.tsx" +import RoomAvatarBody from "./RoomAvatarBody.tsx" +import RoomNameBody from "./RoomNameBody.tsx" import TextMessageBody from "./TextMessageBody.tsx" import UnknownMessageBody from "./UnknownMessageBody.tsx" import EventContentProps from "./props.ts" @@ -22,6 +24,8 @@ export { default as MemberBody } from "./MemberBody.tsx" export { default as PinnedEventsBody } from "./PinnedEventsBody.tsx" export { default as PowerLevelBody } from "./PowerLevelBody.tsx" export { default as RedactedBody } from "./RedactedBody.tsx" +export { default as RoomAvatarBody } from "./RoomAvatarBody.tsx" +export { default as RoomNameBody } from "./RoomNameBody.tsx" export { default as TextMessageBody } from "./TextMessageBody.tsx" export { default as UnknownMessageBody } from "./UnknownMessageBody.tsx" export type { default as EventContentProps } from "./props.ts" @@ -68,6 +72,10 @@ export function getBodyType(evt: MemDBEvent, forReply = false): React.FunctionCo return EncryptedBody case "m.room.member": return MemberBody + case "m.room.name": + return RoomNameBody + case "m.room.avatar": + return RoomAvatarBody case "m.room.server_acl": return ACLBody case "m.room.pinned_events": @@ -82,6 +90,8 @@ export function isSmallEvent(bodyType: React.FunctionComponent