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 = ( items: T[] ): React.RefCallback => { const positionsRef = useRef>(new Map()); const containerRef = useRef(null); useEffect(() => { if (!containerRef.current) return; const currentPositions = new Map(); const elements = new Map(); 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; }; };