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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user