feat(settings): drop user-facing time/date format toggle and derive everything from system locale via Intl.DateTimeFormat

This commit is contained in:
heaven 2026-05-03 19:27:54 +03:00
parent a1ff5db724
commit e547c466a8
19 changed files with 81 additions and 613 deletions

View file

@ -148,7 +148,7 @@ These are vojo additions on top of stock Cinny — they crossed many recent stab
**Jotai** atoms in `src/app/state/`: **Jotai** atoms in `src/app/state/`:
- `settings.ts` — User preferences (`themeId`, `useSystemTheme`, `monochromeMode`, `hideMembershipEvents`, `hideNickAvatarEvents`, …). Persisted to `localStorage['settings']`. The `MessageLayout` enum + `messageSpacing` + `legacyUsernameColor` fields were dropped in P3c; the new `dawn-p3c-cleanup` migration in `getSettings()` strips the orphan keys from existing users' persisted JSON on first load. - `settings.ts` — User preferences (`themeId`, `useSystemTheme`, `monochromeMode`, `hideMembershipEvents`, `hideNickAvatarEvents`, …). Persisted to `localStorage['settings']`. The `MessageLayout` enum + `messageSpacing` + `legacyUsernameColor` fields were dropped in P3c; the new `dawn-p3c-cleanup` migration in `getSettings()` strips the orphan keys from existing users' persisted JSON on first load. The user-facing `hour24Clock` / `dateFormatString` fields were removed too — both now derive from the runtime locale via `Intl.DateTimeFormat` in `utils/time.ts` (24-hour locales → `HH:mm` + `DD/MM/YYYY`; AM/PM locales → `hh:mm A` + `MM/DD/YYYY`). The `system-time-format-cleanup` migration synchronously deletes those keys on first load. **Known platform limitation**: Android's manual «Use 24-hour format» toggle in Date & Time settings is invisible to JS — `Intl` reads only CLDR locale conventions. Russian-locale users with AM/PM toggle get 24-hour format on both web and Capacitor; only a native bridge to `android.text.format.DateFormat.is24HourFormat(context)` would respect that toggle.
- `sessions.ts` — Active session - `sessions.ts` — Active session
- `upload.ts` — Upload progress (in-memory) - `upload.ts` — Upload progress (in-memory)
- `room/``roomInputDrafts` (in-memory), `roomToParents`, `roomToUnread` - `room/``roomInputDrafts` (in-memory), `roomToParents`, `roomToUnread`

View file

@ -99,27 +99,6 @@
"monochrome_mode": "Monochrome Mode", "monochrome_mode": "Monochrome Mode",
"twitter_emoji": "Twitter Emoji", "twitter_emoji": "Twitter Emoji",
"page_zoom": "Page Zoom", "page_zoom": "Page Zoom",
"date_time": "Date & Time",
"hour_24": "24-Hour Time Format",
"date_format": "Date Format",
"custom": "Custom",
"formatting": "Formatting",
"year": "Year",
"two_digit_year": "Two-digit year",
"four_digit_year": "Four-digit year",
"month": "Month",
"the_month": "The month",
"two_digit_month": "Two-digit month",
"short_month_name": "Short month name",
"full_month_name": "Full month name",
"day_of_month": "Day of the Month",
"day_of_month_val": "Day of the month",
"two_digit_day": "Two-digit day of the month",
"day_of_week": "Day of the Week",
"day_of_week_sunday": "Day of the week (Sunday = 0)",
"two_letter_day": "Two-letter day name",
"short_day_name": "Short day name",
"full_day_name": "Full day name",
"save": "Save", "save": "Save",
"editor": "Editor", "editor": "Editor",
"enter_newline": "ENTER for Newline", "enter_newline": "ENTER for Newline",

View file

@ -99,27 +99,6 @@
"monochrome_mode": "Монохромный режим", "monochrome_mode": "Монохромный режим",
"twitter_emoji": "Эмодзи Twitter", "twitter_emoji": "Эмодзи Twitter",
"page_zoom": "Масштаб страницы", "page_zoom": "Масштаб страницы",
"date_time": "Дата и время",
"hour_24": "24-часовой формат",
"date_format": "Формат даты",
"custom": "Пользовательский",
"formatting": "Форматирование",
"year": "Год",
"two_digit_year": "Двузначный год",
"four_digit_year": "Четырёхзначный год",
"month": "Месяц",
"the_month": "Месяц",
"two_digit_month": "Двузначный месяц",
"short_month_name": "Краткое название месяца",
"full_month_name": "Полное название месяца",
"day_of_month": "День месяца",
"day_of_month_val": "День месяца",
"two_digit_day": "Двузначный день месяца",
"day_of_week": "День недели",
"day_of_week_sunday": "День недели (воскресенье = 0)",
"two_letter_day": "Двухбуквенное название дня",
"short_day_name": "Краткое название дня",
"full_day_name": "Полное название дня",
"save": "Сохранить", "save": "Сохранить",
"editor": "Редактор", "editor": "Редактор",
"enter_newline": "ENTER для новой строки", "enter_newline": "ENTER для новой строки",

