From 7866a83399429dab9d6a6735d93c7e8fad99f6cd Mon Sep 17 00:00:00 2001 From: greentore Date: Tue, 18 Apr 2023 16:54:12 +0200 Subject: [PATCH] Switch to native Intl.DateTimeFormat --- package-lock.json | 9 --- package.json | 1 - src/app/atoms/time/Time.jsx | 44 -------------- src/app/atoms/time/Time.tsx | 36 +++++++++++ src/app/organisms/room/RoomViewContent.jsx | 8 +-- src/app/organisms/settings/DeviceManage.jsx | 6 +- src/app/organisms/settings/Settings.jsx | 12 +++- src/app/utils/time.ts | 67 +++++++++++++++++++++ src/client/action/settings.js | 6 ++ src/client/state/cons.js | 2 + src/client/state/settings.js | 15 +++++ 11 files changed, 144 insertions(+), 62 deletions(-) delete mode 100644 src/app/atoms/time/Time.jsx create mode 100644 src/app/atoms/time/Time.tsx create mode 100644 src/app/utils/time.ts diff --git a/package-lock.json b/package-lock.json index d5867ae16d..31c4123862 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "blurhash": "2.0.4", "browser-encrypt-attachment": "0.3.0", "classnames": "2.3.2", - "dateformat": "5.0.3", "emojibase": "6.1.0", "emojibase-data": "7.0.1", "file-saver": "2.0.5", @@ -2210,14 +2209,6 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, - "node_modules/dateformat": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-5.0.3.tgz", - "integrity": "sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA==", - "engines": { - "node": ">=12.20" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index 5eb3fa9885..d044b440cc 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "blurhash": "2.0.4", "browser-encrypt-attachment": "0.3.0", "classnames": "2.3.2", - "dateformat": "5.0.3", "emojibase": "6.1.0", "emojibase-data": "7.0.1", "file-saver": "2.0.5", diff --git a/src/app/atoms/time/Time.jsx b/src/app/atoms/time/Time.jsx deleted file mode 100644 index 750b958fcf..0000000000 --- a/src/app/atoms/time/Time.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import dateFormat from 'dateformat'; -import { isInSameDay } from '../../../util/common'; - -function Time({ timestamp, fullTime }) { - const date = new Date(timestamp); - - const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT'); - let formattedDate = formattedFullTime; - - if (!fullTime) { - const compareDate = new Date(); - const isToday = isInSameDay(date, compareDate); - compareDate.setDate(compareDate.getDate() - 1); - const isYesterday = isInSameDay(date, compareDate); - - formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy'); - if (isYesterday) { - formattedDate = `Yesterday, ${formattedDate}`; - } - } - - return ( - - ); -} - -Time.defaultProps = { - fullTime: false, -}; - -Time.propTypes = { - timestamp: PropTypes.number.isRequired, - fullTime: PropTypes.bool, -}; - -export default Time; diff --git a/src/app/atoms/time/Time.tsx b/src/app/atoms/time/Time.tsx new file mode 100644 index 0000000000..d6f4300a9b --- /dev/null +++ b/src/app/atoms/time/Time.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { isInSameDay, DateTime } from '../../utils/time'; + +interface TimeProps { + timestamp: number; + fullTime?: boolean; +} + +function Time({ timestamp, fullTime }: TimeProps) { + const date = new Date(timestamp); + + const formattedFullTime = DateTime.full(date); + let formattedDate = formattedFullTime; + + if (!fullTime) { + const compareDate = new Date(); + const isToday = isInSameDay(date, compareDate); + compareDate.setDate(compareDate.getDate() - 1); + const isYesterday = isInSameDay(date, compareDate); + + formattedDate = isToday || isYesterday ? DateTime.time(date) : DateTime.dateISO(date); + if (isYesterday) { + const yesterday = DateTime.relative(-1, 'day') ?? 'Yesterday'; + formattedDate = `${yesterday}, ${formattedDate}`; + } + } + + return ( + + ); +} + +export default Time; diff --git a/src/app/organisms/room/RoomViewContent.jsx b/src/app/organisms/room/RoomViewContent.jsx index 5726fe1191..1acd001bcf 100644 --- a/src/app/organisms/room/RoomViewContent.jsx +++ b/src/app/organisms/room/RoomViewContent.jsx @@ -7,14 +7,14 @@ import React, { import PropTypes from 'prop-types'; import './RoomViewContent.scss'; -import dateFormat from 'dateformat'; import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; import { openProfileViewer } from '../../../client/action/navigation'; -import { diffMinutes, isInSameDay, Throttle } from '../../../util/common'; +import { Throttle } from '../../../util/common'; +import { diffMinutes, isInSameDay, DateTime } from '../../utils/time' import { markAsRead } from '../../../client/action/notifications'; import Divider from '../../atoms/divider/Divider'; @@ -99,7 +99,7 @@ function RoomIntroContainer({ event, timeline }) { name={room.name} heading={twemojify(heading)} desc={desc} - time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null} + time={event ? `Created on ${DateTime.full(event.getDate())}` : null} /> ); } @@ -593,7 +593,7 @@ function RoomViewContent({ roomInputRef, eventId, roomTimeline }) { } const dayDivider = prevMEvent && !isInSameDay(mEvent.getDate(), prevMEvent.getDate()); if (dayDivider) { - tl.push(); + tl.push(); itemCountIndex += 1; } diff --git a/src/app/organisms/settings/DeviceManage.jsx b/src/app/organisms/settings/DeviceManage.jsx index 74738ea8b4..ab5142b095 100644 --- a/src/app/organisms/settings/DeviceManage.jsx +++ b/src/app/organisms/settings/DeviceManage.jsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react'; import './DeviceManage.scss'; -import dateFormat from 'dateformat'; import initMatrix from '../../../client/initMatrix'; import { isCrossVerified } from '../../../util/matrixUtil'; +import { DateTime } from '../../utils/time'; import { openReusableDialog, openEmojiVerification } from '../../../client/action/navigation'; import Text from '../../atoms/text/Text'; @@ -184,9 +184,9 @@ function DeviceManage() { Last activity - {dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')} + {` ${DateTime.full(new Date(lastTS))}`} - {lastIP ? ` at ${lastIP}` : ''} + {lastIP ? ` from ${lastIP}` : ''} )} {isCurrentDevice && ( diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index a0869b6154..506ba5c204 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -7,7 +7,7 @@ import settings from '../../../client/state/settings'; import navigation from '../../../client/state/navigation'; import { toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents, - toggleNotifications, toggleNotificationSounds, + toggleNotifications, toggleNotificationSounds, toggleTime12, } from '../../../client/action/settings'; import { usePermission } from '../../hooks/usePermission'; @@ -113,6 +113,16 @@ function AppearanceSection() { )} content={Hide nick and avatar change messages from room timeline.} /> + { toggleTime12(); updateState({}); }} + /> + )} + content={Show timestamps in 12-hour format.} + /> ); diff --git a/src/app/utils/time.ts b/src/app/utils/time.ts new file mode 100644 index 0000000000..da219e70e4 --- /dev/null +++ b/src/app/utils/time.ts @@ -0,0 +1,67 @@ +import cons from '../../client/state/cons'; +import settings from '../../client/state/settings'; + +function capitalize(string: string) { + return string.charAt(0).toLocaleUpperCase() + string.slice(1); +} + +const hourCycle = () => (settings.isTime12 ? 'h12' : 'h23'); + +let fullFormat: Intl.DateTimeFormat; +let dateFormat: Intl.DateTimeFormat; +let timeFormat: Intl.DateTimeFormat; +let relativeFormat: Intl.RelativeTimeFormat | undefined; + +function initDateFormats() { + fullFormat = new Intl.DateTimeFormat('en', { + hourCycle: hourCycle(), + dateStyle: 'full', + timeStyle: 'short', + }); + + dateFormat = new Intl.DateTimeFormat('en', { + dateStyle: 'long', + }); + + timeFormat = new Intl.DateTimeFormat('en', { + hourCycle: hourCycle(), + hour: 'numeric', + minute: 'numeric', + }); + + relativeFormat = Intl.RelativeTimeFormat + ? new Intl.RelativeTimeFormat('en', { numeric: 'auto' }) + : undefined; +} +initDateFormats(); + +export class DateTime { + static full = (date: Date) => fullFormat.format(date); + + static date = (date: Date) => dateFormat.format(date); + + static time = (date: Date) => timeFormat.format(date); + + static relative = (value: number, unit: Intl.RelativeTimeFormatUnit) => + relativeFormat ? capitalize(relativeFormat.format(value, unit)) : undefined; + + static dateISO = (date: Date) => date.toISOString().split('T')[0]; +} + +export function diffMinutes(dt2: Date, dt1: Date): number { + let diff = (dt2.getTime() - dt1.getTime()) / 1000; + diff /= 60; + return Math.abs(Math.round(diff)); +} + +export function isInSameDay(dt2: Date, dt1: Date): boolean { + return ( + dt2.getFullYear() === dt1.getFullYear() && + dt2.getMonth() === dt1.getMonth() && + dt2.getDate() === dt1.getDate() + ); +} + +settings.on(cons.events.settings.TIME12_TOGGLED, () => { + initDateFormats(); +}); diff --git a/src/client/action/settings.js b/src/client/action/settings.js index 7b539c8d74..e9c41c5e40 100644 --- a/src/client/action/settings.js +++ b/src/client/action/settings.js @@ -42,3 +42,9 @@ export function toggleNotificationSounds() { type: cons.actions.settings.TOGGLE_NOTIFICATION_SOUNDS, }); } + +export function toggleTime12() { + appDispatcher.dispatch({ + type: cons.actions.settings.TOGGLE_TIME12, + }); +} diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 8d9fda5439..09b8b14925 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -78,6 +78,7 @@ const cons = { TOGGLE_NICKAVATAR_EVENT: 'TOGGLE_NICKAVATAR_EVENT', TOGGLE_NOTIFICATIONS: 'TOGGLE_NOTIFICATIONS', TOGGLE_NOTIFICATION_SOUNDS: 'TOGGLE_NOTIFICATION_SOUNDS', + TOGGLE_TIME12: 'TOGGLE_TIME12', }, }, events: { @@ -150,6 +151,7 @@ const cons = { NICKAVATAR_EVENTS_TOGGLED: 'NICKAVATAR_EVENTS_TOGGLED', NOTIFICATIONS_TOGGLED: 'NOTIFICATIONS_TOGGLED', NOTIFICATION_SOUNDS_TOGGLED: 'NOTIFICATION_SOUNDS_TOGGLED', + TIME12_TOGGLED: 'TIME12_TOGGLED', }, }, }; diff --git a/src/client/state/settings.js b/src/client/state/settings.js index af2e279ad3..92f2a80aeb 100644 --- a/src/client/state/settings.js +++ b/src/client/state/settings.js @@ -33,6 +33,7 @@ class Settings extends EventEmitter { this.hideNickAvatarEvents = this.getHideNickAvatarEvents(); this._showNotifications = this.getShowNotifications(); this.isNotificationSounds = this.getIsNotificationSounds(); + this.isTime12 = this.getIsTime12(); this.darkModeQueryList = window.matchMedia('(prefers-color-scheme: dark)'); @@ -153,6 +154,15 @@ class Settings extends EventEmitter { return settings.isNotificationSounds; } + getIsTime12() { + if (typeof this.isTime12 === 'boolean') return this.isTime12; + + const settings = getSettings(); + if (settings === null) return false; + if (typeof settings.isTime12 === 'undefined') return false; + return settings.isTime12; + } + setter(action) { const actions = { [cons.actions.settings.TOGGLE_SYSTEM_THEME]: () => { @@ -192,6 +202,11 @@ class Settings extends EventEmitter { setSettings('isNotificationSounds', this.isNotificationSounds); this.emit(cons.events.settings.NOTIFICATION_SOUNDS_TOGGLED, this.isNotificationSounds); }, + [cons.actions.settings.TOGGLE_TIME12]: () => { + this.isTime12 = !this.isTime12; + setSettings('isTime12', this.isTime12); + this.emit(cons.events.settings.TIME12_TOGGLED, this.isTime12); + }, }; actions[action.type]?.();