import { produce } from 'immer'; import { atom, useSetAtom } from 'jotai'; import { ClientEvent, MatrixClient, MatrixEvent, Room, RoomEvent, RoomStateEvent, } from 'matrix-js-sdk'; import { useEffect } from 'react'; import { Membership, RoomToParents, StateEvent } from '../../../types/matrix/room'; import { getRoomToParents, getSpaceChildren, isSpace, isValidChild, mapParentWithChildren, } from '../../utils/room'; export type RoomToParentsAction = | { type: 'INITIALIZE'; roomToParents: RoomToParents; } | { type: 'PUT'; parent: string; children: string[]; } | { type: 'DELETE'; roomId: string; } | { type: 'UNLINK'; parent: string; child: string; }; const baseRoomToParents = atom(new Map()); export const roomToParentsAtom = atom( (get) => get(baseRoomToParents), (get, set, action) => { if (action.type === 'INITIALIZE') { set(baseRoomToParents, action.roomToParents); return; } if (action.type === 'PUT') { set( baseRoomToParents, produce(get(baseRoomToParents), (draftRoomToParents) => { mapParentWithChildren(draftRoomToParents, action.parent, action.children); }), ); return; } if (action.type === 'DELETE') { set( baseRoomToParents, produce(get(baseRoomToParents), (draftRoomToParents) => { const noParentRooms: string[] = []; draftRoomToParents.delete(action.roomId); draftRoomToParents.forEach((parents, child) => { parents.delete(action.roomId); if (parents.size === 0) noParentRooms.push(child); }); noParentRooms.forEach((room) => draftRoomToParents.delete(room)); }), ); return; } if (action.type === 'UNLINK') { // Remove ONE parent from ONE child (a single `m.space.child` was removed). // Unlike DELETE (used when a room is left/deleted), this must NOT touch the // child's other parents, nor strip the child as a parent elsewhere, nor // orphan the child's own descendants. Only prune the child's entry if this // was its last parent. set( baseRoomToParents, produce(get(baseRoomToParents), (draftRoomToParents) => { const parents = draftRoomToParents.get(action.child); if (!parents) return; parents.delete(action.parent); if (parents.size === 0) draftRoomToParents.delete(action.child); }), ); } }, ); export const useBindRoomToParentsAtom = ( mx: MatrixClient, roomToParents: typeof roomToParentsAtom, ) => { const setRoomToParents = useSetAtom(roomToParents); useEffect(() => { setRoomToParents({ type: 'INITIALIZE', roomToParents: getRoomToParents(mx) }); const handleAddRoom = (room: Room) => { if (isSpace(room) && room.getMyMembership() !== Membership.Invite) { setRoomToParents({ type: 'PUT', parent: room.roomId, children: getSpaceChildren(room) }); } }; const handleMembershipChange = (room: Room, membership: string) => { if (isSpace(room) && room.getMyMembership() === Membership.Leave) { setRoomToParents({ type: 'DELETE', roomId: room.roomId }); return; } if (isSpace(room) && membership === Membership.Join) { setRoomToParents({ type: 'PUT', parent: room.roomId, children: getSpaceChildren(room) }); } }; const handleStateChange = (mEvent: MatrixEvent) => { if (mEvent.getType() === StateEvent.SpaceChild) { const childId = mEvent.getStateKey(); const roomId = mEvent.getRoomId(); if (childId && roomId) { if (isValidChild(mEvent)) { setRoomToParents({ type: 'PUT', parent: roomId, children: [childId] }); } else { // A single m.space.child was removed: unlink only THIS parent→child // edge. DELETE here wiped the child's other parents and orphaned its // descendants until a full resync (COR-1). setRoomToParents({ type: 'UNLINK', parent: roomId, child: childId }); } } } }; const handleDeleteRoom = (roomId: string) => { setRoomToParents({ type: 'DELETE', roomId }); }; mx.on(ClientEvent.Room, handleAddRoom); mx.on(RoomEvent.MyMembership, handleMembershipChange); mx.on(RoomStateEvent.Events, handleStateChange); mx.on(ClientEvent.DeleteRoom, handleDeleteRoom); return () => { mx.removeListener(ClientEvent.Room, handleAddRoom); mx.removeListener(RoomEvent.MyMembership, handleMembershipChange); mx.removeListener(RoomStateEvent.Events, handleStateChange); mx.removeListener(ClientEvent.DeleteRoom, handleDeleteRoom); }; }, [mx, setRoomToParents]); };