From 67ea6bfa53d324a3d74e41374dd1c68c7207d118 Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 12 May 2026 10:17:33 +0200 Subject: [PATCH] Refactor room avatar event to MVVM (#33473) * Refactor room avatar event to MVVM * Cover room avatar event factory wrapper * Fix prettier * added screenshots --- apps/web/res/css/_components.pcss | 1 - .../css/views/messages/_RoomAvatarEvent.pcss | 13 -- .../views/messages/RoomAvatarEvent.tsx | 81 ---------- apps/web/src/events/EventTileFactory.tsx | 36 ++++- apps/web/src/i18n/strings/en_EN.json | 4 +- .../event-tile/RoomAvatarEventViewModel.ts | 109 ++++++++++++++ .../events/EventTileFactory-test.ts | 50 +++++++ .../RoomAvatarEventViewModel-test.ts | 138 ++++++++++++++++++ .../changed-auto.png | Bin 0 -> 19804 bytes .../removed-auto.png | Bin 0 -> 19596 bytes .../src/i18n/strings/en_EN.json | 4 + packages/shared-components/src/index.ts | 1 + .../RoomAvatarEventView.module.css | 31 ++++ .../RoomAvatarEventView.stories.tsx | 79 ++++++++++ .../RoomAvatarEventView.test.tsx | 27 ++++ .../RoomAvatarEventView.tsx | 109 ++++++++++++++ .../RoomAvatarEventView.test.tsx.snap | 37 +++++ .../RoomAvatarEventView/index.ts | 8 + 18 files changed, 628 insertions(+), 100 deletions(-) delete mode 100644 apps/web/res/css/views/messages/_RoomAvatarEvent.pcss delete mode 100644 apps/web/src/components/views/messages/RoomAvatarEvent.tsx create mode 100644 apps/web/src/viewmodels/room/timeline/event-tile/RoomAvatarEventViewModel.ts create mode 100644 apps/web/test/viewmodels/event-tiles/RoomAvatarEventViewModel-test.ts create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx/changed-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx/removed-auto.png create mode 100644 packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.module.css create mode 100644 packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx create mode 100644 packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.test.tsx create mode 100644 packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.tsx create mode 100644 packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/__snapshots__/RoomAvatarEventView.test.tsx.snap create mode 100644 packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/index.ts diff --git a/apps/web/res/css/_components.pcss b/apps/web/res/css/_components.pcss index 0c902fee3c..8f64e43de4 100644 --- a/apps/web/res/css/_components.pcss +++ b/apps/web/res/css/_components.pcss @@ -233,7 +233,6 @@ @import "./views/messages/_MediaBody.pcss"; @import "./views/messages/_MessageActionBar.pcss"; @import "./views/messages/_ReactionsRow.pcss"; -@import "./views/messages/_RoomAvatarEvent.pcss"; @import "./views/messages/_TextualEvent.pcss"; @import "./views/messages/_ThreadActionBar.pcss"; @import "./views/messages/_ViewSourceEvent.pcss"; diff --git a/apps/web/res/css/views/messages/_RoomAvatarEvent.pcss b/apps/web/res/css/views/messages/_RoomAvatarEvent.pcss deleted file mode 100644 index 340c5a87bd..0000000000 --- a/apps/web/res/css/views/messages/_RoomAvatarEvent.pcss +++ /dev/null @@ -1,13 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2017 Vector Creations Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_RoomAvatarEvent_avatar { - display: inline; - position: relative; - top: 3px; -} diff --git a/apps/web/src/components/views/messages/RoomAvatarEvent.tsx b/apps/web/src/components/views/messages/RoomAvatarEvent.tsx deleted file mode 100644 index c80c80c836..0000000000 --- a/apps/web/src/components/views/messages/RoomAvatarEvent.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. -Copyright 2017 Vector Creations Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { _t } from "../../../languageHandler"; -import Modal from "../../../Modal"; -import AccessibleButton from "../elements/AccessibleButton"; -import { mediaFromMxc } from "../../../customisations/Media"; -import RoomAvatar from "../avatars/RoomAvatar"; -import ImageView from "../elements/ImageView"; -interface IProps { - /* the MatrixEvent to show */ - mxEvent: MatrixEvent; -} - -export default class RoomAvatarEvent extends React.Component { - private onAvatarClick = (): void => { - const cli = MatrixClientPeg.safeGet(); - const ev = this.props.mxEvent; - const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp; - if (!httpUrl) return; - - const room = cli.getRoom(this.props.mxEvent.getRoomId()); - const text = _t("timeline|m.room.avatar|lightbox_title", { - senderDisplayName: ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(), - roomName: room ? room.name : "", - }); - - const params = { - src: httpUrl, - name: text, - }; - Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true); - }; - - public render(): React.ReactNode { - const ev = this.props.mxEvent; - const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - - if (!ev.getContent().url || ev.getContent().url.trim().length === 0) { - return
{_t("timeline|m.room.avatar|removed", { senderDisplayName })}
; - } - - const room = MatrixClientPeg.safeGet().getRoom(ev.getRoomId()); - // Provide all arguments to RoomAvatar via oobData because the avatar is historic - const oobData = { - avatarUrl: ev.getContent().url, - name: room ? room.name : "", - }; - - return ( - <> - {_t( - "timeline|m.room.avatar|changed_img", - { senderDisplayName: senderDisplayName }, - { - img: () => ( - - - - ), - }, - )} - - ); - } -} diff --git a/apps/web/src/events/EventTileFactory.tsx b/apps/web/src/events/EventTileFactory.tsx index 6ac6adf9cb..dadea9099e 100644 --- a/apps/web/src/events/EventTileFactory.tsx +++ b/apps/web/src/events/EventTileFactory.tsx @@ -22,6 +22,7 @@ import { EncryptionEventView, HiddenBodyView, MKeyVerificationRequestView, + RoomAvatarEventView, TextualEventView, useCreateAutoDisposedViewModel, } from "@element-hq/web-shared-components"; @@ -34,7 +35,7 @@ import MessageEvent from "../components/views/messages/MessageEvent"; import LegacyCallEvent from "../components/views/messages/LegacyCallEvent"; import { CallEvent } from "../components/views/messages/CallEvent"; import { RoomPredecessorTile } from "../components/views/messages/RoomPredecessorTile"; -import RoomAvatarEvent from "../components/views/messages/RoomAvatarEvent"; +import RoomAvatar from "../components/views/avatars/RoomAvatar"; import { WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/WidgetLayoutStore"; import { ALL_RULE_TYPES } from "../mjolnir/BanList"; import { MatrixClientPeg } from "../MatrixClientPeg"; @@ -49,6 +50,7 @@ import { type IBodyProps } from "../components/views/messages/IBodyProps"; import { ModuleApi } from "../modules/Api"; import { EncryptionEventViewModel } from "../viewmodels/room/timeline/event-tile/EncryptionEventViewModel"; import { MKeyVerificationRequestViewModel } from "../viewmodels/room/timeline/event-tile/MKeyVerificationRequestViewModel"; +import { RoomAvatarEventViewModel } from "../viewmodels/room/timeline/event-tile/RoomAvatarEventViewModel"; import { TextualEventViewModel } from "../viewmodels/room/timeline/event-tile/TextualEventViewModel"; import { HiddenBodyViewModel } from "../viewmodels/room/timeline/event-tile/body/HiddenBodyViewModel"; import { ElementCallEventType } from "../call-types"; @@ -124,6 +126,36 @@ function HiddenBodyWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element { } const HiddenEventFactory: Factory = (ref, props) => ; +function RoomAvatarEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element { + const cli = useMatrixClientContext() ?? MatrixClientPeg.safeGet(); + const vm = useCreateAutoDisposedViewModel(() => new RoomAvatarEventViewModel({ mxEvent, cli })); + + useEffect(() => { + vm.setEvent(mxEvent); + }, [mxEvent, vm]); + + const roomId = mxEvent.getRoomId(); + const room = roomId ? cli.getRoom(roomId) : null; + + return ( + ( + + )} + /> + ); +} +const RoomAvatarEventFactory: Factory = (ref, props) => ; + function CallStartedTileViewWrapped({ mxEvent }: IBodyProps): JSX.Element { const vm = useCreateAutoDisposedViewModel(() => new CallStartedTileViewModel({ mxEvent })); return ; @@ -155,7 +187,7 @@ const STATE_EVENT_TILE_TYPES = new Map([ [EventType.RoomCreate, RoomCreateEventFactory], [EventType.RoomMember, TextualEventFactory], [EventType.RoomName, TextualEventFactory], - [EventType.RoomAvatar, (ref, props) => ], + [EventType.RoomAvatar, RoomAvatarEventFactory], [EventType.RoomThirdPartyInvite, TextualEventFactory], [EventType.RoomHistoryVisibility, TextualEventFactory], [EventType.RoomTopic, TextualEventFactory], diff --git a/apps/web/src/i18n/strings/en_EN.json b/apps/web/src/i18n/strings/en_EN.json index 74a978f700..f402a1760f 100644 --- a/apps/web/src/i18n/strings/en_EN.json +++ b/apps/web/src/i18n/strings/en_EN.json @@ -3462,9 +3462,7 @@ "m.poll.start": "%(senderName)s has started a poll - %(pollQuestion)s", "m.room.avatar": { "changed": "%(senderDisplayName)s changed the room avatar.", - "changed_img": "%(senderDisplayName)s changed the room avatar to ", - "lightbox_title": "%(senderDisplayName)s changed the avatar for %(roomName)s", - "removed": "%(senderDisplayName)s removed the room avatar." + "lightbox_title": "%(senderDisplayName)s changed the avatar for %(roomName)s" }, "m.room.canonical_alias": { "alt_added": { diff --git a/apps/web/src/viewmodels/room/timeline/event-tile/RoomAvatarEventViewModel.ts b/apps/web/src/viewmodels/room/timeline/event-tile/RoomAvatarEventViewModel.ts new file mode 100644 index 0000000000..c2a0f11d16 --- /dev/null +++ b/apps/web/src/viewmodels/room/timeline/event-tile/RoomAvatarEventViewModel.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type MatrixClient, type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { type RoomAvatarEventContent } from "matrix-js-sdk/src/types"; +import { + BaseViewModel, + type RoomAvatarEventViewModel as RoomAvatarEventViewModelInterface, + type RoomAvatarEventViewSnapshot, +} from "@element-hq/web-shared-components"; + +import { mediaFromMxc } from "../../../../customisations/Media"; +import { _t } from "../../../../languageHandler"; +import Modal from "../../../../Modal"; +import ImageView from "../../../../components/views/elements/ImageView"; + +export interface RoomAvatarEventViewModelProps { + /** + * Caller-provided client. + */ + cli: MatrixClient; + /** + * Room avatar state event. + */ + mxEvent: MatrixEvent; +} + +/** + * ViewModel for room avatar state events. + */ +export class RoomAvatarEventViewModel + extends BaseViewModel + implements RoomAvatarEventViewModelInterface +{ + public constructor(props: RoomAvatarEventViewModelProps) { + super(props, RoomAvatarEventViewModel.computeSnapshot(props)); + } + + public setEvent(mxEvent: MatrixEvent): void { + this.props = { ...this.props, mxEvent }; + this.updateSnapshotFromProps(); + } + + public onAvatarClick = (): void => { + const avatarUrl = RoomAvatarEventViewModel.getAvatarUrl(this.props.mxEvent); + if (!avatarUrl) return; + + const httpUrl = mediaFromMxc(avatarUrl, this.props.cli).srcHttp; + if (!httpUrl) return; + + Modal.createDialog( + ImageView, + { + src: httpUrl, + name: RoomAvatarEventViewModel.computeLightboxLabel(this.props), + }, + "mx_Dialog_lightbox", + undefined, + true, + ); + }; + + private updateSnapshotFromProps(): void { + this.snapshot.merge(RoomAvatarEventViewModel.computeSnapshot(this.props)); + } + + private static computeSnapshot(props: RoomAvatarEventViewModelProps): RoomAvatarEventViewSnapshot { + const avatarUrl = RoomAvatarEventViewModel.getAvatarUrl(props.mxEvent); + const senderDisplayName = RoomAvatarEventViewModel.getSenderDisplayName(props.mxEvent); + const roomName = RoomAvatarEventViewModel.getRoomName(props); + + return { + senderDisplayName, + roomName, + avatarUrl, + lightboxLabel: RoomAvatarEventViewModel.computeLightboxLabel(props), + isRemoved: !avatarUrl, + }; + } + + private static computeLightboxLabel(props: RoomAvatarEventViewModelProps): string { + return _t("timeline|m.room.avatar|lightbox_title", { + senderDisplayName: RoomAvatarEventViewModel.getSenderDisplayName(props.mxEvent), + roomName: RoomAvatarEventViewModel.getRoomName(props), + }); + } + + private static getSenderDisplayName(mxEvent: MatrixEvent): string { + return mxEvent.sender?.name || mxEvent.getSender() || ""; + } + + private static getRoomName({ cli, mxEvent }: RoomAvatarEventViewModelProps): string { + const roomId = mxEvent.getRoomId(); + if (!roomId) return ""; + + return cli.getRoom(roomId)?.name ?? ""; + } + + private static getAvatarUrl(mxEvent: MatrixEvent): string | undefined { + const avatarUrl = mxEvent.getContent().url; + if (!avatarUrl || avatarUrl.trim().length === 0) return undefined; + + return avatarUrl; + } +} diff --git a/apps/web/test/unit-tests/events/EventTileFactory-test.ts b/apps/web/test/unit-tests/events/EventTileFactory-test.ts index f37e30667f..b8ea0af5e4 100644 --- a/apps/web/test/unit-tests/events/EventTileFactory-test.ts +++ b/apps/web/test/unit-tests/events/EventTileFactory-test.ts @@ -23,6 +23,8 @@ import { createTestClient, mkEvent } from "../../test-utils"; import { TimelineRenderingType } from "../../../src/contexts/RoomContext"; import { ModuleApi } from "../../../src/modules/Api"; import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; +import DMRoomMap from "../../../src/utils/DMRoomMap"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; const roomId = "!room:example.com"; @@ -41,6 +43,18 @@ function makeVerificationRequestEvent({ sender, to }: { sender: string; to: stri }); } +function makeRoomAvatarEvent(url = "mxc://example.com/avatar"): MatrixEvent { + return new MatrixEvent({ + type: EventType.RoomAvatar, + state_key: "", + room_id: roomId, + sender: "@alice:example.com", + content: { + url, + }, + }); +} + describe("pickFactory", () => { let client: MatrixClient; let room: Room; @@ -363,4 +377,40 @@ describe("renderTile", () => { expect(() => render(tile)).toThrow("Attempting to render verification request without a client context!"); }); + + it("renders room avatar events with the wrapped shared-components view", () => { + const room = new Room(roomId, client, client.getSafeUserId()); + room.name = "General"; + room.currentState.setStateEvents([ + new MatrixEvent({ + type: EventType.RoomCreate, + state_key: "", + room_id: room.roomId, + sender: client.getUserId()!, + content: { + creator: client.getUserId()!, + room_version: "9", + }, + }), + ]); + mocked(client.getRoom).mockReturnValue(room); + jest.spyOn(DMRoomMap, "shared").mockReturnValue({ + getUserIdForRoomId: jest.fn().mockReturnValue(null), + } as unknown as DMRoomMap); + jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client); + const roomAvatarEvent = makeRoomAvatarEvent(); + roomAvatarEvent.sender = { name: "Alice" } as MatrixEvent["sender"]; + + const tile = renderTile( + TimelineRenderingType.Room, + { mxEvent: roomAvatarEvent, showHiddenEvents: false }, + client, + ); + if (!tile) throw new Error("Expected a room avatar event tile"); + + render(React.createElement(MatrixClientContext.Provider, { value: client }, tile)); + + expect(screen.getByText("Alice changed the room avatar to")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Alice changed the avatar for General" })).toBeInTheDocument(); + }); }); diff --git a/apps/web/test/viewmodels/event-tiles/RoomAvatarEventViewModel-test.ts b/apps/web/test/viewmodels/event-tiles/RoomAvatarEventViewModel-test.ts new file mode 100644 index 0000000000..9735a4c0aa --- /dev/null +++ b/apps/web/test/viewmodels/event-tiles/RoomAvatarEventViewModel-test.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { EventType, type MatrixClient, MatrixEvent, type Room } from "matrix-js-sdk/src/matrix"; + +import Modal from "../../../src/Modal"; +import { RoomAvatarEventViewModel } from "../../../src/viewmodels/room/timeline/event-tile/RoomAvatarEventViewModel"; + +describe("RoomAvatarEventViewModel", () => { + const roomId = "!room:example.org"; + let cli: MatrixClient; + let room: Room; + let mxcUrlToHttp: jest.Mock; + + beforeEach(() => { + mxcUrlToHttp = jest.fn().mockReturnValue("https://example.org/_matrix/media/v3/download/avatar"); + room = { + name: "General", + } as unknown as Room; + cli = { + getRoom: jest.fn().mockReturnValue(room), + mxcUrlToHttp, + } as unknown as MatrixClient; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const createEvent = ( + url?: string, + sender = "@alice:example.org", + eventRoomId: string | undefined = roomId, + ): MatrixEvent => + new MatrixEvent({ + type: EventType.RoomAvatar, + room_id: eventRoomId, + state_key: "", + sender, + content: { + url, + }, + }); + + it("extracts room avatar event details", () => { + const mxEvent = createEvent("mxc://example.org/avatar"); + mxEvent.sender = { name: "Alice" } as MatrixEvent["sender"]; + + const vm = new RoomAvatarEventViewModel({ cli, mxEvent }); + + expect(vm.getSnapshot()).toEqual({ + senderDisplayName: "Alice", + roomName: "General", + avatarUrl: "mxc://example.org/avatar", + lightboxLabel: "Alice changed the avatar for General", + isRemoved: false, + }); + }); + + it("falls back to the sender ID when no sender member is available", () => { + const vm = new RoomAvatarEventViewModel({ cli, mxEvent: createEvent("mxc://example.org/avatar") }); + + expect(vm.getSnapshot().senderDisplayName).toBe("@alice:example.org"); + }); + + it("marks the event as removed when no avatar URL is present", () => { + const vm = new RoomAvatarEventViewModel({ cli, mxEvent: createEvent("") }); + + expect(vm.getSnapshot()).toMatchObject({ + avatarUrl: undefined, + isRemoved: true, + }); + }); + + it("updates the snapshot when the event changes", () => { + const vm = new RoomAvatarEventViewModel({ cli, mxEvent: createEvent("mxc://example.org/avatar") }); + const listener = jest.fn(); + vm.subscribe(listener); + + vm.setEvent(createEvent("mxc://example.org/next", "@bob:example.org")); + + expect(vm.getSnapshot()).toMatchObject({ + senderDisplayName: "@bob:example.org", + avatarUrl: "mxc://example.org/next", + isRemoved: false, + }); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it("does not emit when the event-derived snapshot is unchanged", () => { + const vm = new RoomAvatarEventViewModel({ cli, mxEvent: createEvent("mxc://example.org/avatar") }); + const listener = jest.fn(); + vm.subscribe(listener); + + vm.setEvent(createEvent("mxc://example.org/avatar")); + + expect(listener).not.toHaveBeenCalled(); + }); + + it("opens the room avatar in the lightbox", () => { + const dialogSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({ close: jest.fn() } as any); + const vm = new RoomAvatarEventViewModel({ cli, mxEvent: createEvent("mxc://example.org/avatar") }); + + vm.onAvatarClick(); + + expect(mxcUrlToHttp).toHaveBeenCalledWith( + "mxc://example.org/avatar", + undefined, + undefined, + undefined, + false, + true, + ); + expect(dialogSpy).toHaveBeenCalledWith( + expect.any(Function), + { + src: "https://example.org/_matrix/media/v3/download/avatar", + name: "@alice:example.org changed the avatar for General", + }, + "mx_Dialog_lightbox", + undefined, + true, + ); + }); + + it("does not open the lightbox when the event has no avatar URL", () => { + const dialogSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({ close: jest.fn() } as any); + const vm = new RoomAvatarEventViewModel({ cli, mxEvent: createEvent("") }); + + vm.onAvatarClick(); + + expect(dialogSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx/changed-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx/changed-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..54413bae3094852112836c2e6ae1f96addd6bb67 GIT binary patch literal 19804 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

