Refactor room avatar event to MVVM (#33473)

* Refactor room avatar event to MVVM

* Cover room avatar event factory wrapper

* Fix prettier

* added screenshots
This commit is contained in:
Zack
2026-05-12 08:17:33 +00:00
committed by GitHub
parent 2933b51fea
commit 67ea6bfa53
18 changed files with 628 additions and 100 deletions
@@ -222,6 +222,10 @@
"m.file": {
"error_invalid": "Invalid file"
},
"m.room.avatar": {
"changed_img": "%(senderDisplayName)s changed the room avatar to<img/>",
"removed": "%(senderDisplayName)s removed the room avatar."
},
"m.room.encryption": {
"disable_attempt": "Ignored attempt to disable encryption",
"disabled": "Encryption not enabled",
+1
View File
@@ -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";
@@ -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;
}
@@ -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 (
<RoomAvatarEventView
vm={vm}
className={className}
renderAvatar={() => (
<span
aria-hidden="true"
style={{
display: "inline-block",
width: 14,
height: 14,
borderRadius: "50%",
background: "#0dbd8b",
}}
/>
)}
/>
);
};
const RoomAvatarEventViewWrapper = withViewDocs(RoomAvatarEventViewWrapperImpl, RoomAvatarEventView);
const meta = {
title: "Timeline/Timeline Event/RoomAvatarEventView",
component: RoomAvatarEventViewWrapper,
tags: ["autodocs"],
args: {
senderDisplayName: "Alice",
roomName: "General",
avatarUrl: "mxc://example.org/avatar",
lightboxLabel: "Alice changed the avatar for General",
isRemoved: false,
onAvatarClick: fn(),
className: "",
},
} satisfies Meta<typeof RoomAvatarEventViewWrapper>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Changed: Story = {};
export const Removed: Story = {
args: {
avatarUrl: undefined,
isRemoved: true,
},
};
@@ -0,0 +1,27 @@
/*
* 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 { composeStories } from "@storybook/react-vite";
import { render } from "@test-utils";
import React from "react";
import { describe, expect, it } from "vitest";
import * as stories from "./RoomAvatarEventView.stories.tsx";
const { Changed, Removed } = composeStories(stories);
describe("RoomAvatarEventView", () => {
it("renders a changed room avatar event", () => {
const { container } = render(<Changed />);
expect(container).toMatchSnapshot();
});
it("renders a removed room avatar event", () => {
const { container } = render(<Removed />);
expect(container).toMatchSnapshot();
});
});
@@ -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 classNames from "classnames";
import React, { type JSX, type ReactNode, type Ref } from "react";
import { useI18n } from "../../../../../core/i18n/i18nContext";
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
import { useEventPresentationAttributes } from "../../../EventPresentation/EventPresentationContext";
import styles from "./RoomAvatarEventView.module.css";
export interface RoomAvatarEventViewSnapshot {
/**
* Display name for the event sender.
*/
senderDisplayName: string;
/**
* Room name at the time the avatar event is rendered.
*/
roomName: string;
/**
* MXC URL from the avatar event content.
*/
avatarUrl?: string;
/**
* Accessible label for opening the avatar preview.
*/
lightboxLabel: string;
/**
* Whether this event removed the room avatar.
*/
isRemoved: boolean;
}
export interface RoomAvatarEventViewActions {
/**
* Invoked when the user opens the avatar image.
*/
onAvatarClick(): void;
}
export type RoomAvatarEventViewModel = ViewModel<RoomAvatarEventViewSnapshot, RoomAvatarEventViewActions>;
export interface RoomAvatarEventViewProps {
/**
* ViewModel providing room avatar event state and actions.
*/
vm: RoomAvatarEventViewModel;
/**
* Renders the avatar thumbnail using the host application's avatar implementation.
*/
renderAvatar(snapshot: RoomAvatarEventViewSnapshot): ReactNode;
/**
* Optional CSS class names applied to the root element.
*/
className?: string;
/**
* Optional ref forwarded to the root element.
*/
ref?: Ref<HTMLElement>;
}
/**
* Renders a room avatar state event.
*/
export function RoomAvatarEventView({
vm,
renderAvatar,
className,
ref,
}: Readonly<RoomAvatarEventViewProps>): JSX.Element {
const snapshot = useViewModel(vm);
const _t = useI18n().translate;
const eventPresentationAttributes = useEventPresentationAttributes();
const classes = classNames(styles.textualEvent, className);
if (snapshot.isRemoved) {
return (
<div className={classes} ref={ref as Ref<HTMLDivElement>} {...eventPresentationAttributes}>
{_t("timeline|m.room.avatar|removed", { senderDisplayName: snapshot.senderDisplayName })}
</div>
);
}
return (
<span className={classes} ref={ref as Ref<HTMLSpanElement>} {...eventPresentationAttributes}>
{_t(
"timeline|m.room.avatar|changed_img",
{ senderDisplayName: snapshot.senderDisplayName },
{
img: () => (
<button
type="button"
className={styles.avatarButton}
onClick={vm.onAvatarClick}
aria-label={snapshot.lightboxLabel}
>
{renderAvatar(snapshot)}
</button>
),
},
)}
</span>
);
}
@@ -0,0 +1,37 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`RoomAvatarEventView > renders a changed room avatar event 1`] = `
<div>
<span
class="RoomAvatarEventView-module_textualEvent"
data-event-density="default"
data-event-layout="group"
>
<span>
Alice changed the room avatar to
<button
aria-label="Alice changed the avatar for General"
class="RoomAvatarEventView-module_avatarButton"
type="button"
>
<span
aria-hidden="true"
style="display: inline-block; width: 14px; height: 14px; border-radius: 50%; background: rgb(13, 189, 139);"
/>
</button>
</span>
</span>
</div>
`;
exports[`RoomAvatarEventView > renders a removed room avatar event 1`] = `
<div>
<div
class="RoomAvatarEventView-module_textualEvent"
data-event-density="default"
data-event-layout="group"
>
Alice removed the room avatar.
</div>
</div>
`;
@@ -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";