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:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
+31
@@ -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;
|
||||
}
|
||||
+79
@@ -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,
|
||||
},
|
||||
};
|
||||
+27
@@ -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();
|
||||
});
|
||||
});
|
||||
+109
@@ -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>
|
||||
);
|
||||
}
|
||||
+37
@@ -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>
|
||||
`;
|
||||
+8
@@ -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";
|
||||
Reference in New Issue
Block a user