Move clock into shared components (#30480)
* refactor: extract `formatSeconds` from `DateUtils` * refactor: move clock into shared-components * refactor: update clock imports * test(e2e): add screenshots
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
@@ -13,6 +13,8 @@ import { type Optional } from "matrix-events-sdk";
|
|||||||
import { _t, getUserLanguage } from "./languageHandler";
|
import { _t, getUserLanguage } from "./languageHandler";
|
||||||
import { getUserTimezone } from "./TimezoneHandler";
|
import { getUserTimezone } from "./TimezoneHandler";
|
||||||
|
|
||||||
|
export { formatSeconds } from "./shared-components/utils/DateUtils";
|
||||||
|
|
||||||
export const MINUTE_MS = 60000;
|
export const MINUTE_MS = 60000;
|
||||||
export const HOUR_MS = MINUTE_MS * 60;
|
export const HOUR_MS = MINUTE_MS * 60;
|
||||||
export const DAY_MS = HOUR_MS * 24;
|
export const DAY_MS = HOUR_MS * 24;
|
||||||
@@ -180,31 +182,6 @@ export function formatTime(date: Date, showTwelveHour = false, locale?: string):
|
|||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSeconds(inSeconds: number): string {
|
|
||||||
const isNegative = inSeconds < 0;
|
|
||||||
inSeconds = Math.abs(inSeconds);
|
|
||||||
|
|
||||||
const hours = Math.floor(inSeconds / (60 * 60))
|
|
||||||
.toFixed(0)
|
|
||||||
.padStart(2, "0");
|
|
||||||
const minutes = Math.floor((inSeconds % (60 * 60)) / 60)
|
|
||||||
.toFixed(0)
|
|
||||||
.padStart(2, "0");
|
|
||||||
const seconds = Math.floor((inSeconds % (60 * 60)) % 60)
|
|
||||||
.toFixed(0)
|
|
||||||
.padStart(2, "0");
|
|
||||||
|
|
||||||
let output = "";
|
|
||||||
if (hours !== "00") output += `${hours}:`;
|
|
||||||
output += `${minutes}:${seconds}`;
|
|
||||||
|
|
||||||
if (isNegative) {
|
|
||||||
output = "-" + output;
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatTimeLeft(inSeconds: number): string {
|
export function formatTimeLeft(inSeconds: number): string {
|
||||||
const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0);
|
const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0);
|
||||||
const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0);
|
const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import Clock from "./Clock";
|
import { Clock } from "../../../shared-components/audio/Clock";
|
||||||
import { type Playback } from "../../../audio/Playback";
|
import { type Playback } from "../../../audio/Playback";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { type IRecordingUpdate } from "../../../audio/VoiceRecording";
|
import { type IRecordingUpdate } from "../../../audio/VoiceRecording";
|
||||||
import Clock from "./Clock";
|
import { Clock } from "../../../shared-components/audio/Clock";
|
||||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||||
import { type VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
import { type VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import Clock from "./Clock";
|
import { Clock } from "../../../shared-components/audio/Clock";
|
||||||
import { type Playback, PlaybackState } from "../../../audio/Playback";
|
import { type Playback, PlaybackState } from "../../../audio/Playback";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { LegacyCallEventGrouperEvent } from "../../structures/LegacyCallEventGro
|
|||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import InfoTooltip, { InfoTooltipKind } from "../elements/InfoTooltip";
|
import InfoTooltip, { InfoTooltipKind } from "../elements/InfoTooltip";
|
||||||
import { formatPreciseDuration } from "../../../DateUtils";
|
import { formatPreciseDuration } from "../../../DateUtils";
|
||||||
import Clock from "../audio_messages/Clock";
|
import { Clock } from "../../../shared-components/audio/Clock";
|
||||||
|
|
||||||
const MAX_NON_NARROW_WIDTH = (450 / 70) * 100;
|
const MAX_NON_NARROW_WIDTH = (450 / 70) * 100;
|
||||||
|
|
||||||
|
|||||||
29
src/shared-components/audio/Clock/Clock.stories.tsx
Normal file
29
src/shared-components/audio/Clock/Clock.stories.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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 from "react";
|
||||||
|
|
||||||
|
import type { Meta, StoryFn } from "@storybook/react-vite";
|
||||||
|
import { Clock } from "./Clock";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Audio/Clock",
|
||||||
|
component: Clock,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
args: {
|
||||||
|
seconds: 20,
|
||||||
|
},
|
||||||
|
} as Meta<typeof Clock>;
|
||||||
|
|
||||||
|
const Template: StoryFn<typeof Clock> = (args) => <Clock {...args} />;
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
|
||||||
|
export const LotOfSeconds = Template.bind({});
|
||||||
|
LotOfSeconds.args = {
|
||||||
|
seconds: 99999999999999,
|
||||||
|
};
|
||||||
26
src/shared-components/audio/Clock/Clock.test.tsx
Normal file
26
src/shared-components/audio/Clock/Clock.test.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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 { composeStories } from "@storybook/react-vite";
|
||||||
|
import { render } from "jest-matrix-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import * as stories from "./Clock.stories.tsx";
|
||||||
|
|
||||||
|
const { Default, LotOfSeconds } = composeStories(stories);
|
||||||
|
|
||||||
|
describe("Clock", () => {
|
||||||
|
it("renders the clock", () => {
|
||||||
|
const { container } = render(<Default />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the clock with a lot of seconds", () => {
|
||||||
|
const { container } = render(<LotOfSeconds />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024 New Vector Ltd.
|
||||||
Copyright 2021-2023 The Matrix.org Foundation C.I.C.
|
Copyright 2021-2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
@@ -9,24 +8,18 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React, { type HTMLProps } from "react";
|
import React, { type HTMLProps } from "react";
|
||||||
import { Temporal } from "temporal-polyfill";
|
import { Temporal } from "temporal-polyfill";
|
||||||
|
|
||||||
import { formatSeconds } from "../../../DateUtils";
|
import { formatSeconds } from "../../utils/DateUtils";
|
||||||
|
|
||||||
interface Props extends Pick<HTMLProps<HTMLSpanElement>, "aria-live" | "role"> {
|
export interface Props extends Pick<HTMLProps<HTMLSpanElement>, "aria-live" | "role"> {
|
||||||
seconds: number;
|
seconds: number;
|
||||||
formatFn: (seconds: number) => string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clock which represents time periods rather than absolute time.
|
* Clock which represents time periods rather than absolute time.
|
||||||
* Simply converts seconds using formatFn.
|
* Simply converts seconds using formatSeconds().
|
||||||
* Defaulting to formatSeconds().
|
|
||||||
* Note that in this case hours will not be displayed, making it possible to see "82:29".
|
* Note that in this case hours will not be displayed, making it possible to see "82:29".
|
||||||
*/
|
*/
|
||||||
export default class Clock extends React.Component<Props> {
|
export class Clock extends React.Component<Props> {
|
||||||
public static defaultProps = {
|
|
||||||
formatFn: formatSeconds,
|
|
||||||
};
|
|
||||||
|
|
||||||
public shouldComponentUpdate(nextProps: Readonly<Props>): boolean {
|
public shouldComponentUpdate(nextProps: Readonly<Props>): boolean {
|
||||||
const currentFloor = Math.floor(this.props.seconds);
|
const currentFloor = Math.floor(this.props.seconds);
|
||||||
const nextFloor = Math.floor(nextProps.seconds);
|
const nextFloor = Math.floor(nextProps.seconds);
|
||||||
@@ -47,9 +40,10 @@ export default class Clock extends React.Component<Props> {
|
|||||||
dateTime={this.calculateDuration(seconds)}
|
dateTime={this.calculateDuration(seconds)}
|
||||||
aria-live={this.props["aria-live"]}
|
aria-live={this.props["aria-live"]}
|
||||||
role={role}
|
role={role}
|
||||||
|
/* Keep class for backward compatibility with parent component */
|
||||||
className="mx_Clock"
|
className="mx_Clock"
|
||||||
>
|
>
|
||||||
{this.props.formatFn(seconds)}
|
{formatSeconds(seconds)}
|
||||||
</time>
|
</time>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Clock renders the clock 1`] = `
|
||||||
|
<div>
|
||||||
|
<time
|
||||||
|
class="mx_Clock"
|
||||||
|
datetime="PT20S"
|
||||||
|
>
|
||||||
|
00:20
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Clock renders the clock with a lot of seconds 1`] = `
|
||||||
|
<div>
|
||||||
|
<time
|
||||||
|
class="mx_Clock"
|
||||||
|
datetime="PT27777777777H46M39S"
|
||||||
|
>
|
||||||
|
27777777777:46:39
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
8
src/shared-components/audio/Clock/index.tsx
Normal file
8
src/shared-components/audio/Clock/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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 { Clock } from "./Clock";
|
||||||
35
src/shared-components/utils/DateUtils.ts
Normal file
35
src/shared-components/utils/DateUtils.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a number of seconds into a human-readable string.
|
||||||
|
* @param inSeconds
|
||||||
|
*/
|
||||||
|
export function formatSeconds(inSeconds: number): string {
|
||||||
|
const isNegative = inSeconds < 0;
|
||||||
|
inSeconds = Math.abs(inSeconds);
|
||||||
|
|
||||||
|
const hours = Math.floor(inSeconds / (60 * 60))
|
||||||
|
.toFixed(0)
|
||||||
|
.padStart(2, "0");
|
||||||
|
const minutes = Math.floor((inSeconds % (60 * 60)) / 60)
|
||||||
|
.toFixed(0)
|
||||||
|
.padStart(2, "0");
|
||||||
|
const seconds = Math.floor((inSeconds % (60 * 60)) % 60)
|
||||||
|
.toFixed(0)
|
||||||
|
.padStart(2, "0");
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
if (hours !== "00") output += `${hours}:`;
|
||||||
|
output += `${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
if (isNegative) {
|
||||||
|
output = "-" + output;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user