From d04c8b3fb417fc9cc3eedcc78d3d8b4a84de035a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Mar 2021 11:36:36 +0000 Subject: [PATCH] Improve discovery of rooms in a space --- res/css/structures/_SpaceRoomDirectory.scss | 271 +++++--- res/img/element-icons/warning-badge.svg | 6 +- .../structures/SpaceRoomDirectory.tsx | 635 +++++++++--------- src/components/structures/SpaceRoomView.tsx | 9 +- .../views/elements/TextWithTooltip.js | 10 +- src/i18n/strings/en_EN.json | 32 +- 6 files changed, 527 insertions(+), 436 deletions(-) diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index b14e92a1af..b20554166a 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -31,7 +31,8 @@ limitations under the License. display: flex; .mx_BaseAvatar { - margin-right: 16px; + margin-right: 12px; + align-self: center; } .mx_BaseAvatar_image { @@ -47,6 +48,7 @@ limitations under the License. } > div { + font-weight: 400; color: $secondary-fg-color; font-size: $font-15px; line-height: $font-24px; @@ -55,38 +57,71 @@ limitations under the License. } .mx_Dialog_content { - // TODO fix scrollbar - //display: flex; - //flex-direction: column; - //height: calc(100% - 80px); - .mx_AccessibleButton_kind_link { padding: 0; } .mx_SearchBox { - margin: 24px 0 28px; + margin: 24px 0 16px; + } + + .mx_SpaceRoomDirectory_noResults { + text-align: center; + + > div { + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + } } .mx_SpaceRoomDirectory_listHeader { display: flex; - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-fg-color; + min-height: 32px; + align-items: center; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; - .mx_FormButton { - margin-bottom: 8px; + .mx_AccessibleButton { + padding: 2px 8px; + font-weight: normal; + + & + .mx_AccessibleButton { + margin-left: 16px; + } } > span { - margin: auto 0 0 auto; + margin-left: auto; + } + } + + .mx_SpaceRoomDirectory_error { + position: relative; + font-weight: $font-semi-bold; + color: $notice-primary-color; + font-size: $font-15px; + line-height: $font-18px; + margin: 20px auto 12px; + padding-left: 24px; + width: max-content; + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + left: 0; + background-image: url("$(res)/img/element-icons/warning-badge.svg"); } } } } .mx_SpaceRoomDirectory_list { - margin-top: 8px; + margin-top: 16px; + padding-bottom: 40px; .mx_SpaceRoomDirectory_roomCount { > h3 { @@ -106,115 +141,128 @@ limitations under the License. } .mx_SpaceRoomDirectory_subspace { - margin-top: 8px; - - .mx_SpaceRoomDirectory_subspace_info { - display: flex; - flex-direction: row; - align-items: center; - margin-bottom: 8px; - color: $secondary-fg-color; - font-weight: $font-semi-bold; - font-size: $font-12px; - line-height: $font-15px; - - .mx_BaseAvatar { - margin-right: 12px; - vertical-align: middle; - } - - .mx_BaseAvatar_image { - border-radius: 8px; - } - - .mx_SpaceRoomDirectory_actions { - text-align: right; - height: min-content; - margin-left: auto; - margin-right: 16px; - display: inline-flex; - } - } - - .mx_SpaceRoomDirectory_subspace_children { - margin-left: 12px; - border-left: 2px solid $space-button-outline-color; - padding-left: 24px; + .mx_BaseAvatar_image { + border-radius: 8px; } } - .mx_SpaceRoomDirectory_roomTile { - padding: 16px; - border-radius: 8px; - border: 1px solid $space-button-outline-color; - margin: 8px 0 16px; - display: flex; - min-height: 76px; - box-sizing: border-box; + .mx_SpaceRoomDirectory_subspace_toggle { + position: absolute; + left: -1px; + top: 10px; + height: 16px; + width: 16px; + border-radius: 4px; + background-color: $primary-bg-color; - &.mx_AccessibleButton:hover { - background-color: rgba(141, 151, 165, 0.1); + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 16px; + width: 16px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $tertiary-fg-color; + mask-size: 16px; + transform: rotate(270deg); + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } + &.mx_SpaceRoomDirectory_subspace_toggle_shown::before { + transform: rotate(0deg); + } + } + + .mx_SpaceRoomDirectory_subspace_children { + position: relative; + padding-left: 12px; + } + + .mx_SpaceRoomDirectory_roomTile { + position: relative; + padding: 6px 16px; + border-radius: 8px; + min-height: 56px; + box-sizing: border-box; + + display: grid; + grid-template-columns: 20px auto max-content; + grid-column-gap: 8px; + align-items: center; + .mx_BaseAvatar { - margin-right: 16px; - margin-top: 6px; + grid-row: 1; + grid-column: 1; + } + + .mx_SpaceRoomDirectory_roomTile_name { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + grid-row: 1; + grid-column: 2; + + .mx_InfoTooltip { + display: inline; + margin-left: 12px; + color: $tertiary-fg-color; + font-size: $font-12px; + line-height: $font-15px; + + .mx_InfoTooltip_icon { + margin-right: 4px; + } + } } .mx_SpaceRoomDirectory_roomTile_info { - display: inline-block; - font-size: $font-15px; - flex-grow: 1; - height: min-content; - margin: auto 0; - - .mx_SpaceRoomDirectory_roomTile_name { - font-weight: $font-semi-bold; - line-height: $font-18px; - } - .mx_SpaceRoomDirectory_roomTile_topic { - line-height: $font-24px; - color: $secondary-fg-color; - } - } - - .mx_SpaceRoomDirectory_roomTile_memberCount { - position: relative; - margin: auto 0 auto 24px; - padding: 0 0 0 28px; - line-height: $font-24px; - display: inline-block; - width: 32px; - - &::before { - position: absolute; - content: ''; - width: 24px; - height: 24px; - top: 0; - left: 0; - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - background-color: $secondary-fg-color; - mask-image: url('$(res)/img/element-icons/community-members.svg'); - } + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-fg-color; + grid-row: 2; + grid-column: 1/3; } .mx_SpaceRoomDirectory_actions { - width: 180px; text-align: right; - margin-left: 28px; - display: inline-flex; - align-items: center; + margin-left: 20px; + grid-column: 3; + grid-row: 1/3; .mx_AccessibleButton { - vertical-align: middle; + padding: 6px 18px; - & + .mx_AccessibleButton { - margin-left: 24px; - } + display: none; } + + .mx_Checkbox { + display: inline-flex; + vertical-align: middle; + margin-left: 12px; + } + } + + &:hover { + background-color: $groupFilterPanel-bg-color; + + .mx_AccessibleButton { + display: inline-block; + } + } + } + + .mx_SpaceRoomDirectory_roomTile, + .mx_SpaceRoomDirectory_subspace_children { + &::before { + content: ""; + position: absolute; + background-color: $groupFilterPanel-bg-color; + width: 1px; + height: 100%; + left: 6px; + top: 0; } } @@ -226,4 +274,17 @@ limitations under the License. color: $secondary-fg-color; } } + + > hr { + border: none; + height: 1px; + background-color: rgba(141, 151, 165, 0.2); + margin: 20px 0; + } + + .mx_SpaceRoomDirectory_createRoom { + display: block; + margin: 16px auto 0; + width: max-content; + } } diff --git a/res/img/element-icons/warning-badge.svg b/res/img/element-icons/warning-badge.svg index ac5991f221..1ae4e40ffe 100644 --- a/res/img/element-icons/warning-badge.svg +++ b/res/img/element-icons/warning-badge.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 9ee16558d3..0a53f38238 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -14,27 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useMemo, useRef, useState} from "react"; +import React, {useMemo, useState} from "react"; import Room from "matrix-js-sdk/src/models/room"; -import MatrixEvent from "matrix-js-sdk/src/models/event"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; +import classNames from "classnames"; +import {sortBy} from "lodash"; import {MatrixClientPeg} from "../../MatrixClientPeg"; import dis from "../../dispatcher/dispatcher"; import {_t} from "../../languageHandler"; import AccessibleButton from "../views/elements/AccessibleButton"; import BaseDialog from "../views/dialogs/BaseDialog"; -import FormButton from "../views/elements/FormButton"; +import Spinner from "../views/elements/Spinner"; import SearchBox from "./SearchBox"; import RoomAvatar from "../views/avatars/RoomAvatar"; import RoomName from "../views/elements/RoomName"; import {useAsyncMemo} from "../../hooks/useAsyncMemo"; -import {shouldShowSpaceSettings} from "../../utils/space"; import {EnhancedMap} from "../../utils/maps"; import StyledCheckbox from "../views/elements/StyledCheckbox"; import AutoHideScrollbar from "./AutoHideScrollbar"; import BaseAvatar from "../views/avatars/BaseAvatar"; import {mediaFromMxc} from "../../customisations/Media"; +import InfoTooltip from "../views/elements/InfoTooltip"; +import TextWithTooltip from "../views/elements/TextWithTooltip"; +import {useStateToggle} from "../../hooks/useStateToggle"; interface IProps { space: Room; @@ -72,215 +75,98 @@ export interface ISpaceSummaryEvent { } /* eslint-enable camelcase */ -interface ISubspaceProps { - space: ISpaceSummaryRoom; - event?: MatrixEvent; +interface ITileProps { + room: ISpaceSummaryRoom; editing?: boolean; - onPreviewClick?(): void; - queueAction?(action: IAction): void; - onJoinClick?(): void; + suggested?: boolean; + selected?: boolean; + numChildRooms?: number; + hasPermissions?: boolean; + onViewRoomClick(autoJoin: boolean): void; + onToggleClick?(): void; } -const SubSpace: React.FC = ({ - space, +const Tile: React.FC = ({ + room, editing, - event, - queueAction, - onJoinClick, - onPreviewClick, + suggested, + selected, + hasPermissions, + onToggleClick, + onViewRoomClick, + numChildRooms, children, }) => { - const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space"); + const name = room.name || room.canonical_alias || room.aliases?.[0] + || (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room")); - const evContent = event?.getContent(); - const [suggested, _setSuggested] = useState(evContent?.suggested); - const [removed, _setRemoved] = useState(!evContent?.via); - - const cli = MatrixClientPeg.get(); - const cliRoom = cli.getRoom(space.room_id); - const myMembership = cliRoom?.getMyMembership(); - - // TODO DRY code - let actions; - if (editing && queueAction) { - if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { - const setSuggested = () => { - _setSuggested(v => { - queueAction({ - event, - removed, - suggested: !v, - }); - return !v; - }); - }; - - const setRemoved = () => { - _setRemoved(v => { - queueAction({ - event, - removed: !v, - suggested, - }); - return !v; - }); - }; - - if (removed) { - actions = - - ; - } else { - actions = - - - ; - } - } else { - actions = - { _t("No permissions")} - ; - } - // TODO confirm remove from space click behaviour here - } else { - if (myMembership === "join") { - actions = - { _t("You're in this space")} - ; - } else if (onJoinClick) { - actions = - - { _t("Preview") } - - - - } - } - - let url: string; - if (space.avatar_url) { - url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio)); - } - - return
-
- - { name } - -
- { actions } -
-
-
- { children } -
-
-}; - -interface IAction { - event: MatrixEvent; - suggested: boolean; - removed: boolean; -} - -interface IRoomTileProps { - room: ISpaceSummaryRoom; - event?: MatrixEvent; - editing?: boolean; - onPreviewClick(): void; - queueAction?(action: IAction): void; - onJoinClick?(): void; -} - -const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinClick }: IRoomTileProps) => { - const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room"); - - const evContent = event?.getContent(); - const [suggested, _setSuggested] = useState(evContent?.suggested); - const [removed, _setRemoved] = useState(!evContent?.via); + const [showChildren, toggleShowChildren] = useStateToggle(true); const cli = MatrixClientPeg.get(); const cliRoom = cli.getRoom(room.room_id); const myMembership = cliRoom?.getMyMembership(); - let actions; - if (editing && queueAction) { - if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { - const setSuggested = () => { - _setSuggested(v => { - queueAction({ - event, - removed, - suggested: !v, - }); - return !v; - }); - }; + const onPreviewClick = () => onViewRoomClick(false); + const onJoinClick = () => onViewRoomClick(true); - const setRemoved = () => { - _setRemoved(v => { - queueAction({ - event, - removed: !v, - suggested, - }); - return !v; - }); - }; + let button; + if (myMembership === "join") { + button = + { _t("Open") } + ; + } else if (onJoinClick) { + button = + { _t("Join") } + ; + } - if (removed) { - actions = - - ; - } else { - actions = - - - ; - } + let checkbox; + if (onToggleClick) { + if (hasPermissions) { + checkbox = ; } else { - actions = - { _t("No permissions")} - ; - } - // TODO confirm remove from space click behaviour here - } else { - if (myMembership === "join") { - actions = - { _t("You're in this room")} - ; - } else if (onJoinClick) { - actions = - - { _t("Preview") } - - - + checkbox = { ev.stopPropagation() }} + > + + ; } } let url: string; if (room.avatar_url) { - url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(32 * window.devicePixelRatio)); + url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(20 * window.devicePixelRatio)); + } + + let description = _t("%(count)s members", { count: room.num_joined_members }); + if (numChildRooms) { + description += " · " + _t("%(count)s rooms", { count: numChildRooms }); + } + if (room.topic) { + description += " · " + room.topic; + } + + let suggestedSection; + if (suggested) { + suggestedSection = + { _t("Suggested") } + ; } const content = - + +
+ { name } + { suggestedSection } +
-
- { name } -
-
- { room.topic } -
+ { description }
-
- { room.num_joined_members } -
-
- { actions } + { button } + { checkbox }
; @@ -290,9 +176,38 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli } - return - { content } - ; + let childToggle; + let childSection; + if (children) { + // the chevron is purposefully a div rather than a button as it should be ignored for a11y + childToggle =
{ + ev.stopPropagation(); + toggleShowChildren(); + }} + />; + if (showChildren) { + childSection =
+ { children } +
; + } + } + + return <> + + { content } + { childToggle } + + { childSection } + ; }; export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => { @@ -325,88 +240,77 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi interface IHierarchyLevelProps { spaceId: string; rooms: Map; - editing?: boolean; - relations: EnhancedMap; + relations: EnhancedMap>; parents: Set; - queueAction?(action: IAction): void; - onPreviewClick(roomId: string): void; - onRemoveFromSpaceClick?(roomId: string): void; - onJoinClick?(roomId: string): void; + selectedMap?: Map>; + onViewRoomClick(roomId: string, autoJoin: boolean): void; + onToggleClick?(parentId: string, childId: string): void; } export const HierarchyLevel = ({ spaceId, rooms, - editing, relations, parents, - onPreviewClick, - onJoinClick, - queueAction, + selectedMap, + onViewRoomClick, + onToggleClick, }: IHierarchyLevelProps) => { const cli = MatrixClientPeg.get(); const space = cli.getRoom(spaceId); - // TODO respect order - const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => { - if (!rooms.has(roomId)) return result; // TODO wat + const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()) + + const sortedChildren = sortBy([...relations.get(spaceId)?.values()], ev => ev.content.order || null); + const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { + const roomId = ev.state_key; + if (!rooms.has(roomId)) return result; result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId); return result; }, [[], []]) || [[], []]; - // Don't render this subspace if it has no rooms we can show - // TODO this is broken - as a space may have subspaces we still need to show - // if (!childRooms.length) return null; - - const userId = cli.getUserId(); - const newParents = new Set(parents).add(spaceId); return { childRooms.map(roomId => ( - { - onPreviewClick(roomId); + suggested={relations.get(spaceId)?.get(roomId)?.content.suggested} + selected={selectedMap?.get(spaceId)?.has(roomId)} + onViewRoomClick={(autoJoin) => { + onViewRoomClick(roomId, autoJoin); }} - onJoinClick={onJoinClick ? () => { - onJoinClick(roomId); - } : undefined} + hasPermissions={hasPermissions} + onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined} /> )) } { subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => ( - { - onPreviewClick(roomId); - }} - onJoinClick={() => { - onJoinClick(roomId); + room={rooms.get(roomId)} + numChildRooms={Array.from(relations.get(roomId)?.values() || []) + .filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length} + suggested={relations.get(spaceId)?.get(roomId)?.content.suggested} + selected={selectedMap?.get(spaceId)?.has(roomId)} + onViewRoomClick={(autoJoin) => { + onViewRoomClick(roomId, autoJoin); }} + hasPermissions={hasPermissions} + onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined} > - + )) } @@ -415,8 +319,8 @@ export const HierarchyLevel = ({ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinished }) => { // TODO pagination const cli = MatrixClientPeg.get(); + const userId = cli.getUserId(); const [query, setQuery] = useState(initialText); - const [isEditing, setIsEditing] = useState(false); const onCreateRoomClick = () => { dis.dispatch({ @@ -426,51 +330,19 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis onFinished(); }; - // stored within a ref as we don't need to re-render when it changes - const pendingActions = useRef(new Map()); + const [selected, setSelected] = useState(new Map>()); // Map> - let adminButton; - if (shouldShowSpaceSettings(cli, space)) { // TODO this is an imperfect test - const onManageButtonClicked = () => { - setIsEditing(true); - }; - - const onSaveButtonClicked = () => { - // TODO setBusy - pendingActions.current.forEach(({event, suggested, removed}) => { - const content = { - ...event.getContent(), - suggested, - }; - - if (removed) { - delete content["via"]; - } - - cli.sendStateEvent(event.getRoomId(), event.getType(), content, event.getStateKey()); - }); - setIsEditing(false); - }; - - if (isEditing) { - adminButton = - - { _t("Promoted to users") } - ; - } else { - adminButton = ; - } - } - - const [rooms, relations, viaMap] = useAsyncMemo(async () => { + const [rooms, parentChildMap, childParentMap, viaMap] = useAsyncMemo(async () => { try { const data = await cli.getSpaceSummary(space.roomId); - const parentChildRelations = new EnhancedMap(); + const parentChildRelations = new EnhancedMap>(); + const childParentRelations = new EnhancedMap>(); const viaMap = new EnhancedMap>(); data.events.map((ev: ISpaceSummaryEvent) => { if (ev.type === EventType.SpaceChild) { - parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key); + parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev); + childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id); } if (Array.isArray(ev.content["via"])) { const set = viaMap.getOrCreate(ev.state_key, new Set()); @@ -478,7 +350,7 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis } }); - return [data.rooms, parentChildRelations, viaMap]; + return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, childParentRelations, viaMap]; } catch (e) { console.error(e); // TODO } @@ -488,54 +360,204 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis const roomsMap = useMemo(() => { if (!rooms) return null; - const lcQuery = query.toLowerCase(); + const lcQuery = query.toLowerCase().trim(); - const filteredRooms = rooms.filter(r => { - return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms - || r.name?.toLowerCase().includes(lcQuery) - || r.topic?.toLowerCase().includes(lcQuery); + const roomsMap = new Map(rooms.map(r => [r.room_id, r])); + if (!lcQuery) return roomsMap; + + const directMatches = rooms.filter(r => { + return r.name?.toLowerCase().includes(lcQuery) || r.topic?.toLowerCase().includes(lcQuery); }); - return new Map(filteredRooms.map(r => [r.room_id, r])); - // const root = rooms.get(space.roomId); - }, [rooms, query]); + // Walk back up the tree to find all parents of the direct matches to show their place in the hierarchy + const visited = new Set(); + const queue = [...directMatches.map(r => r.room_id)]; + while (queue.length) { + const roomId = queue.pop(); + visited.add(roomId); + childParentMap.get(roomId)?.forEach(parentId => { + if (!visited.has(parentId)) { + queue.push(parentId); + } + }); + } + + // Remove any mappings for rooms which were not visited in the walk + Array.from(roomsMap.keys()).forEach(roomId => { + if (!visited.has(roomId)) { + roomsMap.delete(roomId); + } + }); + return roomsMap; + }, [rooms, childParentMap, query]); const title = - +

{ _t("Explore rooms") }

; + const explanation = - _t("If you can't find the room you're looking for, ask for an invite or Create a new room.", null, + _t("If you can't find the room you're looking for, ask for an invite or create a new room.", null, {a: sub => { return {sub}; }}, ); + const [error, setError] = useState(""); + const [removing, setRemoving] = useState(false); + const [saving, setSaving] = useState(false); + let content; if (roomsMap) { - content = - { - pendingActions.current.set(action.event.room_id, action); - }} - onPreviewClick={roomId => { - showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false); - onFinished(); - }} - onJoinClick={(roomId) => { - showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true); - onFinished(); - }} - /> - ; + const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length; + const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at + + let countsStr; + if (numSpaces > 1) { + countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces }); + } else if (numSpaces > 0) { + countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces }); + } else { + countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces }); + } + + let editSection; + if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { + const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { + return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][]; + }); + + let buttons; + if (selectedRelations.length) { + const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { + return parentChildMap.get(parentId)?.get(childId)?.content.suggested; + }); + + const disabled = removing || saving; + + buttons = <> + { + setRemoving(true); + try { + for (const [parentId, childId] of selectedRelations) { + await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId); + parentChildMap.get(parentId).get(childId).content = {}; + parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); + } + } catch (e) { + setError(_t("Failed to remove some rooms. Try again later")); + } + setRemoving(false); + }} + kind="danger_outline" + disabled={disabled} + > + { removing ? _t("Removing...") : _t("Remove") } + + { + setSaving(true); + try { + for (const [parentId, childId] of selectedRelations) { + const suggested = !selectionAllSuggested; + const existingContent = parentChildMap.get(parentId)?.get(childId)?.content; + if (!existingContent || existingContent.suggested === suggested) continue; + + const content = { + ...existingContent, + suggested: !selectionAllSuggested, + }; + + await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId); + + parentChildMap.get(parentId).get(childId).content = content; + parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); + } + } catch (e) { + setError("Failed to update some suggestions. Try again later"); + } + setSaving(false); + }} + kind="primary_outline" + disabled={disabled} + > + { saving + ? _t("Saving...") + : (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested")) + } + + ; + } + + editSection = + { buttons } + ; + } + + let results; + if (roomsMap.size) { + results = <> + { + setError(""); + if (!selected.has(parentId)) { + setSelected(new Map(selected.set(parentId, new Set([childId])))); + return; + } + + const parentSet = selected.get(parentId); + if (!parentSet.has(childId)) { + setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId])))); + return; + } + + parentSet.delete(childId); + setSelected(new Map(selected.set(parentId, new Set(parentSet)))); + }} + onViewRoomClick={(roomId, autoJoin) => { + showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); + onFinished(); + }} + /> +
+ ; + } else { + results =
+

{ _t("No results found") }

+
{ _t("You may want to try a different search or check for typos.") }
+
; + } + + content = <> +
+ { countsStr } + { editSection } +
+ { error &&
+ { error } +
} + + { results } + + { _t("Create room") } + + + ; + } else { + content = ; } // TODO loading state/error state @@ -546,13 +568,10 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis -
- { adminButton } -
{ content }
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 0b0f2a2ac9..2b4168a983 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -230,10 +230,10 @@ const SpaceLanding = ({ space }) => { try { const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join"); - const parentChildRelations = new EnhancedMap(); + const parentChildRelations = new EnhancedMap>(); data.events.map((ev: ISpaceSummaryEvent) => { if (ev.type === EventType.SpaceChild) { - parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key); + parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev); } }); @@ -257,11 +257,10 @@ const SpaceLanding = ({ space }) => { { - showRoom(roomsMap.get(roomId), [], false); // TODO + onViewRoomClick={(roomId, autoJoin) => { + showRoom(roomsMap.get(roomId), [], autoJoin); }} /> ; diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js index e4ad234ae2..0bd491768c 100644 --- a/src/components/views/elements/TextWithTooltip.js +++ b/src/components/views/elements/TextWithTooltip.js @@ -46,12 +46,14 @@ export default class TextWithTooltip extends React.Component { render() { const Tooltip = sdk.getComponent("elements.Tooltip"); + const {class: className, children, tooltip, tooltipClass, ...props} = this.props; + return ( - - {this.props.children} + + {children} {this.state.hover && } ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 07d292a0e7..e4d197470d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2603,20 +2603,30 @@ "Drop file here to upload": "Drop file here to upload", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", - "Undo": "Undo", - "Remove from Space": "Remove from Space", - "No permissions": "No permissions", - "You're in this space": "You're in this space", - "You're in this room": "You're in this room", - "Save changes": "Save changes", - "Promoted to users": "Promoted to users", - "Manage rooms": "Manage rooms", - "Find a room...": "Find a room...", + "Open": "Open", + "You don't have permission": "You don't have permission", + "%(count)s members|other": "%(count)s members", + "%(count)s members|one": "%(count)s member", + "%(count)s rooms|other": "%(count)s rooms", + "%(count)s rooms|one": "%(count)s room", + "This room is suggested as a good one to join": "This room is suggested as a good one to join", + "Suggested": "Suggested", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", + "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s rooms and %(numSpaces)s spaces", + "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces", + "%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space", + "%(count)s rooms and 1 space|one": "%(count)s room and 1 space", + "Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later", + "Removing...": "Removing...", + "Mark as not suggested": "Mark as not suggested", + "Mark as suggested": "Mark as suggested", + "No results found": "No results found", + "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", + "Create room": "Create room", + "Search names and description": "Search names and description", " invites you": " invites you", "Public space": "Public space", "Private space": "Private space", - "%(count)s members|other": "%(count)s members", - "%(count)s members|one": "%(count)s member", "Add existing rooms & spaces": "Add existing rooms & spaces", "Default Rooms": "Default Rooms", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",