From ca3bc30f90709a450811f19f84c604f99ff81d5c Mon Sep 17 00:00:00 2001 From: rbondesson Date: Fri, 20 Feb 2026 10:29:26 +0100 Subject: [PATCH] Refactor MessageTimestamp using MVVM and move to shared-components (#31988) * Create a MessageTimestampView in shared components * Switching to use shared component and view model in element-web * Add .mx_MessageTimestamp tp _common.pcss since it is used extensively in element-web * Added comments to view model * Updating after Add options for consistent screenshots * Moved rendering of late icon to EventTile * Update shared component snaps * Added I18nContext.Provider to Modal.tsx and HtmlExport.tsx to make them work with shared components * Avoid circular dependencies for ModuleApi * Adjust role and wire handlers in view model * Change to role="link" * Revert I18nContext.Provider changes * Updated snapshot * Provide I18nContext for shared-components used inside dialogs and html-export rendered in a separate root. * Add patch for react-sdk-module-api to shared components * Add setProps to MessageTimeViewModel and useEffect on wrappers * Added more tests to improve coverage * Changes after PR review * Use specific setters in the viewmodel more relating to the business logic. * Remove unused CSS properties * New snapshot after merge * Removed aria-hidden logic and display tooltips in stories * Remove await for toolitp in HasInhibitTooltip story * Add screenshots with visible tooltips * Fixes after merge and review comments * Updated snapshots for unit tests * Removed one test since tooltips are not rendered to snapshots --- .stylelintrc.cjs | 1 - .../default-auto.png | Bin 0 -> 19642 bytes .../has-actions-auto.png | Bin 0 -> 4103 bytes .../has-extra-class-names-auto.png | Bin 0 -> 17160 bytes .../has-href-auto.png | Bin 0 -> 17160 bytes .../has-inhibit-tooltip-auto.png | Bin 0 -> 17160 bytes .../has-ts-received-at-auto.png | Bin 0 -> 23294 bytes ...atrix-org+react-sdk-module-api+2.5.0.patch | 99 ++++++++ .../src/i18n/strings/en_EN.json | 4 +- packages/shared-components/src/index.ts | 1 + .../MessageTimestampView.module.css | 16 ++ .../MessageTimestampView.stories.tsx | 80 ++++++ .../MessageTimestampView.test.tsx | 231 ++++++++++++++++++ .../MessageTimestampView.tsx | 140 +++++++++++ .../MessageTimestampView.test.tsx.snap | 37 +++ .../MessageTimestampView/index.tsx | 14 ++ res/css/_common.pcss | 15 ++ res/css/_components.pcss | 1 - res/css/views/messages/_MessageTimestamp.pcss | 32 --- res/css/views/rooms/_EventBubbleTile.pcss | 2 +- res/css/views/rooms/_EventTile.pcss | 7 + src/Modal.tsx | 55 +++-- src/components/views/elements/ImageView.tsx | 28 ++- .../views/messages/MessageTimestamp.tsx | 111 --------- src/components/views/rooms/EventTile.tsx | 46 +++- src/i18n/strings/en_EN.json | 2 - src/utils/exportUtils/HtmlExport.tsx | 58 +++-- .../message-body/MessageTimestampViewModel.ts | 173 +++++++++++++ .../views/messages/MessageTimestamp-test.tsx | 55 ----- .../__snapshots__/MImageBody-test.tsx.snap | 2 +- .../utils/exportUtils/HTMLExport-test.ts | 2 +- .../__snapshots__/HTMLExport-test.ts.snap | 2 +- .../MessageTimestampViewModel-test.tsx | 123 ++++++++++ 33 files changed, 1072 insertions(+), 265 deletions(-) create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/default-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/has-actions-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/has-extra-class-names-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/has-href-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/has-inhibit-tooltip-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/has-ts-received-at-auto.png create mode 100644 packages/shared-components/patches/@matrix-org+react-sdk-module-api+2.5.0.patch create mode 100644 packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.module.css create mode 100644 packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx create mode 100644 packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.test.tsx create mode 100644 packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.tsx create mode 100644 packages/shared-components/src/message-body/MessageTimestampView/__snapshots__/MessageTimestampView.test.tsx.snap create mode 100644 packages/shared-components/src/message-body/MessageTimestampView/index.tsx delete mode 100644 res/css/views/messages/_MessageTimestamp.pcss delete mode 100644 src/components/views/messages/MessageTimestamp.tsx create mode 100644 src/viewmodels/message-body/MessageTimestampViewModel.ts delete mode 100644 test/unit-tests/components/views/messages/MessageTimestamp-test.tsx create mode 100644 test/viewmodels/message-body/MessageTimestampViewModel-test.tsx diff --git a/.stylelintrc.cjs b/.stylelintrc.cjs index 2cbebc0472..97b6e53f31 100644 --- a/.stylelintrc.cjs +++ b/.stylelintrc.cjs @@ -55,7 +55,6 @@ module.exports = { { from: "res/css/views/rooms/_ReadReceiptGroup.pcss", type: "css" }, { from: "res/css/views/rooms/_EditMessageComposer.pcss", type: "css" }, { from: "res/css/views/right_panel/_BaseCard.pcss", type: "css" }, - { from: "res/css/views/messages/_MessageTimestamp.pcss", type: "css" }, { from: "res/css/views/messages/_MessageActionBar.pcss", type: "css" }, { from: "res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss", type: "css" }, { from: "res/css/views/elements/_ToggleSwitch.pcss", type: "css" }, diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/default-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..0b144acf0d9be53353b002e3a130c462dd68a9fc GIT binary patch literal 19642 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 literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/has-actions-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/has-actions-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..652780c8aa79e4499ff43eca0df0d7a2d622761c GIT binary patch literal 4103 zcmeAS@N?(olHy`uVBq!ia0y~yU_QXWz;uCw2`F+wwC^zk1OFaR7srr_IdATIt_V++ zX?Xa0@AgHEUZ>MGXP7L`5R^E}>KI_6eaS;`ZC0qjmJD&-DA(B?N=i~W8%`c;6-Z%8 z%LtN+Qk$08^|ZwJ@@@G%7w`LDYv&z(c-->-&HXhsbBwL*?Ce+=7~agksK~&Ou%9!7 zfq{d?gn@yDNebu`MqUPn#s)TqhJ=#qH*S2^-}mkKwzuy7_OIqyKkMu1;d!U`^V901 z^K~n~JY-iBdwcKib>)8B!Z(JO**}L)k1u)mCi48=AFn1}UiNQW<>}PvXDrjN&2(m9 zFtF^Y`*-xx&CTcbJi5evzv}y9QSI=#_4nSqyAsuf1Z?WLdiSd>w5p6-F_doUho0~gOA){>AIU0$1<1mEW7M)Tlu$b zKKtRO*6zI7<%f?v&R)0g(Wma`J-vT-7Cv4Uey`&B*X`oG-&XZoe|y2~=03mn;Z%OR z*qX0Xr}Hr~99Uxb=f25D??b7l!}+{^oyZLSzdz&F7UgGmYwrGuUT6Px%FOk9f8Ja5 z{`$K+J73?kety^P`!QDT`|?Z-4W7vr$2yO%kKUhX@OMMr-!qf_VzySDowjzre$D^K zXQQ|2{jEBBWKr(z@;6e_(mcm~?6dCgtF8X_#`4Vu=XQ3tw(tM;mUA#LsP$?#8`V`F zv~GTVMVW7Y{p+vFhyTCxwEua!$l0vtG^ zeN-7*J{S#<(Eu6EGLSl9G%t+ih0(k)nisG(7)J|-(ZXT0a2PEdMsDG7&>q-}|NsBh ik`8$wgALS=XJ+{HZoB3M_ZPo`A`G6celF{r5}E*}2>WjU literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/has-extra-class-names-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/MessageTimestampView/MessageTimestampView.stories.tsx/has-extra-class-names-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..7aad2b67b5ccdd75d568cb207c922f331b246ae2 GIT binary patch literal 17160 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 = DialogContent

>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject) => React.ReactNode, props?: Omit): Promise<{ ++ openDialog = DialogContent

>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject) => React.ReactNode, props?: Omit): Promise<{ + didOkOrSubmit: boolean; + model: M; + }>; +diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts +index cb5f2e5..51daa51 100644 +--- a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts ++++ b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts +@@ -66,23 +66,23 @@ export interface SetupEncryptionStoreProjection { + export interface ProvideCryptoSetupExtensions { + examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void; + persistCredentials(credentials: ExtendedMatrixClientCreds): void; +- getSecretStorageKey(): Uint8Array | null; +- createSecretStorageKey(): Uint8Array | null; ++ getSecretStorageKey(): Uint8Array | null; ++ createSecretStorageKey(): Uint8Array | null; + catchAccessSecretStorageError(e: Error): void; + setupEncryptionNeeded: (args: CryptoSetupArgs) => boolean; + /** @deprecated This callback is no longer used by matrix-react-sdk */ +- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise) | null; ++ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise>) | null; + SHOW_ENCRYPTION_SETUP_UI: boolean; + } + export declare abstract class CryptoSetupExtensionsBase implements ProvideCryptoSetupExtensions { + abstract examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void; + abstract persistCredentials(credentials: ExtendedMatrixClientCreds): void; +- abstract getSecretStorageKey(): Uint8Array | null; +- abstract createSecretStorageKey(): Uint8Array | null; ++ abstract getSecretStorageKey(): Uint8Array | null; ++ abstract createSecretStorageKey(): Uint8Array | null; + abstract catchAccessSecretStorageError(e: Error): void; + abstract setupEncryptionNeeded(args: CryptoSetupArgs): boolean; + /** `getDehydrationKeyCallback` is no longer used; we provide an empty impl for type compatibility. */ +- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise) | null; ++ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise>) | null; + abstract SHOW_ENCRYPTION_SETUP_UI: boolean; + } + export interface CryptoSetupArgs { +@@ -98,9 +98,9 @@ export declare class DefaultCryptoSetupExtensions extends CryptoSetupExtensionsB + SHOW_ENCRYPTION_SETUP_UI: boolean; + examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void; + persistCredentials(credentials: ExtendedMatrixClientCreds): void; +- getSecretStorageKey(): Uint8Array | null; +- createSecretStorageKey(): Uint8Array | null; ++ getSecretStorageKey(): Uint8Array | null; ++ createSecretStorageKey(): Uint8Array | null; + catchAccessSecretStorageError(e: Error): void; + setupEncryptionNeeded(args: CryptoSetupArgs): boolean; +- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise) | null; ++ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise>) | null; + } +diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js +index 5d422ed..011c19f 100644 +--- a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js ++++ b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js +@@ -124,34 +124,28 @@ var DefaultCryptoSetupExtensions = /*#__PURE__*/function (_CryptoSetupExtension) + (0, _createClass2["default"])(DefaultCryptoSetupExtensions, [{ + key: "examineLoginResponse", + value: function examineLoginResponse(response, credentials) { +- console.log("Default empty examineLoginResponse() => void"); + } + }, { + key: "persistCredentials", + value: function persistCredentials(credentials) { +- console.log("Default empty persistCredentials() => void"); + } + }, { + key: "getSecretStorageKey", + value: function getSecretStorageKey() { +- console.log("Default empty getSecretStorageKey() => null"); + return null; + } + }, { + key: "createSecretStorageKey", + value: function createSecretStorageKey() { +- console.log("Default empty createSecretStorageKey() => null"); + return null; + } + }, { + key: "catchAccessSecretStorageError", + value: function catchAccessSecretStorageError(e) { +- console.log("Default catchAccessSecretStorageError() => void"); + } + }, { + key: "setupEncryptionNeeded", + value: function setupEncryptionNeeded(args) { +- console.log("Default setupEncryptionNeeded() => false"); + return false; + } + }, { diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json index cab1f9b45b..b9bd1610ac 100644 --- a/packages/shared-components/src/i18n/strings/en_EN.json +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -171,7 +171,9 @@ "parameters_changed": "Some encryption parameters have been changed.", "state_enabled": "Messages and state events in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their profile picture.", "unsupported": "The encryption used by this room isn't supported." - } + }, + "message_timestamp_received_at": "Received at: %(dateTime)s", + "message_timestamp_sent_at": "Sent at: %(dateTime)s" }, "widget": { "context_menu": { diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index 64b92ade5b..f1ed7d0c08 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -17,6 +17,7 @@ export * from "./event-tiles/EncryptionEventView"; export * from "./event-tiles/EventTileBubble"; export * from "./event-tiles/TextualEventView"; export * from "./message-body/MediaBody"; +export * from "./message-body/MessageTimestampView"; export * from "./message-body/DecryptionFailureBodyView"; export * from "./message-body/ReactionsRowButtonTooltip"; export * from "./message-body/TimelineSeparator/"; diff --git a/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.module.css b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.module.css new file mode 100644 index 0000000000..6ee9d62aa1 --- /dev/null +++ b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.module.css @@ -0,0 +1,16 @@ +/* + * 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. + */ + +.content { + color: var(--cpd-color-text-secondary) !important; /* override anchor color */ + font-size: var(--cpd-font-size-body-xs); + font-variant-numeric: tabular-nums; + display: inline-block; + white-space: nowrap; + user-select: none; + text-decoration: none; +} diff --git a/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx new file mode 100644 index 0000000000..e512012a57 --- /dev/null +++ b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx @@ -0,0 +1,80 @@ +/* + * 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, { type ReactNode } from "react"; +import { expect, userEvent, within } from "storybook/test"; + +import type { Meta, StoryFn } from "@storybook/react-vite"; +import { + MessageTimestampView, + type MessageTimestampViewActions, + type MessageTimestampViewSnapshot, +} from "./MessageTimestampView"; +import { useMockedViewModel } from "../../viewmodel/useMockedViewModel"; + +type MessageTimestampProps = MessageTimestampViewSnapshot & MessageTimestampViewActions; +const MessageTimestampWrapper = ({ onClick, onContextMenu, ...rest }: MessageTimestampProps): ReactNode => { + const vm = useMockedViewModel(rest, { + onClick, + onContextMenu, + }); + return ; +}; + +export default { + title: "MessageBody/MessageTimestamp", + component: MessageTimestampWrapper, + tags: ["autodocs"], + args: { + ts: "04:58", + tsSentAt: "Thu, 17 Nov 2022, 4:58:32 pm", + tsReceivedAt: "", + inhibitTooltip: false, + className: "", + href: "", + }, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); +Default.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.hover(canvas.getByText("04:58")); + await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument(); +}; + +export const HasTsReceivedAt = Template.bind({}); +HasTsReceivedAt.args = { + tsReceivedAt: "Thu, 17 Nov 2022, 4:58:33 pm", +}; +HasTsReceivedAt.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.hover(canvas.getByText("04:58")); + await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument(); +}; + +export const HasInhibitTooltip = Template.bind({}); +HasInhibitTooltip.args = { + inhibitTooltip: true, +}; + +export const HasExtraClassNames = Template.bind({}); +HasExtraClassNames.args = { + className: "extra_class_1 extra_class_2", +}; + +export const HasHref = Template.bind({}); +HasHref.args = { + href: "~", +}; + +export const HasActions = Template.bind({}); +HasActions.args = { + onClick: () => console.log("Clicked message timestamp"), + onContextMenu: () => console.log("Context menu on message timestamp"), +}; diff --git a/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.test.tsx b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.test.tsx new file mode 100644 index 0000000000..f51b7ecc8b --- /dev/null +++ b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.test.tsx @@ -0,0 +1,231 @@ +/* + * 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 { render, screen, fireEvent } from "@test-utils"; +import { composeStories } from "@storybook/react-vite"; +import React from "react"; +import userEvent from "@testing-library/user-event"; +import { describe, it, vi, afterEach, expect } from "vitest"; + +import * as stories from "./MessageTimestampView.stories.tsx"; +import { + MessageTimestampView, + type MessageTimestampViewActions, + type MessageTimestampViewSnapshot, +} from "./MessageTimestampView"; +import { MockViewModel } from "../../viewmodel/MockViewModel.ts"; +import { I18nContext } from "../../utils/i18nContext.ts"; +import { I18nApi } from "../../index.ts"; + +const { Default, HasHref, HasExtraClassNames } = composeStories(stories); + +const renderWithI18n = (ui: React.ReactElement): ReturnType => + render(ui, { + wrapper: ({ children }) => {children}, + }); + +describe("MessageTimestampView", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("renders the message timestamp in default state", async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders the message timestamp with extra class names", async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders the message timestamp with href", async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + const onClick = vi.fn((event: React.MouseEvent) => event.preventDefault()); + const onContextMenu = vi.fn((event: React.MouseEvent) => event.preventDefault()); + + class MessageTimestampViewModel + extends MockViewModel + implements MessageTimestampViewActions + { + public onClick = onClick; + public onContextMenu = onContextMenu; + } + + it("should attach vm methods with href", async () => { + const vm = new MessageTimestampViewModel({ + ts: "04:58", + tsSentAt: "Thu, 17 Nov 2022, 4:58:32 pm", + href: "~", + }); + + renderWithI18n(); + + const target = screen.getByRole("link"); + + fireEvent.click(target); + expect(onClick).toHaveBeenCalled(); + + fireEvent.contextMenu(target); + expect(onContextMenu).toHaveBeenCalled(); + }); + + it("should attach vm methods without href", async () => { + const user = userEvent.setup(); + const vm = new MessageTimestampViewModel({ + ts: "04:58", + tsSentAt: "Thu, 17 Nov 2022, 4:58:32 pm", + }); + + renderWithI18n(); + + const target = screen.getByRole("link", { hidden: true }); + + await user.click(target); + expect(onClick).toHaveBeenCalled(); + + await user.pointer({ target, keys: "[MouseRight]" }); + expect(onContextMenu).toHaveBeenCalled(); + }); + + it("should show full date & time on hover", async () => { + const user = userEvent.setup(); + const vm = new MessageTimestampViewModel({ + ts: "08:09", + tsSentAt: "Fri, Dec 17, 2021, 08:09:00", + }); + + renderWithI18n(); + + await user.hover(screen.getByRole("link")); + expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot(`"Fri, Dec 17, 2021, 08:09:00"`); + }); + + it("should show sent & received time on hover if passed", async () => { + const user = userEvent.setup(); + const vm = new MessageTimestampViewModel({ + ts: "08:09", + tsSentAt: "Fri, Dec 17, 2021, 08:09:00", + tsReceivedAt: "Received at: Sat, Dec 18, 2021, 08:09:00", + }); + + renderWithI18n(); + + await user.hover(screen.getByRole("link")); + expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot( + `"Sent at: Fri, Dec 17, 2021, 08:09:00Received at: Received at: Sat, Dec 18, 2021, 08:09:00"`, + ); + }); + + it("handles keyboard activation on span when click handler is set", async () => { + const vm = new MessageTimestampViewModel({ + ts: "12:34", + tsSentAt: "Mon, Jan 1, 2024, 12:34:00", + }); + + renderWithI18n(); + + const target = screen.getByRole("link"); + fireEvent.keyDown(target, { key: "Enter" }); + fireEvent.keyDown(target, { key: " " }); + + expect(onClick).toHaveBeenCalledTimes(2); + }); + + it("ignores other keys when click handler is set", async () => { + const vm = new MessageTimestampViewModel({ + ts: "13:14", + tsSentAt: "Tue, Jun 6, 2023, 13:14:00", + }); + + renderWithI18n(); + + const target = screen.getByRole("link"); + fireEvent.keyDown(target, { key: "Escape" }); + + expect(onClick).not.toHaveBeenCalled(); + }); + + it("ignores keyboard activation when no click handler is provided", async () => { + const vm = new MessageTimestampViewModelNoActions({ + ts: "15:16", + tsSentAt: "Wed, Jul 7, 2021, 15:16:00", + }); + + renderWithI18n(); + + const target = screen.getByText("15:16"); + fireEvent.keyDown(target, { key: "Enter" }); + + expect(onClick).not.toHaveBeenCalled(); + }); + + it("does not wrap tooltip labels when received timestamp is empty", async () => { + const user = userEvent.setup(); + const vm = new MessageTimestampViewModel({ + ts: "09:10", + tsSentAt: "Tue, Feb 2, 2021, 09:10:00", + tsReceivedAt: "", + }); + + renderWithI18n(); + + await user.hover(screen.getByRole("link")); + expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot(`"Tue, Feb 2, 2021, 09:10:00"`); + }); + + class MessageTimestampViewModelNoActions extends MockViewModel {} + + it("renders without tooltip when inhibited and no click handler is provided", async () => { + const vm = new MessageTimestampViewModelNoActions({ + ts: "07:08", + tsSentAt: "Wed, Mar 3, 2021, 07:08:00", + inhibitTooltip: true, + }); + + renderWithI18n(); + + const target = screen.getByText("07:08"); + expect(target).not.toHaveAttribute("role"); + expect(target).not.toHaveAttribute("tabindex"); + expect(screen.queryByRole("tooltip")).toBeNull(); + }); + + it("keeps link semantics when inhibited but click handler exists", async () => { + const vm = new MessageTimestampViewModel({ + ts: "11:12", + tsSentAt: "Thu, Apr 4, 2024, 11:12:00", + inhibitTooltip: true, + }); + + renderWithI18n(); + + const target = screen.getByRole("link"); + expect(target).toHaveAttribute("tabindex", "0"); + expect(screen.queryByRole("tooltip")).toBeNull(); + }); + + it("exposes focusable span when tooltip is enabled without click handler", async () => { + const user = userEvent.setup(); + const vm = new MessageTimestampViewModelNoActions({ + ts: "03:04", + tsSentAt: "Fri, May 5, 2023, 03:04:00", + }); + + renderWithI18n(); + + const target = screen.getByText("03:04"); + expect(target).toHaveAttribute("tabindex", "0"); + expect(target).not.toHaveAttribute("role"); + + await user.hover(target); + expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot(`"Fri, May 5, 2023, 03:04:00"`); + }); +}); diff --git a/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.tsx b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.tsx new file mode 100644 index 0000000000..7ddb4a082c --- /dev/null +++ b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.tsx @@ -0,0 +1,140 @@ +/* + * 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, { type JSX, type MouseEventHandler, type KeyboardEvent, type MouseEvent } from "react"; +import classNames from "classnames"; +import { Tooltip } from "@vector-im/compound-web"; + +import styles from "./MessageTimestampView.module.css"; +import { type ViewModel } from "../../viewmodel/ViewModel"; +import { useViewModel } from "../../viewmodel/useViewModel"; +import { useI18n } from "../../utils/i18nContext"; + +export interface MessageTimestampViewSnapshot { + /** + * The localized timestamp to render in the component + */ + ts: string; + /** + * The localized sent timestamp formatted as full date + */ + tsSentAt: string; + /** + * The localized received timestamp formatted as full date + * If specified will render both the sent-at and received-at timestamps in the tooltip + */ + tsReceivedAt?: string; + /** + * If set to true then no tooltip will be shown + */ + inhibitTooltip?: boolean; + /** + * Extra class name to apply to the component + */ + className?: string; + /** + * If specified, will be rendered as an anchor bearing the href, a `span` element will be used otherwise + */ + href?: string; +} + +export interface MessageTimestampViewActions { + /** + * Optional onClick handler to attach to the DOM element + */ + onClick?: MouseEventHandler; + /** + * Optional onContextMenu handler to attach to the DOM element + */ + onContextMenu?: MouseEventHandler; +} + +/** + * The view model for the message timestamp. + */ +export type MessageTimestampViewModel = ViewModel & MessageTimestampViewActions; + +interface MessageTimestampViewProps { + /** + * The view model for the message timestamp. + */ + vm: MessageTimestampViewModel; +} + +/** + * Displays a message timestamp with optional tooltip details. + * + * 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. + * + * @example + * ```tsx + * + * ``` + */ +export function MessageTimestampView({ vm }: Readonly): JSX.Element { + const { translate: _t } = useI18n(); + + const { ts, tsSentAt, tsReceivedAt, inhibitTooltip, className, href } = useViewModel(vm); + + const onKeyDown = (event: KeyboardEvent): void => { + if (vm.onClick) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + vm.onClick?.(event as unknown as MouseEvent); + } + } + }; + + let label = tsSentAt; + let caption: string | undefined; + if (tsReceivedAt && tsReceivedAt?.length > 0) { + label = _t("timeline|message_timestamp_sent_at", { dateTime: label }); + caption = _t("timeline|message_timestamp_received_at", { + dateTime: tsReceivedAt, + }); + } + + let content; + if (href) { + content = ( + + {ts} + + ); + } else { + content = ( + + {ts} + + ); + } + + if (inhibitTooltip) return content; + + return ( + + {content} + + ); +} diff --git a/packages/shared-components/src/message-body/MessageTimestampView/__snapshots__/MessageTimestampView.test.tsx.snap b/packages/shared-components/src/message-body/MessageTimestampView/__snapshots__/MessageTimestampView.test.tsx.snap new file mode 100644 index 0000000000..67ae66820e --- /dev/null +++ b/packages/shared-components/src/message-body/MessageTimestampView/__snapshots__/MessageTimestampView.test.tsx.snap @@ -0,0 +1,37 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`MessageTimestampView > renders the message timestamp in default state 1`] = ` +

+ + 04:58 + +
+`; + +exports[`MessageTimestampView > renders the message timestamp with extra class names 1`] = ` +
+ + 04:58 + +
+`; + +exports[`MessageTimestampView > renders the message timestamp with href 1`] = ` + +`; diff --git a/packages/shared-components/src/message-body/MessageTimestampView/index.tsx b/packages/shared-components/src/message-body/MessageTimestampView/index.tsx new file mode 100644 index 0000000000..7c2e6fa440 --- /dev/null +++ b/packages/shared-components/src/message-body/MessageTimestampView/index.tsx @@ -0,0 +1,14 @@ +/* + * 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. + */ + +export type { + MessageTimestampViewModel, + MessageTimestampViewSnapshot, + MessageTimestampViewActions, +} from "./MessageTimestampView"; + +export { MessageTimestampView } from "./MessageTimestampView"; diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 736c535d65..3acbdffc01 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -23,6 +23,7 @@ Please see LICENSE files in the repository root for full details. --buttons-dialog-gap-column: $spacing-8; --MBody-border-radius: 8px; --EventTileBubble_margin-block: 10px; + --MessageTimestamp-width: 46px; /* 8 + 30 (avatar) + 8 */ /* Expected z-indexes for dialogs: 4000 - Default wrapper index @@ -908,3 +909,17 @@ legend { -webkit-line-clamp: var(--mx-line-clamp, 1); 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 */ +.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; +} diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 53f99f8a35..32757abf20 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -238,7 +238,6 @@ @import "./views/messages/_MVideoBody.pcss"; @import "./views/messages/_MediaBody.pcss"; @import "./views/messages/_MessageActionBar.pcss"; -@import "./views/messages/_MessageTimestamp.pcss"; @import "./views/messages/_MjolnirBody.pcss"; @import "./views/messages/_PinnedMessageBadge.pcss"; @import "./views/messages/_ReactionsRow.pcss"; diff --git a/res/css/views/messages/_MessageTimestamp.pcss b/res/css/views/messages/_MessageTimestamp.pcss deleted file mode 100644 index d5dda3272d..0000000000 --- a/res/css/views/messages/_MessageTimestamp.pcss +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2015, 2016 OpenMarket 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. -*/ - -:root { - --MessageTimestamp-width: 46px; /* 8 + 30 (avatar) + 8 */ - --MessageTimestamp-max-width: 80px; - --MessageTimestamp-color: $event-timestamp-color; -} - -.mx_MessageTimestamp { - color: var(--MessageTimestamp-color) !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; -} - -.mx_MessageTimestamp_lateIcon { - position: absolute; - right: 100%; - top: 50%; - transform: translateY(-50%); - color: inherit; -} diff --git a/res/css/views/rooms/_EventBubbleTile.pcss b/res/css/views/rooms/_EventBubbleTile.pcss index 24cd126728..d7c88982db 100644 --- a/res/css/views/rooms/_EventBubbleTile.pcss +++ b/res/css/views/rooms/_EventBubbleTile.pcss @@ -38,7 +38,7 @@ Please see LICENSE files in the repository root for full details. .mx_MessageTimestamp { width: unset; /* Cancel the default width */ - max-width: var(--MessageTimestamp-max-width); + max-width: 80px; } .mx_ThreadSummary { diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index cc532079b6..59bb2d23d0 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -66,6 +66,13 @@ $left-gutter: 64px; } } + .mx_MessageTimestamp_lateIcon { + position: absolute; + right: 100%; + top: var(--cpd-space-1x); + color: var(--cpd-color-text-secondary); + } + .mx_EventTileBubble { margin-block: var(--EventTileBubble_margin-block); min-width: 100px; diff --git a/src/Modal.tsx b/src/Modal.tsx index e2873783ea..26d04eb197 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -12,6 +12,7 @@ import { createRoot, type Root } from "react-dom/client"; import classNames from "classnames"; import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; import { Glass, TooltipProvider } from "@vector-im/compound-web"; +import { I18nContext } from "@element-hq/web-shared-components"; import defaultDispatcher from "./dispatcher/dispatcher"; import AsyncWrapper from "./AsyncWrapper"; @@ -436,18 +437,21 @@ export class ModalManager extends TypedEventEmitter - -
- -
{this.staticModal.elem}
-
-
-
- + {/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */} + + +
+ +
{this.staticModal.elem}
+
+
+
+ + ); @@ -465,18 +469,21 @@ export class ModalManager extends TypedEventEmitter - -
- -
{modal.elem}
-
-
-
- + {/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */} + + +
+ +
{modal.elem}
+
+
+
+ + ); diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index ac13f04bc3..983cd42575 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -20,13 +20,13 @@ import { ZoomInIcon, ZoomOutIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { useCreateAutoDisposedViewModel, MessageTimestampView } from "@element-hq/web-shared-components"; import { _t } from "../../../languageHandler"; import MemberAvatar from "../avatars/MemberAvatar"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import MessageContextMenu from "../context_menus/MessageContextMenu"; import { aboveLeftOf } from "../../structures/ContextMenu"; -import MessageTimestamp from "../messages/MessageTimestamp"; import SettingsStore from "../../../settings/SettingsStore"; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; @@ -39,6 +39,10 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { presentableTextForFile } from "../../../utils/FileUtils"; import AccessibleButton from "./AccessibleButton"; import { useDownloadMedia } from "../../../hooks/useDownloadMedia.ts"; +import { + MessageTimestampViewModel, + type MessageTimestampViewModelProps, +} from "../../../viewmodels/message-body/MessageTimestampViewModel.ts"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -465,7 +469,7 @@ export default class ImageView extends React.Component { const senderName = mxEvent.sender?.name ?? mxEvent.getSender(); const sender =
{senderName}
; const messageTimestamp = ( - = ({ url, fileName, m ); }; + +/** + * Wraps MessageTimestampView with a view model synced to the provided props. + * This wrapper can be removed after ImageView has been changed to a function component. + */ +function MessageTimestampWrapper(props: MessageTimestampViewModelProps): JSX.Element { + const vm = useCreateAutoDisposedViewModel(() => new MessageTimestampViewModel(props)); + useEffect(() => { + vm.setTimestamp(props.ts); + vm.setDisplayOptions({ + showTwelveHour: props.showTwelveHour, + showFullDate: props.showFullDate, + showSeconds: props.showSeconds, + }); + vm.setTooltipInhibited(props.inhibitTooltip); + vm.setHref(props.href); + vm.setHandlers({ onClick: props.onClick }); + }, [vm, props]); + return ; +} diff --git a/src/components/views/messages/MessageTimestamp.tsx b/src/components/views/messages/MessageTimestamp.tsx deleted file mode 100644 index c2f26f1ffc..0000000000 --- a/src/components/views/messages/MessageTimestamp.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2015, 2016 OpenMarket 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, { type ReactNode } from "react"; -import { Tooltip } from "@vector-im/compound-web"; - -import { formatFullDate, formatTime, formatFullTime, formatRelativeTime } from "../../../DateUtils"; -import { _t } from "../../../languageHandler"; -import { Icon as LateIcon } from "../../../../res/img/sensor.svg"; - -interface IProps { - ts: number; - /** - * If specified will render both the sent-at and received-at timestamps in the tooltip - */ - receivedTs?: number; - showTwelveHour?: boolean; - showFullDate?: boolean; - showSeconds?: boolean; - showRelative?: boolean; - - /** - * If set to true then no tooltip will be shown - */ - inhibitTooltip?: boolean; - - /** - * If specified, will be rendered as an anchor bearing the href, a `span` element will be used otherwise - */ - href?: string; - /** - * Optional onClick handler to attach to the DOM element - */ - onClick?: React.MouseEventHandler; - /** - * Optional onContextMenu handler to attach to the DOM element - */ - onContextMenu?: React.MouseEventHandler; -} - -export default class MessageTimestamp extends React.Component { - public render(): React.ReactNode { - const date = new Date(this.props.ts); - let timestamp: string; - if (this.props.showRelative) { - timestamp = formatRelativeTime(date, this.props.showTwelveHour); - } else if (this.props.showFullDate) { - timestamp = formatFullDate(date, this.props.showTwelveHour, this.props.showSeconds); - } else if (this.props.showSeconds) { - timestamp = formatFullTime(date, this.props.showTwelveHour); - } else { - timestamp = formatTime(date, this.props.showTwelveHour); - } - - let label = formatFullDate(date, this.props.showTwelveHour); - let caption: string | undefined; - let icon: ReactNode | undefined; - if (this.props.receivedTs !== undefined) { - label = _t("timeline|message_timestamp_sent_at", { dateTime: label }); - const receivedDate = new Date(this.props.receivedTs); - caption = _t("timeline|message_timestamp_received_at", { - dateTime: formatFullDate(receivedDate, this.props.showTwelveHour), - }); - icon = ; - } - - let content; - if (this.props.href) { - content = ( - - {icon} - {timestamp} - - ); - } else { - content = ( - - {icon} - {timestamp} - - ); - } - - if (this.props.inhibitTooltip) return content; - - return ( - - {content} - - ); - } -} diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 6d1ed70a06..47741d3abb 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -36,7 +36,11 @@ import { import { Tooltip } from "@vector-im/compound-web"; import { uniqueId } from "lodash"; import { CircleIcon, CheckCircleIcon, ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; -import { useCreateAutoDisposedViewModel, DecryptionFailureBodyView } from "@element-hq/web-shared-components"; +import { + useCreateAutoDisposedViewModel, + DecryptionFailureBodyView, + MessageTimestampView, +} from "@element-hq/web-shared-components"; import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; import ReplyChain from "../elements/ReplyChain"; @@ -58,7 +62,6 @@ import { Action } from "../../../dispatcher/actions"; import PlatformPeg from "../../../PlatformPeg"; import MemberAvatar from "../avatars/MemberAvatar"; import SenderProfile from "../messages/SenderProfile"; -import MessageTimestamp from "../messages/MessageTimestamp"; import { type IReadReceiptPosition } from "./ReadReceiptMarker"; import MessageActionBar from "../messages/MessageActionBar"; import ReactionsRow from "../messages/ReactionsRow"; @@ -81,6 +84,7 @@ import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge"; import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar"; import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper"; +import { Icon as LateIcon } from "../../../../res/img/sensor.svg"; import PinningUtils from "../../../utils/PinningUtils"; import { PinnedMessageBadge } from "../messages/PinnedMessageBadge"; import { EventPreview } from "./EventPreview"; @@ -88,6 +92,10 @@ import { ElementCallEventType } from "../../../call-types"; import { DecryptionFailureBodyViewModel } from "../../../viewmodels/message-body/DecryptionFailureBodyViewModel"; import { E2eMessageSharedIcon } from "./EventTile/E2eMessageSharedIcon.tsx"; import { E2ePadlock, E2ePadlockIcon } from "./EventTile/E2ePadlock.tsx"; +import { + MessageTimestampViewModel, + type MessageTimestampViewModelProps, +} from "../../../viewmodels/message-body/MessageTimestampViewModel.ts"; export type GetRelationsForEvent = ( eventId: string, @@ -1157,15 +1165,15 @@ export class UnwrappedEventTile extends React.Component ts = this.props.mxEvent.getTs(); } - const messageTimestampProps = { + const messageTimestampProps: MessageTimestampViewModelProps = { showRelative: this.context.timelineRenderingType === TimelineRenderingType.ThreadsList, showTwelveHour: this.props.isTwelveHour, ts, receivedTs: getLateEventInfo(this.props.mxEvent)?.received_ts, }; - const messageTimestamp = ; + const messageTimestamp = ; const linkedMessageTimestamp = ( - ; } + +/** + * Wraps MessageTimestampView with a view model synced to the provided props. + * This wrapper can be removed after EventTile has been changed to a function component. + */ +function MessageTimestampWrapper(props: MessageTimestampViewModelProps): JSX.Element { + const vm = useCreateAutoDisposedViewModel(() => new MessageTimestampViewModel(props)); + useEffect(() => { + vm.setTimestamp(props.ts); + vm.setReceivedTimestamp(props.receivedTs); + vm.setDisplayOptions({ + showTwelveHour: props.showTwelveHour, + showRelative: props.showRelative, + }); + vm.setHref(props.href); + vm.setHandlers({ onClick: props.onClick, onContextMenu: props.onContextMenu }); + }, [vm, props]); + + return ( + <> + {/* Render icon as described in, https://github.com/matrix-org/matrix-react-sdk/pull/11760 */} + {props.receivedTs ? ( + + ) : undefined} + + + ); +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cb57da8eeb..f31931b7b0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3553,8 +3553,6 @@ "label": "Message Actions", "view_in_room": "View in room" }, - "message_timestamp_received_at": "Received at: %(dateTime)s", - "message_timestamp_sent_at": "Sent at: %(dateTime)s", "mjolnir": { "changed_rule_glob": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", "changed_rule_rooms": "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 20b7b681ac..cb5c3629c8 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -13,6 +13,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { logger } from "matrix-js-sdk/src/logger"; import escapeHtml from "escape-html"; import { TooltipProvider } from "@vector-im/compound-web"; +import { I18nContext } from "@element-hq/web-shared-components"; import Exporter from "./Exporter"; import { mediaFromMxc } from "../../customisations/Media"; @@ -267,33 +268,36 @@ export default class HTMLExporter extends Exporter { public getEventTile(mxEv: MatrixEvent, continuation: boolean, ref?: () => void): JSX.Element { return (
- - - - false} - isTwelveHour={false} - last={false} - lastInSection={false} - permalinkCreator={this.permalinkCreator} - lastSuccessful={false} - isSelectedEvent={false} - showReactions={true} - layout={Layout.Group} - showReadReceipts={false} - getRelationsForEvent={this.getRelationsForEvent} - ref={ref} - /> - - - + {/* Export rendering uses an isolated root, so provide I18nContext explicitly. */} + + + + + false} + isTwelveHour={false} + last={false} + lastInSection={false} + permalinkCreator={this.permalinkCreator} + lastSuccessful={false} + isSelectedEvent={false} + showReactions={true} + layout={Layout.Group} + showReadReceipts={false} + getRelationsForEvent={this.getRelationsForEvent} + ref={ref} + /> + + + +
); } diff --git a/src/viewmodels/message-body/MessageTimestampViewModel.ts b/src/viewmodels/message-body/MessageTimestampViewModel.ts new file mode 100644 index 0000000000..8d9299ba88 --- /dev/null +++ b/src/viewmodels/message-body/MessageTimestampViewModel.ts @@ -0,0 +1,173 @@ +/* + * 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 MouseEventHandler } from "react"; +import { + BaseViewModel, + type MessageTimestampViewSnapshot as MessageTimestampViewSnapshotInterface, + type MessageTimestampViewModel as MessageTimestampViewModelInterface, +} from "@element-hq/web-shared-components"; + +import { formatFullDate, formatTime, formatFullTime, formatRelativeTime } from "../../DateUtils"; +import { objectHasDiff } from "../../utils/objects"; + +export interface MessageTimestampViewModelProps { + /** + * Message timestamp in milliseconds since the Unix epoch. + */ + ts: number; + /** + * If specified will render both the sent-at and received-at timestamps in the tooltip + */ + receivedTs?: number; + /** + * If set, use a 12-hour clock for formatted times. + */ + showTwelveHour?: boolean; + /** + * If set, include the full date in the displayed timestamp. + */ + showFullDate?: boolean; + /** + * If set, include seconds in the displayed timestamp. + */ + showSeconds?: boolean; + /** + * If set, display a relative timestamp (e.g. "5 minutes ago"). + */ + showRelative?: boolean; + /** + * If set to true then no tooltip will be shown + */ + inhibitTooltip?: boolean; + /** + * If specified, will be rendered as an anchor bearing the href, a `span` element will be used otherwise + */ + href?: string; + /** + * Optional onClick handler to attach to the DOM element + */ + onClick?: MouseEventHandler; + /** + * Optional onContextMenu handler to attach to the DOM element + */ + onContextMenu?: MouseEventHandler; +} + +/** + * ViewModel for the message timestamp, providing the current state of the component. + */ +export class MessageTimestampViewModel + extends BaseViewModel + implements MessageTimestampViewModelInterface +{ + public onClick?: MouseEventHandler; + public onContextMenu?: MouseEventHandler; + + private static readonly computeSnapshot = ( + props: MessageTimestampViewModelProps, + ): MessageTimestampViewSnapshotInterface => { + const date = new Date(props.ts); + const sentAt = formatFullDate(date, props.showTwelveHour); + + let timestamp: string; + if (props.showRelative) { + timestamp = formatRelativeTime(date, props.showTwelveHour); + } else if (props.showFullDate) { + timestamp = formatFullDate(date, props.showTwelveHour, props.showSeconds); + } else if (props.showSeconds) { + timestamp = formatFullTime(date, props.showTwelveHour); + } else { + timestamp = formatTime(date, props.showTwelveHour); + } + + let receivedAt: string | undefined; + if (props.receivedTs !== undefined) { + const receivedDate = new Date(props.receivedTs); + 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", + }; + }; + + private updateProps(newProps: Partial): void { + const nextProps = { ...this.props, ...newProps }; + if (!objectHasDiff(this.props, nextProps)) return; + + this.props = nextProps; + this.onClick = this.props.onClick; + this.onContextMenu = this.props.onContextMenu; + this.snapshot.set(MessageTimestampViewModel.computeSnapshot(this.props)); + } + + /** + * Create a timestamp view model with initial props and snapshot. + */ + public constructor(props: MessageTimestampViewModelProps) { + super(props, MessageTimestampViewModel.computeSnapshot(props)); + this.onClick = props.onClick; + this.onContextMenu = props.onContextMenu; + } + + /** + * Update the base timestamp (milliseconds since Unix epoch). + */ + public setTimestamp(ts: number): void { + this.updateProps({ ts }); + } + + /** + * Update the optional received timestamp (milliseconds since Unix epoch). + */ + public setReceivedTimestamp(receivedTs?: number): void { + this.updateProps({ receivedTs }); + } + + /** + * Update display formatting options for the rendered timestamp. + */ + public setDisplayOptions(options: { + showTwelveHour?: boolean; + showFullDate?: boolean; + showSeconds?: boolean; + showRelative?: boolean; + }): void { + this.updateProps(options); + } + + /** + * Enable or disable the tooltip rendering. + */ + public setTooltipInhibited(inhibitTooltip?: boolean): void { + this.updateProps({ inhibitTooltip }); + } + + /** + * Update the optional href for link rendering. + */ + public setHref(href?: string): void { + this.updateProps({ href }); + } + + /** + * Update click and context-menu handlers for the rendered element. + */ + public setHandlers(handlers: { + onClick?: MouseEventHandler; + onContextMenu?: MouseEventHandler; + }): void { + this.updateProps(handlers); + } +} diff --git a/test/unit-tests/components/views/messages/MessageTimestamp-test.tsx b/test/unit-tests/components/views/messages/MessageTimestamp-test.tsx deleted file mode 100644 index ceaad8ea01..0000000000 --- a/test/unit-tests/components/views/messages/MessageTimestamp-test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 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 { render, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import MessageTimestamp from "../../../../../src/components/views/messages/MessageTimestamp"; - -jest.mock("../../../../../src/settings/SettingsStore"); - -describe("MessageTimestamp", () => { - // Friday Dec 17 2021, 9:09am - const nowDate = new Date("2021-12-17T08:09:00.000Z"); - - const HOUR_MS = 3600000; - const DAY_MS = HOUR_MS * 24; - - it("should render HH:MM", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchInlineSnapshot(` - - - -`); - }); - - it("should show full date & time on hover", async () => { - const { container } = render(); - await userEvent.hover(container.querySelector(".mx_MessageTimestamp")!); - expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot(`"Fri, Dec 17, 2021, 08:09:00"`); - }); - - it("should show sent & received time on hover if passed", async () => { - const { container } = render( - , - ); - await userEvent.hover(container.querySelector(".mx_MessageTimestamp")!); - expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot( - `"Sent at: Fri, Dec 17, 2021, 08:09:00Received at: Sat, Dec 18, 2021, 08:09:00"`, - ); - }); -}); diff --git a/test/unit-tests/components/views/messages/__snapshots__/MImageBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MImageBody-test.tsx.snap index 5b2befbd61..5a36c73c41 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/MImageBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/MImageBody-test.tsx.snap @@ -93,7 +93,7 @@ exports[` should open ImageView using thumbnail for encrypted svg 1
Thu, Jan 15, 1970, 06:56 diff --git a/test/unit-tests/utils/exportUtils/HTMLExport-test.ts b/test/unit-tests/utils/exportUtils/HTMLExport-test.ts index 102b4b4901..2bb92f37c8 100644 --- a/test/unit-tests/utils/exportUtils/HTMLExport-test.ts +++ b/test/unit-tests/utils/exportUtils/HTMLExport-test.ts @@ -246,7 +246,7 @@ describe("HTMLExport", () => { const file = getMessageFile(exporter); expect(await file.text()).toMatchSnapshot(); - }); + }, 10000); it("should include the room's avatar", async () => { mockMessages(EVENT_MESSAGE); diff --git a/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap b/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap index 115385f2e4..9f2a76734a 100644 --- a/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap +++ b/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap @@ -57,7 +57,7 @@ exports[`HTMLExport should export 1`] = `

-
  • @user48:example.com
    00:00
    Message #48
  • @user47:example.com
    00:00
    Message #47
  • @user46:example.com
    00:00
    Message #46
  • @user45:example.com
    00:00
    Message #45
  • @user44:example.com
    00:00
    Message #44
  • @user43:example.com
    00:00
    Message #43
  • @user42:example.com
    00:00
    Message #42
  • @user41:example.com
    00:00
    Message #41
  • @user40:example.com
    00:00
    Message #40
  • @user39:example.com
    00:00
    Message #39
  • @user38:example.com
    00:00
    Message #38
  • @user37:example.com
    00:00
    Message #37
  • @user36:example.com
    00:00
    Message #36
  • @user35:example.com
    00:00
    Message #35
  • @user34:example.com
    00:00
    Message #34
  • @user33:example.com
    00:00
    Message #33
  • @user32:example.com
    00:00
    Message #32
  • @user31:example.com
    00:00
    Message #31
  • @user30:example.com
    00:00
    Message #30
  • @user29:example.com
    00:00
    Message #29
  • @user28:example.com
    00:00
    Message #28
  • @user27:example.com
    00:00
    Message #27
  • @user26:example.com
    00:00
    Message #26
  • @user25:example.com
    00:00
    Message #25
  • @user24:example.com
    00:00
    Message #24
  • @user23:example.com
    00:00
    Message #23
  • @user22:example.com
    00:00
    Message #22
  • @user21:example.com
    00:00
    Message #21
  • @user20:example.com
    00:00
    Message #20
  • @user19:example.com
    00:00
    Message #19
  • @user18:example.com
    00:00
    Message #18
  • @user17:example.com
    00:00
    Message #17
  • @user16:example.com
    00:00
    Message #16
  • @user15:example.com
    00:00
    Message #15
  • @user14:example.com
    00:00
    Message #14
  • @user13:example.com
    00:00
    Message #13
  • @user12:example.com
    00:00
    Message #12
  • @user11:example.com
    00:00
    Message #11
  • @user10:example.com
    00:00
    Message #10
  • @user9:example.com
    00:00
    Message #9
  • @user8:example.com
    00:00
    Message #8
  • @user7:example.com
    00:00
    Message #7
  • @user6:example.com
    00:00
    Message #6
  • @user5:example.com
    00:00
    Message #5
  • @user4:example.com
    00:00
    Message #4
  • @user3:example.com
    00:00
    Message #3
  • @user2:example.com
    00:00
    Message #2
  • @user1:example.com
    00:00
    Message #1
  • @user0:example.com
    00:00
    Message #0
  • +
  • @user49:example.com
    00:00
    Message #49
  • @user48:example.com
    00:00
    Message #48
  • @user47:example.com
    00:00
    Message #47
  • @user46:example.com
    00:00
    Message #46
  • @user45:example.com
    00:00
    Message #45
  • @user44:example.com
    00:00
    Message #44
  • @user43:example.com
    00:00
    Message #43
  • @user42:example.com
    00:00
    Message #42
  • @user41:example.com
    00:00
    Message #41
  • @user40:example.com
    00:00
    Message #40
  • @user39:example.com
    00:00
    Message #39
  • @user38:example.com
    00:00
    Message #38
  • @user37:example.com
    00:00
    Message #37
  • @user36:example.com
    00:00
    Message #36
  • @user35:example.com
    00:00
    Message #35
  • @user34:example.com
    00:00
    Message #34
  • @user33:example.com
    00:00
    Message #33
  • @user32:example.com
    00:00
    Message #32
  • @user31:example.com
    00:00
    Message #31
  • @user30:example.com
    00:00
    Message #30
  • @user29:example.com
    00:00
    Message #29
  • @user28:example.com
    00:00
    Message #28
  • @user27:example.com
    00:00
    Message #27
  • @user26:example.com
    00:00
    Message #26
  • @user25:example.com
    00:00
    Message #25
  • @user24:example.com
    00:00
    Message #24
  • @user23:example.com
    00:00
    Message #23
  • @user22:example.com
    00:00
    Message #22
  • @user21:example.com
    00:00
    Message #21
  • @user20:example.com
    00:00
    Message #20
  • @user19:example.com
    00:00
    Message #19
  • @user18:example.com
    00:00
    Message #18
  • @user17:example.com
    00:00
    Message #17
  • @user16:example.com
    00:00
    Message #16
  • @user15:example.com
    00:00
    Message #15
  • @user14:example.com
    00:00
    Message #14
  • @user13:example.com
    00:00
    Message #13
  • @user12:example.com
    00:00
    Message #12
  • @user11:example.com
    00:00
    Message #11
  • @user10:example.com
    00:00
    Message #10
  • @user9:example.com
    00:00
    Message #9
  • @user8:example.com
    00:00
    Message #8
  • @user7:example.com
    00:00
    Message #7
  • @user6:example.com
    00:00
    Message #6
  • @user5:example.com
    00:00
    Message #5
  • @user4:example.com
    00:00
    Message #4
  • @user3:example.com
    00:00
    Message #3
  • @user2:example.com
    00:00
    Message #2
  • @user1:example.com
    00:00
    Message #1
  • @user0:example.com
    00:00
    Message #0
  • diff --git a/test/viewmodels/message-body/MessageTimestampViewModel-test.tsx b/test/viewmodels/message-body/MessageTimestampViewModel-test.tsx new file mode 100644 index 0000000000..864d4ce622 --- /dev/null +++ b/test/viewmodels/message-body/MessageTimestampViewModel-test.tsx @@ -0,0 +1,123 @@ +/* + * 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 * as DateUtils from "../../../src/DateUtils"; +import { MessageTimestampViewModel } from "../../../src/viewmodels/message-body/MessageTimestampViewModel"; + +jest.mock("../../../src/settings/SettingsStore"); + +describe("MessageTimestampViewModel", () => { + // Friday Dec 17 2021, 9:09am + const nowDate = new Date("2021-12-17T08:09:00.000Z"); + const HOUR_MS = 3600000; + const DAY_MS = HOUR_MS * 24; + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should return the snapshot", () => { + const vm = new MessageTimestampViewModel({ + ts: nowDate.getTime(), + }); + expect(vm.getSnapshot()).toMatchObject({ + ts: "08:09", + tsSentAt: "Fri, Dec 17, 2021, 08:09:00", + }); + }); + + it("should return the snapshot with tsReceivedAt", () => { + const vm = new MessageTimestampViewModel({ + ts: nowDate.getTime(), + receivedTs: nowDate.getTime() + DAY_MS, + }); + expect(vm.getSnapshot()).toMatchObject({ + ts: "08:09", + tsSentAt: "Fri, Dec 17, 2021, 08:09:00", + tsReceivedAt: "Sat, Dec 18, 2021, 08:09:00", + }); + }); + + it("should return the snapshot with extra 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", + }); + }); + + it("should use formatRelativeTime when showRelative is true", () => { + jest.spyOn(DateUtils, "formatFullDate").mockReturnValue("SENT_AT"); + const formatRelativeTimeSpy = jest.spyOn(DateUtils, "formatRelativeTime").mockReturnValue("RELATIVE"); + + const vm = new MessageTimestampViewModel({ + ts: nowDate.getTime(), + showRelative: true, + showTwelveHour: true, + }); + + expect(vm.getSnapshot()).toMatchObject({ + ts: "RELATIVE", + tsSentAt: "SENT_AT", + }); + expect(formatRelativeTimeSpy).toHaveBeenCalledWith(expect.any(Date), true); + }); + + it("should use full date when showFullDate is true and respect showSeconds", () => { + const formatFullDateSpy = jest + .spyOn(DateUtils, "formatFullDate") + .mockImplementation((_date, _showTwelveHour, showSeconds) => + showSeconds === false ? "FULL_NO_SECONDS" : "SENT_AT", + ); + + const vm = new MessageTimestampViewModel({ + ts: nowDate.getTime(), + showFullDate: true, + showSeconds: false, + }); + + expect(vm.getSnapshot()).toMatchObject({ + ts: "FULL_NO_SECONDS", + tsSentAt: "SENT_AT", + }); + expect(formatFullDateSpy).toHaveBeenCalled(); + }); + + it("should use full time when showSeconds is true without full date", () => { + jest.spyOn(DateUtils, "formatFullDate").mockReturnValue("SENT_AT"); + const formatFullTimeSpy = jest.spyOn(DateUtils, "formatFullTime").mockReturnValue("FULL_TIME"); + const formatTimeSpy = jest.spyOn(DateUtils, "formatTime").mockReturnValue("TIME"); + + const vm = new MessageTimestampViewModel({ + ts: nowDate.getTime(), + showSeconds: true, + }); + + expect(vm.getSnapshot()).toMatchObject({ + ts: "FULL_TIME", + tsSentAt: "SENT_AT", + }); + expect(formatFullTimeSpy).toHaveBeenCalled(); + expect(formatTimeSpy).not.toHaveBeenCalled(); + }); + + it("should include tooltip inhibition and href in the snapshot", () => { + const vm = new MessageTimestampViewModel({ + ts: nowDate.getTime(), + inhibitTooltip: true, + href: "https://example.test", + }); + + expect(vm.getSnapshot()).toMatchObject({ + inhibitTooltip: true, + href: "https://example.test", + }); + }); +});