add jump to time in room timeline

This commit is contained in:
Ajay Bura 2025-06-26 21:37:17 +05:30
parent d59ed1119b
commit d8bcba94c9
3 changed files with 287 additions and 0 deletions

View file

@ -65,6 +65,8 @@ import {
getRoomNotificationModeIcon, getRoomNotificationModeIcon,
useRoomsNotificationPreferencesContext, useRoomsNotificationPreferencesContext,
} from '../../hooks/useRoomsNotificationPreferences'; } from '../../hooks/useRoomsNotificationPreferences';
import { JumpToTime } from './jump-to-time';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
type RoomMenuProps = { type RoomMenuProps = {
room: Room; room: Room;
@ -79,6 +81,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const notificationPreferences = useRoomsNotificationPreferencesContext(); const notificationPreferences = useRoomsNotificationPreferencesContext();
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId); const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
const { navigateRoom } = useRoomNavigate();
const handleMarkAsRead = () => { const handleMarkAsRead = () => {
markAsRead(mx, room.roomId, hideActivity); markAsRead(mx, room.roomId, hideActivity);
@ -175,6 +178,33 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
Room Settings Room Settings
</Text> </Text>
</MenuItem> </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> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>

View 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>
);
}

View file

@ -0,0 +1 @@
export * from './JumpToTime';