Files
cinny/src/app/state/room/roomToParents.ts
T

150 lines
4.8 KiB
TypeScript
Raw Normal View History

import { produce } from 'immer';
2024-01-21 23:50:56 +11:00
import { atom, useSetAtom } from 'jotai';
2023-06-12 21:15:23 +10:00
import {
ClientEvent,
MatrixClient,
MatrixEvent,
Room,
RoomEvent,
RoomStateEvent,
} from 'matrix-js-sdk';
import { useEffect } from 'react';
import { Membership, RoomToParents, StateEvent } from '../../../types/matrix/room';
2023-06-12 21:15:23 +10:00
import {
getRoomToParents,
getSpaceChildren,
isSpace,
isValidChild,
mapParentWithChildren,
} from '../../utils/room';
2023-06-12 21:15:23 +10:00
export type RoomToParentsAction =
| {
type: 'INITIALIZE';
roomToParents: RoomToParents;
}
| {
type: 'PUT';
parent: string;
children: string[];
}
| {
type: 'DELETE';
roomId: string;
}
| {
type: 'UNLINK';
parent: string;
child: string;
2023-06-12 21:15:23 +10:00
};
const baseRoomToParents = atom<RoomToParents>(new Map());
2024-01-21 23:50:56 +11:00
export const roomToParentsAtom = atom<RoomToParents, [RoomToParentsAction], undefined>(
2023-06-12 21:15:23 +10:00
(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);
}),
2023-06-12 21:15:23 +10:00
);
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));
}),
2023-06-12 21:15:23 +10:00
);
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);
}),
);
2023-06-12 21:15:23 +10:00
}
},
2023-06-12 21:15:23 +10:00
);
export const useBindRoomToParentsAtom = (
mx: MatrixClient,
roomToParents: typeof roomToParentsAtom,
2023-06-12 21:15:23 +10:00
) => {
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;
}
2023-06-12 21:15:23 +10:00
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 });
2023-06-12 21:15:23 +10:00
}
}
}
};
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]);
};