diff --git a/src/app/components/create-room/AdditionalCreatorInput.tsx b/src/app/components/create-room/AdditionalCreatorInput.tsx index 51334b49..936b9b94 100644 --- a/src/app/components/create-room/AdditionalCreatorInput.tsx +++ b/src/app/components/create-room/AdditionalCreatorInput.tsx @@ -30,9 +30,7 @@ import { SettingTile } from '../setting-tile'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { stopPropagation } from '../../utils/keyboard'; import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; -import { findAndReplace } from '../../utils/findAndReplace'; -import { highlightText } from '../../styles/CustomHtml.css'; -import { makeHighlightRegex } from '../../plugins/react-custom-html-parser'; +import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser'; export const useAdditionalCreators = (defaultCreators?: string[]) => { const mx = useMatrixClient(); @@ -245,19 +243,9 @@ export function AdditionalCreatorInput({ {queryHighlighRegex - ? findAndReplace( + ? highlightText(queryHighlighRegex, [ getMxIdLocalPart(userId) ?? userId, - queryHighlighRegex, - (match, pushIndex) => ( - - {match[0]} - - ), - (txt) => txt - ) + ]) : getMxIdLocalPart(userId)} diff --git a/src/app/features/add-existing/AddExisting.tsx b/src/app/features/add-existing/AddExisting.tsx new file mode 100644 index 00000000..cbae018f --- /dev/null +++ b/src/app/features/add-existing/AddExisting.tsx @@ -0,0 +1,375 @@ +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) + + )} + + + + + + + + )} + + + +
+
+
+
+
+ ); +} diff --git a/src/app/features/add-existing/index.ts b/src/app/features/add-existing/index.ts new file mode 100644 index 00000000..3607c062 --- /dev/null +++ b/src/app/features/add-existing/index.ts @@ -0,0 +1 @@ +export * from './AddExisting'; diff --git a/src/app/features/create-room/CreateRoomModal.tsx b/src/app/features/create-room/CreateRoomModal.tsx index c1c9ba3e..c9919ba9 100644 --- a/src/app/features/create-room/CreateRoomModal.tsx +++ b/src/app/features/create-room/CreateRoomModal.tsx @@ -54,7 +54,6 @@ function CreateRoomModal({ state }: CreateRoomModalProps) { style={{ padding: config.space.S200, paddingLeft: config.space.S400, - borderBottomWidth: config.borderWidth.B300, }} > diff --git a/src/app/features/lobby/SpaceItem.tsx b/src/app/features/lobby/SpaceItem.tsx index e881a971..64a97900 100644 --- a/src/app/features/lobby/SpaceItem.tsx +++ b/src/app/features/lobby/SpaceItem.tsx @@ -30,12 +30,12 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import * as css from './SpaceItem.css'; import * as styleCss from './style.css'; import { useDraggableItem } from './DnD'; -import { openSpaceAddExisting } from '../../../client/action/navigation'; import { stopPropagation } from '../../utils/keyboard'; import { mxcUrlToHttp } from '../../utils/matrix'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal'; import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal'; +import { AddExistingModal } from '../add-existing'; function SpaceProfileLoading() { return ( @@ -243,6 +243,7 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP function AddRoomButton({ item }: { item: HierarchyItem }) { const [cords, setCords] = useState(); const openCreateRoomModal = useOpenCreateRoomModal(); + const [addExisting, setAddExisting] = useState(false); const handleAddRoom: MouseEventHandler = (evt) => { setCords(evt.currentTarget.getBoundingClientRect()); @@ -254,7 +255,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) { }; const handleAddExisting = () => { - openSpaceAddExisting(item.roomId); + setAddExisting(true); setCords(undefined); }; @@ -300,6 +301,9 @@ function AddRoomButton({ item }: { item: HierarchyItem }) { > Add Room + {addExisting && ( + setAddExisting(false)} /> + )} ); } @@ -307,6 +311,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) { function AddSpaceButton({ item }: { item: HierarchyItem }) { const [cords, setCords] = useState(); const openCreateSpaceModal = useOpenCreateSpaceModal(); + const [addExisting, setAddExisting] = useState(false); const handleAddSpace: MouseEventHandler = (evt) => { setCords(evt.currentTarget.getBoundingClientRect()); @@ -318,7 +323,7 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) { }; const handleAddExisting = () => { - openSpaceAddExisting(item.roomId, true); + setAddExisting(true); setCords(undefined); }; return ( @@ -363,6 +368,9 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) { > Add Space + {addExisting && ( + setAddExisting(false)} /> + )} ); }