From 2a20d9a7df195384e4ae16566970b2fed516b2cf Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 3 Nov 2021 10:16:50 +0000 Subject: [PATCH] Display relative time in thread panel event tile (#7068) * Null-guard for missing root event in thread panel * Add formatRelativeTime date utility * Display relative time format in thread panel event tiles --- src/DateUtils.ts | 25 ++++++++++- .../views/messages/MessageTimestamp.tsx | 7 ++- src/components/views/rooms/EventTile.tsx | 6 ++- test/utils/DateUtils-test.ts | 43 ++++++++++++++++++- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/DateUtils.ts b/src/DateUtils.ts index 221ecfeb2a..c9f33b2eee 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -149,12 +149,20 @@ export function formatSeconds(inSeconds: number): string { } const MILLIS_IN_DAY = 86400000; +function withinPast24Hours(prevDate: Date, nextDate: Date): boolean { + return Math.abs(prevDate.getTime() - nextDate.getTime()) <= MILLIS_IN_DAY; +} + +function withinCurrentYear(prevDate: Date, nextDate: Date): boolean { + return prevDate.getFullYear() === nextDate.getFullYear(); +} + export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean { if (!nextEventDate || !prevEventDate) { return false; } // Return early for events that are > 24h apart - if (Math.abs(prevEventDate.getTime() - nextEventDate.getTime()) > MILLIS_IN_DAY) { + if (!withinPast24Hours(prevEventDate, nextEventDate)) { return true; } @@ -178,3 +186,18 @@ export function formatFullDateNoDayNoTime(date: Date) { pad(date.getDate()) ); } + +export function formatRelativeTime(date: Date, showTwelveHour = false): string { + const now = new Date(Date.now()); + if (withinPast24Hours(date, now)) { + return formatTime(date, showTwelveHour); + } else { + const months = getMonthsArray(); + let relativeDate = `${months[date.getMonth()]} ${date.getDate()}`; + + if (!withinCurrentYear(date, now)) { + relativeDate += `, ${date.getFullYear()}`; + } + return relativeDate; + } +} diff --git a/src/components/views/messages/MessageTimestamp.tsx b/src/components/views/messages/MessageTimestamp.tsx index a657032c86..b1bc5413fe 100644 --- a/src/components/views/messages/MessageTimestamp.tsx +++ b/src/components/views/messages/MessageTimestamp.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; -import { formatFullDate, formatTime, formatFullTime } from '../../../DateUtils'; +import { formatFullDate, formatTime, formatFullTime, formatRelativeTime } from '../../../DateUtils'; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { @@ -24,6 +24,7 @@ interface IProps { showTwelveHour?: boolean; showFullDate?: boolean; showSeconds?: boolean; + showRelative?: boolean; } @replaceableComponent("views.messages.MessageTimestamp") @@ -31,7 +32,9 @@ export default class MessageTimestamp extends React.Component { public render() { const date = new Date(this.props.ts); let timestamp; - if (this.props.showFullDate) { + if (this.props.showRelative) { + timestamp = formatRelativeTime(date, this.props.showTwelveHour); + } else if (this.props.showFullDate) { timestamp = formatFullDate(date, this.props.showTwelveHour, this.props.showSeconds); } else if (this.props.showSeconds) { timestamp = formatFullTime(date, this.props.showTwelveHour); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index c3a561e4c1..63cc53e5b1 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1132,7 +1132,11 @@ export default class EventTile extends React.Component { || this.state.actionBarFocused); const timestamp = showTimestamp ? - : null; + : null; const keyRequestHelpText =
diff --git a/test/utils/DateUtils-test.ts b/test/utils/DateUtils-test.ts index ad0530c87e..4b9680e373 100644 --- a/test/utils/DateUtils-test.ts +++ b/test/utils/DateUtils-test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { formatSeconds } from "../../src/DateUtils"; +import { formatSeconds, formatRelativeTime } from "../../src/DateUtils"; describe("formatSeconds", () => { it("correctly formats time with hours", () => { @@ -29,3 +29,44 @@ describe("formatSeconds", () => { expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (0))).toBe("31:00"); }); }); + +describe("formatRelativeTime", () => { + let dateSpy; + beforeAll(() => { + dateSpy = jest + .spyOn(global.Date, 'now') + // Tuesday, 2 November 2021 11:18:03 + .mockImplementation(() => 1635851883000); + }); + + afterAll(() => { + dateSpy.mockRestore(); + }); + + it("returns hour format for events created less than 24 hours ago", () => { + const date = new Date(1635850883000); + expect(formatRelativeTime(date)).toBe("11:01"); + }); + + it("honours the hour format setting", () => { + const date = new Date(1635850883000); + expect(formatRelativeTime(date)).toBe("11:01"); + expect(formatRelativeTime(date, false)).toBe("11:01"); + expect(formatRelativeTime(date, true)).toBe("11:01AM"); + }); + + it("returns month and day for events created in the current year", () => { + const date = new Date(1632567741000); + expect(formatRelativeTime(date, true)).toBe("Sep 25"); + }); + + it("does not return a leading 0 for single digit days", () => { + const date = new Date(1635764541000); + expect(formatRelativeTime(date, true)).toBe("Nov 1"); + }); + + it("appends the year for events created in previous years", () => { + const date = new Date(1604142141000); + expect(formatRelativeTime(date, true)).toBe("Oct 31, 2020"); + }); +});