mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-04 14:30:29 +03:00
Merge branch 'dev' into improve-space
This commit is contained in:
commit
71e775fed1
55 changed files with 896 additions and 264 deletions
|
|
@ -4,10 +4,25 @@ import PropTypes from 'prop-types';
|
|||
import dateFormat from 'dateformat';
|
||||
import { isInSameDay } from '../../../util/common';
|
||||
|
||||
function Time({ timestamp, fullTime }) {
|
||||
/**
|
||||
* Renders a formatted timestamp.
|
||||
*
|
||||
* Displays the time in hour:minute format if the message is from today or yesterday, unless `fullTime` is true.
|
||||
* For older messages, it shows the date and time.
|
||||
*
|
||||
* @param {number} timestamp - The timestamp to display.
|
||||
* @param {boolean} [fullTime=false] - If true, always show the full date and time.
|
||||
* @param {boolean} hour24Clock - Whether to use 24-hour time format.
|
||||
* @param {string} dateFormatString - Format string for the date part.
|
||||
* @returns {JSX.Element} A <time> element with the formatted date/time.
|
||||
*/
|
||||
function Time({ timestamp, fullTime, hour24Clock, dateFormatString }) {
|
||||
const date = new Date(timestamp);
|
||||
|
||||
const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
|
||||
const formattedFullTime = dateFormat(
|
||||
date,
|
||||
hour24Clock ? 'dd mmmm yyyy, HH:MM' : 'dd mmmm yyyy, hh:MM TT'
|
||||
);
|
||||
let formattedDate = formattedFullTime;
|
||||
|
||||
if (!fullTime) {
|
||||
|
|
@ -16,17 +31,19 @@ function Time({ timestamp, fullTime }) {
|
|||
compareDate.setDate(compareDate.getDate() - 1);
|
||||
const isYesterday = isInSameDay(date, compareDate);
|
||||
|
||||
formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy');
|
||||
const timeFormat = hour24Clock ? 'HH:MM' : 'hh:MM TT';
|
||||
|
||||
formattedDate = dateFormat(
|
||||
date,
|
||||
isToday || isYesterday ? timeFormat : dateFormatString.toLowerCase()
|
||||
);
|
||||
if (isYesterday) {
|
||||
formattedDate = `Yesterday, ${formattedDate}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<time
|
||||
dateTime={date.toISOString()}
|
||||
title={formattedFullTime}
|
||||
>
|
||||
<time dateTime={date.toISOString()} title={formattedFullTime}>
|
||||
{formattedDate}
|
||||
</time>
|
||||
);
|
||||
|
|
@ -39,6 +56,8 @@ Time.defaultProps = {
|
|||
Time.propTypes = {
|
||||
timestamp: PropTypes.number.isRequired,
|
||||
fullTime: PropTypes.bool,
|
||||
hour24Clock: PropTypes.bool.isRequired,
|
||||
dateFormatString: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Time;
|
||||
|
|
|
|||
|
|
@ -203,12 +203,8 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
style={{
|
||||
padding: config.space.S100,
|
||||
}}
|
||||
>
|
||||
<Box direction="Column" gap="100">
|
||||
<Menu style={{ padding: config.space.S200 }}>
|
||||
<Box direction="Column" gap="200">
|
||||
<Box direction="Column" gap="200">
|
||||
<InfoCard
|
||||
variant="SurfaceVariant"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function HexColorPickerPopOut({ picker, onRemove, children }: HexColorPic
|
|||
>
|
||||
<Menu
|
||||
style={{
|
||||
padding: config.space.S100,
|
||||
padding: config.space.S200,
|
||||
borderRadius: config.radii.R500,
|
||||
overflow: 'initial',
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export function JoinRulesSwitcher<T extends ExtendedJoinRules[]>({
|
|||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{rules.map((rule) => (
|
||||
<MenuItem
|
||||
key={rule}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export function ManualVerificationMethodSwitcher({
|
|||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
size="300"
|
||||
variant="Surface"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function MemberSortMenu({ selected, onSelect, requestClose }: MemberSortM
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
<Menu style={{ padding: config.space.S200 }}>
|
||||
{memberSortMenu.map((menuItem, index) => (
|
||||
<MenuItem
|
||||
key={menuItem.name}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function MembershipFilterMenu({
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
<Menu style={{ padding: config.space.S200 }}>
|
||||
{membershipFilterMenu.map((menuItem, index) => (
|
||||
<MenuItem
|
||||
key={menuItem.name}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export function RoomNotificationModeSwitcher({
|
|||
return (
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
offset={5}
|
||||
offset={8}
|
||||
position="Right"
|
||||
align="Start"
|
||||
content={
|
||||
|
|
@ -86,7 +86,7 @@ export function RoomNotificationModeSwitcher({
|
|||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{modes.map((mode) => (
|
||||
<MenuItem
|
||||
key={mode}
|
||||
|
|
|
|||
|
|
@ -157,12 +157,12 @@ export function RenderElement({ attributes, element, children }: RenderElementPr
|
|||
<Text as="pre" className={css.CodeBlock} {...attributes}>
|
||||
<Scroll
|
||||
direction="Horizontal"
|
||||
variant="Secondary"
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
visibility="Hover"
|
||||
hideTrack
|
||||
>
|
||||
<div className={css.CodeBlockInternal()}>{children}</div>
|
||||
<div className={css.CodeBlockInternal}>{children}</div>
|
||||
</Scroll>
|
||||
</Text>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export function HeadingBlockButton() {
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
<Menu style={{ padding: config.space.S200 }}>
|
||||
<Box gap="100">
|
||||
<TooltipProvider
|
||||
tooltip={<BtnTooltip text="Heading 1" shortCode={`${modKey} + 1`} />}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export function UsageSelector({ selected, onChange }: UsageSelectorProps) {
|
|||
);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{allUsages.map((usage) => (
|
||||
<MenuItem
|
||||
key={getUsageStr(usage)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export const ReplyBend = style({
|
|||
|
||||
export const ThreadIndicator = style({
|
||||
opacity: config.opacity.P300,
|
||||
gap: toRem(2),
|
||||
|
||||
selectors: {
|
||||
'button&': {
|
||||
|
|
@ -19,11 +18,6 @@ export const ThreadIndicator = style({
|
|||
},
|
||||
});
|
||||
|
||||
export const ThreadIndicatorIcon = style({
|
||||
width: toRem(14),
|
||||
height: toRem(14),
|
||||
});
|
||||
|
||||
export const Reply = style({
|
||||
marginBottom: toRem(1),
|
||||
minWidth: 0,
|
||||
|
|
|
|||
|
|
@ -38,9 +38,16 @@ export const ReplyLayout = as<'div', ReplyLayoutProps>(
|
|||
);
|
||||
|
||||
export const ThreadIndicator = as<'div'>(({ ...props }, ref) => (
|
||||
<Box className={css.ThreadIndicator} alignItems="Center" {...props} ref={ref}>
|
||||
<Icon className={css.ThreadIndicatorIcon} src={Icons.Message} />
|
||||
<Text size="T200">Threaded reply</Text>
|
||||
<Box
|
||||
shrink="No"
|
||||
className={css.ThreadIndicator}
|
||||
alignItems="Center"
|
||||
gap="100"
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<Icon size="50" src={Icons.Thread} />
|
||||
<Text size="L400">Thread</Text>
|
||||
</Box>
|
||||
));
|
||||
|
||||
|
|
@ -97,7 +104,7 @@ export const Reply = as<'div', ReplyProps>(
|
|||
const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody;
|
||||
|
||||
return (
|
||||
<Box direction="Column" alignItems="Start" {...props} ref={ref}>
|
||||
<Box direction="Row" gap="200" alignItems="Center" {...props} ref={ref}>
|
||||
{threadRootId && (
|
||||
<ThreadIndicator as="button" data-event-id={threadRootId} onClick={onClick} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,19 +5,35 @@ import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/ti
|
|||
export type TimeProps = {
|
||||
compact?: boolean;
|
||||
ts: number;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a formatted timestamp, supporting compact and full display modes.
|
||||
*
|
||||
* Displays the time in hour:minute format if the message is from today, yesterday, or if `compact` is true.
|
||||
* For older messages, it shows the date and time.
|
||||
*
|
||||
* @param {number} ts - The timestamp to display.
|
||||
* @param {boolean} [compact=false] - If true, always show only the time.
|
||||
* @param {boolean} hour24Clock - Whether to use 24-hour time format.
|
||||
* @param {string} dateFormatString - Format string for the date part.
|
||||
* @returns {React.ReactElement} A <Text as="time"> element with the formatted date/time.
|
||||
*/
|
||||
export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
|
||||
({ compact, ts, ...props }, ref) => {
|
||||
({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => {
|
||||
const formattedTime = timeHourMinute(ts, hour24Clock);
|
||||
|
||||
let time = '';
|
||||
if (compact) {
|
||||
time = timeHourMinute(ts);
|
||||
time = formattedTime;
|
||||
} else if (today(ts)) {
|
||||
time = timeHourMinute(ts);
|
||||
time = formattedTime;
|
||||
} else if (yesterday(ts)) {
|
||||
time = `Yesterday ${timeHourMinute(ts)}`;
|
||||
time = `Yesterday ${formattedTime}`;
|
||||
} else {
|
||||
time = `${timeDayMonYear(ts)} ${timeHourMinute(ts)}`;
|
||||
time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -16,13 +16,14 @@ export const PowerSelector = forwardRef<HTMLDivElement, PowerSelectorProps>(
|
|||
ref={ref}
|
||||
style={{
|
||||
maxHeight: '75vh',
|
||||
maxWidth: toRem(300),
|
||||
maxWidth: toRem(200),
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Scroll size="0" hideTrack visibility="Hover">
|
||||
<div style={{ padding: config.space.S100 }}>
|
||||
<div style={{ padding: config.space.S200 }}>
|
||||
{getPowers(powerLevelTags).map((power) => {
|
||||
const selected = value === power;
|
||||
const tag = powerLevelTags[power];
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ export const RoomCard = as<'div', RoomCardProps>(
|
|||
variant="Secondary"
|
||||
size="300"
|
||||
disabled={joining}
|
||||
before={joining && <Spinner size="50" variant="Secondary" fill="Soft" />}
|
||||
before={joining && <Spinner size="50" variant="Secondary" fill="Solid" />}
|
||||
>
|
||||
<Text size="B300" truncate>
|
||||
{joining ? 'Joining' : 'Join'}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import { nameInitials } from '../../utils/common';
|
|||
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
|
||||
export type RoomIntroProps = {
|
||||
room: Room;
|
||||
|
|
@ -43,6 +45,8 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
|
|||
useCallback(async (roomId: string) => mx.joinRoom(roomId), [mx])
|
||||
);
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
|
||||
return (
|
||||
<Box direction="Column" grow="Yes" gap="500" {...props} ref={ref}>
|
||||
<Box>
|
||||
|
|
@ -67,7 +71,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
|
|||
<Text size="T200" priority="300">
|
||||
{'Created by '}
|
||||
<b>@{creatorName}</b>
|
||||
{` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts)}`}
|
||||
{` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts, hour24Clock)}`}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import dayjs from 'dayjs';
|
|||
import * as css from './styles.css';
|
||||
import { PickerColumn } from './PickerColumn';
|
||||
import { hour12to24, hour24to12, hoursToMs, inSameDay, minutesToMs } from '../../utils/time';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
|
||||
type TimePickerProps = {
|
||||
min: number;
|
||||
|
|
@ -13,9 +15,11 @@ type TimePickerProps = {
|
|||
};
|
||||
export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
||||
({ min, max, value, onChange }, ref) => {
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
|
||||
const hour24 = dayjs(value).hour();
|
||||
|
||||
const selectedHour = hour24to12(hour24);
|
||||
const selectedHour = hour24Clock ? hour24 : hour24to12(hour24);
|
||||
const selectedMinute = dayjs(value).minute();
|
||||
const selectedPM = hour24 >= 12;
|
||||
|
||||
|
|
@ -24,7 +28,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||
};
|
||||
|
||||
const handleHour = (hour: number) => {
|
||||
const seconds = hoursToMs(hour12to24(hour, selectedPM));
|
||||
const seconds = hoursToMs(hour24Clock ? hour : hour12to24(hour, selectedPM));
|
||||
const lastSeconds = hoursToMs(hour24);
|
||||
const newValue = value + (seconds - lastSeconds);
|
||||
handleSubmit(newValue);
|
||||
|
|
@ -59,7 +63,22 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||
<Menu className={css.PickerMenu} ref={ref}>
|
||||
<Box direction="Row" gap="200" className={css.PickerContainer}>
|
||||
<PickerColumn title="Hour">
|
||||
{Array.from(Array(12).keys())
|
||||
{hour24Clock
|
||||
? Array.from(Array(24).keys()).map((hour) => (
|
||||
<Chip
|
||||
key={hour}
|
||||
size="500"
|
||||
variant={hour === selectedHour ? 'Primary' : 'Background'}
|
||||
fill="None"
|
||||
radii="300"
|
||||
aria-selected={hour === selectedHour}
|
||||
onClick={() => handleHour(hour)}
|
||||
disabled={(minDay && hour < minHour24) || (maxDay && hour > maxHour24)}
|
||||
>
|
||||
<Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
|
||||
</Chip>
|
||||
))
|
||||
: Array.from(Array(12).keys())
|
||||
.map((i) => {
|
||||
if (i === 0) return 12;
|
||||
return i;
|
||||
|
|
@ -101,6 +120,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||
</Chip>
|
||||
))}
|
||||
</PickerColumn>
|
||||
{!hour24Clock && (
|
||||
<PickerColumn title="Period">
|
||||
<Chip
|
||||
size="500"
|
||||
|
|
@ -125,6 +145,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||
<Text size="T300">PM</Text>
|
||||
</Chip>
|
||||
</PickerColumn>
|
||||
)}
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export function RoomHistoryVisibility({ powerLevels }: RoomHistoryVisibilityProp
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
<Menu style={{ padding: config.space.S200 }}>
|
||||
{visibilityMenu.map((visibility) => (
|
||||
<MenuItem
|
||||
key={visibility}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ function PeekPermissions({ powerLevels, power, permissionGroups, children }: Pee
|
|||
>
|
||||
<Box grow="Yes" tabIndex={0}>
|
||||
<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) => (
|
||||
<Box key={groupIndex} direction="Column" gap="100">
|
||||
<Text size="L400">{group.name}</Text>
|
||||
|
|
|
|||
|
|
@ -234,9 +234,9 @@ export function HierarchyItemMenu({
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ maxWidth: toRem(150), width: '100vw' }}>
|
||||
<Menu style={{ minWidth: toRem(200) }}>
|
||||
{joined && (
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{onTogglePin && (
|
||||
<MenuItem
|
||||
size="300"
|
||||
|
|
@ -296,7 +296,7 @@ export function HierarchyItemMenu({
|
|||
<Line size="300" variant="Surface" direction="Horizontal" />
|
||||
)}
|
||||
{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} />
|
||||
<RemoveMenuItem item={item} requestClose={handleRequestClose} />
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Menu ref={ref} style={{ minWidth: toRem(200) }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
onClick={handleInvite}
|
||||
variant="Primary"
|
||||
|
|
@ -87,7 +87,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
|||
</MenuItem>
|
||||
</Box>
|
||||
<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}>
|
||||
{(promptLeave, setPromptLeave) => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
<Menu style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
size="300"
|
||||
radii="300"
|
||||
|
|
@ -336,7 +336,7 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
<Menu style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
size="300"
|
||||
radii="300"
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ export function MessageSearch({
|
|||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
|
@ -289,6 +292,8 @@ export function MessageSearch({
|
|||
urlPreview={urlPreview}
|
||||
onOpen={navigateRoom}
|
||||
legacyUsernameColor={legacyUsernameColor || mDirects.has(groupRoom.roomId)}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -74,8 +74,7 @@ function OrderButton({ order, onChange }: OrderButtonProps) {
|
|||
<Header size="300" variant="Surface" style={{ padding: `0 ${config.space.S300}` }}>
|
||||
<Text size="L400">Sort by</Text>
|
||||
</Header>
|
||||
<Line variant="Surface" size="300" />
|
||||
<div style={{ padding: config.space.S100 }}>
|
||||
<div style={{ padding: config.space.S200, paddingTop: 0 }}>
|
||||
<MenuItem
|
||||
onClick={() => setOrder()}
|
||||
variant="Surface"
|
||||
|
|
@ -291,7 +290,7 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
|
|||
</Box>
|
||||
</Scroll>
|
||||
<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}>
|
||||
{localSelected && localSelected.length > 0 ? (
|
||||
<Text size="B300">Save ({localSelected.length})</Text>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ type SearchResultGroupProps = {
|
|||
urlPreview?: boolean;
|
||||
onOpen: (roomId: string, eventId: string) => void;
|
||||
legacyUsernameColor?: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
};
|
||||
export function SearchResultGroup({
|
||||
room,
|
||||
|
|
@ -66,6 +68,8 @@ export function SearchResultGroup({
|
|||
urlPreview,
|
||||
onOpen,
|
||||
legacyUsernameColor,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
}: SearchResultGroupProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
|
|
@ -275,7 +279,11 @@ export function SearchResultGroup({
|
|||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time ts={event.origin_server_ts} />
|
||||
<Time
|
||||
ts={event.origin_server_ts}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</Box>
|
||||
<Box shrink="No" gap="200" alignItems="Center">
|
||||
<Chip
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Menu ref={ref} style={{ minWidth: toRem(200) }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
|
|
@ -125,7 +125,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
|||
</RoomNotificationModeSwitcher>
|
||||
</Box>
|
||||
<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
|
||||
onClick={handleInvite}
|
||||
variant="Primary"
|
||||
|
|
@ -161,7 +161,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
|||
</MenuItem>
|
||||
</Box>
|
||||
<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}>
|
||||
{(promptLeave, setPromptLeave) => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -543,7 +543,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
>
|
||||
<Icon src={Icons.Cross} size="50" />
|
||||
</IconButton>
|
||||
<Box direction="Column">
|
||||
<Box direction="Row" gap="200" alignItems="Center">
|
||||
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
|
||||
<ReplyLayout
|
||||
userColor={replyUsernameColor}
|
||||
|
|
|
|||
|
|
@ -450,6 +450,9 @@ export function RoomTimeline({
|
|||
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
|
||||
const [showDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const ignoredUsersList = useIgnoredUsers();
|
||||
const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]);
|
||||
|
||||
|
|
@ -1072,6 +1075,8 @@ export function RoomTimeline({
|
|||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
>
|
||||
{mEvent.isRedacted() ? (
|
||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||
|
|
@ -1154,6 +1159,8 @@ export function RoomTimeline({
|
|||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
>
|
||||
<EncryptedContent mEvent={mEvent}>
|
||||
{() => {
|
||||
|
|
@ -1256,6 +1263,8 @@ export function RoomTimeline({
|
|||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
>
|
||||
{mEvent.isRedacted() ? (
|
||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||
|
|
@ -1284,7 +1293,12 @@ export function RoomTimeline({
|
|||
const parsed = parseMemberEvent(mEvent);
|
||||
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -1321,7 +1335,12 @@ export function RoomTimeline({
|
|||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -1359,7 +1378,12 @@ export function RoomTimeline({
|
|||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -1397,7 +1421,12 @@ export function RoomTimeline({
|
|||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -1437,7 +1466,12 @@ export function RoomTimeline({
|
|||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -1482,7 +1516,12 @@ export function RoomTimeline({
|
|||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -108,8 +108,8 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Menu ref={ref} style={{ minWidth: toRem(200) }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
|
|
@ -144,7 +144,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
|||
</RoomNotificationModeSwitcher>
|
||||
</Box>
|
||||
<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
|
||||
onClick={handleInvite}
|
||||
variant="Primary"
|
||||
|
|
@ -207,7 +207,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
|||
</UseStateProvider>
|
||||
</Box>
|
||||
<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}>
|
||||
{(promptLeave, setPromptLeave) => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import { useRoom } from '../../../hooks/useRoom';
|
|||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { getToday, getYesterday, timeDayMonthYear, timeHourMinute } from '../../../utils/time';
|
||||
import { DatePicker, TimePicker } from '../../../components/time-date';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
|
||||
type JumpToTimeProps = {
|
||||
onCancel: () => void;
|
||||
|
|
@ -45,6 +47,8 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
|
|||
const createTs = useMemo(() => createStateEvent?.getTs() ?? 0, [createStateEvent]);
|
||||
const [ts, setTs] = useState(() => Date.now());
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
|
||||
const [timePickerCords, setTimePickerCords] = useState<RectCords>();
|
||||
const [datePickerCords, setDatePickerCords] = useState<RectCords>();
|
||||
|
||||
|
|
@ -125,7 +129,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
|
|||
after={<Icon size="50" src={Icons.ChevronBottom} />}
|
||||
onClick={handleTimePicker}
|
||||
>
|
||||
<Text size="B300">{timeHourMinute(ts)}</Text>
|
||||
<Text size="B300">{timeHourMinute(ts, hour24Clock)}</Text>
|
||||
</Chip>
|
||||
<PopOut
|
||||
anchor={timePickerCords}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
as,
|
||||
color,
|
||||
config,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import React, {
|
||||
FormEventHandler,
|
||||
|
|
@ -94,10 +95,10 @@ export const MessageQuickReactions = as<'div', MessageQuickReactionsProps>(
|
|||
return (
|
||||
<>
|
||||
<Box
|
||||
style={{ padding: config.space.S200 }}
|
||||
style={{ padding: config.space.S300 }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
gap="200"
|
||||
gap="300"
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
|
|
@ -682,6 +683,8 @@ export type MessageProps = {
|
|||
powerLevelTag?: PowerLevelTag;
|
||||
accessibleTagColors?: Map<string, string>;
|
||||
legacyUsernameColor?: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
};
|
||||
export const Message = as<'div', MessageProps>(
|
||||
(
|
||||
|
|
@ -711,6 +714,8 @@ export const Message = as<'div', MessageProps>(
|
|||
powerLevelTag,
|
||||
accessibleTagColors,
|
||||
legacyUsernameColor,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
|
|
@ -775,7 +780,12 @@ export const Message = as<'div', MessageProps>(
|
|||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -963,7 +973,7 @@ export const Message = as<'div', MessageProps>(
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Menu style={{ minWidth: toRem(200) }}>
|
||||
{canSendReaction && (
|
||||
<MessageQuickReactions
|
||||
onReaction={(key, shortcode) => {
|
||||
|
|
@ -1160,6 +1170,7 @@ export const Event = as<'div', EventProps>(
|
|||
hideReadReceipts,
|
||||
showDeveloperTools,
|
||||
children,
|
||||
style,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
|
|
@ -1226,7 +1237,7 @@ export const Event = as<'div', EventProps>(
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu {...props} ref={ref}>
|
||||
<Menu style={{ minWidth: toRem(200), ...style }} {...props} ref={ref}>
|
||||
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
||||
{!hideReadReceipts && (
|
||||
<MessageReadReceiptItem
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const MessageQuickReaction = style({
|
|||
});
|
||||
|
||||
export const MessageMenuGroup = style({
|
||||
padding: config.space.S100,
|
||||
padding: config.space.S200,
|
||||
});
|
||||
|
||||
export const MessageMenuItemText = style({
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
const theme = useTheme();
|
||||
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const [unpinState, unpin] = useAsyncCallback(
|
||||
useCallback(() => {
|
||||
const pinEvent = getStateEvent(room, StateEvent.RoomPinnedEvents);
|
||||
|
|
@ -205,7 +208,11 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time ts={pinnedEvent.getTs()} />
|
||||
<Time
|
||||
ts={pinnedEvent.getTs()}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</Box>
|
||||
{renderOptions()}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import { SequenceCard } from '../../../components/sequence-card';
|
|||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { LogoutDialog } from '../../../components/LogoutDialog';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
|
||||
export function DeviceTilePlaceholder() {
|
||||
return (
|
||||
|
|
@ -41,6 +43,9 @@ export function DeviceTilePlaceholder() {
|
|||
}
|
||||
|
||||
function DeviceActiveTime({ ts }: { ts: number }) {
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
return (
|
||||
<Text className={BreakWord} size="T200">
|
||||
<Text size="Inherit" as="span" priority="300">
|
||||
|
|
@ -49,7 +54,8 @@ function DeviceActiveTime({ ts }: { ts: number }) {
|
|||
<>
|
||||
{today(ts) && 'Today'}
|
||||
{yesterday(ts) && 'Yesterday'}
|
||||
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)}
|
||||
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts, dateFormatString)}{' '}
|
||||
{timeHourMinute(ts, hour24Clock)}
|
||||
</>
|
||||
</Text>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ export function DeviceVerificationOptions() {
|
|||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
variant="Critical"
|
||||
onClick={handleReset}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import React, {
|
||||
ChangeEventHandler,
|
||||
FormEventHandler,
|
||||
KeyboardEventHandler,
|
||||
MouseEventHandler,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
as,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
config,
|
||||
Header,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
|
|
@ -28,7 +32,7 @@ import FocusTrap from 'focus-trap-react';
|
|||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
||||
import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { KeySymbol } from '../../../utils/key-symbol';
|
||||
import { isMacOS } from '../../../utils/user-agent';
|
||||
|
|
@ -44,6 +48,7 @@ import {
|
|||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
|
||||
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
|
||||
import { useDateFormatItems } from '../../../hooks/useDateFormat';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
|
||||
type ThemeSelectorProps = {
|
||||
|
|
@ -55,7 +60,7 @@ type ThemeSelectorProps = {
|
|||
const ThemeSelector = as<'div', ThemeSelectorProps>(
|
||||
({ themeNames, themes, selected, onSelect, ...props }, 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) => (
|
||||
<MenuItem
|
||||
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() {
|
||||
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||
|
|
@ -423,7 +781,7 @@ function SelectMessageLayout() {
|
|||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{messageLayoutItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.layout}
|
||||
|
|
@ -492,7 +850,7 @@ function SelectMessageSpacing() {
|
|||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{messageSpacingItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.spacing}
|
||||
|
|
@ -637,6 +995,7 @@ export function General({ requestClose }: GeneralProps) {
|
|||
<PageContent>
|
||||
<Box direction="Column" gap="700">
|
||||
<Appearance />
|
||||
<DateAndTime />
|
||||
<Editor />
|
||||
<Messages />
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export function NotificationModeSwitcher({ pushRule, onChange }: NotificationMod
|
|||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{modes.map((mode) => (
|
||||
<MenuItem
|
||||
key={mode}
|
||||
|
|
|
|||
34
src/app/hooks/useDateFormat.ts
Normal file
34
src/app/hooks/useDateFormat.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
|
@ -108,10 +108,10 @@ export function ServerPicker({
|
|||
}}
|
||||
>
|
||||
<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>
|
||||
</Header>
|
||||
<div style={{ padding: config.space.S100, paddingTop: 0 }}>
|
||||
<div style={{ padding: config.space.S200, paddingTop: 0 }}>
|
||||
{serverList?.map((serverName) => (
|
||||
<MenuItem
|
||||
key={serverName}
|
||||
|
|
|
|||
|
|
@ -59,11 +59,11 @@ function UsernameHint({ server }: { server: string }) {
|
|||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
|
||||
<Header size="400" style={{ padding: `0 ${config.space.S400}` }}>
|
||||
<Text size="L400">Hint</Text>
|
||||
</Header>
|
||||
<Box
|
||||
style={{ padding: config.space.S200, paddingTop: 0 }}
|
||||
style={{ padding: config.space.S400, paddingTop: 0 }}
|
||||
direction="Column"
|
||||
tabIndex={0}
|
||||
gap="100"
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ function ClientRootOptions({ mx }: { mx?: MatrixClient }) {
|
|||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{mx && (
|
||||
<MenuItem onClick={() => clearCacheAndReload(mx)} size="300" radii="300">
|
||||
<Text as="span" size="T300" truncate>
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Menu ref={ref} style={{ minWidth: toRem(200) }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ function ThirdPartyProtocolsSelector({
|
|||
<Box
|
||||
direction="Column"
|
||||
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>
|
||||
Protocols
|
||||
|
|
@ -313,11 +313,11 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
|
|||
step={1}
|
||||
outlined
|
||||
type="number"
|
||||
radii="400"
|
||||
radii="300"
|
||||
aria-label="Per Page Item Limit"
|
||||
/>
|
||||
</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>
|
||||
</Button>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, re
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Menu ref={ref} style={{ minWidth: toRem(200) }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ import { testBadWords } from '../../../plugins/bad-words';
|
|||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
||||
import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
|
||||
const COMPACT_CARD_WIDTH = 548;
|
||||
|
||||
|
|
@ -135,10 +137,19 @@ type NavigateHandler = (roomId: string, space: boolean) => void;
|
|||
type InviteCardProps = {
|
||||
invite: InviteData;
|
||||
compact?: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
onNavigate: NavigateHandler;
|
||||
hideAvatar: boolean;
|
||||
};
|
||||
function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps) {
|
||||
function InviteCard({
|
||||
invite,
|
||||
compact,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
onNavigate,
|
||||
hideAvatar,
|
||||
}: InviteCardProps) {
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getSafeUserId();
|
||||
|
||||
|
|
@ -295,7 +306,13 @@ function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps
|
|||
</Box>
|
||||
{invite.inviteTs && (
|
||||
<Box shrink="No">
|
||||
<Time size="T200" ts={invite.inviteTs} priority="300" />
|
||||
<Time
|
||||
size="T200"
|
||||
ts={invite.inviteTs}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
priority="300"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
|
@ -384,8 +401,16 @@ type KnownInvitesProps = {
|
|||
invites: InviteData[];
|
||||
handleNavigate: NavigateHandler;
|
||||
compact: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
};
|
||||
function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) {
|
||||
function KnownInvites({
|
||||
invites,
|
||||
handleNavigate,
|
||||
compact,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
}: KnownInvitesProps) {
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="H4">Primary</Text>
|
||||
|
|
@ -396,6 +421,8 @@ function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) {
|
|||
key={invite.roomId}
|
||||
invite={invite}
|
||||
compact={compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
onNavigate={handleNavigate}
|
||||
hideAvatar={false}
|
||||
/>
|
||||
|
|
@ -420,8 +447,16 @@ type UnknownInvitesProps = {
|
|||
invites: InviteData[];
|
||||
handleNavigate: NavigateHandler;
|
||||
compact: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
};
|
||||
function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProps) {
|
||||
function UnknownInvites({
|
||||
invites,
|
||||
handleNavigate,
|
||||
compact,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
}: UnknownInvitesProps) {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const [declineAllStatus, declineAll] = useAsyncCallback(
|
||||
|
|
@ -459,6 +494,8 @@ function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProp
|
|||
key={invite.roomId}
|
||||
invite={invite}
|
||||
compact={compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
onNavigate={handleNavigate}
|
||||
hideAvatar
|
||||
/>
|
||||
|
|
@ -483,8 +520,16 @@ type SpamInvitesProps = {
|
|||
invites: InviteData[];
|
||||
handleNavigate: NavigateHandler;
|
||||
compact: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
};
|
||||
function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) {
|
||||
function SpamInvites({
|
||||
invites,
|
||||
handleNavigate,
|
||||
compact,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
}: SpamInvitesProps) {
|
||||
const mx = useMatrixClient();
|
||||
const [showInvites, setShowInvites] = useState(false);
|
||||
|
||||
|
|
@ -608,6 +653,8 @@ function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) {
|
|||
key={invite.roomId}
|
||||
invite={invite}
|
||||
compact={compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
onNavigate={handleNavigate}
|
||||
hideAvatar
|
||||
/>
|
||||
|
|
@ -671,6 +718,9 @@ export function Invites() {
|
|||
);
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
|
||||
const handleNavigate = (roomId: string, space: boolean) => {
|
||||
if (space) {
|
||||
navigateSpace(roomId);
|
||||
|
|
@ -723,6 +773,8 @@ export function Invites() {
|
|||
<KnownInvites
|
||||
invites={knownInvites}
|
||||
compact={compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
handleNavigate={handleNavigate}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -731,6 +783,8 @@ export function Invites() {
|
|||
<UnknownInvites
|
||||
invites={unknownInvites}
|
||||
compact={compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
handleNavigate={handleNavigate}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -739,6 +793,8 @@ export function Invites() {
|
|||
<SpamInvites
|
||||
invites={spamInvites}
|
||||
compact={compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
handleNavigate={handleNavigate}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -205,6 +205,8 @@ type RoomNotificationsGroupProps = {
|
|||
hideActivity: boolean;
|
||||
onOpen: (roomId: string, eventId: string) => void;
|
||||
legacyUsernameColor?: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
};
|
||||
function RoomNotificationsGroupComp({
|
||||
room,
|
||||
|
|
@ -214,6 +216,8 @@ function RoomNotificationsGroupComp({
|
|||
hideActivity,
|
||||
onOpen,
|
||||
legacyUsernameColor,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
}: RoomNotificationsGroupProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
|
|
@ -496,7 +500,11 @@ function RoomNotificationsGroupComp({
|
|||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time ts={event.origin_server_ts} />
|
||||
<Time
|
||||
ts={event.origin_server_ts}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</Box>
|
||||
<Box shrink="No" gap="200" alignItems="Center">
|
||||
<Chip
|
||||
|
|
@ -549,6 +557,8 @@ export function Notifications() {
|
|||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const screenSize = useScreenSizeContext();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
|
|
@ -713,6 +723,8 @@ export function Notifications() {
|
|||
legacyUsernameColor={
|
||||
legacyUsernameColor || mDirects.has(groupRoom.roomId)
|
||||
}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Menu ref={ref} style={{ minWidth: toRem(200) }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, re
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Menu ref={ref} style={{ minWidth: toRem(200) }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
|
|
|
|||
|
|
@ -142,8 +142,8 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Menu ref={ref} style={{ minWidth: toRem(200) }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
|
|
@ -169,7 +169,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
|||
)}
|
||||
</Box>
|
||||
<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
|
||||
onClick={handleInvite}
|
||||
variant="Primary"
|
||||
|
|
|
|||
|
|
@ -128,8 +128,8 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Menu ref={ref} style={{ minWidth: toRem(200) }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
|
|
@ -143,7 +143,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
|||
</MenuItem>
|
||||
</Box>
|
||||
<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
|
||||
onClick={handleInvite}
|
||||
variant="Primary"
|
||||
|
|
@ -191,7 +191,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
|||
)}
|
||||
</Box>
|
||||
<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}>
|
||||
{(promptLeave, setPromptLeave) => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import React, {
|
|||
ReactEventHandler,
|
||||
Suspense,
|
||||
lazy,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
|
@ -17,7 +16,7 @@ import {
|
|||
} from 'html-react-parser';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
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 Linkify from 'linkify-react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
|
@ -205,16 +204,13 @@ export const highlightText = (
|
|||
);
|
||||
});
|
||||
|
||||
export function CodeBlock(children: ChildNode[], opts: HTMLReactParserOptions) {
|
||||
const LINE_LIMIT = 14;
|
||||
|
||||
/**
|
||||
/**
|
||||
* 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 => {
|
||||
const extractTextFromChildren = (nodes: ChildNode[]): string => {
|
||||
let text = '';
|
||||
|
||||
nodes.forEach((node) => {
|
||||
|
|
@ -226,58 +222,90 @@ export function CodeBlock(children: ChildNode[], opts: HTMLReactParserOptions) {
|
|||
});
|
||||
|
||||
return text;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const [copied, setCopied] = useTimeoutToggle();
|
||||
const collapsible = useMemo(
|
||||
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 largeCodeBlock = useMemo(
|
||||
() => 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));
|
||||
setCopied();
|
||||
}, [children, extractTextFromChildren, setCopied]);
|
||||
};
|
||||
|
||||
const toggleCollapse = useCallback(() => {
|
||||
setCollapsed((prev) => !prev);
|
||||
}, []);
|
||||
const toggleExpand = () => {
|
||||
setExpand(!expanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={css.CodeBlockControls}>
|
||||
<IconButton
|
||||
variant="Secondary" // Needs a better copy icon
|
||||
size="300"
|
||||
radii="300"
|
||||
<Text size="T300" as="pre" className={css.CodeBlock}>
|
||||
<Header variant="Surface" size="400" className={css.CodeBlockHeader}>
|
||||
<Box grow="Yes">
|
||||
<Text size="L400" truncate>
|
||||
{language ?? 'Code'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No" gap="200">
|
||||
<Chip
|
||||
variant={copied ? 'Success' : 'Surface'}
|
||||
fill="None"
|
||||
radii="Pill"
|
||||
onClick={handleCopy}
|
||||
aria-label="Copy Code Block"
|
||||
before={copied && <Icon size="50" src={Icons.Check} />}
|
||||
>
|
||||
<Icon src={copied ? Icons.Check : Icons.File} size="50" />
|
||||
</IconButton>
|
||||
{collapsible && (
|
||||
<Text size="B300">{copied ? 'Copied' : 'Copy'}</Text>
|
||||
</Chip>
|
||||
{largeCodeBlock && (
|
||||
<IconButton
|
||||
variant="Secondary"
|
||||
size="300"
|
||||
variant="SurfaceVariant"
|
||||
outlined
|
||||
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' } : {}}
|
||||
onClick={toggleExpand}
|
||||
aria-label={expanded ? 'Collapse' : 'Expand'}
|
||||
>
|
||||
<Icon src={collapsed ? Icons.ChevronBottom : Icons.ChevronTop} size="50" />
|
||||
<Icon size="50" src={expanded ? Icons.ChevronTop : Icons.ChevronBottom} />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<Scroll direction="Both" variant="Secondary" size="300" visibility="Hover" hideTrack>
|
||||
<div id="code-block-content" className={css.CodeBlockInternal({ collapsed })}>
|
||||
</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)}
|
||||
</div>
|
||||
</Scroll>
|
||||
</>
|
||||
{largeCodeBlock && !expanded && <Box className={css.CodeBlockBottomShadow} />}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -355,11 +383,7 @@ export const getReactCustomHtmlParser = (
|
|||
}
|
||||
|
||||
if (name === 'pre') {
|
||||
return (
|
||||
<Text {...props} as="pre" className={css.CodeBlock}>
|
||||
{CodeBlock(children, opts)}
|
||||
</Text>
|
||||
);
|
||||
return <CodeBlock opts={opts}>{children}</CodeBlock>;
|
||||
}
|
||||
|
||||
if (name === 'blockquote') {
|
||||
|
|
@ -409,9 +433,9 @@ export const getReactCustomHtmlParser = (
|
|||
}
|
||||
} else {
|
||||
return (
|
||||
<code className={css.Code} {...props}>
|
||||
<Text as="code" size="T300" className={css.Code} {...props}>
|
||||
{domToReact(children, opts)}
|
||||
</code>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { atom } from 'jotai';
|
||||
|
||||
const STORAGE_KEY = 'settings';
|
||||
export type DateFormat = 'D MMM YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD' | '';
|
||||
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
|
||||
export enum MessageLayout {
|
||||
Modern = 0,
|
||||
|
|
@ -35,6 +36,9 @@ export interface Settings {
|
|||
showNotifications: boolean;
|
||||
isNotificationSounds: boolean;
|
||||
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
|
||||
developerTools: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +69,9 @@ const defaultSettings: Settings = {
|
|||
showNotifications: true,
|
||||
isNotificationSounds: true,
|
||||
|
||||
hour24Clock: false,
|
||||
dateFormatString: 'D MMM YYYY',
|
||||
|
||||
developerTools: false,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -41,16 +41,19 @@ export const BlockQuote = style([
|
|||
]);
|
||||
|
||||
const BaseCode = style({
|
||||
fontFamily: 'monospace',
|
||||
color: color.Secondary.OnContainer,
|
||||
background: color.Secondary.Container,
|
||||
border: `${config.borderWidth.B300} solid ${color.Secondary.ContainerLine}`,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
background: color.SurfaceVariant.Container,
|
||||
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderRadius: config.radii.R300,
|
||||
});
|
||||
const CodeFont = style({
|
||||
fontFamily: 'monospace',
|
||||
});
|
||||
|
||||
export const Code = style([
|
||||
DefaultReset,
|
||||
BaseCode,
|
||||
CodeFont,
|
||||
{
|
||||
padding: `0 ${config.space.S100}`,
|
||||
},
|
||||
|
|
@ -86,34 +89,31 @@ export const CodeBlock = style([
|
|||
{
|
||||
fontStyle: 'normal',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
]);
|
||||
export const CodeBlockInternal = recipe({
|
||||
base: {
|
||||
padding: `${config.space.S200} ${config.space.S200} 0`,
|
||||
minWidth: toRem(100),
|
||||
},
|
||||
variants: {
|
||||
collapsed: {
|
||||
true: {
|
||||
maxHeight: `calc(${config.lineHeight.T400} * 9.6)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
export const CodeBlockHeader = style({
|
||||
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
gap: config.space.S200,
|
||||
});
|
||||
export const CodeBlockControls = style({
|
||||
export const CodeBlockInternal = style([
|
||||
CodeFont,
|
||||
{
|
||||
padding: `${config.space.S200} ${config.space.S200} 0`,
|
||||
minWidth: toRem(200),
|
||||
},
|
||||
]);
|
||||
|
||||
export const CodeBlockBottomShadow = style({
|
||||
position: 'absolute',
|
||||
top: config.space.S200,
|
||||
right: config.space.S200,
|
||||
visibility: 'hidden',
|
||||
selectors: {
|
||||
[`${CodeBlock}:hover &`]: {
|
||||
visibility: 'visible',
|
||||
},
|
||||
[`${CodeBlock}:focus-within &`]: {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
pointerEvents: 'none',
|
||||
|
||||
height: config.space.S400,
|
||||
background: `linear-gradient(to top, #00000022, #00000000)`,
|
||||
});
|
||||
|
||||
export const List = style([
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ export const today = (ts: number): boolean => dayjs(ts).isToday();
|
|||
|
||||
export const yesterday = (ts: number): boolean => dayjs(ts).isYesterday();
|
||||
|
||||
export const timeHour = (ts: number): string => dayjs(ts).format('hh');
|
||||
export const timeHour = (ts: number, hour24Clock: boolean): string =>
|
||||
dayjs(ts).format(hour24Clock ? 'HH' : 'hh');
|
||||
export const timeMinute = (ts: number): string => dayjs(ts).format('mm');
|
||||
export const timeAmPm = (ts: number): string => dayjs(ts).format('A');
|
||||
export const timeDay = (ts: number): string => dayjs(ts).format('D');
|
||||
|
|
@ -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 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');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue