* Use shared MessageTimestampView in edit history * Move timestamp typography into shared component * Updated snapshots * Update comments after clean up * Fixed sonar issue and added tests for improved coverage * Fix Prettier issue * Fix font-size on timestamps and toolbar button sizing * Updated snapshots * Updated Storybook screenshots * Updated screenshots * Clean up app/web unused/redundant styling for disambiguated profile * Fix merge error * Fixes after merge * Fixes after merge
@@ -163,10 +163,10 @@ b {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Keep the legacy link colour without overriding Compound anchors. */
|
||||
a:where(:not([data-kind])):hover,
|
||||
a:where(:not([data-kind])):link,
|
||||
a:where(:not([data-kind])):visited {
|
||||
/* Keep the legacy link colour without overriding Compound anchors or shared timestamp styling. */
|
||||
a:where(:not([data-kind]):not(.mx_MessageTimestamp)):hover,
|
||||
a:where(:not([data-kind]):not(.mx_MessageTimestamp)):link,
|
||||
a:where(:not([data-kind]):not(.mx_MessageTimestamp)):visited {
|
||||
color: $accent-alt;
|
||||
}
|
||||
|
||||
@@ -600,6 +600,8 @@ legend {
|
||||
.mx_InviteDialog_section button,
|
||||
.mx_InviteDialog_editor button,
|
||||
.mx_UnknownIdentityUsersWarningDialog button,
|
||||
.mx_ThreadActionBar button,
|
||||
.mx_HistoryActionBar button,
|
||||
[class|="maplibregl"]
|
||||
),
|
||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton),
|
||||
@@ -907,16 +909,8 @@ legend {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* This class is used extensively in element-web and are included here for compatibility with the existing timeline and layout.
|
||||
/* TODO: Review mx_MessageTimestamp usage after finishing migration of timeline tiles to shared components. */
|
||||
/* https://github.com/element-hq/element-web/issues/31651 */
|
||||
/* Layout compatibility hook for timestamp placement in existing app/web timeline surfaces. */
|
||||
.mx_MessageTimestamp {
|
||||
color: var(--cpd-color-text-secondary) !important; /* override anchor color */
|
||||
font-size: $font-10px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
display: block; /* enable the width setting below */
|
||||
width: var(--MessageTimestamp-width);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -67,8 +67,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_MessageTimestamp {
|
||||
margin-left: auto;
|
||||
font: var(--cpd-font-body-xs-regular);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,6 @@ 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.
|
||||
*/
|
||||
|
||||
.mx_DisambiguatedProfile {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
.mx_DisambiguatedProfile_displayName {
|
||||
font: var(--cpd-font-body-md-semibold);
|
||||
margin-inline-end: 0;
|
||||
/* keeps the height in check, important for the bubble apperance */
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.mx_DisambiguatedProfile_mxid {
|
||||
color: $secondary-content;
|
||||
font-size: var(--cpd-font-size-body-sm);
|
||||
margin-inline-start: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/** Disambiguated profile needs to have a different layout in the member tile */
|
||||
.mx_MemberTileView .mx_DisambiguatedProfile {
|
||||
display: flex;
|
||||
|
||||
@@ -138,10 +138,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mx_MessageTimestamp {
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_BaseCard_footer {
|
||||
text-align: left;
|
||||
font-size: $font-12px;
|
||||
|
||||
@@ -288,7 +288,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
/* the selector here is quite weird because timestamps can appear linked & unlinked and in different places */
|
||||
/* in the DOM depending on the specific rendering context */
|
||||
> a, /* timestamp wrapper anchor */
|
||||
> a, /* linked timestamp element */
|
||||
.mx_MessageActionBar + .mx_MessageTimestamp {
|
||||
position: absolute;
|
||||
padding: 4px 8px;
|
||||
@@ -322,7 +322,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
&.mx_EventTile_sticker {
|
||||
> a, /* timestamp wrapper anchor */
|
||||
> a, /* linked timestamp element */
|
||||
.mx_MessageActionBar + .mx_MessageTimestamp {
|
||||
/* position timestamps for stickers to the right of the un-bubbled sticker */
|
||||
right: unset;
|
||||
@@ -434,7 +434,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_EventTile_reply {
|
||||
max-width: 90%;
|
||||
padding: 0;
|
||||
> a, /* timestamp wrapper anchor */
|
||||
> a, /* linked timestamp element */
|
||||
.mx_MessageActionBar + .mx_MessageTimestamp {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -549,7 +549,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
font-style: italic;
|
||||
|
||||
> a {
|
||||
/* timestamp anchor wrapper */
|
||||
/* linked timestamp element */
|
||||
align-self: center;
|
||||
bottom: unset;
|
||||
top: unset;
|
||||
@@ -606,7 +606,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
.mx_EventTile_line {
|
||||
> a, /* timestamp wrapper anchor */
|
||||
> a, /* linked timestamp element */
|
||||
.mx_MessageActionBar + .mx_MessageTimestamp {
|
||||
right: auto;
|
||||
left: -77px;
|
||||
|
||||
@@ -9,12 +9,12 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { createRef } from "react";
|
||||
import { type EventStatus, type IContent, type MatrixEvent, MatrixEventEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
import { ActionBarView, EventContentBodyView } from "@element-hq/web-shared-components";
|
||||
import { ActionBarView, EventContentBodyView, MessageTimestampView } from "@element-hq/web-shared-components";
|
||||
|
||||
import { EditHistoryActionBarViewModel } from "../../../viewmodels/message-body/EditHistoryActionBarViewModel";
|
||||
import { EventContentBodyViewModel } from "../../../viewmodels/message-body/EventContentBodyViewModel";
|
||||
import { MessageTimestampViewModel } from "../../../viewmodels/room/timeline/event-tile/timestamp/MessageTimestampViewModel";
|
||||
import { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils";
|
||||
import { formatTime } from "../../../DateUtils";
|
||||
import Modal from "../../../Modal";
|
||||
import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
|
||||
import ViewSource from "../../structures/ViewSource";
|
||||
@@ -45,8 +45,9 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
|
||||
declare public context: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
private content = createRef<HTMLDivElement>();
|
||||
private EventContentBodyViewModel: EventContentBodyViewModel;
|
||||
private editHistoryActionBarViewModel: EditHistoryActionBarViewModel;
|
||||
private readonly eventContentBodyViewModel: EventContentBodyViewModel;
|
||||
private readonly editHistoryActionBarViewModel: EditHistoryActionBarViewModel;
|
||||
private readonly messageTimestampViewModel: MessageTimestampViewModel;
|
||||
|
||||
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
@@ -60,7 +61,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
|
||||
this.state = { canRedact, sendStatus: event.getAssociatedStatus() };
|
||||
|
||||
const mxEventContent = getReplacedContent(event);
|
||||
this.EventContentBodyViewModel = new EventContentBodyViewModel({
|
||||
this.eventContentBodyViewModel = new EventContentBodyViewModel({
|
||||
mxEvent: event,
|
||||
content: mxEventContent,
|
||||
highlights: [],
|
||||
@@ -72,6 +73,11 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
|
||||
linkify: true,
|
||||
client: cli,
|
||||
});
|
||||
this.messageTimestampViewModel = new MessageTimestampViewModel({
|
||||
ts: event.getTs(),
|
||||
showTwelveHour: props.isTwelveHour,
|
||||
inhibitTooltip: true,
|
||||
});
|
||||
|
||||
this.editHistoryActionBarViewModel = new EditHistoryActionBarViewModel({
|
||||
canRemove: !props.mxEvent.isRedacted() && !props.isBaseEvent && canRedact,
|
||||
@@ -84,7 +90,15 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
|
||||
public componentDidUpdate(prevProps: IProps): void {
|
||||
if (prevProps.mxEvent !== this.props.mxEvent) {
|
||||
const mxEventContent = getReplacedContent(this.props.mxEvent);
|
||||
this.EventContentBodyViewModel.setEventContent(this.props.mxEvent, mxEventContent);
|
||||
this.eventContentBodyViewModel.setEventContent(this.props.mxEvent, mxEventContent);
|
||||
}
|
||||
|
||||
if (prevProps.mxEvent !== this.props.mxEvent || prevProps.isTwelveHour !== this.props.isTwelveHour) {
|
||||
this.messageTimestampViewModel.setProps({
|
||||
ts: this.props.mxEvent.getTs(),
|
||||
showTwelveHour: this.props.isTwelveHour,
|
||||
inhibitTooltip: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.editHistoryActionBarViewModel.setProps({
|
||||
@@ -129,18 +143,12 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
|
||||
public componentWillUnmount(): void {
|
||||
const event = this.props.mxEvent;
|
||||
event.localRedactionEvent()?.off(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
|
||||
this.EventContentBodyViewModel.dispose();
|
||||
this.eventContentBodyViewModel.dispose();
|
||||
this.editHistoryActionBarViewModel.dispose();
|
||||
this.messageTimestampViewModel.dispose();
|
||||
}
|
||||
|
||||
private renderActionBar(): React.ReactNode {
|
||||
this.editHistoryActionBarViewModel.setProps({
|
||||
canRemove: !this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact,
|
||||
showViewSource: SettingsStore.getValue("developerMode"),
|
||||
onRemoveClick: this.onRedactClick,
|
||||
onViewSourceClick: this.onViewSourceClick,
|
||||
});
|
||||
|
||||
return (
|
||||
<ActionBarView vm={this.editHistoryActionBarViewModel} className="mx_ThreadActionBar mx_HistoryActionBar" />
|
||||
);
|
||||
@@ -157,7 +165,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
|
||||
if (this.props.previousEdit) {
|
||||
contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content);
|
||||
} else {
|
||||
contentElements = <EventContentBodyView vm={this.EventContentBodyViewModel} as="span" />;
|
||||
contentElements = <EventContentBodyView vm={this.eventContentBodyViewModel} as="span" />;
|
||||
}
|
||||
if (mxEvent.getContent().msgtype === MsgType.Emote) {
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
@@ -177,7 +185,6 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour);
|
||||
const isSending = ["sending", "queued", "encrypting"].includes(this.state.sendStatus!);
|
||||
const classes = classNames("mx_EventTile", {
|
||||
// Note: we keep the `sending` state class for tests, not for our styles
|
||||
@@ -187,7 +194,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
|
||||
<li>
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_line">
|
||||
<span className="mx_MessageTimestamp">{timestamp}</span>
|
||||
<MessageTimestampView vm={this.messageTimestampViewModel} className="mx_MessageTimestamp" />
|
||||
{contentContainer}
|
||||
{this.renderActionBar()}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
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 from "react";
|
||||
import { act, fireEvent, render, waitFor } from "jest-matrix-react";
|
||||
import { MatrixEventEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { mkEvent, stubClient } from "../../../../test-utils";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import type { SettingKey } from "../../../../../src/settings/Settings";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import EditHistoryMessage from "../../../../../src/components/views/messages/EditHistoryMessage";
|
||||
import ConfirmAndWaitRedactDialog from "../../../../../src/components/views/dialogs/ConfirmAndWaitRedactDialog";
|
||||
import ViewSource from "../../../../../src/components/structures/ViewSource";
|
||||
import { EventContentBodyViewModel } from "../../../../../src/viewmodels/message-body/EventContentBodyViewModel";
|
||||
import { EditHistoryActionBarViewModel } from "../../../../../src/viewmodels/message-body/EditHistoryActionBarViewModel";
|
||||
import { MessageTimestampViewModel } from "../../../../../src/viewmodels/room/timeline/event-tile/timestamp/MessageTimestampViewModel";
|
||||
|
||||
describe("EditHistoryMessage", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: ReturnType<typeof stubClient>;
|
||||
let developerMode = false;
|
||||
|
||||
const renderComponent = (props: React.ComponentProps<typeof EditHistoryMessage>) => {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<EditHistoryMessage {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
developerMode = false;
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(((settingName: SettingKey) => {
|
||||
switch (settingName) {
|
||||
case "developerMode":
|
||||
return developerMode;
|
||||
case "showTwelveHourTimestamps":
|
||||
case "TextualBody.enableBigEmoji":
|
||||
return false;
|
||||
case "Pill.shouldShowPillAvatar":
|
||||
return true;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}) as typeof SettingsStore.getValue);
|
||||
jest.spyOn(Modal, "createDialog").mockReturnValue({} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders replaced content, tracks send status, and disposes view models on unmount", async () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
id: "$event",
|
||||
room: roomId,
|
||||
user: "@alice:example.com",
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Original body",
|
||||
"m.new_content": {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Edited body",
|
||||
},
|
||||
},
|
||||
});
|
||||
const localRedactionEvent = {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
};
|
||||
const getAssociatedStatus = jest.fn().mockReturnValue("sending");
|
||||
const eventContentDisposeSpy = jest.spyOn(EventContentBodyViewModel.prototype, "dispose");
|
||||
const actionBarDisposeSpy = jest.spyOn(EditHistoryActionBarViewModel.prototype, "dispose");
|
||||
const timestampDisposeSpy = jest.spyOn(MessageTimestampViewModel.prototype, "dispose");
|
||||
|
||||
(event as any).localRedactionEvent = jest.fn().mockReturnValue(localRedactionEvent);
|
||||
(event as any).getAssociatedStatus = getAssociatedStatus;
|
||||
|
||||
const { container, getByRole, getByText, unmount } = renderComponent({ mxEvent: event });
|
||||
|
||||
expect(getByText("Edited body")).toBeInTheDocument();
|
||||
expect(container.querySelector(".mx_EventTile_sending")).not.toBeNull();
|
||||
expect(getByRole("button", { name: /remove/i })).toBeInTheDocument();
|
||||
expect(localRedactionEvent.on).toHaveBeenCalledWith(MatrixEventEvent.Status, expect.any(Function));
|
||||
|
||||
getAssociatedStatus.mockReturnValue(null);
|
||||
const onStatusChanged = localRedactionEvent.on.mock.calls[0][1];
|
||||
act(() => {
|
||||
onStatusChanged();
|
||||
});
|
||||
|
||||
await waitFor(() => expect(container.querySelector(".mx_EventTile_sending")).toBeNull());
|
||||
|
||||
unmount();
|
||||
|
||||
expect(localRedactionEvent.off).toHaveBeenCalledWith(MatrixEventEvent.Status, expect.any(Function));
|
||||
expect(eventContentDisposeSpy).toHaveBeenCalled();
|
||||
expect(actionBarDisposeSpy).toHaveBeenCalled();
|
||||
expect(timestampDisposeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders diffs for previous edits and formats emotes with the sender name", () => {
|
||||
const previousEdit = mkEvent({
|
||||
event: true,
|
||||
id: "$prev",
|
||||
room: roomId,
|
||||
user: "@alice:example.com",
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: MsgType.Emote,
|
||||
body: "smiles",
|
||||
},
|
||||
});
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
id: "$event",
|
||||
room: roomId,
|
||||
user: "@alice:example.com",
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: MsgType.Emote,
|
||||
body: "waves",
|
||||
},
|
||||
});
|
||||
if (event.sender) {
|
||||
event.sender.name = "Alice";
|
||||
}
|
||||
|
||||
const { container, getByText } = renderComponent({ mxEvent: event, previousEdit });
|
||||
|
||||
expect(getByText("Alice")).toHaveClass("mx_EditHistoryMessage_emoteSender");
|
||||
expect(container.querySelector(".mx_EditHistoryMessage_deletion")).not.toBeNull();
|
||||
expect(container.querySelector(".mx_EditHistoryMessage_insertion")).not.toBeNull();
|
||||
expect(container.querySelector(".mx_EditHistoryMessage_deletion")).not.toBeEmptyDOMElement();
|
||||
expect(container.querySelector(".mx_EditHistoryMessage_insertion")).not.toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("renders redacted events and opens view source from the action bar", () => {
|
||||
developerMode = true;
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
id: "$event",
|
||||
room: roomId,
|
||||
user: "@alice:example.com",
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Removed",
|
||||
},
|
||||
unsigned: {
|
||||
redacted_because: {
|
||||
event_id: "$redaction",
|
||||
sender: "@moderator:example.com",
|
||||
type: "m.room.redaction",
|
||||
origin_server_ts: Date.UTC(2024, 0, 1, 12, 0, 0),
|
||||
content: {},
|
||||
unsigned: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
jest.spyOn(event, "isRedacted").mockReturnValue(true);
|
||||
|
||||
const { container, getByRole, queryByRole } = renderComponent({ mxEvent: event, isBaseEvent: true });
|
||||
|
||||
expect(container.querySelector(".mx_RedactedBody")).not.toBeNull();
|
||||
expect(queryByRole("button", { name: /remove/i })).toBeNull();
|
||||
|
||||
fireEvent.click(getByRole("button", { name: /view source/i }));
|
||||
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(
|
||||
ViewSource,
|
||||
{
|
||||
mxEvent: event,
|
||||
ignoreEdits: true,
|
||||
},
|
||||
"mx_Dialog_viewsource",
|
||||
);
|
||||
});
|
||||
|
||||
it("opens the redact dialog and uses the client redact callback", async () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
id: "$event",
|
||||
room: roomId,
|
||||
user: "@alice:example.com",
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Editable",
|
||||
},
|
||||
});
|
||||
|
||||
const { getByRole } = renderComponent({ mxEvent: event });
|
||||
|
||||
fireEvent.click(getByRole("button", { name: /remove/i }));
|
||||
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(
|
||||
ConfirmAndWaitRedactDialog,
|
||||
expect.objectContaining({
|
||||
event,
|
||||
redact: expect.any(Function),
|
||||
}),
|
||||
"mx_Dialog_confirmredact",
|
||||
);
|
||||
|
||||
const [, dialogProps] = (Modal.createDialog as jest.Mock).mock.calls[0];
|
||||
await dialogProps.redact();
|
||||
|
||||
expect(client.redactEvent).toHaveBeenCalledWith(roomId, "$event");
|
||||
});
|
||||
|
||||
it("updates content and timestamp view models when props change", () => {
|
||||
developerMode = true;
|
||||
const firstEventTs = Date.parse("2021-12-17T08:09:00.000Z");
|
||||
const secondEventTs = Date.parse("2021-12-17T09:09:00.000Z");
|
||||
const firstEvent = mkEvent({
|
||||
event: true,
|
||||
id: "$first",
|
||||
room: roomId,
|
||||
user: "@alice:example.com",
|
||||
type: "m.room.message",
|
||||
ts: firstEventTs,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "First",
|
||||
},
|
||||
});
|
||||
const secondEvent = mkEvent({
|
||||
event: true,
|
||||
id: "$second",
|
||||
room: roomId,
|
||||
user: "@alice:example.com",
|
||||
type: "m.room.message",
|
||||
ts: secondEventTs,
|
||||
content: {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Second",
|
||||
"m.new_content": {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Second edited",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { getByRole, getByText, queryByRole, rerender } = renderComponent({
|
||||
mxEvent: firstEvent,
|
||||
isTwelveHour: false,
|
||||
});
|
||||
|
||||
expect(getByRole("button", { name: /remove/i })).toBeInTheDocument();
|
||||
expect(getByText("08:09")).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<EditHistoryMessage mxEvent={secondEvent} isBaseEvent={true} isTwelveHour={true} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
expect(getByText("Second edited")).toBeInTheDocument();
|
||||
expect(getByText("9:09 AM")).toBeInTheDocument();
|
||||
expect(queryByRole("button", { name: /remove/i })).toBeNull();
|
||||
expect(getByRole("button", { name: /view source/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -72,7 +72,8 @@ exports[`<MessageEditHistory /> should match the snapshot 1`] = `
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
aria-live="off"
|
||||
class="mx_MessageTimestamp _content_1jaux_8"
|
||||
>
|
||||
00:00
|
||||
</span>
|
||||
@@ -212,7 +213,8 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
aria-live="off"
|
||||
class="mx_MessageTimestamp _content_1jaux_8"
|
||||
>
|
||||
00:00
|
||||
</span>
|
||||
@@ -263,7 +265,8 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
aria-live="off"
|
||||
class="mx_MessageTimestamp _content_1jaux_8"
|
||||
>
|
||||
00:00
|
||||
</span>
|
||||
@@ -325,7 +328,8 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
aria-live="off"
|
||||
class="mx_MessageTimestamp _content_1jaux_8"
|
||||
>
|
||||
00:00
|
||||
</span>
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 9.2 KiB |
@@ -7,7 +7,7 @@
|
||||
|
||||
.content {
|
||||
color: var(--cpd-color-text-secondary); /* override anchor color */
|
||||
font-size: var(--cpd-font-size-body-xs);
|
||||
font-size: 0.625rem; /* There is no Compound equivalent to $font-10px */
|
||||
font-variant-numeric: tabular-nums;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface MessageTimestampViewActions {
|
||||
* The view model for the message timestamp.
|
||||
*
|
||||
* Snapshot data describes timestamp content and rendering behavior, while
|
||||
* container styling is supplied via component props.
|
||||
* host layout and placement styling is supplied via component props.
|
||||
*/
|
||||
export type MessageTimestampViewModel = ViewModel<MessageTimestampViewSnapshot> & MessageTimestampViewActions;
|
||||
|
||||
@@ -74,11 +74,11 @@ interface MessageTimestampViewProps {
|
||||
* The view model provides the timestamp values and display options. The component
|
||||
* can render as a link when `href` is set, and can show both sent-at and received-at
|
||||
* times in the tooltip when `tsReceivedAt` is provided. Use `className` for
|
||||
* host-level styling.
|
||||
* host-level layout and placement styling.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <MessageTimestampView vm={messageTimestampViewModel} className="mx_MessageTimestamp" />
|
||||
* <MessageTimestampView vm={messageTimestampViewModel} />
|
||||
* ```
|
||||
*/
|
||||
export function MessageTimestampView({ vm, className }: Readonly<MessageTimestampViewProps>): JSX.Element {
|
||||
|
||||