Iterate Spaces admin UX around room management

This commit is contained in:
Michael Telatynski 2021-05-05 17:25:29 +01:00
parent 81b97590f6
commit 650933096a
6 changed files with 183 additions and 167 deletions

View file

@ -86,7 +86,7 @@ limitations under the License.
color: $primary-fg-color; color: $primary-fg-color;
.mx_AccessibleButton { .mx_AccessibleButton {
padding: 2px 8px; padding: 4px 8px;
font-weight: normal; font-weight: normal;
& + .mx_AccessibleButton { & + .mx_AccessibleButton {

View file

@ -254,6 +254,27 @@ $SpaceRoomViewInnerWidth: 428px;
mask-image: url('$(res)/img/element-icons/room/invite.svg'); mask-image: url('$(res)/img/element-icons/room/invite.svg');
} }
} }
.mx_SpaceRoomView_landing_settingsButton {
position: relative;
margin-left: 16px;
width: 24px;
height: 24px;
&::before {
position: absolute;
content: "";
left: 0;
top: 0;
height: 24px;
width: 24px;
background: $tertiary-fg-color;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/settings.svg');
}
}
} }
.mx_SpaceRoomView_landing_topic { .mx_SpaceRoomView_landing_topic {
@ -268,80 +289,6 @@ $SpaceRoomViewInnerWidth: 428px;
background-color: $groupFilterPanel-bg-color; background-color: $groupFilterPanel-bg-color;
} }
.mx_SpaceRoomView_landing_adminButtons {
margin-top: 24px;
.mx_AccessibleButton {
position: relative;
width: 160px;
height: 124px;
box-sizing: border-box;
padding: 72px 16px 0;
border-radius: 12px;
border: 1px solid $input-border-color;
margin-right: 28px;
margin-bottom: 20px;
font-size: $font-14px;
display: inline-block;
vertical-align: bottom;
&:last-child {
margin-right: 0;
}
&:hover {
background-color: rgba(141, 151, 165, 0.1);
}
&::before, &::after {
position: absolute;
content: "";
left: 16px;
top: 16px;
height: 40px;
width: 40px;
border-radius: 20px;
}
&::after {
mask-position: center;
mask-size: 30px;
mask-repeat: no-repeat;
background: #ffffff; // white icon fill
}
&.mx_SpaceRoomView_landing_addButton {
&::before {
background-color: #ac3ba8;
}
&::after {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
}
}
&.mx_SpaceRoomView_landing_createButton {
&::before {
background-color: #368bd6;
}
&::after {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
}
}
&.mx_SpaceRoomView_landing_settingsButton {
&::before {
background-color: #5c56f5;
}
&::after {
mask-image: url('$(res)/img/element-icons/settings.svg');
}
}
}
}
.mx_SearchBox { .mx_SearchBox {
margin: 0 0 20px; margin: 0 0 20px;
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useMemo, useState} from "react"; import React, {ReactNode, useMemo, useState} from "react";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixClient} from "matrix-js-sdk/src/client";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
@ -39,11 +39,13 @@ import {mediaFromMxc} from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip"; import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip";
import {useStateToggle} from "../../hooks/useStateToggle"; import {useStateToggle} from "../../hooks/useStateToggle";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
interface IHierarchyProps { interface IHierarchyProps {
space: Room; space: Room;
initialText?: string; initialText?: string;
refreshToken?: any; refreshToken?: any;
additionalButtons?: ReactNode;
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void; showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
} }
@ -350,6 +352,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
initialText = "", initialText = "",
showRoom, showRoom,
refreshToken, refreshToken,
additionalButtons,
children, children,
}) => { }) => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -415,22 +418,31 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces }); countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
} }
let editSection; let manageButtons;
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][]; return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
}); });
let buttons;
if (selectedRelations.length) {
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
return parentChildMap.get(parentId)?.get(childId)?.content.suggested; return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
}); });
const disabled = removing || saving; const disabled = !selectedRelations.length || removing || saving;
buttons = <> let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
<AccessibleButton let props = {};
if (!selectedRelations.length) {
Button = AccessibleTooltipButton;
props = {
tooltip: _t("Select a room below first"),
yOffset: -40,
};
}
manageButtons = <>
<Button
{...props}
onClick={async () => { onClick={async () => {
setRemoving(true); setRemoving(true);
try { try {
@ -448,8 +460,9 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
disabled={disabled} disabled={disabled}
> >
{ removing ? _t("Removing...") : _t("Remove") } { removing ? _t("Removing...") : _t("Remove") }
</AccessibleButton> </Button>
<AccessibleButton <Button
{...props}
onClick={async () => { onClick={async () => {
setSaving(true); setSaving(true);
try { try {
@ -480,15 +493,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
? _t("Saving...") ? _t("Saving...")
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested")) : (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
} }
</AccessibleButton> </Button>
</>; </>;
} }
editSection = <span>
{ buttons }
</span>;
}
let results; let results;
if (roomsMap.size) { if (roomsMap.size) {
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
@ -532,7 +540,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
content = <> content = <>
<div className="mx_SpaceRoomDirectory_listHeader"> <div className="mx_SpaceRoomDirectory_listHeader">
{ countsStr } { countsStr }
{ editSection } <span>
{ additionalButtons }
{ manageButtons }
</span>
</div> </div>
{ error && <div className="mx_SpaceRoomDirectory_error"> { error && <div className="mx_SpaceRoomDirectory_error">
{ error } { error }

View file

@ -54,6 +54,12 @@ import FacePile from "../views/elements/FacePile";
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
import {allSettled} from "../../utils/promise"; import {allSettled} from "../../utils/promise";
import {calculateRoomVia} from "../../utils/permalinks/Permalinks"; import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
interface IProps { interface IProps {
space: Room; space: Room;
@ -217,6 +223,67 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
</div>; </div>;
}; };
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
const cli = useContext(MatrixClientContext);
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu;
if (menuDisplayed) {
const rect = handle.current.getBoundingClientRect();
contextMenu = <IconizedContextMenu
left={rect.left + window.pageXOffset + 0}
top={rect.bottom + window.pageYOffset + 8}
chevronFace={ChevronFace.None}
onFinished={closeMenu}
className="mx_RoomTile_contextMenu"
compact
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
if (await showCreateNewRoom(cli, space)) {
onNewRoomAdded();
}
}}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
const [added] = await showAddExistingRooms(cli, space);
if (added) {
onNewRoomAdded();
}
}}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
return <>
<ContextMenuButton
kind="primary"
inputRef={handle}
onClick={openMenu}
isExpanded={menuDisplayed}
label={_t("Add")}
>
{ _t("Add") }
</ContextMenuButton>
{ contextMenu }
</>;
};
const SpaceLanding = ({ space }) => { const SpaceLanding = ({ space }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space); const myMembership = useMyRoomMembership(space);
@ -241,32 +308,20 @@ const SpaceLanding = ({ space }) => {
const [refreshToken, forceUpdate] = useStateToggle(false); const [refreshToken, forceUpdate] = useStateToggle(false);
let addRoomButtons; let addRoomButton;
if (canAddRooms) { if (canAddRooms) {
addRoomButtons = <React.Fragment> addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />;
<AccessibleButton className="mx_SpaceRoomView_landing_addButton" onClick={async () => {
const [added] = await showAddExistingRooms(cli, space);
if (added) {
forceUpdate();
}
}}>
{ _t("Add existing rooms & spaces") }
</AccessibleButton>
<AccessibleButton className="mx_SpaceRoomView_landing_createButton" onClick={() => {
showCreateNewRoom(cli, space);
}}>
{ _t("Create a new room") }
</AccessibleButton>
</React.Fragment>;
} }
let settingsButton; let settingsButton;
if (shouldShowSpaceSettings(cli, space)) { if (shouldShowSpaceSettings(cli, space)) {
settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => { settingsButton = <AccessibleTooltipButton
className="mx_SpaceRoomView_landing_settingsButton"
onClick={() => {
showSpaceSettings(cli, space); showSpaceSettings(cli, space);
}}> }}
{ _t("Settings") } title={_t("Settings")}
</AccessibleButton>; />;
} }
const onMembersClick = () => { const onMembersClick = () => {
@ -293,17 +348,19 @@ const SpaceLanding = ({ space }) => {
<SpaceInfo space={space} /> <SpaceInfo space={space} />
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} /> <FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
{ inviteButton } { inviteButton }
{ settingsButton }
</div> </div>
<div className="mx_SpaceRoomView_landing_topic"> <div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} /> <RoomTopic room={space} />
</div> </div>
<hr /> <hr />
<div className="mx_SpaceRoomView_landing_adminButtons">
{ addRoomButtons }
{ settingsButton }
</div>
<SpaceHierarchy space={space} showRoom={showRoom} refreshToken={refreshToken} /> <SpaceHierarchy
space={space}
showRoom={showRoom}
refreshToken={refreshToken}
additionalButtons={addRoomButton}
/>
</div>; </div>;
}; };

View file

@ -2655,6 +2655,7 @@
"%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room 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|other": "%(count)s rooms and 1 space",
"%(count)s rooms and 1 space|one": "%(count)s room 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",
"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...",
"Mark as not suggested": "Mark as not suggested", "Mark as not suggested": "Mark as not suggested",
@ -2667,7 +2668,6 @@
"Public space": "Public space", "Public space": "Public space",
"Private space": "Private space", "Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you", "<inviter/> invites you": "<inviter/> invites you",
"Add existing rooms & spaces": "Add existing rooms & spaces",
"Welcome to <name/>": "Welcome to <name/>", "Welcome to <name/>": "Welcome to <name/>",
"Random": "Random", "Random": "Random",
"Support": "Support", "Support": "Support",

View file

@ -83,6 +83,7 @@ export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
if (shouldCreate) { if (shouldCreate) {
await createRoom(opts); await createRoom(opts);
} }
return shouldCreate;
}; };
export const showSpaceInvite = (space: Room, initialText = "") => { export const showSpaceInvite = (space: Room, initialText = "") => {