/* 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 {MatrixClient} from "matrix-js-sdk/src/client"; import {_t} from '../../../languageHandler'; import {IDialogProps} from "./IDialogProps"; 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 {sleep} from "../../../utils/promise"; 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 {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; 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 extends IDialogProps { matrixClient: MatrixClient; space: Room; onCreateRoomClick(cli: MatrixClient, space: Room): void; } const Entry = ({ room, checked, onChange }) => { return ; }; interface IAddExistingToSpaceProps { space: Room; footerPrompt?: ReactNode; emptySelectionButton?: ReactNode; onFinished(added: boolean): void; } export const AddExistingToSpace: React.FC = ({ space, footerPrompt, emptySelectionButton, 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, room) => { if (checked) { selectedToAdd.add(room); } else { selectedToAdd.delete(room); } setSelectedToAdd(new Set(selectedToAdd)); } : null; const [truncateAt, setTruncateAt] = useState(20); function overflowTile(overflowCount, totalCount) { const text = _t("and %(count)s others...", { count: overflowCount }); return ( } name={text} presenceState="online" suppressOnHover={true} onClick={() => setTruncateAt(totalCount)} /> ); } return
{ rooms.length > 0 ? (

{ _t("Rooms") }

rooms.slice(start, end).map(room => { onChange(checked, room); } : null} />, )} getChildCount={() => rooms.length} />
) : undefined } { spaces.length > 0 ? (

{ _t("Spaces") }

{ _t("Feeling experimental?") }
{ _t("You can add existing spaces to a space.") }
{ spaces.map(space => { return { onChange(checked, space); } : null} />; }) }
) : null } { dms.length > 0 ? (

{ _t("Direct Messages") }

{ dms.map(room => { return { onChange(checked, room); } : null} />; }) }
) : null } { spaces.length + rooms.length + dms.length < 1 ? { _t("No results") } : undefined }
{ footer }
; }; const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { const [selectedSpace, setSelectedSpace] = useState(space); const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); let spaceOptionSection; if (existingSubspaces.length > 0) { const options = [space, ...existingSubspaces].map((space) => { const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", { mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace, }); return
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
; }); spaceOptionSection = ( { setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space); }} value={selectedSpace.roomId} label={_t("Space selection")} > { options } ); } else { spaceOptionSection =
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
; } const title =

{ _t("Add existing rooms") }

{ spaceOptionSection }
; return
{ _t("Want to add a new room instead?") }
onCreateRoomClick(cli, space)} kind="link"> { _t("Create a new room") } } />
onFinished(false)} />
; }; export default AddExistingToSpaceDialog;