Add settings to enable 24-hour time format and customizable date format (#2347)

* 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
This commit is contained in:
Gimle Larpes 2025-07-27 15:13:00 +03:00 committed by GitHub
parent 67b05eeb09
commit 9183fd66b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 691 additions and 82 deletions

View file

@ -4,10 +4,25 @@ import PropTypes from 'prop-types';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { isInSameDay } from '../../../util/common'; 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 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; let formattedDate = formattedFullTime;
if (!fullTime) { if (!fullTime) {
@ -16,17 +31,19 @@ function Time({ timestamp, fullTime }) {
compareDate.setDate(compareDate.getDate() - 1); compareDate.setDate(compareDate.getDate() - 1);
const isYesterday = isInSameDay(date, compareDate); 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) { if (isYesterday) {
formattedDate = `Yesterday, ${formattedDate}`; formattedDate = `Yesterday, ${formattedDate}`;
} }
} }
return ( return (
<time <time dateTime={date.toISOString()} title={formattedFullTime}>
dateTime={date.toISOString()}
title={formattedFullTime}
>
{formattedDate} {formattedDate}
</time> </time>
); );
@ -39,6 +56,8 @@ Time.defaultProps = {
Time.propTypes = { Time.propTypes = {
timestamp: PropTypes.number.isRequired, timestamp: PropTypes.number.isRequired,
fullTime: PropTypes.bool, fullTime: PropTypes.bool,
hour24Clock: PropTypes.bool.isRequired,
dateFormatString: PropTypes.string.isRequired,
}; };
export default Time; export default Time;

View file

@ -5,19 +5,35 @@ 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.
*
* 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>>( 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 = ''; let time = '';
if (compact) { if (compact) {
time = timeHourMinute(ts); time = formattedTime;
} else if (today(ts)) { } else if (today(ts)) {
time = timeHourMinute(ts); time = formattedTime;
} else if (yesterday(ts)) { } else if (yesterday(ts)) {
time = `Yesterday ${timeHourMinute(ts)}`; time = `Yesterday ${formattedTime}`;
} else { } else {
time = `${timeDayMonYear(ts)} ${timeHourMinute(ts)}`; time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
} }
return ( return (

View file

@ -15,6 +15,8 @@ import { nameInitials } from '../../utils/common';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta'; import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList'; import { mDirectAtom } from '../../state/mDirectList';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
export type RoomIntroProps = { export type RoomIntroProps = {
room: Room; room: Room;
@ -43,6 +45,8 @@ 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>
@ -67,7 +71,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
<Text size="T200" priority="300"> <Text size="T200" priority="300">
{'Created by '} {'Created by '}
<b>@{creatorName}</b> <b>@{creatorName}</b>
{` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts)}`} {` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts, hour24Clock)}`}
</Text> </Text>
)} )}
</Box> </Box>

View file

@ -4,6 +4,8 @@ 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 { hour12to24, hour24to12, hoursToMs, inSameDay, minutesToMs } from '../../utils/time';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
type TimePickerProps = { type TimePickerProps = {
min: number; min: number;
@ -13,9 +15,11 @@ 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 = hour24to12(hour24); const selectedHour = hour24Clock ? hour24 : hour24to12(hour24);
const selectedMinute = dayjs(value).minute(); const selectedMinute = dayjs(value).minute();
const selectedPM = hour24 >= 12; const selectedPM = hour24 >= 12;
@ -24,7 +28,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
}; };
const handleHour = (hour: number) => { const handleHour = (hour: number) => {
const seconds = hoursToMs(hour12to24(hour, selectedPM)); const seconds = hoursToMs(hour24Clock ? 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);
@ -59,28 +63,43 @@ 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">
{Array.from(Array(12).keys()) {hour24Clock
.map((i) => { ? Array.from(Array(24).keys()).map((hour) => (
if (i === 0) return 12; <Chip
return i; key={hour}
}) size="500"
.map((hour) => ( variant={hour === selectedHour ? 'Primary' : 'Background'}
<Chip fill="None"
key={hour} radii="300"
size="500" aria-selected={hour === selectedHour}
variant={hour === selectedHour ? 'Primary' : 'Background'} onClick={() => handleHour(hour)}
fill="None" disabled={(minDay && hour < minHour24) || (maxDay && hour > maxHour24)}
radii="300" >
aria-selected={hour === selectedHour} <Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
onClick={() => handleHour(hour)} </Chip>
disabled={ ))
(minDay && hour12to24(hour, selectedPM) < minHour24) || : Array.from(Array(12).keys())
(maxDay && hour12to24(hour, selectedPM) > maxHour24) .map((i) => {
} if (i === 0) return 12;
> return i;
<Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text> })
</Chip> .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>
<PickerColumn title="Minutes"> <PickerColumn title="Minutes">
{Array.from(Array(60).keys()).map((minute) => ( {Array.from(Array(60).keys()).map((minute) => (
@ -101,30 +120,32 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
</Chip> </Chip>
))} ))}
</PickerColumn> </PickerColumn>
<PickerColumn title="Period"> {!hour24Clock && (
<Chip <PickerColumn title="Period">
size="500" <Chip
variant={!selectedPM ? 'Primary' : 'SurfaceVariant'} size="500"
fill="None" variant={!selectedPM ? 'Primary' : 'SurfaceVariant'}
radii="300" fill="None"
aria-selected={!selectedPM} radii="300"
onClick={() => handlePeriod(false)} aria-selected={!selectedPM}
disabled={minDay && minPM} onClick={() => handlePeriod(false)}
> disabled={minDay && minPM}
<Text size="T300">AM</Text> >
</Chip> <Text size="T300">AM</Text>
<Chip </Chip>
size="500" <Chip
variant={selectedPM ? 'Primary' : 'SurfaceVariant'} size="500"
fill="None" variant={selectedPM ? 'Primary' : 'SurfaceVariant'}
radii="300" fill="None"
aria-selected={selectedPM} radii="300"
onClick={() => handlePeriod(true)} aria-selected={selectedPM}
disabled={maxDay && !maxPM} onClick={() => handlePeriod(true)}
> disabled={maxDay && !maxPM}
<Text size="T300">PM</Text> >
</Chip> <Text size="T300">PM</Text>
</PickerColumn> </Chip>
</PickerColumn>
)}
</Box> </Box>
</Menu> </Menu>
); );

View file

@ -57,6 +57,9 @@ export function MessageSearch({
const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
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();
@ -289,6 +292,8 @@ export function MessageSearch({
urlPreview={urlPreview} urlPreview={urlPreview}
onOpen={navigateRoom} onOpen={navigateRoom}
legacyUsernameColor={legacyUsernameColor || mDirects.has(groupRoom.roomId)} legacyUsernameColor={legacyUsernameColor || mDirects.has(groupRoom.roomId)}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
</VirtualTile> </VirtualTile>
); );

View file

@ -57,6 +57,8 @@ 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,
@ -66,6 +68,8 @@ export function SearchResultGroup({
urlPreview, urlPreview,
onOpen, onOpen,
legacyUsernameColor, legacyUsernameColor,
hour24Clock,
dateFormatString,
}: SearchResultGroupProps) { }: SearchResultGroupProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
@ -275,7 +279,11 @@ export function SearchResultGroup({
</Username> </Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />} {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box> </Box>
<Time ts={event.origin_server_ts} /> <Time
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

@ -450,6 +450,9 @@ export function RoomTimeline({
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]);
@ -1072,6 +1075,8 @@ export function RoomTimeline({
powerLevelTag={getPowerLevelTag(senderPowerLevel)} powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors} accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct} legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
> >
{mEvent.isRedacted() ? ( {mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} /> <RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
@ -1154,6 +1159,8 @@ export function RoomTimeline({
powerLevelTag={getPowerLevelTag(senderPowerLevel)} powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors} accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct} legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
> >
<EncryptedContent mEvent={mEvent}> <EncryptedContent mEvent={mEvent}>
{() => { {() => {
@ -1256,6 +1263,8 @@ export function RoomTimeline({
powerLevelTag={getPowerLevelTag(senderPowerLevel)} powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors} accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct} legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
> >
{mEvent.isRedacted() ? ( {mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} /> <RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
@ -1284,7 +1293,12 @@ export function RoomTimeline({
const parsed = parseMemberEvent(mEvent); const parsed = parseMemberEvent(mEvent);
const timeJSX = ( const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} /> <Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
); );
return ( return (
@ -1321,7 +1335,12 @@ export function RoomTimeline({
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId); const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = ( const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} /> <Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
); );
return ( return (
@ -1359,7 +1378,12 @@ export function RoomTimeline({
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId); const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = ( const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} /> <Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
); );
return ( return (
@ -1397,7 +1421,12 @@ export function RoomTimeline({
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId); const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = ( const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} /> <Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
); );
return ( return (
@ -1437,7 +1466,12 @@ export function RoomTimeline({
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId); const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = ( const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} /> <Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
); );
return ( return (
@ -1482,7 +1516,12 @@ export function RoomTimeline({
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId); const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = ( const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} /> <Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
); );
return ( return (

View file

@ -29,6 +29,8 @@ 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;
@ -45,6 +47,8 @@ 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>();
@ -125,7 +129,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)}</Text> <Text size="B300">{timeHourMinute(ts, hour24Clock)}</Text>
</Chip> </Chip>
<PopOut <PopOut
anchor={timePickerCords} anchor={timePickerCords}

View file

@ -682,6 +682,8 @@ export type MessageProps = {
powerLevelTag?: PowerLevelTag; powerLevelTag?: PowerLevelTag;
accessibleTagColors?: Map<string, string>; accessibleTagColors?: Map<string, string>;
legacyUsernameColor?: boolean; legacyUsernameColor?: boolean;
hour24Clock: boolean;
dateFormatString: string;
}; };
export const Message = as<'div', MessageProps>( export const Message = as<'div', MessageProps>(
( (
@ -711,6 +713,8 @@ export const Message = as<'div', MessageProps>(
powerLevelTag, powerLevelTag,
accessibleTagColors, accessibleTagColors,
legacyUsernameColor, legacyUsernameColor,
hour24Clock,
dateFormatString,
children, children,
...props ...props
}, },
@ -775,7 +779,12 @@ export const Message = as<'div', MessageProps>(
</Text> </Text>
</> </>
)} )}
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} /> <Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
</Box> </Box>
</Box> </Box>
); );

View file

@ -102,6 +102,9 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
const theme = useTheme(); const theme = useTheme();
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags); const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const [unpinState, unpin] = useAsyncCallback( const [unpinState, unpin] = useAsyncCallback(
useCallback(() => { useCallback(() => {
const pinEvent = getStateEvent(room, StateEvent.RoomPinnedEvents); const pinEvent = getStateEvent(room, StateEvent.RoomPinnedEvents);
@ -205,7 +208,11 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
</Username> </Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />} {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box> </Box>
<Time ts={pinnedEvent.getTs()} /> <Time
ts={pinnedEvent.getTs()}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
</Box> </Box>
{renderOptions()} {renderOptions()}
</Box> </Box>

View file

@ -27,6 +27,8 @@ 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 (
@ -41,6 +43,9 @@ export function DeviceTilePlaceholder() {
} }
function DeviceActiveTime({ ts }: { ts: number }) { function DeviceActiveTime({ ts }: { ts: number }) {
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
return ( return (
<Text className={BreakWord} size="T200"> <Text className={BreakWord} size="T200">
<Text size="Inherit" as="span" priority="300"> <Text size="Inherit" as="span" priority="300">
@ -49,7 +54,8 @@ function DeviceActiveTime({ ts }: { ts: number }) {
<> <>
{today(ts) && 'Today'} {today(ts) && 'Today'}
{yesterday(ts) && 'Yesterday'} {yesterday(ts) && 'Yesterday'}
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)} {!today(ts) && !yesterday(ts) && timeDayMonYear(ts, dateFormatString)}{' '}
{timeHourMinute(ts, hour24Clock)}
</> </>
</Text> </Text>
); );

View file

@ -1,15 +1,19 @@
import React, { import React, {
ChangeEventHandler, ChangeEventHandler,
FormEventHandler,
KeyboardEventHandler, KeyboardEventHandler,
MouseEventHandler, MouseEventHandler,
useEffect,
useState, useState,
} from 'react'; } from 'react';
import dayjs from 'dayjs';
import { import {
as, as,
Box, Box,
Button, Button,
Chip, Chip,
config, config,
Header,
Icon, Icon,
IconButton, IconButton,
Icons, Icons,
@ -28,7 +32,7 @@ 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 { MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings'; import { DateFormat, MessageLayout, MessageSpacing, 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';
@ -44,6 +48,7 @@ import {
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout'; import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing'; import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
import { useDateFormatItems } from '../../../hooks/useDateFormat';
import { SequenceCardStyle } from '../styles.css'; import { SequenceCardStyle } from '../styles.css';
type ThemeSelectorProps = { 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() { function Editor() {
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
@ -637,6 +995,7 @@ 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

@ -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',
},
],
[]
);

View file

@ -65,6 +65,8 @@ import { testBadWords } from '../../../plugins/bad-words';
import { allRoomsAtom } from '../../../state/room-list/roomList'; import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers'; import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported'; import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
const COMPACT_CARD_WIDTH = 548; const COMPACT_CARD_WIDTH = 548;
@ -135,10 +137,19 @@ type NavigateHandler = (roomId: string, space: boolean) => void;
type InviteCardProps = { type InviteCardProps = {
invite: InviteData; invite: InviteData;
compact?: boolean; compact?: boolean;
hour24Clock: boolean;
dateFormatString: string;
onNavigate: NavigateHandler; onNavigate: NavigateHandler;
hideAvatar: boolean; hideAvatar: boolean;
}; };
function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps) { function InviteCard({
invite,
compact,
hour24Clock,
dateFormatString,
onNavigate,
hideAvatar,
}: InviteCardProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const userId = mx.getSafeUserId(); const userId = mx.getSafeUserId();
@ -295,7 +306,13 @@ function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps
</Box> </Box>
{invite.inviteTs && ( {invite.inviteTs && (
<Box shrink="No"> <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>
)} )}
</Box> </Box>
@ -384,8 +401,16 @@ type KnownInvitesProps = {
invites: InviteData[]; invites: InviteData[];
handleNavigate: NavigateHandler; handleNavigate: NavigateHandler;
compact: boolean; compact: boolean;
hour24Clock: boolean;
dateFormatString: string;
}; };
function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) { function KnownInvites({
invites,
handleNavigate,
compact,
hour24Clock,
dateFormatString,
}: KnownInvitesProps) {
return ( return (
<Box direction="Column" gap="200"> <Box direction="Column" gap="200">
<Text size="H4">Primary</Text> <Text size="H4">Primary</Text>
@ -396,6 +421,8 @@ function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) {
key={invite.roomId} key={invite.roomId}
invite={invite} invite={invite}
compact={compact} compact={compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
onNavigate={handleNavigate} onNavigate={handleNavigate}
hideAvatar={false} hideAvatar={false}
/> />
@ -420,8 +447,16 @@ type UnknownInvitesProps = {
invites: InviteData[]; invites: InviteData[];
handleNavigate: NavigateHandler; handleNavigate: NavigateHandler;
compact: boolean; compact: boolean;
hour24Clock: boolean;
dateFormatString: string;
}; };
function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProps) { function UnknownInvites({
invites,
handleNavigate,
compact,
hour24Clock,
dateFormatString,
}: UnknownInvitesProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [declineAllStatus, declineAll] = useAsyncCallback( const [declineAllStatus, declineAll] = useAsyncCallback(
@ -459,6 +494,8 @@ function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProp
key={invite.roomId} key={invite.roomId}
invite={invite} invite={invite}
compact={compact} compact={compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
onNavigate={handleNavigate} onNavigate={handleNavigate}
hideAvatar hideAvatar
/> />
@ -483,8 +520,16 @@ type SpamInvitesProps = {
invites: InviteData[]; invites: InviteData[];
handleNavigate: NavigateHandler; handleNavigate: NavigateHandler;
compact: boolean; compact: boolean;
hour24Clock: boolean;
dateFormatString: string;
}; };
function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) { function SpamInvites({
invites,
handleNavigate,
compact,
hour24Clock,
dateFormatString,
}: SpamInvitesProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [showInvites, setShowInvites] = useState(false); const [showInvites, setShowInvites] = useState(false);
@ -608,6 +653,8 @@ function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) {
key={invite.roomId} key={invite.roomId}
invite={invite} invite={invite}
compact={compact} compact={compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
onNavigate={handleNavigate} onNavigate={handleNavigate}
hideAvatar hideAvatar
/> />
@ -671,6 +718,9 @@ export function Invites() {
); );
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const handleNavigate = (roomId: string, space: boolean) => { const handleNavigate = (roomId: string, space: boolean) => {
if (space) { if (space) {
navigateSpace(roomId); navigateSpace(roomId);
@ -723,6 +773,8 @@ export function Invites() {
<KnownInvites <KnownInvites
invites={knownInvites} invites={knownInvites}
compact={compact} compact={compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
handleNavigate={handleNavigate} handleNavigate={handleNavigate}
/> />
)} )}
@ -731,6 +783,8 @@ export function Invites() {
<UnknownInvites <UnknownInvites
invites={unknownInvites} invites={unknownInvites}
compact={compact} compact={compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
handleNavigate={handleNavigate} handleNavigate={handleNavigate}
/> />
)} )}
@ -739,6 +793,8 @@ export function Invites() {
<SpamInvites <SpamInvites
invites={spamInvites} invites={spamInvites}
compact={compact} compact={compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
handleNavigate={handleNavigate} handleNavigate={handleNavigate}
/> />
)} )}

View file

@ -205,6 +205,8 @@ type RoomNotificationsGroupProps = {
hideActivity: boolean; hideActivity: boolean;
onOpen: (roomId: string, eventId: string) => void; onOpen: (roomId: string, eventId: string) => void;
legacyUsernameColor?: boolean; legacyUsernameColor?: boolean;
hour24Clock: boolean;
dateFormatString: string;
}; };
function RoomNotificationsGroupComp({ function RoomNotificationsGroupComp({
room, room,
@ -214,6 +216,8 @@ function RoomNotificationsGroupComp({
hideActivity, hideActivity,
onOpen, onOpen,
legacyUsernameColor, legacyUsernameColor,
hour24Clock,
dateFormatString,
}: RoomNotificationsGroupProps) { }: RoomNotificationsGroupProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
@ -496,7 +500,11 @@ function RoomNotificationsGroupComp({
</Username> </Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />} {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box> </Box>
<Time ts={event.origin_server_ts} /> <Time
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
@ -549,6 +557,8 @@ export function Notifications() {
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
@ -713,6 +723,8 @@ export function Notifications() {
legacyUsernameColor={ legacyUsernameColor={
legacyUsernameColor || mDirects.has(groupRoom.roomId) legacyUsernameColor || mDirects.has(groupRoom.roomId)
} }
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
</VirtualTile> </VirtualTile>
); );

View file

@ -1,6 +1,7 @@
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' | '';
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500'; export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
export enum MessageLayout { export enum MessageLayout {
Modern = 0, Modern = 0,
@ -35,6 +36,9 @@ export interface Settings {
showNotifications: boolean; showNotifications: boolean;
isNotificationSounds: boolean; isNotificationSounds: boolean;
hour24Clock: boolean;
dateFormatString: string;
developerTools: boolean; developerTools: boolean;
} }
@ -65,6 +69,9 @@ const defaultSettings: Settings = {
showNotifications: true, showNotifications: true,
isNotificationSounds: true, isNotificationSounds: true,
hour24Clock: false,
dateFormatString: 'D MMM YYYY',
developerTools: false, developerTools: false,
}; };

View 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 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 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');
@ -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 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): 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'); export const timeDayMonthYear = (ts: number): string => dayjs(ts).format('D MMMM YYYY');