Merge pull request #5792 from matrix-org/t3chguy/spaces4.12

Fixing spaces papercuts
This commit is contained in:
Michael Telatynski 2021-03-26 15:01:31 +00:00 committed by GitHub
commit 83612dd4ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 502 additions and 349 deletions

View file

@ -117,6 +117,7 @@
@import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_EditableItemList.scss";
@import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_ErrorBoundary.scss";
@import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_EventListSummary.scss";
@import "./views/elements/_FacePile.scss";
@import "./views/elements/_Field.scss"; @import "./views/elements/_Field.scss";
@import "./views/elements/_FormButton.scss"; @import "./views/elements/_FormButton.scss";
@import "./views/elements/_ImageView.scss"; @import "./views/elements/_ImageView.scss";

View file

@ -22,7 +22,7 @@ limitations under the License.
// keep border thickness consistent to prevent movement // keep border thickness consistent to prevent movement
border: 1px solid transparent; border: 1px solid transparent;
height: 28px; height: 28px;
padding: 2px; padding: 1px;
// Create a flexbox for the icons (easier to manage) // Create a flexbox for the icons (easier to manage)
display: flex; display: flex;

View file

@ -330,10 +330,6 @@ $activeBorderColor: $secondary-fg-color;
mask-image: url('$(res)/img/element-icons/leave.svg'); mask-image: url('$(res)/img/element-icons/leave.svg');
} }
.mx_SpacePanel_iconHome::before {
mask-image: url('$(res)/img/element-icons/roomlist/home.svg');
}
.mx_SpacePanel_iconMembers::before { .mx_SpacePanel_iconMembers::before {
mask-image: url('$(res)/img/element-icons/room/members.svg'); mask-image: url('$(res)/img/element-icons/room/members.svg');
} }

View file

@ -182,7 +182,7 @@ limitations under the License.
.mx_SpaceRoomDirectory_roomTile { .mx_SpaceRoomDirectory_roomTile {
position: relative; position: relative;
padding: 6px 16px; padding: 8px 16px;
border-radius: 8px; border-radius: 8px;
min-height: 56px; min-height: 56px;
box-sizing: border-box; box-sizing: border-box;
@ -190,6 +190,7 @@ limitations under the License.
display: grid; display: grid;
grid-template-columns: 20px auto max-content; grid-template-columns: 20px auto max-content;
grid-column-gap: 8px; grid-column-gap: 8px;
grid-row-gap: 6px;
align-items: center; align-items: center;
.mx_BaseAvatar { .mx_BaseAvatar {
@ -213,16 +214,28 @@ limitations under the License.
.mx_InfoTooltip_icon { .mx_InfoTooltip_icon {
margin-right: 4px; margin-right: 4px;
position: relative;
vertical-align: text-top;
&::before {
position: absolute;
top: 0;
left: 0;
}
} }
} }
} }
.mx_SpaceRoomDirectory_roomTile_info { .mx_SpaceRoomDirectory_roomTile_info {
font-size: $font-12px; font-size: $font-14px;
line-height: $font-15px; line-height: $font-18px;
color: $tertiary-fg-color; color: $secondary-fg-color;
grid-row: 2; grid-row: 2;
grid-column: 1/3; grid-column: 1/3;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
} }
.mx_SpaceRoomDirectory_actions { .mx_SpaceRoomDirectory_actions {
@ -232,9 +245,9 @@ limitations under the License.
grid-row: 1/3; grid-row: 1/3;
.mx_AccessibleButton { .mx_AccessibleButton {
padding: 6px 18px; padding: 8px 18px;
display: inline-block;
display: none; visibility: hidden;
} }
.mx_Checkbox { .mx_Checkbox {
@ -248,7 +261,7 @@ limitations under the License.
background-color: $groupFilterPanel-bg-color; background-color: $groupFilterPanel-bg-color;
.mx_AccessibleButton { .mx_AccessibleButton {
display: inline-block; visibility: visible;
} }
} }
} }

View file

@ -22,7 +22,7 @@ $SpaceRoomViewInnerWidth: 428px;
width: 432px; width: 432px;
box-sizing: border-box; box-sizing: border-box;
border-radius: 8px; border-radius: 8px;
border: 1px solid $space-button-outline-color; border: 1px solid $input-border-color;
font-size: $font-15px; font-size: $font-15px;
margin: 20px 0; margin: 20px 0;
@ -122,7 +122,6 @@ $SpaceRoomViewInnerWidth: 428px;
max-width: 480px; max-width: 480px;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 2px 15px 30px $dialog-shadow-color; box-shadow: 2px 15px 30px $dialog-shadow-color;
border: 1px solid $input-border-color;
border-radius: 8px; border-radius: 8px;
.mx_SpaceRoomView_preview_inviter { .mx_SpaceRoomView_preview_inviter {
@ -154,53 +153,6 @@ $SpaceRoomViewInnerWidth: 428px;
margin: 20px 0 !important; // override default margin from above margin: 20px 0 !important; // override default margin from above
} }
.mx_SpaceRoomView_preview_info {
color: $tertiary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
margin: 20px 0;
.mx_SpaceRoomView_preview_info_public,
.mx_SpaceRoomView_preview_info_private {
padding-left: 20px;
position: relative;
&::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
top: 0;
left: -2px;
mask-position: center;
mask-repeat: no-repeat;
background-color: $tertiary-fg-color;
}
}
.mx_SpaceRoomView_preview_info_public::before {
mask-size: 12px;
mask-image: url("$(res)/img/globe.svg");
}
.mx_SpaceRoomView_preview_info_private::before {
mask-size: 14px;
mask-image: url("$(res)/img/element-icons/lock.svg");
}
.mx_AccessibleButton_kind_link {
color: inherit;
position: relative;
padding-left: 16px;
&::before {
content: "·"; // visual separator
position: absolute;
left: 6px;
}
}
}
.mx_SpaceRoomView_preview_topic { .mx_SpaceRoomView_preview_topic {
font-size: $font-14px; font-size: $font-14px;
line-height: $font-22px; line-height: $font-22px;
@ -254,36 +206,90 @@ $SpaceRoomViewInnerWidth: 428px;
vertical-align: middle; vertical-align: middle;
} }
} }
}
.mx_SpaceRoomView_landing_memberCount { .mx_SpaceRoomView_landing_info {
display: flex;
align-items: center;
.mx_SpaceRoomView_info {
display: inline-block;
margin: 0;
}
.mx_FacePile {
display: inline-block;
margin-left: auto;
margin-right: 12px;
.mx_FacePile_faces {
cursor: pointer;
> span:hover {
.mx_BaseAvatar {
filter: brightness(0.8);
}
}
> span:first-child {
position: relative; position: relative;
margin-left: 24px;
padding: 0 0 0 28px; .mx_BaseAvatar {
line-height: $font-24px; filter: brightness(0.8);
vertical-align: text-bottom; }
&::before {
content: "";
z-index: 1;
position: absolute;
top: 0;
left: 0;
height: 30px;
width: 30px;
background: #ffffff; // white icon fill
mask-position: center;
mask-size: 24px;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
}
}
}
}
.mx_SpaceRoomView_landing_inviteButton {
position: relative;
padding-left: 40px;
height: min-content;
&::before { &::before {
position: absolute; position: absolute;
content: ''; content: "";
width: 24px; left: 8px;
height: 24px; height: 16px;
top: 0; width: 16px;
left: 0; background: #ffffff; // white icon fill
mask-position: center; mask-position: center;
mask-size: 16px;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: contain; mask-image: url('$(res)/img/element-icons/room/invite.svg');
background-color: $accent-color;
mask-image: url('$(res)/img/element-icons/community-members.svg');
} }
} }
} }
.mx_SpaceRoomView_landing_topic { .mx_SpaceRoomView_landing_topic {
font-size: $font-15px; font-size: $font-15px;
margin-top: 12px;
margin-bottom: 16px;
}
> hr {
border: none;
height: 1px;
background-color: $groupFilterPanel-bg-color;
} }
.mx_SpaceRoomView_landing_adminButtons { .mx_SpaceRoomView_landing_adminButtons {
margin-top: 32px; margin-top: 24px;
.mx_AccessibleButton { .mx_AccessibleButton {
position: relative; position: relative;
@ -292,9 +298,9 @@ $SpaceRoomViewInnerWidth: 428px;
box-sizing: border-box; box-sizing: border-box;
padding: 72px 16px 0; padding: 72px 16px 0;
border-radius: 12px; border-radius: 12px;
border: 1px solid $space-button-outline-color; border: 1px solid $input-border-color;
margin-right: 28px; margin-right: 28px;
margin-bottom: 28px; margin-bottom: 20px;
font-size: $font-14px; font-size: $font-14px;
display: inline-block; display: inline-block;
vertical-align: bottom; vertical-align: bottom;
@ -324,16 +330,6 @@ $SpaceRoomViewInnerWidth: 428px;
background: #ffffff; // white icon fill background: #ffffff; // white icon fill
} }
&.mx_SpaceRoomView_landing_inviteButton {
&::before {
background-color: $accent-color;
}
&::after {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
&.mx_SpaceRoomView_landing_addButton { &.mx_SpaceRoomView_landing_addButton {
&::before { &::before {
background-color: #ac3ba8; background-color: #ac3ba8;
@ -366,12 +362,8 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
.mx_SpaceRoomDirectory_list { .mx_SearchBox {
max-width: 600px; margin: 0 0 20px;
.mx_SpaceRoomDirectory_roomTile_actions {
display: none;
}
} }
} }
@ -424,3 +416,50 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
} }
.mx_SpaceRoomView_info {
color: $secondary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
margin: 20px 0;
.mx_SpaceRoomView_info_public,
.mx_SpaceRoomView_info_private {
padding-left: 20px;
position: relative;
&::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
top: 0;
left: -2px;
mask-position: center;
mask-repeat: no-repeat;
background-color: $tertiary-fg-color;
}
}
.mx_SpaceRoomView_info_public::before {
mask-size: 12px;
mask-image: url("$(res)/img/globe.svg");
}
.mx_SpaceRoomView_info_private::before {
mask-size: 14px;
mask-image: url("$(res)/img/element-icons/lock.svg");
}
.mx_AccessibleButton_kind_link {
color: inherit;
position: relative;
padding-left: 16px;
&::before {
content: "·"; // visual separator
position: absolute;
left: 6px;
}
}
}

View file

@ -0,0 +1,42 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_FacePile {
.mx_FacePile_faces {
display: inline-flex;
flex-direction: row-reverse;
vertical-align: middle;
> span + span {
margin-right: -8px;
}
.mx_BaseAvatar_image {
border: 1px solid $primary-bg-color;
}
.mx_BaseAvatar_initial {
margin: 1px; // to offset the border on the image
}
}
> span {
margin-left: 12px;
font-size: $font-14px;
line-height: $font-24px;
color: $tertiary-fg-color;
}
}

View file

@ -37,7 +37,7 @@ limitations under the License.
.mx_RoomList_explorePrompt { .mx_RoomList_explorePrompt {
margin: 4px 12px 4px; margin: 4px 12px 4px;
padding-top: 12px; padding-top: 12px;
border-top: 1px solid $tertiary-fg-color; border-top: 1px solid $input-border-color;
font-size: $font-14px; font-size: $font-14px;
div:first-child { div:first-child {

View file

@ -123,7 +123,6 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: rgba(141, 151, 165, 0.2);
$roomtile-preview-color: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -120,7 +120,6 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: rgba(141, 151, 165, 0.2);
$roomtile-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -187,7 +187,6 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: #E3E8F0;
$voice-record-stop-border-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color; $voice-record-stop-symbol-color: $warning-color;

View file

@ -178,7 +178,6 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: #E3E8F0;
$voice-record-stop-border-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color; $voice-record-stop-symbol-color: $warning-color;

View file

@ -80,10 +80,10 @@ import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages"; import { shouldUseLoginForWelcome } from "../../utils/pages";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import SpaceRoomDirectory from "./SpaceRoomDirectory";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore"; import RoomListStore from "../../stores/room-list/RoomListStore";
import {RoomUpdateCause} from "../../stores/room-list/models"; import {RoomUpdateCause} from "../../stores/room-list/models";
import defaultDispatcher from "../../dispatcher/dispatcher";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -690,10 +690,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
case Action.ViewRoomDirectory: { case Action.ViewRoomDirectory: {
if (SpaceStore.instance.activeSpace) { if (SpaceStore.instance.activeSpace) {
Modal.createTrackedDialog("Space room directory", "", SpaceRoomDirectory, { defaultDispatcher.dispatch({
space: SpaceStore.instance.activeSpace, action: "view_room",
initialText: payload.initialText, room_id: SpaceStore.instance.activeSpace.roomId,
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true); });
} else { } else {
const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
Modal.createTrackedDialog('Room directory', '', RoomDirectory, { Modal.createTrackedDialog('Room directory', '', RoomDirectory, {

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -26,6 +26,7 @@ import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore"; import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -53,6 +54,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
}; };
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
} }
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void { public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
@ -72,6 +75,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
public componentWillUnmount() { public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
} }
private onAction = (payload: ActionPayload) => { private onAction = (payload: ActionPayload) => {

View file

@ -40,10 +40,11 @@ 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";
interface IProps { interface IHierarchyProps {
space: Room; space: Room;
initialText?: string; initialText?: string;
onFinished(): void; refreshToken?: any;
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
} }
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -111,7 +112,7 @@ const Tile: React.FC<ITileProps> = ({
let button; let button;
if (myMembership === "join") { if (myMembership === "join") {
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline"> button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
{ _t("Open") } { _t("View") }
</AccessibleButton>; </AccessibleButton>;
} else if (onJoinClick) { } else if (onJoinClick) {
button = <AccessibleButton onClick={onJoinClick} kind="primary"> button = <AccessibleButton onClick={onJoinClick} kind="primary">
@ -251,7 +252,7 @@ export const HierarchyLevel = ({
}: IHierarchyLevelProps) => { }: IHierarchyLevelProps) => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const space = cli.getRoom(spaceId); const space = cli.getRoom(spaceId);
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()) const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null); const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null);
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
@ -344,22 +345,20 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a
}, [space, refreshToken], []); }, [space, refreshToken], []);
}; };
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => { export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
space,
initialText = "",
showRoom,
refreshToken,
children,
}) => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const userId = cli.getUserId(); const userId = cli.getUserId();
const [query, setQuery] = useState(initialText); const [query, setQuery] = useState(initialText);
const onCreateRoomClick = () => {
dis.dispatch({
action: 'view_create_room',
public: true,
});
onFinished();
};
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>> const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space); const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken);
const roomsMap = useMemo(() => { const roomsMap = useMemo(() => {
if (!rooms) return null; if (!rooms) return null;
@ -394,21 +393,6 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
return roomsMap; return roomsMap;
}, [rooms, childParentMap, query]); }, [rooms, childParentMap, query]);
const title = <React.Fragment>
<RoomAvatar room={space} height={32} width={32} />
<div>
<h1>{ _t("Explore rooms") }</h1>
<div><RoomName room={space} /></div>
</div>
</React.Fragment>;
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,
{a: sub => {
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
}},
);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [removing, setRemoving] = useState(false); const [removing, setRemoving] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -503,6 +487,8 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
let results; let results;
if (roomsMap.size) { if (roomsMap.size) {
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
results = <> results = <>
<HierarchyLevel <HierarchyLevel
spaceId={space.roomId} spaceId={space.roomId}
@ -510,7 +496,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
relations={parentChildMap} relations={parentChildMap}
parents={new Set()} parents={new Set()}
selectedMap={selected} selectedMap={selected}
onToggleClick={(parentId, childId) => { onToggleClick={hasPermissions ? (parentId, childId) => {
setError(""); setError("");
if (!selected.has(parentId)) { if (!selected.has(parentId)) {
setSelected(new Map(selected.set(parentId, new Set([childId])))); setSelected(new Map(selected.set(parentId, new Set([childId]))));
@ -525,13 +511,12 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
parentSet.delete(childId); parentSet.delete(childId);
setSelected(new Map(selected.set(parentId, new Set(parentSet)))); setSelected(new Map(selected.set(parentId, new Set(parentSet))));
}} } : undefined}
onViewRoomClick={(roomId, autoJoin) => { onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
onFinished();
}} }}
/> />
<hr /> { children && <hr /> }
</>; </>;
} else { } else {
results = <div className="mx_SpaceRoomDirectory_noResults"> results = <div className="mx_SpaceRoomDirectory_noResults">
@ -550,25 +535,17 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
</div> } </div> }
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list"> <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
{ results } { results }
<AccessibleButton { children }
onClick={onCreateRoomClick}
kind="primary"
className="mx_SpaceRoomDirectory_createRoom"
>
{ _t("Create room") }
</AccessibleButton>
</AutoHideScrollbar> </AutoHideScrollbar>
</>; </>;
} else { } else if (!rooms) {
content = <Spinner />; content = <Spinner />;
} else {
content = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
} }
// TODO loading state/error state // TODO loading state/error state
return ( return <>
<BaseDialog className="mx_SpaceRoomDirectory" hasCancel={true} onFinished={onFinished} title={title}>
<div className="mx_Dialog_content">
{ explanation }
<SearchBox <SearchBox
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Search names and description") } placeholder={ _t("Search names and description") }
@ -578,6 +555,58 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
/> />
{ content } { content }
</>;
};
interface IProps {
space: Room;
initialText?: string;
onFinished(): void;
}
const SpaceRoomDirectory: React.FC<IProps> = ({ space, onFinished, initialText }) => {
const onCreateRoomClick = () => {
dis.dispatch({
action: 'view_create_room',
public: true,
});
onFinished();
};
const title = <React.Fragment>
<RoomAvatar room={space} height={32} width={32} />
<div>
<h1>{ _t("Explore rooms") }</h1>
<div><RoomName room={space} /></div>
</div>
</React.Fragment>;
return (
<BaseDialog className="mx_SpaceRoomDirectory" hasCancel={true} onFinished={onFinished} title={title}>
<div className="mx_Dialog_content">
{ _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 => {
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
}},
) }
<SpaceHierarchy
space={space}
showRoom={(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
showRoom(room, viaServers, autoJoin);
onFinished();
}}
initialText={initialText}
>
<AccessibleButton
onClick={onCreateRoomClick}
kind="primary"
className="mx_SpaceRoomDirectory_createRoom"
>
{ _t("Create room") }
</AccessibleButton>
</SpaceHierarchy>
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {RefObject, useContext, useMemo, useRef, useState} from "react"; import React, {RefObject, useContext, useRef, useState} from "react";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {EventType} from "matrix-js-sdk/src/@types/event";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import {EventSubscription} from "fbemitter"; import {EventSubscription} from "fbemitter";
@ -46,11 +46,11 @@ import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanel
import {useStateArray} from "../../hooks/useStateArray"; import {useStateArray} from "../../hooks/useStateArray";
import SpacePublicShare from "../views/spaces/SpacePublicShare"; import SpacePublicShare from "../views/spaces/SpacePublicShare";
import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space"; import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
import {HierarchyLevel, ISpaceSummaryRoom, showRoom, useSpaceSummary} from "./SpaceRoomDirectory"; import {showRoom, SpaceHierarchy} from "./SpaceRoomDirectory";
import AutoHideScrollbar from "./AutoHideScrollbar";
import MemberAvatar from "../views/avatars/MemberAvatar"; import MemberAvatar from "../views/avatars/MemberAvatar";
import {useStateToggle} from "../../hooks/useStateToggle"; import {useStateToggle} from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile";
interface IProps { interface IProps {
space: Room; space: Room;
@ -92,6 +92,41 @@ const useMyRoomMembership = (room: Room) => {
return membership; return membership;
}; };
const SpaceInfo = ({ space }) => {
const joinRule = space.getJoinRule();
let visibilitySection;
if (joinRule === "public") {
visibilitySection = <span className="mx_SpaceRoomView_info_public">
{ _t("Public space") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_info_private">
{ _t("Private space") }
</span>;
}
return <div className="mx_SpaceRoomView_info">
{ visibilitySection }
{ joinRule === "public" && <RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount> }
</div>
};
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space); const myMembership = useMyRoomMembership(space);
@ -158,43 +193,13 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
joinButtons = <InlineSpinner />; joinButtons = <InlineSpinner />;
} }
let visibilitySection;
if (space.getJoinRule() === "public") {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_public">
{ _t("Public space") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_private">
{ _t("Private space") }
</span>;
}
return <div className="mx_SpaceRoomView_preview"> return <div className="mx_SpaceRoomView_preview">
{ inviterSection } { inviterSection }
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} /> <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<h1 className="mx_SpaceRoomView_preview_name"> <h1 className="mx_SpaceRoomView_preview_name">
<RoomName room={space} /> <RoomName room={space} />
</h1> </h1>
<div className="mx_SpaceRoomView_preview_info"> <SpaceInfo space={space} />
{ visibilitySection }
<RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
className="mx_SpaceRoomView_preview_memberCount"
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount>
</div>
<RoomTopic room={space}> <RoomTopic room={space}>
{(topic, ref) => {(topic, ref) =>
<div className="mx_SpaceRoomView_preview_topic" ref={ref}> <div className="mx_SpaceRoomView_preview_topic" ref={ref}>
@ -202,6 +207,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
</div> </div>
} }
</RoomTopic> </RoomTopic>
{ space.getJoinRule() === "public" && <FacePile room={space} /> }
<div className="mx_SpaceRoomView_preview_joinButtons"> <div className="mx_SpaceRoomView_preview_joinButtons">
{ joinButtons } { joinButtons }
</div> </div>
@ -216,10 +222,14 @@ const SpaceLanding = ({ space }) => {
let inviteButton; let inviteButton;
if (myMembership === "join" && space.canInvite(userId)) { if (myMembership === "join" && space.canInvite(userId)) {
inviteButton = ( inviteButton = (
<AccessibleButton className="mx_SpaceRoomView_landing_inviteButton" onClick={() => { <AccessibleButton
kind="primary"
className="mx_SpaceRoomView_landing_inviteButton"
onClick={() => {
showRoomInviteDialog(space.roomId); showRoomInviteDialog(space.roomId);
}}> }}
{ _t("Invite people") } >
{ _t("Invite") }
</AccessibleButton> </AccessibleButton>
); );
} }
@ -256,36 +266,13 @@ const SpaceLanding = ({ space }) => {
</AccessibleButton>; </AccessibleButton>;
} }
const [rooms, relations, viaMap] = useSpaceSummary(cli, space, refreshToken); const onMembersClick = () => {
const [roomsMap, numRooms] = useMemo(() => { defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
if (!rooms) return []; action: Action.SetRightPanelPhase,
const roomsMap = new Map<string, ISpaceSummaryRoom>(rooms.map(r => [r.room_id, r])); phase: RightPanelPhases.RoomMemberList,
const numRooms = rooms.filter(r => r.room_type !== RoomType.Space).length; refireParams: { space },
return [roomsMap, numRooms]; });
}, [rooms]); };
let previewRooms;
if (roomsMap) {
previewRooms = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
<div className="mx_SpaceRoomDirectory_roomCount">
<h3>{ myMembership === "join" ? _t("Rooms") : _t("Default Rooms")}</h3>
<span>{ numRooms }</span>
</div>
<HierarchyLevel
spaceId={space.roomId}
rooms={roomsMap}
relations={relations}
parents={new Set()}
onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
}}
/>
</AutoHideScrollbar>;
} else if (!rooms) {
previewRooms = <InlineSpinner />;
} else {
previewRooms = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
}
return <div className="mx_SpaceRoomView_landing"> return <div className="mx_SpaceRoomView_landing">
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} /> <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
@ -294,45 +281,26 @@ const SpaceLanding = ({ space }) => {
{(name) => { {(name) => {
const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow"> const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
<h1>{ name }</h1> <h1>{ name }</h1>
<RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
className="mx_SpaceRoomView_landing_memberCount"
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount>
</div> }; </div> };
if (shouldShowSpaceSettings(cli, space)) {
if (space.getJoinRule() === "public") {
return _t("Your public space <name/>", {}, tags) as JSX.Element;
} else {
return _t("Your private space <name/>", {}, tags) as JSX.Element;
}
}
return _t("Welcome to <name/>", {}, tags) as JSX.Element; return _t("Welcome to <name/>", {}, tags) as JSX.Element;
}} }}
</RoomName> </RoomName>
</div> </div>
<div className="mx_SpaceRoomView_landing_info">
<SpaceInfo space={space} />
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
{ inviteButton }
</div>
<div className="mx_SpaceRoomView_landing_topic"> <div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} /> <RoomTopic room={space} />
</div> </div>
<hr />
<div className="mx_SpaceRoomView_landing_adminButtons"> <div className="mx_SpaceRoomView_landing_adminButtons">
{ inviteButton }
{ addRoomButtons } { addRoomButtons }
{ settingsButton } { settingsButton }
</div> </div>
{ previewRooms } <SpaceHierarchy space={space} showRoom={showRoom} refreshToken={refreshToken} />
</div>; </div>;
}; };
@ -675,9 +643,13 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
case Phase.PublicCreateRooms: case Phase.PublicCreateRooms:
return <SpaceSetupFirstRooms return <SpaceSetupFirstRooms
space={this.props.space} space={this.props.space}
title={_t("What are some things you want to discuss?")} title={_t("What are some things you want to discuss in %(spaceName)s?", {
description={_t("Let's create a room for each of them. " + spaceName: this.props.space.name,
"You can add more later too, including already existing ones.")} })}
description={
_t("Let's create a room for each of them.") + "\n" +
_t("You can add more later too, including already existing ones.")
}
onFinished={() => this.setState({ phase: Phase.PublicShare })} onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>; />;
case Phase.PublicShare: case Phase.PublicShare:

View file

@ -126,8 +126,8 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
<div> <div>
{ _t("Make this space private") } { _t("Make this space private") }
<ToggleSwitch <ToggleSwitch
checked={joinRule === "private"} checked={joinRule !== "public"}
onChange={checked => setJoinRule(checked ? "private" : "invite")} onChange={checked => setJoinRule(checked ? "invite" : "public")}
disabled={!canSetJoinRule} disabled={!canSetJoinRule}
aria-label={_t("Make this space private")} aria-label={_t("Make this space private")}
/> />

View file

@ -0,0 +1,66 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { HTMLAttributes } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { sortBy } from "lodash";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from "../../../languageHandler";
import DMRoomMap from "../../../utils/DMRoomMap";
import TextWithTooltip from "../elements/TextWithTooltip";
import { useRoomMembers } from "../../../hooks/useRoomMembers";
const DEFAULT_NUM_FACES = 5;
interface IProps extends HTMLAttributes<HTMLSpanElement> {
room: Room;
onlyKnownUsers?: boolean;
numShown?: number;
}
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => {
let members = useRoomMembers(room);
// sort users with an explicit avatar first
const iteratees = [member => !!member.getMxcAvatarUrl()];
if (onlyKnownUsers) {
members = members.filter(isKnownMember);
} else {
// sort known users first
iteratees.unshift(member => isKnownMember(member));
}
if (members.length < 1) return null;
const shownMembers = sortBy(members, iteratees).slice(0, numShown);
return <div {...props} className="mx_FacePile">
<div className="mx_FacePile_faces">
{ shownMembers.map(member => {
return <TextWithTooltip key={member.userId} tooltip={member.name}>
<MemberAvatar member={member} width={28} height={28} />
</TextWithTooltip>;
}) }
</div>
{ onlyKnownUsers && <span>
{ _t("%(count)s people you know have already joined", { count: members.length }) }
</span> }
</div>
};
export default FacePile;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -29,6 +29,7 @@ import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
import {showSpaceInvite} from "../../../utils/space";
const NewRoomIntro = () => { const NewRoomIntro = () => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
@ -116,7 +117,7 @@ const NewRoomIntro = () => {
className="mx_NewRoomIntro_inviteButton" className="mx_NewRoomIntro_inviteButton"
kind="primary" kind="primary"
onClick={() => { onClick={() => {
dis.dispatch({ action: "view_invite", roomId }); showSpaceInvite(parentSpace);
}} }}
> >
{_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })} {_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })}

View file

@ -50,14 +50,10 @@ import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import CallHandler from "../../../CallHandler"; import CallHandler from "../../../CallHandler";
import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore"; import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space"; import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory"; import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
import { showRoomInviteDialog } from "../../../RoomInvite";
import Modal from "../../../Modal";
import SpacePublicShare from "../spaces/SpacePublicShare";
import InfoDialog from "../dialogs/InfoDialog";
interface IProps { interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void; onKeyDown: (ev: React.KeyboardEvent) => void;
@ -431,21 +427,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private onSpaceInviteClick = () => { private onSpaceInviteClick = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search; const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
if (this.props.activeSpace.getJoinRule() === "public") { showSpaceInvite(this.props.activeSpace, initialText);
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite to %(spaceName)s", { spaceName: this.props.activeSpace.name }),
description: <React.Fragment>
<span>{ _t("Share your public space") }</span>
<SpacePublicShare space={this.props.activeSpace} onFinished={() => modal.close()} />
</React.Fragment>,
fixedWidth: false,
button: false,
className: "mx_SpacePanel_sharePublicSpace",
hasCloseButton: true,
});
} else {
showRoomInviteDialog(this.props.activeSpace.roomId, initialText);
}
}; };
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] { private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {

View file

@ -148,7 +148,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
<SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} /> <SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} />
<AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={!name && !busy}> <AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={!name || busy}>
{ busy ? _t("Creating...") : _t("Create") } { busy ? _t("Creating...") : _t("Create") }
</AccessibleButton> </AccessibleButton>
</React.Fragment>; </React.Fragment>;

