Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a9b0abf
Refactoring of timeline stamp, move it to shared components
ZacksBot Jan 13, 2026
026a027
Fix position of the import based on lint recommendations.
ZacksBot Jan 14, 2026
51a2480
Use TimelineSeparatorView from shared-components across codebase
ZacksBot Jan 14, 2026
fb786f5
Restore snapshots to correct starting values in classname
ZacksBot Jan 14, 2026
8ac830f
Remove unnecessary export type
ZacksBot Jan 15, 2026
cf8741f
updated copyright date to fit current year
ZacksBot Jan 16, 2026
1340a62
Add stories and tests for TimelineSeparatorView variants
ZacksBot Jan 19, 2026
1149289
Refactor TimelineSeparatorView: remove SeparatorKind usage and clean …
ZacksBot Jan 19, 2026
790ae5f
Refactor TimelineSeparator: simplify children prop documentation and …
ZacksBot Jan 20, 2026
0b8b5e8
Update of jest snapshot (auto generated after running test)
ZacksBot Jan 20, 2026
5f34279
Update packages/shared-components/src/message-body/TimelineSeparator/…
ZacksBot Jan 20, 2026
021c699
Update packages/shared-components/src/message-body/TimelineSeparator/…
ZacksBot Jan 20, 2026
5597f78
Update packages/shared-components/src/message-body/TimelineSeparator/…
ZacksBot Jan 20, 2026
3b9a304
Update packages/shared-components/src/message-body/TimelineSeparator/…
ZacksBot Jan 20, 2026
ea77d3b
Update packages/shared-components/src/message-body/TimelineSeparator/…
ZacksBot Jan 20, 2026
bb0a19e
Update packages/shared-components/src/message-body/TimelineSeparator/…
ZacksBot Jan 20, 2026
3b7868e
Enhance SeparatorKind enum documentation for clarity on timeline even…
ZacksBot Jan 20, 2026
2d3908a
Merge branch 'develop' into refactor/timeline-separator
ZacksBot Jan 20, 2026
9274256
Refactor TimelineSeparator integration and add TimelineSeparatorViewM…
ZacksBot Jan 20, 2026
645838a
Update copyright year in TimelineSeparatorViewModel to 2026
ZacksBot Jan 20, 2026
a06a41d
Refactor TimelineSeparator styles and update class names for css
ZacksBot Jan 20, 2026
8b55a91
Refactor TimelineSeparator component: update margin styles, simplify …
ZacksBot Jan 21, 2026
b4563c6
Refactor TimelineSeparatorView: import PropsWithChildren for improved…
ZacksBot Jan 21, 2026
43f7f27
Refactor TimelineSeparatorViewModel: replace ReactNode with PropsWith…
ZacksBot Jan 21, 2026
a558b16
Merge branch 'develop' into refactor/timeline-separator
ZacksBot Feb 2, 2026
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
1 change: 1 addition & 0 deletions packages/shared-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from "./composer/Banner";
export * from "./crypto/SasEmoji";
export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./message-body/TimelineSeparator/";
export * from "./message-body/DecryptionFailureBodyView";
export * from "./message-body/ReactionsRowButtonTooltip";
export * from "./pill-input/Pill";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.
*/

.timelineSeparator {
clear: both;
margin: var(--cpdSpace1X, var(--cpdSpace0X));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this?

display: flex;
align-items: center;
font: var(--cpd-font-body-md-regular);
color: var(--cpd-color-text-primary);
}

