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,28 +63,43 @@ 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())
 | 
			
		||||
              .map((i) => {
 | 
			
		||||
                if (i === 0) return 12;
 | 
			
		||||
                return i;
 | 
			
		||||
              })
 | 
			
		||||
              .map((hour) => (
 | 
			
		||||
                <Chip
 | 
			
		||||
                  key={hour}
 | 
			
		||||
                  size="500"
 | 
			
		||||
                  variant={hour === selectedHour ? 'Primary' : 'Background'}
 | 
			
		||||
                  fill="None"
 | 
			
		||||
                  radii="300"
 | 
			
		||||
                  aria-selected={hour === selectedHour}
 | 
			
		||||
                  onClick={() => handleHour(hour)}
 | 
			
		||||
                  disabled={
 | 
			
		||||
                    (minDay && hour12to24(hour, selectedPM) < minHour24) ||
 | 
			
		||||
                    (maxDay && hour12to24(hour, selectedPM) > maxHour24)
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
                  <Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
 | 
			
		||||
                </Chip>
 | 
			
		||||
              ))}
 | 
			
		||||
            {hour24Clock
 | 
			
		||||
              ? Array.from(Array(24).keys()).map((hour) => (
 | 
			
		||||
                  <Chip
 | 
			
		||||
                    key={hour}
 | 
			
		||||
                    size="500"
 | 
			
		||||
                    variant={hour === selectedHour ? 'Primary' : 'Background'}
 | 
			
		||||
                    fill="None"
 | 
			
		||||
                    radii="300"
 | 
			
		||||
                    aria-selected={hour === selectedHour}
 | 
			
		||||
                    onClick={() => handleHour(hour)}
 | 
			
		||||
                    disabled={(minDay && hour < minHour24) || (maxDay && hour > maxHour24)}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
 | 
			
		||||
                  </Chip>
 | 
			
		||||
                ))
 | 
			
		||||
              : Array.from(Array(12).keys())
 | 
			
		||||
                  .map((i) => {
 | 
			
		||||
                    if (i === 0) return 12;
 | 
			
		||||
                    return i;
 | 
			
		||||
                  })
 | 
			
		||||
                  .map((hour) => (
 | 
			
		||||
                    <Chip
 | 
			
		||||
                      key={hour}
 | 
			
		||||
                      size="500"
 | 
			
		||||
                      variant={hour === selectedHour ? 'Primary' : 'Background'}
 | 
			
		||||
                      fill="None"
 | 
			
		||||
                      radii="300"
 | 
			
		||||
                      aria-selected={hour === selectedHour}
 | 
			
		||||
                      onClick={() => handleHour(hour)}
 | 
			
		||||
                      disabled={
 | 
			
		||||
                        (minDay && hour12to24(hour, selectedPM) < minHour24) ||
 | 
			
		||||
                        (maxDay && hour12to24(hour, selectedPM) > maxHour24)
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      <Text size="T300">{hour < 10 ? `0${hour}` : hour}</Text>
 | 
			
		||||
                    </Chip>
 | 
			
		||||
                  ))}
 | 
			
		||||
          </PickerColumn>
 | 
			
		||||
          <PickerColumn title="Minutes">
 | 
			
		||||
            {Array.from(Array(60).keys()).map((minute) => (
 | 
			
		||||
| 
						 | 
				
			
			@ -101,30 +120,32 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
 | 
			
		|||
              </Chip>
 | 
			
		||||
            ))}
 | 
			
		||||
          </PickerColumn>
 | 
			
		||||
          <PickerColumn title="Period">
 | 
			
		||||
            <Chip
 | 
			
		||||
              size="500"
 | 
			
		||||
              variant={!selectedPM ? 'Primary' : 'SurfaceVariant'}
 | 
			
		||||
              fill="None"
 | 
			
		||||
              radii="300"
 | 
			
		||||
              aria-selected={!selectedPM}
 | 
			
		||||
              onClick={() => handlePeriod(false)}
 | 
			
		||||
              disabled={minDay && minPM}
 | 
			
		||||
            >
 | 
			
		||||
              <Text size="T300">AM</Text>
 | 
			
		||||
            </Chip>
 | 
			
		||||
            <Chip
 | 
			
		||||
              size="500"
 | 
			
		||||
              variant={selectedPM ? 'Primary' : 'SurfaceVariant'}
 | 
			
		||||
              fill="None"
 | 
			
		||||
              radii="300"
 | 
			
		||||
              aria-selected={selectedPM}
 | 
			
		||||
              onClick={() => handlePeriod(true)}
 | 
			
		||||
              disabled={maxDay && !maxPM}
 | 
			
		||||
            >
 | 
			
		||||
              <Text size="T300">PM</Text>
 | 
			
		||||
            </Chip>
 | 
			
		||||
          </PickerColumn>
 | 
			
		||||
          {!hour24Clock && (
 | 
			
		||||
            <PickerColumn title="Period">
 | 
			
		||||
              <Chip
 | 
			
		||||
                size="500"
 | 
			
		||||
                variant={!selectedPM ? 'Primary' : 'SurfaceVariant'}
 | 
			
		||||
                fill="None"
 | 
			
		||||
                radii="300"
 | 
			
		||||
                aria-selected={!selectedPM}
 | 
			
		||||
                onClick={() => handlePeriod(false)}
 | 
			
		||||
                disabled={minDay && minPM}
 | 
			
		||||
              >
 | 
			
		||||
                <Text size="T300">AM</Text>
 | 
			
		||||
              </Chip>
 | 
			
		||||
              <Chip
 | 
			
		||||
                size="500"
 | 
			
		||||
                variant={selectedPM ? 'Primary' : 'SurfaceVariant'}
 | 
			
		||||
                fill="None"
 | 
			
		||||
                radii="300"
 | 
			
		||||
                aria-selected={selectedPM}
 | 
			
		||||
                onClick={() => handlePeriod(true)}
 | 
			
		||||
                disabled={maxDay && !maxPM}
 | 
			
		||||
              >
 | 
			
		||||
                <Text size="T300">PM</Text>
 | 
			
		||||
              </Chip>
 | 
			
		||||
            </PickerColumn>
 | 
			
		||||
          )}
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Menu>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,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;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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(
 | 
			
		||||
  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"
 | 
			
		||||
          onClick={handleCopy}
 | 
			
		||||
          aria-label="Copy Code Block"
 | 
			
		||||
        >
 | 
			
		||||
          <Icon src={copied ? Icons.Check : Icons.File} size="50" />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
        {collapsible && (
 | 
			
		||||
          <IconButton
 | 
			
		||||
            variant="Secondary"
 | 
			
		||||
            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' } : {}}
 | 
			
		||||
    <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}
 | 
			
		||||
            before={copied && <Icon size="50" src={Icons.Check} />}
 | 
			
		||||
          >
 | 
			
		||||
            <Icon src={collapsed ? Icons.ChevronBottom : Icons.ChevronTop} size="50" />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
      <Scroll direction="Both" variant="Secondary" size="300" visibility="Hover" hideTrack>
 | 
			
		||||
        <div id="code-block-content" className={css.CodeBlockInternal({ collapsed })}>
 | 
			
		||||
            <Text size="B300">{copied ? 'Copied' : 'Copy'}</Text>
 | 
			
		||||
          </Chip>
 | 
			
		||||
          {largeCodeBlock && (
 | 
			
		||||
            <IconButton
 | 
			
		||||
              size="300"
 | 
			
		||||
              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)}
 | 
			
		||||
        </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({
 | 
			
		||||
  position: 'absolute',
 | 
			
		||||
  top: config.space.S200,
 | 
			
		||||
  right: config.space.S200,
 | 
			
		||||
  visibility: 'hidden',
 | 
			
		||||
  selectors: {
 | 
			
		||||
    [`${CodeBlock}:hover &`]: {
 | 
			
		||||
      visibility: 'visible',
 | 
			
		||||
    },
 | 
			
		||||
    [`${CodeBlock}:focus-within &`]: {
 | 
			
		||||
      visibility: 'visible',
 | 
			
		||||
    },
 | 
			
		||||
export const CodeBlockInternal = style([
 | 
			
		||||
  CodeFont,
 | 
			
		||||
  {
 | 
			
		||||
    padding: `${config.space.S200} ${config.space.S200} 0`,
 | 
			
		||||
    minWidth: toRem(200),
 | 
			
		||||
  },
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
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([
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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