Merge pull request #5977 from matrix-org/t3chguy/fix/17176
Iterate Spaces admin UX around room managementpull/21833/head
commit
6338ced8a3
|
@ -86,7 +86,7 @@ limitations under the License.
|
|||
color: $primary-fg-color;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
padding: 2px 8px;
|
||||
padding: 4px 12px;
|
||||
font-weight: normal;
|
||||
|
||||
& + .mx_AccessibleButton {
|
||||
|
@ -94,6 +94,11 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_danger_outline,
|
||||
.mx_AccessibleButton_kind_primary_outline {
|
||||
padding: 3px 12px; // to account for the 1px border
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
@ -246,11 +251,17 @@ limitations under the License.
|
|||
grid-row: 1/3;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
padding: 8px 18px;
|
||||
line-height: $font-24px;
|
||||
padding: 4px 16px;
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_danger_outline,
|
||||
.mx_AccessibleButton_kind_primary_outline {
|
||||
padding: 3px 16px; // to account for the 1px border
|
||||
}
|
||||
|
||||
.mx_Checkbox {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -238,7 +238,8 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
|
||||
.mx_SpaceRoomView_landing_inviteButton {
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
padding: 4px 18px 4px 40px;
|
||||
line-height: $font-24px;
|
||||
height: min-content;
|
||||
|
||||
&::before {
|
||||
|
@ -254,6 +255,27 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
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 {
|
||||
|
@ -268,80 +290,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
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 {
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
|
|
@ -76,12 +76,16 @@ limitations under the License.
|
|||
border: 1px solid $button-danger-bg-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled,
|
||||
.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
|
||||
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled {
|
||||
color: $button-danger-disabled-fg-color;
|
||||
background-color: $button-danger-disabled-bg-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
|
||||
color: $button-danger-disabled-bg-color;
|
||||
border-color: $button-danger-disabled-bg-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm {
|
||||
padding: 5px 12px;
|
||||
color: $button-danger-fg-color;
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
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 {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||
|
@ -24,7 +24,7 @@ import {sortBy} from "lodash";
|
|||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import {_t} from "../../languageHandler";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import SearchBox from "./SearchBox";
|
||||
|
@ -40,11 +40,13 @@ import InfoTooltip from "../views/elements/InfoTooltip";
|
|||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
import {getOrder} from "../../stores/SpaceStore";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
|
||||
interface IHierarchyProps {
|
||||
space: Room;
|
||||
initialText?: string;
|
||||
refreshToken?: any;
|
||||
additionalButtons?: ReactNode;
|
||||
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
|
||||
}
|
||||
|
||||
|
@ -107,8 +109,16 @@ const Tile: React.FC<ITileProps> = ({
|
|||
const cliRoom = cli.getRoom(room.room_id);
|
||||
const myMembership = cliRoom?.getMyMembership();
|
||||
|
||||
const onPreviewClick = () => onViewRoomClick(false);
|
||||
const onJoinClick = () => onViewRoomClick(true);
|
||||
const onPreviewClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(false);
|
||||
}
|
||||
const onJoinClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(true);
|
||||
}
|
||||
|
||||
let button;
|
||||
if (myMembership === "join") {
|
||||
|
@ -355,6 +365,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
initialText = "",
|
||||
showRoom,
|
||||
refreshToken,
|
||||
additionalButtons,
|
||||
children,
|
||||
}) => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -420,78 +431,83 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
||||
}
|
||||
|
||||
let editSection;
|
||||
let manageButtons;
|
||||
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 selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||
});
|
||||
|
||||
const disabled = removing || saving;
|
||||
const disabled = !selectedRelations.length || removing || saving;
|
||||
|
||||
buttons = <>
|
||||
<AccessibleButton
|
||||
onClick={async () => {
|
||||
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") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={async () => {
|
||||
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"))
|
||||
}
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
editSection = <span>
|
||||
{ buttons }
|
||||
</span>;
|
||||
manageButtons = <>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
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") }
|
||||
</Button>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
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"))
|
||||
}
|
||||
</Button>
|
||||
</>;
|
||||
}
|
||||
|
||||
let results;
|
||||
|
@ -537,7 +553,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
content = <>
|
||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||
{ countsStr }
|
||||
{ editSection }
|
||||
<span>
|
||||
{ additionalButtons }
|
||||
{ manageButtons }
|
||||
</span>
|
||||
</div>
|
||||
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||
{ error }
|
||||
|
|
|
@ -54,6 +54,12 @@ import FacePile from "../views/elements/FacePile";
|
|||
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
||||
import {sleep} from "../../utils/promise";
|
||||
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 {
|
||||
space: Room;
|
||||
|
@ -217,6 +223,67 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
</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 cli = useContext(MatrixClientContext);
|
||||
const myMembership = useMyRoomMembership(space);
|
||||
|
@ -241,32 +308,20 @@ const SpaceLanding = ({ space }) => {
|
|||
|
||||
const [refreshToken, forceUpdate] = useStateToggle(false);
|
||||
|
||||
let addRoomButtons;
|
||||
let addRoomButton;
|
||||
if (canAddRooms) {
|
||||
addRoomButtons = <React.Fragment>
|
||||
<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>;
|
||||
addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />;
|
||||
}
|
||||
|
||||
let settingsButton;
|
||||
if (shouldShowSpaceSettings(cli, space)) {
|
||||
settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => {
|
||||
showSpaceSettings(cli, space);
|
||||
}}>
|
||||
{ _t("Settings") }
|
||||
</AccessibleButton>;
|
||||
settingsButton = <AccessibleTooltipButton
|
||||
className="mx_SpaceRoomView_landing_settingsButton"
|
||||
onClick={() => {
|
||||
showSpaceSettings(cli, space);
|
||||
}}
|
||||
title={_t("Settings")}
|
||||
/>;
|
||||
}
|
||||
|
||||
const onMembersClick = () => {
|
||||
|
@ -293,17 +348,19 @@ const SpaceLanding = ({ space }) => {
|
|||
<SpaceInfo space={space} />
|
||||
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||
{ inviteButton }
|
||||
{ settingsButton }
|
||||
</div>
|
||||
<div className="mx_SpaceRoomView_landing_topic">
|
||||
<RoomTopic room={space} />
|
||||
</div>
|
||||
<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>;
|
||||
};
|
||||
|
||||
|
|
|
@ -2657,6 +2657,7 @@
|
|||
"%(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",
|
||||
"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",
|
||||
|
@ -2669,7 +2670,6 @@
|
|||
"Public space": "Public space",
|
||||
"Private space": "Private space",
|
||||
"<inviter/> invites you": "<inviter/> invites you",
|
||||
"Add existing rooms & spaces": "Add existing rooms & spaces",
|
||||
"Welcome to <name/>": "Welcome to <name/>",
|
||||
"Random": "Random",
|
||||
"Support": "Support",
|
||||
|
|
|
@ -83,6 +83,7 @@ export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
|
|||
if (shouldCreate) {
|
||||
await createRoom(opts);
|
||||
}
|
||||
return shouldCreate;
|
||||
};
|
||||
|
||||
export const showSpaceInvite = (space: Room, initialText = "") => {
|
||||
|
|
Loading…
Reference in New Issue