Merge pull request #5789 from matrix-org/t3chguy/spaces4.11

Tweak and fix some space features
This commit is contained in:
Michael Telatynski 2021-03-25 09:02:11 +00:00 committed by GitHub
commit 760b11f214
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 343 additions and 194 deletions

View file

@ -130,6 +130,10 @@ $roomListCollapsedWidth: 68px;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background: $secondary-fg-color; background: $secondary-fg-color;
} }
&.mx_LeftPanel_exploreButton_space::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
}
} }
} }

View file

@ -146,9 +146,6 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpaceButton_toggleCollapse { .mx_SpaceButton_toggleCollapse {
width: $gutterSize; width: $gutterSize;
// negative margin to place it correctly even with the complex
// 4px selection border each space button has when active
margin-right: -4px;
height: 20px; height: 20px;
mask-position: center; mask-position: center;
mask-size: 20px; mask-size: 20px;
@ -342,11 +339,15 @@ $activeBorderColor: $secondary-fg-color;
} }
.mx_SpacePanel_iconPlus::before { .mx_SpacePanel_iconPlus::before {
mask-image: url('$(res)/img/element-icons/plus.svg'); mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg');
}
.mx_SpacePanel_iconHash::before {
mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg');
} }
.mx_SpacePanel_iconExplore::before { .mx_SpacePanel_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
} }
} }

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 $input-darker-bg-color; border: 1px solid $space-button-outline-color;
font-size: $font-15px; font-size: $font-15px;
margin: 20px 0; margin: 20px 0;
@ -89,7 +89,7 @@ $SpaceRoomViewInnerWidth: 428px;
width: $SpaceRoomViewInnerWidth; width: $SpaceRoomViewInnerWidth;
text-align: right; // button alignment right text-align: right; // button alignment right
.mx_FormButton { .mx_AccessibleButton_hasKind {
padding: 8px 22px; padding: 8px 22px;
margin-left: 16px; margin-left: 16px;
} }

View file

@ -28,22 +28,23 @@ limitations under the License.
flex-direction: column; flex-direction: column;
flex-wrap: nowrap; flex-wrap: nowrap;
min-height: 0; min-height: 0;
height: 80vh;
.mx_Dialog_title { .mx_Dialog_title {
display: flex; display: flex;
.mx_BaseAvatar {
display: inline-flex;
margin: 5px 16px 5px 5px;
vertical-align: middle;
}
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
border-radius: 8px; border-radius: 8px;
margin: 0; margin: 0;
vertical-align: unset; vertical-align: unset;
} }
.mx_BaseAvatar {
display: inline-flex;
margin: 5px 16px 5px 5px;
vertical-align: middle;
}
> div { > div {
> h1 { > h1 {
font-weight: $font-semi-bold; font-weight: $font-semi-bold;
@ -101,6 +102,7 @@ limitations under the License.
.mx_SearchBox { .mx_SearchBox {
margin: 0; margin: 0;
flex-grow: 0;
} }
.mx_AddExistingToSpaceDialog_errorText { .mx_AddExistingToSpaceDialog_errorText {
@ -112,7 +114,10 @@ limitations under the License.
} }
.mx_AddExistingToSpaceDialog_content { .mx_AddExistingToSpaceDialog_content {
flex-grow: 1;
.mx_AddExistingToSpaceDialog_noResults { .mx_AddExistingToSpaceDialog_noResults {
display: block;
margin-top: 24px; margin-top: 24px;
} }
} }
@ -162,8 +167,14 @@ limitations under the License.
> span { > span {
flex-grow: 1; flex-grow: 1;
font-size: $font-12px; font-size: $font-14px;
line-height: $font-15px; line-height: $font-15px;
font-weight: $font-semi-bold;
.mx_AccessibleButton {
font-size: inherit;
display: inline-block;
}
> * { > * {
vertical-align: middle; vertical-align: middle;

View file

@ -49,7 +49,7 @@ limitations under the License.
} }
} }
.mx_FormButton { .mx_AccessibleButton_hasKind {
padding: 8px 22px; padding: 8px 22px;
} }
} }

View file

@ -33,8 +33,13 @@ limitations under the License.
.mx_AccessibleButton { .mx_AccessibleButton {
line-height: $font-24px; line-height: $font-24px;
display: inline-block;
&::before { & + .mx_AccessibleButton {
margin-left: 12px;
}
&:not(.mx_AccessibleButton_kind_primary_outline)::before {
content: ''; content: '';
display: inline-block; display: inline-block;
background-color: $button-fg-color; background-color: $button-fg-color;

View file

@ -27,6 +27,9 @@ limitations under the License.
.mx_RoomList_iconExplore::before { .mx_RoomList_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
} }
.mx_RoomList_iconBrowse::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
}
.mx_RoomList_iconDialpad::before { .mx_RoomList_iconDialpad::before {
mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg'); mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg');
} }
@ -35,28 +38,32 @@ limitations under the License.
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 $tertiary-fg-color;
font-size: $font-13px; font-size: $font-14px;
div:first-child { div:first-child {
font-weight: $font-semi-bold; font-weight: $font-semi-bold;
line-height: $font-18px;
color: $primary-fg-color;
} }
.mx_AccessibleButton { .mx_AccessibleButton {
color: $secondary-fg-color; color: $primary-fg-color;
position: relative; position: relative;
padding: 0 0 0 24px; padding: 8px 8px 8px 32px;
font-size: inherit; font-size: inherit;
margin-top: 8px; margin-top: 12px;
display: block; display: block;
text-align: start; text-align: start;
background-color: $roomlist-button-bg-color;
border-radius: 4px;
&::before { &::before {
content: ''; content: '';
width: 16px; width: 16px;
height: 16px; height: 16px;
position: absolute; position: absolute;
top: 0; top: 8px;
left: 0; left: 8px;
background: $secondary-fg-color; background: $secondary-fg-color;
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
@ -70,5 +77,13 @@ limitations under the License.
&.mx_RoomList_explorePrompt_explore::before { &.mx_RoomList_explorePrompt_explore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
} }
&.mx_RoomList_explorePrompt_spaceInvite::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
&.mx_RoomList_explorePrompt_spaceExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
}
} }
} }

View file

@ -79,7 +79,7 @@ $spacePanelWidth: 71px;
} }
} }
.mx_FormButton { .mx_AccessibleButton_kind_primary {
padding: 8px 22px; padding: 8px 22px;
margin-left: auto; margin-left: auto;
display: block; display: block;

View file

@ -0,0 +1,4 @@
<svg width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.00262 5.60945C7.02444 6.31867 7.18204 6.99371 7.45029 7.60945H5.83106L5.49798 11.0235H8.60274L8.757 9.44233C9.29964 9.94168 9.94406 10.3321 10.6556 10.579L10.6122 11.0235H12.7966C13.3489 11.0235 13.7966 11.4712 13.7966 12.0235C13.7966 12.5758 13.3489 13.0235 12.7966 13.0235H10.4171L10.1823 15.4305C10.1287 15.9802 9.63959 16.3823 9.08991 16.3287C8.54024 16.2751 8.13811 15.786 8.19174 15.2363L8.40762 13.0235H5.30286L5.06803 15.4305C5.0144 15.9802 4.52533 16.3823 3.97565 16.3287C3.42598 16.2751 3.02385 15.786 3.07748 15.2363L3.29336 13.0235H1.6665C1.11422 13.0235 0.666504 12.5758 0.666504 12.0235C0.666504 11.4712 1.11422 11.0235 1.6665 11.0235H3.48848L3.82156 7.60945H2.26807C1.71578 7.60945 1.26807 7.16173 1.26807 6.60945C1.26807 6.05716 1.71578 5.60945 2.26807 5.60945H4.01668L4.28073 2.90297C4.33436 2.3533 4.82343 1.95117 5.37311 2.0048C5.92278 2.05842 6.32491 2.5475 6.27128 3.09717L6.02618 5.60945H7.00262Z" fill="#8D99A5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4224 5.37843C14.4224 6.50754 13.5071 7.42287 12.3779 7.42287C11.2488 7.42287 10.3335 6.50754 10.3335 5.37843C10.3335 4.24931 11.2488 3.33398 12.3779 3.33398C13.5071 3.33398 14.4224 4.24931 14.4224 5.37843ZM15.8496 7.45454C16.2133 6.84764 16.4224 6.13745 16.4224 5.37843C16.4224 3.14474 14.6116 1.33398 12.3779 1.33398C10.1443 1.33398 8.3335 3.14474 8.3335 5.37843C8.3335 7.61211 10.1443 9.42287 12.3779 9.42287C13.1369 9.42287 13.8471 9.21381 14.454 8.85013C14.4853 8.89368 14.5205 8.93528 14.5597 8.97444L16.293 10.7078C16.6835 11.0983 17.3167 11.0983 17.7072 10.7078C18.0977 10.3172 18.0977 9.68408 17.7072 9.29356L15.9739 7.56023C15.9347 7.52107 15.8931 7.48584 15.8496 7.45454Z" fill="#8D99A5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -49,11 +49,12 @@ export function showStartChatInviteDialog(initialText) {
); );
} }
export function showRoomInviteDialog(roomId) { export function showRoomInviteDialog(roomId, initialText = "") {
// This dialog handles the room creation internally - we don't need to worry about it. // This dialog handles the room creation internally - we don't need to worry about it.
Modal.createTrackedDialog( Modal.createTrackedDialog(
"Invite Users", "", InviteDialog, { "Invite Users", "", InviteDialog, {
kind: KIND_INVITE, kind: KIND_INVITE,
initialText,
roomId, roomId,
}, },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,

View file

@ -16,9 +16,11 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import { createRef } from "react"; import { createRef } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import GroupFilterPanel from "./GroupFilterPanel"; import GroupFilterPanel from "./GroupFilterPanel";
import CustomRoomTagPanel from "./CustomRoomTagPanel"; import CustomRoomTagPanel from "./CustomRoomTagPanel";
import classNames from "classnames";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import RoomList from "../views/rooms/RoomList"; import RoomList from "../views/rooms/RoomList";
@ -40,6 +42,7 @@ import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget"; import LeftPanelWidget from "./LeftPanelWidget";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import {mediaFromMxc} from "../../customisations/Media"; import {mediaFromMxc} from "../../customisations/Media";
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -49,6 +52,7 @@ interface IProps {
interface IState { interface IState {
showBreadcrumbs: boolean; showBreadcrumbs: boolean;
showGroupFilterPanel: boolean; showGroupFilterPanel: boolean;
activeSpace?: Room;
} }
// List of CSS classes which should be included in keyboard navigation within the room list // List of CSS classes which should be included in keyboard navigation within the room list
@ -74,11 +78,13 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.state = { this.state = {
showBreadcrumbs: BreadcrumbsStore.instance.visible, showBreadcrumbs: BreadcrumbsStore.instance.visible,
showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'),
activeSpace: SpaceStore.instance.activeSpace,
}; };
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate); OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
this.bgImageWatcherRef = SettingsStore.watchSetting( this.bgImageWatcherRef = SettingsStore.watchSetting(
"RoomList.backgroundImage", null, this.onBackgroundImageUpdate); "RoomList.backgroundImage", null, this.onBackgroundImageUpdate);
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
@ -96,9 +102,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
} }
private updateActiveSpace = (activeSpace: Room) => {
this.setState({ activeSpace });
};
private onExplore = () => { private onExplore = () => {
dis.fire(Action.ViewRoomDirectory); dis.fire(Action.ViewRoomDirectory);
}; };
@ -381,7 +392,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onEnter={this.onEnter} onEnter={this.onEnter}
/> />
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_LeftPanel_exploreButton" className={classNames("mx_LeftPanel_exploreButton", {
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
})}
onClick={this.onExplore} onClick={this.onExplore}
title={_t("Explore rooms")} title={_t("Explore rooms")}
/> />
@ -407,6 +420,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onBlur={this.onBlur} onBlur={this.onBlur}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
onResize={this.onResize} onResize={this.onResize}
activeSpace={this.state.activeSpace}
/>; />;
const containerClasses = classNames({ const containerClasses = classNames({

View file

@ -74,7 +74,6 @@ function canElementReceiveInput(el) {
interface IProps { interface IProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>; onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[];
hideToSRUsers: boolean; hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
@ -143,9 +142,6 @@ class LoggedInView extends React.Component<IProps, IState> {
// transitioned to PWLU) // transitioned to PWLU)
onRegistered: PropTypes.func, onRegistered: PropTypes.func,
// Used by the RoomView to handle joining rooms
viaServers: PropTypes.arrayOf(PropTypes.string),
// and lots and lots of other stuff. // and lots and lots of other stuff.
}; };
@ -625,11 +621,9 @@ class LoggedInView extends React.Component<IProps, IState> {
case PageTypes.RoomView: case PageTypes.RoomView:
pageElement = <RoomView pageElement = <RoomView
ref={this._roomView} ref={this._roomView}
autoJoin={this.props.autoJoin}
onRegistered={this.props.onRegistered} onRegistered={this.props.onRegistered}
threepidInvite={this.props.threepidInvite} threepidInvite={this.props.threepidInvite}
oobData={this.props.roomOobData} oobData={this.props.roomOobData}
viaServers={this.props.viaServers}
key={this.props.currentRoomId || 'roomview'} key={this.props.currentRoomId || 'roomview'}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
justCreatedOpts={this.props.roomJustCreatedOpts} justCreatedOpts={this.props.roomJustCreatedOpts}

View file

@ -202,7 +202,6 @@ interface IState {
ready: boolean; ready: boolean;
threepidInvite?: IThreepidInvite, threepidInvite?: IThreepidInvite,
roomOobData?: object; roomOobData?: object;
viaServers?: string[];
pendingInitialSync?: boolean; pendingInitialSync?: boolean;
justRegistered?: boolean; justRegistered?: boolean;
roomJustCreatedOpts?: IOpts; roomJustCreatedOpts?: IOpts;
@ -929,7 +928,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
page_type: PageTypes.RoomView, page_type: PageTypes.RoomView,
threepidInvite: roomInfo.threepid_invite, threepidInvite: roomInfo.threepid_invite,
roomOobData: roomInfo.oob_data, roomOobData: roomInfo.oob_data,
viaServers: roomInfo.via_servers,
ready: true, ready: true,
roomJustCreatedOpts: roomInfo.justCreatedOpts, roomJustCreatedOpts: roomInfo.justCreatedOpts,
}, () => { }, () => {

View file

@ -112,10 +112,6 @@ interface IProps {
inviterName?: string; inviterName?: string;
}; };
// Servers the RoomView can use to try and assist joins
viaServers?: string[];
autoJoin?: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
justCreatedOpts?: IOpts; justCreatedOpts?: IOpts;
@ -450,9 +446,7 @@ export default class RoomView extends React.Component<IProps, IState> {
// now not joined because the js-sdk peeking API will clobber our historical room, // now not joined because the js-sdk peeking API will clobber our historical room,
// making it impossible to indicate a newly joined room. // making it impossible to indicate a newly joined room.
if (!joining && roomId) { if (!joining && roomId) {
if (this.props.autoJoin) { if (!room && shouldPeek) {
this.onJoinButtonClicked();
} else if (!room && shouldPeek) {
console.info("Attempting to peek into room %s", roomId); console.info("Attempting to peek into room %s", roomId);
this.setState({ this.setState({
peekLoading: true, peekLoading: true,
@ -1123,7 +1117,7 @@ export default class RoomView extends React.Component<IProps, IState> {
const signUrl = this.props.threepidInvite?.signUrl; const signUrl = this.props.threepidInvite?.signUrl;
dis.dispatch({ dis.dispatch({
action: 'join_room', action: 'join_room',
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers }, opts: { inviteSignUrl: signUrl },
_type: "unknown", // TODO: instrumentation _type: "unknown", // TODO: instrumentation
}); });
return Promise.resolve(); return Promise.resolve();

View file

@ -32,6 +32,8 @@ export default class SearchBox extends React.Component {
onKeyDown: PropTypes.func, onKeyDown: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
placeholder: PropTypes.string.isRequired, placeholder: PropTypes.string.isRequired,
autoFocus: PropTypes.bool,
initialValue: PropTypes.string,
// If true, the search box will focus and clear itself // If true, the search box will focus and clear itself
// on room search focus action (it would be nicer to take // on room search focus action (it would be nicer to take
@ -49,7 +51,7 @@ export default class SearchBox extends React.Component {
this._search = createRef(); this._search = createRef();
this.state = { this.state = {
searchTerm: "", searchTerm: this.props.initialValue || "",
blurred: true, blurred: true,
}; };
} }
@ -158,6 +160,7 @@ export default class SearchBox extends React.Component {
onBlur={this._onBlur} onBlur={this._onBlur}
placeholder={ placeholder } placeholder={ placeholder }
autoComplete="off" autoComplete="off"
autoFocus={this.props.autoFocus}
/> />
{ clearButton } { clearButton }
</div> </div>

View file

@ -15,7 +15,8 @@ limitations under the License.
*/ */
import React, {useMemo, 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 {MatrixClient} from "matrix-js-sdk/src/client";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
import classNames from "classnames"; import classNames from "classnames";
import {sortBy} from "lodash"; import {sortBy} from "lodash";
@ -77,7 +78,6 @@ export interface ISpaceSummaryEvent {
interface ITileProps { interface ITileProps {
room: ISpaceSummaryRoom; room: ISpaceSummaryRoom;
editing?: boolean;
suggested?: boolean; suggested?: boolean;
selected?: boolean; selected?: boolean;
numChildRooms?: number; numChildRooms?: number;
@ -88,7 +88,6 @@ interface ITileProps {
const Tile: React.FC<ITileProps> = ({ const Tile: React.FC<ITileProps> = ({
room, room,
editing,
suggested, suggested,
selected, selected,
hasPermissions, hasPermissions,
@ -170,12 +169,6 @@ const Tile: React.FC<ITileProps> = ({
</div> </div>
</React.Fragment>; </React.Fragment>;
if (editing) {
return <div className="mx_SpaceRoomDirectory_roomTile">
{ content }
</div>
}
let childToggle; let childToggle;
let childSection; let childSection;
if (children) { if (children) {
@ -201,7 +194,7 @@ const Tile: React.FC<ITileProps> = ({
className={classNames("mx_SpaceRoomDirectory_roomTile", { className={classNames("mx_SpaceRoomDirectory_roomTile", {
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space, mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
})} })}
onClick={hasPermissions ? onToggleClick : onPreviewClick} onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
> >
{ content } { content }
{ childToggle } { childToggle }
@ -240,7 +233,7 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi
interface IHierarchyLevelProps { interface IHierarchyLevelProps {
spaceId: string; spaceId: string;
rooms: Map<string, ISpaceSummaryRoom>; rooms: Map<string, ISpaceSummaryRoom>;
relations: EnhancedMap<string, Map<string, ISpaceSummaryEvent>>; relations: Map<string, Map<string, ISpaceSummaryEvent>>;
parents: Set<string>; parents: Set<string>;
selectedMap?: Map<string, Set<string>>; selectedMap?: Map<string, Set<string>>;
onViewRoomClick(roomId: string, autoJoin: boolean): void; onViewRoomClick(roomId: string, autoJoin: boolean): void;
@ -260,7 +253,7 @@ export const HierarchyLevel = ({
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) => {
const roomId = ev.state_key; const roomId = ev.state_key;
if (!rooms.has(roomId)) return result; if (!rooms.has(roomId)) return result;
@ -316,23 +309,15 @@ export const HierarchyLevel = ({
</React.Fragment> </React.Fragment>
}; };
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => { // mutate argument refreshToken to force a reload
export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
ISpaceSummaryRoom[],
Map<string, Map<string, ISpaceSummaryEvent>>,
Map<string, Set<string>>,
Map<string, Set<string>>,
] | [] => {
// TODO pagination // TODO pagination
const cli = MatrixClientPeg.get(); return useAsyncMemo(async () => {
const userId = cli.getUserId();
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 [rooms, parentChildMap, childParentMap, viaMap] = useAsyncMemo(async () => {
try { try {
const data = await cli.getSpaceSummary(space.roomId); const data = await cli.getSpaceSummary(space.roomId);
@ -350,13 +335,31 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
} }
}); });
return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, childParentRelations, viaMap]; return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations];
} catch (e) { } catch (e) {
console.error(e); // TODO console.error(e); // TODO
} }
return []; return [];
}, [space], []); }, [space, refreshToken], []);
};
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
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 [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space);
const roomsMap = useMemo(() => { const roomsMap = useMemo(() => {
if (!rooms) return null; if (!rooms) return null;
@ -570,6 +573,8 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
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") }
onSearch={setQuery} onSearch={setQuery}
autoFocus={true}
initialValue={initialText}
/> />
{ content } { content }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {RefObject, useContext, useRef, useState} from "react"; import React, {RefObject, useContext, useMemo, useRef, useState} from "react";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {EventType, RoomType} 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";
@ -26,7 +26,6 @@ import AccessibleButton from "../views/elements/AccessibleButton";
import RoomName from "../views/elements/RoomName"; import RoomName from "../views/elements/RoomName";
import RoomTopic from "../views/elements/RoomTopic"; import RoomTopic from "../views/elements/RoomTopic";
import InlineSpinner from "../views/elements/InlineSpinner"; import InlineSpinner from "../views/elements/InlineSpinner";
import FormButton from "../views/elements/FormButton";
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite"; import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
import {useRoomMembers} from "../../hooks/useRoomMembers"; import {useRoomMembers} from "../../hooks/useRoomMembers";
import createRoom, {IOpts, Preset} from "../../createRoom"; import createRoom, {IOpts, Preset} from "../../createRoom";
@ -47,9 +46,7 @@ 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, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom} from "./SpaceRoomDirectory"; import {HierarchyLevel, ISpaceSummaryRoom, showRoom, useSpaceSummary} from "./SpaceRoomDirectory";
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
import {EnhancedMap} from "../../utils/maps";
import AutoHideScrollbar from "./AutoHideScrollbar"; 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";
@ -124,30 +121,36 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
} }
joinButtons = <> joinButtons = <>
<FormButton <AccessibleButton
label={_t("Reject")}
kind="secondary" kind="secondary"
onClick={() => { onClick={() => {
setBusy(true); setBusy(true);
onRejectButtonClicked(); onRejectButtonClicked();
}} /> }}
<FormButton >
label={_t("Accept")} { _t("Reject") }
</AccessibleButton>
<AccessibleButton
kind="primary"
onClick={() => { onClick={() => {
setBusy(true); setBusy(true);
onJoinButtonClicked(); onJoinButtonClicked();
}} }}
/> >
{ _t("Accept") }
</AccessibleButton>
</>; </>;
} else { } else {
joinButtons = ( joinButtons = (
<FormButton <AccessibleButton
label={_t("Join")} kind="primary"
onClick={() => { onClick={() => {
setBusy(true); setBusy(true);
onJoinButtonClicked(); onJoinButtonClicked();
}} }}
/> >
{ _t("Join") }
</AccessibleButton>
) )
} }
@ -223,7 +226,7 @@ const SpaceLanding = ({ space }) => {
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const [_, forceUpdate] = useStateToggle(false); // TODO const [refreshToken, forceUpdate] = useStateToggle(false);
let addRoomButtons; let addRoomButtons;
if (canAddRooms) { if (canAddRooms) {
@ -253,26 +256,13 @@ const SpaceLanding = ({ space }) => {
</AccessibleButton>; </AccessibleButton>;
} }
const [loading, roomsMap, relations, numRooms] = useAsyncMemo(async () => { const [rooms, relations, viaMap] = useSpaceSummary(cli, space, refreshToken);
try { const [roomsMap, numRooms] = useMemo(() => {
const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join"); if (!rooms) return [];
const roomsMap = new Map<string, ISpaceSummaryRoom>(rooms.map(r => [r.room_id, r]));
const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>(); const numRooms = rooms.filter(r => r.room_type !== RoomType.Space).length;
data.events.map((ev: ISpaceSummaryEvent) => { return [roomsMap, numRooms];
if (ev.type === EventType.SpaceChild) { }, [rooms]);
parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
}
});
const roomsMap = new Map<string, ISpaceSummaryRoom>(data.rooms.map(r => [r.room_id, r]));
const numRooms = data.rooms.filter(r => r.room_type !== RoomType.Space).length;
return [false, roomsMap, parentChildRelations, numRooms];
} catch (e) {
console.error(e); // TODO
}
return [false];
}, [space, _], [true]);
let previewRooms; let previewRooms;
if (roomsMap) { if (roomsMap) {
@ -287,11 +277,11 @@ const SpaceLanding = ({ space }) => {
relations={relations} relations={relations}
parents={new Set()} parents={new Set()}
onViewRoomClick={(roomId, autoJoin) => { onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), [], autoJoin); showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
}} }}
/> />
</AutoHideScrollbar>; </AutoHideScrollbar>;
} else if (loading) { } else if (!rooms) {
previewRooms = <InlineSpinner />; previewRooms = <InlineSpinner />;
} else { } else {
previewRooms = <p>{_t("Your server does not support showing space hierarchies.")}</p>; previewRooms = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
@ -407,11 +397,13 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
{ fields } { fields }
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<FormButton <AccessibleButton
label={buttonLabel} kind="primary"
disabled={busy} disabled={busy}
onClick={onClick} onClick={onClick}
/> >
{ buttonLabel }
</AccessibleButton>
</div> </div>
</div>; </div>;
}; };
@ -419,14 +411,16 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
const SpaceSetupPublicShare = ({ space, onFinished }) => { const SpaceSetupPublicShare = ({ space, onFinished }) => {
return <div className="mx_SpaceRoomView_publicShare"> return <div className="mx_SpaceRoomView_publicShare">
<h1>{ _t("Share %(name)s", { name: space.name }) }</h1> <h1>{ _t("Share %(name)s", { name: space.name }) }</h1>
<div className="mx_SpacePublicShare_description"> <div className="mx_SpaceRoomView_description">
{ _t("It's just you at the moment, it will be even better with others.") } { _t("It's just you at the moment, it will be even better with others.") }
</div> </div>
<SpacePublicShare space={space} onFinished={onFinished} /> <SpacePublicShare space={space} />
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<FormButton label={_t("Go to my first room")} onClick={onFinished} /> <AccessibleButton kind="primary" onClick={onFinished}>
{ _t("Go to my first room") }
</AccessibleButton>
</div> </div>
</div>; </div>;
}; };
@ -545,7 +539,9 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
</div> </div>
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<FormButton label={buttonLabel} disabled={busy} onClick={onClick} /> <AccessibleButton kind="primary" disabled={busy} onClick={onClick}>
{ buttonLabel }
</AccessibleButton>
</div> </div>
</div>; </div>;
}; };
@ -630,6 +626,8 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
}; };
private goToFirstRoom = async () => { private goToFirstRoom = async () => {
// TODO actually go to the first room
const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId); const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId);
if (childRooms.length) { if (childRooms.length) {
const room = childRooms[0]; const room = childRooms[0];

View file

@ -22,7 +22,6 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
import {_t} from '../../../languageHandler'; import {_t} from '../../../languageHandler';
import {IDialogProps} from "./IDialogProps"; import {IDialogProps} from "./IDialogProps";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import FormButton from "../elements/FormButton";
import Dropdown from "../elements/Dropdown"; import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox"; import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
@ -110,7 +109,7 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
const title = <React.Fragment> const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} /> <RoomAvatar room={selectedSpace} height={40} width={40} />
<div> <div>
<h1>{ _t("Add existing spaces/rooms") }</h1> <h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection } { spaceOptionSection }
</div> </div>
</React.Fragment>; </React.Fragment>;
@ -128,29 +127,9 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") } placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery} onSearch={setQuery}
autoComplete={true}
/> />
<AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog"> <AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog">
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
<h3>{ _t("Spaces") }</h3>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={(checked) => {
if (checked) {
selectedToAdd.add(space);
} else {
selectedToAdd.delete(space);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/>;
}) }
</div>
) : null }
{ rooms.length > 0 ? ( { rooms.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section"> <div className="mx_AddExistingToSpaceDialog_section">
<h3>{ _t("Rooms") }</h3> <h3>{ _t("Rooms") }</h3>
@ -172,6 +151,27 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</div> </div>
) : undefined } ) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
<h3>{ _t("Spaces") }</h3>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={(checked) => {
if (checked) {
selectedToAdd.add(space);
} else {
selectedToAdd.delete(space);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults"> { spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
{ _t("No results") } { _t("No results") }
</span> : undefined } </span> : undefined }
@ -185,8 +185,8 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</AccessibleButton> </AccessibleButton>
</span> </span>
<FormButton <AccessibleButton
label={busy ? _t("Applying...") : _t("Apply")} kind="primary"
disabled={busy || selectedToAdd.size < 1} disabled={busy || selectedToAdd.size < 1}
onClick={async () => { onClick={async () => {
setBusy(true); setBusy(true);
@ -200,7 +200,9 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
} }
setBusy(false); setBusy(false);
}} }}
/> >
{ busy ? _t("Adding...") : _t("Add") }
</AccessibleButton>
</div> </div>
</BaseDialog>; </BaseDialog>;
}; };

View file

@ -28,7 +28,6 @@ import {getTopic} from "../elements/RoomTopic";
import {avatarUrlForRoom} from "../../../Avatar"; import {avatarUrlForRoom} from "../../../Avatar";
import ToggleSwitch from "../elements/ToggleSwitch"; import ToggleSwitch from "../elements/ToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import FormButton from "../elements/FormButton";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import {allSettled} from "../../../utils/promise"; import {allSettled} from "../../../utils/promise";
@ -134,16 +133,17 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
/> />
</div> </div>
<FormButton <AccessibleButton
kind="danger" kind="danger"
label={_t("Leave Space")}
onClick={() => { onClick={() => {
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: "leave_room", action: "leave_room",
room_id: space.roomId, room_id: space.roomId,
}); });
}} }}
/> >
{ _t("Leave Space") }
</AccessibleButton>
<div className="mx_SpaceSettingsDialog_buttons"> <div className="mx_SpaceSettingsDialog_buttons">
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}> <AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
@ -152,7 +152,9 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
<AccessibleButton onClick={onFinished} disabled={busy} kind="link"> <AccessibleButton onClick={onFinished} disabled={busy} kind="link">
{ _t("Cancel") } { _t("Cancel") }
</AccessibleButton> </AccessibleButton>
<FormButton onClick={onSave} disabled={busy} label={busy ? _t("Saving...") : _t("Save Changes")} /> <AccessibleButton onClick={onSave} disabled={busy} kind="primary">
{ busy ? _t("Saving...") : _t("Save Changes") }
</AccessibleButton>
</div> </div>
</div> </div>
</BaseDialog>; </BaseDialog>;

View file

@ -28,6 +28,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; 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";
const NewRoomIntro = () => { const NewRoomIntro = () => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
@ -100,17 +101,48 @@ const NewRoomIntro = () => {
}); });
} }
let buttons; let parentSpace;
if (room.canInvite(cli.getUserId())) { if (
const onInviteClick = () => { SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) &&
dis.dispatch({ action: "view_invite", roomId }); SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId)
}; ) {
parentSpace = SpaceStore.instance.activeSpace;
}
let buttons;
if (parentSpace) {
buttons = <div className="mx_NewRoomIntro_buttons"> buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}> <AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })}
</AccessibleButton>
{ room.canInvite(cli.getUserId()) && <AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary_outline"
onClick={() => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to just this room")}
</AccessibleButton> }
</div>;
} else if (room.canInvite(cli.getUserId())) {
buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to this room")} {_t("Invite to this room")}
</AccessibleButton> </AccessibleButton>
</div> </div>;
} }
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url; const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;

View file

@ -20,6 +20,7 @@ import React, { ReactComponentElement } from "react";
import { Dispatcher } from "flux"; import { Dispatcher } from "flux";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import * as fbEmitter from "fbemitter"; import * as fbEmitter from "fbemitter";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t, _td } from "../../../languageHandler"; import { _t, _td } from "../../../languageHandler";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
@ -48,12 +49,15 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
import AccessibleButton from "../elements/AccessibleButton"; 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 } from "../../../utils/space";
import { EventType } from "matrix-js-sdk/src/@types/event";
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;
@ -62,6 +66,7 @@ interface IProps {
onResize: () => void; onResize: () => void;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
isMinimized: boolean; isMinimized: boolean;
activeSpace: Room;
} }
interface IState { interface IState {
@ -194,8 +199,8 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
: _t("You do not have permissions to add rooms to this space")} : _t("You do not have permissions to add rooms to this space")}
/> />
<IconizedContextMenuOption <IconizedContextMenuOption
label={_t("Explore space rooms")} label={_t("Explore rooms")}
iconClassName="mx_RoomList_iconExplore" iconClassName="mx_RoomList_iconBrowse"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -424,6 +429,25 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
dis.dispatch({ action: Action.ViewRoomDirectory, initialText }); dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
}; };
private onSpaceInviteClick = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
if (this.props.activeSpace.getJoinRule() === "public") {
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>[] {
return this.state.suggestedRooms.map(room => { return this.state.suggestedRooms.map(room => {
const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"); const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
@ -569,7 +593,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
kind="link" kind="link"
onClick={this.onExplore} onClick={this.onExplore}
> >
{_t("Explore all public rooms")} { this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
</AccessibleButton>
</div>;
} else if (this.props.activeSpace) {
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Quick actions") }</div>
{ this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && <AccessibleButton
className="mx_RoomList_explorePrompt_spaceInvite"
onClick={this.onSpaceInviteClick}
>
{_t("Invite people")}
</AccessibleButton> }
<AccessibleButton
className="mx_RoomList_explorePrompt_spaceExplore"
onClick={this.onExplore}
>
{_t("Explore rooms")}
</AccessibleButton> </AccessibleButton>
</div>; </div>;
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) { } else if (Object.values(this.state.sublists).some(list => list.length > 0)) {

View file

@ -21,7 +21,6 @@ import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu"; import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
import FormButton from "../elements/FormButton";
import createRoom, {IStateEvent, Preset} from "../../../createRoom"; import createRoom, {IStateEvent, Preset} from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import SpaceBasicSettings from "./SpaceBasicSettings"; import SpaceBasicSettings from "./SpaceBasicSettings";
@ -89,6 +88,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
power_level_content_override: { power_level_content_override: {
// Only allow Admins to write to the timeline to prevent hidden sync spam // Only allow Admins to write to the timeline to prevent hidden sync spam
events_default: 100, events_default: 100,
...Visibility.Public ? { invite: 0 } : {},
}, },
}, },
spinner: false, spinner: false,
@ -148,11 +148,9 @@ 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} />
<FormButton <AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={!name && !busy}>
label={busy ? _t("Creating...") : _t("Create")} { busy ? _t("Creating...") : _t("Create") }
onClick={onSpaceCreateClick} </AccessibleButton>
disabled={!name && !busy}
/>
</React.Fragment>; </React.Fragment>;
} }

View file

@ -26,7 +26,7 @@ import {showRoomInviteDialog} from "../../../RoomInvite";
interface IProps { interface IProps {
space: Room; space: Room;
onFinished(): void; onFinished?(): void;
} }
const SpacePublicShare = ({ space, onFinished }: IProps) => { const SpacePublicShare = ({ space, onFinished }: IProps) => {
@ -54,7 +54,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
className="mx_SpacePublicShare_inviteButton" className="mx_SpacePublicShare_inviteButton"
onClick={() => { onClick={() => {
showRoomInviteDialog(space.roomId); showRoomInviteDialog(space.roomId);
onFinished(); if (onFinished) onFinished();
}} }}
> >
<h3>{ _t("Invite people") }</h3> <h3>{ _t("Invite people") }</h3>

View file

@ -30,9 +30,14 @@ import IconizedContextMenu, {
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import {toRightOf} from "../../structures/ContextMenu"; import {toRightOf} from "../../structures/ContextMenu";
import {shouldShowSpaceSettings, showCreateNewRoom, showSpaceSettings} from "../../../utils/space"; import {
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
showSpaceSettings,
} from "../../../utils/space";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {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 Modal from "../../../Modal";
import SpacePublicShare from "./SpacePublicShare"; import SpacePublicShare from "./SpacePublicShare";
@ -127,7 +132,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
if (this.props.space.getJoinRule() === "public") { if (this.props.space.getJoinRule() === "public") {
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, { const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite members"), title: _t("Invite to %(spaceName)s", { spaceName: this.props.space.name }),
description: <React.Fragment> description: <React.Fragment>
<span>{ _t("Share your public space") }</span> <span>{ _t("Share your public space") }</span>
<SpacePublicShare space={this.props.space} onFinished={() => modal.close()} /> <SpacePublicShare space={this.props.space} onFinished={() => modal.close()} />
@ -170,6 +175,14 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
this.setState({contextMenuPosition: null}); // also close the menu this.setState({contextMenuPosition: null}); // also close the menu
}; };
private onAddExistingRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showAddExistingRooms(this.context, this.props.space);
this.setState({contextMenuPosition: null}); // also close the menu
};
private onMembersClick = (ev: ButtonEvent) => { private onMembersClick = (ev: ButtonEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -236,15 +249,20 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
</IconizedContextMenuOptionList>; </IconizedContextMenuOptionList>;
} }
let newRoomOption; let newRoomSection;
if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
newRoomOption = ( newRoomSection = <IconizedContextMenuOptionList first>
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus" iconClassName="mx_SpacePanel_iconPlus"
label={_t("New room")} label={_t("Create new room")}
onClick={this.onNewRoomClick} onClick={this.onNewRoomClick}
/> />
); <IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHash"
label={_t("Add existing room")}
onClick={this.onAddExistingRoomClick}
/>
</IconizedContextMenuOptionList>;
} }
contextMenu = <IconizedContextMenu contextMenu = <IconizedContextMenu
@ -274,8 +292,8 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
label={_t("Explore rooms")} label={_t("Explore rooms")}
onClick={this.onExploreRoomsClick} onClick={this.onExploreRoomsClick}
/> />
{ newRoomOption }
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>
{ newRoomSection }
{ leaveSection } { leaveSection }
</IconizedContextMenu>; </IconizedContextMenu>;
} }
@ -335,7 +353,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const avatarSize = isNested ? 24 : 32; const avatarSize = isNested ? 24 : 32;
const toggleCollapseButton = childSpaces && childSpaces.length ? const toggleCollapseButton = childSpaces && childSpaces.length ?
<button <AccessibleButton
className="mx_SpaceButton_toggleCollapse" className="mx_SpaceButton_toggleCollapse"
onClick={evt => this.toggleCollapse(evt)} onClick={evt => this.toggleCollapse(evt)}
/> : null; /> : null;

View file

@ -1012,11 +1012,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 members": "Invite members", "Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Share your public space": "Share your public space", "Share your public space": "Share your public space",
"Settings": "Settings", "Settings": "Settings",
"Leave space": "Leave space", "Leave space": "Leave space",
"New room": "New room", "Create new room": "Create new room",
"Add existing room": "Add existing room",
"Space Home": "Space Home", "Space Home": "Space Home",
"Members": "Members", "Members": "Members",
"Explore rooms": "Explore rooms", "Explore rooms": "Explore rooms",
@ -1479,6 +1480,7 @@
"<a>Add a topic</a> to help people know what it is about.": "<a>Add a topic</a> to help people know what it is about.", "<a>Add a topic</a> to help people know what it is about.": "<a>Add a topic</a> to help people know what it is about.",
"You created this room.": "You created this room.", "You created this room.": "You created this room.",
"%(displayName)s created this room.": "%(displayName)s created this room.", "%(displayName)s created this room.": "%(displayName)s created this room.",
"Invite to just this room": "Invite to just this room",
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
"This is the start of <roomName/>.": "This is the start of <roomName/>.", "This is the start of <roomName/>.": "This is the start of <roomName/>.",
"No pinned messages.": "No pinned messages.", "No pinned messages.": "No pinned messages.",
@ -1525,11 +1527,8 @@
"Start chat": "Start chat", "Start chat": "Start chat",
"Rooms": "Rooms", "Rooms": "Rooms",
"Add room": "Add room", "Add room": "Add room",
"Create new room": "Create new room",
"You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space", "You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space",
"Add existing room": "Add existing room",
"You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space", "You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space",
"Explore space rooms": "Explore space rooms",
"Explore community rooms": "Explore community rooms", "Explore community rooms": "Explore community rooms",
"Explore public rooms": "Explore public rooms", "Explore public rooms": "Explore public rooms",
"Low priority": "Low priority", "Low priority": "Low priority",
@ -1541,6 +1540,7 @@
"Can't see what youre looking for?": "Can't see what youre looking for?", "Can't see what youre looking for?": "Can't see what youre looking for?",
"Start a new chat": "Start a new chat", "Start a new chat": "Start a new chat",
"Explore all public rooms": "Explore all public rooms", "Explore all public rooms": "Explore all public rooms",
"Quick actions": "Quick actions",
"Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below", "Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below",
"%(count)s results|other": "%(count)s results", "%(count)s results|other": "%(count)s results",
"%(count)s results|one": "%(count)s result", "%(count)s results|one": "%(count)s result",
@ -2004,14 +2004,13 @@
"%(networkName)s rooms": "%(networkName)s rooms", "%(networkName)s rooms": "%(networkName)s rooms",
"Matrix rooms": "Matrix rooms", "Matrix rooms": "Matrix rooms",
"Space selection": "Space selection", "Space selection": "Space selection",
"Add existing spaces/rooms": "Add existing spaces/rooms", "Add existing rooms": "Add existing rooms",
"Filter your rooms and spaces": "Filter your rooms and spaces", "Filter your rooms and spaces": "Filter your rooms and spaces",
"Spaces": "Spaces", "Spaces": "Spaces",
"Don't want to add an existing room?": "Don't want to add an existing room?", "Don't want to add an existing room?": "Don't want to add an existing room?",
"Create a new room": "Create a new room", "Create a new room": "Create a new room",
"Applying...": "Applying...",
"Apply": "Apply",
"Failed to add rooms to space": "Failed to add rooms to space", "Failed to add rooms to space": "Failed to add rooms to space",
"Adding...": "Adding...",
"Matrix ID": "Matrix ID", "Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID", "Matrix Room ID": "Matrix Room ID",
"email address": "email address", "email address": "email address",
@ -2203,7 +2202,6 @@
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).", "Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
"Go": "Go", "Go": "Go",
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Unnamed Space": "Unnamed Space", "Unnamed Space": "Unnamed Space",
"Invite to %(roomName)s": "Invite to %(roomName)s", "Invite to %(roomName)s": "Invite to %(roomName)s",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.", "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",

View file

@ -273,7 +273,10 @@ class RoomViewStore extends Store<ActionPayload> {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const address = this.state.roomAlias || this.state.roomId; const address = this.state.roomAlias || this.state.roomId;
try { try {
await retry<void, MatrixError>(() => cli.joinRoom(address, payload.opts), NUM_JOIN_RETRY, (err) => { await retry<void, MatrixError>(() => cli.joinRoom(address, {
viaServers: payload.via_servers,
...payload.opts,
}), NUM_JOIN_RETRY, (err) => {
// if we received a Gateway timeout then retry // if we received a Gateway timeout then retry
return err.httpStatus === 504; return err.httpStatus === 504;
}); });

View file

@ -34,6 +34,7 @@ import {setHasDiff} from "../utils/sets";
import {objectDiff} from "../utils/objects"; import {objectDiff} from "../utils/objects";
import {arrayHasDiff} from "../utils/arrays"; import {arrayHasDiff} from "../utils/arrays";
import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
import RoomViewStore from "./RoomViewStore";
type SpaceKey = string | symbol; type SpaceKey = string | symbol;
@ -195,15 +196,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
}; };
public rebuild = throttle(() => { // exported for tests private rebuild = throttle(() => {
const visibleRooms = this.matrixClient.getVisibleRooms(); // get all most-upgraded rooms & spaces except spaces which have been left (historical)
const visibleRooms = this.matrixClient.getVisibleRooms().filter(r => {
// Sort spaces by room ID to force the loop breaking to be deterministic return !r.isSpaceRoom() || r.getMyMembership() === "join";
const spaces = sortBy(this.getSpaces(), space => space.roomId); });
const unseenChildren = new Set<Room>([...visibleRooms, ...spaces]);
const unseenChildren = new Set<Room>(visibleRooms);
const backrefs = new EnhancedMap<string, Set<string>>(); const backrefs = new EnhancedMap<string, Set<string>>();
// Sort spaces by room ID to force the cycle breaking to be deterministic
const spaces = sortBy(visibleRooms.filter(r => r.isSpaceRoom()), space => space.roomId);
// TODO handle cleaning up links when a Space is removed // TODO handle cleaning up links when a Space is removed
spaces.forEach(space => { spaces.forEach(space => {
const children = this.getChildren(space.roomId); const children = this.getChildren(space.roomId);
@ -216,7 +220,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren)); const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
// untested algorithm to handle full-cycles // somewhat algorithm to handle full-cycles
const detachedNodes = new Set<Room>(spaces); const detachedNodes = new Set<Room>(spaces);
const markTreeChildren = (rootSpace: Room, unseen: Set<Room>) => { const markTreeChildren = (rootSpace: Room, unseen: Set<Room>) => {
@ -365,6 +369,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.onRoomsUpdate(); this.onRoomsUpdate();
} }
// if the user was looking at the room and then joined select that space
if (room.getMyMembership() === "join" && room.roomId === RoomViewStore.getRoomId()) {
this.setActiveSpace(room);
}
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) {