Implement more meta-spaces (#7077)

This commit is contained in:
Michael Telatynski 2021-11-11 13:07:41 +00:00 committed by GitHub
parent dadac386fe
commit 5ad3261cb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 970 additions and 353 deletions

View file

@ -17,7 +17,6 @@ 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 dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
@ -37,10 +36,12 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore";
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps {
isMinimized: boolean;
@ -49,7 +50,7 @@ interface IProps {
interface IState {
showBreadcrumbs: boolean;
activeSpace?: Room;
activeSpace: SpaceKey;
}
@replaceableComponent("structures.LeftPanel")
@ -61,6 +62,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private focusedElement = null;
private isDoingStickyHeaders = false;
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
constructor(props: IProps) {
super(props);
@ -98,7 +102,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
}
}
private updateActiveSpace = (activeSpace: Room) => {
private updateActiveSpace = (activeSpace: SpaceKey) => {
this.setState({ activeSpace });
};
@ -343,6 +347,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
/>;
}
const space = this.state.activeSpace[0] === "!" ? this.context.getRoom(this.state.activeSpace) : null;
return (
<div
className="mx_LeftPanel_filterContainer"
@ -363,9 +368,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
})}
onClick={this.onExplore}
title={this.state.activeSpace
? _t("Explore %(spaceName)s", { spaceName: this.state.activeSpace.name })
: _t("Explore rooms")}
title={space ? _t("Explore %(spaceName)s", { spaceName: space.name }) : _t("Explore rooms")}
/>
</div>
);

View file

@ -64,7 +64,7 @@ import MyGroups from "./MyGroups";
import UserView from "./UserView";
import GroupView from "./GroupView";
import BackdropPanel from "./BackdropPanel";
import SpaceStore from "../../stores/SpaceStore";
import SpaceStore from "../../stores/spaces/SpaceStore";
import classNames from 'classnames';
import GroupFilterPanel from './GroupFilterPanel';
import CustomRoomTagPanel from './CustomRoomTagPanel';

View file

@ -78,7 +78,7 @@ import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages";
import SpaceStore from "../../stores/SpaceStore";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { replaceableComponent } from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { RoomUpdateCause } from "../../stores/room-list/models";
@ -712,10 +712,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break;
}
case Action.ViewRoomDirectory: {
if (SpaceStore.instance.activeSpace) {
if (SpaceStore.instance.activeSpace[0] === "!") {
defaultDispatcher.dispatch({
action: "view_room",
room_id: SpaceStore.instance.activeSpace.roomId,
room_id: SpaceStore.instance.activeSpace,
});
} else {
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {

View file

@ -50,7 +50,7 @@ import NotificationPanel from "./NotificationPanel";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
import { throttle } from 'lodash';
import SpaceStore from "../../stores/SpaceStore";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import { E2EStatus } from '../../utils/ShieldUtils';
import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads';

View file

@ -28,7 +28,8 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/spaces";
interface IProps {
isMinimized: boolean;

View file

@ -88,7 +88,7 @@ import RoomStatusBar from "./RoomStatusBar";
import MessageComposer from '../views/rooms/MessageComposer';
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
import SpaceStore from "../../stores/SpaceStore";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';

View file

@ -49,7 +49,7 @@ import { mediaFromMxc } from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip";
import { useStateToggle } from "../../hooks/useStateToggle";
import { getChildOrder } from "../../stores/SpaceStore";
import { getChildOrder } from "../../stores/spaces/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { linkifyElement } from "../../HtmlUtils";
import { useDispatcher } from "../../hooks/useDispatcher";

View file

@ -57,7 +57,7 @@ import {
} from "../../utils/space";
import SpaceHierarchy, { joinRoom, showRoom } from "./SpaceHierarchy";
import MemberAvatar from "../views/avatars/MemberAvatar";
import SpaceStore from "../../stores/SpaceStore";
import SpaceStore from "../../stores/spaces/SpaceStore";
import FacePile from "../views/elements/FacePile";
import {
AddExistingToSpace,

View file

@ -54,7 +54,8 @@ import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototyp
import { UIFeature } from "../../settings/UIFeature";
import HostSignupAction from "./HostSignupAction";
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import RoomName from "../views/elements/RoomName";
import { replaceableComponent } from "../../utils/replaceableComponent";
import InlineSpinner from "../views/elements/InlineSpinner";
@ -90,6 +91,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
isDarkTheme: this.isUserOnDarkTheme(),
isHighContrast: this.isUserOnHighContrastTheme(),
pendingRoomJoin: new Set<string>(),
selectedSpace: SpaceStore.instance.activeSpaceRoom,
};
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
@ -162,8 +164,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.forceUpdate();
};
private onSelectedSpaceUpdate = async (selectedSpace?: Room) => {
this.setState({ selectedSpace });
private onSelectedSpaceUpdate = async () => {
this.setState({
selectedSpace: SpaceStore.instance.activeSpaceRoom,
});
};
private onThemeChanged = () => {

View file

@ -24,7 +24,7 @@ import { _t } from '../../../languageHandler';
import BaseDialog from "./BaseDialog";
import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar";
import { getDisplayAliasForRoom } from "../../../Rooms";
import AccessibleButton from "../elements/AccessibleButton";

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, { ComponentProps, useMemo, useState } from 'react';
import ConfirmUserActionDialog from "./ConfirmUserActionDialog";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { Room } from "matrix-js-sdk/src/models/room";
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";

View file

@ -32,7 +32,7 @@ import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
interface IProps {

View file

@ -32,7 +32,7 @@ import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/P
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import Spinner from "../elements/Spinner";
import { mediaFromMxc } from "../../../customisations/Media";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
import dis from "../../../dispatcher/dispatcher";

View file

@ -25,7 +25,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { BetaPill } from "../beta/BetaCard";
import Field from "../elements/Field";
import RoomAliasField from "../elements/RoomAliasField";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
import { SubspaceSelector } from "./AddExistingToSpaceDialog";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";

View file

@ -43,7 +43,7 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
const AVATAR_SIZE = 30;

View file

@ -71,7 +71,7 @@ import QuestionDialog from "./QuestionDialog";
import Spinner from "../elements/Spinner";
import BaseDialog from "./BaseDialog";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";

View file

@ -21,7 +21,7 @@ import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t } from '../../../languageHandler';
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
interface IProps {

View file

@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleButton from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
@ -75,7 +75,7 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
const [spacesContainingRoom, otherEntries] = useMemo(() => {
const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom());
return [
spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r).has(room.roomId)),
spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r.roomId).has(room.roomId)),
selected.map(roomId => {
const room = cli.getRoom(roomId);
if (!room) {

View file

@ -34,6 +34,7 @@ import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
import SidebarUserSettingsTab from "../settings/tabs/user/SidebarUserSettingsTab";
export enum UserTab {
General = "USER_GENERAL_TAB",
@ -41,6 +42,7 @@ export enum UserTab {
Flair = "USER_FLAIR_TAB",
Notifications = "USER_NOTIFICATIONS_TAB",
Preferences = "USER_PREFERENCES_TAB",
Sidebar = "USER_SIDEBAR_TAB",
Voice = "USER_VOICE_TAB",
Security = "USER_SECURITY_TAB",
Labs = "USER_LABS_TAB",
@ -117,6 +119,15 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
<PreferencesUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
if (SettingsStore.getValue("feature_spaces_metaspaces")) {
tabs.push(new Tab(
UserTab.Sidebar,
_td("Sidebar"),
"mx_UserSettingsDialog_sidebarIcon",
<SidebarUserSettingsTab />,
));
}
if (SettingsStore.getValue(UIFeature.Voip)) {
tabs.push(new Tab(
UserTab.Voice,

View file

@ -69,7 +69,7 @@ import RoomName from "../elements/RoomName";
import { mediaFromMxc } from "../../../customisations/Media";
import UIStore from "../../../stores/UIStore";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog";
import { bulkSpaceBehaviour } from "../../../utils/space";

View file

@ -43,7 +43,7 @@ import EntityTile from "./EntityTile";
import MemberTile from "./MemberTile";
import BaseAvatar from '../avatars/BaseAvatar';
import { throttle } from 'lodash';
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";

View file

@ -31,7 +31,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import dis from "../../../dispatcher/dispatcher";
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../../dispatcher/actions";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { showSpaceInvite } from "../../../utils/space";
import { privateShouldBeEncrypted } from "../../../createRoom";
import EventTileBubble from "../messages/EventTileBubble";
@ -126,12 +126,12 @@ const NewRoomIntro = () => {
});
}
let parentSpace;
let parentSpace: Room;
if (
SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) &&
SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getUserId()) &&
SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId)
) {
parentSpace = SpaceStore.instance.activeSpace;
parentSpace = SpaceStore.instance.activeSpaceRoom;
}
let buttons;

View file

@ -21,7 +21,7 @@ import * as fbEmitter from "fbemitter";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t, _td } from "../../../languageHandler";
import { RovingTabIndexProvider, IState as IRovingTabIndexState } from "../../../accessibility/RovingTabIndex";
import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore";
@ -44,7 +44,8 @@ import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import SpaceStore, { ISuggestedRoom, SUGGESTED_ROOMS } from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { ISuggestedRoom, MetaSpace, SpaceKey, UPDATE_SUGGESTED_ROOMS } from "../../../stores/spaces";
import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomAvatar from "../avatars/RoomAvatar";
@ -52,6 +53,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
@ -61,7 +63,7 @@ interface IProps {
onListCollapse?: (isExpanded: boolean) => void;
resizeNotifier: ResizeNotifier;
isMinimized: boolean;
activeSpace: Room;
activeSpace: SpaceKey;
}
interface IState {
@ -131,9 +133,10 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
defaultHidden: false,
addRoomLabel: _td("Add room"),
addRoomContextMenu: (onFinished: () => void) => {
if (SpaceStore.instance.activeSpace) {
const canAddRooms = SpaceStore.instance.activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
MatrixClientPeg.get().getUserId());
if (SpaceStore.instance.activeSpaceRoom) {
const userId = MatrixClientPeg.get().getUserId();
const space = SpaceStore.instance.activeSpaceRoom;
const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
return <IconizedContextMenuOptionList first>
{
@ -146,7 +149,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
e.preventDefault();
e.stopPropagation();
onFinished();
showCreateNewRoom(SpaceStore.instance.activeSpace);
showCreateNewRoom(space);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
@ -159,7 +162,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
e.preventDefault();
e.stopPropagation();
onFinished();
showAddExistingRooms(SpaceStore.instance.activeSpace);
showAddExistingRooms(space);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
@ -251,6 +254,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private roomStoreToken: fbEmitter.EventSubscription;
private treeRef = createRef<HTMLDivElement>();
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
constructor(props: IProps) {
super(props);
@ -264,14 +270,14 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
public componentDidMount(): void {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms);
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
this.updateLists(); // trigger the first update
}
public componentWillUnmount() {
SpaceStore.instance.off(SUGGESTED_ROOMS, this.updateSuggestedRooms);
SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
defaultDispatcher.unregister(this.dispatcherRef);
if (this.customTagStoreRef) this.customTagStoreRef.remove();
@ -379,7 +385,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private onSpaceInviteClick = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
showSpaceInvite(this.props.activeSpace, initialText);
showSpaceInvite(this.context.getRoom(this.props.activeSpace), initialText);
};
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
@ -485,6 +491,15 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
: TAG_AESTHETICS[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
let alwaysVisible = ALWAYS_VISIBLE_TAGS.includes(orderedTagId);
if (
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId !== DefaultTagID.Favourite) ||
(this.props.activeSpace === MetaSpace.People && orderedTagId !== DefaultTagID.DM) ||
(this.props.activeSpace === MetaSpace.Orphans && orderedTagId === DefaultTagID.DM)
) {
alwaysVisible = false;
}
// The cost of mounting/unmounting this component offsets the cost
// of keeping it in the DOM and hiding it when it is not required
return <RoomSublist
@ -500,7 +515,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
showSkeleton={showSkeleton}
extraTiles={extraTiles}
resizeNotifier={this.props.resizeNotifier}
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
alwaysVisible={alwaysVisible}
onListCollapse={this.props.onListCollapse}
/>;
});
@ -515,6 +530,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
public render() {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const activeSpace = this.props.activeSpace[0] === "!" ? cli.getRoom(this.props.activeSpace) : null;
let explorePrompt: JSX.Element;
if (!this.props.isMinimized) {
@ -533,17 +549,16 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
kind="link"
onClick={this.onExplore}
>
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
{ activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
</AccessibleButton>
</div>;
} else if (
this.props.activeSpace?.canInvite(userId) ||
this.props.activeSpace?.getMyMembership() === "join" ||
this.props.activeSpace?.getJoinRule() === JoinRule.Public
activeSpace?.canInvite(userId) ||
activeSpace?.getMyMembership() === "join" ||
activeSpace?.getJoinRule() === JoinRule.Public
) {
const spaceName = this.props.activeSpace.name;
const canInvite = this.props.activeSpace?.canInvite(userId) ||
this.props.activeSpace?.getJoinRule() === JoinRule.Public;
const spaceName = activeSpace.name;
const canInvite = activeSpace?.canInvite(userId) || activeSpace?.getJoinRule() === JoinRule.Public;
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Quick actions") }</div>
{ canInvite && <AccessibleTooltipButton
@ -553,7 +568,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
>
{ _t("Invite people") }
</AccessibleTooltipButton> }
{ this.props.activeSpace?.getMyMembership() === "join" && <AccessibleTooltipButton
{ activeSpace?.getMyMembership() === "join" && <AccessibleTooltipButton
className="mx_RoomList_explorePrompt_spaceExplore"
onClick={this.onExplore}
title={_t("Explore %(spaceName)s", { spaceName })}

View file

@ -19,7 +19,7 @@ import React, { useEffect, useState } from "react";
import { _t } from "../../../languageHandler";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
interface IProps {
onVisibilityChange?: () => void;

View file

@ -27,7 +27,7 @@ import RoomName from "../elements/RoomName";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import ErrorDialog from '../dialogs/ErrorDialog';
import AccessibleButton from '../elements/AccessibleButton';
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";

View file

@ -23,7 +23,7 @@ import StyledRadioGroup, { IDefinition } from "../elements/StyledRadioGroup";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
@ -67,8 +67,8 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
const editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
let selected = restrictedAllowRoomIds;
if (!selected?.length && SpaceStore.instance.activeSpace) {
selected = [SpaceStore.instance.activeSpace.roomId];
if (!selected?.length && SpaceStore.instance.activeSpaceRoom) {
selected = [SpaceStore.instance.activeSpaceRoom.roomId];
}
const matrixClient = MatrixClientPeg.get();
@ -176,9 +176,9 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
{ moreText && <span>{ moreText }</span> }
</div>
</div>;
} else if (SpaceStore.instance.activeSpace) {
} else if (SpaceStore.instance.activeSpaceRoom) {
description = _t("Anyone in <spaceName/> can find and join. You can select other spaces too.", {}, {
spaceName: () => <b>{ SpaceStore.instance.activeSpace.name }</b>,
spaceName: () => <b>{ SpaceStore.instance.activeSpaceRoom.name }</b>,
});
} else {
description = _t("Anyone in a space can find and join. You can select multiple spaces.");

View file

@ -0,0 +1,123 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent } from 'react';
import { _t } from "../../../../../languageHandler";
import SettingsStore from "../../../../../settings/SettingsStore";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import StyledCheckbox from "../../../elements/StyledCheckbox";
import { useSettingValue } from "../../../../../hooks/useSettings";
import { MetaSpace } from "../../../../../stores/spaces";
const onMetaSpaceChangeFactory = (metaSpace: MetaSpace) => (e: ChangeEvent<HTMLInputElement>) => {
const currentValue = SettingsStore.getValue("Spaces.enabledMetaSpaces");
SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.ACCOUNT, {
...currentValue,
[metaSpace]: e.target.checked,
});
};
const SidebarUserSettingsTab = () => {
const {
[MetaSpace.Home]: homeEnabled,
[MetaSpace.Favourites]: favouritesEnabled,
[MetaSpace.People]: peopleEnabled,
[MetaSpace.Orphans]: orphansEnabled,
} = useSettingValue<Record<MetaSpace, boolean>>("Spaces.enabledMetaSpaces");
const allRoomsInHome = useSettingValue<boolean>("Spaces.allRoomsInHome");
return (
<div className="mx_SettingsTab mx_SidebarUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Sidebar") }</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
<div className="mx_SettingsTab_subsectionText">{ _t("Spaces are ways to group rooms and people.") }</div>
<div className="mx_SidebarUserSettingsTab_subheading">{ _t("Spaces to show") }</div>
<div className="mx_SettingsTab_subsectionText">
{ _t("Along with the spaces you're in, you can use some pre-built ones too.") }
</div>
<StyledCheckbox
checked={!!homeEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.Home)}
className="mx_SidebarUserSettingsTab_homeCheckbox"
>
{ _t("Home") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Home is useful for getting an overview of everything.") }
</div>
<StyledCheckbox
checked={allRoomsInHome}
disabled={!homeEnabled}
onChange={e => {
SettingsStore.setValue(
"Spaces.allRoomsInHome",
null,
SettingLevel.ACCOUNT,
e.target.checked,
);
}}
className="mx_SidebarUserSettingsTab_homeAllRoomsCheckbox"
>
{ _t("Show all rooms") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Show all your rooms in Home, even if they're in a space.") }
</div>
<StyledCheckbox
checked={!!favouritesEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.Favourites)}
className="mx_SidebarUserSettingsTab_favouritesCheckbox"
>
{ _t("Favourites") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Automatically group all your favourite rooms and people together in one place.") }
</div>
<StyledCheckbox
checked={!!peopleEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.People)}
className="mx_SidebarUserSettingsTab_peopleCheckbox"
>
{ _t("People") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Automatically group all your people together in one place.") }
</div>
<StyledCheckbox
checked={!!orphansEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.Orphans)}
className="mx_SidebarUserSettingsTab_orphansCheckbox"
>
{ _t("Rooms outside of a space") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Automatically group all your rooms that aren't part of a space in one place.") }
</div>
</div>
</div>
);
};
export default SidebarUserSettingsTab;

View file

@ -119,6 +119,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
rageshakeLabel: "spaces-feedback",
rageshakeData: Object.fromEntries([
"Spaces.allRoomsInHome",
"Spaces.enabledMetaSpaces",
].map(k => [k, SettingsStore.getValue(k)])),
});
}}

View file

@ -34,13 +34,15 @@ import SpaceCreateMenu from "./SpaceCreateMenu";
import { SpaceButton, SpaceItem } from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import SpaceStore, {
HOME_SPACE,
import SpaceStore from "../../../stores/spaces/SpaceStore";
import {
MetaSpace,
SpaceKey,
UPDATE_HOME_BEHAVIOUR,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
} from "../../../stores/SpaceStore";
} from "../../../stores/spaces";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
@ -53,17 +55,21 @@ import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import UIStore from "../../../stores/UIStore";
const useSpaces = (): [Room[], Room[], Room | null] => {
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
return SpaceStore.instance.invitedSpaces;
});
const spaces = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, () => {
return SpaceStore.instance.spacePanelSpaces;
});
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
const [metaSpaces, actualSpaces] = useEventEmitterState<[MetaSpace[], Room[]]>(
SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES,
() => [
SpaceStore.instance.enabledMetaSpaces,
SpaceStore.instance.spacePanelSpaces,
],
);
const activeSpace = useEventEmitterState<SpaceKey>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpace;
});
return [invites, spaces, activeSpace];
return [invites, metaSpaces, actualSpaces, activeSpace];
};
interface IInnerSpacePanelProps {
@ -99,37 +105,76 @@ const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps<typeof S
</IconizedContextMenu>;
};
interface IHomeButtonProps {
interface IMetaSpaceButtonProps extends ComponentProps<typeof SpaceButton> {
selected: boolean;
isPanelCollapsed: boolean;
}
const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => {
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
type MetaSpaceButtonProps = Pick<IMetaSpaceButtonProps, "selected" | "isPanelCollapsed">;
const MetaSpaceButton = ({ selected, isPanelCollapsed, ...props }: IMetaSpaceButtonProps) => {
return <li
className={classNames("mx_SpaceItem", {
"collapsed": isPanelCollapsed,
})}
role="treeitem"
>
<SpaceButton
className="mx_SpaceButton_home"
onClick={() => SpaceStore.instance.setActiveSpace(null)}
selected={selected}
label={allRoomsInHome ? _t("All rooms") : _t("Home")}
notificationState={allRoomsInHome
? RoomNotificationStateStore.instance.globalState
: SpaceStore.instance.getNotificationState(HOME_SPACE)}
isNarrow={isPanelCollapsed}
ContextMenuComponent={HomeButtonContextMenu}
contextMenuTooltip={_t("Options")}
/>
<SpaceButton {...props} selected={selected} isNarrow={isPanelCollapsed} />
</li>;
};
const HomeButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
return <MetaSpaceButton
spaceKey={MetaSpace.Home}
className="mx_SpaceButton_home"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={allRoomsInHome ? _t("All rooms") : _t("Home")}
notificationState={allRoomsInHome
? RoomNotificationStateStore.instance.globalState
: SpaceStore.instance.getNotificationState(MetaSpace.Home)}
ContextMenuComponent={HomeButtonContextMenu}
contextMenuTooltip={_t("Options")}
/>;
};
const FavouritesButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
return <MetaSpaceButton
spaceKey={MetaSpace.Favourites}
className="mx_SpaceButton_favourites"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={_t("Favourites")}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.Favourites)}
/>;
};
const PeopleButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
return <MetaSpaceButton
spaceKey={MetaSpace.People}
className="mx_SpaceButton_people"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={_t("People")}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.People)}
/>;
};
const OrphansButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
return <MetaSpaceButton
spaceKey={MetaSpace.Orphans}
className="mx_SpaceButton_orphans"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={_t("Other rooms")}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.Orphans)}
/>;
};
const CreateSpaceButton = ({
isPanelCollapsed,
setPanelCollapsed,
@ -181,13 +226,25 @@ const CreateSpaceButton = ({
</li>;
};
const metaSpaceComponentMap: Record<MetaSpace, typeof HomeButton> = {
[MetaSpace.Home]: HomeButton,
[MetaSpace.Favourites]: FavouritesButton,
[MetaSpace.People]: PeopleButton,
[MetaSpace.Orphans]: OrphansButton,
};
// Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCollapsed, setPanelCollapsed }) => {
const [invites, spaces, activeSpace] = useSpaces();
const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces();
const activeSpaces = activeSpace ? [activeSpace] : [];
const metaSpacesSection = metaSpaces.map(key => {
const Component = metaSpaceComponentMap[key];
return <Component key={key} selected={activeSpace === key} isPanelCollapsed={isPanelCollapsed} />;
});
return <div className="mx_SpaceTreeLevel">
<HomeButton selected={!activeSpace} isPanelCollapsed={isPanelCollapsed} />
{ metaSpacesSection }
{ invites.map(s => (
<SpaceItem
key={s.roomId}
@ -197,7 +254,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
onExpand={() => setPanelCollapsed(false)}
/>
)) }
{ spaces.map((s, i) => (
{ actualSpaces.map((s, i) => (
<Draggable key={s.roomId} draggableId={s.roomId} index={i}>
{ (provided, snapshot) => (
<SpaceItem

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, {
createRef,
MouseEvent,
InputHTMLAttributes,
LegacyRef,
ComponentProps,
@ -26,14 +25,15 @@ import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { SpaceKey } from "../../../stores/spaces";
import SpaceTreeLevelLayoutStore from "../../../stores/spaces/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge";
import { _t } from "../../../languageHandler";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { toRightOf, useContextMenu } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "../elements/AccessibleButton";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
@ -43,8 +43,9 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
interface IButtonProps extends Omit<ComponentProps<typeof AccessibleTooltipButton>, "title"> {
interface IButtonProps extends Omit<ComponentProps<typeof AccessibleTooltipButton>, "title" | "onClick"> {
space?: Room;
spaceKey?: SpaceKey;
className?: string;
selected?: boolean;
label: string;
@ -53,14 +54,14 @@ interface IButtonProps extends Omit<ComponentProps<typeof AccessibleTooltipButto
isNarrow?: boolean;
avatarSize?: number;
ContextMenuComponent?: ComponentType<ComponentProps<typeof SpaceContextMenu>>;
onClick(ev: MouseEvent): void;
onClick?(ev?: ButtonEvent): void;
}
export const SpaceButton: React.FC<IButtonProps> = ({
space,
spaceKey,
className,
selected,
onClick,
label,
contextMenuTooltip,
notificationState,
@ -88,7 +89,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
notifBadge = <div className="mx_SpacePanel_badgeContainer">
<NotificationBadge
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space || null)}
onClick={() => SpaceStore.instance.setActiveRoomInSpace(spaceKey ?? space.roomId)}
forceCount={false}
notification={notificationState}
aria-label={ariaLabel}
@ -116,7 +117,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
mx_SpaceButton_narrow: isNarrow,
})}
title={label}
onClick={onClick}
onClick={spaceKey ? () => SpaceStore.instance.setActiveSpace(spaceKey) : props.onClick}
onContextMenu={openMenu}
forceHide={!isNarrow || menuDisplayed}
inputRef={handle}
@ -146,7 +147,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
space?: Room;
activeSpaces: Room[];
activeSpaces: SpaceKey[];
isNested?: boolean;
isPanelCollapsed?: boolean;
onExpand?: Function;
@ -258,7 +259,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
private onClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
SpaceStore.instance.setActiveSpace(this.props.space);
SpaceStore.instance.setActiveSpace(this.props.space.roomId);
};
render() {
@ -316,7 +317,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
{...restDragHandleProps}
space={space}
className={isInvite ? "mx_SpaceButton_invite" : undefined}
selected={activeSpaces.includes(space)}
selected={activeSpaces.includes(space.roomId)}
label={space.name}
contextMenuTooltip={_t("Space options")}
notificationState={notificationState}
@ -337,7 +338,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
interface ITreeLevelProps {
spaces: Room[];
activeSpaces: Room[];
activeSpaces: SpaceKey[];
isNested?: boolean;
parents: Set<string>;
}