Refactor hidden body into shared MVVM (#33403)
* Refactor hidden body into shared MVVM * Snapshots * Update packages/shared-components/src/room/timeline/event-tile/body/HiddenBodyView/HiddenBodyView.stories.tsx Co-authored-by: Florian Duros <florian.duros@ormaz.fr> * Update apps/web/src/viewmodels/room/timeline/event-tile/body/HiddenBodyViewModel.ts Co-authored-by: Florian Duros <florian.duros@ormaz.fr> * Use compound Text in HiddenBodyView * Update snapshots --------- Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
This commit is contained in:
@@ -222,7 +222,6 @@
|
||||
@import "./views/messages/_CallEvent.pcss";
|
||||
@import "./views/messages/_CreateEvent.pcss";
|
||||
@import "./views/messages/_DisambiguatedProfile.pcss";
|
||||
@import "./views/messages/_HiddenBody.pcss";
|
||||
@import "./views/messages/_LegacyCallEvent.pcss";
|
||||
@import "./views/messages/_MFileBody.pcss";
|
||||
@import "./views/messages/_MImageBody.pcss";
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 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_HiddenBody {
|
||||
white-space: pre-wrap;
|
||||
color: $muted-fg-color;
|
||||
vertical-align: middle;
|
||||
|
||||
svg {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
display: inline-block;
|
||||
margin-right: var(--cpd-space-1-5x);
|
||||
color: $muted-fg-color;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 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 { VisibilityOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { type IBodyProps } from "./IBodyProps";
|
||||
|
||||
/**
|
||||
* A message hidden from the user pending moderation.
|
||||
*
|
||||
* Note: This component must not be used when the user is the author of the message
|
||||
* or has a sufficient powerlevel to see the message.
|
||||
*/
|
||||
const HiddenBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => {
|
||||
let text;
|
||||
const visibility = mxEvent.messageVisibility();
|
||||
switch (visibility.visible) {
|
||||
case true:
|
||||
throw new Error("HiddenBody should only be applied to hidden messages");
|
||||
case false:
|
||||
if (visibility.reason) {
|
||||
text = _t("timeline|pending_moderation_reason", { reason: visibility.reason });
|
||||
} else {
|
||||
text = _t("timeline|pending_moderation");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="mx_HiddenBody" ref={ref}>
|
||||
<VisibilityOffIcon />
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default HiddenBody;
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX } from "react";
|
||||
import React, { type JSX, useEffect } from "react";
|
||||
import {
|
||||
type MatrixEvent,
|
||||
EventType,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
EncryptionEventView,
|
||||
HiddenBodyView,
|
||||
TextualEventView,
|
||||
useCreateAutoDisposedViewModel,
|
||||
} from "@element-hq/web-shared-components";
|
||||
@@ -41,13 +42,13 @@ import { WidgetType } from "../widgets/WidgetType";
|
||||
import MJitsiWidgetEvent from "../components/views/messages/MJitsiWidgetEvent";
|
||||
import { hasText } from "../TextForEvent";
|
||||
import { getMessageModerationState, MessageModerationState } from "../utils/EventUtils";
|
||||
import HiddenBody from "../components/views/messages/HiddenBody";
|
||||
import ViewSourceEvent from "../components/views/messages/ViewSourceEvent";
|
||||
import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline";
|
||||
import { type IBodyProps } from "../components/views/messages/IBodyProps";
|
||||
import { ModuleApi } from "../modules/Api";
|
||||
import { EncryptionEventViewModel } from "../viewmodels/room/timeline/event-tile/EncryptionEventViewModel";
|
||||
import { TextualEventViewModel } from "../viewmodels/room/timeline/event-tile/TextualEventViewModel";
|
||||
import { HiddenBodyViewModel } from "../viewmodels/room/timeline/event-tile/body/HiddenBodyViewModel";
|
||||
import { ElementCallEventType } from "../call-types";
|
||||
|
||||
// Subset of EventTile's IProps plus some mixins
|
||||
@@ -95,7 +96,16 @@ const EncryptionEventFactory: Factory = (ref, props) => {
|
||||
return <EncryptionEventWrappedView ref={ref} {...props} />;
|
||||
};
|
||||
const VerificationReqFactory: Factory = (_ref, props) => <MKeyVerificationRequest {...props} />;
|
||||
const HiddenEventFactory: Factory = (ref, props) => <HiddenBody ref={ref} {...props} />;
|
||||
function HiddenBodyWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
||||
const vm = useCreateAutoDisposedViewModel(() => new HiddenBodyViewModel({ mxEvent }));
|
||||
|
||||
useEffect(() => {
|
||||
vm.setEvent(mxEvent);
|
||||
}, [mxEvent, vm]);
|
||||
|
||||
return <HiddenBodyView vm={vm} ref={ref} className="mx_HiddenBody" />;
|
||||
}
|
||||
const HiddenEventFactory: Factory = (ref, props) => <HiddenBodyWrappedView ref={ref} {...props} />;
|
||||
|
||||
// These factories are exported for reference comparison against pickFactory()
|
||||
export const JitsiEventFactory: Factory = (ref, props) => <MJitsiWidgetEvent ref={ref} {...props} />;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
BaseViewModel,
|
||||
type HiddenBodyViewModel as HiddenBodyViewModelInterface,
|
||||
type HiddenBodyViewSnapshot,
|
||||
} from "@element-hq/web-shared-components";
|
||||
|
||||
export interface HiddenBodyViewModelProps {
|
||||
/**
|
||||
* The hidden event being rendered.
|
||||
*/
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewModel for messages hidden pending moderation.
|
||||
*/
|
||||
export class HiddenBodyViewModel
|
||||
extends BaseViewModel<HiddenBodyViewSnapshot, HiddenBodyViewModelProps>
|
||||
implements HiddenBodyViewModelInterface
|
||||
{
|
||||
private static readonly computeSnapshot = ({ mxEvent }: HiddenBodyViewModelProps): HiddenBodyViewSnapshot => {
|
||||
const visibility = mxEvent.messageVisibility();
|
||||
|
||||
if (visibility.visible) {
|
||||
throw new Error("HiddenBodyViewModel should only be applied to hidden messages");
|
||||
}
|
||||
|
||||
return {
|
||||
reason: visibility.reason || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
public constructor(props: HiddenBodyViewModelProps) {
|
||||
super(props, HiddenBodyViewModel.computeSnapshot(props));
|
||||
}
|
||||
|
||||
public setEvent(mxEvent: MatrixEvent): void {
|
||||
this.props.mxEvent = mxEvent;
|
||||
this.snapshot.merge(HiddenBodyViewModel.computeSnapshot(this.props));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { HiddenBodyViewModel } from "../../../src/viewmodels/room/timeline/event-tile/body/HiddenBodyViewModel";
|
||||
|
||||
describe("HiddenBodyViewModel", () => {
|
||||
const createEvent = (visible: boolean, reason?: string | null): MatrixEvent =>
|
||||
({
|
||||
messageVisibility: jest.fn().mockReturnValue({
|
||||
visible,
|
||||
reason,
|
||||
}),
|
||||
}) as unknown as MatrixEvent;
|
||||
|
||||
it("extracts a moderation reason from a hidden event", () => {
|
||||
const vm = new HiddenBodyViewModel({ mxEvent: createEvent(false, "spam") });
|
||||
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
reason: "spam",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits the reason when the hidden event has no reason", () => {
|
||||
const vm = new HiddenBodyViewModel({ mxEvent: createEvent(false, null) });
|
||||
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
reason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("throws when created for a visible event", () => {
|
||||
expect(() => new HiddenBodyViewModel({ mxEvent: createEvent(true) })).toThrow(
|
||||
"HiddenBodyViewModel should only be applied to hidden messages",
|
||||
);
|
||||
});
|
||||
|
||||
it("updates the snapshot when the event changes", () => {
|
||||
const vm = new HiddenBodyViewModel({ mxEvent: createEvent(false) });
|
||||
|
||||
vm.setEvent(createEvent(false, "abuse"));
|
||||
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
reason: "abuse",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not emit when setEvent receives the current event", () => {
|
||||
const event = createEvent(false, "spam");
|
||||
const vm = new HiddenBodyViewModel({ mxEvent: event });
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setEvent(event);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user