Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2025 Element Creations Ltd.
* Copyright 2017 Vector 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.
*/

.dateSeparator {
clear: both;
margin: 4px 0;
display: flex;
align-items: center;
font: var(--cpd-font-body-md-regular);
color: var(--cpd-color-text-primary);
}

.dateSeparator > hr {
flex: 1 1 0;
height: 0;
border: none;
border-bottom: 1px solid var(--cpd-color-gray-400);
}

.dateContent {
padding: 0 25px;
}

.dateHeading {
flex: 0 0 auto;
margin: 0;
font-size: inherit;
font-weight: inherit;
color: inherit;
text-transform: capitalize;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2025 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 { Meta, StoryObj } from "@storybook/react-vite";
import { DateSeparator } from "./DateSeparator";

const now = Date.now();
const DAY_MS = 24 * 60 * 60 * 1000;

const meta: Meta<typeof DateSeparator> = {
title: "Event Tiles/DateSeparator",
component: DateSeparator,
tags: ["autodocs"],
args: {
locale: "en",
},
};

export default meta;
type Story = StoryObj<typeof DateSeparator>;

export const Today: Story = {
args: {
ts: now,
},
};

export const Yesterday: Story = {
args: {
ts: now - DAY_MS,
},
};

export const LastWeek: Story = {
args: {
ts: now - 4 * DAY_MS,
},
};

export const LongAgo: Story = {
args: {
ts: now - 365 * DAY_MS,
},
};

export const DisableRelativeTimestamps: Story = {
args: {
ts: now,
disableRelativeTimestamps: true,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2025 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 } from "jest-matrix-react";
import React from "react";

import { DateSeparator } from "./DateSeparator";

describe("DateSeparator", () => {
beforeEach(() => {
jest.useFakeTimers();
// Set a fixed "now" time for consistent testing
jest.setSystemTime(new Date("2024-11-03T12:00:00Z"));
});

afterEach(() => {
jest.useRealTimers();
});

it("renders today's date", () => {
const { container } = render(<DateSeparator ts={new Date("2024-11-03T10:00:00Z").getTime()} locale="en" />);
expect(container).toMatchSnapshot();
expect(container.textContent).toContain("today");
});

it("renders yesterday's date", () => {
const { container } = render(<DateSeparator ts={new Date("2024-11-02T10:00:00Z").getTime()} locale="en" />);
expect(container).toMatchSnapshot();
expect(container.textContent).toContain("yesterday");
});

it("renders a weekday for dates within the last 6 days", () => {
// 4 days ago
const { container } = render(<DateSeparator ts={new Date("2024-10-30T10:00:00Z").getTime()} locale="en" />);
expect(container).toMatchSnapshot();
// Should show a day name like "Wednesday"
expect(container.querySelector(".mx_DateSeparator_dateHeading")).toBeTruthy();
});

it("renders full date for dates older than 6 days", () => {
const { container } = render(<DateSeparator ts={new Date("2024-10-01T10:00:00Z").getTime()} locale="en" />);
expect(container).toMatchSnapshot();
expect(container.textContent).toContain("Oct");
});

it("renders full date when relative timestamps are disabled", () => {
const { container } = render(
<DateSeparator ts={new Date("2024-11-03T10:00:00Z").getTime()} locale="en" disableRelativeTimestamps />,
);
expect(container).toMatchSnapshot();
// Should show full date even though it's today
expect(container.textContent).toContain("Nov");
});

it("applies custom className", () => {
const { container } = render(<DateSeparator ts={Date.now()} locale="en" className="custom-class" />);
expect(container.querySelector(".mx_DateSeparator.custom-class")).toBeTruthy();
});

it("has correct ARIA attributes", () => {
const { container } = render(<DateSeparator ts={Date.now()} locale="en" />);
const separator = container.querySelector('[role="separator"]');
expect(separator).toBeTruthy();
expect(separator?.getAttribute("aria-label")).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2025 Element Creations Ltd.
* Copyright 2015-2021 The Matrix.org Foundation C.I.C.
* Copyright 2018 Michael Telatynski <[email protected]>
*
* 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, { useMemo } from "react";
import classNames from "classnames";

import { _t } from "../../utils/i18n";
import { formatFullDateNoTime, getDaysArray, DAY_MS } from "../../utils/DateUtils";
import styles from "./DateSeparator.module.css";

export interface Props {
/** The timestamp (in milliseconds) to display */
ts: number;
/** The locale to use for formatting. Defaults to "en" */
locale?: string;
/** Whether to disable relative timestamps (e.g., "Today", "Yesterday"). If true, always shows full date */
disableRelativeTimestamps?: boolean;
/** Additional CSS class name */
className?: string;
}

/**
* Get the label for a date separator
* @param ts - The timestamp (in milliseconds) to display
* @param locale - The locale to use for formatting
* @param disableRelativeTimestamps - Whether to disable relative timestamps
* @returns The formatted label string
*/
function getLabel(ts: number, locale: string, disableRelativeTimestamps: boolean): string {
try {
const date = new Date(ts);

// If relative timestamps are disabled, return the full date
if (disableRelativeTimestamps) return formatFullDateNoTime(date, locale);

const today = new Date();
const yesterday = new Date();
const days = getDaysArray("long", locale);
const relativeTimeFormat = new Intl.RelativeTimeFormat(locale, { style: "long", numeric: "auto" });
yesterday.setDate(today.getDate() - 1);

if (date.toDateString() === today.toDateString()) {
return relativeTimeFormat.format(0, "day"); // Today
} else if (date.toDateString() === yesterday.toDateString()) {
return relativeTimeFormat.format(-1, "day"); // Yesterday
} else if (today.getTime() - date.getTime() < 6 * DAY_MS) {
return days[date.getDay()]; // Sunday-Saturday
} else {
return formatFullDateNoTime(date, locale);
}
} catch {
return _t("common|message_timestamp_invalid");
}
}

/**
* Timeline separator component to render within a MessagePanel bearing the date of the ts given
*/
export const DateSeparator: React.FC<Props> = ({ ts, locale = "en", disableRelativeTimestamps = false, className }) => {
const label = useMemo(
() => getLabel(ts, locale, disableRelativeTimestamps),
[ts, locale, disableRelativeTimestamps],
);

return (
<div
className={classNames(styles.dateSeparator, "mx_DateSeparator", className)}
role="separator"
aria-label={label}
>
<hr role="none" />
<div className={classNames(styles.dateContent, "mx_DateSeparator_dateContent")}>
<h2 className={classNames(styles.dateHeading, "mx_DateSeparator_dateHeading")} aria-hidden="true">
{label}
</h2>
</div>
<hr role="none" />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`DateSeparator renders a weekday for dates within the last 6 days 1`] = `
<div>
<div
aria-label="Wednesday"
class="dateSeparator mx_DateSeparator"
role="separator"
>
<hr
role="none"
/>
<div
class="dateContent mx_DateSeparator_dateContent"
>
<h2
aria-hidden="true"
class="dateHeading mx_DateSeparator_dateHeading"
>
Wednesday
</h2>
</div>
<hr
role="none"
/>
</div>
</div>
`;

exports[`DateSeparator renders full date for dates older than 6 days 1`] = `
<div>
<div
aria-label="Tue, Oct 1, 2024"
class="dateSeparator mx_DateSeparator"
role="separator"
>
<hr
role="none"
/>
<div
class="dateContent mx_DateSeparator_dateContent"
>
<h2
aria-hidden="true"
class="dateHeading mx_DateSeparator_dateHeading"
>
Tue, Oct 1, 2024
</h2>
</div>
<hr
role="none"
/>
</div>
</div>
`;

exports[`DateSeparator renders full date when relative timestamps are disabled 1`] = `
<div>
<div
aria-label="Sun, Nov 3, 2024"
class="dateSeparator mx_DateSeparator"
role="separator"
>
<hr
role="none"
/>
<div
class="dateContent mx_DateSeparator_dateContent"
>
<h2
aria-hidden="true"
class="dateHeading mx_DateSeparator_dateHeading"
>
Sun, Nov 3, 2024
</h2>
</div>
<hr
role="none"
/>
</div>
</div>
`;

exports[`DateSeparator renders today's date 1`] = `
<div>
<div
aria-label="today"
class="dateSeparator mx_DateSeparator"
role="separator"
>
<hr
role="none"
/>
<div
class="dateContent mx_DateSeparator_dateContent"
>
<h2
aria-hidden="true"
class="dateHeading mx_DateSeparator_dateHeading"
>
today
</h2>
</div>
<hr
role="none"
/>
</div>
</div>
`;

exports[`DateSeparator renders yesterday's date 1`] = `
<div>
<div
aria-label="yesterday"
class="dateSeparator mx_DateSeparator"
role="separator"
>
<hr
role="none"
/>
<div
class="dateContent mx_DateSeparator_dateContent"
>
<h2
aria-hidden="true"
class="dateHeading mx_DateSeparator_dateHeading"
>
yesterday
</h2>
</div>
<hr
role="none"
/>
</div>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2025 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 { DateSeparator } from "./DateSeparator";
export type { Props as DateSeparatorProps } from "./DateSeparator";
Loading
Loading