diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 10357117da..7c3cd1c513 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -130,6 +130,10 @@ $roomListCollapsedWidth: 68px; mask-repeat: no-repeat; background: $secondary-fg-color; } + + &.mx_LeftPanel_exploreButton_space::before { + mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); + } } } diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index ffe67ce6ab..5cca4aca11 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -146,9 +146,6 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton_toggleCollapse { 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; mask-position: center; mask-size: 20px; @@ -342,11 +339,15 @@ $activeBorderColor: $secondary-fg-color; } .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 { - mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); + mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); } } diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index a7ce630b96..3d3b5d1bb8 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -22,7 +22,7 @@ $SpaceRoomViewInnerWidth: 428px; width: 432px; box-sizing: border-box; border-radius: 8px; - border: 1px solid $input-darker-bg-color; + border: 1px solid $space-button-outline-color; font-size: $font-15px; margin: 20px 0; @@ -89,7 +89,7 @@ $SpaceRoomViewInnerWidth: 428px; width: $SpaceRoomViewInnerWidth; text-align: right; // button alignment right - .mx_FormButton { + .mx_AccessibleButton_hasKind { padding: 8px 22px; margin-left: 16px; } diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 0c9d8e3840..a7cfd7bde6 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -28,22 +28,23 @@ limitations under the License. flex-direction: column; flex-wrap: nowrap; min-height: 0; + height: 80vh; .mx_Dialog_title { display: flex; - .mx_BaseAvatar { - display: inline-flex; - margin: 5px 16px 5px 5px; - vertical-align: middle; - } - .mx_BaseAvatar_image { border-radius: 8px; margin: 0; vertical-align: unset; } + .mx_BaseAvatar { + display: inline-flex; + margin: 5px 16px 5px 5px; + vertical-align: middle; + } + > div { > h1 { font-weight: $font-semi-bold; @@ -101,6 +102,7 @@ limitations under the License. .mx_SearchBox { margin: 0; + flex-grow: 0; } .mx_AddExistingToSpaceDialog_errorText { @@ -112,7 +114,10 @@ limitations under the License. } .mx_AddExistingToSpaceDialog_content { + flex-grow: 1; + .mx_AddExistingToSpaceDialog_noResults { + display: block; margin-top: 24px; } } @@ -162,8 +167,14 @@ limitations under the License. > span { flex-grow: 1; - font-size: $font-12px; + font-size: $font-14px; line-height: $font-15px; + font-weight: $font-semi-bold; + + .mx_AccessibleButton { + font-size: inherit; + display: inline-block; + } > * { vertical-align: middle; diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss index c1fa539e9b..6e5fd9c8c8 100644 --- a/res/css/views/dialogs/_SpaceSettingsDialog.scss +++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss @@ -49,7 +49,7 @@ limitations under the License. } } - .mx_FormButton { + .mx_AccessibleButton_hasKind { padding: 8px 22px; } } diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss index 4322ba341c..9c2a428cb3 100644 --- a/res/css/views/rooms/_NewRoomIntro.scss +++ b/res/css/views/rooms/_NewRoomIntro.scss @@ -33,8 +33,13 @@ limitations under the License. .mx_AccessibleButton { line-height: $font-24px; + display: inline-block; - &::before { + & + .mx_AccessibleButton { + margin-left: 12px; + } + + &:not(.mx_AccessibleButton_kind_primary_outline)::before { content: ''; display: inline-block; background-color: $button-fg-color; diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index d49ed4b736..22440fa6db 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -27,6 +27,9 @@ limitations under the License. .mx_RoomList_iconExplore::before { 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 { mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg'); } @@ -35,28 +38,32 @@ limitations under the License. margin: 4px 12px 4px; padding-top: 12px; border-top: 1px solid $tertiary-fg-color; - font-size: $font-13px; + font-size: $font-14px; div:first-child { font-weight: $font-semi-bold; + line-height: $font-18px; + color: $primary-fg-color; } .mx_AccessibleButton { - color: $secondary-fg-color; + color: $primary-fg-color; position: relative; - padding: 0 0 0 24px; + padding: 8px 8px 8px 32px; font-size: inherit; - margin-top: 8px; + margin-top: 12px; display: block; text-align: start; + background-color: $roomlist-button-bg-color; + border-radius: 4px; &::before { content: ''; width: 16px; height: 16px; position: absolute; - top: 0; - left: 0; + top: 8px; + left: 8px; background: $secondary-fg-color; mask-position: center; mask-size: contain; @@ -70,5 +77,13 @@ limitations under the License. &.mx_RoomList_explorePrompt_explore::before { 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'); + } } } diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index bea39e2389..ef3fea351b 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -79,7 +79,7 @@ $spacePanelWidth: 71px; } } - .mx_FormButton { + .mx_AccessibleButton_kind_primary { padding: 8px 22px; margin-left: auto; display: block; diff --git a/res/img/element-icons/roomlist/browse.svg b/res/img/element-icons/roomlist/browse.svg new file mode 100644 index 0000000000..04714e2881 --- /dev/null +++ b/res/img/element-icons/roomlist/browse.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 9ae41b851a..aa758ecbdc 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -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. Modal.createTrackedDialog( "Invite Users", "", InviteDialog, { kind: KIND_INVITE, + initialText, roomId, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index f46319235a..2861cfd7e7 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -16,9 +16,11 @@ limitations under the License. import * as React from "react"; import { createRef } from "react"; +import classNames from "classnames"; +import { Room } from "matrix-js-sdk/src/models/room"; + import GroupFilterPanel from "./GroupFilterPanel"; import CustomRoomTagPanel from "./CustomRoomTagPanel"; -import classNames from "classnames"; import dis from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import RoomList from "../views/rooms/RoomList"; @@ -40,6 +42,7 @@ import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; import {replaceableComponent} from "../../utils/replaceableComponent"; import {mediaFromMxc} from "../../customisations/Media"; +import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; interface IProps { isMinimized: boolean; @@ -49,6 +52,7 @@ interface IProps { interface IState { showBreadcrumbs: boolean; showGroupFilterPanel: boolean; + activeSpace?: Room; } // 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 { this.state = { showBreadcrumbs: BreadcrumbsStore.instance.visible, showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), + activeSpace: SpaceStore.instance.activeSpace, }; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate); + SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); this.bgImageWatcherRef = SettingsStore.watchSetting( "RoomList.backgroundImage", null, this.onBackgroundImageUpdate); this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { @@ -96,9 +102,14 @@ export default class LeftPanel extends React.Component { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate); + SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); } + private updateActiveSpace = (activeSpace: Room) => { + this.setState({ activeSpace }); + }; + private onExplore = () => { dis.fire(Action.ViewRoomDirectory); }; @@ -381,7 +392,9 @@ export default class LeftPanel extends React.Component { onEnter={this.onEnter} /> @@ -407,6 +420,7 @@ export default class LeftPanel extends React.Component { onBlur={this.onBlur} isMinimized={this.props.isMinimized} onResize={this.onResize} + activeSpace={this.state.activeSpace} />; const containerClasses = classNames({ diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 60a2bf4ada..20a3b811c5 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -74,7 +74,6 @@ function canElementReceiveInput(el) { interface IProps { matrixClient: MatrixClient; onRegistered: (credentials: IMatrixClientCreds) => Promise; - viaServers?: string[]; hideToSRUsers: boolean; resizeNotifier: ResizeNotifier; // eslint-disable-next-line camelcase @@ -143,9 +142,6 @@ class LoggedInView extends React.Component { // transitioned to PWLU) onRegistered: PropTypes.func, - // Used by the RoomView to handle joining rooms - viaServers: PropTypes.arrayOf(PropTypes.string), - // and lots and lots of other stuff. }; @@ -625,11 +621,9 @@ class LoggedInView extends React.Component { case PageTypes.RoomView: pageElement = { page_type: PageTypes.RoomView, threepidInvite: roomInfo.threepid_invite, roomOobData: roomInfo.oob_data, - viaServers: roomInfo.via_servers, ready: true, roomJustCreatedOpts: roomInfo.justCreatedOpts, }, () => { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 706cd5ded8..8a9c7cabd9 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -112,10 +112,6 @@ interface IProps { inviterName?: string; }; - // Servers the RoomView can use to try and assist joins - viaServers?: string[]; - - autoJoin?: boolean; resizeNotifier: ResizeNotifier; justCreatedOpts?: IOpts; @@ -450,9 +446,7 @@ export default class RoomView extends React.Component { // now not joined because the js-sdk peeking API will clobber our historical room, // making it impossible to indicate a newly joined room. if (!joining && roomId) { - if (this.props.autoJoin) { - this.onJoinButtonClicked(); - } else if (!room && shouldPeek) { + if (!room && shouldPeek) { console.info("Attempting to peek into room %s", roomId); this.setState({ peekLoading: true, @@ -1123,7 +1117,7 @@ export default class RoomView extends React.Component { const signUrl = this.props.threepidInvite?.signUrl; dis.dispatch({ action: 'join_room', - opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers }, + opts: { inviteSignUrl: signUrl }, _type: "unknown", // TODO: instrumentation }); return Promise.resolve(); diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 6daa8526bc..abeb858274 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -32,6 +32,8 @@ export default class SearchBox extends React.Component { onKeyDown: PropTypes.func, className: PropTypes.string, placeholder: PropTypes.string.isRequired, + autoFocus: PropTypes.bool, + initialValue: PropTypes.string, // If true, the search box will focus and clear itself // 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.state = { - searchTerm: "", + searchTerm: this.props.initialValue || "", blurred: true, }; } @@ -158,6 +160,7 @@ export default class SearchBox extends React.Component { onBlur={this._onBlur} placeholder={ placeholder } autoComplete="off" + autoFocus={this.props.autoFocus} /> { clearButton } diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 0a53f38238..0dfb33379d 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -15,7 +15,8 @@ limitations under the License. */ 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 classNames from "classnames"; import {sortBy} from "lodash"; @@ -77,7 +78,6 @@ export interface ISpaceSummaryEvent { interface ITileProps { room: ISpaceSummaryRoom; - editing?: boolean; suggested?: boolean; selected?: boolean; numChildRooms?: number; @@ -88,7 +88,6 @@ interface ITileProps { const Tile: React.FC = ({ room, - editing, suggested, selected, hasPermissions, @@ -170,12 +169,6 @@ const Tile: React.FC = ({ ; - if (editing) { - return
- { content } -
- } - let childToggle; let childSection; if (children) { @@ -201,7 +194,7 @@ const Tile: React.FC = ({ className={classNames("mx_SpaceRoomDirectory_roomTile", { mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space, })} - onClick={hasPermissions ? onToggleClick : onPreviewClick} + onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick} > { content } { childToggle } @@ -240,7 +233,7 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi interface IHierarchyLevelProps { spaceId: string; rooms: Map; - relations: EnhancedMap>; + relations: Map>; parents: Set; selectedMap?: Map>; onViewRoomClick(roomId: string, autoJoin: boolean): void; @@ -260,7 +253,7 @@ export const HierarchyLevel = ({ const space = cli.getRoom(spaceId); 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 roomId = ev.state_key; if (!rooms.has(roomId)) return result; @@ -316,23 +309,15 @@ export const HierarchyLevel = ({ }; -const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinished }) => { +// mutate argument refreshToken to force a reload +export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [ + ISpaceSummaryRoom[], + Map>, + Map>, + Map>, +] | [] => { // TODO pagination - 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>()); // Map> - - const [rooms, parentChildMap, childParentMap, viaMap] = useAsyncMemo(async () => { + return useAsyncMemo(async () => { try { const data = await cli.getSpaceSummary(space.roomId); @@ -350,13 +335,31 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis } }); - return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, childParentRelations, viaMap]; + return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations]; } catch (e) { console.error(e); // TODO } return []; - }, [space], []); + }, [space, refreshToken], []); +}; + +const SpaceRoomDirectory: React.FC = ({ 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>()); // Map> + + const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space); const roomsMap = useMemo(() => { if (!rooms) return null; @@ -570,6 +573,8 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis className="mx_textinput_icon mx_textinput_search" placeholder={ _t("Search names and description") } onSearch={setQuery} + autoFocus={true} + initialValue={initialText} /> { content } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 95846d8e21..cea59093ac 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and 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 {Room} from "matrix-js-sdk/src/models/room"; import {EventSubscription} from "fbemitter"; @@ -26,7 +26,6 @@ import AccessibleButton from "../views/elements/AccessibleButton"; import RoomName from "../views/elements/RoomName"; import RoomTopic from "../views/elements/RoomTopic"; import InlineSpinner from "../views/elements/InlineSpinner"; -import FormButton from "../views/elements/FormButton"; import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite"; import {useRoomMembers} from "../../hooks/useRoomMembers"; import createRoom, {IOpts, Preset} from "../../createRoom"; @@ -47,9 +46,7 @@ import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanel import {useStateArray} from "../../hooks/useStateArray"; import SpacePublicShare from "../views/spaces/SpacePublicShare"; import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space"; -import {HierarchyLevel, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom} from "./SpaceRoomDirectory"; -import {useAsyncMemo} from "../../hooks/useAsyncMemo"; -import {EnhancedMap} from "../../utils/maps"; +import {HierarchyLevel, ISpaceSummaryRoom, showRoom, useSpaceSummary} from "./SpaceRoomDirectory"; import AutoHideScrollbar from "./AutoHideScrollbar"; import MemberAvatar from "../views/avatars/MemberAvatar"; import {useStateToggle} from "../../hooks/useStateToggle"; @@ -124,30 +121,36 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => } joinButtons = <> - { setBusy(true); onRejectButtonClicked(); - }} /> - + { _t("Reject") } + + { setBusy(true); onJoinButtonClicked(); }} - /> + > + { _t("Accept") } + ; } else { joinButtons = ( - { setBusy(true); onJoinButtonClicked(); }} - /> + > + { _t("Join") } + ) } @@ -223,7 +226,7 @@ const SpaceLanding = ({ space }) => { const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); - const [_, forceUpdate] = useStateToggle(false); // TODO + const [refreshToken, forceUpdate] = useStateToggle(false); let addRoomButtons; if (canAddRooms) { @@ -253,26 +256,13 @@ const SpaceLanding = ({ space }) => { ; } - const [loading, roomsMap, relations, numRooms] = useAsyncMemo(async () => { - try { - const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join"); - - const parentChildRelations = new EnhancedMap>(); - data.events.map((ev: ISpaceSummaryEvent) => { - if (ev.type === EventType.SpaceChild) { - parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev); - } - }); - - const roomsMap = new Map(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]); + const [rooms, relations, viaMap] = useSpaceSummary(cli, space, refreshToken); + const [roomsMap, numRooms] = useMemo(() => { + if (!rooms) return []; + const roomsMap = new Map(rooms.map(r => [r.room_id, r])); + const numRooms = rooms.filter(r => r.room_type !== RoomType.Space).length; + return [roomsMap, numRooms]; + }, [rooms]); let previewRooms; if (roomsMap) { @@ -287,11 +277,11 @@ const SpaceLanding = ({ space }) => { relations={relations} parents={new Set()} onViewRoomClick={(roomId, autoJoin) => { - showRoom(roomsMap.get(roomId), [], autoJoin); + showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); }} /> ; - } else if (loading) { + } else if (!rooms) { previewRooms = ; } else { previewRooms =

{_t("Your server does not support showing space hierarchies.")}

; @@ -407,11 +397,13 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { { fields }
- + > + { buttonLabel } +
; }; @@ -419,14 +411,16 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { const SpaceSetupPublicShare = ({ space, onFinished }) => { return

{ _t("Share %(name)s", { name: space.name }) }

-
+
{ _t("It's just you at the moment, it will be even better with others.") }
- +
- + + { _t("Go to my first room") } +
; }; @@ -545,7 +539,9 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
- + + { buttonLabel } +
; }; @@ -630,6 +626,8 @@ export default class SpaceRoomView extends React.PureComponent { }; private goToFirstRoom = async () => { + // TODO actually go to the first room + const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId); if (childRooms.length) { const room = childRooms[0]; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 500637244a..04bec39238 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -22,7 +22,6 @@ import {MatrixClient} from "matrix-js-sdk/src/client"; import {_t} from '../../../languageHandler'; import {IDialogProps} from "./IDialogProps"; import BaseDialog from "./BaseDialog"; -import FormButton from "../elements/FormButton"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; import SpaceStore from "../../../stores/SpaceStore"; @@ -110,7 +109,7 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, const title =
-

{ _t("Add existing spaces/rooms") }

+

{ _t("Add existing rooms") }

{ spaceOptionSection }
; @@ -128,29 +127,9 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, className="mx_textinput_icon mx_textinput_search" placeholder={ _t("Filter your rooms and spaces") } onSearch={setQuery} + autoComplete={true} /> - { spaces.length > 0 ? ( -
-

{ _t("Spaces") }

- { spaces.map(space => { - return { - if (checked) { - selectedToAdd.add(space); - } else { - selectedToAdd.delete(space); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} - />; - }) } -
- ) : null } - { rooms.length > 0 ? (

{ _t("Rooms") }

@@ -172,6 +151,27 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space,
) : undefined } + { spaces.length > 0 ? ( +
+

{ _t("Spaces") }

+ { spaces.map(space => { + return { + if (checked) { + selectedToAdd.add(space); + } else { + selectedToAdd.delete(space); + } + setSelectedToAdd(new Set(selectedToAdd)); + }} + />; + }) } +
+ ) : null } + { spaces.length + rooms.length < 1 ? { _t("No results") } : undefined } @@ -185,8 +185,8 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, - { setBusy(true); @@ -200,7 +200,9 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, } setBusy(false); }} - /> + > + { busy ? _t("Adding...") : _t("Add") } + ; }; diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index f6bf5b87e6..b016e320eb 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -28,7 +28,6 @@ import {getTopic} from "../elements/RoomTopic"; import {avatarUrlForRoom} from "../../../Avatar"; import ToggleSwitch from "../elements/ToggleSwitch"; import AccessibleButton from "../elements/AccessibleButton"; -import FormButton from "../elements/FormButton"; import Modal from "../../../Modal"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {allSettled} from "../../../utils/promise"; @@ -134,16 +133,17 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin /> - { defaultDispatcher.dispatch({ action: "leave_room", room_id: space.roomId, }); }} - /> + > + { _t("Leave Space") } +
Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}> @@ -152,7 +152,9 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin { _t("Cancel") } - + + { busy ? _t("Saving...") : _t("Save Changes") } +
; diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index ce426a64ed..c85b9d7868 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -28,6 +28,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; import {Action} from "../../../dispatcher/actions"; import dis from "../../../dispatcher/dispatcher"; +import SpaceStore from "../../../stores/SpaceStore"; const NewRoomIntro = () => { const cli = useContext(MatrixClientContext); @@ -100,17 +101,48 @@ const NewRoomIntro = () => { }); } - let buttons; - if (room.canInvite(cli.getUserId())) { - const onInviteClick = () => { - dis.dispatch({ action: "view_invite", roomId }); - }; + let parentSpace; + if ( + SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) && + SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId) + ) { + parentSpace = SpaceStore.instance.activeSpace; + } + let buttons; + if (parentSpace) { buttons =
- + { + dis.dispatch({ action: "view_invite", roomId }); + }} + > + {_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })} + + { room.canInvite(cli.getUserId()) && { + dis.dispatch({ action: "view_invite", roomId }); + }} + > + {_t("Invite to just this room")} + } +
; + } else if (room.canInvite(cli.getUserId())) { + buttons =
+ { + dis.dispatch({ action: "view_invite", roomId }); + }} + > {_t("Invite to this room")} -
+ ; } const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url; diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 4378154d8f..e83b07f71b 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -20,6 +20,7 @@ import React, { ReactComponentElement } from "react"; import { Dispatcher } from "flux"; import { Room } from "matrix-js-sdk/src/models/room"; import * as fbEmitter from "fbemitter"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t, _td } from "../../../languageHandler"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; @@ -48,12 +49,15 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con import AccessibleButton from "../elements/AccessibleButton"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; 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 { EventType } from "matrix-js-sdk/src/@types/event"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import RoomAvatar from "../avatars/RoomAvatar"; 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 { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -62,6 +66,7 @@ interface IProps { onResize: () => void; resizeNotifier: ResizeNotifier; isMinimized: boolean; + activeSpace: Room; } interface IState { @@ -194,8 +199,8 @@ const TAG_AESTHETICS: ITagAestheticsMap = { : _t("You do not have permissions to add rooms to this space")} /> { e.preventDefault(); e.stopPropagation(); @@ -424,6 +429,25 @@ export default class RoomList extends React.PureComponent { 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: + { _t("Share your public space") } + modal.close()} /> + , + fixedWidth: false, + button: false, + className: "mx_SpacePanel_sharePublicSpace", + hasCloseButton: true, + }); + } else { + showRoomInviteDialog(this.props.activeSpace.roomId, initialText); + } + }; + private renderSuggestedRooms(): ReactComponentElement[] { return this.state.suggestedRooms.map(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 { kind="link" onClick={this.onExplore} > - {_t("Explore all public rooms")} + { this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") } + + ; + } else if (this.props.activeSpace) { + explorePrompt =
+
{ _t("Quick actions") }
+ { this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && + {_t("Invite people")} + } + + {_t("Explore rooms")}
; } else if (Object.values(this.state.sublists).some(list => list.length > 0)) { diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 879cf929e0..9ee6edc489 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -21,7 +21,6 @@ import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types import {_t} from "../../../languageHandler"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {ChevronFace, ContextMenu} from "../../structures/ContextMenu"; -import FormButton from "../elements/FormButton"; import createRoom, {IStateEvent, Preset} from "../../../createRoom"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import SpaceBasicSettings from "./SpaceBasicSettings"; @@ -89,6 +88,7 @@ const SpaceCreateMenu = ({ onFinished }) => { power_level_content_override: { // Only allow Admins to write to the timeline to prevent hidden sync spam events_default: 100, + ...Visibility.Public ? { invite: 0 } : {}, }, }, spinner: false, @@ -148,11 +148,9 @@ const SpaceCreateMenu = ({ onFinished }) => { - + + { busy ? _t("Creating...") : _t("Create") } + ; } diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx index b2d3b7ce29..fa81b75525 100644 --- a/src/components/views/spaces/SpacePublicShare.tsx +++ b/src/components/views/spaces/SpacePublicShare.tsx @@ -26,7 +26,7 @@ import {showRoomInviteDialog} from "../../../RoomInvite"; interface IProps { space: Room; - onFinished(): void; + onFinished?(): void; } const SpacePublicShare = ({ space, onFinished }: IProps) => { @@ -54,7 +54,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => { className="mx_SpacePublicShare_inviteButton" onClick={() => { showRoomInviteDialog(space.roomId); - onFinished(); + if (onFinished) onFinished(); }} >

{ _t("Invite people") }

diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 83bc2296e7..ca6f90fa91 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -30,9 +30,14 @@ import IconizedContextMenu, { import {_t} from "../../../languageHandler"; import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; 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 {ButtonEvent} from "../elements/AccessibleButton"; +import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import Modal from "../../../Modal"; import SpacePublicShare from "./SpacePublicShare"; @@ -127,7 +132,7 @@ export class SpaceItem extends React.PureComponent { if (this.props.space.getJoinRule() === "public") { 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: { _t("Share your public space") } modal.close()} /> @@ -170,6 +175,14 @@ export class SpaceItem extends React.PureComponent { 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) => { ev.preventDefault(); ev.stopPropagation(); @@ -236,15 +249,20 @@ export class SpaceItem extends React.PureComponent { ; } - let newRoomOption; + let newRoomSection; if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { - newRoomOption = ( + newRoomSection = - ); + + ; } contextMenu = { label={_t("Explore rooms")} onClick={this.onExploreRoomsClick} /> - { newRoomOption } + { newRoomSection } { leaveSection } ; } @@ -335,7 +353,7 @@ export class SpaceItem extends React.PureComponent { const avatarSize = isNested ? 24 : 32; const toggleCollapseButton = childSpaces && childSpaces.length ? -