Refactor DateSeparator using MVVM and move to shared-components (#32482)

* Refactor DateSeparator using MVVM and move to shared-components

* Add a few more stories, tests and screenshots

* Use the shared component and viewmodel in element-web

* Renaming custom content property an updating snapshots

* Fix lint errors and update snapshot after merge

* Change lifecycle handling for DateSeparatoreViewModel in components where manual handling is preferrable over wrapper component.

* Move context menu from viewmodel to shared components - step 1

* Create a jump to date picker component in shared components

* Add tests for coverage and fix layout issues and roving indexes

* Make element-web use the new component

* Simplify context menu and adjusting tests

* The HTMLExport now render shared components and need a I18nContext.Provider

* Updating unit tests for context menu

* Changed to {translate: _t} to let scripts pick up translations

* Fix lint issue and updating screenshots after merge

* Update snaps for element web components

* Renaming MVVM view components with suffix View.

* Fixing problem with input date calendar icon and system dark theme

* Changed the rendering of the menu and added a separate button component

* Handle input control with useRef in onKeyDown

* Updating DateSeparator snapshots on unit tests

* Updating layout after compound Menu got a className property

* Move files to new subfolder after merge

* Updated snapshot after merge

* Updating lock file

* Updates to styling from PR review

* Updates to focus/blur functionality

* Fixed tabbing and export documentation to stories

* Updated snapshots

---------

Co-authored-by: Zack <zazi21@student.bth.se>
This commit is contained in:
rbondesson
2026-03-02 13:18:51 +01:00
committed by GitHub
parent 76cbfb1ae4
commit 11030ae68d
48 changed files with 1819 additions and 1048 deletions
@@ -18,7 +18,11 @@ import {
} from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { isSupportedReceiptType } from "matrix-js-sdk/src/utils";
import { TimelineSeparator } from "@element-hq/web-shared-components";
import {
DateSeparatorView,
TimelineSeparator,
useCreateAutoDisposedViewModel,
} from "@element-hq/web-shared-components";
import shouldHideEvent from "../../shouldHideEvent";
import { formatDate, wantsDateSeparator } from "../../DateUtils";
@@ -37,7 +41,6 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
import type LegacyCallEventGrouper from "./LegacyCallEventGrouper";
import WhoIsTypingTile from "../views/rooms/WhoIsTypingTile";
import ScrollPanel, { type IScrollState } from "./ScrollPanel";
import DateSeparator from "../views/messages/DateSeparator";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import Spinner from "../views/elements/Spinner";
import { type RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
@@ -53,10 +56,19 @@ import { MainGrouper } from "./grouper/MainGrouper";
import { CreationGrouper } from "./grouper/CreationGrouper";
import { _t } from "../../languageHandler";
import { getLateEventInfo } from "./grouper/LateEventGrouper";
import { DateSeparatorViewModel } from "../../viewmodels/timeline/DateSeparatorViewModel";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
/**
* Creates and auto-disposes the DateSeparatorViewModel for message panel rendering.
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): JSX.Element {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
return <DateSeparatorView vm={vm} />;
}
/**
* Indicates which separator (if any) should be rendered between timeline events.
*/
@@ -757,9 +769,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
const wantsSeparator = this.wantsSeparator(prevEvent, mxEv);
if (!isGrouped && this.props.room) {
if (wantsSeparator === SeparatorKind.Date) {
const separatorRoomId = this.props.room.roomId;
ret.push(
<li key={ts1}>
<DateSeparator key={ts1} roomId={this.props.room.roomId} ts={ts1} />
<li key={`${separatorRoomId}-${ts1}`}>
<DateSeparatorWrapper key={`${separatorRoomId}-${ts1}`} roomId={separatorRoomId} ts={ts1} />
</li>,
);
} else if (wantsSeparator === SeparatorKind.LateEvent) {
@@ -9,20 +9,29 @@ Please see LICENSE files in the repository root for full details.
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 { DateSeparatorView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
import { BaseGrouper } from "./BaseGrouper";
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 { DateSeparatorViewModel } from "../../../viewmodels/timeline/DateSeparatorViewModel";
// 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
// the first non-state event, beacon_info event or membership event which is not regarding the sender of the `m.room.create` event
/**
* Creates and auto-disposes the DateSeparatorViewModel for creation-group rendering.
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): ReactNode {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
return <DateSeparatorView vm={vm} />;
}
export class CreationGrouper extends BaseGrouper {
public static canStartGroup = function (_panel: MessagePanel, { event }: WrappedEvent): boolean {
return event.getType() === EventType.RoomCreate;
@@ -86,10 +95,11 @@ export class CreationGrouper extends BaseGrouper {
const lastShownEvent = this.lastShownEvent;
if (panel.wantsSeparator(this.prevEvent, createEvent.event) === SeparatorKind.Date) {
const separatorRoomId = createEvent.event.getRoomId()!;
const ts = createEvent.event.getTs();
ret.push(
<li key={ts + "~"}>
<DateSeparator roomId={createEvent.event.getRoomId()!} ts={ts} />
<li key={`${separatorRoomId}-${ts}~`}>
<DateSeparatorWrapper roomId={separatorRoomId} ts={ts} />
</li>,
);
}
@@ -8,15 +8,16 @@ Please see LICENSE files in the repository root for full details.
import React, { type ReactNode } from "react";
import { EventType, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { DateSeparatorView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
import type MessagePanel from "../MessagePanel";
import { SeparatorKind, type WrappedEvent } from "../MessagePanel";
import { BaseGrouper } from "./BaseGrouper";
import { hasText } from "../../../TextForEvent";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import DateSeparator from "../../views/messages/DateSeparator";
import HistoryTile from "../../views/rooms/HistoryTile";
import EventListSummary from "../../views/elements/EventListSummary";
import { DateSeparatorViewModel } from "../../../viewmodels/timeline/DateSeparatorViewModel";
const groupedStateEvents = [
EventType.RoomMember,
@@ -25,6 +26,14 @@ const groupedStateEvents = [
EventType.RoomPinnedEvents,
];
/**
* Creates and auto-disposes the DateSeparatorViewModel for grouped timeline rendering.
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): ReactNode {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
return <DateSeparatorView vm={vm} />;
}
// Wrap consecutive grouped events in a ListSummary
export class MainGrouper extends BaseGrouper {
public static canStartGroup = function (panel: MessagePanel, { event: ev, shouldShow }: WrappedEvent): boolean {
@@ -113,10 +122,11 @@ export class MainGrouper extends BaseGrouper {
const ret: ReactNode[] = [];
if (panel.wantsSeparator(this.prevEvent, this.events[0].event) === SeparatorKind.Date) {
const separatorRoomId = this.events[0].event.getRoomId()!;
const ts = this.events[0].event.getTs();
ret.push(
<li key={ts + "~"}>
<DateSeparator roomId={this.events[0].event.getRoomId()!} ts={ts} />
<li key={`${separatorRoomId}-${ts}~`}>
<DateSeparatorWrapper roomId={separatorRoomId} ts={ts} />
</li>,
);
}