.timelineSeparator > hr {
flex: 1 1 0;
height: 0;
border: none;
border-bottom: 1px solid var(--cpd-color-gray-400);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

import React, { type JSX } from "react";

import type { Meta, StoryFn } from "@storybook/react-vite";
import { TimelineSeparatorView, type TimelineSeparatorViewSnapshot } from "./TimelineSeparatorView";
import { useMockedViewModel } from "../../useMockedViewModel";
import styles from "./TimelineSeparatorView.module.css";

type TimelineSeparatorProps = TimelineSeparatorViewSnapshot;
const TimelineSeparatorViewWrapper = (props: TimelineSeparatorProps): JSX.Element => {
// There is no action (undefined second param)
const vm = useMockedViewModel(props, undefined);
return <TimelineSeparatorView vm={vm} />;
};

export default {
title: "MessageBody/TimelineSeparatorView",
component: TimelineSeparatorViewWrapper,
tags: ["autodocs"],
args: {
label: "Label Separator",
children: "Timeline Separator",
},
} as Meta<typeof TimelineSeparatorViewWrapper>;

const Template: StoryFn<typeof TimelineSeparatorViewWrapper> = (args: any) => <TimelineSeparatorViewWrapper {...args} />;

export const Default = Template.bind({});

export const WithHtmlChild = Template.bind({});
WithHtmlChild.args = {
label: "Custom Label",
children: <h2 className={styles.timelineSeparator} aria-hidden="true">Thursday</h2>,
};

export const WithDateEvent = Template.bind({});
WithDateEvent.args = {
label: "Date Event Separator",
children: "Wednesday",
};

export const WithLateEvent = Template.bind({});
WithLateEvent.args = {
label: "Late Event Separator",
children: "Fri, Jan 9, 2026",
};

export const WithoutChildren = Template.bind({});
WithoutChildren.args = {
children: undefined,
label: "Separator without children",
};





Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 } from "jest-matrix-react";
import { composeStories } from "@storybook/react-vite";
import React from "react";

import * as stories from "./TimelineSeparatorView.stories.tsx";

const { Default, WithHtmlChild, WithoutChildren, WithDateEvent, WithLateEvent } = composeStories(stories);