$>hoV{rY}1m}3P)qlC6#I%HvLxhuGs!? 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 dM2mw7G^_nv3ZJZ)7zryh+ZA@pGp#(1{6D5xF", + "removed": "%(senderDisplayName)s removed the room avatar." + }, "m.room.encryption": { "disable_attempt": "Ignored attempt to disable encryption", "disabled": "Encryption not enabled", diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index fa52da1a02..8c3f7a43c6 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -43,6 +43,7 @@ export * from "./room/timeline/event-tile/call"; export * from "./room/timeline/event-tile/EventTileView/EventTileBubble"; export * from "./room/timeline/event-tile/EventTileView/MKeyVerificationRequestView"; export * from "./room/timeline/event-tile/EventTileView/PinnedMessageBadge"; +export * from "./room/timeline/event-tile/EventTileView/RoomAvatarEventView"; export * from "./room/timeline/event-tile/EventTileView/TextualEventView"; export * from "./room/timeline/event-tile/body/AudioPlayerView"; export * from "./room/timeline/event-tile/body/DecryptionFailureBodyView"; diff --git a/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.module.css b/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.module.css new file mode 100644 index 0000000000..24fcda18d0 --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.module.css @@ -0,0 +1,31 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.textualEvent { + font-size: var(--cpd-font-size-body-sm); + line-height: normal; + overflow-y: hidden; + color: var(--cpd-color-text-secondary); +} + +.textualEvent[data-event-layout="irc"] { + padding: 1px 0; + display: inline-block; + line-height: 1.125rem; +} + +.avatarButton { + display: inline; + position: relative; + top: 3px; + margin-inline-start: 0.25em; + padding: 0; + border: 0; + background: none; + color: inherit; + cursor: pointer; +} diff --git a/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx b/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx new file mode 100644 index 0000000000..121ce86b0f --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useMockedViewModel } from "../../../../../core/viewmodel"; +import { withViewDocs } from "../../../../../../.storybook/withViewDocs"; +import { + RoomAvatarEventView, + type RoomAvatarEventViewActions, + type RoomAvatarEventViewSnapshot, +} from "./RoomAvatarEventView"; + +type RoomAvatarEventViewStoryProps = RoomAvatarEventViewSnapshot & + RoomAvatarEventViewActions & { + className?: string; + }; + +const RoomAvatarEventViewWrapperImpl = ({ + onAvatarClick, + className, + ...snapshot +}: RoomAvatarEventViewStoryProps): JSX.Element => { + const vm = useMockedViewModel(snapshot, { onAvatarClick }); + + return ( + ( +

} {...eventPresentationAttributes}> + {_t("timeline|m.room.avatar|removed", { senderDisplayName: snapshot.senderDisplayName })} +
+ ); + } + + return ( + } {...eventPresentationAttributes}> + {_t( + "timeline|m.room.avatar|changed_img", + { senderDisplayName: snapshot.senderDisplayName }, + { + img: () => ( + + ), + }, + )} + + ); +} diff --git a/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/__snapshots__/RoomAvatarEventView.test.tsx.snap b/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/__snapshots__/RoomAvatarEventView.test.tsx.snap new file mode 100644 index 0000000000..82fc07b6ba --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/__snapshots__/RoomAvatarEventView.test.tsx.snap @@ -0,0 +1,37 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`RoomAvatarEventView > renders a changed room avatar event 1`] = ` +
+ + + Alice changed the room avatar to + + + +
+`; + +exports[`RoomAvatarEventView > renders a removed room avatar event 1`] = ` +
+
+ Alice removed the room avatar. +
+
+`; diff --git a/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/index.ts b/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/index.ts new file mode 100644 index 0000000000..d2d6eb2aae --- /dev/null +++ b/packages/shared-components/src/room/timeline/event-tile/EventTileView/RoomAvatarEventView/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export * from "./RoomAvatarEventView";