mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 14:30:29 +03:00 
			
		
		
		
	add jump to time in room timeline
This commit is contained in:
		
							parent
							
								
									d59ed1119b
								
							
						
					
					
						commit
						d8bcba94c9
					
				
					 3 changed files with 287 additions and 0 deletions
				
			
		| 
						 | 
				
			
			@ -65,6 +65,8 @@ import {
 | 
			
		|||
  getRoomNotificationModeIcon,
 | 
			
		||||
  useRoomsNotificationPreferencesContext,
 | 
			
		||||
} from '../../hooks/useRoomsNotificationPreferences';
 | 
			
		||||
import { JumpToTime } from './jump-to-time';
 | 
			
		||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 | 
			
		||||
 | 
			
		||||
type RoomMenuProps = {
 | 
			
		||||
  room: Room;
 | 
			
		||||
| 
						 | 
				
			
			@ -79,6 +81,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
 | 
			
		|||
  const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
 | 
			
		||||
  const notificationPreferences = useRoomsNotificationPreferencesContext();
 | 
			
		||||
  const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
 | 
			
		||||
  const { navigateRoom } = useRoomNavigate();
 | 
			
		||||
 | 
			
		||||
  const handleMarkAsRead = () => {
 | 
			
		||||
    markAsRead(mx, room.roomId, hideActivity);
 | 
			
		||||
| 
						 | 
				
			
			@ -175,6 +178,33 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
 | 
			
		|||
            Room Settings
 | 
			
		||||
          </Text>
 | 
			
		||||
        </MenuItem>
 | 
			
		||||
        <UseStateProvider initial={false}>
 | 
			
		||||
          {(promptJump, setPromptJump) => (
 | 
			
		||||
            <>
 | 
			
		||||
              <MenuItem
 | 
			
		||||
                onClick={() => setPromptJump(true)}
 | 
			
		||||
                size="300"
 | 
			
		||||
                after={<Icon size="100" src={Icons.RecentClock} />}
 | 
			
		||||
                radii="300"
 | 
			
		||||
                aria-pressed={promptJump}
 | 
			
		||||
              >
 | 
			
		||||
                <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
			
		||||
                  Jump to Time
 | 
			
		||||
                </Text>
 | 
			
		||||
              </MenuItem>
 | 
			
		||||
              {promptJump && (
 | 
			
		||||
                <JumpToTime
 | 
			
		||||
                  onSubmit={(eventId) => {
 | 
			
		||||
                    setPromptJump(false);
 | 
			
		||||
                    navigateRoom(room.roomId, eventId);
 | 
			
		||||
                    requestClose();
 | 
			
		||||
                  }}
 | 
			
		||||
                  onCancel={() => setPromptJump(false)}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        </UseStateProvider>
 | 
			
		||||
      </Box>
 | 
			
		||||
      <Line variant="Surface" size="300" />
 | 
			
		||||
      <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										256
									
								
								src/app/features/room/jump-to-time/JumpToTime.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								src/app/features/room/jump-to-time/JumpToTime.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,256 @@
 | 
			
		|||
import React, { MouseEventHandler, useCallback, useMemo, useState } from 'react';
 | 
			
		||||
import FocusTrap from 'focus-trap-react';
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  Overlay,
 | 
			
		||||
  OverlayCenter,
 | 
			
		||||
  OverlayBackdrop,
 | 
			
		||||
  Header,
 | 
			
		||||
  config,
 | 
			
		||||
  Box,
 | 
			
		||||
  Text,
 | 
			
		||||
  IconButton,
 | 
			
		||||
  Icon,
 | 
			
		||||
  Icons,
 | 
			
		||||
  color,
 | 
			
		||||
  Button,
 | 
			
		||||
  Spinner,
 | 
			
		||||
  Chip,
 | 
			
		||||
  PopOut,
 | 
			
		||||
  RectCords,
 | 
			
		||||
} from 'folds';
 | 
			
		||||
import { Direction, MatrixError } from 'matrix-js-sdk';
 | 
			
		||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
 | 
			
		||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
 | 
			
		||||
import { stopPropagation } from '../../../utils/keyboard';
 | 
			
		||||
import { useAlive } from '../../../hooks/useAlive';
 | 
			
		||||
import { useStateEvent } from '../../../hooks/useStateEvent';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
type JumpToTimeProps = {
 | 
			
		||||
  onCancel: () => void;
 | 
			
		||||
  onSubmit: (eventId: string) => void;
 | 
			
		||||
};
 | 
			
		||||
export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
 | 
			
		||||
  const mx = useMatrixClient();
 | 
			
		||||
  const room = useRoom();
 | 
			
		||||
  const alive = useAlive();
 | 
			
		||||
  const createStateEvent = useStateEvent(room, StateEvent.RoomCreate);
 | 
			
		||||
 | 
			
		||||
  const todayTs = getToday();
 | 
			
		||||
  const yesterdayTs = getYesterday();
 | 
			
		||||
  const createTs = useMemo(() => createStateEvent?.getTs() ?? 0, [createStateEvent]);
 | 
			
		||||
  const [ts, setTs] = useState(() => Date.now());
 | 
			
		||||
 | 
			
		||||
  const [timePickerCords, setTimePickerCords] = useState<RectCords>();
 | 
			
		||||
  const [datePickerCords, setDatePickerCords] = useState<RectCords>();
 | 
			
		||||
 | 
			
		||||
  const handleTimePicker: MouseEventHandler<HTMLButtonElement> = (evt) => {
 | 
			
		||||
    setTimePickerCords(evt.currentTarget.getBoundingClientRect());
 | 
			
		||||
  };
 | 
			
		||||
  const handleDatePicker: MouseEventHandler<HTMLButtonElement> = (evt) => {
 | 
			
		||||
    setDatePickerCords(evt.currentTarget.getBoundingClientRect());
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleToday = () => {
 | 
			
		||||
    setTs(todayTs < createTs ? createTs : todayTs);
 | 
			
		||||
  };
 | 
			
		||||
  const handleYesterday = () => {
 | 
			
		||||
    setTs(yesterdayTs < createTs ? createTs : yesterdayTs);
 | 
			
		||||
  };
 | 
			
		||||
  const handleBeginning = () => setTs(createTs);
 | 
			
		||||
 | 
			
		||||
  const [timestampState, timestampToEvent] = useAsyncCallback<string, MatrixError, [number]>(
 | 
			
		||||
    useCallback(
 | 
			
		||||
      async (newTs) => {
 | 
			
		||||
        const result = await mx.timestampToEvent(room.roomId, newTs, Direction.Forward);
 | 
			
		||||
        return result.event_id;
 | 
			
		||||
      },
 | 
			
		||||
      [mx, room]
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = () => {
 | 
			
		||||
    timestampToEvent(ts).then((eventId) => {
 | 
			
		||||
      if (alive()) {
 | 
			
		||||
        onSubmit(eventId);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Overlay open backdrop={<OverlayBackdrop />}>
 | 
			
		||||
      <OverlayCenter>
 | 
			
		||||
        <FocusTrap
 | 
			
		||||
          focusTrapOptions={{
 | 
			
		||||
            initialFocus: false,
 | 
			
		||||
            onDeactivate: onCancel,
 | 
			
		||||
            clickOutsideDeactivates: true,
 | 
			
		||||
            escapeDeactivates: stopPropagation,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Dialog variant="Surface">
 | 
			
		||||
            <Header
 | 
			
		||||
              style={{
 | 
			
		||||
                padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
 | 
			
		||||
                borderBottomWidth: config.borderWidth.B300,
 | 
			
		||||
              }}
 | 
			
		||||
              variant="Surface"
 | 
			
		||||
              size="500"
 | 
			
		||||
            >
 | 
			
		||||
              <Box grow="Yes">
 | 
			
		||||
                <Text size="H4">Jump to Time</Text>
 | 
			
		||||
              </Box>
 | 
			
		||||
              <IconButton size="300" onClick={onCancel} radii="300">
 | 
			
		||||
                <Icon src={Icons.Cross} />
 | 
			
		||||
              </IconButton>
 | 
			
		||||
            </Header>
 | 
			
		||||
            <Box style={{ padding: config.space.S400 }} direction="Column" gap="500">
 | 
			
		||||
              <Box direction="Row" gap="300">
 | 
			
		||||
                <Box direction="Column" gap="100">
 | 
			
		||||
                  <Text size="L400" priority="400">
 | 
			
		||||
                    Time
 | 
			
		||||
                  </Text>
 | 
			
		||||
                  <Box gap="100" alignItems="Center">
 | 
			
		||||
                    <Chip
 | 
			
		||||
                      size="500"
 | 
			
		||||
                      variant="Surface"
 | 
			
		||||
                      fill="None"
 | 
			
		||||
                      outlined
 | 
			
		||||
                      radii="300"
 | 
			
		||||
                      aria-pressed={!!timePickerCords}
 | 
			
		||||
                      after={<Icon size="50" src={Icons.ChevronBottom} />}
 | 
			
		||||
                      onClick={handleTimePicker}
 | 
			
		||||
                    >
 | 
			
		||||
                      <Text size="B300">{timeHourMinute(ts)}</Text>
 | 
			
		||||
                    </Chip>
 | 
			
		||||
                    <PopOut
 | 
			
		||||
                      anchor={timePickerCords}
 | 
			
		||||
                      offset={5}
 | 
			
		||||
                      position="Bottom"
 | 
			
		||||
                      align="Center"
 | 
			
		||||
                      content={
 | 
			
		||||
                        <FocusTrap
 | 
			
		||||
                          focusTrapOptions={{
 | 
			
		||||
                            initialFocus: false,
 | 
			
		||||
                            onDeactivate: () => setTimePickerCords(undefined),
 | 
			
		||||
                            clickOutsideDeactivates: true,
 | 
			
		||||
                            isKeyForward: (evt: KeyboardEvent) =>
 | 
			
		||||
                              evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
 | 
			
		||||
                            isKeyBackward: (evt: KeyboardEvent) =>
 | 
			
		||||
                              evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
 | 
			
		||||
                            escapeDeactivates: stopPropagation,
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <TimePicker min={createTs} max={Date.now()} value={ts} onChange={setTs} />
 | 
			
		||||
                        </FocusTrap>
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box direction="Column" gap="100">
 | 
			
		||||
                  <Text size="L400" priority="400">
 | 
			
		||||
                    Date
 | 
			
		||||
                  </Text>
 | 
			
		||||
                  <Box gap="100" alignItems="Center">
 | 
			
		||||
                    <Chip
 | 
			
		||||
                      size="500"
 | 
			
		||||
                      variant="Surface"
 | 
			
		||||
                      fill="None"
 | 
			
		||||
                      outlined
 | 
			
		||||
                      radii="300"
 | 
			
		||||
                      aria-pressed={!!datePickerCords}
 | 
			
		||||
                      after={<Icon size="50" src={Icons.ChevronBottom} />}
 | 
			
		||||
                      onClick={handleDatePicker}
 | 
			
		||||
                    >
 | 
			
		||||
                      <Text size="B300">{timeDayMonthYear(ts)}</Text>
 | 
			
		||||
                    </Chip>
 | 
			
		||||
                    <PopOut
 | 
			
		||||
                      anchor={datePickerCords}
 | 
			
		||||
                      offset={5}
 | 
			
		||||
                      position="Bottom"
 | 
			
		||||
                      align="Center"
 | 
			
		||||
                      content={
 | 
			
		||||
                        <FocusTrap
 | 
			
		||||
                          focusTrapOptions={{
 | 
			
		||||
                            initialFocus: false,
 | 
			
		||||
                            onDeactivate: () => setDatePickerCords(undefined),
 | 
			
		||||
                            clickOutsideDeactivates: true,
 | 
			
		||||
                            isKeyForward: (evt: KeyboardEvent) =>
 | 
			
		||||
                              evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
 | 
			
		||||
                            isKeyBackward: (evt: KeyboardEvent) =>
 | 
			
		||||
                              evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
 | 
			
		||||
                            escapeDeactivates: stopPropagation,
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <DatePicker min={createTs} max={Date.now()} value={ts} onChange={setTs} />
 | 
			
		||||
                        </FocusTrap>
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </Box>
 | 
			
		||||
              </Box>
 | 
			
		||||
              <Box direction="Column" gap="100">
 | 
			
		||||
                <Text size="L400">Preset</Text>
 | 
			
		||||
                <Box gap="200">
 | 
			
		||||
                  {createTs < todayTs && (
 | 
			
		||||
                    <Chip
 | 
			
		||||
                      variant={ts === todayTs ? 'Success' : 'SurfaceVariant'}
 | 
			
		||||
                      radii="Pill"
 | 
			
		||||
                      aria-pressed={ts === todayTs}
 | 
			
		||||
                      onClick={handleToday}
 | 
			
		||||
                    >
 | 
			
		||||
                      <Text size="B300">Today</Text>
 | 
			
		||||
                    </Chip>
 | 
			
		||||
                  )}
 | 
			
		||||
                  {createTs < yesterdayTs && (
 | 
			
		||||
                    <Chip
 | 
			
		||||
                      variant={ts === yesterdayTs ? 'Success' : 'SurfaceVariant'}
 | 
			
		||||
                      radii="Pill"
 | 
			
		||||
                      aria-pressed={ts === yesterdayTs}
 | 
			
		||||
                      onClick={handleYesterday}
 | 
			
		||||
                    >
 | 
			
		||||
                      <Text size="B300">Yesterday</Text>
 | 
			
		||||
                    </Chip>
 | 
			
		||||
                  )}
 | 
			
		||||
                  <Chip
 | 
			
		||||
                    variant={ts === createTs ? 'Success' : 'SurfaceVariant'}
 | 
			
		||||
                    radii="Pill"
 | 
			
		||||
                    aria-pressed={ts === createTs}
 | 
			
		||||
                    onClick={handleBeginning}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Text size="B300">Beginning</Text>
 | 
			
		||||
                  </Chip>
 | 
			
		||||
                </Box>
 | 
			
		||||
              </Box>
 | 
			
		||||
              {timestampState.status === AsyncStatus.Error && (
 | 
			
		||||
                <Text style={{ color: color.Critical.Main }} size="T300">
 | 
			
		||||
                  {timestampState.error.message}
 | 
			
		||||
                </Text>
 | 
			
		||||
              )}
 | 
			
		||||
              <Button
 | 
			
		||||
                type="submit"
 | 
			
		||||
                variant="Primary"
 | 
			
		||||
                before={
 | 
			
		||||
                  timestampState.status === AsyncStatus.Loading ? (
 | 
			
		||||
                    <Spinner fill="Solid" variant="Primary" size="200" />
 | 
			
		||||
                  ) : undefined
 | 
			
		||||
                }
 | 
			
		||||
                aria-disabled={
 | 
			
		||||
                  timestampState.status === AsyncStatus.Loading ||
 | 
			
		||||
                  timestampState.status === AsyncStatus.Success
 | 
			
		||||
                }
 | 
			
		||||
                onClick={handleSubmit}
 | 
			
		||||
              >
 | 
			
		||||
                <Text size="B400">Open Timeline</Text>
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Dialog>
 | 
			
		||||
        </FocusTrap>
 | 
			
		||||
      </OverlayCenter>
 | 
			
		||||
    </Overlay>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/app/features/room/jump-to-time/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app/features/room/jump-to-time/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export * from './JumpToTime';
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue