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>,
);
}
@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react";
import { type MatrixEvent, EventType, RelationType, type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { DateSeparatorView } from "@element-hq/web-shared-components";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
@@ -18,7 +19,7 @@ import BaseDialog from "./BaseDialog";
import ScrollPanel from "../../structures/ScrollPanel";
import Spinner from "../elements/Spinner";
import EditHistoryMessage from "../messages/EditHistoryMessage";
import DateSeparator from "../messages/DateSeparator";
import { DateSeparatorViewModel } from "../../../viewmodels/timeline/DateSeparatorViewModel";
interface IProps {
mxEvent: MatrixEvent;
@@ -35,6 +36,8 @@ interface IState {
}
export default class MessageEditHistoryDialog extends React.PureComponent<IProps, IState> {
private dateSeparatorVms = new Map<string, DateSeparatorViewModel>();
public constructor(props: IProps) {
super(props);
this.state = {
@@ -47,6 +50,16 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps
};
}
private getDateSeparatorVm(roomId: string, ts: number): DateSeparatorViewModel {
const key = `${roomId}-${ts}`;
let vm = this.dateSeparatorVms.get(key);
if (!vm) {
vm = new DateSeparatorViewModel({ roomId, ts });
this.dateSeparatorVms.set(key, vm);
}
return vm;
}
private loadMoreEdits = async (backwards?: boolean): Promise<boolean> => {
if (backwards || (!this.state.nextBatch && !this.state.isLoading)) {
// bail out on backwards as we only paginate in one direction
@@ -108,6 +121,13 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps
this.loadMoreEdits();
}
public componentWillUnmount(): void {
for (const vm of this.dateSeparatorVms.values()) {
vm.dispose();
}
this.dateSeparatorVms.clear();
}
private renderEdits(): JSX.Element[] {
const nodes: JSX.Element[] = [];
let lastEvent: MatrixEvent;
@@ -119,9 +139,11 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps
const baseEventId = this.props.mxEvent.getId();
allEvents.forEach((e, i) => {
if (!lastEvent || wantsDateSeparator(lastEvent.getDate() || undefined, e.getDate() || undefined)) {
const separatorRoomId = e.getRoomId()!;
const separatorTs = e.getTs();
nodes.push(
<li key={e.getTs() + "~"}>
<DateSeparator roomId={e.getRoomId()!} ts={e.getTs()} />
<li key={`${separatorRoomId}-${separatorTs}~`}>
<DateSeparatorView vm={this.getDateSeparatorVm(separatorRoomId, separatorTs)} />
</li>,
);
}
@@ -1,344 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
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 { Direction, ConnectionError, MatrixError, HTTPError } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { capitalize } from "lodash";
import { ChevronDownIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { TimelineSeparator } from "@element-hq/web-shared-components";
import { _t, getUserLanguage } from "../../../languageHandler";
import { formatFullDateNoDay, formatFullDateNoTime, getDaysArray } from "../../../DateUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import BugReportDialog from "../dialogs/BugReportDialog";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import { contextMenuBelow } from "../rooms/RoomTile";
import { ContextMenuTooltipButton } from "../../structures/ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import JumpToDatePicker from "./JumpToDatePicker";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import RoomContext from "../../../contexts/RoomContext";
interface IProps {
roomId: string;
ts: number;
forExport?: boolean;
}
interface IState {
contextMenuPosition?: DOMRect;
jumpToDateEnabled: boolean;
}
/**
* Timeline separator component to render within a MessagePanel bearing the date of the ts given
*
* Has additional jump to date functionality when labs flag is enabled
*/
export default class DateSeparator extends React.Component<IProps, IState> {
public static contextType = RoomContext;
declare public context: React.ContextType<typeof RoomContext>;
private settingWatcherRef?: string;
public constructor(props: IProps) {
super(props);
this.state = {
jumpToDateEnabled: SettingsStore.getValue("feature_jump_to_date"),
};
}
public componentDidMount(): void {
// We're using a watcher so the date headers in the timeline are updated
// when the lab setting is toggled.
this.settingWatcherRef = SettingsStore.watchSetting(
"feature_jump_to_date",
null,
(settingName, roomId, level, newValAtLevel, newVal) => {
this.setState({ jumpToDateEnabled: newVal });
},
);
}
public componentWillUnmount(): void {
SettingsStore.unwatchSetting(this.settingWatcherRef);
}
private onContextMenuOpenClick = (e: ButtonEvent): void => {
e.preventDefault();
e.stopPropagation();
const target = e.target as HTMLButtonElement;
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
};
private onContextMenuCloseClick = (): void => {
this.closeMenu();
};
private closeMenu = (): void => {
this.setState({
contextMenuPosition: undefined,
});
};
private get relativeTimeFormat(): Intl.RelativeTimeFormat {
return new Intl.RelativeTimeFormat(getUserLanguage(), { style: "long", numeric: "auto" });
}
private getLabel(): string {
try {
const date = new Date(this.props.ts);
const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates);
// During the time the archive is being viewed, a specific day might not make sense, so we return the full date
if (this.props.forExport || disableRelativeTimestamps) return formatFullDateNoTime(date);
const today = new Date();
const yesterday = new Date();
const days = getDaysArray("long");
yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) {
return this.relativeTimeFormat.format(0, "day"); // Today
} else if (date.toDateString() === yesterday.toDateString()) {
return this.relativeTimeFormat.format(-1, "day"); // Yesterday
} else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()]; // Sunday-Saturday
} else {
return formatFullDateNoTime(date);
}
} catch {
return _t("common|message_timestamp_invalid");
}
}
private pickDate = async (inputTimestamp: number | string | Date): Promise<void> => {
const unixTimestamp = new Date(inputTimestamp).getTime();
const roomIdForJumpRequest = this.props.roomId;
try {
const cli = MatrixClientPeg.safeGet();
const { event_id: eventId, origin_server_ts: originServerTs } = await cli.timestampToEvent(
roomIdForJumpRequest,
unixTimestamp,
Direction.Forward,
);
logger.log(
`/timestamp_to_event: ` +
`found ${eventId} (${originServerTs}) for timestamp=${unixTimestamp} (looking forward)`,
);
// Only try to navigate to the room if the user is still viewing the same
// room. We don't want to jump someone back to a room after a slow request
// if they've already navigated away to another room.
const currentRoomId = this.context.roomViewStore.getRoomId();
if (currentRoomId === roomIdForJumpRequest) {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
event_id: eventId,
highlighted: true,
room_id: roomIdForJumpRequest,
metricsTrigger: undefined, // room doesn't change
});
} else {
logger.debug(
`No longer navigating to date in room (jump to date) because the user already switched ` +
`to another room: currentRoomId=${currentRoomId}, roomIdForJumpRequest=${roomIdForJumpRequest}`,
);
}
} catch (err) {
logger.error(
`Error occured while trying to find event in ${roomIdForJumpRequest} ` +
`at timestamp=${unixTimestamp}:`,
err,
);
// Only display an error if the user is still viewing the same room. We
// don't want to worry someone about an error in a room they no longer care
// about after a slow request if they've already navigated away to another
// room.
const currentRoomId = this.context.roomViewStore.getRoomId();
if (currentRoomId === roomIdForJumpRequest) {
let friendlyErrorMessage = "An error occured while trying to find and jump to the given date.";
let submitDebugLogsContent: JSX.Element = <></>;
if (err instanceof ConnectionError) {
friendlyErrorMessage = _t("room|error_jump_to_date_connection");
} else if (err instanceof MatrixError) {
if (err?.errcode === "M_NOT_FOUND") {
friendlyErrorMessage = _t("room|error_jump_to_date_not_found", {
dateString: formatFullDateNoDay(new Date(unixTimestamp)),
});
} else {
friendlyErrorMessage = _t("room|error_jump_to_date", {
statusCode: err?.httpStatus || _t("room|unknown_status_code_for_timeline_jump"),
errorCode: err?.errcode || _t("common|unavailable"),
});
}
} else if (err instanceof HTTPError) {
friendlyErrorMessage = err.message;
} else {
// We only give the option to submit logs for actual errors, not network problems.
submitDebugLogsContent = (
<p>
{_t(
"room|error_jump_to_date_send_logs_prompt",
{},
{
debugLogsLink: (sub) => (
<AccessibleButton
// This is by default a `<div>` which we
// can't nest within a `<p>` here so update
// this to a be a inline anchor element.
element="a"
kind="link"
onClick={() => this.onBugReport(err instanceof Error ? err : undefined)}
data-testid="jump-to-date-error-submit-debug-logs-button"
>
{sub}
</AccessibleButton>
),
},
)}
</p>
);
}
Modal.createDialog(ErrorDialog, {
title: _t("room|error_jump_to_date_title"),
description: (
<div data-testid="jump-to-date-error-content">
<p>{friendlyErrorMessage}</p>
{submitDebugLogsContent}
<details>
<summary>{_t("room|error_jump_to_date_details")}</summary>
<p>{String(err)}</p>
</details>
</div>
),
});
}
}
};
private onBugReport = (err?: Error): void => {
Modal.createDialog(BugReportDialog, {
error: err,
initialText: "Error occured while using jump to date #jump-to-date",
});
};
private onLastWeekClicked = (): void => {
const date = new Date();
date.setDate(date.getDate() - 7);
this.pickDate(date);
this.closeMenu();
};
private onLastMonthClicked = (): void => {
const date = new Date();
// Month numbers are 0 - 11 and `setMonth` handles the negative rollover
date.setMonth(date.getMonth() - 1, 1);
this.pickDate(date);
this.closeMenu();
};
private onTheBeginningClicked = (): void => {
const date = new Date(0);
this.pickDate(date);
this.closeMenu();
};
private onDatePicked = (dateString: string): void => {
this.pickDate(dateString);
this.closeMenu();
};
private renderJumpToDateMenu(): React.ReactElement {
let contextMenu: JSX.Element | undefined;
if (this.state.contextMenuPosition) {
const relativeTimeFormat = this.relativeTimeFormat;
contextMenu = (
<IconizedContextMenu
{...contextMenuBelow(this.state.contextMenuPosition)}
onFinished={this.onContextMenuCloseClick}
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={capitalize(relativeTimeFormat.format(-1, "week"))}
onClick={this.onLastWeekClicked}
data-testid="jump-to-date-last-week"
/>
<IconizedContextMenuOption
label={capitalize(relativeTimeFormat.format(-1, "month"))}
onClick={this.onLastMonthClicked}
data-testid="jump-to-date-last-month"
/>
<IconizedContextMenuOption
label={_t("room|jump_to_date_beginning")}
onClick={this.onTheBeginningClicked}
data-testid="jump-to-date-beginning"
/>
</IconizedContextMenuOptionList>
<IconizedContextMenuOptionList>
<JumpToDatePicker ts={this.props.ts} onDatePicked={this.onDatePicked} />
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
}
return (
<ContextMenuTooltipButton
className="mx_DateSeparator_jumpToDateMenu mx_DateSeparator_dateContent"
data-testid="jump-to-date-separator-button"
onClick={this.onContextMenuOpenClick}
isExpanded={!!this.state.contextMenuPosition}
title={_t("room|jump_to_date")}
>
<h2 className="mx_DateSeparator_dateHeading" aria-hidden="true">
{this.getLabel()}
</h2>
<ChevronDownIcon className="mx_DateSeparator_chevron" />
{contextMenu}
</ContextMenuTooltipButton>
);
}
public render(): React.ReactNode {
const label = this.getLabel();
let dateHeaderContent: JSX.Element;
if (this.state.jumpToDateEnabled && !this.props.forExport) {
dateHeaderContent = this.renderJumpToDateMenu();
} else {
dateHeaderContent = (
<div className="mx_DateSeparator_dateContent">
<h2 className="mx_DateSeparator_dateHeading" aria-hidden="true">
{label}
</h2>
</div>
);
}
return (
<TimelineSeparator label={label} className="mx_TimelineSeparator">
{dateHeaderContent}
</TimelineSeparator>
);
}
}
@@ -1,64 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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, { useState, type FormEvent } from "react";
import { _t } from "../../../languageHandler";
import Field from "../elements/Field";
import { RovingAccessibleButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
import { formatDateForInput } from "../../../DateUtils";
interface IProps {
ts: number;
onDatePicked: (dateString: string) => void;
}
const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
const date = new Date(ts);
const dateInputDefaultValue = formatDateForInput(date);
const [dateValue, setDateValue] = useState(dateInputDefaultValue);
const [onFocus, isActive, refCallback] = useRovingTabIndex<HTMLInputElement>();
const onDateValueInput = (ev: React.InputEvent<HTMLInputElement>): void => setDateValue(ev.currentTarget.value);
const onJumpToDateSubmit = (ev: FormEvent): void => {
ev.preventDefault();
onDatePicked(dateValue);
};
return (
<form className="mx_JumpToDatePicker_form" onSubmit={onJumpToDateSubmit}>
<span className="mx_JumpToDatePicker_label">{_t("room|jump_to_date")}</span>
<Field
element="input"
type="date"
onInput={onDateValueInput}
value={dateValue}
// Prevent people from selecting a day in the future (there won't be any
// events there anyway).
max={formatDateForInput(new Date())}
className="mx_JumpToDatePicker_datePicker"
label={_t("room|jump_to_date_prompt")}
onFocus={onFocus}
inputRef={refCallback}
tabIndex={isActive ? 0 : -1}
/>
<RovingAccessibleButton
element="button"
type="submit"
kind="primary"
className="mx_JumpToDatePicker_submitButton"
onClick={onJumpToDateSubmit}
>
{_t("action|go")}
</RovingAccessibleButton>
</form>
);
};
export default JumpToDatePicker;
@@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { type JSX } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { DateSeparatorView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import SettingsStore from "../../../settings/SettingsStore";
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import DateSeparator from "../messages/DateSeparator";
import EventTile from "./EventTile";
import { shouldFormContinuation } from "../../structures/MessagePanel";
import { wantsDateSeparator } from "../../../DateUtils";
@@ -21,6 +21,7 @@ import type LegacyCallEventGrouper from "../../structures/LegacyCallEventGrouper
import { buildLegacyCallEventGroupers } from "../../structures/LegacyCallEventGrouper";
import { haveRendererForEvent } from "../../../events/EventTileFactory";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { DateSeparatorViewModel } from "../../../viewmodels/timeline/DateSeparatorViewModel";
interface IProps {
// a list of strings to be highlighted in the results
@@ -34,6 +35,14 @@ interface IProps {
permalinkCreator?: RoomPermalinkCreator;
}
/**
* Creates and auto-disposes the DateSeparatorViewModel for search result rendering.
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): JSX.Element {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
return <DateSeparatorView vm={vm} />;
}
export default class SearchResultTile extends React.Component<IProps> {
public static contextType = RoomContext;
declare public context: React.ContextType<typeof RoomContext>;
@@ -57,7 +66,10 @@ export default class SearchResultTile extends React.Component<IProps> {
const eventId = resultEvent.getId();
const ts1 = resultEvent.getTs();
const ret = [<DateSeparator key={ts1 + "-search"} roomId={resultEvent.getRoomId()!} ts={ts1} />];
const separatorRoomId = resultEvent.getRoomId()!;
const ret = [
<DateSeparatorWrapper key={`${separatorRoomId}-${ts1}-search`} roomId={separatorRoomId} ts={ts1} />,
];
const layout = SettingsStore.getValue("layout");
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");