Iterate Spaces admin UX around room management
This commit is contained in:
parent
81b97590f6
commit
650933096a
6 changed files with 183 additions and 167 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 = "") => {
|
||||||
|
|
Loading…
Reference in a new issue