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>
@@ -718,7 +718,7 @@ test.describe("Timeline", () => {
|
|||||||
await viewSourceEventExpanded.hover();
|
await viewSourceEventExpanded.hover();
|
||||||
const toggleEventButton = viewSourceEventExpanded.getByRole("button", { name: "toggle event" });
|
const toggleEventButton = viewSourceEventExpanded.getByRole("button", { name: "toggle event" });
|
||||||
// Check size and position of toggle on expanded view source event
|
// Check size and position of toggle on expanded view source event
|
||||||
// See: _ViewSourceEvent.pcss
|
// See: ViewSourceEventView.module.css
|
||||||
await expect(toggleEventButton).toHaveCSS("height", "16px"); // --ViewSourceEvent_toggle-size
|
await expect(toggleEventButton).toHaveCSS("height", "16px"); // --ViewSourceEvent_toggle-size
|
||||||
await expect(toggleEventButton).toHaveCSS("align-self", "flex-end");
|
await expect(toggleEventButton).toHaveCSS("align-self", "flex-end");
|
||||||
// Click again to collapse the source
|
// Click again to collapse the source
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -234,7 +234,6 @@
|
|||||||
@import "./views/messages/_ReactionsRow.pcss";
|
@import "./views/messages/_ReactionsRow.pcss";
|
||||||
@import "./views/messages/_TextualEvent.pcss";
|
@import "./views/messages/_TextualEvent.pcss";
|
||||||
@import "./views/messages/_ThreadActionBar.pcss";
|
@import "./views/messages/_ThreadActionBar.pcss";
|
||||||
@import "./views/messages/_ViewSourceEvent.pcss";
|
|
||||||
@import "./views/messages/_common_CryptoEvent.pcss";
|
@import "./views/messages/_common_CryptoEvent.pcss";
|
||||||
@import "./views/polls/pollHistory/_PollHistory.pcss";
|
@import "./views/polls/pollHistory/_PollHistory.pcss";
|
||||||
@import "./views/polls/pollHistory/_PollHistoryList.pcss";
|
@import "./views/polls/pollHistory/_PollHistoryList.pcss";
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_EventTile_content.mx_ViewSourceEvent {
|
|
||||||
display: flex;
|
|
||||||
opacity: 0.6;
|
|
||||||
font-size: $font-12px;
|
|
||||||
width: 100%;
|
|
||||||
overflow-x: auto; /* Cancel overflow setting of .mx_EventTile_content */
|
|
||||||
line-height: normal; /* Align with avatar and E2E icon */
|
|
||||||
|
|
||||||
pre,
|
|
||||||
code {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
line-height: 1.2;
|
|
||||||
margin: 3.5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ViewSourceEvent_toggle {
|
|
||||||
--ViewSourceEvent_toggle-size: 16px;
|
|
||||||
|
|
||||||
visibility: hidden;
|
|
||||||
/* icon */
|
|
||||||
width: var(--ViewSourceEvent_toggle-size);
|
|
||||||
min-width: var(--ViewSourceEvent_toggle-size);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
color: $accent;
|
|
||||||
width: var(--ViewSourceEvent_toggle-size);
|
|
||||||
height: var(--ViewSourceEvent_toggle-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover & {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle {
|
|
||||||
align-self: flex-end;
|
|
||||||
height: var(--ViewSourceEvent_toggle-size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
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 from "react";
|
|
||||||
import { type MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { CollapseIcon, ExpandIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
mxEvent: MatrixEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
expanded: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ViewSourceEvent extends React.PureComponent<IProps, IState> {
|
|
||||||
public constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
expanded: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
|
||||||
const { mxEvent } = this.props;
|
|
||||||
|
|
||||||
const client = MatrixClientPeg.safeGet();
|
|
||||||
client.decryptEventIfNeeded(mxEvent);
|
|
||||||
|
|
||||||
if (mxEvent.isBeingDecrypted()) {
|
|
||||||
mxEvent.once(MatrixEventEvent.Decrypted, () => this.forceUpdate());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onToggle = (ev: ButtonEvent): void => {
|
|
||||||
ev.preventDefault();
|
|
||||||
const { expanded } = this.state;
|
|
||||||
this.setState({
|
|
||||||
expanded: !expanded,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const { mxEvent } = this.props;
|
|
||||||
const { expanded } = this.state;
|
|
||||||
|
|
||||||
let content;
|
|
||||||
if (expanded) {
|
|
||||||
content = <pre>{JSON.stringify(mxEvent, null, 4)}</pre>;
|
|
||||||
} else {
|
|
||||||
content = <code>{`{ "type": ${mxEvent.getType()} }`}</code>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = classNames("mx_ViewSourceEvent mx_EventTile_content", {
|
|
||||||
mx_ViewSourceEvent_expanded: expanded,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={classes}>
|
|
||||||
{content}
|
|
||||||
<AccessibleButton
|
|
||||||
kind="link"
|
|
||||||
title={_t("devtools|toggle_event")}
|
|
||||||
className="mx_ViewSourceEvent_toggle"
|
|
||||||
onClick={this.onToggle}
|
|
||||||
>
|
|
||||||
{expanded ? <CollapseIcon /> : <ExpandIcon />}
|
|
||||||
</AccessibleButton>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
MKeyVerificationRequestView,
|
MKeyVerificationRequestView,
|
||||||
RoomAvatarEventView,
|
RoomAvatarEventView,
|
||||||
TextualEventView,
|
TextualEventView,
|
||||||
|
ViewSourceEventView,
|
||||||
useCreateAutoDisposedViewModel,
|
useCreateAutoDisposedViewModel,
|
||||||
} from "@element-hq/web-shared-components";
|
} from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
@@ -44,7 +45,6 @@ import { useMatrixClientContext } from "../contexts/MatrixClientContext";
|
|||||||
import { WidgetType } from "../widgets/WidgetType";
|
import { WidgetType } from "../widgets/WidgetType";
|
||||||
import { hasText } from "../TextForEvent";
|
import { hasText } from "../TextForEvent";
|
||||||
import { getMessageModerationState, MessageModerationState } from "../utils/EventUtils";
|
import { getMessageModerationState, MessageModerationState } from "../utils/EventUtils";
|
||||||
import ViewSourceEvent from "../components/views/messages/ViewSourceEvent";
|
|
||||||
import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline";
|
import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline";
|
||||||
import { type IBodyProps } from "../components/views/messages/IBodyProps";
|
import { type IBodyProps } from "../components/views/messages/IBodyProps";
|
||||||
import { ModuleApi } from "../modules/Api";
|
import { ModuleApi } from "../modules/Api";
|
||||||
@@ -54,6 +54,7 @@ import { MKeyVerificationRequestViewModel } from "../viewmodels/room/timeline/ev
|
|||||||
import { RoomAvatarEventViewModel } from "../viewmodels/room/timeline/event-tile/RoomAvatarEventViewModel";
|
import { RoomAvatarEventViewModel } from "../viewmodels/room/timeline/event-tile/RoomAvatarEventViewModel";
|
||||||
import { TextualEventViewModel } from "../viewmodels/room/timeline/event-tile/TextualEventViewModel";
|
import { TextualEventViewModel } from "../viewmodels/room/timeline/event-tile/TextualEventViewModel";
|
||||||
import { HiddenBodyViewModel } from "../viewmodels/room/timeline/event-tile/body/HiddenBodyViewModel";
|
import { HiddenBodyViewModel } from "../viewmodels/room/timeline/event-tile/body/HiddenBodyViewModel";
|
||||||
|
import { ViewSourceEventViewModel } from "../viewmodels/room/timeline/event-tile/body/ViewSourceEventViewModel";
|
||||||
import { ElementCallEventType } from "../call-types";
|
import { ElementCallEventType } from "../call-types";
|
||||||
import { CallStartedTileViewModel } from "../viewmodels/room/timeline/event-tile/call/CallStartedTileViewModel";
|
import { CallStartedTileViewModel } from "../viewmodels/room/timeline/event-tile/call/CallStartedTileViewModel";
|
||||||
|
|
||||||
@@ -127,6 +128,24 @@ function HiddenBodyWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
const HiddenEventFactory: Factory = (ref, props) => <HiddenBodyWrappedView ref={ref} {...props} />;
|
const HiddenEventFactory: Factory = (ref, props) => <HiddenBodyWrappedView ref={ref} {...props} />;
|
||||||
|
|
||||||
|
function ViewSourceEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
||||||
|
const cli = useMatrixClientContext();
|
||||||
|
const vm = useCreateAutoDisposedViewModel(() => new ViewSourceEventViewModel({ mxEvent, cli }));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
vm.setProps({ cli, mxEvent });
|
||||||
|
}, [cli, mxEvent, vm]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewSourceEventView
|
||||||
|
vm={vm}
|
||||||
|
ref={ref}
|
||||||
|
className="mx_ViewSourceEvent mx_EventTile_content"
|
||||||
|
expandedClassName="mx_ViewSourceEvent_expanded"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MJitsiWidgetEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
function MJitsiWidgetEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
||||||
const cli = useMatrixClientContext();
|
const cli = useMatrixClientContext();
|
||||||
const vm = useCreateAutoDisposedViewModel(() => new MJitsiWidgetEventViewModel({ mxEvent, cli }));
|
const vm = useCreateAutoDisposedViewModel(() => new MJitsiWidgetEventViewModel({ mxEvent, cli }));
|
||||||
@@ -178,8 +197,8 @@ export const CallStartedEventFactory: Factory = (ref, props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// These factories are exported for reference comparison against pickFactory()
|
// These factories are exported for reference comparison against pickFactory()
|
||||||
|
export const JSONEventFactory: Factory = (ref, props) => <ViewSourceEventWrappedView ref={ref} {...props} />;
|
||||||
export const JitsiEventFactory: Factory = (ref, props) => <MJitsiWidgetEventWrappedView ref={ref} {...props} />;
|
export const JitsiEventFactory: Factory = (ref, props) => <MJitsiWidgetEventWrappedView ref={ref} {...props} />;
|
||||||
export const JSONEventFactory: Factory = (ref, props) => <ViewSourceEvent ref={ref} {...props} />;
|
|
||||||
export const RoomCreateEventFactory: Factory = (_ref, props) => <RoomPredecessorTile {...props} />;
|
export const RoomCreateEventFactory: Factory = (_ref, props) => <RoomPredecessorTile {...props} />;
|
||||||
|
|
||||||
const EVENT_TILE_TYPES = new Map<string, Factory>([
|
const EVENT_TILE_TYPES = new Map<string, Factory>([
|
||||||
|
|||||||
@@ -892,7 +892,6 @@
|
|||||||
"thread_root_id": "Thread Root ID: %(threadRootId)s",
|
"thread_root_id": "Thread Root ID: %(threadRootId)s",
|
||||||
"threads_timeline": "Threads timeline",
|
"threads_timeline": "Threads timeline",
|
||||||
"title": "Developer tools",
|
"title": "Developer tools",
|
||||||
"toggle_event": "toggle event",
|
|
||||||
"toolbox": "Toolbox",
|
"toolbox": "Toolbox",
|
||||||
"use_at_own_risk": "This UI does NOT check the types of the values. Use at your own risk.",
|
"use_at_own_risk": "This UI does NOT check the types of the values. Use at your own risk.",
|
||||||
"user_avatar": "Avatar: %(avatar)s",
|
"user_avatar": "Avatar: %(avatar)s",
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
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 { type MouseEvent } from "react";
|
||||||
|
import { type MatrixClient, type MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
import {
|
||||||
|
BaseViewModel,
|
||||||
|
Disposables,
|
||||||
|
type ViewSourceEventViewModel as ViewSourceEventViewModelInterface,
|
||||||
|
type ViewSourceEventViewSnapshot,
|
||||||
|
} from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
|
export interface ViewSourceEventViewModelProps {
|
||||||
|
/**
|
||||||
|
* The hidden event whose source is being rendered.
|
||||||
|
*/
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
/**
|
||||||
|
* Matrix client used to request decryption before rendering event source.
|
||||||
|
*/
|
||||||
|
cli: MatrixClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel for hidden event source rendering.
|
||||||
|
*/
|
||||||
|
export class ViewSourceEventViewModel
|
||||||
|
extends BaseViewModel<ViewSourceEventViewSnapshot, ViewSourceEventViewModelProps>
|
||||||
|
implements ViewSourceEventViewModelInterface
|
||||||
|
{
|
||||||
|
private decryptionListenerDisposables?: Disposables;
|
||||||
|
|
||||||
|
private static computeSnapshot(
|
||||||
|
{ mxEvent }: ViewSourceEventViewModelProps,
|
||||||
|
expanded: boolean,
|
||||||
|
): ViewSourceEventViewSnapshot {
|
||||||
|
return {
|
||||||
|
expanded,
|
||||||
|
preview: `{ "type": ${mxEvent.getType()} }`,
|
||||||
|
source: expanded ? ViewSourceEventViewModel.computeSource(mxEvent) : "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static computeSource(mxEvent: MatrixEvent): string {
|
||||||
|
return JSON.stringify(mxEvent, null, 4) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(props: ViewSourceEventViewModelProps) {
|
||||||
|
super(props, ViewSourceEventViewModel.computeSnapshot(props, false));
|
||||||
|
this.disposables.track(() => this.removeDecryptionListener());
|
||||||
|
this.setupDecryptionListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setProps(newProps: Partial<ViewSourceEventViewModelProps>): void {
|
||||||
|
const nextProps = { ...this.props, ...newProps };
|
||||||
|
const eventChanged = this.props.mxEvent !== nextProps.mxEvent;
|
||||||
|
const clientChanged = this.props.cli !== nextProps.cli;
|
||||||
|
|
||||||
|
if (!eventChanged && !clientChanged) return;
|
||||||
|
|
||||||
|
this.props = nextProps;
|
||||||
|
|
||||||
|
this.setupDecryptionListener();
|
||||||
|
|
||||||
|
if (eventChanged) {
|
||||||
|
this.updateSnapshotFromProps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onToggle = (event: MouseEvent<HTMLButtonElement>): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const expanded = !this.snapshot.current.expanded;
|
||||||
|
this.snapshot.merge({
|
||||||
|
expanded,
|
||||||
|
source: expanded ? ViewSourceEventViewModel.computeSource(this.props.mxEvent) : "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateSnapshotFromProps(): void {
|
||||||
|
this.snapshot.merge(ViewSourceEventViewModel.computeSnapshot(this.props, this.snapshot.current.expanded));
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupDecryptionListener(): void {
|
||||||
|
this.removeDecryptionListener();
|
||||||
|
|
||||||
|
const { cli, mxEvent } = this.props;
|
||||||
|
cli.decryptEventIfNeeded(mxEvent);
|
||||||
|
|
||||||
|
if (!mxEvent.isBeingDecrypted()) return;
|
||||||
|
|
||||||
|
const onDecrypted = (): void => {
|
||||||
|
this.removeDecryptionListener();
|
||||||
|
if (this.props.mxEvent !== mxEvent) return;
|
||||||
|
|
||||||
|
this.updateSnapshotFromProps();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.decryptionListenerDisposables = new Disposables();
|
||||||
|
this.decryptionListenerDisposables.trackListener(mxEvent, MatrixEventEvent.Decrypted, onDecrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeDecryptionListener(): void {
|
||||||
|
this.decryptionListenerDisposables?.dispose();
|
||||||
|
this.decryptionListenerDisposables = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
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 { type MouseEvent } from "react";
|
||||||
|
import { type MatrixClient, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { ViewSourceEventViewModel } from "../../../src/viewmodels/room/timeline/event-tile/body/ViewSourceEventViewModel";
|
||||||
|
|
||||||
|
describe("ViewSourceEventViewModel", () => {
|
||||||
|
const createClient = (): MatrixClient =>
|
||||||
|
({
|
||||||
|
decryptEventIfNeeded: jest.fn().mockResolvedValue(undefined),
|
||||||
|
}) as unknown as MatrixClient;
|
||||||
|
|
||||||
|
const createEvent = (type = "m.room.message", content: Record<string, unknown> = {}): MatrixEvent =>
|
||||||
|
new MatrixEvent({
|
||||||
|
type,
|
||||||
|
event_id: "$event:example.org",
|
||||||
|
sender: "@alice:example.org",
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createClickEvent = (): MouseEvent<HTMLButtonElement> =>
|
||||||
|
({
|
||||||
|
preventDefault: jest.fn(),
|
||||||
|
}) as unknown as MouseEvent<HTMLButtonElement>;
|
||||||
|
|
||||||
|
it("creates a collapsed event source snapshot and requests decryption", () => {
|
||||||
|
const cli = createClient();
|
||||||
|
const mxEvent = createEvent("m.room.member");
|
||||||
|
const vm = new ViewSourceEventViewModel({ cli, mxEvent });
|
||||||
|
|
||||||
|
expect(cli.decryptEventIfNeeded).toHaveBeenCalledWith(mxEvent);
|
||||||
|
expect(vm.getSnapshot()).toEqual({
|
||||||
|
expanded: false,
|
||||||
|
preview: '{ "type": m.room.member }',
|
||||||
|
source: "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles expanded state", () => {
|
||||||
|
const mxEvent = createEvent();
|
||||||
|
const vm = new ViewSourceEventViewModel({ cli: createClient(), mxEvent });
|
||||||
|
const event = createClickEvent();
|
||||||
|
|
||||||
|
vm.onToggle(event);
|
||||||
|
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(vm.getSnapshot().expanded).toBe(true);
|
||||||
|
expect(vm.getSnapshot().source).toBe(JSON.stringify(mxEvent, null, 4));
|
||||||
|
|
||||||
|
vm.onToggle(createClickEvent());
|
||||||
|
|
||||||
|
expect(vm.getSnapshot().expanded).toBe(false);
|
||||||
|
expect(vm.getSnapshot().source).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the event source when the event changes", () => {
|
||||||
|
const cli = createClient();
|
||||||
|
const oldEvent = createEvent("m.room.message");
|
||||||
|
const newEvent = createEvent("m.room.topic", { topic: "New topic" });
|
||||||
|
const vm = new ViewSourceEventViewModel({ cli, mxEvent: oldEvent });
|
||||||
|
|
||||||
|
vm.onToggle(createClickEvent());
|
||||||
|
vm.setProps({ mxEvent: newEvent });
|
||||||
|
|
||||||
|
expect(cli.decryptEventIfNeeded).toHaveBeenCalledWith(newEvent);
|
||||||
|
expect(vm.getSnapshot()).toEqual({
|
||||||
|
expanded: true,
|
||||||
|
preview: '{ "type": m.room.topic }',
|
||||||
|
source: JSON.stringify(newEvent, null, 4),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes the previous decryption listener when the event changes", () => {
|
||||||
|
const oldEvent = createEvent("m.room.encrypted");
|
||||||
|
jest.spyOn(oldEvent, "isBeingDecrypted").mockReturnValue(true);
|
||||||
|
const offSpy = jest.spyOn(oldEvent, "off");
|
||||||
|
const vm = new ViewSourceEventViewModel({ cli: createClient(), mxEvent: oldEvent });
|
||||||
|
|
||||||
|
vm.setProps({ mxEvent: createEvent("m.room.message") });
|
||||||
|
|
||||||
|
expect(offSpy).toHaveBeenCalledWith(MatrixEventEvent.Decrypted, expect.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the decryption request when the client changes", () => {
|
||||||
|
const oldClient = createClient();
|
||||||
|
const newClient = createClient();
|
||||||
|
const mxEvent = createEvent();
|
||||||
|
const vm = new ViewSourceEventViewModel({ cli: oldClient, mxEvent });
|
||||||
|
const listener = jest.fn();
|
||||||
|
vm.subscribe(listener);
|
||||||
|
|
||||||
|
vm.setProps({ cli: newClient });
|
||||||
|
|
||||||
|
expect(newClient.decryptEventIfNeeded).toHaveBeenCalledWith(mxEvent);
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not emit when setProps receives unchanged props", () => {
|
||||||
|
const cli = createClient();
|
||||||
|
const mxEvent = createEvent();
|
||||||
|
const vm = new ViewSourceEventViewModel({ cli, mxEvent });
|
||||||
|
const listener = jest.fn();
|
||||||
|
vm.subscribe(listener);
|
||||||
|
|
||||||
|
vm.setProps({ cli, mxEvent });
|
||||||
|
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the source after decryption completes", () => {
|
||||||
|
const mxEvent = createEvent("m.room.encrypted", { ciphertext: "encrypted" });
|
||||||
|
jest.spyOn(mxEvent, "isBeingDecrypted").mockReturnValue(true);
|
||||||
|
const vm = new ViewSourceEventViewModel({ cli: createClient(), mxEvent });
|
||||||
|
vm.onToggle(createClickEvent());
|
||||||
|
const listener = jest.fn();
|
||||||
|
vm.subscribe(listener);
|
||||||
|
|
||||||
|
mxEvent.getContent().body = "decrypted";
|
||||||
|
mxEvent.emit(MatrixEventEvent.Decrypted, mxEvent);
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vm.getSnapshot().source).toContain("decrypted");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes decryption listeners on dispose", () => {
|
||||||
|
const mxEvent = createEvent("m.room.encrypted");
|
||||||
|
jest.spyOn(mxEvent, "isBeingDecrypted").mockReturnValue(true);
|
||||||
|
const offSpy = jest.spyOn(mxEvent, "off");
|
||||||
|
const vm = new ViewSourceEventViewModel({ cli: createClient(), mxEvent });
|
||||||
|
|
||||||
|
vm.dispose();
|
||||||
|
|
||||||
|
expect(offSpy).toHaveBeenCalledWith(MatrixEventEvent.Decrypted, expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -41,6 +41,9 @@
|
|||||||
"preferences": "Preferences",
|
"preferences": "Preferences",
|
||||||
"state_encryption_enabled": "Experimental state encryption enabled"
|
"state_encryption_enabled": "Experimental state encryption enabled"
|
||||||
},
|
},
|
||||||
|
"devtools": {
|
||||||
|
"toggle_event": "toggle event"
|
||||||
|
},
|
||||||
"keyboard": {
|
"keyboard": {
|
||||||
"shift": "Shift"
|
"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/MVideoBodyView";
|
||||||
export * from "./room/timeline/event-tile/body/TextualBodyView";
|
export * from "./room/timeline/event-tile/body/TextualBodyView";
|
||||||
export * from "./room/timeline/event-tile/body/UnknownBodyView";
|
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 "./room/timeline/event-tile/EventTileView/TileErrorView";
|
||||||
export * from "./core/pill-input/Pill";
|
export * from "./core/pill-input/Pill";
|
||||||
export * from "./core/pill-input/PillInput";
|
export * from "./core/pill-input/PillInput";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
@@ -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";
|
||||||