describe("TimelineSeparatorView", () => {
afterEach(() => {
jest.clearAllMocks();
});

describe("Snapshot tests", () => {
it("renders the timeline separator in default state", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});

it("renders the timeline separator with HTML child", () => {
const { container } = render(<WithHtmlChild />);
expect(container).toMatchSnapshot();
});

it("renders the timeline separator without children", () => {
const { container } = render(<WithDateEvent />);
expect(container).toMatchSnapshot();
});

it("renders the timeline separator without children", () => {
const { container } = render(<WithLateEvent />);
expect(container).toMatchSnapshot();
});
it("renders the timeline separator without children", () => {
const { container } = render(<WithoutChildren />);
expect(container).toMatchSnapshot();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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 { PropsWithChildren, type JSX } from "react";
import React from "react";
import classNames from "classnames";

import { type ViewModel } from "../../viewmodel/ViewModel";
import { useViewModel } from "../../useViewModel";
import styles from "./TimelineSeparatorView.module.css";


/**
* Snapshot interface for the timeline separator view model.
*/
export interface TimelineSeparatorViewSnapshot {
/**
* Accessible label for the separator (for example: "Today", "Yesterday", or a date).
*/
label: string;
/**
* Optional children to render inside the timeline separator
*/
children?: PropsWithChildren["children"];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be fine using ["children"];

}

/**
* The view model for the timeline separator.
*/
export type TimelineSeparatorViewModel = ViewModel<TimelineSeparatorViewSnapshot>;

interface TimelineSeparatorViewProps {
/**
* The view model for the timeline separator.
*/
vm: TimelineSeparatorViewModel;
}

/**
* TimelineSeparatorView component renders a visual separator inside the message timeline.
* It draws horizontal rules with an accessible label and optional children rendered between them.
*
* @param label the accessible label string describing the separator (used for `aria-label`)
* @param children optional React nodes to render between the separators
*
*/
Comment on lines +44 to +50
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@florianduros Does this look better? If not, what do you suggest?

export function TimelineSeparatorView({ vm }: Readonly<TimelineSeparatorViewProps>): JSX.Element {
const {
label, children,
} = useViewModel(vm);

// Keep mx_TimelineSeparator to support the compatibility with existing timeline and the all the layout
return (
<div className={classNames("mx_TimelineSeparator", styles.timelineSeparator)} role="separator" aria-label={label}>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{classNames("mx_TimelineSeparator", styles.timelineSeparator)}

I would like to hear your opinion regarding this, this is like to comment says, to "Keep mx_TimelineSeparator to support the compatibility with existing timeline and the all the layout"

<hr role="none" />
{children}
<hr role="none" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`TimelineSeparatorView Snapshot tests renders the timeline separator in default state 1`] = `
<div>
<div
aria-label="Label Separator"
class="mx_TimelineSeparator timelineSeparator"
role="separator"
>
<hr
role="none"
/>
Timeline Separator
<hr
role="none"
/>
</div>
</div>
`;

exports[`TimelineSeparatorView Snapshot tests renders the timeline separator with HTML child 1`] = `
<div>
<div
aria-label="Custom Label"
class="mx_TimelineSeparator timelineSeparator"
role="separator"
>
<hr
role="none"
/>
<h2
aria-hidden="true"
class="timelineSeparator"
>
Thursday
</h2>
<hr
role="none"
/>
</div>
</div>
`;

exports[`TimelineSeparatorView Snapshot tests renders the timeline separator without children 1`] = `
<div>
<div
aria-label="Date Event Separator"
class="mx_TimelineSeparator timelineSeparator"
role="separator"
>
<hr
role="none"
/>
Wednesday
<hr
role="none"
/>
</div>
</div>
`;

exports[`TimelineSeparatorView Snapshot tests renders the timeline separator without children 2`] = `
<div>
<div
aria-label="Late Event Separator"
class="mx_TimelineSeparator timelineSeparator"
role="separator"
>
<hr
role="none"
/>
Fri, Jan 9, 2026
<hr
role="none"
/>
</div>
</div>
`;

exports[`TimelineSeparatorView Snapshot tests renders the timeline separator without children 3`] = `
<div>
<div
aria-label="Separator without children"
class="mx_TimelineSeparator timelineSeparator"
role="separator"
>
<hr
role="none"
/>
<hr
role="none"
/>
</div>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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 { TimelineSeparatorView, type TimelineSeparatorViewSnapshot, type TimelineSeparatorViewModel } from "./TimelineSeparatorView";
23 changes: 19 additions & 4 deletions src/components/structures/MessagePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
} from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { isSupportedReceiptType } from "matrix-js-sdk/src/utils";
import {
TimelineSeparatorView,
useCreateAutoDisposedViewModel,
} from "@element-hq/web-shared-components";

import shouldHideEvent from "../../shouldHideEvent";
import { formatDate, wantsDateSeparator } from "../../DateUtils";
Expand All @@ -37,7 +41,6 @@ import type LegacyCallEventGrouper from "./LegacyCallEventGrouper";
import WhoIsTypingTile from "../views/rooms/WhoIsTypingTile";
import ScrollPanel, { type IScrollState } from "./ScrollPanel";
import DateSeparator from "../views/messages/DateSeparator";
import TimelineSeparator, { SeparatorKind } from "../views/messages/TimelineSeparator";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import Spinner from "../views/elements/Spinner";
import { type RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
Expand All @@ -53,10 +56,23 @@ import { MainGrouper } from "./grouper/MainGrouper";
import { CreationGrouper } from "./grouper/CreationGrouper";
import { _t } from "../../languageHandler";
import { getLateEventInfo } from "./grouper/LateEventGrouper";
import { TimelineSeparatorViewModel } from "../../viewmodels/message-body/TimelineSeparatorViewModel";

const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];

/**
* Indicates which separator (if any) should be rendered between timeline events.
*/
export const enum SeparatorKind {
/** No separator should be shown between the two events. */
None,
/** Insert a date separator (oriented by event date boundaries). */
Date,
/** Insert a late-event separator when events belong to different late groups. */
LateEvent,
}

// check if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
export function shouldFormContinuation(
Expand Down Expand Up @@ -754,11 +770,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
const text = _t("timeline|late_event_separator", {
dateTime: formatDate(mxEv.getDate() ?? new Date()),
});
const timeLineSeperatorVM = useCreateAutoDisposedViewModel(() => new TimelineSeparatorViewModel({ label: text, children: text }));
ret.push(
<li key={ts1}>
<TimelineSeparator key={ts1} label={text}>
{text}
</TimelineSeparator>
<TimelineSeparatorView key={ts1} vm={timeLineSeperatorVM} />
</li>,
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/structures/grouper/CreationGrouper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import React, { type ReactNode } from "react";
import { EventType, M_BEACON_INFO, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";


import { BaseGrouper } from "./BaseGrouper";
import { type WrappedEvent } from "../MessagePanel";
import { SeparatorKind, type WrappedEvent } from "../MessagePanel";
import type MessagePanel from "../MessagePanel";
import DMRoomMap from "../../../utils/DMRoomMap";
import { _t } from "../../../languageHandler";
import DateSeparator from "../../views/messages/DateSeparator";
import NewRoomIntro from "../../views/rooms/NewRoomIntro";
import GenericEventListSummary from "../../views/elements/GenericEventListSummary";
import { SeparatorKind } from "../../views/messages/TimelineSeparator";

// Wrap initial room creation events into a GenericEventListSummary
// Grouping only events sent by the same user that sent the `m.room.create` and only until
Expand Down
Loading
Loading