Refactor view source event to MVVM (#33428)
* Refactor view source event to MVVM * remove unused variable since movement * Update view source event screenshots * Update packages/shared-components/src/room/timeline/event-tile/body/ViewSourceEventView/ViewSourceEventView.tsx Co-authored-by: Florian Duros <florian.duros@ormaz.fr> * Use view model disposables for source event decryption * Consolidate source event view model updates * Fix prettier * Fix view source expanded class name * Remove void from source event decryption --------- Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -41,6 +41,9 @@
|
||||
"preferences": "Preferences",
|
||||
"state_encryption_enabled": "Experimental state encryption enabled"
|
||||
},
|
||||
"devtools": {
|
||||
"toggle_event": "toggle event"
|
||||
},
|
||||
"keyboard": {
|
||||
"shift": "Shift"
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ export * from "./room/timeline/event-tile/body/MjolnirBodyView";
|
||||
export * from "./room/timeline/event-tile/body/MVideoBodyView";
|
||||
export * from "./room/timeline/event-tile/body/TextualBodyView";
|
||||
export * from "./room/timeline/event-tile/body/UnknownBodyView";
|
||||
export * from "./room/timeline/event-tile/body/ViewSourceEventView";
|
||||
export * from "./room/timeline/event-tile/EventTileView/TileErrorView";
|
||||
export * from "./core/pill-input/Pill";
|
||||
export * from "./core/pill-input/PillInput";
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 {
|
||||
display: flex;
|
||||
color: var(--cpd-color-text-secondary);
|
||||
font-size: var(--cpd-font-size-body-xs);
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.source {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
pre.source {
|
||||
line-height: 1.2;
|
||||
margin: 3.5px 0;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
--ViewSourceEvent_toggle-size: 16px;
|
||||
|
||||
appearance: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: var(--cpd-color-icon-accent-primary);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
visibility: hidden;
|
||||
width: var(--ViewSourceEvent_toggle-size);
|
||||
min-width: var(--ViewSourceEvent_toggle-size);
|
||||
height: var(--ViewSourceEvent_toggle-size);
|
||||
}
|
||||
|
||||
.content:hover .toggle,
|
||||
.toggle:focus-visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.toggle:focus-visible {
|
||||
outline: 2px solid var(--cpd-color-border-focused);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--cpd-space-1x);
|
||||
}
|
||||
|
||||
.toggle svg {
|
||||
width: var(--ViewSourceEvent_toggle-size);
|
||||
height: var(--ViewSourceEvent_toggle-size);
|
||||
}
|
||||
|
||||
.expanded .toggle {
|
||||
align-self: flex-end;
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 {
|
||||
ViewSourceEventView,
|
||||
type ViewSourceEventViewActions,
|
||||
type ViewSourceEventViewSnapshot,
|
||||
} from "./ViewSourceEventView";
|
||||
|
||||
type ViewSourceEventViewProps = ViewSourceEventViewSnapshot &
|
||||
ViewSourceEventViewActions & {
|
||||
className?: string;
|
||||
expandedClassName?: string;
|
||||
};
|
||||
|
||||
const source = JSON.stringify(
|
||||
{
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.org",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Hello",
|
||||
},
|
||||
},
|
||||
null,
|
||||
4,
|
||||
);
|
||||
|
||||
const ViewSourceEventViewWrapperImpl = ({
|
||||
onToggle,
|
||||
className,
|
||||
expandedClassName,
|
||||
...snapshot
|
||||
}: ViewSourceEventViewProps): JSX.Element => {
|
||||
const vm = useMockedViewModel(snapshot, { onToggle });
|
||||
|
||||
return <ViewSourceEventView vm={vm} className={className} expandedClassName={expandedClassName} />;
|
||||
};
|
||||
|
||||
const ViewSourceEventViewWrapper = withViewDocs(ViewSourceEventViewWrapperImpl, ViewSourceEventView);
|
||||
|
||||
const meta = {
|
||||
title: "Timeline/Timeline Event/ViewSourceEventView",
|
||||
component: ViewSourceEventViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
expanded: false,
|
||||
preview: '{ "type": m.room.message }',
|
||||
source,
|
||||
onToggle: fn(),
|
||||
className: "",
|
||||
expandedClassName: "",
|
||||
},
|
||||
} satisfies Meta<typeof ViewSourceEventViewWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Expanded: Story = {
|
||||
args: {
|
||||
expanded: true,
|
||||
},
|
||||
};
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 { fireEvent, render, screen } from "@test-utils";
|
||||
import React from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { MockViewModel } from "../../../../../core/viewmodel";
|
||||
import {
|
||||
ViewSourceEventView,
|
||||
type ViewSourceEventViewActions,
|
||||
type ViewSourceEventViewModel,
|
||||
type ViewSourceEventViewSnapshot,
|
||||
} from "./ViewSourceEventView";
|
||||
import * as stories from "./ViewSourceEventView.stories";
|
||||
|
||||
const { Default, Expanded } = composeStories(stories);
|
||||
|
||||
class TestViewSourceEventViewModel
|
||||
extends MockViewModel<ViewSourceEventViewSnapshot>
|
||||
implements ViewSourceEventViewActions
|
||||
{
|
||||
public constructor(
|
||||
snapshot: ViewSourceEventViewSnapshot,
|
||||
public onToggle: ViewSourceEventViewActions["onToggle"],
|
||||
) {
|
||||
super(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
const createVm = (
|
||||
snapshot: Partial<ViewSourceEventViewSnapshot> = {},
|
||||
onToggle: ViewSourceEventViewActions["onToggle"] = vi.fn(),
|
||||
): ViewSourceEventViewModel =>
|
||||
new TestViewSourceEventViewModel(
|
||||
{
|
||||
expanded: false,
|
||||
preview: '{ "type": m.room.message }',
|
||||
source: '{\n "type": "m.room.message"\n}',
|
||||
...snapshot,
|
||||
},
|
||||
onToggle,
|
||||
) as ViewSourceEventViewModel;
|
||||
|
||||
describe("ViewSourceEventView", () => {
|
||||
const getToggleButton = (container: HTMLElement): HTMLButtonElement => {
|
||||
const button = container.querySelector<HTMLButtonElement>('button[aria-label="toggle event"]');
|
||||
|
||||
if (!button) {
|
||||
throw new Error("Expected view source toggle button to be rendered");
|
||||
}
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
it("renders the default story", () => {
|
||||
const { container } = render(<Default />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(screen.getByText('{ "type": m.room.message }')).toBeInTheDocument();
|
||||
expect(getToggleButton(container)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the expanded story", () => {
|
||||
const { container } = render(<Expanded />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(screen.getByText(/"sender": "@alice:example\.org"/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("invokes the toggle action", () => {
|
||||
const onToggle = vi.fn();
|
||||
const vm = createVm({}, onToggle);
|
||||
|
||||
const { container } = render(<ViewSourceEventView vm={vm} />);
|
||||
|
||||
fireEvent.click(getToggleButton(container));
|
||||
|
||||
expect(onToggle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("applies custom class names to the root element", () => {
|
||||
const vm = createVm({ expanded: true });
|
||||
|
||||
const { container } = render(
|
||||
<ViewSourceEventView vm={vm} className="custom-source" expandedClassName="custom-expanded" />,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toHaveClass("custom-source", "custom-expanded");
|
||||
});
|
||||
|
||||
it("forwards the provided ref to the root span", () => {
|
||||
const ref = React.createRef<HTMLSpanElement>();
|
||||
const vm = createVm();
|
||||
|
||||
render(<ViewSourceEventView vm={vm} ref={ref} />);
|
||||
|
||||
expect(ref.current).toBeInstanceOf(HTMLSpanElement);
|
||||
});
|
||||
});
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 MouseEventHandler, type Ref } from "react";
|
||||
import classNames from "classnames";
|
||||
import { CollapseIcon, ExpandIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
||||
import { useI18n } from "../../../../../core/i18n/i18nContext";
|
||||
import styles from "./ViewSourceEventView.module.css";
|
||||
|
||||
export interface ViewSourceEventViewSnapshot {
|
||||
/**
|
||||
* Whether the full event source is visible.
|
||||
*/
|
||||
expanded: boolean;
|
||||
/**
|
||||
* Collapsed one-line event summary.
|
||||
*/
|
||||
preview: string;
|
||||
/**
|
||||
* Pretty-printed event source.
|
||||
*/
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface ViewSourceEventViewActions {
|
||||
/**
|
||||
* Invoked when the user expands or collapses the event source.
|
||||
*/
|
||||
onToggle: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export type ViewSourceEventViewModel = ViewModel<ViewSourceEventViewSnapshot, ViewSourceEventViewActions>;
|
||||
|
||||
interface ViewSourceEventViewProps {
|
||||
/**
|
||||
* ViewModel providing the event source snapshot and actions.
|
||||
*/
|
||||
vm: ViewSourceEventViewModel;
|
||||
/**
|
||||
* Optional CSS class names applied to the root element.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Optional CSS class name applied to the root element while expanded.
|
||||
*/
|
||||
expandedClassName?: string;
|
||||
/**
|
||||
* Optional ref forwarded to the root element.
|
||||
*/
|
||||
ref?: Ref<HTMLSpanElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a collapsible event source preview for hidden timeline events.
|
||||
*/
|
||||
export function ViewSourceEventView({
|
||||
vm,
|
||||
className,
|
||||
expandedClassName,
|
||||
ref,
|
||||
}: Readonly<ViewSourceEventViewProps>): JSX.Element {
|
||||
const { expanded, preview, source } = useViewModel(vm);
|
||||
const _t = useI18n().translate;
|
||||
const toggleLabel = _t("devtools|toggle_event");
|
||||
|
||||
const classes = classNames(
|
||||
styles.content,
|
||||
className,
|
||||
{
|
||||
[styles.expanded]: expanded,
|
||||
},
|
||||
expanded && expandedClassName,
|
||||
);
|
||||
|
||||
return (
|
||||
<span className={classes} ref={ref}>
|
||||
{expanded ? (
|
||||
<pre className={styles.source}>{source}</pre>
|
||||
) : (
|
||||
<code className={styles.source}>{preview}</code>
|
||||
)}
|
||||
<Tooltip description={toggleLabel} placement="top">
|
||||
<button type="button" aria-label={toggleLabel} className={styles.toggle} onClick={vm.onToggle}>
|
||||
{expanded ? <CollapseIcon /> : <ExpandIcon />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ViewSourceEventView > renders the default story 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="ViewSourceEventView-module_content"
|
||||
>
|
||||
<code
|
||||
class="ViewSourceEventView-module_source"
|
||||
>
|
||||
{ "type": m.room.message }
|
||||
</code>
|
||||
<button
|
||||
aria-label="toggle event"
|
||||
class="ViewSourceEventView-module_toggle"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21 3.997a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 20 3h-8a1 1 0 1 0 0 2h5.586L5 17.586V12a1 1 0 1 0-2 0v8.003a1 1 0 0 0 .29.702l.005.004c.18.18.43.291.705.291h8a1 1 0 1 0 0-2H6.414L19 6.414V12a1 1 0 1 0 2 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ViewSourceEventView > renders the expanded story 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="ViewSourceEventView-module_content ViewSourceEventView-module_expanded"
|
||||
>
|
||||
<pre
|
||||
class="ViewSourceEventView-module_source"
|
||||
>
|
||||
{
|
||||
"type": "m.room.message",
|
||||
"sender": "@alice:example.org",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "Hello"
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
<button
|
||||
aria-label="toggle event"
|
||||
class="ViewSourceEventView-module_toggle"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 11.034a1 1 0 0 0 .29.702l.005.005c.18.18.43.29.705.29h8a1 1 0 0 0 0-2h-5.586L22 3.445a1 1 0 0 0-1.414-1.414L14 8.617V3.031a1 1 0 1 0-2 0zm0 1.963a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 11 12H3a1 1 0 1 0 0 2h5.586L2 20.586A1 1 0 1 0 3.414 22L10 15.414V21a1 1 0 0 0 2 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 "./ViewSourceEventView";
|
||||
Reference in New Issue
Block a user