import FocusTrap from 'focus-trap-react'; import { Avatar, Box, Button, config, Header, Icon, IconButton, Icons, Input, Menu, MenuItem, Modal, Overlay, OverlayBackdrop, OverlayCenter, Scroll, Spinner, Text, } from 'folds'; import React, { ChangeEventHandler, MouseEventHandler, useCallback, useMemo, useRef, useState, } from 'react'; import { useAtomValue } from 'jotai'; import { useVirtualizer } from '@tanstack/react-virtual'; import { Room } from 'matrix-js-sdk'; import { stopPropagation } from '../../utils/keyboard'; import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { allRoomsAtom } from '../../state/room-list/roomList'; import { mDirectAtom } from '../../state/mDirectList'; import { roomToParentsAtom } from '../../state/room/roomToParents'; import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom'; import { VirtualTile } from '../../components/virtualizer'; import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { nameInitials } from '../../utils/common'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { factoryRoomIdByAtoZ } from '../../utils/sort'; import { SearchItemStrGetter, useAsyncSearch, UseAsyncSearchOptions, } from '../../hooks/useAsyncSearch'; import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { StateEvent } from '../../../types/matrix/room'; import { getViaServers } from '../../plugins/via-servers'; import { rateLimitedActions } from '../../utils/matrix'; import { useAlive } from '../../hooks/useAlive'; const SEARCH_OPTS: UseAsyncSearchOptions = { limit: 500, matchOptions: { contain: true, }, normalizeOptions: { ignoreWhitespace: false, }, }; type AddExistingModalProps = { parentId: string; space?: boolean; requestClose: () => void; }; export function AddExistingModal({ parentId, space, requestClose }: AddExistingModalProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const alive = useAlive(); const mDirects = useAtomValue(mDirectAtom); const spaces = useSpaces(mx, allRoomsAtom); const rooms = useRooms(mx, allRoomsAtom, mDirects); const directs = useDirects(mx, allRoomsAtom, mDirects); const roomIdToParents = useAtomValue(roomToParentsAtom); const scrollRef = useRef(null); const [selected, setSelected] = useState([]); const allRoomsSet = useAllJoinedRoomsSet(); const getRoom = useGetRoom(allRoomsSet); const allItems: string[] = useMemo(() => { const rIds = space ? [...spaces] : [...rooms, ...directs]; return rIds .filter((rId) => rId !== parentId && !roomIdToParents.get(rId)?.has(parentId)) .sort(factoryRoomIdByAtoZ(mx)); }, [spaces, rooms, directs, space, parentId, roomIdToParents, mx]); const getRoomNameStr: SearchItemStrGetter = useCallback( (rId) => getRoom(rId)?.name ?? rId, [getRoom] ); const [searchResult, searchRoom, resetSearch] = useAsyncSearch( allItems, getRoomNameStr, SEARCH_OPTS ); const queryHighlighRegex = searchResult?.query ? makeHighlightRegex(searchResult.query.split(' ')) : undefined; const items = searchResult ? searchResult.items : allItems; const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => scrollRef.current, estimateSize: () => 32, overscan: 5, }); const vItems = virtualizer.getVirtualItems(); const handleSearchChange: ChangeEventHandler = (evt) => { const value = evt.currentTarget.value.trim(); if (!value) { resetSearch(); return; } searchRoom(value); }; const [applyState, applyChanges] = useAsyncCallback( useCallback( async (selectedRooms) => { await rateLimitedActions(selectedRooms, async (room) => { const via = getViaServers(room); await mx.sendStateEvent( parentId, StateEvent.SpaceChild as any, { auto_join: false, suggested: false, via, }, room.roomId ); }); }, [mx, parentId] ) ); const applyingChanges = applyState.status === AsyncStatus.Loading; const handleRoomClick: MouseEventHandler = (evt) => { const roomId = evt.currentTarget.getAttribute('data-room-id'); if (!roomId) return; if (selected?.includes(roomId)) { setSelected(selected?.filter((rId) => rId !== roomId)); return; } const addedRooms = [...(selected ?? [])]; addedRooms.push(roomId); setSelected(addedRooms); }; const handleApplyChanges = () => { const selectedRooms = selected.map((rId) => getRoom(rId)).filter((room) => room !== undefined); applyChanges(selectedRooms).then(() => { if (alive()) { setSelected([]); requestClose(); } }); }; const resetChanges = () => { setSelected([]); }; return ( }>
Add Existing
} placeholder="Search" size="400" variant="Background" outlined /> {vItems.length === 0 && ( {searchResult ? 'No Match Found' : `No ${space ? 'Spaces' : 'Rooms'}`} {searchResult ? `No match found for "${searchResult.query}".` : `You do not have any ${space ? 'Spaces' : 'Rooms'} to display yet.`} )} {vItems.map((vItem) => { const roomId = items[vItem.index]; const room = getRoom(roomId); if (!room) return null; const selectedItem = selected?.includes(roomId); const dm = mDirects.has(room.roomId); return ( {dm || room.isSpaceRoom() ? ( ( {nameInitials(room.name)} )} /> ) : ( )} } after={selectedItem && } > {queryHighlighRegex ? highlightText(queryHighlighRegex, [room.name]) : room.name} ); })} {selected.length > 0 && ( {applyState.status === AsyncStatus.Error ? ( Failed to apply changes! Please try again. ) : ( Apply when ready. ({selected.length} Selected) )} )}
); }