import dateFormat from 'dateformat';
import { isInSameDay } from '../../../util/common';
-function Time({ timestamp, fullTime }) {
+/**
+ * Renders a formatted timestamp.
+ *
+ * Displays the time in hour:minute format if the message is from today or yesterday, unless `fullTime` is true.
+ * For older messages, it shows the date and time.
+ *
+ * @param {number} timestamp - The timestamp to display.
+ * @param {boolean} [fullTime=false] - If true, always show the full date and time.
+ * @param {boolean} hour24Clock - Whether to use 24-hour time format.
+ * @param {string} dateFormatString - Format string for the date part.
+ * @returns {JSX.Element} A <time> element with the formatted date/time.
+ */
+function Time({ timestamp, fullTime, hour24Clock, dateFormatString }) {
const date = new Date(timestamp);
- const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
+ const formattedFullTime = dateFormat(
+ date,
+ hour24Clock ? 'dd mmmm yyyy, HH:MM' : 'dd mmmm yyyy, hh:MM TT'
+ );
let formattedDate = formattedFullTime;
if (!fullTime) {
compareDate.setDate(compareDate.getDate() - 1);
const isYesterday = isInSameDay(date, compareDate);
- formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy');
+ const timeFormat = hour24Clock ? 'HH:MM' : 'hh:MM TT';
+
+ formattedDate = dateFormat(
+ date,
+ isToday || isYesterday ? timeFormat : dateFormatString.toLowerCase()
+ );
if (isYesterday) {
formattedDate = `Yesterday, ${formattedDate}`;
}
}
return (
- <time
- dateTime={date.toISOString()}
- title={formattedFullTime}
- >
+ <time dateTime={date.toISOString()} title={formattedFullTime}>
{formattedDate}
</time>
);
Time.propTypes = {
timestamp: PropTypes.number.isRequired,
fullTime: PropTypes.bool,
+ hour24Clock: PropTypes.bool.isRequired,
+ dateFormatString: PropTypes.string.isRequired,
};
export default Time;
export type TimeProps = {
compact?: boolean;
ts: number;
+ hour24Clock: boolean;
+ dateFormatString: string;
};
+/**
+ * Renders a formatted timestamp, supporting compact and full display modes.
+ *
+ * Displays the time in hour:minute format if the message is from today, yesterday, or if `compact` is true.
+ * For older messages, it shows the date and time.
+ *
+ * @param {number} ts - The timestamp to display.
+ * @param {boolean} [compact=false] - If true, always show only the time.
+ * @param {boolean} hour24Clock - Whether to use 24-hour time format.
+ * @param {string} dateFormatString - Format string for the date part.
+ * @returns {React.ReactElement} A <Text as="time"> element with the formatted date/time.
+ */
export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
- ({ compact, ts, ...props }, ref) => {
+ ({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => {
+ const formattedTime = timeHourMinute(ts, hour24Clock);
+
let time = '';
if (compact) {
- time = timeHourMinute(ts);
+ time = formattedTime;
} else if (today(ts)) {
- time = timeHourMinute(ts);
+ time = formattedTime;
} else if (yesterday(ts)) {
- time = `Yesterday ${timeHourMinute(ts)}`;
+ time = `Yesterday ${formattedTime}`;
} else {
- time = `${timeDayMonYear(ts)} ${timeHourMinute(ts)}`;
+ time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
}
return (
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { useSetting } from '../../state/hooks/settings';
+import { settingsAtom } from '../../state/settings';
export type RoomIntroProps = {
room: Room;
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>
<Text size="T200" priority="300">
{'Created by '}
<b>@{creatorName}</b>
- {` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts)}`}
+ {` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts, hour24Clock)}`}
</Text>
)}
</Box>
import * as css from './styles.css';
import { PickerColumn } from './PickerColumn';
import { hour12to24, hour24to12, hoursToMs, inSameDay, minutesToMs } from '../../utils/time';
+import { useSetting } from '../../state/hooks/settings';
+import { settingsAtom } from '../../state/settings';
type TimePickerProps = {
min: number;
};
export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
({ min, max, value, onChange }, ref) => {
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+
const hour24 = dayjs(value).hour();
- const selectedHour = hour24to12(hour24);
+ const selectedHour = hour24Clock ? hour24 : hour24to12(hour24);
const selectedMinute = dayjs(value).minute();
const selectedPM = hour24 >= 12;
};
const handleHour = (hour: number) => {
- const seconds = hoursToMs(hour12to24(hour, selectedPM));
+ const seconds = hoursToMs(hour24Clock ? hour : hour12to24(hour, selectedPM));
const lastSeconds = hoursToMs(hour24);
const newValue = value + (seconds - lastSeconds);
handleSubmit(newValue);
<Menu className={css.PickerMenu} ref={ref}>
<Box direction="Row" gap="200" className={css.PickerContainer}>
<PickerColumn title="Hour">
- {Array.from(Array(12).keys())
- .map((i) => {
- if (i === 0) return 12;
- return i;
- })
- .map((hour) => (
- <Chip
- key={hour}
- size="500"
- variant={hour === selectedHour ? 'Primary' : 'Background'}
- fill="None"
- radii="300"
- aria-selected={hour === selectedHour}
- onClick={() => handleHour(hour)}
- disabled={
- (minDay && hour12to24(hour, selectedPM) < minHour24) ||
- (maxDay && hour12to24(hour, selectedPM) > maxHour24)
- }
- >
- <Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
- </Chip>
- ))}
+ {hour24Clock
+ ? Array.from(Array(24).keys()).map((hour) => (
+ <Chip
+ key={hour}
+ size="500"
+ variant={hour === selectedHour ? 'Primary' : 'Background'}
+ fill="None"
+ radii="300"
+ aria-selected={hour === selectedHour}
+ onClick={() => handleHour(hour)}
+ disabled={(minDay && hour < minHour24) || (maxDay && hour > maxHour24)}
+ >
+ <Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
+ </Chip>
+ ))
+ : Array.from(Array(12).keys())
+ .map((i) => {
+ if (i === 0) return 12;
+ return i;
+ })
+ .map((hour) => (
+ <Chip
+ key={hour}
+ size="500"
+ variant={hour === selectedHour ? 'Primary' : 'Background'}
+ fill="None"
+ radii="300"
+ aria-selected={hour === selectedHour}
+ onClick={() => handleHour(hour)}
+ disabled={
+ (minDay && hour12to24(hour, selectedPM) < minHour24) ||
+ (maxDay && hour12to24(hour, selectedPM) > maxHour24)
+ }
+ >
+ <Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
+ </Chip>
+ ))}
</PickerColumn>
<PickerColumn title="Minutes">
{Array.from(Array(60).keys()).map((minute) => (
</Chip>
))}
</PickerColumn>
- <PickerColumn title="Period">
- <Chip
- size="500"
- variant={!selectedPM ? 'Primary' : 'SurfaceVariant'}
- fill="None"
- radii="300"
- aria-selected={!selectedPM}
- onClick={() => handlePeriod(false)}
- disabled={minDay && minPM}
- >
- <Text size="T300">AM</Text>
- </Chip>
- <Chip
- size="500"
- variant={selectedPM ? 'Primary' : 'SurfaceVariant'}
- fill="None"
- radii="300"
- aria-selected={selectedPM}
- onClick={() => handlePeriod(true)}
- disabled={maxDay && !maxPM}
- >
- <Text size="T300">PM</Text>
- </Chip>
- </PickerColumn>
+ {!hour24Clock && (
+ <PickerColumn title="Period">
+ <Chip
+ size="500"
+ variant={!selectedPM ? 'Primary' : 'SurfaceVariant'}
+ fill="None"
+ radii="300"
+ aria-selected={!selectedPM}
+ onClick={() => handlePeriod(false)}
+ disabled={minDay && minPM}
+ >
+ <Text size="T300">AM</Text>
+ </Chip>
+ <Chip
+ size="500"
+ variant={selectedPM ? 'Primary' : 'SurfaceVariant'}
+ fill="None"
+ radii="300"
+ aria-selected={selectedPM}
+ onClick={() => handlePeriod(true)}
+ disabled={maxDay && !maxPM}
+ >
+ <Text size="T300">PM</Text>
+ </Chip>
+ </PickerColumn>
+ )}
</Box>
</Menu>
);
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
const searchInputRef = useRef<HTMLInputElement>(null);
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
const [searchParams, setSearchParams] = useSearchParams();
urlPreview={urlPreview}
onOpen={navigateRoom}
legacyUsernameColor={legacyUsernameColor || mDirects.has(groupRoom.roomId)}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
/>
</VirtualTile>
);
urlPreview?: boolean;
onOpen: (roomId: string, eventId: string) => void;
legacyUsernameColor?: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
};
export function SearchResultGroup({
room,
urlPreview,
onOpen,
legacyUsernameColor,
+ hour24Clock,
+ dateFormatString,
}: SearchResultGroupProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
</Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
- <Time ts={event.origin_server_ts} />
+ <Time
+ ts={event.origin_server_ts}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ />
</Box>
<Box shrink="No" gap="200" alignItems="Center">
<Chip
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]);
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
>
{mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
>
<EncryptedContent mEvent={mEvent}>
{() => {
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
>
{mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
const parsed = parseMemberEvent(mEvent);
const timeJSX = (
- <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+ <Time
+ ts={mEvent.getTs()}
+ compact={messageLayout === MessageLayout.Compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ />
);
return (
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
- <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+ <Time
+ ts={mEvent.getTs()}
+ compact={messageLayout === MessageLayout.Compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ />
);
return (
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
- <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+ <Time
+ ts={mEvent.getTs()}
+ compact={messageLayout === MessageLayout.Compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ />
);
return (
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
- <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+ <Time
+ ts={mEvent.getTs()}
+ compact={messageLayout === MessageLayout.Compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ />
);
return (
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
- <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+ <Time
+ ts={mEvent.getTs()}
+ compact={messageLayout === MessageLayout.Compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ />
);
return (
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
- <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+ <Time
+ ts={mEvent.getTs()}
+ compact={messageLayout === MessageLayout.Compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ />
);
return (
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;
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>();
after={<Icon size="50" src={Icons.ChevronBottom} />}
onClick={handleTimePicker}
>
- <Text size="B300">{timeHourMinute(ts)}</Text>
+ <Text size="B300">{timeHourMinute(ts, hour24Clock)}</Text>
</Chip>
<PopOut
anchor={timePickerCords}
powerLevelTag?: PowerLevelTag;
accessibleTagColors?: Map<string, string>;
legacyUsernameColor?: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
};
export const Message = as<'div', MessageProps>(
(
powerLevelTag,
accessibleTagColors,
legacyUsernameColor,
+ hour24Clock,
+ dateFormatString,
children,
...props
},
</Text>
</>
)}
- <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+ <Time
+ ts={mEvent.getTs()}
+ compact={messageLayout === MessageLayout.Compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ />
</Box>
</Box>
);
const theme = useTheme();
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
const [unpinState, unpin] = useAsyncCallback(
useCallback(() => {
const pinEvent = getStateEvent(room, StateEvent.RoomPinnedEvents);
</Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
- <Time ts={pinnedEvent.getTs()} />
+ <Time
+ ts={pinnedEvent.getTs()}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ />
</Box>
{renderOptions()}
</Box>
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 (
}
function DeviceActiveTime({ ts }: { ts: number }) {
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
return (
<Text className={BreakWord} size="T200">
<Text size="Inherit" as="span" priority="300">
<>
{today(ts) && 'Today'}
{yesterday(ts) && 'Yesterday'}
- {!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)}
+ {!today(ts) && !yesterday(ts) && timeDayMonYear(ts, dateFormatString)}{' '}
+ {timeHourMinute(ts, hour24Clock)}
</>
</Text>
);
import React, {
ChangeEventHandler,
+ FormEventHandler,
KeyboardEventHandler,
MouseEventHandler,
+ useEffect,
useState,
} from 'react';
+import dayjs from 'dayjs';
import {
as,
Box,
Button,
Chip,
config,
+ Header,
Icon,
IconButton,
Icons,
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
import { useSetting } from '../../../state/hooks/settings';
-import { MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
+import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
import { SettingTile } from '../../../components/setting-tile';
import { KeySymbol } from '../../../utils/key-symbol';
import { isMacOS } from '../../../utils/user-agent';
import { stopPropagation } from '../../../utils/keyboard';
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
+import { useDateFormatItems } from '../../../hooks/useDateFormat';
import { SequenceCardStyle } from '../styles.css';
type ThemeSelectorProps = {
);
}
+type DateHintProps = {
+ hasChanges: boolean;
+ handleReset: () => void;
+};
+function DateHint({ hasChanges, handleReset }: DateHintProps) {
+ const [anchor, setAnchor] = useState<RectCords>();
+ const categoryPadding = { padding: config.space.S200, paddingTop: 0 };
+
+ const handleOpenMenu: MouseEventHandler<HTMLElement> = (evt) => {
+ setAnchor(evt.currentTarget.getBoundingClientRect());
+ };
+ return (
+ <PopOut
+ anchor={anchor}
+ position="Top"
+ align="End"
+ content={
+ <FocusTrap
+ focusTrapOptions={{
+ initialFocus: false,
+ onDeactivate: () => setAnchor(undefined),
+ clickOutsideDeactivates: true,
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ <Menu style={{ maxHeight: '85vh', overflowY: 'auto' }}>
+ <Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
+ <Text size="L400">Formatting</Text>
+ </Header>
+
+ <Box direction="Column">
+ <Box style={categoryPadding} direction="Column">
+ <Header size="300">
+ <Text size="L400">Year</Text>
+ </Header>
+ <Box direction="Column" tabIndex={0} gap="100">
+ <Text size="T300">
+ YY
+ <Text as="span" size="Inherit" priority="300">
+ {': '}
+ Two-digit year
+ </Text>{' '}
+ </Text>
+ <Text size="T300">
+ YYYY
+ <Text as="span" size="Inherit" priority="300">
+ {': '}Four-digit year
+ </Text>
+ </Text>
+ </Box>
+ </Box>
+
+ <Box style={categoryPadding} direction="Column">
+ <Header size="300">
+ <Text size="L400">Month</Text>
+ </Header>
+ <Box direction="Column" tabIndex={0} gap="100">
+ <Text size="T300">
+ M
+ <Text as="span" size="Inherit" priority="300">
+ {': '}The month
+ </Text>
+ </Text>
+ <Text size="T300">
+ MM
+ <Text as="span" size="Inherit" priority="300">
+ {': '}Two-digit month
+ </Text>{' '}
+ </Text>
+ <Text size="T300">
+ MMM
+ <Text as="span" size="Inherit" priority="300">
+ {': '}Short month name
+ </Text>
+ </Text>
+ <Text size="T300">
+ MMMM
+ <Text as="span" size="Inherit" priority="300">
+ {': '}Full month name
+ </Text>
+ </Text>
+ </Box>
+ </Box>
+
+ <Box style={categoryPadding} direction="Column">
+ <Header size="300">
+ <Text size="L400">Day of the Month</Text>
+ </Header>
+ <Box direction="Column" tabIndex={0} gap="100">
+ <Text size="T300">
+ D
+ <Text as="span" size="Inherit" priority="300">
+ {': '}Day of the month
+ </Text>
+ </Text>
+ <Text size="T300">
+ DD
+ <Text as="span" size="Inherit" priority="300">
+ {': '}Two-digit day of the month
+ </Text>
+ </Text>
+ </Box>
+ </Box>
+ <Box style={categoryPadding} direction="Column">
+ <Header size="300">
+ <Text size="L400">Day of the Week</Text>
+ </Header>
+ <Box direction="Column" tabIndex={0} gap="100">
+ <Text size="T300">
+ d
+ <Text as="span" size="Inherit" priority="300">
+ {': '}Day of the week (Sunday = 0)
+ </Text>
+ </Text>
+ <Text size="T300">
+ dd
+ <Text as="span" size="Inherit" priority="300">
+ {': '}Two-letter day name
+ </Text>
+ </Text>
+ <Text size="T300">
+ ddd
+ <Text as="span" size="Inherit" priority="300">
+ {': '}Short day name
+ </Text>
+ </Text>
+ <Text size="T300">
+ dddd
+ <Text as="span" size="Inherit" priority="300">
+ {': '}Full day name
+ </Text>
+ </Text>
+ </Box>
+ </Box>
+ </Box>
+ </Menu>
+ </FocusTrap>
+ }
+ >
+ {hasChanges ? (
+ <IconButton
+ tabIndex={-1}
+ onClick={handleReset}
+ type="reset"
+ variant="Secondary"
+ size="300"
+ radii="300"
+ >
+ <Icon src={Icons.Cross} size="100" />
+ </IconButton>
+ ) : (
+ <IconButton
+ tabIndex={-1}
+ onClick={handleOpenMenu}
+ type="button"
+ variant="Secondary"
+ size="300"
+ radii="300"
+ aria-pressed={!!anchor}
+ >
+ <Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} />
+ </IconButton>
+ )}
+ </PopOut>
+ );
+}
+
+type CustomDateFormatProps = {
+ value: string;
+ onChange: (format: string) => void;
+};
+function CustomDateFormat({ value, onChange }: CustomDateFormatProps) {
+ const [dateFormatCustom, setDateFormatCustom] = useState(value);
+
+ useEffect(() => {
+ setDateFormatCustom(value);
+ }, [value]);
+
+ const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
+ const format = evt.currentTarget.value;
+ setDateFormatCustom(format);
+ };
+
+ const handleReset = () => {
+ setDateFormatCustom(value);
+ };
+
+ const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+ evt.preventDefault();
+
+ const target = evt.target as HTMLFormElement | undefined;
+ const customDateFormatInput = target?.customDateFormatInput as HTMLInputElement | undefined;
+ const format = customDateFormatInput?.value;
+ if (!format) return;
+
+ onChange(format);
+ };
+
+ const hasChanges = dateFormatCustom !== value;
+ return (
+ <SettingTile>
+ <Box as="form" onSubmit={handleSubmit} gap="200">
+ <Box grow="Yes" direction="Column">
+ <Input
+ required
+ name="customDateFormatInput"
+ value={dateFormatCustom}
+ onChange={handleChange}
+ maxLength={16}
+ autoComplete="off"
+ variant="Secondary"
+ radii="300"
+ style={{ paddingRight: config.space.S200 }}
+ after={<DateHint hasChanges={hasChanges} handleReset={handleReset} />}
+ />
+ </Box>
+ <Button
+ size="400"
+ variant={hasChanges ? 'Success' : 'Secondary'}
+ fill={hasChanges ? 'Solid' : 'Soft'}
+ outlined
+ radii="300"
+ disabled={!hasChanges}
+ type="submit"
+ >
+ <Text size="B400">Save</Text>
+ </Button>
+ </Box>
+ </SettingTile>
+ );
+}
+
+type PresetDateFormatProps = {
+ value: string;
+ onChange: (format: string) => void;
+};
+function PresetDateFormat({ value, onChange }: PresetDateFormatProps) {
+ const [menuCords, setMenuCords] = useState<RectCords>();
+ const dateFormatItems = useDateFormatItems();
+
+ const getDisplayDate = (format: string): string =>
+ format !== '' ? dayjs().format(format) : 'Custom';
+
+ const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+ setMenuCords(evt.currentTarget.getBoundingClientRect());
+ };
+
+ const handleSelect = (format: DateFormat) => {
+ onChange(format);
+ setMenuCords(undefined);
+ };
+
+ return (
+ <>
+ <Button
+ size="300"
+ variant="Secondary"
+ outlined
+ fill="Soft"
+ radii="300"
+ after={<Icon size="300" src={Icons.ChevronBottom} />}
+ onClick={handleMenu}
+ >
+ <Text size="T300">
+ {getDisplayDate(dateFormatItems.find((i) => i.format === value)?.format ?? value)}
+ </Text>
+ </Button>
+ <PopOut
+ anchor={menuCords}
+ offset={5}
+ position="Bottom"
+ align="End"
+ content={
+ <FocusTrap
+ focusTrapOptions={{
+ initialFocus: false,
+ onDeactivate: () => setMenuCords(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) =>
+ evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+ isKeyBackward: (evt: KeyboardEvent) =>
+ evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ <Menu>
+ <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+ {dateFormatItems.map((item) => (
+ <MenuItem
+ key={item.format}
+ size="300"
+ variant={value === item.format ? 'Primary' : 'Surface'}
+ radii="300"
+ onClick={() => handleSelect(item.format)}
+ >
+ <Text size="T300">{getDisplayDate(item.format)}</Text>
+ </MenuItem>
+ ))}
+ </Box>
+ </Menu>
+ </FocusTrap>
+ }
+ />
+ </>
+ );
+}
+
+function SelectDateFormat() {
+ const [dateFormatString, setDateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+ const [selectedDateFormat, setSelectedDateFormat] = useState(dateFormatString);
+ const customDateFormat = selectedDateFormat === '';
+
+ const handlePresetChange = (format: string) => {
+ setSelectedDateFormat(format);
+ if (format !== '') {
+ setDateFormatString(format);
+ }
+ };
+
+ return (
+ <>
+ <SettingTile
+ title="Date Format"
+ description={customDateFormat ? dayjs().format(dateFormatString) : ''}
+ after={<PresetDateFormat value={selectedDateFormat} onChange={handlePresetChange} />}
+ />
+ {customDateFormat && (
+ <CustomDateFormat value={dateFormatString} onChange={setDateFormatString} />
+ )}
+ </>
+ );
+}
+
+function DateAndTime() {
+ const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+
+ return (
+ <Box direction="Column" gap="100">
+ <Text size="L400">Date & Time</Text>
+ <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+ <SettingTile
+ title="24-Hour Time Format"
+ after={<Switch variant="Primary" value={hour24Clock} onChange={setHour24Clock} />}
+ />
+ </SequenceCard>
+
+ <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+ <SelectDateFormat />
+ </SequenceCard>
+ </Box>
+ );
+}
+
function Editor() {
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
<PageContent>
<Box direction="Column" gap="700">
<Appearance />
+ <DateAndTime />
<Editor />
<Messages />
</Box>
--- /dev/null
+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',
+ },
+ ],
+ []
+ );
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
const COMPACT_CARD_WIDTH = 548;
type InviteCardProps = {
invite: InviteData;
compact?: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
onNavigate: NavigateHandler;
hideAvatar: boolean;
};
-function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps) {
+function InviteCard({
+ invite,
+ compact,
+ hour24Clock,
+ dateFormatString,
+ onNavigate,
+ hideAvatar,
+}: InviteCardProps) {
const mx = useMatrixClient();
const userId = mx.getSafeUserId();
</Box>
{invite.inviteTs && (
<Box shrink="No">
- <Time size="T200" ts={invite.inviteTs} priority="300" />
+ <Time
+ size="T200"
+ ts={invite.inviteTs}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ priority="300"
+ />
</Box>
)}
</Box>
invites: InviteData[];
handleNavigate: NavigateHandler;
compact: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
};
-function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) {
+function KnownInvites({
+ invites,
+ handleNavigate,
+ compact,
+ hour24Clock,
+ dateFormatString,
+}: KnownInvitesProps) {
return (
<Box direction="Column" gap="200">
<Text size="H4">Primary</Text>
key={invite.roomId}
invite={invite}
compact={compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
onNavigate={handleNavigate}
hideAvatar={false}
/>
invites: InviteData[];
handleNavigate: NavigateHandler;
compact: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
};
-function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProps) {
+function UnknownInvites({
+ invites,
+ handleNavigate,
+ compact,
+ hour24Clock,
+ dateFormatString,
+}: UnknownInvitesProps) {
const mx = useMatrixClient();
const [declineAllStatus, declineAll] = useAsyncCallback(
key={invite.roomId}
invite={invite}
compact={compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
onNavigate={handleNavigate}
hideAvatar
/>
invites: InviteData[];
handleNavigate: NavigateHandler;
compact: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
};
-function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) {
+function SpamInvites({
+ invites,
+ handleNavigate,
+ compact,
+ hour24Clock,
+ dateFormatString,
+}: SpamInvitesProps) {
const mx = useMatrixClient();
const [showInvites, setShowInvites] = useState(false);
key={invite.roomId}
invite={invite}
compact={compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
onNavigate={handleNavigate}
hideAvatar
/>
);
const screenSize = useScreenSizeContext();
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
const handleNavigate = (roomId: string, space: boolean) => {
if (space) {
navigateSpace(roomId);
<KnownInvites
invites={knownInvites}
compact={compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
handleNavigate={handleNavigate}
/>
)}
<UnknownInvites
invites={unknownInvites}
compact={compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
handleNavigate={handleNavigate}
/>
)}
<SpamInvites
invites={spamInvites}
compact={compact}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
handleNavigate={handleNavigate}
/>
)}
hideActivity: boolean;
onOpen: (roomId: string, eventId: string) => void;
legacyUsernameColor?: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
};
function RoomNotificationsGroupComp({
room,
hideActivity,
onOpen,
legacyUsernameColor,
+ hour24Clock,
+ dateFormatString,
}: RoomNotificationsGroupProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
</Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
- <Time ts={event.origin_server_ts} />
+ <Time
+ ts={event.origin_server_ts}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ />
</Box>
<Box shrink="No" gap="200" alignItems="Center">
<Chip
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const screenSize = useScreenSizeContext();
const mDirects = useAtomValue(mDirectAtom);
legacyUsernameColor={
legacyUsernameColor || mDirects.has(groupRoom.roomId)
}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
/>
</VirtualTile>
);
import { atom } from 'jotai';
const STORAGE_KEY = 'settings';
+export type DateFormat = 'D MMM YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD' | '';
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
export enum MessageLayout {
Modern = 0,
showNotifications: boolean;
isNotificationSounds: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
+
developerTools: boolean;
}
showNotifications: true,
isNotificationSounds: true,
+ hour24Clock: false,
+ dateFormatString: 'D MMM YYYY',
+
developerTools: false,
};
export const yesterday = (ts: number): boolean => dayjs(ts).isYesterday();
-export const timeHour = (ts: number): string => dayjs(ts).format('hh');
+export const timeHour = (ts: number, hour24Clock: boolean): string =>
+ dayjs(ts).format(hour24Clock ? 'HH' : 'hh');
export const timeMinute = (ts: number): string => dayjs(ts).format('mm');
export const timeAmPm = (ts: number): string => dayjs(ts).format('A');
export const timeDay = (ts: number): string => dayjs(ts).format('D');
export const timeMonth = (ts: number): string => dayjs(ts).format('MMMM');
export const timeYear = (ts: number): string => dayjs(ts).format('YYYY');
-export const timeHourMinute = (ts: number): string => dayjs(ts).format('hh:mm A');
+export const timeHourMinute = (ts: number, hour24Clock: boolean): string =>
+ dayjs(ts).format(hour24Clock ? 'HH:mm' : 'hh:mm A');
-export const timeDayMonYear = (ts: number): string => dayjs(ts).format('D MMM YYYY');
+export const timeDayMonYear = (ts: number, dateFormatString: string): string =>
+ dayjs(ts).format(dateFormatString);
export const timeDayMonthYear = (ts: number): string => dayjs(ts).format('D MMMM YYYY');