View file

@ -6,26 +6,17 @@ import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/ti
export type TimeProps = { export type TimeProps = {
compact?: boolean; compact?: boolean;
ts: number; ts: number;
hour24Clock: boolean;
dateFormatString: string;
}; };
/** /**
* Renders a formatted timestamp, supporting compact and full display modes. * Renders a formatted timestamp using the system locale's hour and date format.
* *
* Displays the time in hour:minute format if the message is from today, yesterday, or if `compact` is true. * Today/yesterday/`compact` show time only; older messages show date + time.
* 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>>( export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => { ({ compact, ts, ...props }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const formattedTime = timeHourMinute(ts, hour24Clock); const formattedTime = timeHourMinute(ts);
let time = ''; let time = '';
if (compact) { if (compact) {
@ -35,7 +26,7 @@ export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
} else if (yesterday(ts)) { } else if (yesterday(ts)) {
time = `${t('Room.yesterday')} ${formattedTime}`; time = `${t('Room.yesterday')} ${formattedTime}`;
} else { } else {
time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`; time = `${timeDayMonYear(ts)} ${formattedTime}`;
} }
return ( return (

View file

@ -14,8 +14,6 @@ import { nameInitials } from '../../utils/common';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta'; import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { useIsOneOnOne } from '../../hooks/useRoom'; import { useIsOneOnOne } from '../../hooks/useRoom';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { InviteUserPrompt } from '../invite-user-prompt'; import { InviteUserPrompt } from '../invite-user-prompt';
export type RoomIntroProps = { export type RoomIntroProps = {
@ -51,8 +49,6 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
useCallback(async (roomId: string) => mx.joinRoom(roomId), [mx]) useCallback(async (roomId: string) => mx.joinRoom(roomId), [mx])
); );
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
return ( return (
<Box direction="Column" grow="Yes" gap="500" {...props} ref={ref}> <Box direction="Column" grow="Yes" gap="500" {...props} ref={ref}>
<Box> <Box>
@ -80,7 +76,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
values={{ values={{
creator: creatorName, creator: creatorName,
date: timeDayMonthYear(ts), date: timeDayMonthYear(ts),
time: timeHourMinute(ts, hour24Clock), time: timeHourMinute(ts),
}} }}
components={{ bold: <b /> }} components={{ bold: <b /> }}
/> />

View file

@ -3,9 +3,14 @@ import { Menu, Box, Text, Chip } from 'folds';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import * as css from './styles.css'; import * as css from './styles.css';
import { PickerColumn } from './PickerColumn'; import { PickerColumn } from './PickerColumn';
import { hour12to24, hour24to12, hoursToMs, inSameDay, minutesToMs } from '../../utils/time'; import {
import { useSetting } from '../../state/hooks/settings'; SYSTEM_HOUR_24,
import { settingsAtom } from '../../state/settings'; hour12to24,
hour24to12,
hoursToMs,
inSameDay,
minutesToMs,
} from '../../utils/time';
type TimePickerProps = { type TimePickerProps = {
min: number; min: number;
@ -15,11 +20,9 @@ type TimePickerProps = {
}; };
export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>( export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
({ min, max, value, onChange }, ref) => { ({ min, max, value, onChange }, ref) => {
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const hour24 = dayjs(value).hour(); const hour24 = dayjs(value).hour();
const selectedHour = hour24Clock ? hour24 : hour24to12(hour24); const selectedHour = SYSTEM_HOUR_24 ? hour24 : hour24to12(hour24);
const selectedMinute = dayjs(value).minute(); const selectedMinute = dayjs(value).minute();
const selectedPM = hour24 >= 12; const selectedPM = hour24 >= 12;
@ -28,7 +31,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
}; };
const handleHour = (hour: number) => { const handleHour = (hour: number) => {
const seconds = hoursToMs(hour24Clock ? hour : hour12to24(hour, selectedPM)); const seconds = hoursToMs(SYSTEM_HOUR_24 ? hour : hour12to24(hour, selectedPM));
const lastSeconds = hoursToMs(hour24); const lastSeconds = hoursToMs(hour24);
const newValue = value + (seconds - lastSeconds); const newValue = value + (seconds - lastSeconds);
handleSubmit(newValue); handleSubmit(newValue);
@ -63,7 +66,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
<Menu className={css.PickerMenu} ref={ref}> <Menu className={css.PickerMenu} ref={ref}>
<Box direction="Row" gap="200" className={css.PickerContainer}> <Box direction="Row" gap="200" className={css.PickerContainer}>
<PickerColumn title="Hour"> <PickerColumn title="Hour">
{hour24Clock {SYSTEM_HOUR_24
? Array.from(Array(24).keys()).map((hour) => ( ? Array.from(Array(24).keys()).map((hour) => (
<Chip <Chip
key={hour} key={hour}
@ -120,7 +123,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
</Chip> </Chip>
))} ))}
</PickerColumn> </PickerColumn>
{!hour24Clock && ( {!SYSTEM_HOUR_24 && (
<PickerColumn title="Period"> <PickerColumn title="Period">
<Chip <Chip
size="500" size="500"

View file

@ -6,8 +6,6 @@ import { SettingTile } from '../setting-tile';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { BreakWord } from '../../styles/Text.css'; import { BreakWord } from '../../styles/Text.css';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { timeDayMonYear, timeHourMinute } from '../../utils/time'; import { timeDayMonYear, timeHourMinute } from '../../utils/time';
type UserKickAlertProps = { type UserKickAlertProps = {
@ -16,11 +14,8 @@ type UserKickAlertProps = {
ts?: number; ts?: number;
}; };
export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) { export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); const time = ts ? timeHourMinute(ts) : undefined;
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); const date = ts ? timeDayMonYear(ts) : undefined;
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
return ( return (
<CutoutCard style={{ padding: config.space.S200 }} variant="Critical"> <CutoutCard style={{ padding: config.space.S200 }} variant="Critical">
@ -66,11 +61,8 @@ type UserBanAlertProps = {
export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBanAlertProps) { export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBanAlertProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = useRoom(); const room = useRoom();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); const time = ts ? timeHourMinute(ts) : undefined;
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); const date = ts ? timeDayMonYear(ts) : undefined;
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
const [unbanState, unban] = useAsyncCallback<undefined, Error, []>( const [unbanState, unban] = useAsyncCallback<undefined, Error, []>(
useCallback(async () => { useCallback(async () => {
@ -141,11 +133,8 @@ type UserInviteAlertProps = {
export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: UserInviteAlertProps) { export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: UserInviteAlertProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = useRoom(); const room = useRoom();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); const time = ts ? timeHourMinute(ts) : undefined;
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); const date = ts ? timeDayMonYear(ts) : undefined;
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
const [kickState, kick] = useAsyncCallback<undefined, Error, []>( const [kickState, kick] = useAsyncCallback<undefined, Error, []>(
useCallback(async () => { useCallback(async () => {

View file

@ -60,9 +60,6 @@ export function MessageSearch({
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const scrollTopAnchorRef = useRef<HTMLDivElement>(null); const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -299,8 +296,6 @@ export function MessageSearch({
urlPreview={urlPreview} urlPreview={urlPreview}
onOpen={navigateRoom} onOpen={navigateRoom}
legacyUsernameColor={isOneOnOneRoom(groupRoom)} legacyUsernameColor={isOneOnOneRoom(groupRoom)}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
</VirtualTile> </VirtualTile>
); );

View file

@ -61,8 +61,6 @@ type SearchResultGroupProps = {
urlPreview?: boolean; urlPreview?: boolean;
onOpen: (roomId: string, eventId: string) => void; onOpen: (roomId: string, eventId: string) => void;
legacyUsernameColor?: boolean; legacyUsernameColor?: boolean;
hour24Clock: boolean;
dateFormatString: string;
}; };
export function SearchResultGroup({ export function SearchResultGroup({
room, room,
@ -72,8 +70,6 @@ export function SearchResultGroup({
urlPreview, urlPreview,
onOpen, onOpen,
legacyUsernameColor, legacyUsernameColor,
hour24Clock,
dateFormatString,
}: SearchResultGroupProps) { }: SearchResultGroupProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const mx = useMatrixClient(); const mx = useMatrixClient();
@ -292,11 +288,7 @@ export function SearchResultGroup({
</Username> </Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />} {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box> </Box>
<Time <Time ts={event.origin_server_ts} />
ts={event.origin_server_ts}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
</Box> </Box>
<Box shrink="No" gap="200" alignItems="Center"> <Box shrink="No" gap="200" alignItems="Center">
<Chip <Chip

View file

@ -260,7 +260,7 @@ const deriveRoomPreview = (room: Room): RoomPreview => {
return { ts: room.getLastActiveTimestamp() || undefined, text: '' }; return { ts: room.getLastActiveTimestamp() || undefined, text: '' };
}; };
const formatRowTime = (ts: number, hour24Clock: boolean): string => { const formatRowTime = (ts: number): string => {
const now = Date.now(); const now = Date.now();
const diff = now - ts; const diff = now - ts;
const day = 24 * 60 * 60 * 1000; const day = 24 * 60 * 60 * 1000;
@ -270,7 +270,7 @@ const formatRowTime = (ts: number, hour24Clock: boolean): string => {
date.getFullYear() === today.getFullYear() && date.getFullYear() === today.getFullYear() &&
date.getMonth() === today.getMonth() && date.getMonth() === today.getMonth() &&
date.getDate() === today.getDate(); date.getDate() === today.getDate();
if (sameDay) return timeHourMinute(ts, hour24Clock); if (sameDay) return timeHourMinute(ts);
if (diff < 7 * day) { if (diff < 7 * day) {
return date.toLocaleDateString(undefined, { weekday: 'short' }); return date.toLocaleDateString(undefined, { weekday: 'short' });
} }
@ -287,7 +287,6 @@ export function DmStreamRow({ room, selected, notificationMode, linkPath }: DmSt
const { t } = useTranslation(); const { t } = useTranslation();
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover }); const { hoverProps } = useHover({ onHoverChange: setHover });
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
@ -302,7 +301,7 @@ export function DmStreamRow({ room, selected, notificationMode, linkPath }: DmSt
const preview = deriveRoomPreview(room); const preview = deriveRoomPreview(room);
const previewText = preview.text; const previewText = preview.text;
const previewTs = preview.ts; const previewTs = preview.ts;
const timeLabel = previewTs ? formatRowTime(previewTs, hour24Clock) : ''; const timeLabel = previewTs ? formatRowTime(previewTs) : '';
const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => { const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
evt.preventDefault(); evt.preventDefault();

View file

@ -435,9 +435,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents'); const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
const [showDeveloperTools] = useSetting(settingsAtom, 'developerTools'); const [showDeveloperTools] = useSetting(settingsAtom, 'developerTools');
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const ignoredUsersList = useIgnoredUsers(); const ignoredUsersList = useIgnoredUsers();
const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]); const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]);
@ -1133,8 +1130,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
memberPowerTag={getMemberPowerTag(senderId)} memberPowerTag={getMemberPowerTag(senderId)}
accessibleTagColors={accessiblePowerTagColors} accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={isOneOnOne} legacyUsernameColor={isOneOnOne}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
streamRailStart={streamRailStart} streamRailStart={streamRailStart}
streamRailEnd={streamRailEnd} streamRailEnd={streamRailEnd}
> >
@ -1235,8 +1230,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')} memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')}
accessibleTagColors={accessiblePowerTagColors} accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={isOneOnOne} legacyUsernameColor={isOneOnOne}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
streamRailStart={streamRailStart} streamRailStart={streamRailStart}
streamRailEnd={streamRailEnd} streamRailEnd={streamRailEnd}
> >
@ -1349,8 +1342,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')} memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')}
accessibleTagColors={accessiblePowerTagColors} accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={isOneOnOne} legacyUsernameColor={isOneOnOne}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
streamRailStart={streamRailStart} streamRailStart={streamRailStart}
streamRailEnd={streamRailEnd} streamRailEnd={streamRailEnd}
> >
@ -1397,8 +1388,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<Time <Time
ts={mEvent.getTs()} ts={mEvent.getTs()}
compact compact
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
); );
@ -1447,8 +1436,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<Time <Time
ts={mEvent.getTs()} ts={mEvent.getTs()}
compact compact
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
); );
@ -1498,8 +1485,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<Time <Time
ts={mEvent.getTs()} ts={mEvent.getTs()}
compact compact
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
); );
@ -1549,8 +1534,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<Time <Time
ts={mEvent.getTs()} ts={mEvent.getTs()}
compact compact
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
); );
@ -1608,8 +1591,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<Time <Time
ts={mEvent.getTs()} ts={mEvent.getTs()}
compact compact
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
); );
@ -1656,8 +1637,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<Time <Time
ts={mEvent.getTs()} ts={mEvent.getTs()}
compact compact
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
); );
@ -1706,8 +1685,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<Time <Time
ts={mEvent.getTs()} ts={mEvent.getTs()}
compact compact
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
); );

View file

@ -30,8 +30,6 @@ import { useRoom } from '../../../hooks/useRoom';
import { StateEvent } from '../../../../types/matrix/room'; import { StateEvent } from '../../../../types/matrix/room';
import { getToday, getYesterday, timeDayMonthYear, timeHourMinute } from '../../../utils/time'; import { getToday, getYesterday, timeDayMonthYear, timeHourMinute } from '../../../utils/time';
import { DatePicker, TimePicker } from '../../../components/time-date'; import { DatePicker, TimePicker } from '../../../components/time-date';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
type JumpToTimeProps = { type JumpToTimeProps = {
onCancel: () => void; onCancel: () => void;
@ -49,8 +47,6 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
const createTs = useMemo(() => createStateEvent?.getTs() ?? 0, [createStateEvent]); const createTs = useMemo(() => createStateEvent?.getTs() ?? 0, [createStateEvent]);
const [ts, setTs] = useState(() => Date.now()); const [ts, setTs] = useState(() => Date.now());
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [timePickerCords, setTimePickerCords] = useState<RectCords>(); const [timePickerCords, setTimePickerCords] = useState<RectCords>();
const [datePickerCords, setDatePickerCords] = useState<RectCords>(); const [datePickerCords, setDatePickerCords] = useState<RectCords>();
@ -131,7 +127,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
after={<Icon size="50" src={Icons.ChevronBottom} />} after={<Icon size="50" src={Icons.ChevronBottom} />}
onClick={handleTimePicker} onClick={handleTimePicker}
> >
<Text size="B300">{timeHourMinute(ts, hour24Clock)}</Text> <Text size="B300">{timeHourMinute(ts)}</Text>
</Chip> </Chip>
<PopOut <PopOut
anchor={timePickerCords} anchor={timePickerCords}

View file

@ -668,8 +668,6 @@ export type MessageProps = {
memberPowerTag?: MemberPowerTag; memberPowerTag?: MemberPowerTag;
accessibleTagColors?: Map<string, string>; accessibleTagColors?: Map<string, string>;
legacyUsernameColor?: boolean; legacyUsernameColor?: boolean;
hour24Clock: boolean;
dateFormatString: string;
streamRailStart?: boolean; streamRailStart?: boolean;
streamRailEnd?: boolean; streamRailEnd?: boolean;
}; };
@ -699,8 +697,6 @@ export const Message = as<'div', MessageProps>(
memberPowerTag, memberPowerTag,
accessibleTagColors, accessibleTagColors,
legacyUsernameColor, legacyUsernameColor,
hour24Clock,
dateFormatString,
streamRailStart, streamRailStart,
streamRailEnd, streamRailEnd,
children, children,
@ -1047,14 +1043,7 @@ export const Message = as<'div', MessageProps>(
</div> </div>
)} )}
<StreamLayout <StreamLayout
time={ time={<Time ts={mEvent.getTs()} compact />}
<Time
ts={mEvent.getTs()}
compact
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
}
dotColor={dot.color} dotColor={dot.color}
dotOpacity={dot.opacity} dotOpacity={dot.opacity}
isOwn={isOwnMessage} isOwn={isOwnMessage}

View file

@ -97,8 +97,6 @@ type PinnedMessageProps = {
getMemberPowerTag: GetMemberPowerTag; getMemberPowerTag: GetMemberPowerTag;
accessibleTagColors: Map<string, string>; accessibleTagColors: Map<string, string>;
legacyUsernameColor: boolean; legacyUsernameColor: boolean;
hour24Clock: boolean;
dateFormatString: string;
}; };
function PinnedMessage({ function PinnedMessage({
room, room,
@ -109,8 +107,6 @@ function PinnedMessage({
getMemberPowerTag, getMemberPowerTag,
accessibleTagColors, accessibleTagColors,
legacyUsernameColor, legacyUsernameColor,
hour24Clock,
dateFormatString,
}: PinnedMessageProps) { }: PinnedMessageProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const pinnedEvent = useRoomEvent(room, eventId); const pinnedEvent = useRoomEvent(room, eventId);
@ -221,11 +217,7 @@ function PinnedMessage({
</Username> </Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />} {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box> </Box>
<Time <Time ts={pinnedEvent.getTs()} />
ts={pinnedEvent.getTs()}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
</Box> </Box>
{renderOptions()} {renderOptions()}
</Box> </Box>
@ -279,9 +271,6 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
const isOneOnOne = useIsOneOnOne(); const isOneOnOne = useIsOneOnOne();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const { navigateRoom } = useRoomNavigate(); const { navigateRoom } = useRoomNavigate();
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
@ -499,8 +488,6 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
getMemberPowerTag={getMemberPowerTag} getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessibleTagColors} accessibleTagColors={accessibleTagColors}
legacyUsernameColor={isOneOnOne} legacyUsernameColor={isOneOnOne}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
</SequenceCard> </SequenceCard>
</VirtualTile> </VirtualTile>

View file

@ -28,8 +28,6 @@ import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css'; import { SequenceCardStyle } from '../styles.css';
import { LogoutDialog } from '../../../components/LogoutDialog'; import { LogoutDialog } from '../../../components/LogoutDialog';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
export function DeviceTilePlaceholder() { export function DeviceTilePlaceholder() {
return ( return (
@ -45,8 +43,6 @@ export function DeviceTilePlaceholder() {
function DeviceActiveTime({ ts }: { ts: number }) { function DeviceActiveTime({ ts }: { ts: number }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
return ( return (
<Text className={BreakWord} size="T200"> <Text className={BreakWord} size="T200">
@ -56,8 +52,8 @@ function DeviceActiveTime({ ts }: { ts: number }) {
<> <>
{today(ts) && t('Settings.today')} {today(ts) && t('Settings.today')}
{yesterday(ts) && t('Settings.yesterday')} {yesterday(ts) && t('Settings.yesterday')}
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts, dateFormatString)}{' '} {!today(ts) && !yesterday(ts) && timeDayMonYear(ts)}{' '}
{timeHourMinute(ts, hour24Clock)} {timeHourMinute(ts)}
</> </>
</Text> </Text>
); );

View file

@ -1,25 +1,10 @@
import React, { import React, { ChangeEventHandler, KeyboardEventHandler, useState } from 'react';
ChangeEventHandler,
FormEventHandler,
KeyboardEventHandler,
MouseEventHandler,
useEffect,
useState,
} from 'react';
import dayjs from 'dayjs';
import { import {
Box, Box,
Button,
config,
Header,
Icon, Icon,
IconButton, IconButton,
Icons, Icons,
Input, Input,
Menu,
MenuItem,
PopOut,
RectCords,
Scroll, Scroll,
Switch, Switch,
Text, Text,
@ -27,16 +12,13 @@ import {
} from 'folds'; } from 'folds';
import { isKeyHotkey } from 'is-hotkey'; import { isKeyHotkey } from 'is-hotkey';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import FocusTrap from 'focus-trap-react';
import { Page, PageContent, PageHeader } from '../../../components/page'; import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
import { useSetting } from '../../../state/hooks/settings'; import { useSetting } from '../../../state/hooks/settings';
import { DateFormat, settingsAtom } from '../../../state/settings'; import { settingsAtom } from '../../../state/settings';
import { SettingTile } from '../../../components/setting-tile'; import { SettingTile } from '../../../components/setting-tile';
import { KeySymbol } from '../../../utils/key-symbol'; import { KeySymbol } from '../../../utils/key-symbol';
import { isMacOS } from '../../../utils/user-agent'; import { isMacOS } from '../../../utils/user-agent';
import { stopPropagation } from '../../../utils/keyboard';
import { useDateFormatItems } from '../../../hooks/useDateFormat';
import { SequenceCardStyle } from '../styles.css'; import { SequenceCardStyle } from '../styles.css';
function ThemeSelect() { function ThemeSelect() {
@ -128,375 +110,6 @@ function Appearance() {
); );
} }
type DateHintProps = {
hasChanges: boolean;
handleReset: () => void;
};
function DateHint({ hasChanges, handleReset }: DateHintProps) {
const { t } = useTranslation();
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">{t('Settings.formatting')}</Text>
</Header>
<Box direction="Column">
<Box style={categoryPadding} direction="Column">
<Header size="300">
<Text size="L400">{t('Settings.year')}</Text>
</Header>
<Box direction="Column" tabIndex={0} gap="100">
<Text size="T300">
YY
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.two_digit_year')}
</Text>{' '}
</Text>
<Text size="T300">
YYYY
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.four_digit_year')}
</Text>
</Text>
</Box>
</Box>
<Box style={categoryPadding} direction="Column">
<Header size="300">
<Text size="L400">{t('Settings.month')}</Text>
</Header>
<Box direction="Column" tabIndex={0} gap="100">
<Text size="T300">
M
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.the_month')}
</Text>
</Text>
<Text size="T300">
MM
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.two_digit_month')}
</Text>{' '}
</Text>
<Text size="T300">
MMM
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.short_month_name')}
</Text>
</Text>
<Text size="T300">
MMMM
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.full_month_name')}
</Text>
</Text>
</Box>
</Box>
<Box style={categoryPadding} direction="Column">
<Header size="300">
<Text size="L400">{t('Settings.day_of_month')}</Text>
</Header>
<Box direction="Column" tabIndex={0} gap="100">
<Text size="T300">
D
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.day_of_month_val')}
</Text>
</Text>
<Text size="T300">
DD
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.two_digit_day')}
</Text>
</Text>
</Box>
</Box>
<Box style={categoryPadding} direction="Column">
<Header size="300">
<Text size="L400">{t('Settings.day_of_week')}</Text>
</Header>
<Box direction="Column" tabIndex={0} gap="100">
<Text size="T300">
d
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.day_of_week_sunday')}
</Text>
</Text>
<Text size="T300">
dd
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.two_letter_day')}
</Text>
</Text>
<Text size="T300">
ddd
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.short_day_name')}
</Text>
</Text>
<Text size="T300">
dddd
<Text as="span" size="Inherit" priority="300">
{': '}
{t('Settings.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 { t } = useTranslation();
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">{t('Settings.save')}</Text>
</Button>
</Box>
</SettingTile>
);
}
type PresetDateFormatProps = {
value: string;
onChange: (format: string) => void;
};
function PresetDateFormat({ value, onChange }: PresetDateFormatProps) {
const { t } = useTranslation();
const [menuCords, setMenuCords] = useState<RectCords>();
const dateFormatItems = useDateFormatItems();
const getDisplayDate = (format: string): string =>
format !== '' ? dayjs().format(format) : t('Settings.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 { t } = useTranslation();
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={t('Settings.date_format')}
description={customDateFormat ? dayjs().format(dateFormatString) : ''}
after={<PresetDateFormat value={selectedDateFormat} onChange={handlePresetChange} />}
/>
{customDateFormat && (
<CustomDateFormat value={dateFormatString} onChange={setDateFormatString} />
)}
</>
);
}
function DateAndTime() {
const { t } = useTranslation();
const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock');
return (
<Box direction="Column" gap="100">
<Text size="L400">{t('Settings.date_time')}</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title={t('Settings.hour_24')}
after={<Switch variant="Primary" value={hour24Clock} onChange={setHour24Clock} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SelectDateFormat />
</SequenceCard>
</Box>
);
}
function Editor() { function Editor() {
const { t } = useTranslation(); const { t } = useTranslation();
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
@ -636,7 +249,6 @@ export function General({ requestClose }: GeneralProps) {
<PageContent> <PageContent>
<Box direction="Column" gap="700"> <Box direction="Column" gap="700">
<Appearance /> <Appearance />
<DateAndTime />
<Editor /> <Editor />
<Messages /> <Messages />
</Box> </Box>

View file

@ -1,38 +0,0 @@
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: 'YYYY-MM-DD',
name: 'YYYY-MM-DD',
},
{
format: '',
name: 'Custom',
},
],
[]
);

View file

@ -1,13 +1,6 @@
import { atom } from 'jotai'; import { atom } from 'jotai';
const STORAGE_KEY = 'settings'; const STORAGE_KEY = 'settings';
export type DateFormat =
| 'D MMM YYYY'
| 'DD/MM/YYYY'
| 'MM/DD/YYYY'
| 'YYYY/MM/DD'
| 'YYYY-MM-DD'
| '';
export interface Settings { export interface Settings {
themeId?: string; themeId?: string;
@ -32,9 +25,6 @@ export interface Settings {
isNotificationSounds: boolean; isNotificationSounds: boolean;
inviteSpamFilter: boolean; inviteSpamFilter: boolean;
hour24Clock: boolean;
dateFormatString: string;
developerTools: boolean; developerTools: boolean;
migrationsApplied?: Record<string, boolean>; migrationsApplied?: Record<string, boolean>;
@ -42,6 +32,7 @@ export interface Settings {
const DAWN_MIGRATION_KEY = 'dawn-redesign-v1'; const DAWN_MIGRATION_KEY = 'dawn-redesign-v1';
const P3C_CLEANUP_KEY = 'dawn-p3c-cleanup'; const P3C_CLEANUP_KEY = 'dawn-p3c-cleanup';
const SYSTEM_TIME_FORMAT_CLEANUP_KEY = 'system-time-format-cleanup';
const defaultSettings: Settings = { const defaultSettings: Settings = {
themeId: undefined, themeId: undefined,
@ -66,9 +57,6 @@ const defaultSettings: Settings = {
isNotificationSounds: true, isNotificationSounds: true,
inviteSpamFilter: true, inviteSpamFilter: true,
hour24Clock: false,
dateFormatString: 'D MMM YYYY',
developerTools: false, developerTools: false,
}; };
@ -121,6 +109,21 @@ export const getSettings = (): Settings => {
setSettings(merged); setSettings(merged);
} }
// System time-format migration: drop the user-facing `hour24Clock` /
// `dateFormatString` fields — both now derived from the runtime locale via
// `Intl.DateTimeFormat` (see `utils/time.ts`). One-shot strip of the orphan
// keys from existing users' persisted JSON.
if (!merged.migrationsApplied?.[SYSTEM_TIME_FORMAT_CLEANUP_KEY]) {
const orphan = merged as unknown as Record<string, unknown>;
delete orphan.hour24Clock;
delete orphan.dateFormatString;
merged.migrationsApplied = {
...(merged.migrationsApplied ?? {}),
[SYSTEM_TIME_FORMAT_CLEANUP_KEY]: true,
};
setSettings(merged);
}
return merged; return merged;
}; };

View file

@ -5,12 +5,36 @@ import isYesterday from 'dayjs/plugin/isYesterday';
dayjs.extend(isToday); dayjs.extend(isToday);
dayjs.extend(isYesterday); dayjs.extend(isYesterday);
// Detect once at module load — the runtime locale doesn't change without a
// page reload. AM/PM systems set `hour12: true`; 24-hour systems leave it
// false/undefined. Older WebView builds may omit `hour12` entirely, so we
// fall back to the modern `hourCycle` field (h11/h12 = 12-hour, h23/h24 =
// 24-hour) before defaulting to 24-hour.
const detectSystemHour24 = (): boolean => {
try {
const opts = new Intl.DateTimeFormat(undefined, { hour: 'numeric' }).resolvedOptions();
if (opts.hour12 === true) return false;
if (opts.hour12 === false) return true;
const cycle = (opts as { hourCycle?: string }).hourCycle;
if (cycle === 'h11' || cycle === 'h12') return false;
if (cycle === 'h23' || cycle === 'h24') return true;
return true;
} catch {
return true;
}
};
export const SYSTEM_HOUR_24: boolean = detectSystemHour24();
// 24-hour systems get the European day-month-year layout; 12-hour systems get
// the American month-day-year layout.
const SYSTEM_DAY_MON_YEAR_FORMAT = SYSTEM_HOUR_24 ? 'DD/MM/YYYY' : 'MM/DD/YYYY';
export const today = (ts: number): boolean => dayjs(ts).isToday(); export const today = (ts: number): boolean => dayjs(ts).isToday();
export const yesterday = (ts: number): boolean => dayjs(ts).isYesterday(); export const yesterday = (ts: number): boolean => dayjs(ts).isYesterday();
export const timeHour = (ts: number, hour24Clock: boolean): string => export const timeHour = (ts: number): string => dayjs(ts).format(SYSTEM_HOUR_24 ? 'HH' : 'hh');
dayjs(ts).format(hour24Clock ? 'HH' : 'hh');
export const timeMinute = (ts: number): string => dayjs(ts).format('mm'); export const timeMinute = (ts: number): string => dayjs(ts).format('mm');
export const timeAmPm = (ts: number): string => dayjs(ts).format('A'); export const timeAmPm = (ts: number): string => dayjs(ts).format('A');
export const timeDay = (ts: number): string => dayjs(ts).format('D'); export const timeDay = (ts: number): string => dayjs(ts).format('D');
@ -18,11 +42,10 @@ export const timeMon = (ts: number): string => dayjs(ts).format('MMM');
export const timeMonth = (ts: number): string => dayjs(ts).format('MMMM'); export const timeMonth = (ts: number): string => dayjs(ts).format('MMMM');
export const timeYear = (ts: number): string => dayjs(ts).format('YYYY'); export const timeYear = (ts: number): string => dayjs(ts).format('YYYY');
export const timeHourMinute = (ts: number, hour24Clock: boolean): string => export const timeHourMinute = (ts: number): string =>
dayjs(ts).format(hour24Clock ? 'HH:mm' : 'hh:mm A'); dayjs(ts).format(SYSTEM_HOUR_24 ? 'HH:mm' : 'hh:mm A');
export const timeDayMonYear = (ts: number, dateFormatString: string): string => export const timeDayMonYear = (ts: number): string => dayjs(ts).format(SYSTEM_DAY_MON_YEAR_FORMAT);
dayjs(ts).format(dateFormatString);
export const timeDayMonthYear = (ts: number): string => dayjs(ts).format('D MMMM YYYY'); export const timeDayMonthYear = (ts: number): string => dayjs(ts).format('D MMMM YYYY');