Add settings to enable 24-hour time format and customizable date format (#2347)
authorGimle Larpes <97182804+GimleLarpes@users.noreply.github.com>
Sun, 27 Jul 2025 12:13:00 +0000 (15:13 +0300)
committerGitHub <noreply@github.com>
Sun, 27 Jul 2025 12:13:00 +0000 (22:13 +1000)
* Add setting to enable 24-hour time format

* added hour24Clock to TimeProps

* Add incomplete dateFormatString setting

* Move 24-hour  toggle to Appearance

* Add "Date & Time" subheading, cleanup after merge

* Add setting for date formatting

* Fix minor formatting and naming issues

* Document functions

* adress most comments

* add hint for date formatting

* add support for 24hr time to TimePicker

* prevent overflow on small displays

17 files changed:
src/app/atoms/time/Time.jsx
src/app/components/message/Time.tsx
src/app/components/room-intro/RoomIntro.tsx
src/app/components/time-date/TimePicker.tsx
src/app/features/message-search/MessageSearch.tsx
src/app/features/message-search/SearchResultGroup.tsx
src/app/features/room/RoomTimeline.tsx
src/app/features/room/jump-to-time/JumpToTime.tsx
src/app/features/room/message/Message.tsx
src/app/features/room/room-pin-menu/RoomPinMenu.tsx
src/app/features/settings/devices/DeviceTile.tsx
src/app/features/settings/general/General.tsx
src/app/hooks/useDateFormat.ts [new file with mode: 0644]
src/app/pages/client/inbox/Invites.tsx
src/app/pages/client/inbox/Notifications.tsx
src/app/state/settings.ts
src/app/utils/time.ts

index 750b958fcf5f045b877d936eb8e61f4e8343280d..d7bbe43962c0c9d058219e0a4e8852696e0d79c1 100644 (file)
@@ -4,10 +4,25 @@ import PropTypes from 'prop-types';
 import dateFormat from 'dateformat';
 import { isInSameDay } from '../../../util/common';
 
-function Time({ timestamp, fullTime }) {
+/**
+ * Renders a formatted timestamp.
+ *
+ * Displays the time in hour:minute format if the message is from today or yesterday, unless `fullTime` is true.
+ * For older messages, it shows the date and time.
+ *
+ * @param {number} timestamp - The timestamp to display.
+ * @param {boolean} [fullTime=false] - If true, always show the full date and time.
+ * @param {boolean} hour24Clock - Whether to use 24-hour time format.
+ * @param {string} dateFormatString - Format string for the date part.
+ * @returns {JSX.Element} A <time> element with the formatted date/time.
+ */
+function Time({ timestamp, fullTime, hour24Clock, dateFormatString }) {
   const date = new Date(timestamp);
 
-  const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
+  const formattedFullTime = dateFormat(
+    date,
+    hour24Clock ? 'dd mmmm yyyy, HH:MM' : 'dd mmmm yyyy, hh:MM TT'
+  );
   let formattedDate = formattedFullTime;
 
   if (!fullTime) {
@@ -16,17 +31,19 @@ function Time({ timestamp, fullTime }) {
     compareDate.setDate(compareDate.getDate() - 1);
     const isYesterday = isInSameDay(date, compareDate);
 
-    formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy');
+    const timeFormat = hour24Clock ? 'HH:MM' : 'hh:MM TT';
+
+    formattedDate = dateFormat(
+      date,
+      isToday || isYesterday ? timeFormat : dateFormatString.toLowerCase()
+    );
     if (isYesterday) {
       formattedDate = `Yesterday, ${formattedDate}`;
     }
   }
 
   return (
-    <time
-      dateTime={date.toISOString()}
-      title={formattedFullTime}
-    >
+    <time dateTime={date.toISOString()} title={formattedFullTime}>
       {formattedDate}
     </time>
   );
@@ -39,6 +56,8 @@ Time.defaultProps = {
 Time.propTypes = {
   timestamp: PropTypes.number.isRequired,
   fullTime: PropTypes.bool,
+  hour24Clock: PropTypes.bool.isRequired,
+  dateFormatString: PropTypes.string.isRequired,
 };
 
 export default Time;
index a5126015f4de4a01591ed847fcdeeda58cf76c71..3eab5cc2f1ac99f7c1e68e79705e60cc08da95ae 100644 (file)
@@ -5,19 +5,35 @@ import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/ti
 export type TimeProps = {
   compact?: boolean;
   ts: number;
+  hour24Clock: boolean;
+  dateFormatString: string;
 };
 
+/**
+ * Renders a formatted timestamp, supporting compact and full display modes.
+ *
+ * Displays the time in hour:minute format if the message is from today, yesterday, or if `compact` is true.
+ * For older messages, it shows the date and time.
+ *
+ * @param {number} ts - The timestamp to display.
+ * @param {boolean} [compact=false] - If true, always show only the time.
+ * @param {boolean} hour24Clock - Whether to use 24-hour time format.
+ * @param {string} dateFormatString - Format string for the date part.
+ * @returns {React.ReactElement} A <Text as="time"> element with the formatted date/time.
+ */
 export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
-  ({ compact, ts, ...props }, ref) => {
+  ({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => {
+    const formattedTime = timeHourMinute(ts, hour24Clock);
+
     let time = '';
     if (compact) {
-      time = timeHourMinute(ts);
+      time = formattedTime;
     } else if (today(ts)) {
-      time = timeHourMinute(ts);
+      time = formattedTime;
     } else if (yesterday(ts)) {
-      time = `Yesterday ${timeHourMinute(ts)}`;
+      time = `Yesterday ${formattedTime}`;
     } else {
-      time = `${timeDayMonYear(ts)} ${timeHourMinute(ts)}`;
+      time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
     }
 
     return (
index 9e1a4f12fd4cd5b3a1499bef34c57559d7370a23..b02d9f5a902968b1d448c0cdae6a1d5138745f52 100644 (file)
@@ -15,6 +15,8 @@ import { nameInitials } from '../../utils/common';
 import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
 import { mDirectAtom } from '../../state/mDirectList';
 import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { useSetting } from '../../state/hooks/settings';
+import { settingsAtom } from '../../state/settings';
 
 export type RoomIntroProps = {
   room: Room;
@@ -43,6 +45,8 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
     useCallback(async (roomId: string) => mx.joinRoom(roomId), [mx])
   );
 
+  const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+
   return (
     <Box direction="Column" grow="Yes" gap="500" {...props} ref={ref}>
       <Box>
@@ -67,7 +71,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
             <Text size="T200" priority="300">
               {'Created by '}
               <b>@{creatorName}</b>
-              {` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts)}`}
+              {` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts, hour24Clock)}`}
             </Text>
           )}
         </Box>
index 1dd0958bd4459b67c2c00fd0763a653eee9a67f1..c835ed09636e34aad885fb101b46ec9fe287dc11 100644 (file)
@@ -4,6 +4,8 @@ import dayjs from 'dayjs';
 import * as css from './styles.css';
 import { PickerColumn } from './PickerColumn';
 import { hour12to24, hour24to12, hoursToMs, inSameDay, minutesToMs } from '../../utils/time';
+import { useSetting } from '../../state/hooks/settings';
+import { settingsAtom } from '../../state/settings';
 
 type TimePickerProps = {
   min: number;
@@ -13,9 +15,11 @@ type TimePickerProps = {
 };
 export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
   ({ min, max, value, onChange }, ref) => {
+    const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+
     const hour24 = dayjs(value).hour();
 
-    const selectedHour = hour24to12(hour24);
+    const selectedHour = hour24Clock ? hour24 : hour24to12(hour24);
     const selectedMinute = dayjs(value).minute();
     const selectedPM = hour24 >= 12;
 
@@ -24,7 +28,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
     };
 
     const handleHour = (hour: number) => {
-      const seconds = hoursToMs(hour12to24(hour, selectedPM));
+      const seconds = hoursToMs(hour24Clock ? hour : hour12to24(hour, selectedPM));
       const lastSeconds = hoursToMs(hour24);
       const newValue = value + (seconds - lastSeconds);
       handleSubmit(newValue);
@@ -59,28 +63,43 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
       <Menu className={css.PickerMenu} ref={ref}>
         <Box direction="Row" gap="200" className={css.PickerContainer}>
           <PickerColumn title="Hour">
-            {Array.from(Array(12).keys())
-              .map((i) => {
-                if (i === 0) return 12;
-                return i;
-              })
-              .map((hour) => (
-                <Chip
-                  key={hour}
-                  size="500"
-                  variant={hour === selectedHour ? 'Primary' : 'Background'}
-                  fill="None"
-                  radii="300"
-                  aria-selected={hour === selectedHour}
-                  onClick={() => handleHour(hour)}
-                  disabled={
-                    (minDay && hour12to24(hour, selectedPM) < minHour24) ||
-                    (maxDay && hour12to24(hour, selectedPM) > maxHour24)
-                  }
-                >
-                  <Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
-                </Chip>
-              ))}
+            {hour24Clock
+              ? Array.from(Array(24).keys()).map((hour) => (
+                  <Chip
+                    key={hour}
+                    size="500"
+                    variant={hour === selectedHour ? 'Primary' : 'Background'}
+                    fill="None"
+                    radii="300"
+                    aria-selected={hour === selectedHour}
+                    onClick={() => handleHour(hour)}
+                    disabled={(minDay && hour < minHour24) || (maxDay && hour > maxHour24)}
+                  >
+                    <Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
+                  </Chip>
+                ))
+              : Array.from(Array(12).keys())
+                  .map((i) => {
+                    if (i === 0) return 12;
+                    return i;
+                  })
+                  .map((hour) => (
+                    <Chip
+                      key={hour}
+                      size="500"
+                      variant={hour === selectedHour ? 'Primary' : 'Background'}
+                      fill="None"
+                      radii="300"
+                      aria-selected={hour === selectedHour}
+                      onClick={() => handleHour(hour)}
+                      disabled={
+                        (minDay && hour12to24(hour, selectedPM) < minHour24) ||
+                        (maxDay && hour12to24(hour, selectedPM) > maxHour24)
+                      }
+                    >
+                      <Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
+                    </Chip>
+                  ))}
           </PickerColumn>
           <PickerColumn title="Minutes">
             {Array.from(Array(60).keys()).map((minute) => (
@@ -101,30 +120,32 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
               </Chip>
             ))}
           </PickerColumn>
-          <PickerColumn title="Period">
-            <Chip
-              size="500"
-              variant={!selectedPM ? 'Primary' : 'SurfaceVariant'}
-              fill="None"
-              radii="300"
-              aria-selected={!selectedPM}
-              onClick={() => handlePeriod(false)}
-              disabled={minDay && minPM}
-            >
-              <Text size="T300">AM</Text>
-            </Chip>
-            <Chip
-              size="500"
-              variant={selectedPM ? 'Primary' : 'SurfaceVariant'}
-              fill="None"
-              radii="300"
-              aria-selected={selectedPM}
-              onClick={() => handlePeriod(true)}
-              disabled={maxDay && !maxPM}
-            >
-              <Text size="T300">PM</Text>
-            </Chip>
-          </PickerColumn>
+          {!hour24Clock && (
+            <PickerColumn title="Period">
+              <Chip
+                size="500"
+                variant={!selectedPM ? 'Primary' : 'SurfaceVariant'}
+                fill="None"
+                radii="300"
+                aria-selected={!selectedPM}
+                onClick={() => handlePeriod(false)}
+                disabled={minDay && minPM}
+              >
+                <Text size="T300">AM</Text>
+              </Chip>
+              <Chip
+                size="500"
+                variant={selectedPM ? 'Primary' : 'SurfaceVariant'}
+                fill="None"
+                radii="300"
+                aria-selected={selectedPM}
+                onClick={() => handlePeriod(true)}
+                disabled={maxDay && !maxPM}
+              >
+                <Text size="T300">PM</Text>
+              </Chip>
+            </PickerColumn>
+          )}
         </Box>
       </Menu>
     );
index 415aa2456b5a5359dc12b1ac4786c695c6fa1e13..26085b5f07f1c85df82121c4a664142f2791a7a3 100644 (file)
@@ -57,6 +57,9 @@ export function MessageSearch({
   const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
   const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
 
+  const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+  const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
   const searchInputRef = useRef<HTMLInputElement>(null);
   const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
   const [searchParams, setSearchParams] = useSearchParams();
@@ -289,6 +292,8 @@ export function MessageSearch({
                     urlPreview={urlPreview}
                     onOpen={navigateRoom}
                     legacyUsernameColor={legacyUsernameColor || mDirects.has(groupRoom.roomId)}
+                    hour24Clock={hour24Clock}
+                    dateFormatString={dateFormatString}
                   />
                 </VirtualTile>
               );
index c2e6c0a1d25e532196adb439594541e78b42cf1a..bc94092bec3494a0a7dbc882da397db68e53fe38 100644 (file)
@@ -57,6 +57,8 @@ type SearchResultGroupProps = {
   urlPreview?: boolean;
   onOpen: (roomId: string, eventId: string) => void;
   legacyUsernameColor?: boolean;
+  hour24Clock: boolean;
+  dateFormatString: string;
 };
 export function SearchResultGroup({
   room,
@@ -66,6 +68,8 @@ export function SearchResultGroup({
   urlPreview,
   onOpen,
   legacyUsernameColor,
+  hour24Clock,
+  dateFormatString,
 }: SearchResultGroupProps) {
   const mx = useMatrixClient();
   const useAuthentication = useMediaAuthentication();
@@ -275,7 +279,11 @@ export function SearchResultGroup({
                       </Username>
                       {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
                     </Box>
-                    <Time ts={event.origin_server_ts} />
+                    <Time
+                      ts={event.origin_server_ts}
+                      hour24Clock={hour24Clock}
+                      dateFormatString={dateFormatString}
+                    />
                   </Box>
                   <Box shrink="No" gap="200" alignItems="Center">
                     <Chip
index f2218b04fbaca1b18a0efe858f85bb87d312bd4b..244eb327367061f249ee94e13a480a4c9f7bd81d 100644 (file)
@@ -450,6 +450,9 @@ export function RoomTimeline({
   const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
   const [showDeveloperTools] = useSetting(settingsAtom, 'developerTools');
 
+  const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+  const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
   const ignoredUsersList = useIgnoredUsers();
   const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]);
 
@@ -1072,6 +1075,8 @@ export function RoomTimeline({
             powerLevelTag={getPowerLevelTag(senderPowerLevel)}
             accessibleTagColors={accessibleTagColors}
             legacyUsernameColor={legacyUsernameColor || direct}
+            hour24Clock={hour24Clock}
+            dateFormatString={dateFormatString}
           >
             {mEvent.isRedacted() ? (
               <RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
@@ -1154,6 +1159,8 @@ export function RoomTimeline({
             powerLevelTag={getPowerLevelTag(senderPowerLevel)}
             accessibleTagColors={accessibleTagColors}
             legacyUsernameColor={legacyUsernameColor || direct}
+            hour24Clock={hour24Clock}
+            dateFormatString={dateFormatString}
           >
             <EncryptedContent mEvent={mEvent}>
               {() => {
@@ -1256,6 +1263,8 @@ export function RoomTimeline({
             powerLevelTag={getPowerLevelTag(senderPowerLevel)}
             accessibleTagColors={accessibleTagColors}
             legacyUsernameColor={legacyUsernameColor || direct}
+            hour24Clock={hour24Clock}
+            dateFormatString={dateFormatString}
           >
             {mEvent.isRedacted() ? (
               <RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
@@ -1284,7 +1293,12 @@ export function RoomTimeline({
         const parsed = parseMemberEvent(mEvent);
 
         const timeJSX = (
-          <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+          <Time
+            ts={mEvent.getTs()}
+            compact={messageLayout === MessageLayout.Compact}
+            hour24Clock={hour24Clock}
+            dateFormatString={dateFormatString}
+          />
         );
 
         return (
@@ -1321,7 +1335,12 @@ export function RoomTimeline({
         const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
 
         const timeJSX = (
-          <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+          <Time
+            ts={mEvent.getTs()}
+            compact={messageLayout === MessageLayout.Compact}
+            hour24Clock={hour24Clock}
+            dateFormatString={dateFormatString}
+          />
         );
 
         return (
@@ -1359,7 +1378,12 @@ export function RoomTimeline({
         const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
 
         const timeJSX = (
-          <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+          <Time
+            ts={mEvent.getTs()}
+            compact={messageLayout === MessageLayout.Compact}
+            hour24Clock={hour24Clock}
+            dateFormatString={dateFormatString}
+          />
         );
 
         return (
@@ -1397,7 +1421,12 @@ export function RoomTimeline({
         const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
 
         const timeJSX = (
-          <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+          <Time
+            ts={mEvent.getTs()}
+            compact={messageLayout === MessageLayout.Compact}
+            hour24Clock={hour24Clock}
+            dateFormatString={dateFormatString}
+          />
         );
 
         return (
@@ -1437,7 +1466,12 @@ export function RoomTimeline({
       const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
 
       const timeJSX = (
-        <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+        <Time
+          ts={mEvent.getTs()}
+          compact={messageLayout === MessageLayout.Compact}
+          hour24Clock={hour24Clock}
+          dateFormatString={dateFormatString}
+        />
       );
 
       return (
@@ -1482,7 +1516,12 @@ export function RoomTimeline({
       const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
 
       const timeJSX = (
-        <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+        <Time
+          ts={mEvent.getTs()}
+          compact={messageLayout === MessageLayout.Compact}
+          hour24Clock={hour24Clock}
+          dateFormatString={dateFormatString}
+        />
       );
 
       return (
index 8c4e2c0b90564fcd88ec6dde41badf18bca54a9e..223c6cf69f82c61430003924b37aeb749059e3f8 100644 (file)
@@ -29,6 +29,8 @@ import { useRoom } from '../../../hooks/useRoom';
 import { StateEvent } from '../../../../types/matrix/room';
 import { getToday, getYesterday, timeDayMonthYear, timeHourMinute } from '../../../utils/time';
 import { DatePicker, TimePicker } from '../../../components/time-date';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
 
 type JumpToTimeProps = {
   onCancel: () => void;
@@ -45,6 +47,8 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
   const createTs = useMemo(() => createStateEvent?.getTs() ?? 0, [createStateEvent]);
   const [ts, setTs] = useState(() => Date.now());
 
+  const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+
   const [timePickerCords, setTimePickerCords] = useState<RectCords>();
   const [datePickerCords, setDatePickerCords] = useState<RectCords>();
 
@@ -125,7 +129,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
                       after={<Icon size="50" src={Icons.ChevronBottom} />}
                       onClick={handleTimePicker}
                     >
-                      <Text size="B300">{timeHourMinute(ts)}</Text>
+                      <Text size="B300">{timeHourMinute(ts, hour24Clock)}</Text>
                     </Chip>
                     <PopOut
                       anchor={timePickerCords}
index c5de9ea189bd0a5665f2111478bcbbde3fe77082..e906a0244e34d938b76f73a356292a61282249f8 100644 (file)
@@ -682,6 +682,8 @@ export type MessageProps = {
   powerLevelTag?: PowerLevelTag;
   accessibleTagColors?: Map<string, string>;
   legacyUsernameColor?: boolean;
+  hour24Clock: boolean;
+  dateFormatString: string;
 };
 export const Message = as<'div', MessageProps>(
   (
@@ -711,6 +713,8 @@ export const Message = as<'div', MessageProps>(
       powerLevelTag,
       accessibleTagColors,
       legacyUsernameColor,
+      hour24Clock,
+      dateFormatString,
       children,
       ...props
     },
@@ -775,7 +779,12 @@ export const Message = as<'div', MessageProps>(
               </Text>
             </>
           )}
-          <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+          <Time
+            ts={mEvent.getTs()}
+            compact={messageLayout === MessageLayout.Compact}
+            hour24Clock={hour24Clock}
+            dateFormatString={dateFormatString}
+          />
         </Box>
       </Box>
     );
index fdc1978409012b985f68e76e2a42c5088e53bf88..8e73e66ee72612e9b2427ec6299d105c8a1586d1 100644 (file)
@@ -102,6 +102,9 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
   const theme = useTheme();
   const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
 
+  const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+  const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
   const [unpinState, unpin] = useAsyncCallback(
     useCallback(() => {
       const pinEvent = getStateEvent(room, StateEvent.RoomPinnedEvents);
@@ -205,7 +208,11 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
             </Username>
             {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
           </Box>
-          <Time ts={pinnedEvent.getTs()} />
+          <Time
+            ts={pinnedEvent.getTs()}
+            hour24Clock={hour24Clock}
+            dateFormatString={dateFormatString}
+          />
         </Box>
         {renderOptions()}
       </Box>
index b4bc9fcbd812db33562b38fa33e9872fb210ba3f..71b684f56fe16af9d8605cb17d34d417f996d9c8 100644 (file)
@@ -27,6 +27,8 @@ import { SequenceCard } from '../../../components/sequence-card';
 import { SequenceCardStyle } from '../styles.css';
 import { LogoutDialog } from '../../../components/LogoutDialog';
 import { stopPropagation } from '../../../utils/keyboard';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
 
 export function DeviceTilePlaceholder() {
   return (
@@ -41,6 +43,9 @@ export function DeviceTilePlaceholder() {
 }
 
 function DeviceActiveTime({ ts }: { ts: number }) {
+  const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+  const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
   return (
     <Text className={BreakWord} size="T200">
       <Text size="Inherit" as="span" priority="300">
@@ -49,7 +54,8 @@ function DeviceActiveTime({ ts }: { ts: number }) {
       <>
         {today(ts) && 'Today'}
         {yesterday(ts) && 'Yesterday'}
-        {!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)}
+        {!today(ts) && !yesterday(ts) && timeDayMonYear(ts, dateFormatString)}{' '}
+        {timeHourMinute(ts, hour24Clock)}
       </>
     </Text>
   );
index 04e2728b504d489f62d898a60e54dfc93fe9da6d..ed11ec4d75b40cfa62e61366c011d52afc3b06e7 100644 (file)
@@ -1,15 +1,19 @@
 import React, {
   ChangeEventHandler,
+  FormEventHandler,
   KeyboardEventHandler,
   MouseEventHandler,
+  useEffect,
   useState,
 } from 'react';
+import dayjs from 'dayjs';
 import {
   as,
   Box,
   Button,
   Chip,
   config,
+  Header,
   Icon,
   IconButton,
   Icons,
@@ -28,7 +32,7 @@ import FocusTrap from 'focus-trap-react';
 import { Page, PageContent, PageHeader } from '../../../components/page';
 import { SequenceCard } from '../../../components/sequence-card';
 import { useSetting } from '../../../state/hooks/settings';
-import { MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
+import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
 import { SettingTile } from '../../../components/setting-tile';
 import { KeySymbol } from '../../../utils/key-symbol';
 import { isMacOS } from '../../../utils/user-agent';
@@ -44,6 +48,7 @@ import {
 import { stopPropagation } from '../../../utils/keyboard';
 import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
 import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
+import { useDateFormatItems } from '../../../hooks/useDateFormat';
 import { SequenceCardStyle } from '../styles.css';
 
 type ThemeSelectorProps = {
@@ -341,6 +346,359 @@ function Appearance() {
   );
 }
 
+type DateHintProps = {
+  hasChanges: boolean;
+  handleReset: () => void;
+};
+function DateHint({ hasChanges, handleReset }: DateHintProps) {
+  const [anchor, setAnchor] = useState<RectCords>();
+  const categoryPadding = { padding: config.space.S200, paddingTop: 0 };
+
+  const handleOpenMenu: MouseEventHandler<HTMLElement> = (evt) => {
+    setAnchor(evt.currentTarget.getBoundingClientRect());
+  };
+  return (
+    <PopOut
+      anchor={anchor}
+      position="Top"
+      align="End"
+      content={
+        <FocusTrap
+          focusTrapOptions={{
+            initialFocus: false,
+            onDeactivate: () => setAnchor(undefined),
+            clickOutsideDeactivates: true,
+            escapeDeactivates: stopPropagation,
+          }}
+        >
+          <Menu style={{ maxHeight: '85vh', overflowY: 'auto' }}>
+            <Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
+              <Text size="L400">Formatting</Text>
+            </Header>
+
+            <Box direction="Column">
+              <Box style={categoryPadding} direction="Column">
+                <Header size="300">
+                  <Text size="L400">Year</Text>
+                </Header>
+                <Box direction="Column" tabIndex={0} gap="100">
+                  <Text size="T300">
+                    YY
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}
+                      Two-digit year
+                    </Text>{' '}
+                  </Text>
+                  <Text size="T300">
+                    YYYY
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}Four-digit year
+                    </Text>
+                  </Text>
+                </Box>
+              </Box>
+
+              <Box style={categoryPadding} direction="Column">
+                <Header size="300">
+                  <Text size="L400">Month</Text>
+                </Header>
+                <Box direction="Column" tabIndex={0} gap="100">
+                  <Text size="T300">
+                    M
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}The month
+                    </Text>
+                  </Text>
+                  <Text size="T300">
+                    MM
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}Two-digit month
+                    </Text>{' '}
+                  </Text>
+                  <Text size="T300">
+                    MMM
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}Short month name
+                    </Text>
+                  </Text>
+                  <Text size="T300">
+                    MMMM
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}Full month name
+                    </Text>
+                  </Text>
+                </Box>
+              </Box>
+
+              <Box style={categoryPadding} direction="Column">
+                <Header size="300">
+                  <Text size="L400">Day of the Month</Text>
+                </Header>
+                <Box direction="Column" tabIndex={0} gap="100">
+                  <Text size="T300">
+                    D
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}Day of the month
+                    </Text>
+                  </Text>
+                  <Text size="T300">
+                    DD
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}Two-digit day of the month
+                    </Text>
+                  </Text>
+                </Box>
+              </Box>
+              <Box style={categoryPadding} direction="Column">
+                <Header size="300">
+                  <Text size="L400">Day of the Week</Text>
+                </Header>
+                <Box direction="Column" tabIndex={0} gap="100">
+                  <Text size="T300">
+                    d
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}Day of the week (Sunday = 0)
+                    </Text>
+                  </Text>
+                  <Text size="T300">
+                    dd
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}Two-letter day name
+                    </Text>
+                  </Text>
+                  <Text size="T300">
+                    ddd
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}Short day name
+                    </Text>
+                  </Text>
+                  <Text size="T300">
+                    dddd
+                    <Text as="span" size="Inherit" priority="300">
+                      {': '}Full day name
+                    </Text>
+                  </Text>
+                </Box>
+              </Box>
+            </Box>
+          </Menu>
+        </FocusTrap>
+      }
+    >
+      {hasChanges ? (
+        <IconButton
+          tabIndex={-1}
+          onClick={handleReset}
+          type="reset"
+          variant="Secondary"
+          size="300"
+          radii="300"
+        >
+          <Icon src={Icons.Cross} size="100" />
+        </IconButton>
+      ) : (
+        <IconButton
+          tabIndex={-1}
+          onClick={handleOpenMenu}
+          type="button"
+          variant="Secondary"
+          size="300"
+          radii="300"
+          aria-pressed={!!anchor}
+        >
+          <Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} />
+        </IconButton>
+      )}
+    </PopOut>
+  );
+}
+
+type CustomDateFormatProps = {
+  value: string;
+  onChange: (format: string) => void;
+};
+function CustomDateFormat({ value, onChange }: CustomDateFormatProps) {
+  const [dateFormatCustom, setDateFormatCustom] = useState(value);
+
+  useEffect(() => {
+    setDateFormatCustom(value);
+  }, [value]);
+
+  const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
+    const format = evt.currentTarget.value;
+    setDateFormatCustom(format);
+  };
+
+  const handleReset = () => {
+    setDateFormatCustom(value);
+  };
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const customDateFormatInput = target?.customDateFormatInput as HTMLInputElement | undefined;
+    const format = customDateFormatInput?.value;
+    if (!format) return;
+
+    onChange(format);
+  };
+
+  const hasChanges = dateFormatCustom !== value;
+  return (
+    <SettingTile>
+      <Box as="form" onSubmit={handleSubmit} gap="200">
+        <Box grow="Yes" direction="Column">
+          <Input
+            required
+            name="customDateFormatInput"
+            value={dateFormatCustom}
+            onChange={handleChange}
+            maxLength={16}
+            autoComplete="off"
+            variant="Secondary"
+            radii="300"
+            style={{ paddingRight: config.space.S200 }}
+            after={<DateHint hasChanges={hasChanges} handleReset={handleReset} />}
+          />
+        </Box>
+        <Button
+          size="400"
+          variant={hasChanges ? 'Success' : 'Secondary'}
+          fill={hasChanges ? 'Solid' : 'Soft'}
+          outlined
+          radii="300"
+          disabled={!hasChanges}
+          type="submit"
+        >
+          <Text size="B400">Save</Text>
+        </Button>
+      </Box>
+    </SettingTile>
+  );
+}
+
+type PresetDateFormatProps = {
+  value: string;
+  onChange: (format: string) => void;
+};
+function PresetDateFormat({ value, onChange }: PresetDateFormatProps) {
+  const [menuCords, setMenuCords] = useState<RectCords>();
+  const dateFormatItems = useDateFormatItems();
+
+  const getDisplayDate = (format: string): string =>
+    format !== '' ? dayjs().format(format) : 'Custom';
+
+  const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setMenuCords(evt.currentTarget.getBoundingClientRect());
+  };
+
+  const handleSelect = (format: DateFormat) => {
+    onChange(format);
+    setMenuCords(undefined);
+  };
+
+  return (
+    <>
+      <Button
+        size="300"
+        variant="Secondary"
+        outlined
+        fill="Soft"
+        radii="300"
+        after={<Icon size="300" src={Icons.ChevronBottom} />}
+        onClick={handleMenu}
+      >
+        <Text size="T300">
+          {getDisplayDate(dateFormatItems.find((i) => i.format === value)?.format ?? value)}
+        </Text>
+      </Button>
+      <PopOut
+        anchor={menuCords}
+        offset={5}
+        position="Bottom"
+        align="End"
+        content={
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setMenuCords(undefined),
+              clickOutsideDeactivates: true,
+              isKeyForward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+              isKeyBackward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <Menu>
+              <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+                {dateFormatItems.map((item) => (
+                  <MenuItem
+                    key={item.format}
+                    size="300"
+                    variant={value === item.format ? 'Primary' : 'Surface'}
+                    radii="300"
+                    onClick={() => handleSelect(item.format)}
+                  >
+                    <Text size="T300">{getDisplayDate(item.format)}</Text>
+                  </MenuItem>
+                ))}
+              </Box>
+            </Menu>
+          </FocusTrap>
+        }
+      />
+    </>
+  );
+}
+
+function SelectDateFormat() {
+  const [dateFormatString, setDateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+  const [selectedDateFormat, setSelectedDateFormat] = useState(dateFormatString);
+  const customDateFormat = selectedDateFormat === '';
+
+  const handlePresetChange = (format: string) => {
+    setSelectedDateFormat(format);
+    if (format !== '') {
+      setDateFormatString(format);
+    }
+  };
+
+  return (
+    <>
+      <SettingTile
+        title="Date Format"
+        description={customDateFormat ? dayjs().format(dateFormatString) : ''}
+        after={<PresetDateFormat value={selectedDateFormat} onChange={handlePresetChange} />}
+      />
+      {customDateFormat && (
+        <CustomDateFormat value={dateFormatString} onChange={setDateFormatString} />
+      )}
+    </>
+  );
+}
+
+function DateAndTime() {
+  const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Date & Time</Text>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="24-Hour Time Format"
+          after={<Switch variant="Primary" value={hour24Clock} onChange={setHour24Clock} />}
+        />
+      </SequenceCard>
+
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SelectDateFormat />
+      </SequenceCard>
+    </Box>
+  );
+}
+
 function Editor() {
   const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
   const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
@@ -637,6 +995,7 @@ export function General({ requestClose }: GeneralProps) {
           <PageContent>
             <Box direction="Column" gap="700">
               <Appearance />
+              <DateAndTime />
               <Editor />
               <Messages />
             </Box>
diff --git a/src/app/hooks/useDateFormat.ts b/src/app/hooks/useDateFormat.ts
new file mode 100644 (file)
index 0000000..520d4b0
--- /dev/null
@@ -0,0 +1,34 @@
+import { useMemo } from 'react';
+import { DateFormat } from '../state/settings';
+
+export type DateFormatItem = {
+  name: string;
+  format: DateFormat;
+};
+
+export const useDateFormatItems = (): DateFormatItem[] =>
+  useMemo(
+    () => [
+      {
+        format: 'D MMM YYYY',
+        name: 'D MMM YYYY',
+      },
+      {
+        format: 'DD/MM/YYYY',
+        name: 'DD/MM/YYYY',
+      },
+      {
+        format: 'MM/DD/YYYY',
+        name: 'MM/DD/YYYY',
+      },
+      {
+        format: 'YYYY/MM/DD',
+        name: 'YYYY/MM/DD',
+      },
+      {
+        format: '',
+        name: 'Custom',
+      },
+    ],
+    []
+  );
index 84c37f4791535d175a5d72cd678c7447e75171a4..bd9b694d166b3596ad8f28af39f31c2b6bd38d68 100644 (file)
@@ -65,6 +65,8 @@ import { testBadWords } from '../../../plugins/bad-words';
 import { allRoomsAtom } from '../../../state/room-list/roomList';
 import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
 import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
 
 const COMPACT_CARD_WIDTH = 548;
 
@@ -135,10 +137,19 @@ type NavigateHandler = (roomId: string, space: boolean) => void;
 type InviteCardProps = {
   invite: InviteData;
   compact?: boolean;
+  hour24Clock: boolean;
+  dateFormatString: string;
   onNavigate: NavigateHandler;
   hideAvatar: boolean;
 };
-function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps) {
+function InviteCard({
+  invite,
+  compact,
+  hour24Clock,
+  dateFormatString,
+  onNavigate,
+  hideAvatar,
+}: InviteCardProps) {
   const mx = useMatrixClient();
   const userId = mx.getSafeUserId();
 
@@ -295,7 +306,13 @@ function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps
         </Box>
         {invite.inviteTs && (
           <Box shrink="No">
-            <Time size="T200" ts={invite.inviteTs} priority="300" />
+            <Time
+              size="T200"
+              ts={invite.inviteTs}
+              hour24Clock={hour24Clock}
+              dateFormatString={dateFormatString}
+              priority="300"
+            />
           </Box>
         )}
       </Box>
@@ -384,8 +401,16 @@ type KnownInvitesProps = {
   invites: InviteData[];
   handleNavigate: NavigateHandler;
   compact: boolean;
+  hour24Clock: boolean;
+  dateFormatString: string;
 };
-function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) {
+function KnownInvites({
+  invites,
+  handleNavigate,
+  compact,
+  hour24Clock,
+  dateFormatString,
+}: KnownInvitesProps) {
   return (
     <Box direction="Column" gap="200">
       <Text size="H4">Primary</Text>
@@ -396,6 +421,8 @@ function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) {
               key={invite.roomId}
               invite={invite}
               compact={compact}
+              hour24Clock={hour24Clock}
+              dateFormatString={dateFormatString}
               onNavigate={handleNavigate}
               hideAvatar={false}
             />
@@ -420,8 +447,16 @@ type UnknownInvitesProps = {
   invites: InviteData[];
   handleNavigate: NavigateHandler;
   compact: boolean;
+  hour24Clock: boolean;
+  dateFormatString: string;
 };
-function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProps) {
+function UnknownInvites({
+  invites,
+  handleNavigate,
+  compact,
+  hour24Clock,
+  dateFormatString,
+}: UnknownInvitesProps) {
   const mx = useMatrixClient();
 
   const [declineAllStatus, declineAll] = useAsyncCallback(
@@ -459,6 +494,8 @@ function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProp
               key={invite.roomId}
               invite={invite}
               compact={compact}
+              hour24Clock={hour24Clock}
+              dateFormatString={dateFormatString}
               onNavigate={handleNavigate}
               hideAvatar
             />
@@ -483,8 +520,16 @@ type SpamInvitesProps = {
   invites: InviteData[];
   handleNavigate: NavigateHandler;
   compact: boolean;
+  hour24Clock: boolean;
+  dateFormatString: string;
 };
-function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) {
+function SpamInvites({
+  invites,
+  handleNavigate,
+  compact,
+  hour24Clock,
+  dateFormatString,
+}: SpamInvitesProps) {
   const mx = useMatrixClient();
   const [showInvites, setShowInvites] = useState(false);
 
@@ -608,6 +653,8 @@ function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) {
                 key={invite.roomId}
                 invite={invite}
                 compact={compact}
+                hour24Clock={hour24Clock}
+                dateFormatString={dateFormatString}
                 onNavigate={handleNavigate}
                 hideAvatar
               />
@@ -671,6 +718,9 @@ export function Invites() {
   );
   const screenSize = useScreenSizeContext();
 
+  const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+  const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
   const handleNavigate = (roomId: string, space: boolean) => {
     if (space) {
       navigateSpace(roomId);
@@ -723,6 +773,8 @@ export function Invites() {
                   <KnownInvites
                     invites={knownInvites}
                     compact={compact}
+                    hour24Clock={hour24Clock}
+                    dateFormatString={dateFormatString}
                     handleNavigate={handleNavigate}
                   />
                 )}
@@ -731,6 +783,8 @@ export function Invites() {
                   <UnknownInvites
                     invites={unknownInvites}
                     compact={compact}
+                    hour24Clock={hour24Clock}
+                    dateFormatString={dateFormatString}
                     handleNavigate={handleNavigate}
                   />
                 )}
@@ -739,6 +793,8 @@ export function Invites() {
                   <SpamInvites
                     invites={spamInvites}
                     compact={compact}
+                    hour24Clock={hour24Clock}
+                    dateFormatString={dateFormatString}
                     handleNavigate={handleNavigate}
                   />
                 )}
index 80ce25a98f18db15305840bb3cd447486f37c1c0..a49577438be8a4f469031cc3cd91d9a6bfb248e2 100644 (file)
@@ -205,6 +205,8 @@ type RoomNotificationsGroupProps = {
   hideActivity: boolean;
   onOpen: (roomId: string, eventId: string) => void;
   legacyUsernameColor?: boolean;
+  hour24Clock: boolean;
+  dateFormatString: string;
 };
 function RoomNotificationsGroupComp({
   room,
@@ -214,6 +216,8 @@ function RoomNotificationsGroupComp({
   hideActivity,
   onOpen,
   legacyUsernameColor,
+  hour24Clock,
+  dateFormatString,
 }: RoomNotificationsGroupProps) {
   const mx = useMatrixClient();
   const useAuthentication = useMediaAuthentication();
@@ -496,7 +500,11 @@ function RoomNotificationsGroupComp({
                       </Username>
                       {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
                     </Box>
-                    <Time ts={event.origin_server_ts} />
+                    <Time
+                      ts={event.origin_server_ts}
+                      hour24Clock={hour24Clock}
+                      dateFormatString={dateFormatString}
+                    />
                   </Box>
                   <Box shrink="No" gap="200" alignItems="Center">
                     <Chip
@@ -549,6 +557,8 @@ export function Notifications() {
   const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
   const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
   const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
+  const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+  const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
   const screenSize = useScreenSizeContext();
   const mDirects = useAtomValue(mDirectAtom);
 
@@ -713,6 +723,8 @@ export function Notifications() {
                           legacyUsernameColor={
                             legacyUsernameColor || mDirects.has(groupRoom.roomId)
                           }
+                          hour24Clock={hour24Clock}
+                          dateFormatString={dateFormatString}
                         />
                       </VirtualTile>
                     );
index 799747ac7a82768f1b2ba59466fb1fc4133fb3da..15bf9a140247df0bca6397b675bdd4bc5b5dc860 100644 (file)
@@ -1,6 +1,7 @@
 import { atom } from 'jotai';
 
 const STORAGE_KEY = 'settings';
+export type DateFormat = 'D MMM YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD' | '';
 export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
 export enum MessageLayout {
   Modern = 0,
@@ -35,6 +36,9 @@ export interface Settings {
   showNotifications: boolean;
   isNotificationSounds: boolean;
 
+  hour24Clock: boolean;
+  dateFormatString: string;
+
   developerTools: boolean;
 }
 
@@ -65,6 +69,9 @@ const defaultSettings: Settings = {
   showNotifications: true,
   isNotificationSounds: true,
 
+  hour24Clock: false,
+  dateFormatString: 'D MMM YYYY',
+
   developerTools: false,
 };
 
index f230e59b601dab10dd81d178553ca47ee996222e..8f1e30e93b451eae3d01278c380d5ab40eb81afd 100644 (file)
@@ -9,7 +9,8 @@ export const today = (ts: number): boolean => dayjs(ts).isToday();
 
 export const yesterday = (ts: number): boolean => dayjs(ts).isYesterday();
 
-export const timeHour = (ts: number): string => dayjs(ts).format('hh');
+export const timeHour = (ts: number, hour24Clock: boolean): string =>
+  dayjs(ts).format(hour24Clock ? 'HH' : 'hh');
 export const timeMinute = (ts: number): string => dayjs(ts).format('mm');
 export const timeAmPm = (ts: number): string => dayjs(ts).format('A');
 export const timeDay = (ts: number): string => dayjs(ts).format('D');
@@ -17,9 +18,11 @@ export const timeMon = (ts: number): string => dayjs(ts).format('MMM');
 export const timeMonth = (ts: number): string => dayjs(ts).format('MMMM');
 export const timeYear = (ts: number): string => dayjs(ts).format('YYYY');
 
-export const timeHourMinute = (ts: number): string => dayjs(ts).format('hh:mm A');
+export const timeHourMinute = (ts: number, hour24Clock: boolean): string =>
+  dayjs(ts).format(hour24Clock ? 'HH:mm' : 'hh:mm A');
 
-export const timeDayMonYear = (ts: number): string => dayjs(ts).format('D MMM YYYY');
+export const timeDayMonYear = (ts: number, dateFormatString: string): string =>
+  dayjs(ts).format(dateFormatString);
 
 export const timeDayMonthYear = (ts: number): string => dayjs(ts).format('D MMMM YYYY');