111 lines
3.4 KiB
TypeScript
111 lines
3.4 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { MatrixClient, MatrixEvent, Room, RoomEvent, IRoomTimelineData } from 'matrix-js-sdk';
|
|
|
|
/**
|
|
* Hook that tracks timeline events across all rooms to trigger re-sorting of room lists.
|
|
* Returns a counter that increments whenever a message is sent or received in any room.
|
|
* This can be used as a dependency in useMemo to trigger re-sorting of room lists.
|
|
*
|
|
* @param mx - The Matrix client instance
|
|
* @param roomIds - Optional array of room IDs to track. If not provided, tracks all rooms.
|
|
* @returns A number that increments on each timeline event
|
|
*/
|
|
export const useRoomListReorder = (mx: MatrixClient, roomIds?: string[]): number => {
|
|
const [updateCounter, setUpdateCounter] = useState(0);
|
|
|
|
useEffect(() => {
|
|
const handleTimelineEvent = (
|
|
mEvent: MatrixEvent,
|
|
room: Room | undefined,
|
|
toStartOfTimeline: boolean | undefined,
|
|
removed: boolean,
|
|
data: IRoomTimelineData
|
|
) => {
|
|
if (!room || !data.liveEvent) return;
|
|
if (toStartOfTimeline) return;
|
|
|
|
if (roomIds && !roomIds.includes(room.roomId)) return;
|
|
|
|
setUpdateCounter((prev) => prev + 1);
|
|
};
|
|
|
|
mx.on(RoomEvent.Timeline, handleTimelineEvent);
|
|
|
|
return () => {
|
|
mx.removeListener(RoomEvent.Timeline, handleTimelineEvent);
|
|
};
|
|
}, [mx, roomIds]);
|
|
|
|
return updateCounter;
|
|
};
|
|
|
|
/**
|
|
* Hook that implements FLIP (First, Last, Invert, Play) animation for list reordering.
|
|
* Tracks element positions before and after reorder, then animates the difference.
|
|
*
|
|
* @param items - Array of item IDs in their current order
|
|
* @returns Ref callback to attach to the list container
|
|
*/
|
|
export const useListReorderAnimation = <T extends string>(
|
|
items: T[]
|
|
): React.RefCallback<HTMLElement> => {
|
|
const positionsRef = useRef<Map<T, number>>(new Map());
|
|
const containerRef = useRef<HTMLElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
const currentPositions = new Map<T, number>();
|
|
const elements = new Map<T, HTMLElement>();
|
|
|
|
items.forEach((itemId) => {
|
|
const element = containerRef.current!.querySelector(`[data-room-id="${itemId}"]`) as HTMLElement;
|
|
if (element) {
|
|
currentPositions.set(itemId, element.offsetTop);
|
|
elements.set(itemId, element);
|
|
}
|
|
});
|
|
|
|
if (positionsRef.current.size === 0) {
|
|
positionsRef.current = currentPositions;
|
|
return;
|
|
}
|
|
|
|
const animations: Array<{ element: HTMLElement; deltaY: number }> = [];
|
|
|
|
items.forEach((itemId) => {
|
|
const element = elements.get(itemId);
|
|
const oldPos = positionsRef.current.get(itemId);
|
|
const newPos = currentPositions.get(itemId);
|
|
|
|
if (element && oldPos !== undefined && newPos !== undefined) {
|
|
const deltaY = oldPos - newPos;
|
|
|
|
if (Math.abs(deltaY) > 1) {
|
|
animations.push({ element, deltaY });
|
|
}
|
|
}
|
|
});
|
|
|
|
if (animations.length > 0) {
|
|
animations.forEach(({ element, deltaY }) => {
|
|
element.style.transform = `translateY(${deltaY}px)`;
|
|
element.style.transition = 'none';
|
|
});
|
|
|
|
requestAnimationFrame(() => {
|
|
animations.forEach(({ element }) => {
|
|
element.style.transition = 'transform 300ms cubic-bezier(0.4, 0, 0.2, 1)';
|
|
element.style.transform = '';
|
|
});
|
|
});
|
|
}
|
|
|
|
positionsRef.current = currentPositions;
|
|
}, [items]);
|
|
|
|
return (container: HTMLElement | null) => {
|
|
containerRef.current = container;
|
|
};
|
|
};
|