feat(settings): drop user-facing time/date format toggle and derive everything from system locale via Intl.DateTimeFormat
This commit is contained in:
parent
541f9181d4
commit
f63e43bfc6
19 changed files with 81 additions and 613 deletions
|
|
@ -148,7 +148,7 @@ These are vojo additions on top of stock Cinny — they crossed many recent stab
|
|||
|
||||
**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
|
||||
- `upload.ts` — Upload progress (in-memory)
|
||||
- `room/` — `roomInputDrafts` (in-memory), `roomToParents`, `roomToUnread`
|
||||
|
|
|
|||
|
|
@ -99,27 +99,6 @@
|
|||
"monochrome_mode": "Monochrome Mode",
|
||||
"twitter_emoji": "Twitter Emoji",
|
||||
"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",
|
||||
"editor": "Editor",
|
||||
"enter_newline": "ENTER for Newline",
|
||||
|
|
|
|||
|
|
@ -99,27 +99,6 @@
|
|||
"monochrome_mode": "Монохромный режим",
|
||||
"twitter_emoji": "Эмодзи Twitter",
|
||||
"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": "Сохранить",
|
||||
"editor": "Редактор",
|
||||
"enter_newline": "ENTER для новой строки",
|
||||
|
|
|
|||
|
|
@ -6,26 +6,17 @@ 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* Today/yesterday/`compact` show time only; older messages show date + time.
|
||||
*/
|
||||
export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
|
||||
({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => {
|
||||
({ compact, ts, ...props }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const formattedTime = timeHourMinute(ts, hour24Clock);
|
||||
const formattedTime = timeHourMinute(ts);
|
||||
|
||||
let time = '';
|
||||
if (compact) {
|
||||
|
|
@ -35,7 +26,7 @@ export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
|
|||
} else if (yesterday(ts)) {
|
||||
time = `${t('Room.yesterday')} ${formattedTime}`;
|
||||
} else {
|
||||
time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
|
||||
time = `${timeDayMonYear(ts)} ${formattedTime}`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ import { nameInitials } from '../../utils/common';
|
|||
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
||||
import { useIsOneOnOne } from '../../hooks/useRoom';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { InviteUserPrompt } from '../invite-user-prompt';
|
||||
|
||||
export type RoomIntroProps = {
|
||||
|
|
@ -51,8 +49,6 @@ 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>
|
||||
|
|
@ -80,7 +76,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
|
|||
values={{
|
||||
creator: creatorName,
|
||||
date: timeDayMonthYear(ts),
|
||||
time: timeHourMinute(ts, hour24Clock),
|
||||
time: timeHourMinute(ts),
|
||||
}}
|
||||
components={{ bold: <b /> }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,14 @@ import { Menu, Box, Text, Chip } from 'folds';
|
|||
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';
|
||||
import {
|
||||
SYSTEM_HOUR_24,
|
||||
hour12to24,
|
||||
hour24to12,
|
||||
hoursToMs,
|
||||
inSameDay,
|
||||
minutesToMs,
|
||||
} from '../../utils/time';
|
||||
|
||||
type TimePickerProps = {
|
||||
min: number;
|
||||
|
|
@ -15,11 +20,9 @@ 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 = hour24Clock ? hour24 : hour24to12(hour24);
|
||||
const selectedHour = SYSTEM_HOUR_24 ? hour24 : hour24to12(hour24);
|
||||
const selectedMinute = dayjs(value).minute();
|
||||
const selectedPM = hour24 >= 12;
|
||||
|
||||
|
|
@ -28,7 +31,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||
};
|
||||
|
||||
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 newValue = value + (seconds - lastSeconds);
|
||||
handleSubmit(newValue);
|
||||
|
|
@ -63,7 +66,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||
<Menu className={css.PickerMenu} ref={ref}>
|
||||
<Box direction="Row" gap="200" className={css.PickerContainer}>
|
||||
<PickerColumn title="Hour">
|
||||
{hour24Clock
|
||||
{SYSTEM_HOUR_24
|
||||
? Array.from(Array(24).keys()).map((hour) => (
|
||||
<Chip
|
||||
key={hour}
|
||||
|
|
@ -120,7 +123,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||
</Chip>
|
||||
))}
|
||||
</PickerColumn>
|
||||
{!hour24Clock && (
|
||||
{!SYSTEM_HOUR_24 && (
|
||||
<PickerColumn title="Period">
|
||||
<Chip
|
||||
size="500"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import { SettingTile } from '../setting-tile';
|
|||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { BreakWord } from '../../styles/Text.css';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { timeDayMonYear, timeHourMinute } from '../../utils/time';
|
||||
|
||||
type UserKickAlertProps = {
|
||||
|
|
@ -16,11 +14,8 @@ type UserKickAlertProps = {
|
|||
ts?: number;
|
||||
};
|
||||
export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
|
||||
const time = ts ? timeHourMinute(ts) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts) : undefined;
|
||||
|
||||
return (
|
||||
<CutoutCard style={{ padding: config.space.S200 }} variant="Critical">
|
||||
|
|
@ -66,11 +61,8 @@ type UserBanAlertProps = {
|
|||
export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBanAlertProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
|
||||
const time = ts ? timeHourMinute(ts) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts) : undefined;
|
||||
|
||||
const [unbanState, unban] = useAsyncCallback<undefined, Error, []>(
|
||||
useCallback(async () => {
|
||||
|
|
@ -141,11 +133,8 @@ type UserInviteAlertProps = {
|
|||
export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: UserInviteAlertProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
|
||||
const time = ts ? timeHourMinute(ts) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts) : undefined;
|
||||
|
||||
const [kickState, kick] = useAsyncCallback<undefined, Error, []>(
|
||||
useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -60,9 +60,6 @@ export function MessageSearch({
|
|||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
|
@ -299,8 +296,6 @@ export function MessageSearch({
|
|||
urlPreview={urlPreview}
|
||||
onOpen={navigateRoom}
|
||||
legacyUsernameColor={isOneOnOneRoom(groupRoom)}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -61,8 +61,6 @@ type SearchResultGroupProps = {
|
|||
urlPreview?: boolean;
|
||||
onOpen: (roomId: string, eventId: string) => void;
|
||||
legacyUsernameColor?: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
};
|
||||
export function SearchResultGroup({
|
||||
room,
|
||||
|
|
@ -72,8 +70,6 @@ export function SearchResultGroup({
|
|||
urlPreview,
|
||||
onOpen,
|
||||
legacyUsernameColor,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
}: SearchResultGroupProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
|
|
@ -292,11 +288,7 @@ export function SearchResultGroup({
|
|||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time
|
||||
ts={event.origin_server_ts}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
<Time ts={event.origin_server_ts} />
|
||||
</Box>
|
||||
<Box shrink="No" gap="200" alignItems="Center">
|
||||
<Chip
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ const deriveRoomPreview = (room: Room): RoomPreview => {
|
|||
return { ts: room.getLastActiveTimestamp() || undefined, text: '' };
|
||||
};
|
||||
|
||||
const formatRowTime = (ts: number, hour24Clock: boolean): string => {
|
||||
const formatRowTime = (ts: number): string => {
|
||||
const now = Date.now();
|
||||
const diff = now - ts;
|
||||
const day = 24 * 60 * 60 * 1000;
|
||||
|
|
@ -270,7 +270,7 @@ const formatRowTime = (ts: number, hour24Clock: boolean): string => {
|
|||
date.getFullYear() === today.getFullYear() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getDate() === today.getDate();
|
||||
if (sameDay) return timeHourMinute(ts, hour24Clock);
|
||||
if (sameDay) return timeHourMinute(ts);
|
||||
if (diff < 7 * day) {
|
||||
return date.toLocaleDateString(undefined, { weekday: 'short' });
|
||||
}
|
||||
|
|
@ -287,7 +287,6 @@ export function DmStreamRow({ room, selected, notificationMode, linkPath }: DmSt
|
|||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [hover, setHover] = useState(false);
|
||||
const { hoverProps } = useHover({ onHoverChange: setHover });
|
||||
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
|
||||
|
|
@ -302,7 +301,7 @@ export function DmStreamRow({ room, selected, notificationMode, linkPath }: DmSt
|
|||
const preview = deriveRoomPreview(room);
|
||||
const previewText = preview.text;
|
||||
const previewTs = preview.ts;
|
||||
const timeLabel = previewTs ? formatRowTime(previewTs, hour24Clock) : '';
|
||||
const timeLabel = previewTs ? formatRowTime(previewTs) : '';
|
||||
|
||||
const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
|
|
|||
|
|
@ -435,9 +435,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
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]);
|
||||
|
||||
|
|
@ -1133,8 +1130,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
memberPowerTag={getMemberPowerTag(senderId)}
|
||||
accessibleTagColors={accessiblePowerTagColors}
|
||||
legacyUsernameColor={isOneOnOne}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
streamRailStart={streamRailStart}
|
||||
streamRailEnd={streamRailEnd}
|
||||
>
|
||||
|
|
@ -1235,8 +1230,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')}
|
||||
accessibleTagColors={accessiblePowerTagColors}
|
||||
legacyUsernameColor={isOneOnOne}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
streamRailStart={streamRailStart}
|
||||
streamRailEnd={streamRailEnd}
|
||||
>
|
||||
|
|
@ -1349,8 +1342,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')}
|
||||
accessibleTagColors={accessiblePowerTagColors}
|
||||
legacyUsernameColor={isOneOnOne}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
streamRailStart={streamRailStart}
|
||||
streamRailEnd={streamRailEnd}
|
||||
>
|
||||
|
|
@ -1397,8 +1388,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -1447,8 +1436,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -1498,8 +1485,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -1549,8 +1534,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -1608,8 +1591,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -1656,8 +1637,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -1706,8 +1685,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,6 @@ 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;
|
||||
|
|
@ -49,8 +47,6 @@ 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>();
|
||||
|
||||
|
|
@ -131,7 +127,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
|
|||
after={<Icon size="50" src={Icons.ChevronBottom} />}
|
||||
onClick={handleTimePicker}
|
||||
>
|
||||
<Text size="B300">{timeHourMinute(ts, hour24Clock)}</Text>
|
||||
<Text size="B300">{timeHourMinute(ts)}</Text>
|
||||
</Chip>
|
||||
<PopOut
|
||||
anchor={timePickerCords}
|
||||
|
|
|
|||
|
|
@ -668,8 +668,6 @@ export type MessageProps = {
|
|||
memberPowerTag?: MemberPowerTag;
|
||||
accessibleTagColors?: Map<string, string>;
|
||||
legacyUsernameColor?: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
streamRailStart?: boolean;
|
||||
streamRailEnd?: boolean;
|
||||
};
|
||||
|
|
@ -699,8 +697,6 @@ export const Message = as<'div', MessageProps>(
|
|||
memberPowerTag,
|
||||
accessibleTagColors,
|
||||
legacyUsernameColor,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
streamRailStart,
|
||||
streamRailEnd,
|
||||
children,
|
||||
|
|
@ -1047,14 +1043,7 @@ export const Message = as<'div', MessageProps>(
|
|||
</div>
|
||||
)}
|
||||
<StreamLayout
|
||||
time={
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
}
|
||||
time={<Time ts={mEvent.getTs()} compact />}
|
||||
dotColor={dot.color}
|
||||
dotOpacity={dot.opacity}
|
||||
isOwn={isOwnMessage}
|
||||
|
|
|
|||
|
|
@ -97,8 +97,6 @@ type PinnedMessageProps = {
|
|||
getMemberPowerTag: GetMemberPowerTag;
|
||||
accessibleTagColors: Map<string, string>;
|
||||
legacyUsernameColor: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
};
|
||||
function PinnedMessage({
|
||||
room,
|
||||
|
|
@ -109,8 +107,6 @@ function PinnedMessage({
|
|||
getMemberPowerTag,
|
||||
accessibleTagColors,
|
||||
legacyUsernameColor,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
}: PinnedMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const pinnedEvent = useRoomEvent(room, eventId);
|
||||
|
|
@ -221,11 +217,7 @@ function PinnedMessage({
|
|||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time
|
||||
ts={pinnedEvent.getTs()}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
<Time ts={pinnedEvent.getTs()} />
|
||||
</Box>
|
||||
{renderOptions()}
|
||||
</Box>
|
||||
|
|
@ -279,9 +271,6 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
|
|||
|
||||
const isOneOnOne = useIsOneOnOne();
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -499,8 +488,6 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
|
|||
getMemberPowerTag={getMemberPowerTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={isOneOnOne}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</VirtualTile>
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ 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 (
|
||||
|
|
@ -45,8 +43,6 @@ export function DeviceTilePlaceholder() {
|
|||
|
||||
function DeviceActiveTime({ ts }: { ts: number }) {
|
||||
const { t } = useTranslation();
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
return (
|
||||
<Text className={BreakWord} size="T200">
|
||||
|
|
@ -56,8 +52,8 @@ function DeviceActiveTime({ ts }: { ts: number }) {
|
|||
<>
|
||||
{today(ts) && t('Settings.today')}
|
||||
{yesterday(ts) && t('Settings.yesterday')}
|
||||
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts, dateFormatString)}{' '}
|
||||
{timeHourMinute(ts, hour24Clock)}
|
||||
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)}{' '}
|
||||
{timeHourMinute(ts)}
|
||||
</>
|
||||
</Text>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,10 @@
|
|||
import React, {
|
||||
ChangeEventHandler,
|
||||
FormEventHandler,
|
||||
KeyboardEventHandler,
|
||||
MouseEventHandler,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { ChangeEventHandler, KeyboardEventHandler, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
config,
|
||||
Header,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Input,
|
||||
Menu,
|
||||
MenuItem,
|
||||
PopOut,
|
||||
RectCords,
|
||||
Scroll,
|
||||
Switch,
|
||||
Text,
|
||||
|
|
@ -27,16 +12,13 @@ import {
|
|||
} from 'folds';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 { DateFormat, settingsAtom } from '../../../state/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { KeySymbol } from '../../../utils/key-symbol';
|
||||
import { isMacOS } from '../../../utils/user-agent';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { useDateFormatItems } from '../../../hooks/useDateFormat';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
|
||||
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() {
|
||||
const { t } = useTranslation();
|
||||
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
|
|
@ -636,7 +249,6 @@ export function General({ requestClose }: GeneralProps) {
|
|||
<PageContent>
|
||||
<Box direction="Column" gap="700">
|
||||
<Appearance />
|
||||
<DateAndTime />
|
||||
<Editor />
|
||||
<Messages />
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
|
@ -1,13 +1,6 @@
|
|||
import { atom } from 'jotai';
|
||||
|
||||
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 {
|
||||
themeId?: string;
|
||||
|
|
@ -32,9 +25,6 @@ export interface Settings {
|
|||
isNotificationSounds: boolean;
|
||||
inviteSpamFilter: boolean;
|
||||
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
|
||||
developerTools: boolean;
|
||||
|
||||
migrationsApplied?: Record<string, boolean>;
|
||||
|
|
@ -42,6 +32,7 @@ export interface Settings {
|
|||
|
||||
const DAWN_MIGRATION_KEY = 'dawn-redesign-v1';
|
||||
const P3C_CLEANUP_KEY = 'dawn-p3c-cleanup';
|
||||
const SYSTEM_TIME_FORMAT_CLEANUP_KEY = 'system-time-format-cleanup';
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
themeId: undefined,
|
||||
|
|
@ -66,9 +57,6 @@ const defaultSettings: Settings = {
|
|||
isNotificationSounds: true,
|
||||
inviteSpamFilter: true,
|
||||
|
||||
hour24Clock: false,
|
||||
dateFormatString: 'D MMM YYYY',
|
||||
|
||||
developerTools: false,
|
||||
};
|
||||
|
||||
|
|
@ -121,6 +109,21 @@ export const getSettings = (): Settings => {
|
|||
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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,36 @@ import isYesterday from 'dayjs/plugin/isYesterday';
|
|||
dayjs.extend(isToday);
|
||||
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 yesterday = (ts: number): boolean => dayjs(ts).isYesterday();
|
||||
|
||||
export const timeHour = (ts: number, hour24Clock: boolean): string =>
|
||||
dayjs(ts).format(hour24Clock ? 'HH' : 'hh');
|
||||
export const timeHour = (ts: number): string => dayjs(ts).format(SYSTEM_HOUR_24 ? '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');
|
||||
|
|
@ -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 timeYear = (ts: number): string => dayjs(ts).format('YYYY');
|
||||
|
||||
export const timeHourMinute = (ts: number, hour24Clock: boolean): string =>
|
||||
dayjs(ts).format(hour24Clock ? 'HH:mm' : 'hh:mm A');
|
||||
export const timeHourMinute = (ts: number): string =>
|
||||
dayjs(ts).format(SYSTEM_HOUR_24 ? 'HH:mm' : 'hh:mm A');
|
||||
|
||||
export const timeDayMonYear = (ts: number, dateFormatString: string): string =>
|
||||
dayjs(ts).format(dateFormatString);
|
||||
export const timeDayMonYear = (ts: number): string => dayjs(ts).format(SYSTEM_DAY_MON_YEAR_FORMAT);
|
||||
|
||||
export const timeDayMonthYear = (ts: number): string => dayjs(ts).format('D MMMM YYYY');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue