Merge pull request #5776 from matrix-org/t3chguy/spaces4.10
Improve discovery of rooms in a space
This commit is contained in:
commit
03ab2dc8e0
6 changed files with 527 additions and 436 deletions
|
@ -31,7 +31,8 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.mx_BaseAvatar {
|
.mx_BaseAvatar {
|
||||||
margin-right: 16px;
|
margin-right: 12px;
|
||||||
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BaseAvatar_image {
|
.mx_BaseAvatar_image {
|
||||||
|
@ -47,6 +48,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
|
font-weight: 400;
|
||||||
color: $secondary-fg-color;
|
color: $secondary-fg-color;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
|
@ -55,38 +57,71 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog_content {
|
.mx_Dialog_content {
|
||||||
// TODO fix scrollbar
|
|
||||||
//display: flex;
|
|
||||||
//flex-direction: column;
|
|
||||||
//height: calc(100% - 80px);
|
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_link {
|
.mx_AccessibleButton_kind_link {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SearchBox {
|
.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 {
|
.mx_SpaceRoomDirectory_listHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: $font-12px;
|
min-height: 32px;
|
||||||
line-height: $font-15px;
|
align-items: center;
|
||||||
color: $secondary-fg-color;
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
|
||||||
.mx_FormButton {
|
.mx_AccessibleButton {
|
||||||
margin-bottom: 8px;
|
padding: 2px 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
& + .mx_AccessibleButton {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> span {
|
> 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 {
|
.mx_SpaceRoomDirectory_list {
|
||||||
margin-top: 8px;
|
margin-top: 16px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
|
||||||
.mx_SpaceRoomDirectory_roomCount {
|
.mx_SpaceRoomDirectory_roomCount {
|
||||||
> h3 {
|
> h3 {
|
||||||
|
@ -106,115 +141,128 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomDirectory_subspace {
|
.mx_SpaceRoomDirectory_subspace {
|
||||||
margin-top: 8px;
|
.mx_BaseAvatar_image {
|
||||||
|
border-radius: 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_SpaceRoomDirectory_roomTile {
|
.mx_SpaceRoomDirectory_subspace_toggle {
|
||||||
padding: 16px;
|
position: absolute;
|
||||||
border-radius: 8px;
|
left: -1px;
|
||||||
border: 1px solid $space-button-outline-color;
|
top: 10px;
|
||||||
margin: 8px 0 16px;
|
height: 16px;
|
||||||
display: flex;
|
width: 16px;
|
||||||
min-height: 76px;
|
border-radius: 4px;
|
||||||
box-sizing: border-box;
|
background-color: $primary-bg-color;
|
||||||
|
|
||||||
&.mx_AccessibleButton:hover {
|
&::before {
|
||||||
background-color: rgba(141, 151, 165, 0.1);
|
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 {
|
.mx_BaseAvatar {
|
||||||
margin-right: 16px;
|
grid-row: 1;
|
||||||
margin-top: 6px;
|
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 {
|
.mx_SpaceRoomDirectory_roomTile_info {
|
||||||
display: inline-block;
|
font-size: $font-12px;
|
||||||
font-size: $font-15px;
|
line-height: $font-15px;
|
||||||
flex-grow: 1;
|
color: $tertiary-fg-color;
|
||||||
height: min-content;
|
grid-row: 2;
|
||||||
margin: auto 0;
|
grid-column: 1/3;
|
||||||
|
|
||||||
.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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomDirectory_actions {
|
.mx_SpaceRoomDirectory_actions {
|
||||||
width: 180px;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-left: 28px;
|
margin-left: 20px;
|
||||||
display: inline-flex;
|
grid-column: 3;
|
||||||
align-items: center;
|
grid-row: 1/3;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
vertical-align: middle;
|
padding: 6px 18px;
|
||||||
|
|
||||||
& + .mx_AccessibleButton {
|
display: none;
|
||||||
margin-left: 24px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="8" cy="8" r="8" fill="#737D8C" style="mix-blend-mode:multiply"/>
|
<circle cx="8" cy="8" r="8" fill="#FF4B55"/>
|
||||||
<rect x="7" y="3" width="2" height="6" rx="1" fill="white"/>
|
<rect x="7" y="3" width="2" height="6" rx="1" fill="white"/>
|
||||||
<rect x="7" y="11" width="2" height="2" rx="1" fill="white"/>
|
<rect x="7" y="11" width="2" height="2" rx="1" fill="white"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 303 B After Width: | Height: | Size: 283 B |
|
@ -14,27 +14,30 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 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 {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import {sortBy} from "lodash";
|
||||||
|
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import {_t} from "../../languageHandler";
|
import {_t} from "../../languageHandler";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
import FormButton from "../views/elements/FormButton";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import SearchBox from "./SearchBox";
|
import SearchBox from "./SearchBox";
|
||||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||||
import RoomName from "../views/elements/RoomName";
|
import RoomName from "../views/elements/RoomName";
|
||||||
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
|
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
|
||||||
import {shouldShowSpaceSettings} from "../../utils/space";
|
|
||||||
import {EnhancedMap} from "../../utils/maps";
|
import {EnhancedMap} from "../../utils/maps";
|
||||||
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||||
import {mediaFromMxc} from "../../customisations/Media";
|
import {mediaFromMxc} from "../../customisations/Media";
|
||||||
|
import InfoTooltip from "../views/elements/InfoTooltip";
|
||||||
|
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||||
|
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -72,215 +75,98 @@ export interface ISpaceSummaryEvent {
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
interface ISubspaceProps {
|
interface ITileProps {
|
||||||
space: ISpaceSummaryRoom;
|
room: ISpaceSummaryRoom;
|
||||||
event?: MatrixEvent;
|
|
||||||
editing?: boolean;
|
editing?: boolean;
|
||||||
onPreviewClick?(): void;
|
suggested?: boolean;
|
||||||
queueAction?(action: IAction): void;
|
selected?: boolean;
|
||||||
onJoinClick?(): void;
|
numChildRooms?: number;
|
||||||
|
hasPermissions?: boolean;
|
||||||
|
onViewRoomClick(autoJoin: boolean): void;
|
||||||
|
onToggleClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubSpace: React.FC<ISubspaceProps> = ({
|
const Tile: React.FC<ITileProps> = ({
|
||||||
space,
|
room,
|
||||||
editing,
|
editing,
|
||||||
event,
|
suggested,
|
||||||
queueAction,
|
selected,
|
||||||
onJoinClick,
|
hasPermissions,
|
||||||
onPreviewClick,
|
onToggleClick,
|
||||||
|
onViewRoomClick,
|
||||||
|
numChildRooms,
|
||||||
children,
|
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 [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||||
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 = <React.Fragment>
|
|
||||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
|
|
||||||
</React.Fragment>;
|
|
||||||
} else {
|
|
||||||
actions = <React.Fragment>
|
|
||||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
|
|
||||||
<StyledCheckbox checked={suggested} onChange={setSuggested} />
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
|
||||||
{ _t("No permissions")}
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
// TODO confirm remove from space click behaviour here
|
|
||||||
} else {
|
|
||||||
if (myMembership === "join") {
|
|
||||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
|
||||||
{ _t("You're in this space")}
|
|
||||||
</span>;
|
|
||||||
} else if (onJoinClick) {
|
|
||||||
actions = <React.Fragment>
|
|
||||||
<AccessibleButton onClick={onPreviewClick} kind="link">
|
|
||||||
{ _t("Preview") }
|
|
||||||
</AccessibleButton>
|
|
||||||
<FormButton onClick={onJoinClick} label={_t("Join")} />
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let url: string;
|
|
||||||
if (space.avatar_url) {
|
|
||||||
url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio));
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="mx_SpaceRoomDirectory_subspace">
|
|
||||||
<div className="mx_SpaceRoomDirectory_subspace_info">
|
|
||||||
<BaseAvatar name={name} idName={space.room_id} url={url} width={24} height={24} />
|
|
||||||
{ name }
|
|
||||||
|
|
||||||
<div className="mx_SpaceRoomDirectory_actions">
|
|
||||||
{ actions }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mx_SpaceRoomDirectory_subspace_children">
|
|
||||||
{ children }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
};
|
|
||||||
|
|
||||||
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 cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const cliRoom = cli.getRoom(room.room_id);
|
const cliRoom = cli.getRoom(room.room_id);
|
||||||
const myMembership = cliRoom?.getMyMembership();
|
const myMembership = cliRoom?.getMyMembership();
|
||||||
|
|
||||||
let actions;
|
const onPreviewClick = () => onViewRoomClick(false);
|
||||||
if (editing && queueAction) {
|
const onJoinClick = () => onViewRoomClick(true);
|
||||||
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 = () => {
|
let button;
|
||||||
_setRemoved(v => {
|
if (myMembership === "join") {
|
||||||
queueAction({
|
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
||||||
event,
|
{ _t("Open") }
|
||||||
removed: !v,
|
</AccessibleButton>;
|
||||||
suggested,
|
} else if (onJoinClick) {
|
||||||
});
|
button = <AccessibleButton onClick={onJoinClick} kind="primary">
|
||||||
return !v;
|
{ _t("Join") }
|
||||||
});
|
</AccessibleButton>;
|
||||||
};
|
}
|
||||||
|
|
||||||
if (removed) {
|
let checkbox;
|
||||||
actions = <React.Fragment>
|
if (onToggleClick) {
|
||||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
|
if (hasPermissions) {
|
||||||
</React.Fragment>;
|
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} />;
|
||||||
} else {
|
|
||||||
actions = <React.Fragment>
|
|
||||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
|
|
||||||
<StyledCheckbox checked={suggested} onChange={setSuggested} />
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
checkbox = <TextWithTooltip
|
||||||
{ _t("No permissions")}
|
tooltip={_t("You don't have permission")}
|
||||||
</span>;
|
onClick={ev => { ev.stopPropagation() }}
|
||||||
}
|
>
|
||||||
// TODO confirm remove from space click behaviour here
|
<StyledCheckbox disabled={true} />
|
||||||
} else {
|
</TextWithTooltip>;
|
||||||
if (myMembership === "join") {
|
|
||||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
|
||||||
{ _t("You're in this room")}
|
|
||||||
</span>;
|
|
||||||
} else if (onJoinClick) {
|
|
||||||
actions = <React.Fragment>
|
|
||||||
<AccessibleButton onClick={onPreviewClick} kind="link">
|
|
||||||
{ _t("Preview") }
|
|
||||||
</AccessibleButton>
|
|
||||||
<FormButton onClick={onJoinClick} label={_t("Join")} />
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let url: string;
|
let url: string;
|
||||||
if (room.avatar_url) {
|
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 = <InfoTooltip tooltip={_t("This room is suggested as a good one to join")}>
|
||||||
|
{ _t("Suggested") }
|
||||||
|
</InfoTooltip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = <React.Fragment>
|
const content = <React.Fragment>
|
||||||
<BaseAvatar name={name} idName={room.room_id} url={url} width={32} height={32} />
|
<BaseAvatar name={name} idName={room.room_id} url={url} width={20} height={20} />
|
||||||
|
<div className="mx_SpaceRoomDirectory_roomTile_name">
|
||||||
|
{ name }
|
||||||
|
{ suggestedSection }
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mx_SpaceRoomDirectory_roomTile_info">
|
<div className="mx_SpaceRoomDirectory_roomTile_info">
|
||||||
<div className="mx_SpaceRoomDirectory_roomTile_name">
|
{ description }
|
||||||
{ name }
|
|
||||||
</div>
|
|
||||||
<div className="mx_SpaceRoomDirectory_roomTile_topic">
|
|
||||||
{ room.topic }
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_SpaceRoomDirectory_roomTile_memberCount">
|
|
||||||
{ room.num_joined_members }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx_SpaceRoomDirectory_actions">
|
<div className="mx_SpaceRoomDirectory_actions">
|
||||||
{ actions }
|
{ button }
|
||||||
|
{ checkbox }
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
||||||
|
@ -290,9 +176,38 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <AccessibleButton className="mx_SpaceRoomDirectory_roomTile" onClick={onPreviewClick}>
|
let childToggle;
|
||||||
{ content }
|
let childSection;
|
||||||
</AccessibleButton>;
|
if (children) {
|
||||||
|
// the chevron is purposefully a div rather than a button as it should be ignored for a11y
|
||||||
|
childToggle = <div
|
||||||
|
className={classNames("mx_SpaceRoomDirectory_subspace_toggle", {
|
||||||
|
mx_SpaceRoomDirectory_subspace_toggle_shown: showChildren,
|
||||||
|
})}
|
||||||
|
onClick={ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
toggleShowChildren();
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
if (showChildren) {
|
||||||
|
childSection = <div className="mx_SpaceRoomDirectory_subspace_children">
|
||||||
|
{ children }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<AccessibleButton
|
||||||
|
className={classNames("mx_SpaceRoomDirectory_roomTile", {
|
||||||
|
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
|
||||||
|
})}
|
||||||
|
onClick={hasPermissions ? onToggleClick : onPreviewClick}
|
||||||
|
>
|
||||||
|
{ content }
|
||||||
|
{ childToggle }
|
||||||
|
</AccessibleButton>
|
||||||
|
{ childSection }
|
||||||
|
</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
|
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
|
||||||
|
@ -325,88 +240,77 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi
|
||||||
interface IHierarchyLevelProps {
|
interface IHierarchyLevelProps {
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
rooms: Map<string, ISpaceSummaryRoom>;
|
rooms: Map<string, ISpaceSummaryRoom>;
|
||||||
editing?: boolean;
|
relations: EnhancedMap<string, Map<string, ISpaceSummaryEvent>>;
|
||||||
relations: EnhancedMap<string, string[]>;
|
|
||||||
parents: Set<string>;
|
parents: Set<string>;
|
||||||
queueAction?(action: IAction): void;
|
selectedMap?: Map<string, Set<string>>;
|
||||||
onPreviewClick(roomId: string): void;
|
onViewRoomClick(roomId: string, autoJoin: boolean): void;
|
||||||
onRemoveFromSpaceClick?(roomId: string): void;
|
onToggleClick?(parentId: string, childId: string): void;
|
||||||
onJoinClick?(roomId: string): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HierarchyLevel = ({
|
export const HierarchyLevel = ({
|
||||||
spaceId,
|
spaceId,
|
||||||
rooms,
|
rooms,
|
||||||
editing,
|
|
||||||
relations,
|
relations,
|
||||||
parents,
|
parents,
|
||||||
onPreviewClick,
|
selectedMap,
|
||||||
onJoinClick,
|
onViewRoomClick,
|
||||||
queueAction,
|
onToggleClick,
|
||||||
}: IHierarchyLevelProps) => {
|
}: IHierarchyLevelProps) => {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const space = cli.getRoom(spaceId);
|
const space = cli.getRoom(spaceId);
|
||||||
// TODO respect order
|
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())
|
||||||
const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => {
|
|
||||||
if (!rooms.has(roomId)) return result; // TODO wat
|
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);
|
result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId);
|
||||||
return result;
|
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);
|
const newParents = new Set(parents).add(spaceId);
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{
|
{
|
||||||
childRooms.map(roomId => (
|
childRooms.map(roomId => (
|
||||||
<RoomTile
|
<Tile
|
||||||
key={roomId}
|
key={roomId}
|
||||||
room={rooms.get(roomId)}
|
room={rooms.get(roomId)}
|
||||||
event={space?.currentState.maySendStateEvent(EventType.SpaceChild, userId)
|
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
|
||||||
? space?.currentState.getStateEvents(EventType.SpaceChild, roomId)
|
selected={selectedMap?.get(spaceId)?.has(roomId)}
|
||||||
: undefined}
|
onViewRoomClick={(autoJoin) => {
|
||||||
editing={editing}
|
onViewRoomClick(roomId, autoJoin);
|
||||||
queueAction={queueAction}
|
|
||||||
onPreviewClick={() => {
|
|
||||||
onPreviewClick(roomId);
|
|
||||||
}}
|
}}
|
||||||
onJoinClick={onJoinClick ? () => {
|
hasPermissions={hasPermissions}
|
||||||
onJoinClick(roomId);
|
onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined}
|
||||||
} : undefined}
|
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => (
|
subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => (
|
||||||
<SubSpace
|
<Tile
|
||||||
key={roomId}
|
key={roomId}
|
||||||
space={rooms.get(roomId)}
|
room={rooms.get(roomId)}
|
||||||
event={space?.currentState.getStateEvents(EventType.SpaceChild, roomId)}
|
numChildRooms={Array.from(relations.get(roomId)?.values() || [])
|
||||||
editing={editing}
|
.filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length}
|
||||||
queueAction={queueAction}
|
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
|
||||||
onPreviewClick={() => {
|
selected={selectedMap?.get(spaceId)?.has(roomId)}
|
||||||
onPreviewClick(roomId);
|
onViewRoomClick={(autoJoin) => {
|
||||||
}}
|
onViewRoomClick(roomId, autoJoin);
|
||||||
onJoinClick={() => {
|
|
||||||
onJoinClick(roomId);
|
|
||||||
}}
|
}}
|
||||||
|
hasPermissions={hasPermissions}
|
||||||
|
onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined}
|
||||||
>
|
>
|
||||||
<HierarchyLevel
|
<HierarchyLevel
|
||||||
spaceId={roomId}
|
spaceId={roomId}
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
editing={editing}
|
|
||||||
relations={relations}
|
relations={relations}
|
||||||
parents={newParents}
|
parents={newParents}
|
||||||
onPreviewClick={onPreviewClick}
|
selectedMap={selectedMap}
|
||||||
onJoinClick={onJoinClick}
|
onViewRoomClick={onViewRoomClick}
|
||||||
queueAction={queueAction}
|
onToggleClick={onToggleClick}
|
||||||
/>
|
/>
|
||||||
</SubSpace>
|
</Tile>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -415,8 +319,8 @@ export const HierarchyLevel = ({
|
||||||
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => {
|
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => {
|
||||||
// TODO pagination
|
// TODO pagination
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
const userId = cli.getUserId();
|
||||||
const [query, setQuery] = useState(initialText);
|
const [query, setQuery] = useState(initialText);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
|
|
||||||
const onCreateRoomClick = () => {
|
const onCreateRoomClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -426,51 +330,19 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
// stored within a ref as we don't need to re-render when it changes
|
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
|
||||||
const pendingActions = useRef(new Map<string, IAction>());
|
|
||||||
|
|
||||||
let adminButton;
|
const [rooms, parentChildMap, childParentMap, viaMap] = useAsyncMemo(async () => {
|
||||||
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 = <React.Fragment>
|
|
||||||
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
|
|
||||||
<span>{ _t("Promoted to users") }</span>
|
|
||||||
</React.Fragment>;
|
|
||||||
} else {
|
|
||||||
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [rooms, relations, viaMap] = useAsyncMemo(async () => {
|
|
||||||
try {
|
try {
|
||||||
const data = await cli.getSpaceSummary(space.roomId);
|
const data = await cli.getSpaceSummary(space.roomId);
|
||||||
|
|
||||||
const parentChildRelations = new EnhancedMap<string, string[]>();
|
const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>();
|
||||||
|
const childParentRelations = new EnhancedMap<string, Set<string>>();
|
||||||
const viaMap = new EnhancedMap<string, Set<string>>();
|
const viaMap = new EnhancedMap<string, Set<string>>();
|
||||||
data.events.map((ev: ISpaceSummaryEvent) => {
|
data.events.map((ev: ISpaceSummaryEvent) => {
|
||||||
if (ev.type === EventType.SpaceChild) {
|
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"])) {
|
if (Array.isArray(ev.content["via"])) {
|
||||||
const set = viaMap.getOrCreate(ev.state_key, new Set());
|
const set = viaMap.getOrCreate(ev.state_key, new Set());
|
||||||
|
@ -478,7 +350,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return [data.rooms, parentChildRelations, viaMap];
|
return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, childParentRelations, viaMap];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e); // TODO
|
console.error(e); // TODO
|
||||||
}
|
}
|
||||||
|
@ -488,54 +360,204 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
|
||||||
|
|
||||||
const roomsMap = useMemo(() => {
|
const roomsMap = useMemo(() => {
|
||||||
if (!rooms) return null;
|
if (!rooms) return null;
|
||||||
const lcQuery = query.toLowerCase();
|
const lcQuery = query.toLowerCase().trim();
|
||||||
|
|
||||||
const filteredRooms = rooms.filter(r => {
|
const roomsMap = new Map<string, ISpaceSummaryRoom>(rooms.map(r => [r.room_id, r]));
|
||||||
return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms
|
if (!lcQuery) return roomsMap;
|
||||||
|| r.name?.toLowerCase().includes(lcQuery)
|
|
||||||
|| r.topic?.toLowerCase().includes(lcQuery);
|
const directMatches = rooms.filter(r => {
|
||||||
|
return r.name?.toLowerCase().includes(lcQuery) || r.topic?.toLowerCase().includes(lcQuery);
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Map<string, ISpaceSummaryRoom>(filteredRooms.map(r => [r.room_id, r]));
|
// Walk back up the tree to find all parents of the direct matches to show their place in the hierarchy
|
||||||
// const root = rooms.get(space.roomId);
|
const visited = new Set<string>();
|
||||||
}, [rooms, query]);
|
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 = <React.Fragment>
|
const title = <React.Fragment>
|
||||||
<RoomAvatar room={space} height={40} width={40} />
|
<RoomAvatar room={space} height={32} width={32} />
|
||||||
<div>
|
<div>
|
||||||
<h1>{ _t("Explore rooms") }</h1>
|
<h1>{ _t("Explore rooms") }</h1>
|
||||||
<div><RoomName room={space} /></div>
|
<div><RoomName room={space} /></div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
||||||
const explanation =
|
const explanation =
|
||||||
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
|
_t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.", null,
|
||||||
{a: sub => {
|
{a: sub => {
|
||||||
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
|
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
|
||||||
}},
|
}},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [removing, setRemoving] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (roomsMap) {
|
if (roomsMap) {
|
||||||
content = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
|
const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
|
||||||
<HierarchyLevel
|
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
||||||
spaceId={space.roomId}
|
|
||||||
rooms={roomsMap}
|
let countsStr;
|
||||||
editing={isEditing}
|
if (numSpaces > 1) {
|
||||||
relations={relations}
|
countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces });
|
||||||
parents={new Set()}
|
} else if (numSpaces > 0) {
|
||||||
queueAction={action => {
|
countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces });
|
||||||
pendingActions.current.set(action.event.room_id, action);
|
} else {
|
||||||
}}
|
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
||||||
onPreviewClick={roomId => {
|
}
|
||||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false);
|
|
||||||
onFinished();
|
let editSection;
|
||||||
}}
|
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||||
onJoinClick={(roomId) => {
|
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
||||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true);
|
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
||||||
onFinished();
|
});
|
||||||
}}
|
|
||||||
/>
|
let buttons;
|
||||||
</AutoHideScrollbar>;
|
if (selectedRelations.length) {
|
||||||
|
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||||
|
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabled = 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>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
editSection = <span>
|
||||||
|
{ buttons }
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let results;
|
||||||
|
if (roomsMap.size) {
|
||||||
|
results = <>
|
||||||
|
<HierarchyLevel
|
||||||
|
spaceId={space.roomId}
|
||||||
|
rooms={roomsMap}
|
||||||
|
relations={parentChildMap}
|
||||||
|
parents={new Set()}
|
||||||
|
selectedMap={selected}
|
||||||
|
onToggleClick={(parentId, childId) => {
|
||||||
|
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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
</>;
|
||||||
|
} else {
|
||||||
|
results = <div className="mx_SpaceRoomDirectory_noResults">
|
||||||
|
<h3>{ _t("No results found") }</h3>
|
||||||
|
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = <>
|
||||||
|
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||||
|
{ countsStr }
|
||||||
|
{ editSection }
|
||||||
|
</div>
|
||||||
|
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||||
|
{ error }
|
||||||
|
</div> }
|
||||||
|
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
|
||||||
|
{ results }
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={onCreateRoomClick}
|
||||||
|
kind="primary"
|
||||||
|
className="mx_SpaceRoomDirectory_createRoom"
|
||||||
|
>
|
||||||
|
{ _t("Create room") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
</>;
|
||||||
|
} else {
|
||||||
|
content = <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO loading state/error state
|
// TODO loading state/error state
|
||||||
|
@ -546,13 +568,10 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
|
||||||
|
|
||||||
<SearchBox
|
<SearchBox
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={ _t("Find a room...") }
|
placeholder={ _t("Search names and description") }
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
|
||||||
{ adminButton }
|
|
||||||
</div>
|
|
||||||
{ content }
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -257,10 +257,10 @@ const SpaceLanding = ({ space }) => {
|
||||||
try {
|
try {
|
||||||
const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join");
|
const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join");
|
||||||
|
|
||||||
const parentChildRelations = new EnhancedMap<string, string[]>();
|
const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>();
|
||||||
data.events.map((ev: ISpaceSummaryEvent) => {
|
data.events.map((ev: ISpaceSummaryEvent) => {
|
||||||
if (ev.type === EventType.SpaceChild) {
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -284,11 +284,10 @@ const SpaceLanding = ({ space }) => {
|
||||||
<HierarchyLevel
|
<HierarchyLevel
|
||||||
spaceId={space.roomId}
|
spaceId={space.roomId}
|
||||||
rooms={roomsMap}
|
rooms={roomsMap}
|
||||||
editing={false}
|
|
||||||
relations={relations}
|
relations={relations}
|
||||||
parents={new Set()}
|
parents={new Set()}
|
||||||
onPreviewClick={roomId => {
|
onViewRoomClick={(roomId, autoJoin) => {
|
||||||
showRoom(roomsMap.get(roomId), [], false); // TODO
|
showRoom(roomsMap.get(roomId), [], autoJoin);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</AutoHideScrollbar>;
|
</AutoHideScrollbar>;
|
||||||
|
|
|
@ -46,12 +46,14 @@ export default class TextWithTooltip extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||||
|
|
||||||
|
const {class: className, children, tooltip, tooltipClass, ...props} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={this.props.class}>
|
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
|
||||||
{this.props.children}
|
{children}
|
||||||
{this.state.hover && <Tooltip
|
{this.state.hover && <Tooltip
|
||||||
label={this.props.tooltip}
|
label={tooltip}
|
||||||
tooltipClassName={this.props.tooltipClass}
|
tooltipClassName={tooltipClass}
|
||||||
className={"mx_TextWithTooltip_tooltip"} /> }
|
className={"mx_TextWithTooltip_tooltip"} /> }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2607,20 +2607,30 @@
|
||||||
"Drop file here to upload": "Drop file here to upload",
|
"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.|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.",
|
"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",
|
"Open": "Open",
|
||||||
"Remove from Space": "Remove from Space",
|
"You don't have permission": "You don't have permission",
|
||||||
"No permissions": "No permissions",
|
"%(count)s members|other": "%(count)s members",
|
||||||
"You're in this space": "You're in this space",
|
"%(count)s members|one": "%(count)s member",
|
||||||
"You're in this room": "You're in this room",
|
"%(count)s rooms|other": "%(count)s rooms",
|
||||||
"Save changes": "Save changes",
|
"%(count)s rooms|one": "%(count)s room",
|
||||||
"Promoted to users": "Promoted to users",
|
"This room is suggested as a good one to join": "This room is suggested as a good one to join",
|
||||||
"Manage rooms": "Manage rooms",
|
"Suggested": "Suggested",
|
||||||
"Find a room...": "Find a room...",
|
"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>.",
|
||||||
|
"%(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",
|
||||||
"<inviter/> invites you": "<inviter/> invites you",
|
"<inviter/> invites you": "<inviter/> invites you",
|
||||||
"Public space": "Public space",
|
"Public space": "Public space",
|
||||||
"Private space": "Private 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",
|
"Add existing rooms & spaces": "Add existing rooms & spaces",
|
||||||
"Default Rooms": "Default Rooms",
|
"Default Rooms": "Default Rooms",
|
||||||
"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.",
|
||||||
|
|
Loading…
Reference in a new issue