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 0000000000..54413bae30 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx/changed-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx/removed-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx/removed-auto.png new file mode 100644 index 0000000000..3951d681b8 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room/timeline/event-tile/EventTileView/RoomAvatarEventView/RoomAvatarEventView.stories.tsx/removed-auto.png differ diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json index bf2775e354..70b1193704 100644 --- a/packages/shared-components/src/i18n/strings/en_EN.json +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -222,6 +222,10 @@ "m.file": { "error_invalid": "Invalid file" }, + "m.room.avatar": { + "changed_img": "%(senderDisplayName)s changed the room avatar to", + "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 ( + ( +