View file

@ -34,21 +34,17 @@ import {
shouldShowSpaceSettings, shouldShowSpaceSettings,
showAddExistingRooms, showAddExistingRooms,
showCreateNewRoom, showCreateNewRoom,
showSpaceInvite,
showSpaceSettings, showSpaceSettings,
} from "../../../utils/space"; } from "../../../utils/space";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton"; import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import Modal from "../../../Modal";
import SpacePublicShare from "./SpacePublicShare";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import RoomViewStore from "../../../stores/RoomViewStore"; import RoomViewStore from "../../../stores/RoomViewStore";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {showRoomInviteDialog} from "../../../RoomInvite";
import InfoDialog from "../dialogs/InfoDialog";
import {EventType} from "matrix-js-sdk/src/@types/event"; import {EventType} from "matrix-js-sdk/src/@types/event";
import SpaceRoomDirectory from "../../structures/SpaceRoomDirectory";
interface IItemProps { interface IItemProps {
space?: Room; space?: Room;
@ -115,36 +111,11 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
this.setState({contextMenuPosition: null}); this.setState({contextMenuPosition: null});
}; };
private onHomeClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch({
action: "view_room",
room_id: this.props.space.roomId,
});
this.setState({contextMenuPosition: null}); // also close the menu
};
private onInviteClick = (ev: ButtonEvent) => { private onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
if (this.props.space.getJoinRule() === "public") { showSpaceInvite(this.props.space);
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite to %(spaceName)s", { spaceName: this.props.space.name }),
description: <React.Fragment>
<span>{ _t("Share your public space") }</span>
<SpacePublicShare space={this.props.space} onFinished={() => modal.close()} />
</React.Fragment>,
fixedWidth: false,
button: false,
className: "mx_SpacePanel_sharePublicSpace",
hasCloseButton: true,
});
} else {
showRoomInviteDialog(this.props.space.roomId);
}
this.setState({contextMenuPosition: null}); // also close the menu this.setState({contextMenuPosition: null}); // also close the menu
}; };
@ -206,9 +177,10 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
Modal.createTrackedDialog("Space room directory", "Space panel", SpaceRoomDirectory, { defaultDispatcher.dispatch({
space: this.props.space, action: "view_room",
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true); room_id: this.props.space.roomId,
});
this.setState({contextMenuPosition: null}); // also close the menu this.setState({contextMenuPosition: null}); // also close the menu
}; };
@ -249,6 +221,8 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
</IconizedContextMenuOptionList>; </IconizedContextMenuOptionList>;
} }
const canAddRooms = this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
let newRoomSection; let newRoomSection;
if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
newRoomSection = <IconizedContextMenuOptionList first> newRoomSection = <IconizedContextMenuOptionList first>
@ -276,11 +250,6 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
</div> </div>
<IconizedContextMenuOptionList first> <IconizedContextMenuOptionList first>
{ inviteOption } { inviteOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHome"
label={_t("Space Home")}
onClick={this.onHomeClick}
/>
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconMembers" iconClassName="mx_SpacePanel_iconMembers"
label={_t("Members")} label={_t("Members")}
@ -289,7 +258,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
{ settingsOption } { settingsOption }
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore" iconClassName="mx_SpacePanel_iconExplore"
label={_t("Explore rooms")} label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
onClick={this.onExploreRoomsClick} onClick={this.onExploreRoomsClick}
/> />
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>

View file

@ -723,6 +723,8 @@
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Share your public space": "Share your public space",
"Unknown App": "Unknown App", "Unknown App": "Unknown App",
"Help us improve %(brand)s": "Help us improve %(brand)s", "Help us improve %(brand)s": "Help us improve %(brand)s",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.", "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
@ -1012,14 +1014,12 @@
"Share invite link": "Share invite link", "Share invite link": "Share invite link",
"Invite people": "Invite people", "Invite people": "Invite people",
"Invite with email or username": "Invite with email or username", "Invite with email or username": "Invite with email or username",
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Share your public space": "Share your public space",
"Settings": "Settings", "Settings": "Settings",
"Leave space": "Leave space", "Leave space": "Leave space",
"Create new room": "Create new room", "Create new room": "Create new room",
"Add existing room": "Add existing room", "Add existing room": "Add existing room",
"Space Home": "Space Home",
"Members": "Members", "Members": "Members",
"Manage & explore rooms": "Manage & explore rooms",
"Explore rooms": "Explore rooms", "Explore rooms": "Explore rooms",
"Space options": "Space options", "Space options": "Space options",
"Remove": "Remove", "Remove": "Remove",
@ -1910,6 +1910,8 @@
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.", "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
"collapse": "collapse", "collapse": "collapse",
"expand": "expand", "expand": "expand",
"%(count)s people you know have already joined|other": "%(count)s people you know have already joined",
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
"You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)", "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",
"Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s", "Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s",
"Rotate Left": "Rotate Left", "Rotate Left": "Rotate Left",
@ -2610,7 +2612,6 @@
"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.",
"Open": "Open",
"You don't have permission": "You don't have permission", "You don't have permission": "You don't have permission",
"%(count)s members|other": "%(count)s members", "%(count)s members|other": "%(count)s members",
"%(count)s members|one": "%(count)s member", "%(count)s members|one": "%(count)s member",
@ -2618,7 +2619,6 @@
"%(count)s rooms|one": "%(count)s room", "%(count)s rooms|one": "%(count)s room",
"This room is suggested as a good one to join": "This room is suggested as a good one to join", "This room is suggested as a good one to join": "This room is suggested as a good one to join",
"Suggested": "Suggested", "Suggested": "Suggested",
"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|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 %(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",
@ -2629,16 +2629,14 @@
"Mark as suggested": "Mark as suggested", "Mark as suggested": "Mark as suggested",
"No results found": "No results found", "No results found": "No results found",
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
"Create room": "Create room", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
"Search names and description": "Search names and description", "Search names and description": "Search names and description",
"<inviter/> invites you": "<inviter/> invites you", "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
"Create room": "Create room",
"Public space": "Public space", "Public space": "Public space",
"Private space": "Private space", "Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you",
"Add existing rooms & spaces": "Add existing rooms & spaces", "Add existing rooms & spaces": "Add existing rooms & spaces",
"Default Rooms": "Default Rooms",
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
"Your public space <name/>": "Your public space <name/>",
"Your private space <name/>": "Your private space <name/>",
"Welcome to <name/>": "Welcome to <name/>", "Welcome to <name/>": "Welcome to <name/>",
"Random": "Random", "Random": "Random",
"Support": "Support", "Support": "Support",
@ -2660,8 +2658,9 @@
"Invite your teammates": "Invite your teammates", "Invite your teammates": "Invite your teammates",
"Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.", "Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.",
"Invite by username": "Invite by username", "Invite by username": "Invite by username",
"What are some things you want to discuss?": "What are some things you want to discuss?", "What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?",
"Let's create a room for each of them. You can add more later too, including already existing ones.": "Let's create a room for each of them. You can add more later too, including already existing ones.", "Let's create a room for each of them.": "Let's create a room for each of them.",
"You can add more later too, including already existing ones.": "You can add more later too, including already existing ones.",
"What projects are you working on?": "What projects are you working on?", "What projects are you working on?": "What projects are you working on?",
"We'll create rooms for each of them. You can add more later too, including already existing ones.": "We'll create rooms for each of them. You can add more later too, including already existing ones.", "We'll create rooms for each of them. You can add more later too, including already existing ones.": "We'll create rooms for each of them. You can add more later too, including already existing ones.",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",

View file

@ -122,7 +122,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const data = await this.fetchSuggestedRooms(space); const data = await this.fetchSuggestedRooms(space);
if (this._activeSpace === space) { if (this._activeSpace === space) {
this._suggestedRooms = data.rooms.filter(roomInfo => { this._suggestedRooms = data.rooms.filter(roomInfo => {
return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id); return roomInfo.room_type !== RoomType.Space
&& this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join";
}); });
this.emit(SUGGESTED_ROOMS, this._suggestedRooms); this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
} }
@ -294,6 +295,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
}; };
private onSpaceMembersChange = (ev: MatrixEvent) => {
// skip this update if we do not have a DM with this user
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
this.onRoomsUpdate();
};
private onRoomsUpdate = throttle(() => { private onRoomsUpdate = throttle(() => {
// TODO resolve some updates as deltas // TODO resolve some updates as deltas
const visibleRooms = this.matrixClient.getVisibleRooms(); const visibleRooms = this.matrixClient.getVisibleRooms();
@ -374,21 +381,28 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.setActiveSpace(room); this.setActiveSpace(room);
} }
if (room.getMyMembership() === "join") {
const numSuggestedRooms = this._suggestedRooms.length; const numSuggestedRooms = this._suggestedRooms.length;
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId); this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
if (numSuggestedRooms !== this._suggestedRooms.length) { if (numSuggestedRooms !== this._suggestedRooms.length) {
this.emit(SUGGESTED_ROOMS, this._suggestedRooms); this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
} }
}
}; };
private onRoomState = (ev: MatrixEvent) => { private onRoomState = (ev: MatrixEvent) => {
const room = this.matrixClient.getRoom(ev.getRoomId()); const room = this.matrixClient.getRoom(ev.getRoomId());
if (!room) return; if (!room) return;
if (ev.getType() === EventType.SpaceChild && room.isSpaceRoom()) { switch (ev.getType()) {
case EventType.SpaceChild:
if (room.isSpaceRoom()) {
this.onSpaceUpdate(); this.onSpaceUpdate();
this.emit(room.roomId); this.emit(room.roomId);
} else if (ev.getType() === EventType.SpaceParent) { }
break;
case EventType.SpaceParent:
// TODO rebuild the space parent and not the room - check permissions? // TODO rebuild the space parent and not the room - check permissions?
// TODO confirm this after implementing parenting behaviour // TODO confirm this after implementing parenting behaviour
if (room.isSpaceRoom()) { if (room.isSpaceRoom()) {
@ -397,6 +411,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.onRoomUpdate(room); this.onRoomUpdate(room);
} }
this.emit(room.roomId); this.emit(room.roomId);
break;
case EventType.RoomMember:
if (room.isSpaceRoom()) {
this.onSpaceMembersChange(ev);
}
break;
} }
}; };

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React 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} from "matrix-js-sdk/src/@types/event"; import {EventType} from "matrix-js-sdk/src/@types/event";
@ -24,6 +25,10 @@ import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog
import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog"; import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog";
import CreateRoomDialog from "../components/views/dialogs/CreateRoomDialog"; import CreateRoomDialog from "../components/views/dialogs/CreateRoomDialog";
import createRoom, {IOpts} from "../createRoom"; import createRoom, {IOpts} from "../createRoom";
import {_t} from "../languageHandler";
import SpacePublicShare from "../components/views/spaces/SpacePublicShare";
import InfoDialog from "../components/views/dialogs/InfoDialog";
import { showRoomInviteDialog } from "../RoomInvite";
export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => { export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
const userId = cli.getUserId(); const userId = cli.getUserId();
@ -79,3 +84,21 @@ export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
await createRoom(opts); await createRoom(opts);
} }
}; };
export const showSpaceInvite = (space: Room, initialText = "") => {
if (space.getJoinRule() === "public") {
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite to %(spaceName)s", { spaceName: space.name }),
description: <React.Fragment>
<span>{ _t("Share your public space") }</span>
<SpacePublicShare space={space} onFinished={() => modal.close()} />
</React.Fragment>,
fixedWidth: false,
button: false,
className: "mx_SpacePanel_sharePublicSpace",
hasCloseButton: true,
});
} else {
showRoomInviteDialog(space.roomId, initialText);
}
};