/* Copyright 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import React, { ReactNode, useContext, useMemo, useState } from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; import { sleep } from "matrix-js-sdk/src/utils"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t } from '../../../languageHandler'; import BaseDialog from "./BaseDialog"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; import SpaceStore from "../../../stores/SpaceStore"; import RoomAvatar from "../avatars/RoomAvatar"; import { getDisplayAliasForRoom } from "../../../Rooms"; import AccessibleButton from "../elements/AccessibleButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import DMRoomMap from "../../../utils/DMRoomMap"; import { calculateRoomVia } from "../../../utils/permalinks/Permalinks"; import StyledCheckbox from "../elements/StyledCheckbox"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import ProgressBar from "../elements/ProgressBar"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; import TruncatedList from "../elements/TruncatedList"; import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; interface IProps { space: Room; onCreateRoomClick(): void; onAddSubspaceClick(): void; onFinished(added?: boolean): void; } export const Entry = ({ room, checked, onChange }) => { return ; }; interface IAddExistingToSpaceProps { space: Room; footerPrompt?: ReactNode; filterPlaceholder: string; emptySelectionButton?: ReactNode; onFinished(added: boolean): void; roomsRenderer?( rooms: Room[], selectedToAdd: Set, onChange: undefined | ((checked: boolean, room: Room) => void), truncateAt: number, overflowTile: (overflowCount: number, totalCount: number) => JSX.Element, ): ReactNode; spacesRenderer?( spaces: Room[], selectedToAdd: Set, onChange?: (checked: boolean, room: Room) => void, ): ReactNode; dmsRenderer?( dms: Room[], selectedToAdd: Set, onChange?: (checked: boolean, room: Room) => void, ): ReactNode; } export const AddExistingToSpace: React.FC = ({ space, footerPrompt, emptySelectionButton, filterPlaceholder, roomsRenderer, dmsRenderer, spacesRenderer, onFinished, }) => { const cli = useContext(MatrixClientContext); const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]); const [selectedToAdd, setSelectedToAdd] = useState(new Set()); const [progress, setProgress] = useState(null); const [error, setError] = useState(null); const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase().trim(); const existingSubspacesSet = useMemo(() => new Set(SpaceStore.instance.getChildSpaces(space.roomId)), [space]); const existingRoomsSet = useMemo(() => new Set(SpaceStore.instance.getChildRooms(space.roomId)), [space]); const [spaces, rooms, dms] = useMemo(() => { let rooms = visibleRooms; if (lcQuery) { const matcher = new QueryMatcher(visibleRooms, { keys: ["name"], funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)], shouldMatchWordsOnly: false, }); rooms = matcher.match(lcQuery); } const joinRule = space.getJoinRule(); return sortRooms(rooms).reduce((arr, room) => { if (room.isSpaceRoom()) { if (room !== space && !existingSubspacesSet.has(room)) { arr[0].push(room); } } else if (!existingRoomsSet.has(room)) { if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { arr[1].push(room); } else if (joinRule !== "public") { // Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones. arr[2].push(room); } } return arr; }, [[], [], []]); }, [visibleRooms, space, lcQuery, existingRoomsSet, existingSubspacesSet]); const addRooms = async () => { setError(null); setProgress(0); let error; for (const room of selectedToAdd) { const via = calculateRoomVia(room); try { await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { if (e.errcode === "M_LIMIT_EXCEEDED") { await sleep(e.data.retry_after_ms); return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry } throw e; }); setProgress(i => i + 1); } catch (e) { console.error("Failed to add rooms to space", e); setError(error = e); break; } } if (!error) { onFinished(true); } }; const busy = progress !== null; let footer; if (error) { footer = <>
{ _t("Not all selected were added") }
{ _t("Try again") }
{ _t("Retry") } ; } else if (busy) { footer =
{ _t("Adding rooms... (%(progress)s out of %(count)s)", { count: selectedToAdd.size, progress, }) }
; } else { let button = emptySelectionButton; if (!button || selectedToAdd.size > 0) { button = { _t("Add") } ; } footer = <> { footerPrompt } { button } ; } const onChange = !busy && !error ? (checked: boolean, room: Room) => { if (checked) { selectedToAdd.add(room); } else { selectedToAdd.delete(room); } setSelectedToAdd(new Set(selectedToAdd)); } : null; const [truncateAt, setTruncateAt] = useState(20); function overflowTile(overflowCount: number, totalCount: number): JSX.Element { const text = _t("and %(count)s others...", { count: overflowCount }); return ( } name={text} presenceState="online" suppressOnHover={true} onClick={() => setTruncateAt(totalCount)} /> ); } let noResults = true; if ((roomsRenderer && rooms.length > 0) || (dmsRenderer && dms.length > 0) || (!roomsRenderer && !dmsRenderer && spacesRenderer && spaces.length > 0) // only count spaces when alone ) { noResults = false; } return
{ rooms.length > 0 && roomsRenderer ? ( roomsRenderer(rooms, selectedToAdd, onChange, truncateAt, overflowTile) ) : undefined } { spaces.length > 0 && spacesRenderer ? ( spacesRenderer(spaces, selectedToAdd, onChange) ) : null } { dms.length > 0 && dmsRenderer ? ( dmsRenderer(dms, selectedToAdd, onChange) ) : null } { noResults ? { _t("No results") } : undefined }
{ footer }
; }; export const defaultRoomsRenderer: IAddExistingToSpaceProps["roomsRenderer"] = ( rooms, selectedToAdd, onChange, truncateAt, overflowTile, ) => (

{ _t("Rooms") }

rooms.slice(start, end).map(room => { onChange(checked, room); } : null} />, )} getChildCount={() => rooms.length} />
); export const defaultSpacesRenderer: IAddExistingToSpaceProps["spacesRenderer"] = (spaces, selectedToAdd, onChange) => (
{ spaces.map(space => { return { onChange(checked, space); } : null} />; }) }
); export const defaultDmsRenderer: IAddExistingToSpaceProps["dmsRenderer"] = (dms, selectedToAdd, onChange) => (

{ _t("Direct Messages") }

{ dms.map(room => { return { onChange(checked, room); } : null} />; }) }
); interface ISubspaceSelectorProps { title: string; space: Room; value: Room; onChange(space: Room): void; } export const SubspaceSelector = ({ title, space, value, onChange }: ISubspaceSelectorProps) => { const options = useMemo(() => { return [space, ...SpaceStore.instance.getChildSpaces(space.roomId).filter(space => { return space.currentState.maySendStateEvent(EventType.SpaceChild, space.client.credentials.userId); })]; }, [space]); let body; if (options.length > 1) { body = ( { onChange(options.find(space => space.roomId === key) || space); }} value={value.roomId} label={_t("Space selection")} > { options.map((space) => { const classes = classNames({ mx_SubspaceSelector_dropdownOptionActive: space === value, }); return
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
; }) }
); } else { body = (
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
); } return

{ title }

{ body }
; }; const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, onAddSubspaceClick, onFinished }) => { const [selectedSpace, setSelectedSpace] = useState(space); return )} className="mx_AddExistingToSpaceDialog" contentId="mx_AddExistingToSpace" onFinished={onFinished} fixedWidth={false} >
{ _t("Want to add a new room instead?") }
{ onCreateRoomClick(); onFinished(); }} > { _t("Create a new room") } } filterPlaceholder={_t("Search for rooms")} roomsRenderer={defaultRoomsRenderer} spacesRenderer={() => (

{ _t("Spaces") }

{ onAddSubspaceClick(); onFinished(); }} > { _t("Adding spaces has moved.") }
)} dmsRenderer={defaultDmsRenderer} />
; }; export default AddExistingToSpaceDialog;