Refactor and Move RedactedBodyView To Shared Components (#32772)
* refactoring and creation of shared-components for reductedBodyView * move redacted message rendering to shared MVVM view * Update snapshots + fix lint errors * Remove MatrixClientPeg and use reguler react matrix client context * Stop resyncing redacted body view models with mxEvent * Fix redacted_because test fixtures for stricter event typing * Simplify redacted body client access * Watch timestamp setting in redacted body view model * Refactor redacted and decryption failure body factories into MBodyFactory * Prettier Fix * Refactor FileBody into same pattern for consitancy
This commit is contained in:
@@ -21,6 +21,7 @@ export * from "./message-body/EventContentBody";
|
||||
export * from "./message-body/MediaBody";
|
||||
export * from "./message-body/MessageTimestampView";
|
||||
export * from "./message-body/DecryptionFailureBodyView";
|
||||
export * from "./message-body/RedactedBodyView";
|
||||
export * from "./message-body/FileBodyView";
|
||||
export * from "./message-body/PinnedMessageBadge";
|
||||
export * from "./message-body/ReactionsRowButtonTooltip";
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.content {
|
||||
white-space: pre-wrap;
|
||||
color: var(--cpd-color-text-secondary);
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--cpd-color-icon-secondary);
|
||||
flex: 0 0 14px;
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 { expect, userEvent, within } from "storybook/test";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { useMockedViewModel } from "../../viewmodel";
|
||||
import { withViewDocs } from "../../../.storybook/withViewDocs";
|
||||
import { RedactedBodyView, type RedactedBodyViewSnapshot } from "./RedactedBodyView";
|
||||
|
||||
type RedactedBodyProps = RedactedBodyViewSnapshot;
|
||||
|
||||
const RedactedBodyViewWrapperImpl = ({
|
||||
className,
|
||||
...rest
|
||||
}: RedactedBodyProps & { className?: string }): JSX.Element => {
|
||||
const vm = useMockedViewModel(rest, {});
|
||||
|
||||
return <RedactedBodyView vm={vm} className={className} />;
|
||||
};
|
||||
|
||||
const RedactedBodyViewWrapper = withViewDocs(RedactedBodyViewWrapperImpl, RedactedBodyView);
|
||||
|
||||
const meta = {
|
||||
title: "MessageBody/RedactedBodyView",
|
||||
component: RedactedBodyViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
text: "Message deleted",
|
||||
tooltip: "This message was deleted on Thu, 17 Nov 2022, 4:58:32 pm",
|
||||
className: "",
|
||||
},
|
||||
} satisfies Meta<typeof RedactedBodyViewWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.hover(canvas.getByText("Message deleted"));
|
||||
await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const SelfRedaction: Story = {
|
||||
args: {
|
||||
text: "Message deleted",
|
||||
tooltip: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const RedactedByAnotherUser: Story = {
|
||||
args: {
|
||||
text: "Message deleted by Alice",
|
||||
},
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 { render, screen } from "@test-utils";
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { MockViewModel } from "../../viewmodel";
|
||||
import { RedactedBodyView, type RedactedBodyViewModel, type RedactedBodyViewSnapshot } from "./RedactedBodyView";
|
||||
import * as stories from "./RedactedBodyView.stories";
|
||||
|
||||
const { Default, SelfRedaction } = composeStories(stories);
|
||||
|
||||
describe("RedactedBodyView", () => {
|
||||
it("renders the default redacted body", () => {
|
||||
const { container } = render(<Default />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders without a tooltip when none is provided", () => {
|
||||
const { container } = render(<SelfRedaction />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the tooltip on hover when tooltip text exists", async () => {
|
||||
const user = userEvent.setup();
|
||||
const vm = new MockViewModel<RedactedBodyViewSnapshot>({
|
||||
text: "Message deleted by Alice",
|
||||
tooltip: "This message was deleted on Thu, 17 Nov 2022, 4:58:32 pm",
|
||||
}) as RedactedBodyViewModel;
|
||||
|
||||
render(<RedactedBodyView vm={vm} />);
|
||||
|
||||
await user.hover(screen.getByText("Message deleted by Alice"));
|
||||
|
||||
expect(await screen.findByRole("tooltip")).toHaveTextContent(
|
||||
"This message was deleted on Thu, 17 Nov 2022, 4:58:32 pm",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies a custom className to the root span", () => {
|
||||
const vm = new MockViewModel<RedactedBodyViewSnapshot>({
|
||||
text: "Message deleted",
|
||||
}) as RedactedBodyViewModel;
|
||||
|
||||
const { container } = render(<RedactedBodyView vm={vm} className="custom-redacted another-class" />);
|
||||
|
||||
expect(container.firstChild).toHaveClass("custom-redacted", "another-class");
|
||||
});
|
||||
|
||||
it("forwards the provided ref to the root span", () => {
|
||||
const ref = React.createRef<HTMLSpanElement>();
|
||||
const vm = new MockViewModel<RedactedBodyViewSnapshot>({
|
||||
text: "Message deleted",
|
||||
}) as RedactedBodyViewModel;
|
||||
|
||||
render(<RedactedBodyView vm={vm} ref={ref} />);
|
||||
|
||||
expect(ref.current).toBeInstanceOf(HTMLSpanElement);
|
||||
expect(ref.current).toHaveTextContent("Message deleted");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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, type Ref } from "react";
|
||||
import classNames from "classnames";
|
||||
import { DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
import { type ViewModel } from "../../viewmodel";
|
||||
import { useViewModel } from "../../viewmodel/useViewModel";
|
||||
import styles from "./RedactedBodyView.module.css";
|
||||
|
||||
export interface RedactedBodyViewSnapshot {
|
||||
/**
|
||||
* Localized redaction message content.
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
* Optional localized tooltip shown with the redaction timestamp.
|
||||
*/
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export type RedactedBodyViewModel = ViewModel<RedactedBodyViewSnapshot>;
|
||||
|
||||
interface RedactedBodyViewProps {
|
||||
/**
|
||||
* ViewModel providing the rendered text and tooltip.
|
||||
*/
|
||||
vm: RedactedBodyViewModel;
|
||||
/**
|
||||
* Optional CSS class name applied to the root span.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Optional ref forwarded to the root span.
|
||||
*/
|
||||
ref?: Ref<HTMLSpanElement>;
|
||||
}
|
||||
|
||||
export function RedactedBodyView({ vm, className, ref }: Readonly<RedactedBodyViewProps>): JSX.Element {
|
||||
const { text, tooltip } = useViewModel(vm);
|
||||
|
||||
const content = (
|
||||
<span className={classNames(styles.content, className)} ref={ref}>
|
||||
<DeleteIcon className={styles.icon} aria-hidden="true" />
|
||||
<span>{text}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return <Tooltip description={tooltip}>{content}</Tooltip>;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`RedactedBodyView > renders the default redacted body 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Message deleted
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RedactedBodyView > renders without a tooltip when none is provided 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Message deleted
|
||||
</span>
|
||||
</span>
|
||||
</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 { RedactedBodyView, type RedactedBodyViewSnapshot, type RedactedBodyViewModel } from "./RedactedBodyView";
|
||||
Reference in New Issue
Block a user