post-merge tidy up

pull/21833/head
Michael Telatynski 2021-08-12 12:03:14 +01:00
parent 0a209afdc2
commit 85b1f166e8
3 changed files with 154 additions and 126 deletions

View File

@ -116,6 +116,12 @@ limitations under the License.
} }
} }
.mx_SpaceHierarchy_list {
list-style: none;
padding: 0;
margin: 0;
}
.mx_SpaceHierarchy_roomCount { .mx_SpaceHierarchy_roomCount {
> h3 { > h3 {
display: inline; display: inline;
@ -264,7 +270,7 @@ limitations under the License.
} }
} }
li.mx_SpaceRoomDirectory_roomTileWrapper { li.mx_SpaceHierarchy_roomTileWrapper {
list-style: none; list-style: none;
} }

View File

@ -23,15 +23,18 @@ import React, {
useState, useState,
KeyboardEvent, KeyboardEvent,
KeyboardEventHandler, KeyboardEventHandler,
useContext,
SetStateAction,
Dispatch,
} from "react"; } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; import { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import classNames from "classnames"; import classNames from "classnames";
import { sortBy } from "lodash"; import { sortBy } from "lodash";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
@ -53,12 +56,13 @@ import { Action } from "../../dispatcher/actions";
import { Key } from "../../Keyboard"; import { Key } from "../../Keyboard";
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex"; import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
import { getDisplayAliasForRoom } from "./RoomDirectory"; import { getDisplayAliasForRoom } from "./RoomDirectory";
import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps { interface IProps {
space: Room; space: Room;
initialText?: string; initialText?: string;
additionalButtons?: ReactNode; additionalButtons?: ReactNode;
showRoom(hierarchy: RoomHierarchy, roomId: string, autoJoin?: boolean): void; showRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, autoJoin?: boolean): void;
} }
interface ITileProps { interface ITileProps {
@ -81,7 +85,7 @@ const Tile: React.FC<ITileProps> = ({
numChildRooms, numChildRooms,
children, children,
}) => { }) => {
const cli = MatrixClientPeg.get(); const cli = useContext(MatrixClientContext);
const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null; const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0] const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0]
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room")); || (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
@ -238,7 +242,7 @@ const Tile: React.FC<ITileProps> = ({
handled = true; handled = true;
if (showChildren) { if (showChildren) {
const childSection = ref.current?.nextElementSibling; const childSection = ref.current?.nextElementSibling;
childSection?.querySelector<HTMLDivElement>(".mx_SpaceRoomDirectory_roomTile")?.focus(); childSection?.querySelector<HTMLDivElement>(".mx_SpaceHierarchy_roomTile")?.focus();
} else { } else {
toggleShowChildren(); toggleShowChildren();
} }
@ -253,7 +257,7 @@ const Tile: React.FC<ITileProps> = ({
} }
return <li return <li
className="mx_SpaceRoomDirectory_roomTileWrapper" className="mx_SpaceHierarchy_roomTileWrapper"
role="treeitem" role="treeitem"
aria-expanded={children ? showChildren : undefined} aria-expanded={children ? showChildren : undefined}
> >
@ -274,12 +278,12 @@ const Tile: React.FC<ITileProps> = ({
</li>; </li>;
}; };
export const showRoom = (hierarchy: RoomHierarchy, roomId: string, autoJoin = false) => { export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, autoJoin = false) => {
const room = hierarchy.roomMap.get(roomId); const room = hierarchy.roomMap.get(roomId);
// Don't let the user view a room they won't be able to either peek or join: // Don't let the user view a room they won't be able to either peek or join:
// fail earlier so they don't have to click back to the directory. // fail earlier so they don't have to click back to the directory.
if (MatrixClientPeg.get().isGuest()) { if (cli.isGuest()) {
if (!room.world_readable && !room.guest_can_join) { if (!room.world_readable && !room.guest_can_join) {
dis.dispatch({ action: "require_registration" }); dis.dispatch({ action: "require_registration" });
return; return;
@ -322,7 +326,7 @@ export const HierarchyLevel = ({
onViewRoomClick, onViewRoomClick,
onToggleClick, onToggleClick,
}: IHierarchyLevelProps) => { }: IHierarchyLevelProps) => {
const cli = MatrixClientPeg.get(); const cli = useContext(MatrixClientContext);
const space = cli.getRoom(root.room_id); const space = cli.getRoom(root.room_id);
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
@ -462,14 +466,107 @@ const useIntersectionObserver = (callback: () => void) => {
}; };
}; };
interface IManageButtonsProps {
hierarchy: RoomHierarchy;
selected: Map<string, Set<string>>;
setSelected: Dispatch<SetStateAction<Map<string, Set<string>>>>;
setError: Dispatch<SetStateAction<string>>;
}
const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageButtonsProps) => {
const cli = useContext(MatrixClientContext);
const [removing, setRemoving] = useState(false);
const [saving, setSaving] = useState(false);
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
return [
...selected.get(parentId).values(),
].map(childId => [parentId, childId]) as [string, string][];
});
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
return hierarchy.isSuggested(parentId, childId);
});
const disabled = !selectedRelations.length || removing || saving;
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
let props = {};
if (!selectedRelations.length) {
Button = AccessibleTooltipButton;
props = {
tooltip: _t("Select a room below first"),
yOffset: -40,
};
}
return <>
<Button
{...props}
onClick={async () => {
setRemoving(true);
try {
for (const [parentId, childId] of selectedRelations) {
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
hierarchy.removeRelation(parentId, childId);
}
} catch (e) {
setError(_t("Failed to remove some rooms. Try again later"));
}
setRemoving(false);
setSelected(new Map());
}}
kind="danger_outline"
disabled={disabled}
>
{ removing ? _t("Removing...") : _t("Remove") }
</Button>
<Button
{...props}
onClick={async () => {
setSaving(true);
try {
for (const [parentId, childId] of selectedRelations) {
const suggested = !selectionAllSuggested;
const existingContent = hierarchy.getRelation(parentId, childId)?.content;
if (!existingContent || existingContent.suggested === suggested) continue;
const content = {
...existingContent,
suggested: !selectionAllSuggested,
};
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
// mutate the local state to save us having to refetch the world
existingContent.suggested = content.suggested;
}
} catch (e) {
setError("Failed to update some suggestions. Try again later");
}
setSaving(false);
setSelected(new Map());
}}
kind="primary_outline"
disabled={disabled}
>
{ saving
? _t("Saving...")
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
}
</Button>
</>;
};
const SpaceHierarchy = ({ const SpaceHierarchy = ({
space, space,
initialText = "", initialText = "",
showRoom, showRoom,
additionalButtons, additionalButtons,
}: IProps) => { }: IProps) => {
const cli = MatrixClientPeg.get(); const cli = useContext(MatrixClientContext);
const userId = cli.getUserId();
const [query, setQuery] = useState(initialText); const [query, setQuery] = useState(initialText);
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>> const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
@ -502,8 +599,6 @@ const SpaceHierarchy = ({
}, [rooms, hierarchy, query]); }, [rooms, hierarchy, query]);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [removing, setRemoving] = useState(false);
const [saving, setSaving] = useState(false);
const loaderRef = useIntersectionObserver(loadMore); const loaderRef = useIntersectionObserver(loadMore);
@ -511,13 +606,29 @@ const SpaceHierarchy = ({
return <p>{ _t("Your server does not support showing space hierarchies.") }</p>; return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
} }
const onKeyDown = (ev: KeyboardEvent, state: IState) => { const onKeyDown = (ev: KeyboardEvent, state: IState): void => {
if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceRoomDirectory_search")) { if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceHierarchy_search")) {
state.refs[0]?.current?.focus(); state.refs[0]?.current?.focus();
} }
}; };
// TODO loading state/error state const onToggleClick = (parentId: string, childId: string): void => {
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))));
};
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown> return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
{ ({ onKeyDownHandler }) => { { ({ onKeyDownHandler }) => {
let content: JSX.Element; let content: JSX.Element;
@ -526,93 +637,11 @@ const SpaceHierarchy = ({
if (loading && !rooms.length) { if (loading && !rooms.length) {
content = <Spinner />; content = <Spinner />;
} else { } else {
let manageButtons; const hasPermissions = space?.getMyMembership() === "join" &&
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { space.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
return [
...selected.get(parentId).values(),
].map(childId => [parentId, childId]) as [string, string][];
});
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
return hierarchy.isSuggested(parentId, childId);
});
const disabled = !selectedRelations.length || removing || saving;
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
let props = {};
if (!selectedRelations.length) {
Button = AccessibleTooltipButton;
props = {
tooltip: _t("Select a room below first"),
yOffset: -40,
};
}
manageButtons = <>
<Button
{...props}
onClick={async () => {
setRemoving(true);
try {
for (const [parentId, childId] of selectedRelations) {
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
hierarchy.removeRelation(parentId, childId);
}
} catch (e) {
setError(_t("Failed to remove some rooms. Try again later"));
}
setRemoving(false);
setSelected(new Map());
}}
kind="danger_outline"
disabled={disabled}
>
{ removing ? _t("Removing...") : _t("Remove") }
</Button>
<Button
{...props}
onClick={async () => {
setSaving(true);
try {
for (const [parentId, childId] of selectedRelations) {
const suggested = !selectionAllSuggested;
const existingContent = hierarchy.getRelation(parentId, childId)?.content;
if (!existingContent || existingContent.suggested === suggested) continue;
const content = {
...existingContent,
suggested: !selectionAllSuggested,
};
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
// mutate the local state to save us having to refetch the world
existingContent.suggested = content.suggested;
}
} catch (e) {
setError("Failed to update some suggestions. Try again later");
}
setSaving(false);
setSelected(new Map());
}}
kind="primary_outline"
disabled={disabled}
>
{ saving
? _t("Saving...")
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
}
</Button>
</>;
}
let results: JSX.Element; let results: JSX.Element;
if (filteredRoomSet.size) { if (filteredRoomSet.size) {
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
results = <> results = <>
<HierarchyLevel <HierarchyLevel
root={hierarchy.roomMap.get(space.roomId)} root={hierarchy.roomMap.get(space.roomId)}
@ -620,24 +649,9 @@ const SpaceHierarchy = ({
hierarchy={hierarchy} hierarchy={hierarchy}
parents={new Set()} parents={new Set()}
selectedMap={selected} selectedMap={selected}
onToggleClick={hasPermissions ? (parentId, childId) => { onToggleClick={hasPermissions ? onToggleClick : undefined}
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))));
} : undefined}
onViewRoomClick={(roomId, autoJoin) => { onViewRoomClick={(roomId, autoJoin) => {
showRoom(hierarchy, roomId, autoJoin); showRoom(cli, hierarchy, roomId, autoJoin);
}} }}
/> />
</>; </>;
@ -659,13 +673,25 @@ const SpaceHierarchy = ({
<h4>{ query.trim() ? _t("Results") : _t("Rooms and spaces") }</h4> <h4>{ query.trim() ? _t("Results") : _t("Rooms and spaces") }</h4>
<span> <span>
{ additionalButtons } { additionalButtons }
{ manageButtons } { hasPermissions && (
<ManageButtons
hierarchy={hierarchy}
selected={selected}
setSelected={setSelected}
setError={setError}
/>
) }
</span> </span>
</div> </div>
{ error && <div className="mx_SpaceHierarchy_error"> { error && <div className="mx_SpaceHierarchy_error">
{ error } { error }
</div> } </div> }
<ul onKeyDown={onKeyDownHandler} role="tree" aria-label={_t("Space")}> <ul
className="mx_SpaceHierarchy_list"
onKeyDown={onKeyDownHandler}
role="tree"
aria-label={_t("Space")}
>
{ results } { results }
</ul> </ul>
{ loader } { loader }
@ -674,7 +700,7 @@ const SpaceHierarchy = ({
return <> return <>
<SearchBox <SearchBox
className="mx_SpaceRoomDirectory_search mx_textinput_icon mx_textinput_search" className="mx_SpaceHierarchy_search mx_textinput_icon mx_textinput_search"
placeholder={_t("Search names and descriptions")} placeholder={_t("Search names and descriptions")}
onSearch={setQuery} onSearch={setQuery}
autoFocus={true} autoFocus={true}

View File

@ -2844,10 +2844,6 @@
"This room is suggested as a good one to join": "This room is suggested as a good one to join", "This room is suggested as a good one to join": "This room is suggested as a good one to join",
"Suggested": "Suggested", "Suggested": "Suggested",
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
"%(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",
"Select a room below first": "Select a room below first", "Select a room below first": "Select a room below first",
"Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later", "Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later",
"Removing...": "Removing...", "Removing...": "Removing...",
@ -2855,10 +2851,10 @@
"Mark as suggested": "Mark as suggested", "Mark as suggested": "Mark as suggested",
"No results found": "No results found", "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.", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
"Results": "Results",
"Rooms and spaces": "Rooms and spaces",
"Space": "Space", "Space": "Space",
"Search names and descriptions": "Search names and descriptions", "Search names and descriptions": "Search names and descriptions",
"If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
"Create room": "Create room",
"Private space": "Private space", "Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you", "<inviter/> invites you": "<inviter/> invites you",
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>", "To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>",