Refactor className and children to component properties instead och view model snapshots in shared components (#32711)

* Refactor className? to component property in EncryptionEventView

* Refactor extraClassNames to default react className as component property for DecryptionFailureBodyView

* Refactor className to component property for MessageTimestampView

* Refactor className and children to component properties for ReactionsRowButton

* Refactor className to component property for DisambiguatedProfile

* Refactor className to a component property in DateSeparatorView

* Fix for lint errors and EncryptionEventView unsupported icon color

* EncryptionEventView fix for icon color css specificity/order
This commit is contained in:
rbondesson
2026-03-05 09:36:45 +01:00
committed by GitHub
parent 1963f268aa
commit 83d732d60e
61 changed files with 232 additions and 255 deletions
@@ -9,29 +9,7 @@ Please see LICENSE files in the repository root for full details.
.mx_EventTileBubble.mx_cryptoEvent {
margin: var(--EventTileBubble_margin-block) auto;
&.mx_cryptoEvent_icon svg {
svg[data-state="supported"] {
color: $header-panel-text-primary-color;
}
.mx_cryptoEvent_state,
.mx_cryptoEvent_buttons {
grid-column: 3;
grid-row: 1 / 3;
}
.mx_cryptoEvent_buttons {
align-items: center;
display: flex;
gap: 5px;
}
.mx_cryptoEvent_state {
width: 130px;
padding: 10px 20px;
margin: auto 0;
text-align: center;
color: $tertiary-content;
overflow-wrap: break-word;
font-size: $font-12px;
}
}
@@ -66,7 +66,7 @@ const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): JSX.Element {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
return <DateSeparatorView vm={vm} />;
return <DateSeparatorView vm={vm} className="mx_TimelineSeparator" />;
}
/**
@@ -416,7 +416,8 @@ function RoomStatusBarWrappedView(props: ConstructorParameters<typeof RoomStatus
function EncryptionEventWrappedView({ mxEvent }: { mxEvent: MatrixEvent }): ReactElement | null {
const cli = useMatrixClientContext();
const vm = useCreateAutoDisposedViewModel(() => new EncryptionEventViewModel({ mxEvent, cli }));
return <EncryptionEventView vm={vm} />;
return <EncryptionEventView vm={vm} className="mx_EventTileBubble mx_cryptoEvent" />;
}
export class RoomView extends React.Component<IRoomProps, IRoomState> {
@@ -45,7 +45,7 @@ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resize
<ScrollPanel className="mx_RoomView_messagePanel">
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
className="mx_EventTileBubble mx_cryptoEvent"
title={_t("room|waiting_for_join_title", { brand })}
subtitle={_t("room|waiting_for_join_subtitle", { brand })}
/>
@@ -29,7 +29,7 @@ import { DateSeparatorViewModel } from "../../../viewmodels/timeline/DateSeparat
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): ReactNode {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
return <DateSeparatorView vm={vm} />;
return <DateSeparatorView vm={vm} className="mx_TimelineSeparator" />;
}
export class CreationGrouper extends BaseGrouper {
@@ -31,7 +31,7 @@ const groupedStateEvents = [
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): ReactNode {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
return <DateSeparatorView vm={vm} />;
return <DateSeparatorView vm={vm} className="mx_TimelineSeparator" />;
}
// Wrap consecutive grouped events in a ListSummary
@@ -143,7 +143,10 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps
const separatorTs = e.getTs();
nodes.push(
<li key={`${separatorRoomId}-${separatorTs}~`}>
<DateSeparatorView vm={this.getDateSeparatorVm(separatorRoomId, separatorTs)} />
<DateSeparatorView
vm={this.getDateSeparatorVm(separatorRoomId, separatorTs)}
className="mx_TimelineSeparator"
/>
</li>,
);
}
@@ -657,5 +657,5 @@ function MessageTimestampWrapper(props: MessageTimestampViewModelProps): JSX.Ele
vm.setHref(props.href);
vm.setHandlers({ onClick: props.onClick });
}, [vm, props]);
return <MessageTimestampView vm={vm} />;
return <MessageTimestampView vm={vm} className="mx_MessageTimestamp" />;
}
@@ -75,7 +75,7 @@ const MKeyVerificationRequest: React.FC<Props> = ({ mxEvent, timestamp }) => {
return (
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
className="mx_EventTileBubble mx_cryptoEvent"
title={title}
subtitle={subtitle}
>
@@ -347,5 +347,5 @@ function DecryptionFailureBodyWrapper({ mxEvent, ref }: IBodyProps): JSX.Element
useEffect(() => {
vm.setVerificationState(verificationState);
}, [verificationState, vm]);
return <DecryptionFailureBodyView vm={vm} ref={ref} />;
return <DecryptionFailureBodyView vm={vm} ref={ref} className="mx_DecryptionFailureBody mx_EventTile_content" />;
}
@@ -37,7 +37,6 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
colored: true,
emphasizeDisplayName: true,
withTooltip,
className: "mx_DisambiguatedProfile",
}),
);
@@ -45,7 +44,7 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
disambiguatedProfileVM.setMember(sender ?? "", member);
}, [disambiguatedProfileVM, member, sender]);
return mxEvent.getContent().msgtype !== MsgType.Emote ? (
<DisambiguatedProfileView vm={disambiguatedProfileVM} />
<DisambiguatedProfileView vm={disambiguatedProfileVM} className="mx_DisambiguatedProfile" />
) : (
<></>
);
@@ -1617,7 +1617,7 @@ function DecryptionFailureBodyWrapper({ mxEvent }: { mxEvent: MatrixEvent }): JS
vm.setVerificationState(verificationState);
}, [verificationState, vm]);
return <DecryptionFailureBodyView vm={vm} />;
return <DecryptionFailureBodyView vm={vm} className="mx_DecryptionFailureBody mx_EventTile_content" />;
}
/**
@@ -1643,7 +1643,7 @@ function MessageTimestampWrapper(props: MessageTimestampViewModelProps): JSX.Ele
{props.receivedTs ? (
<LateIcon className="mx_MessageTimestamp_lateIcon" width="16" height="16" />
) : undefined}
<MessageTimestampView vm={vm} />
<MessageTimestampView vm={vm} className="mx_MessageTimestamp" />
</>
);
}
@@ -1859,10 +1859,6 @@ function ReactionsRowWrapper({ mxEvent, reactions }: Readonly<ReactionsRowWrappe
snapshot.showAllButtonVisible,
]);
useEffect(() => {
vm.setChildren(items);
}, [items, vm]);
if (!snapshot.isVisible || !items?.length) {
return null;
}
@@ -1878,7 +1874,9 @@ function ReactionsRowWrapper({ mxEvent, reactions }: Readonly<ReactionsRowWrappe
return (
<>
<ReactionsRowView vm={vm} />
<ReactionsRowView vm={vm} className="mx_ReactionsRow">
{items}
</ReactionsRowView>
{contextMenu}
</>
);
@@ -53,13 +53,12 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
fallbackName: name,
member,
withTooltip: true,
className: "mx_DisambiguatedProfile",
}),
);
useEffect(() => {
disambiguatedProfileVM.setMember(name, member);
}, [disambiguatedProfileVM, member, name]);
const nameJSX = <DisambiguatedProfileView vm={disambiguatedProfileVM} />;
const nameJSX = <DisambiguatedProfileView vm={disambiguatedProfileVM} className="mx_DisambiguatedProfile" />;
const presenceState = member.presenceState;
let presenceJSX: JSX.Element | undefined;
@@ -40,7 +40,7 @@ interface IProps {
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): JSX.Element {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
return <DateSeparatorView vm={vm} />;
return <DateSeparatorView vm={vm} className="mx_TimelineSeparator" />;
}
export default class SearchResultTile extends React.Component<IProps> {
+2 -1
View File
@@ -88,7 +88,8 @@ export const TextualEventFactory: Factory = (ref, props) => {
function EncryptionEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
const cli = useMatrixClientContext();
const vm = useCreateAutoDisposedViewModel(() => new EncryptionEventViewModel({ mxEvent, cli }));
return <EncryptionEventView vm={vm} ref={ref} />;
return <EncryptionEventView vm={vm} ref={ref} className="mx_EventTileBubble mx_cryptoEvent" />;
}
const EncryptionEventFactory: Factory = (ref, props) => {
return <EncryptionEventWrappedView ref={ref} {...props} />;
@@ -266,7 +266,7 @@ export default class HTMLExporter extends Exporter {
try {
const dateSeparator = (
<li key={ts}>
<DateSeparatorView vm={dateSeparatorViewModel} />
<DateSeparatorView vm={dateSeparatorViewModel} className="mx_TimelineSeparator" />
</li>
);
return this.renderToStaticMarkupWithProviders(dateSeparator);
@@ -78,13 +78,11 @@ export class EncryptionEventViewModel
props: EncryptionEventViewModelProps,
isEncrypted: boolean,
): EncryptionEventViewSnapshotInterface {
// Keep legacy class names for compatibility with existing timeline layout and styling.
const newSnapshot: EncryptionEventViewSnapshotInterface = {
state: EncryptionEventState.CHANGED,
encryptedStateEvents: undefined,
userName: undefined,
timestamp: props.timestamp,
className: "mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon",
};
const content = props.mxEvent.getContent<RoomEncryptionEventContent>();
@@ -113,8 +111,6 @@ export class EncryptionEventViewModel
newSnapshot.state = EncryptionEventState.DISABLE_ATTEMPT;
} else {
newSnapshot.state = EncryptionEventState.UNSUPPORTED;
// Unsupported branch matches legacy EncryptionEvent class usage (no icon class).
newSnapshot.className = "mx_EventTileBubble mx_cryptoEvent";
}
return newSnapshot;
@@ -22,10 +22,6 @@ export interface DecryptionFailureBodyViewModelProps {
* The local device verification state.
*/
verificationState?: boolean;
/**
* Extra CSS classes to apply to the component
*/
extraClassNames?: string[];
}
/**
@@ -62,30 +58,21 @@ export class DecryptionFailureBodyViewModel
/**
* @param decryptionFailureCode - The decryption failure code for the event.
* @param verificationState - The local device verification state.
* @param extraClassNames - Extra CSS classes to apply to the component.
*/
private static readonly computeSnapshot = (
decryptionFailureCode: DecryptionFailureCode | null,
verificationState?: boolean,
extraClassNames?: string[],
): DecryptionFailureBodyViewSnapshotInterface => {
// Keep mx_DecryptionFailureBody and mx_EventTile_content to support the compatibility with existing timeline and the all the layout
const defaultClassNames = ["mx_DecryptionFailureBody", "mx_EventTile_content"];
return {
decryptionFailureReason: DecryptionFailureBodyViewModel.getDecryptionReasonFromCode(decryptionFailureCode),
isLocalDeviceVerified: verificationState,
extraClassNames: extraClassNames ? defaultClassNames.concat(extraClassNames) : defaultClassNames,
};
};
public constructor(props: DecryptionFailureBodyViewModelProps) {
super(
props,
DecryptionFailureBodyViewModel.computeSnapshot(
props.decryptionFailureCode,
props.verificationState,
props.extraClassNames,
),
DecryptionFailureBodyViewModel.computeSnapshot(props.decryptionFailureCode, props.verificationState),
);
}
@@ -91,14 +91,12 @@ export class MessageTimestampViewModel
receivedAt = formatFullDate(receivedDate, props.showTwelveHour);
}
// Keep mx_MessageTimestamp for compatibility with the existing timeline and layout.
return {
ts: timestamp,
tsSentAt: sentAt,
tsReceivedAt: receivedAt,
inhibitTooltip: props.inhibitTooltip,
href: props.href,
className: "mx_MessageTimestamp",
};
};
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type MouseEvent, type MouseEventHandler, type ReactNode } from "react";
import { type MouseEvent, type MouseEventHandler } from "react";
import {
BaseViewModel,
type ReactionsRowViewSnapshot,
@@ -41,10 +41,6 @@ export interface ReactionsRowViewModelProps {
* Optional callback invoked on add-reaction button context-menu.
*/
onAddReactionContextMenu?: MouseEventHandler<HTMLButtonElement>;
/**
* Reaction row children (typically reaction buttons).
*/
children?: ReactNode;
}
interface InternalProps extends ReactionsRowViewModelProps {
@@ -59,18 +55,16 @@ export class ReactionsRowViewModel
props: InternalProps,
): Pick<
ReactionsRowViewSnapshot,
"isVisible" | "showAllButtonVisible" | "showAddReactionButton" | "addReactionButtonActive" | "children"
"isVisible" | "showAllButtonVisible" | "showAddReactionButton" | "addReactionButtonActive"
> => ({
isVisible: props.isActionable && props.reactionGroupCount > 0,
showAllButtonVisible: props.reactionGroupCount > MAX_ITEMS_WHEN_LIMITED + 1 && !props.showAll,
showAddReactionButton: props.canReact,
addReactionButtonActive: !!props.addReactionButtonActive,
children: props.children,
});
private static readonly computeSnapshot = (props: InternalProps): ReactionsRowViewSnapshot => ({
ariaLabel: _t("common|reactions"),
className: "mx_ReactionsRow",
showAllButtonLabel: _t("action|show_all"),
addReactionButtonLabel: _t("timeline|reactions|add_reaction_prompt"),
addReactionButtonVisible: false,
@@ -136,15 +130,6 @@ export class ReactionsRowViewModel
this.snapshot.merge({ addReactionButtonActive });
}
public setChildren(children?: ReactNode): void {
this.props = {
...this.props,
children,
};
this.snapshot.merge({ children });
}
public setAddReactionHandlers({
onAddReactionClick,
onAddReactionContextMenu,
@@ -66,10 +66,6 @@ export interface DisambiguatedProfileViewModelProps {
* Optional click handler for the profile.
*/
onClick?: DisambiguatedProfileViewActions["onClick"];
/**
* Optional CSS class name to apply to the profile.
*/
className?: string;
}
/**
@@ -83,7 +79,7 @@ export class DisambiguatedProfileViewModel
private static readonly computeSnapshot = (
props: DisambiguatedProfileViewModelProps,
): DisambiguatedProfileViewSnapshot => {
const { member, fallbackName, colored, emphasizeDisplayName, withTooltip, className } = props;
const { member, fallbackName, colored, emphasizeDisplayName, withTooltip } = props;
// Compute display name
const displayName = member?.rawDisplayName || fallbackName;
@@ -123,7 +119,6 @@ export class DisambiguatedProfileViewModel
return {
displayName,
colorClass,
className,
displayIdentifier,
title,
emphasizeDisplayName,
@@ -68,7 +68,6 @@ export class DateSeparatorViewModel
super(props, {
label: DateSeparatorViewModel.computeLabel(props, relativeDatesEnabled),
className: "mx_TimelineSeparator",
});
this.relativeDatesEnabled = relativeDatesEnabled;
@@ -101,7 +100,6 @@ export class DateSeparatorViewModel
const label = DateSeparatorViewModel.computeLabel(this.props, this.relativeDatesEnabled);
return {
label,
className: "mx_TimelineSeparator",
jumpToEnabled: this.jumpToDateEnabled && !this.props.forExport,
jumpFromDate: formatDateForInput(new Date(this.props.ts)),
};
@@ -717,9 +717,10 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
style="height: 400px;"
>
<div
class="_container_sq5fu_8 mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
class="_container_sq5fu_8 mx_EventTileBubble mx_cryptoEvent _content_m88ar_8"
>
<svg
data-state="supported"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -64,7 +64,6 @@ describe("EncryptionEventViewModel", () => {
await waitFor(() => expect(vm.getSnapshot().state).toBe(EncryptionEventState.ENABLED));
expect(vm.getSnapshot()).toMatchObject({
state: EncryptionEventState.ENABLED,
className: "mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon",
encryptedStateEvents: false,
});
});
@@ -125,7 +124,6 @@ describe("EncryptionEventViewModel", () => {
const vm = createVm();
await waitFor(() => expect(vm.getSnapshot().state).toBe(EncryptionEventState.UNSUPPORTED));
expect(vm.getSnapshot().className).toBe("mx_EventTileBubble mx_cryptoEvent");
});
it("sets ENABLED_DM with partner display name", async () => {
@@ -22,19 +22,6 @@ describe("DecryptionFailureBodyViewModel", () => {
});
});
it("should return the snapshot with extra class names", () => {
const vm = new DecryptionFailureBodyViewModel({
decryptionFailureCode: null,
verificationState: true,
extraClassNames: ["custom-class"],
});
expect(vm.getSnapshot()).toMatchObject({
decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT,
isLocalDeviceVerified: true,
extraClassNames: ["mx_DecryptionFailureBody", "mx_EventTile_content", "custom-class"],
});
});
it.each([
{
code: DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
@@ -42,14 +42,13 @@ describe("MessageTimestampViewModel", () => {
});
});
it("should return the snapshot with extra class names", () => {
it("should return the snapshot without presentation class names", () => {
const vm = new MessageTimestampViewModel({
ts: nowDate.getTime(),
});
expect(vm.getSnapshot()).toMatchObject({
ts: "08:09",
tsSentAt: "Fri, Dec 17, 2021, 08:09:00",
className: "mx_MessageTimestamp",
});
});
@@ -29,7 +29,6 @@ describe("ReactionsRowViewModel", () => {
expect(snapshot.showAllButtonVisible).toBe(true);
expect(snapshot.showAddReactionButton).toBe(true);
expect(snapshot.addReactionButtonActive).toBe(false);
expect(snapshot.className).toContain("mx_ReactionsRow");
});
it("hides show-all after onShowAllClick", () => {
@@ -31,7 +31,6 @@ describe("DisambiguatedProfileViewModel", () => {
expect(vm.getSnapshot()).toEqual({
displayName: "Alice",
colorClass: "mx_Username_color3",
className: undefined,
displayIdentifier: "@alice:example.org",
title: "Alice (@alice:example.org)",
emphasizeDisplayName: true,
@@ -47,23 +46,12 @@ describe("DisambiguatedProfileViewModel", () => {
expect(vm.getSnapshot()).toMatchObject({
displayName: "Fallback",
colorClass: undefined,
className: undefined,
displayIdentifier: undefined,
title: undefined,
emphasizeDisplayName: undefined,
});
});
it("should pass through className prop", () => {
const vm = new DisambiguatedProfileViewModel({
member,
fallbackName: "Fallback",
className: "mx_DisambiguatedProfile",
});
expect(vm.getSnapshot().className).toBe("mx_DisambiguatedProfile");
});
it("should delegate onClick without emitting a snapshot update", () => {
const onClick = jest.fn();
const vm = new DisambiguatedProfileViewModel({
@@ -109,7 +109,6 @@ describe("DateSeparatorViewModel", () => {
const vm = createViewModel();
expect(vm.getSnapshot().label).toBe("today");
expect(vm.getSnapshot().className).toBe("mx_TimelineSeparator");
});
it("uses full date when exporting", () => {