Add setting to enable 24-hour time format

This commit is contained in:
Gimle Larpes 2025-05-28 21:17:38 +02:00
parent a23279e633
commit 8b4524607b
14 changed files with 114 additions and 30 deletions

View file

@ -4,10 +4,13 @@ import PropTypes from 'prop-types';
import dateFormat from 'dateformat';
import { isInSameDay } from '../../../util/common';
function Time({ timestamp, fullTime }) {
function Time({ timestamp, fullTime, hour24Clock }) {
const date = new Date(timestamp);
const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
const formattedFullTime = dateFormat(
date,
hour24Clock ? 'dd mmmm yyyy, HH:MM' : 'dd mmmm yyyy, hh:MM TT'
);
let formattedDate = formattedFullTime;
if (!fullTime) {
@ -16,17 +19,16 @@ function Time({ timestamp, fullTime }) {
compareDate.setDate(compareDate.getDate() - 1);
const isYesterday = isInSameDay(date, compareDate);
formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy');
const timeFormat = hour24Clock ? 'HH:MM' : 'hh:MM TT';
formattedDate = dateFormat(date, isToday || isYesterday ? timeFormat : 'dd/mm/yyyy');
if (isYesterday) {
formattedDate = `Yesterday, ${formattedDate}`;
}
}
return (
<time
dateTime={date.toISOString()}
title={formattedFullTime}
>
<time dateTime={date.toISOString()} title={formattedFullTime}>
{formattedDate}
</time>
);
@ -39,6 +41,7 @@ Time.defaultProps = {
Time.propTypes = {
timestamp: PropTypes.number.isRequired,
fullTime: PropTypes.bool,
hour24Clock: PropTypes.bool.isRequired,
};
export default Time;

View file

@ -8,16 +8,18 @@ export type TimeProps = {
};
export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
({ compact, ts, ...props }, ref) => {
({ compact, hour24Clock, 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)} ${formattedTime}`;
}
return (

View file

@ -15,6 +15,8 @@ import { nameInitials } from '../../utils/common';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
export type RoomIntroProps = {
room: Room;
@ -43,6 +45,8 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
useCallback(async (roomId: string) => mx.joinRoom(roomId), [mx])
);
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
return (
<Box direction="Column" grow="Yes" gap="500" {...props} ref={ref}>
<Box>
@ -67,7 +71,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
<Text size="T200" priority="300">
{'Created by '}
<b>@{creatorName}</b>
{` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts)}`}
{` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts, hour24Clock)}`}
</Text>
)}
</Box>

View file

@ -57,6 +57,8 @@ export function MessageSearch({
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const searchInputRef = useRef<HTMLInputElement>(null);
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
const [searchParams, setSearchParams] = useSearchParams();
@ -289,6 +291,7 @@ export function MessageSearch({
urlPreview={urlPreview}
onOpen={navigateRoom}
legacyUsernameColor={legacyUsernameColor || mDirects.has(groupRoom.roomId)}
hour24Clock={hour24Clock}
/>
</VirtualTile>
);

View file

@ -57,6 +57,7 @@ type SearchResultGroupProps = {
urlPreview?: boolean;
onOpen: (roomId: string, eventId: string) => void;
legacyUsernameColor?: boolean;
hour24Clock: boolean;
};
export function SearchResultGroup({
room,
@ -66,6 +67,7 @@ export function SearchResultGroup({
urlPreview,
onOpen,
legacyUsernameColor,
hour24Clock,
}: SearchResultGroupProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
@ -275,7 +277,7 @@ export function SearchResultGroup({
</Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
<Time ts={event.origin_server_ts} />
<Time ts={event.origin_server_ts} hour24Clock={hour24Clock} />
</Box>
<Box shrink="No" gap="200" alignItems="Center">
<Chip

View file

@ -448,6 +448,7 @@ export function RoomTimeline({
const [encUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
const showUrlPreview = room.hasEncryptionStateEvent() ? encUrlPreview : urlPreview;
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const ignoredUsersList = useIgnoredUsers();
const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]);
@ -1068,6 +1069,7 @@ export function RoomTimeline({
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
>
{mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
@ -1149,6 +1151,7 @@ export function RoomTimeline({
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
>
<EncryptedContent mEvent={mEvent}>
{() => {
@ -1250,6 +1253,7 @@ export function RoomTimeline({
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
>
{mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
@ -1278,7 +1282,11 @@ export function RoomTimeline({
const parsed = parseMemberEvent(mEvent);
const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
<Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
/>
);
return (
@ -1314,7 +1322,11 @@ export function RoomTimeline({
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
<Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
/>
);
return (
@ -1351,7 +1363,11 @@ export function RoomTimeline({
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
<Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
/>
);
return (
@ -1388,7 +1404,11 @@ export function RoomTimeline({
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
<Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
/>
);
return (
@ -1427,7 +1447,11 @@ export function RoomTimeline({
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
<Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
/>
);
return (
@ -1471,7 +1495,11 @@ export function RoomTimeline({
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
<Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
/>
);
return (

View file

@ -678,6 +678,7 @@ export type MessageProps = {
powerLevelTag?: PowerLevelTag;
accessibleTagColors?: Map<string, string>;
legacyUsernameColor?: boolean;
hour24Clock: boolean;
};
export const Message = as<'div', MessageProps>(
(
@ -706,6 +707,7 @@ export const Message = as<'div', MessageProps>(
powerLevelTag,
accessibleTagColors,
legacyUsernameColor,
hour24Clock,
children,
...props
},
@ -770,7 +772,11 @@ export const Message = as<'div', MessageProps>(
</Text>
</>
)}
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
<Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
/>
</Box>
</Box>
);

View file

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

View file

@ -27,6 +27,8 @@ import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { LogoutDialog } from '../../../components/LogoutDialog';
import { stopPropagation } from '../../../utils/keyboard';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
export function DeviceTilePlaceholder() {
return (
@ -41,6 +43,8 @@ export function DeviceTilePlaceholder() {
}
function DeviceActiveTime({ ts }: { ts: number }) {
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
return (
<Text className={BreakWord} size="T200">
<Text size="Inherit" as="span" priority="300">
@ -49,7 +53,7 @@ function DeviceActiveTime({ ts }: { ts: number }) {
<>
{today(ts) && 'Today'}
{yesterday(ts) && 'Yesterday'}
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)}
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts, hour24Clock)}
</>
</Text>
);

View file

@ -530,6 +530,7 @@ function Messages() {
const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview');
const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock');
return (
<Box direction="Column" gap="100">
@ -608,6 +609,12 @@ function Messages() {
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="24-Hour Time Format"
after={<Switch variant="Primary" value={hour24Clock} onChange={setHour24Clock} />}
/>
</SequenceCard>
</Box>
);
}

View file

@ -65,6 +65,8 @@ import { testBadWords } from '../../../plugins/bad-words';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
const COMPACT_CARD_WIDTH = 548;
@ -135,10 +137,11 @@ type NavigateHandler = (roomId: string, space: boolean) => void;
type InviteCardProps = {
invite: InviteData;
compact?: boolean;
hour24Clock: boolean;
onNavigate: NavigateHandler;
hideAvatar: boolean;
};
function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps) {
function InviteCard({ invite, compact, hour24Clock, onNavigate, hideAvatar }: InviteCardProps) {
const mx = useMatrixClient();
const userId = mx.getSafeUserId();
@ -295,7 +298,7 @@ function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps
</Box>
{invite.inviteTs && (
<Box shrink="No">
<Time size="T200" ts={invite.inviteTs} priority="300" />
<Time size="T200" ts={invite.inviteTs} hour24Clock={hour24Clock} priority="300" />
</Box>
)}
</Box>
@ -384,8 +387,9 @@ type KnownInvitesProps = {
invites: InviteData[];
handleNavigate: NavigateHandler;
compact: boolean;
hour24Clock: boolean;
};
function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) {
function KnownInvites({ invites, handleNavigate, compact, hour24Clock }: KnownInvitesProps) {
return (
<Box direction="Column" gap="200">
<Text size="H4">Primary</Text>
@ -396,6 +400,7 @@ function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) {
key={invite.roomId}
invite={invite}
compact={compact}
hour24Clock={hour24Clock}
onNavigate={handleNavigate}
hideAvatar={false}
/>
@ -420,8 +425,9 @@ type UnknownInvitesProps = {
invites: InviteData[];
handleNavigate: NavigateHandler;
compact: boolean;
hour24Clock: boolean;
};
function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProps) {
function UnknownInvites({ invites, handleNavigate, compact, hour24Clock }: UnknownInvitesProps) {
const mx = useMatrixClient();
const [declineAllStatus, declineAll] = useAsyncCallback(
@ -459,6 +465,7 @@ function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProp
key={invite.roomId}
invite={invite}
compact={compact}
hour24Clock={hour24Clock}
onNavigate={handleNavigate}
hideAvatar
/>
@ -483,8 +490,9 @@ type SpamInvitesProps = {
invites: InviteData[];
handleNavigate: NavigateHandler;
compact: boolean;
hour24Clock: boolean;
};
function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) {
function SpamInvites({ invites, handleNavigate, compact, hour24Clock }: SpamInvitesProps) {
const mx = useMatrixClient();
const [showInvites, setShowInvites] = useState(false);
@ -608,6 +616,7 @@ function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) {
key={invite.roomId}
invite={invite}
compact={compact}
hour24Clock={hour24Clock}
onNavigate={handleNavigate}
hideAvatar
/>
@ -671,6 +680,8 @@ export function Invites() {
);
const screenSize = useScreenSizeContext();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const handleNavigate = (roomId: string, space: boolean) => {
if (space) {
navigateSpace(roomId);
@ -723,6 +734,7 @@ export function Invites() {
<KnownInvites
invites={knownInvites}
compact={compact}
hour24Clock={hour24Clock}
handleNavigate={handleNavigate}
/>
)}
@ -731,6 +743,7 @@ export function Invites() {
<UnknownInvites
invites={unknownInvites}
compact={compact}
hour24Clock={hour24Clock}
handleNavigate={handleNavigate}
/>
)}
@ -739,6 +752,7 @@ export function Invites() {
<SpamInvites
invites={spamInvites}
compact={compact}
hour24Clock={hour24Clock}
handleNavigate={handleNavigate}
/>
)}

View file

@ -205,6 +205,7 @@ type RoomNotificationsGroupProps = {
hideActivity: boolean;
onOpen: (roomId: string, eventId: string) => void;
legacyUsernameColor?: boolean;
hour24Clock: boolean;
};
function RoomNotificationsGroupComp({
room,
@ -214,6 +215,7 @@ function RoomNotificationsGroupComp({
hideActivity,
onOpen,
legacyUsernameColor,
hour24Clock,
}: RoomNotificationsGroupProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
@ -496,7 +498,7 @@ function RoomNotificationsGroupComp({
</Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
<Time ts={event.origin_server_ts} />
<Time ts={event.origin_server_ts} hour24Clock={hour24Clock} />
</Box>
<Box shrink="No" gap="200" alignItems="Center">
<Chip
@ -549,6 +551,7 @@ export function Notifications() {
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const screenSize = useScreenSizeContext();
const mDirects = useAtomValue(mDirectAtom);
@ -713,6 +716,7 @@ export function Notifications() {
legacyUsernameColor={
legacyUsernameColor || mDirects.has(groupRoom.roomId)
}
hour24Clock={hour24Clock}
/>
</VirtualTile>
);

View file

@ -35,6 +35,8 @@ export interface Settings {
showNotifications: boolean;
isNotificationSounds: boolean;
hour24Clock: boolean;
developerTools: boolean;
}
@ -65,6 +67,8 @@ const defaultSettings: Settings = {
showNotifications: true,
isNotificationSounds: true,
hour24Clock: 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 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');