Merge branch 'dev' into improve-space

This commit is contained in:
Gimle Larpes 2025-07-27 17:34:34 +03:00 committed by GitHub
commit 71e775fed1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 896 additions and 264 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

@ -203,12 +203,8 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Menu <Menu style={{ padding: config.space.S200 }}>
style={{ <Box direction="Column" gap="200">
padding: config.space.S100,
}}
>
<Box direction="Column" gap="100">
<Box direction="Column" gap="200"> <Box direction="Column" gap="200">
<InfoCard <InfoCard
variant="SurfaceVariant" variant="SurfaceVariant"

View file

@ -30,7 +30,7 @@ export function HexColorPickerPopOut({ picker, onRemove, children }: HexColorPic
> >
<Menu <Menu
style={{ style={{
padding: config.space.S100, padding: config.space.S200,
borderRadius: config.radii.R500, borderRadius: config.radii.R500,
overflow: 'initial', overflow: 'initial',
}} }}

View file

@ -109,7 +109,7 @@ export function JoinRulesSwitcher<T extends ExtendedJoinRules[]>({
}} }}
> >
<Menu> <Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{rules.map((rule) => ( {rules.map((rule) => (
<MenuItem <MenuItem
key={rule} key={rule}

View file

@ -78,7 +78,7 @@ export function ManualVerificationMethodSwitcher({
}} }}
> >
<Menu> <Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
size="300" size="300"
variant="Surface" variant="Surface"

View file

@ -23,7 +23,7 @@ export function MemberSortMenu({ selected, onSelect, requestClose }: MemberSortM
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Menu style={{ padding: config.space.S100 }}> <Menu style={{ padding: config.space.S200 }}>
{memberSortMenu.map((menuItem, index) => ( {memberSortMenu.map((menuItem, index) => (
<MenuItem <MenuItem
key={menuItem.name} key={menuItem.name}

View file

@ -27,7 +27,7 @@ export function MembershipFilterMenu({
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Menu style={{ padding: config.space.S100 }}> <Menu style={{ padding: config.space.S200 }}>
{membershipFilterMenu.map((menuItem, index) => ( {membershipFilterMenu.map((menuItem, index) => (
<MenuItem <MenuItem
key={menuItem.name} key={menuItem.name}

View file

@ -70,7 +70,7 @@ export function RoomNotificationModeSwitcher({
return ( return (
<PopOut <PopOut
anchor={menuCords} anchor={menuCords}
offset={5} offset={8}
position="Right" position="Right"
align="Start" align="Start"
content={ content={
@ -86,7 +86,7 @@ export function RoomNotificationModeSwitcher({
}} }}
> >
<Menu> <Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{modes.map((mode) => ( {modes.map((mode) => (
<MenuItem <MenuItem
key={mode} key={mode}

View file

@ -157,12 +157,12 @@ export function RenderElement({ attributes, element, children }: RenderElementPr
<Text as="pre" className={css.CodeBlock} {...attributes}> <Text as="pre" className={css.CodeBlock} {...attributes}>
<Scroll <Scroll
direction="Horizontal" direction="Horizontal"
variant="Secondary" variant="SurfaceVariant"
size="300" size="300"
visibility="Hover" visibility="Hover"
hideTrack hideTrack
> >
<div className={css.CodeBlockInternal()}>{children}</div> <div className={css.CodeBlockInternal}>{children}</div>
</Scroll> </Scroll>
</Text> </Text>
); );

View file

@ -155,7 +155,7 @@ export function HeadingBlockButton() {
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Menu style={{ padding: config.space.S100 }}> <Menu style={{ padding: config.space.S200 }}>
<Box gap="100"> <Box gap="100">
<TooltipProvider <TooltipProvider
tooltip={<BtnTooltip text="Heading 1" shortCode={`${modKey} + 1`} />} tooltip={<BtnTooltip text="Heading 1" shortCode={`${modKey} + 1`} />}

View file

@ -33,7 +33,7 @@ export function UsageSelector({ selected, onChange }: UsageSelectorProps) {
); );
return ( return (
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{allUsages.map((usage) => ( {allUsages.map((usage) => (
<MenuItem <MenuItem
key={getUsageStr(usage)} key={getUsageStr(usage)}

View file

@ -7,7 +7,6 @@ export const ReplyBend = style({
export const ThreadIndicator = style({ export const ThreadIndicator = style({
opacity: config.opacity.P300, opacity: config.opacity.P300,
gap: toRem(2),
selectors: { selectors: {
'button&': { 'button&': {
@ -19,11 +18,6 @@ export const ThreadIndicator = style({
}, },
}); });
export const ThreadIndicatorIcon = style({
width: toRem(14),
height: toRem(14),
});
export const Reply = style({ export const Reply = style({
marginBottom: toRem(1), marginBottom: toRem(1),
minWidth: 0, minWidth: 0,

View file

@ -38,9 +38,16 @@ export const ReplyLayout = as<'div', ReplyLayoutProps>(
); );
export const ThreadIndicator = as<'div'>(({ ...props }, ref) => ( export const ThreadIndicator = as<'div'>(({ ...props }, ref) => (
<Box className={css.ThreadIndicator} alignItems="Center" {...props} ref={ref}> <Box
<Icon className={css.ThreadIndicatorIcon} src={Icons.Message} /> shrink="No"
<Text size="T200">Threaded reply</Text> className={css.ThreadIndicator}
alignItems="Center"
gap="100"
{...props}
ref={ref}
>
<Icon size="50" src={Icons.Thread} />
<Text size="L400">Thread</Text>
</Box> </Box>
)); ));
@ -97,7 +104,7 @@ export const Reply = as<'div', ReplyProps>(
const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody; const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody;
return ( return (
<Box direction="Column" alignItems="Start" {...props} ref={ref}> <Box direction="Row" gap="200" alignItems="Center" {...props} ref={ref}>
{threadRootId && ( {threadRootId && (
<ThreadIndicator as="button" data-event-id={threadRootId} onClick={onClick} /> <ThreadIndicator as="button" data-event-id={threadRootId} onClick={onClick} />
)} )}

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

@ -16,13 +16,14 @@ export const PowerSelector = forwardRef<HTMLDivElement, PowerSelectorProps>(
ref={ref} ref={ref}
style={{ style={{
maxHeight: '75vh', maxHeight: '75vh',
maxWidth: toRem(300), maxWidth: toRem(200),
width: '100vw',
display: 'flex', display: 'flex',
}} }}
> >
<Box grow="Yes"> <Box grow="Yes">
<Scroll size="0" hideTrack visibility="Hover"> <Scroll size="0" hideTrack visibility="Hover">
<div style={{ padding: config.space.S100 }}> <div style={{ padding: config.space.S200 }}>
{getPowers(powerLevelTags).map((power) => { {getPowers(powerLevelTags).map((power) => {
const selected = value === power; const selected = value === power;
const tag = powerLevelTags[power]; const tag = powerLevelTags[power];

View file

@ -273,7 +273,7 @@ export const RoomCard = as<'div', RoomCardProps>(
variant="Secondary" variant="Secondary"
size="300" size="300"
disabled={joining} disabled={joining}
before={joining && <Spinner size="50" variant="Secondary" fill="Soft" />} before={joining && <Spinner size="50" variant="Secondary" fill="Solid" />}
> >
<Text size="B300" truncate> <Text size="B300" truncate>
{joining ? 'Joining' : 'Join'} {joining ? 'Joining' : 'Join'}

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

@ -119,7 +119,7 @@ export function RoomHistoryVisibility({ powerLevels }: RoomHistoryVisibilityProp
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Menu style={{ padding: config.space.S100 }}> <Menu style={{ padding: config.space.S200 }}>
{visibilityMenu.map((visibility) => ( {visibilityMenu.map((visibility) => (
<MenuItem <MenuItem
key={visibility} key={visibility}

View file

@ -63,7 +63,7 @@ function PeekPermissions({ powerLevels, power, permissionGroups, children }: Pee
> >
<Box grow="Yes" tabIndex={0}> <Box grow="Yes" tabIndex={0}>
<Scroll size="0" hideTrack visibility="Hover"> <Scroll size="0" hideTrack visibility="Hover">
<Box style={{ padding: config.space.S200 }} direction="Column" gap="400"> <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
{permissionGroups.map((group, groupIndex) => ( {permissionGroups.map((group, groupIndex) => (
<Box key={groupIndex} direction="Column" gap="100"> <Box key={groupIndex} direction="Column" gap="100">
<Text size="L400">{group.name}</Text> <Text size="L400">{group.name}</Text>

View file

@ -234,9 +234,9 @@ export function HierarchyItemMenu({
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Menu style={{ maxWidth: toRem(150), width: '100vw' }}> <Menu style={{ minWidth: toRem(200) }}>
{joined && ( {joined && (
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{onTogglePin && ( {onTogglePin && (
<MenuItem <MenuItem
size="300" size="300"
@ -296,7 +296,7 @@ export function HierarchyItemMenu({
<Line size="300" variant="Surface" direction="Horizontal" /> <Line size="300" variant="Surface" direction="Horizontal" />
)} )}
{canEditChild && ( {canEditChild && (
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<SuggestMenuItem item={item} requestClose={handleRequestClose} /> <SuggestMenuItem item={item} requestClose={handleRequestClose} />
<RemoveMenuItem item={item} requestClose={handleRequestClose} /> <RemoveMenuItem item={item} requestClose={handleRequestClose} />
</Box> </Box>

View file

@ -60,8 +60,8 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
}; };
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ minWidth: toRem(200) }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleInvite} onClick={handleInvite}
variant="Primary" variant="Primary"
@ -87,7 +87,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
</MenuItem> </MenuItem>
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<UseStateProvider initial={false}> <UseStateProvider initial={false}>
{(promptLeave, setPromptLeave) => ( {(promptLeave, setPromptLeave) => (
<> <>

View file

@ -274,7 +274,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Menu style={{ padding: config.space.S100 }}> <Menu style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
size="300" size="300"
radii="300" radii="300"
@ -336,7 +336,7 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Menu style={{ padding: config.space.S100 }}> <Menu style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
size="300" size="300"
radii="300" radii="300"

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

@ -74,8 +74,7 @@ function OrderButton({ order, onChange }: OrderButtonProps) {
<Header size="300" variant="Surface" style={{ padding: `0 ${config.space.S300}` }}> <Header size="300" variant="Surface" style={{ padding: `0 ${config.space.S300}` }}>
<Text size="L400">Sort by</Text> <Text size="L400">Sort by</Text>
</Header> </Header>
<Line variant="Surface" size="300" /> <div style={{ padding: config.space.S200, paddingTop: 0 }}>
<div style={{ padding: config.space.S100 }}>
<MenuItem <MenuItem
onClick={() => setOrder()} onClick={() => setOrder()}
variant="Surface" variant="Surface"
@ -291,7 +290,7 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
</Box> </Box>
</Scroll> </Scroll>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box shrink="No" direction="Column" gap="100" style={{ padding: config.space.S200 }}> <Box shrink="No" direction="Column" gap="200" style={{ padding: config.space.S200 }}>
<Button size="300" variant="Secondary" radii="300" onClick={handleSave}> <Button size="300" variant="Secondary" radii="300" onClick={handleSave}>
{localSelected && localSelected.length > 0 ? ( {localSelected && localSelected.length > 0 ? (
<Text size="B300">Save ({localSelected.length})</Text> <Text size="B300">Save ({localSelected.length})</Text>

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

@ -89,8 +89,8 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
}; };
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ minWidth: toRem(200) }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
size="300" size="300"
@ -125,7 +125,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
</RoomNotificationModeSwitcher> </RoomNotificationModeSwitcher>
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleInvite} onClick={handleInvite}
variant="Primary" variant="Primary"
@ -161,7 +161,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
</MenuItem> </MenuItem>
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<UseStateProvider initial={false}> <UseStateProvider initial={false}>
{(promptLeave, setPromptLeave) => ( {(promptLeave, setPromptLeave) => (
<> <>

View file

@ -543,7 +543,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
> >
<Icon src={Icons.Cross} size="50" /> <Icon src={Icons.Cross} size="50" />
</IconButton> </IconButton>
<Box direction="Column"> <Box direction="Row" gap="200" alignItems="Center">
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />} {replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
<ReplyLayout <ReplyLayout
userColor={replyUsernameColor} userColor={replyUsernameColor}

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

@ -108,8 +108,8 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
}; };
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ minWidth: toRem(200) }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
size="300" size="300"
@ -144,7 +144,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
</RoomNotificationModeSwitcher> </RoomNotificationModeSwitcher>
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleInvite} onClick={handleInvite}
variant="Primary" variant="Primary"
@ -207,7 +207,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
</UseStateProvider> </UseStateProvider>
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<UseStateProvider initial={false}> <UseStateProvider initial={false}>
{(promptLeave, setPromptLeave) => ( {(promptLeave, setPromptLeave) => (
<> <>

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

@ -22,6 +22,7 @@ import {
as, as,
color, color,
config, config,
toRem,
} from 'folds'; } from 'folds';
import React, { import React, {
FormEventHandler, FormEventHandler,
@ -94,10 +95,10 @@ export const MessageQuickReactions = as<'div', MessageQuickReactionsProps>(
return ( return (
<> <>
<Box <Box
style={{ padding: config.space.S200 }} style={{ padding: config.space.S300 }}
alignItems="Center" alignItems="Center"
justifyContent="Center" justifyContent="Center"
gap="200" gap="300"
{...props} {...props}
ref={ref} ref={ref}
> >
@ -682,6 +683,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 +714,8 @@ export const Message = as<'div', MessageProps>(
powerLevelTag, powerLevelTag,
accessibleTagColors, accessibleTagColors,
legacyUsernameColor, legacyUsernameColor,
hour24Clock,
dateFormatString,
children, children,
...props ...props
}, },
@ -775,7 +780,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>
); );
@ -963,7 +973,7 @@ export const Message = as<'div', MessageProps>(
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Menu> <Menu style={{ minWidth: toRem(200) }}>
{canSendReaction && ( {canSendReaction && (
<MessageQuickReactions <MessageQuickReactions
onReaction={(key, shortcode) => { onReaction={(key, shortcode) => {
@ -1160,6 +1170,7 @@ export const Event = as<'div', EventProps>(
hideReadReceipts, hideReadReceipts,
showDeveloperTools, showDeveloperTools,
children, children,
style,
...props ...props
}, },
ref ref
@ -1226,7 +1237,7 @@ export const Event = as<'div', EventProps>(
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Menu {...props} ref={ref}> <Menu style={{ minWidth: toRem(200), ...style }} {...props} ref={ref}>
<Box direction="Column" gap="100" className={css.MessageMenuGroup}> <Box direction="Column" gap="100" className={css.MessageMenuGroup}>
{!hideReadReceipts && ( {!hideReadReceipts && (
<MessageReadReceiptItem <MessageReadReceiptItem

View file

@ -30,7 +30,7 @@ export const MessageQuickReaction = style({
}); });
export const MessageMenuGroup = style({ export const MessageMenuGroup = style({
padding: config.space.S100, padding: config.space.S200,
}); });
export const MessageMenuItemText = style({ export const MessageMenuItemText = style({

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

@ -315,7 +315,7 @@ export function DeviceVerificationOptions() {
}} }}
> >
<Menu> <Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
variant="Critical" variant="Critical"
onClick={handleReset} onClick={handleReset}

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 = {
@ -55,7 +60,7 @@ type ThemeSelectorProps = {
const ThemeSelector = as<'div', ThemeSelectorProps>( const ThemeSelector = as<'div', ThemeSelectorProps>(
({ themeNames, themes, selected, onSelect, ...props }, ref) => ( ({ themeNames, themes, selected, onSelect, ...props }, ref) => (
<Menu {...props} ref={ref}> <Menu {...props} ref={ref}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{themes.map((theme) => ( {themes.map((theme) => (
<MenuItem <MenuItem
key={theme.id} key={theme.id}
@ -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');
@ -423,7 +781,7 @@ function SelectMessageLayout() {
}} }}
> >
<Menu> <Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{messageLayoutItems.map((item) => ( {messageLayoutItems.map((item) => (
<MenuItem <MenuItem
key={item.layout} key={item.layout}
@ -492,7 +850,7 @@ function SelectMessageSpacing() {
}} }}
> >
<Menu> <Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{messageSpacingItems.map((item) => ( {messageSpacingItems.map((item) => (
<MenuItem <MenuItem
key={item.spacing} key={item.spacing}
@ -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

@ -92,7 +92,7 @@ export function NotificationModeSwitcher({ pushRule, onChange }: NotificationMod
}} }}
> >
<Menu> <Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{modes.map((mode) => ( {modes.map((mode) => (
<MenuItem <MenuItem
key={mode} key={mode}

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

@ -108,10 +108,10 @@ export function ServerPicker({
}} }}
> >
<Menu> <Menu>
<Header size="300" style={{ padding: `0 ${config.space.S200}` }}> <Header size="400" style={{ padding: `0 ${config.space.S300}` }}>
<Text size="L400">Homeserver List</Text> <Text size="L400">Homeserver List</Text>
</Header> </Header>
<div style={{ padding: config.space.S100, paddingTop: 0 }}> <div style={{ padding: config.space.S200, paddingTop: 0 }}>
{serverList?.map((serverName) => ( {serverList?.map((serverName) => (
<MenuItem <MenuItem
key={serverName} key={serverName}

View file

@ -59,11 +59,11 @@ function UsernameHint({ server }: { server: string }) {
}} }}
> >
<Menu> <Menu>
<Header size="300" style={{ padding: `0 ${config.space.S200}` }}> <Header size="400" style={{ padding: `0 ${config.space.S400}` }}>
<Text size="L400">Hint</Text> <Text size="L400">Hint</Text>
</Header> </Header>
<Box <Box
style={{ padding: config.space.S200, paddingTop: 0 }} style={{ padding: config.space.S400, paddingTop: 0 }}
direction="Column" direction="Column"
tabIndex={0} tabIndex={0}
gap="100" gap="100"

View file

@ -91,7 +91,7 @@ function ClientRootOptions({ mx }: { mx?: MatrixClient }) {
}} }}
> >
<Menu> <Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{mx && ( {mx && (
<MenuItem onClick={() => clearCacheAndReload(mx)} size="300" radii="300"> <MenuItem onClick={() => clearCacheAndReload(mx)} size="300" radii="300">
<Text as="span" size="T300" truncate> <Text as="span" size="T300" truncate>

View file

@ -67,8 +67,8 @@ const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }
}; };
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ minWidth: toRem(200) }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
size="300" size="300"

View file

@ -193,7 +193,7 @@ function ThirdPartyProtocolsSelector({
<Box <Box
direction="Column" direction="Column"
gap="100" gap="100"
style={{ padding: config.space.S100, minWidth: toRem(100) }} style={{ padding: config.space.S200, minWidth: toRem(100) }}
> >
<Text style={{ padding: config.space.S100 }} size="L400" truncate> <Text style={{ padding: config.space.S100 }} size="L400" truncate>
Protocols Protocols
@ -313,11 +313,11 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
step={1} step={1}
outlined outlined
type="number" type="number"
radii="400" radii="300"
aria-label="Per Page Item Limit" aria-label="Per Page Item Limit"
/> />
</Box> </Box>
<Button type="submit" size="300" variant="Primary" radii="400"> <Button type="submit" size="300" variant="Primary" radii="300">
<Text size="B300">Change Limit</Text> <Text size="B300">Change Limit</Text>
</Button> </Button>
</Box> </Box>

View file

@ -75,8 +75,8 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, re
}; };
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ minWidth: toRem(200) }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
size="300" size="300"

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

@ -42,8 +42,8 @@ const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }
}; };
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ minWidth: toRem(200) }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
size="300" size="300"

View file

@ -43,8 +43,8 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, re
}; };
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ minWidth: toRem(200) }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
size="300" size="300"

View file

@ -142,8 +142,8 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
}; };
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ minWidth: toRem(200) }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
size="300" size="300"
@ -169,7 +169,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
)} )}
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleInvite} onClick={handleInvite}
variant="Primary" variant="Primary"

View file

@ -128,8 +128,8 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
}; };
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ minWidth: toRem(200) }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
size="300" size="300"
@ -143,7 +143,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
</MenuItem> </MenuItem>
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<MenuItem <MenuItem
onClick={handleInvite} onClick={handleInvite}
variant="Primary" variant="Primary"
@ -191,7 +191,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
)} )}
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<UseStateProvider initial={false}> <UseStateProvider initial={false}>
{(promptLeave, setPromptLeave) => ( {(promptLeave, setPromptLeave) => (
<> <>

View file

@ -4,7 +4,6 @@ import React, {
ReactEventHandler, ReactEventHandler,
Suspense, Suspense,
lazy, lazy,
useCallback,
useMemo, useMemo,
useState, useState,
} from 'react'; } from 'react';
@ -17,7 +16,7 @@ import {
} from 'html-react-parser'; } from 'html-react-parser';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk';
import classNames from 'classnames'; import classNames from 'classnames';
import { Icon, IconButton, Icons, Scroll, Text } from 'folds'; import { Box, Chip, config, Header, Icon, IconButton, Icons, Scroll, Text, toRem } from 'folds';
import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyjs'; import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyjs';
import Linkify from 'linkify-react'; import Linkify from 'linkify-react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
@ -205,79 +204,108 @@ export const highlightText = (
); );
}); });
export function CodeBlock(children: ChildNode[], opts: HTMLReactParserOptions) { /**
* Recursively extracts and concatenates all text content from an array of ChildNode objects.
*
* @param {ChildNode[]} nodes - An array of ChildNode objects to extract text from.
* @returns {string} The concatenated plain text content of all descendant text nodes.
*/
const extractTextFromChildren = (nodes: ChildNode[]): string => {
let text = '';
nodes.forEach((node) => {
if (node.type === 'text') {
text += node.data;
} else if (node instanceof Element && node.children) {
text += extractTextFromChildren(node.children);
}
});
return text;
};
export function CodeBlock({
children,
opts,
}: {
children: ChildNode[];
opts: HTMLReactParserOptions;
}) {
const code = children[0];
const languageClass =
code instanceof Element && code.name === 'code' ? code.attribs.class : undefined;
const language =
languageClass && languageClass.startsWith('language-')
? languageClass.replace('language-', '')
: languageClass;
const LINE_LIMIT = 14; const LINE_LIMIT = 14;
const largeCodeBlock = useMemo(
/**
* Recursively extracts and concatenates all text content from an array of ChildNode objects.
*
* @param {ChildNode[]} nodes - An array of ChildNode objects to extract text from.
* @returns {string} The concatenated plain text content of all descendant text nodes.
*/
const extractTextFromChildren = useCallback((nodes: ChildNode[]): string => {
let text = '';
nodes.forEach((node) => {
if (node.type === 'text') {
text += node.data;
} else if (node instanceof Element && node.children) {
text += extractTextFromChildren(node.children);
}
});
return text;
}, []);
const [copied, setCopied] = useTimeoutToggle();
const collapsible = useMemo(
() => extractTextFromChildren(children).split('\n').length > LINE_LIMIT, () => extractTextFromChildren(children).split('\n').length > LINE_LIMIT,
[children, extractTextFromChildren] [children]
); );
const [collapsed, setCollapsed] = useState(collapsible);
const handleCopy = useCallback(() => { const [expanded, setExpand] = useState(false);
const [copied, setCopied] = useTimeoutToggle();
const handleCopy = () => {
copyToClipboard(extractTextFromChildren(children)); copyToClipboard(extractTextFromChildren(children));
setCopied(); setCopied();
}, [children, extractTextFromChildren, setCopied]); };
const toggleCollapse = useCallback(() => { const toggleExpand = () => {
setCollapsed((prev) => !prev); setExpand(!expanded);
}, []); };
return ( return (
<> <Text size="T300" as="pre" className={css.CodeBlock}>
<div className={css.CodeBlockControls}> <Header variant="Surface" size="400" className={css.CodeBlockHeader}>
<IconButton <Box grow="Yes">
variant="Secondary" // Needs a better copy icon <Text size="L400" truncate>
size="300" {language ?? 'Code'}
radii="300" </Text>
onClick={handleCopy} </Box>
aria-label="Copy Code Block" <Box shrink="No" gap="200">
> <Chip
<Icon src={copied ? Icons.Check : Icons.File} size="50" /> variant={copied ? 'Success' : 'Surface'}
</IconButton> fill="None"
{collapsible && ( radii="Pill"
<IconButton onClick={handleCopy}
variant="Secondary" before={copied && <Icon size="50" src={Icons.Check} />}
size="300"
radii="300"
onClick={toggleCollapse}
aria-expanded={!collapsed}
aria-pressed={!collapsed}
aria-controls="code-block-content"
aria-label={collapsed ? 'Show Full Code Block' : 'Show Code Block Preview'}
style={collapsed ? { visibility: 'visible' } : {}}
> >
<Icon src={collapsed ? Icons.ChevronBottom : Icons.ChevronTop} size="50" /> <Text size="B300">{copied ? 'Copied' : 'Copy'}</Text>
</IconButton> </Chip>
)} {largeCodeBlock && (
</div> <IconButton
<Scroll direction="Both" variant="Secondary" size="300" visibility="Hover" hideTrack> size="300"
<div id="code-block-content" className={css.CodeBlockInternal({ collapsed })}> variant="SurfaceVariant"
outlined
radii="300"
onClick={toggleExpand}
aria-label={expanded ? 'Collapse' : 'Expand'}
>
<Icon size="50" src={expanded ? Icons.ChevronTop : Icons.ChevronBottom} />
</IconButton>
)}
</Box>
</Header>
<Scroll
style={{
maxHeight: largeCodeBlock && !expanded ? toRem(300) : undefined,
paddingBottom: largeCodeBlock ? config.space.S400 : undefined,
}}
direction="Both"
variant="SurfaceVariant"
size="300"
visibility="Hover"
hideTrack
>
<div id="code-block-content" className={css.CodeBlockInternal}>
{domToReact(children, opts)} {domToReact(children, opts)}
</div> </div>
</Scroll> </Scroll>
</> {largeCodeBlock && !expanded && <Box className={css.CodeBlockBottomShadow} />}
</Text>
); );
} }
@ -355,11 +383,7 @@ export const getReactCustomHtmlParser = (
} }
if (name === 'pre') { if (name === 'pre') {
return ( return <CodeBlock opts={opts}>{children}</CodeBlock>;
<Text {...props} as="pre" className={css.CodeBlock}>
{CodeBlock(children, opts)}
</Text>
);
} }
if (name === 'blockquote') { if (name === 'blockquote') {
@ -409,9 +433,9 @@ export const getReactCustomHtmlParser = (
} }
} else { } else {
return ( return (
<code className={css.Code} {...props}> <Text as="code" size="T300" className={css.Code} {...props}>
{domToReact(children, opts)} {domToReact(children, opts)}
</code> </Text>
); );
} }
} }

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

@ -41,16 +41,19 @@ export const BlockQuote = style([
]); ]);
const BaseCode = style({ const BaseCode = style({
fontFamily: 'monospace', color: color.SurfaceVariant.OnContainer,
color: color.Secondary.OnContainer, background: color.SurfaceVariant.Container,
background: color.Secondary.Container, border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
border: `${config.borderWidth.B300} solid ${color.Secondary.ContainerLine}`,
borderRadius: config.radii.R300, borderRadius: config.radii.R300,
}); });
const CodeFont = style({
fontFamily: 'monospace',
});
export const Code = style([ export const Code = style([
DefaultReset, DefaultReset,
BaseCode, BaseCode,
CodeFont,
{ {
padding: `0 ${config.space.S100}`, padding: `0 ${config.space.S100}`,
}, },
@ -86,34 +89,31 @@ export const CodeBlock = style([
{ {
fontStyle: 'normal', fontStyle: 'normal',
position: 'relative', position: 'relative',
overflow: 'hidden',
}, },
]); ]);
export const CodeBlockInternal = recipe({ export const CodeBlockHeader = style({
base: { padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
padding: `${config.space.S200} ${config.space.S200} 0`, borderBottomWidth: config.borderWidth.B300,
minWidth: toRem(100), gap: config.space.S200,
},
variants: {
collapsed: {
true: {
maxHeight: `calc(${config.lineHeight.T400} * 9.6)`,
},
},
},
}); });
export const CodeBlockControls = style({ export const CodeBlockInternal = style([
position: 'absolute', CodeFont,
top: config.space.S200, {
right: config.space.S200, padding: `${config.space.S200} ${config.space.S200} 0`,
visibility: 'hidden', minWidth: toRem(200),
selectors: {
[`${CodeBlock}:hover &`]: {
visibility: 'visible',
},
[`${CodeBlock}:focus-within &`]: {
visibility: 'visible',
},
}, },
]);
export const CodeBlockBottomShadow = style({
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
pointerEvents: 'none',
height: config.space.S400,
background: `linear-gradient(to top, #00000022, #00000000)`,
}); });
export const List = style([ export const List = style([

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');