refactor(time): one timestamp formatter honouring the clock/date settings (#139)
CI / Build & Quality Checks (push) Successful in 1m30s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 6s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
CI / Build & Quality Checks (push) Successful in 1m30s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 6s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
Audit of every rendered time found five families of ad-hoc formatting: the shared Time component + copies of its today/yesterday branch (forwarded header, thread summary, read receipts, device tile, moderation alerts, edit history), locale-default toLocale*String calls that ignored the user's 12/24 h and date-format settings (scheduled tray, reminders, schedule preview, notification snooze, bookmarks, threads list, search cache line, room insights, media gallery), a hard-coded en-US date in the activity log, and three relative-age variants. utils/formatTimestamp.ts now holds the rules — today → time; yesterday / tomorrow → day word + time; last 6 days → weekday + time; older → date + time in dateFormatString — plus autoDate / time / date / dateTime styles, formatDayDivider (full weekday), formatShortAge (room list) and formatRelativeAge (list rows). useTimestampFormatter binds them to the settings. 11 unit tests with an injected 'now'. Visible changes are limited to consistency: 12 h times keep the existing zero-padded hh:mm A; the a11y label and Created-by line use the user's date format instead of a fixed long month. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -24,32 +24,19 @@ import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import { getMouseEventCords } from '../../utils/dom';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { today, yesterday, timeHourMinute, timeMon, timeDay, timeYear } from '../../utils/time';
|
||||
import { TimestampPrefs, formatTimestamp } from '../../utils/formatTimestamp';
|
||||
import { useTimestampFormatter } from '../../hooks/useTimestampFormatter';
|
||||
|
||||
function formatReadTs(ts: number, hour24Clock: boolean): string {
|
||||
const timeStr = timeHourMinute(ts, hour24Clock);
|
||||
if (today(ts)) return `Today at ${timeStr}`;
|
||||
if (yesterday(ts)) return `Yesterday at ${timeStr}`;
|
||||
const sameYear = timeYear(ts) === timeYear(Date.now());
|
||||
return sameYear
|
||||
? `${timeMon(ts)} ${timeDay(ts)} at ${timeStr}`
|
||||
: `${timeMon(ts)} ${timeDay(ts)} ${timeYear(ts)} at ${timeStr}`;
|
||||
}
|
||||
const formatReadTs = (ts: number, prefs: TimestampPrefs): string => formatTimestamp(ts, prefs);
|
||||
|
||||
type EventReaderItemProps = {
|
||||
room: Room;
|
||||
readerId: string;
|
||||
hour24Clock: boolean;
|
||||
prefs: TimestampPrefs;
|
||||
lotusTerminal: boolean;
|
||||
onSelect: React.MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
function EventReaderItem({
|
||||
room,
|
||||
readerId,
|
||||
hour24Clock,
|
||||
lotusTerminal,
|
||||
onSelect,
|
||||
}: EventReaderItemProps) {
|
||||
function EventReaderItem({ room, readerId, prefs, lotusTerminal, onSelect }: EventReaderItemProps) {
|
||||
const { name, avatarUrl } = useMemberAvatar(room, readerId, 100, 100);
|
||||
const receiptTs = room.getReadReceiptForUserId(readerId)?.data.ts;
|
||||
|
||||
@@ -86,7 +73,7 @@ function EventReaderItem({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{formatReadTs(receiptTs, hour24Clock)}
|
||||
{formatReadTs(receiptTs, prefs)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
@@ -106,7 +93,7 @@ export const EventReaders = as<'div', EventReadersProps>(
|
||||
const latestEventReaders = useRoomEventReaders(room, eventId).filter((id) => id !== myUserId);
|
||||
const openProfile = useOpenUserRoomProfile();
|
||||
const space = useSpaceOptionally();
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const { prefs } = useTimestampFormatter();
|
||||
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
||||
|
||||
return (
|
||||
@@ -157,7 +144,7 @@ export const EventReaders = as<'div', EventReadersProps>(
|
||||
key={readerId}
|
||||
room={room}
|
||||
readerId={readerId}
|
||||
hour24Clock={hour24Clock}
|
||||
prefs={prefs}
|
||||
lotusTerminal={lotusTerminal}
|
||||
onSelect={(event) => {
|
||||
openProfile(
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as css from './Reply.css';
|
||||
import { ForwardedMeta } from '../../features/room/message/forwardContent';
|
||||
import { getMemberDisplayName } from '../../utils/room';
|
||||
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/time';
|
||||
import { formatTimestamp } from '../../utils/formatTimestamp';
|
||||
|
||||
type ForwardedHeaderProps = {
|
||||
mx: MatrixClient;
|
||||
@@ -32,11 +32,7 @@ export const ForwardedHeader = as<'div', ForwardedHeaderProps>(
|
||||
getMxIdLocalPart(meta.sender) ??
|
||||
meta.sender;
|
||||
const ts = meta.origin_server_ts;
|
||||
const when = today(ts)
|
||||
? timeHourMinute(ts, hour24Clock)
|
||||
: yesterday(ts)
|
||||
? `Yesterday ${timeHourMinute(ts, hour24Clock)}`
|
||||
: `${timeDayMonYear(ts, dateFormatString)} ${timeHourMinute(ts, hour24Clock)}`;
|
||||
const when = formatTimestamp(ts, { hour24Clock, dateFormatString });
|
||||
const canJump = !!sourceRoom && !!onJump;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { Text, as } from 'folds';
|
||||
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/time';
|
||||
import { formatTimestamp } from '../../utils/formatTimestamp';
|
||||
|
||||
export type TimeProps = {
|
||||
compact?: boolean;
|
||||
@@ -12,8 +12,8 @@ export type TimeProps = {
|
||||
/**
|
||||
* 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.
|
||||
* `compact` always shows the clock time; otherwise the shared `formatTimestamp`
|
||||
* rules apply (today → time, yesterday/this week → day word + time, else date + time).
|
||||
*
|
||||
* @param {number} ts - The timestamp to display.
|
||||
* @param {boolean} [compact=false] - If true, always show only the time.
|
||||
@@ -23,18 +23,7 @@ export type TimeProps = {
|
||||
*/
|
||||
export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
|
||||
({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => {
|
||||
const formattedTime = timeHourMinute(ts, hour24Clock);
|
||||
|
||||
let time = '';
|
||||
if (compact) {
|
||||
time = formattedTime;
|
||||
} else if (today(ts)) {
|
||||
time = formattedTime;
|
||||
} else if (yesterday(ts)) {
|
||||
time = `Yesterday ${formattedTime}`;
|
||||
} else {
|
||||
time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
|
||||
}
|
||||
const time = formatTimestamp(ts, { hour24Clock, dateFormatString }, compact ? 'time' : 'auto');
|
||||
|
||||
return (
|
||||
<Text as="time" style={{ flexShrink: 0 }} size="T200" priority="300" {...props} ref={ref}>
|
||||
|
||||
@@ -19,15 +19,13 @@ import { getMemberDisplayName, getStateEvent } from '../../utils/room';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { timeDayMonthYear, timeHourMinute } from '../../utils/time';
|
||||
import { useTimestampFormatter } from '../../hooks/useTimestampFormatter';
|
||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
import { RoomAvatar } from '../room-avatar';
|
||||
import { nameInitials } from '../../utils/common';
|
||||
import { useRoomAvatar, useLocalRoomName, 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';
|
||||
import { InviteUserPrompt } from '../invite-user-prompt';
|
||||
import { RoomTopicViewer } from '../room-topic-viewer';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
@@ -68,7 +66,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
|
||||
useCallback(async (roomId: string) => mx.joinRoom(roomId), [mx]),
|
||||
);
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const { format } = useTimestampFormatter();
|
||||
|
||||
return (
|
||||
<Box direction="Column" grow="Yes" gap="500" {...props} ref={ref}>
|
||||
@@ -135,7 +133,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, hour24Clock)}`}
|
||||
{` on ${format(ts, 'dateTime')}`}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -6,9 +6,7 @@ import { SettingTile } from '../setting-tile';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { BreakWord } from '../../styles/Text.css';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { timeDayMonYear, timeHourMinute } from '../../utils/time';
|
||||
import { useTimestampFormatter } from '../../hooks/useTimestampFormatter';
|
||||
|
||||
type UserKickAlertProps = {
|
||||
reason?: string;
|
||||
@@ -16,11 +14,8 @@ type UserKickAlertProps = {
|
||||
ts?: number;
|
||||
};
|
||||
export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
|
||||
const { format } = useTimestampFormatter();
|
||||
const when = ts ? format(ts) : undefined;
|
||||
|
||||
return (
|
||||
<CutoutCard style={{ padding: config.space.S200 }} variant="Critical">
|
||||
@@ -28,11 +23,7 @@ export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
|
||||
<Box direction="Column" gap="200">
|
||||
<Box gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="L400">Kicked User</Text>
|
||||
{time && date && (
|
||||
<Text size="T200">
|
||||
{date} {time}
|
||||
</Text>
|
||||
)}
|
||||
{when && <Text size="T200">{when}</Text>}
|
||||
</Box>
|
||||
<Box direction="Column">
|
||||
{kickedBy && (
|
||||
@@ -66,11 +57,8 @@ type UserBanAlertProps = {
|
||||
export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBanAlertProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
|
||||
const { format } = useTimestampFormatter();
|
||||
const when = ts ? format(ts) : undefined;
|
||||
|
||||
const [unbanState, unban] = useAsyncCallback<undefined, Error, []>(
|
||||
useCallback(async () => {
|
||||
@@ -86,11 +74,7 @@ export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBan
|
||||
<Box direction="Column" gap="200">
|
||||
<Box gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="L400">Banned User</Text>
|
||||
{time && date && (
|
||||
<Text size="T200">
|
||||
{date} {time}
|
||||
</Text>
|
||||
)}
|
||||
{when && <Text size="T200">{when}</Text>}
|
||||
</Box>
|
||||
<Box direction="Column">
|
||||
{bannedBy && (
|
||||
@@ -141,11 +125,8 @@ type UserInviteAlertProps = {
|
||||
export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: UserInviteAlertProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
|
||||
const { format } = useTimestampFormatter();
|
||||
const when = ts ? format(ts) : undefined;
|
||||
|
||||
const [kickState, kick] = useAsyncCallback<undefined, Error, []>(
|
||||
useCallback(async () => {
|
||||
@@ -161,11 +142,7 @@ export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: User
|
||||
<Box direction="Column" gap="200">
|
||||
<Box gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="L400">Invited User</Text>
|
||||
{time && date && (
|
||||
<Text size="T200">
|
||||
{date} {time}
|
||||
</Text>
|
||||
)}
|
||||
{when && <Text size="T200">{when}</Text>}
|
||||
</Box>
|
||||
<Box direction="Column">
|
||||
{invitedBy && (
|
||||
|
||||
Reference in New Issue
Block a user