Merge pull request #6594 from matrix-org/t3chguy/fix/18088

This commit is contained in:
Michael Telatynski 2021-09-16 10:16:33 +01:00 committed by GitHub
commit bbc8e4da23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 355 additions and 227 deletions

View file

@ -73,12 +73,6 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
.mx_GroupFilterPanel .mx_TagTile { .mx_GroupFilterPanel .mx_TagTile {
// opacity: 0.5; // opacity: 0.5;
position: relative; position: relative;
.mx_BetaDot {
position: absolute;
right: -13px;
top: -11px;
}
} }
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype { .mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {

View file

@ -103,6 +103,16 @@ $activeBorderColor: $secondary-content;
} }
} }
.mx_SpaceItem_new {
position: relative;
.mx_BetaDot {
position: absolute;
left: 33px;
top: -5px;
}
}
.mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton { .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
margin-left: $gutterSize; margin-left: $gutterSize;
min-width: 40px; min-width: 40px;
@ -194,23 +204,18 @@ $activeBorderColor: $secondary-content;
} }
&.mx_SpaceButton_new .mx_SpaceButton_icon { &.mx_SpaceButton_new .mx_SpaceButton_icon {
background-color: $accent-color; background-color: $roomlist-button-bg-color;
transition: all .1s ease-in-out; // TODO transition
&::before { &::before {
background-color: #ffffff; background-color: $primary-content;
mask-image: url('$(res)/img/element-icons/plus.svg'); mask-image: url('$(res)/img/element-icons/plus.svg');
transition: all .2s ease-in-out; // TODO transition transition: all .2s ease-in-out; // TODO transition
} }
} }
&.mx_SpaceButton_newCancel .mx_SpaceButton_icon { &.mx_SpaceButton_newCancel .mx_SpaceButton_icon::before {
background-color: $icon-button-color;
&::before {
transform: rotate(45deg); transform: rotate(45deg);
} }
}
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
border-radius: 8px; border-radius: 8px;

View file

@ -215,10 +215,11 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
> .mx_BaseAvatar_image, > .mx_RoomAvatar_isSpaceRoom {
> .mx_BaseAvatar > .mx_BaseAvatar_image { &.mx_BaseAvatar_image, .mx_BaseAvatar_image {
border-radius: 12px; border-radius: 12px;
} }
}
h1.mx_SpaceRoomView_preview_name { h1.mx_SpaceRoomView_preview_name {
margin: 20px 0 !important; // override default margin from above margin: 20px 0 !important; // override default margin from above

View file

@ -113,6 +113,7 @@ $dot-size: 12px;
animation: mx_Beta_bluePulse 2s infinite; animation: mx_Beta_bluePulse 2s infinite;
animation-iteration-count: 20; animation-iteration-count: 20;
position: relative; position: relative;
pointer-events: none;
&::after { &::after {
content: ""; content: "";

View file

@ -21,6 +21,17 @@ limitations under the License.
.mx_SettingsTab_section { .mx_SettingsTab_section {
margin-bottom: 30px; margin-bottom: 30px;
> details {
> summary {
cursor: pointer;
color: $primary-content;
}
& + .mx_SettingsFlag {
margin-top: 20px;
}
}
} }
.mx_PreferencesUserSettingsTab_CommunityMigrator { .mx_PreferencesUserSettingsTab_CommunityMigrator {

0
res/img/betas/.gitkeep Normal file
View file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

View file

@ -146,19 +146,13 @@ class GroupFilterPanel extends React.Component<IGroupFilterPanelProps, IGroupFil
mx_GroupFilterPanel_items_selected: itemsSelected, mx_GroupFilterPanel_items_selected: itemsSelected,
}); });
let betaDot;
if (SettingsStore.getBetaInfo("feature_spaces") && !localStorage.getItem("mx_seenSpacesBeta")) {
betaDot = <div className="mx_BetaDot" />;
}
let createButton = ( let createButton = (
<ActionButton <ActionButton
tooltip tooltip
label={_t("Communities")} label={_t("Communities")}
action="toggle_my_groups" action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus"> className="mx_TagTile mx_TagTile_plus"
{ betaDot } />
</ActionButton>
); );
if (SettingsStore.getValue("feature_communities_v2_prototypes")) { if (SettingsStore.getValue("feature_communities_v2_prototypes")) {

View file

@ -0,0 +1,116 @@
/*
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, { useContext } from "react";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { _t } from "../../languageHandler";
import AccessibleButton from "../views/elements/AccessibleButton";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import { IGroupSummary } from "../views/dialogs/CreateSpaceFromCommunityDialog";
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
import Spinner from "../views/elements/Spinner";
import GroupAvatar from "../views/avatars/GroupAvatar";
import { linkifyElement } from "../../HtmlUtils";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import MainSplit from './MainSplit';
interface IProps {
groupId: string;
}
const onSwapClick = () => {
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Preferences,
});
};
// XXX: temporary community migration component, reuses SpaceRoomView & SpacePreview classes for simplicity
const LegacyCommunityPreview = ({ groupId }: IProps) => {
const cli = useContext(MatrixClientContext);
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [cli, groupId]);
if (!groupSummary) {
return <main className="mx_SpaceRoomView">
<MainSplit>
<div className="mx_SpaceRoomView_preview">
<Spinner />
</div>
</MainSplit>
</main>;
}
let visibilitySection: JSX.Element;
if (groupSummary.profile.is_public) {
visibilitySection = <span className="mx_SpaceRoomView_info_public">
{ _t("Public community") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_info_private">
{ _t("Private community") }
</span>;
}
return <main className="mx_SpaceRoomView">
<ErrorBoundary>
<MainSplit>
<div className="mx_SpaceRoomView_preview">
<GroupAvatar
groupId={groupId}
groupName={groupSummary.profile.name}
groupAvatarUrl={groupSummary.profile.avatar_url}
height={80}
width={80}
resizeMethod='crop'
/>
<h1 className="mx_SpaceRoomView_preview_name">
{ groupSummary.profile.name }
</h1>
<div className="mx_SpaceRoomView_info">
{ visibilitySection }
</div>
<div className="mx_SpaceRoomView_preview_topic" ref={e => e && linkifyElement(e)}>
{ groupSummary.profile.short_description }
</div>
<div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
{ groupSummary.user?.membership === "join"
? _t("To view %(communityName)s, swap to communities in your <a>preferences</a>", {
communityName: groupSummary.profile.name,
}, {
a: sub => (
<AccessibleButton onClick={onSwapClick} kind="link">{ sub }</AccessibleButton>
),
})
: _t("To join %(communityName)s, swap to communities in your <a>preferences</a>", {
communityName: groupSummary.profile.name,
}, {
a: sub => (
<AccessibleButton onClick={onSwapClick} kind="link">{ sub }</AccessibleButton>
),
})
}
</div>
</div>
</MainSplit>
</ErrorBoundary>
</main>;
};
export default LegacyCommunityPreview;

View file

@ -69,6 +69,7 @@ import classNames from 'classnames';
import GroupFilterPanel from './GroupFilterPanel'; import GroupFilterPanel from './GroupFilterPanel';
import CustomRoomTagPanel from './CustomRoomTagPanel'; import CustomRoomTagPanel from './CustomRoomTagPanel';
import { mediaFromMxc } from "../../customisations/Media"; import { mediaFromMxc } from "../../customisations/Media";
import LegacyCommunityPreview from "./LegacyCommunityPreview";
// We need to fetch each pinned message individually (if we don't already have it) // We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity. // so each pinned message may trigger a request. Limit the number per room for sanity.
@ -629,11 +630,15 @@ class LoggedInView extends React.Component<IProps, IState> {
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />; pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
break; break;
case PageTypes.GroupView: case PageTypes.GroupView:
if (SpaceStore.spacesEnabled) {
pageElement = <LegacyCommunityPreview groupId={this.props.currentGroupId} />;
} else {
pageElement = <GroupView pageElement = <GroupView
groupId={this.props.currentGroupId} groupId={this.props.currentGroupId}
isNew={this.props.currentGroupIsNew} isNew={this.props.currentGroupIsNew}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
/>; />;
}
break; break;
} }

View file

@ -1800,11 +1800,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
subAction: params.action, subAction: params.action,
}); });
} else if (screen.indexOf('group/') === 0) { } else if (screen.indexOf('group/') === 0) {
if (SpaceStore.spacesEnabled) {
dis.dispatch({ action: "view_home_page" });
return;
}
const groupId = screen.substring(6); const groupId = screen.substring(6);
// TODO: Check valid group ID // TODO: Check valid group ID

View file

@ -25,7 +25,6 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import BetaCard from "../views/beta/BetaCard";
@replaceableComponent("structures.MyGroups") @replaceableComponent("structures.MyGroups")
export default class MyGroups extends React.Component { export default class MyGroups extends React.Component {
@ -138,7 +137,6 @@ export default class MyGroups extends React.Component {
</div> </div>
</div>*/ } </div>*/ }
</div> </div>
<BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
<div className="mx_MyGroups_content"> <div className="mx_MyGroups_content">
{ contentHeader } { contentHeader }
{ content } { content }

View file

@ -156,10 +156,10 @@ const SpaceInfo = ({ space }) => {
</div>; </div>;
}; };
const onBetaClick = () => { const onPreferencesClick = () => {
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: Action.ViewUserSettings, action: Action.ViewUserSettings,
initialTabId: UserTab.Labs, initialTabId: UserTab.Preferences,
}); });
}; };
@ -286,15 +286,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
if (!spacesEnabled) { if (!spacesEnabled) {
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt"> footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
{ myMembership === "join" { myMembership === "join"
? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", { ? _t("To view this Space, hide communities in your <a>preferences</a>", {}, {
spaceName: space.name, a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
}, {
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
}) })
: _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", { : _t("To join this Space, hide communities in your <a>preferences</a>", {}, {
spaceName: space.name, a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
}, {
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
}) })
} }
</div>; </div>;
@ -731,7 +727,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
</div> </div>
<div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer"> <div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
<BetaPill onClick={onBetaClick} /> <BetaPill />
{ _t("<b>This is an experimental feature.</b> For now, " + { _t("<b>This is an experimental feature.</b> For now, " +
"new users receiving an invite will have to open the invite on <link/> to actually join.", {}, { "new users receiving an invite will have to open the invite on <link/> to actually join.", {}, {
b: sub => <b>{ sub }</b>, b: sub => <b>{ sub }</b>,

View file

@ -28,7 +28,6 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
import SettingsFlag from '../../../elements/SettingsFlag'; import SettingsFlag from '../../../elements/SettingsFlag';
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import SpaceStore from "../../../../../stores/SpaceStore";
import GroupAvatar from "../../../avatars/GroupAvatar"; import GroupAvatar from "../../../avatars/GroupAvatar";
import dis from "../../../../../dispatcher/dispatcher"; import dis from "../../../../../dispatcher/dispatcher";
import GroupActions from "../../../../../actions/GroupActions"; import GroupActions from "../../../../../actions/GroupActions";
@ -145,7 +144,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
]; ];
static COMMUNITIES_SETTINGS = [ static COMMUNITIES_SETTINGS = [
// TODO: part of delabsing move the toggle here - https://github.com/vector-im/element-web/issues/18088 "showCommunitiesInsteadOfSpaces",
]; ];
static KEYBINDINGS_SETTINGS = [ static KEYBINDINGS_SETTINGS = [
@ -286,9 +285,17 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
}; };
private renderGroup(settingIds: string[]): React.ReactNodeArray { private renderGroup(
return settingIds.filter(SettingsStore.isEnabled).map(i => { settingIds: string[],
return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />; level = SettingLevel.ACCOUNT,
includeDisabled = false,
): React.ReactNodeArray {
if (!includeDisabled) {
settingIds = settingIds.filter(SettingsStore.isEnabled);
}
return settingIds.map(i => {
return <SettingsFlag key={i} name={i} level={level} />;
}); });
} }
@ -334,10 +341,10 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) } { this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
</div> </div>
{ SpaceStore.spacesEnabled && <div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span> <span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) } { this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT, true) }
</div> } </div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Communities") }</span> <span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
@ -349,7 +356,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
<p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p> <p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
<CommunityMigrator onFinished={this.props.closeSettingsFn} /> <CommunityMigrator onFinished={this.props.closeSettingsFn} />
</details> </details>
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS) } { this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS, SettingLevel.DEVICE) }
</div> </div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">

View file

@ -117,9 +117,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
"Your feedback will help inform the next versions."), "Your feedback will help inform the next versions."),
rageshakeLabel: "spaces-feedback", rageshakeLabel: "spaces-feedback",
rageshakeData: Object.fromEntries([ rageshakeData: Object.fromEntries([
"feature_spaces.all_rooms", "Spaces.allRoomsInHome",
"feature_spaces.space_member_dms",
"feature_spaces.space_dm_badges",
].map(k => [k, SettingsStore.getValue(k)])), ].map(k => [k, SettingsStore.getValue(k)])),
}); });
}} }}
@ -301,13 +299,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
/> />
<p> <p>
{ _t("You can also create a Space from a <a>community</a>.", {}, { { _t("You can also make Spaces from <a>communities</a>.", {}, {
a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}> a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
{ sub } { sub }
</AccessibleButton>, </AccessibleButton>,
}) } }) }
<br /> <br />
{ _t("To join an existing space you'll need an invite.") } { _t("To join a space you'll need an invite.") }
</p> </p>
<SpaceFeedbackPrompt onClick={onFinished} /> <SpaceFeedbackPrompt onClick={onFinished} />

View file

@ -151,12 +151,19 @@ const CreateSpaceButton = ({
} }
const onNewClick = menuDisplayed ? closeMenu : () => { const onNewClick = menuDisplayed ? closeMenu : () => {
// persist that the user has interacted with this, use it to dismiss the beta dot
localStorage.setItem("mx_seenSpaces", "1");
if (!isPanelCollapsed) setPanelCollapsed(true); if (!isPanelCollapsed) setPanelCollapsed(true);
openMenu(); openMenu();
}; };
let betaDot: JSX.Element;
if (!localStorage.getItem("mx_seenSpaces") && !SpaceStore.instance.spacePanelSpaces.length) {
betaDot = <div className="mx_BetaDot" />;
}
return <li return <li
className={classNames("mx_SpaceItem", { className={classNames("mx_SpaceItem mx_SpaceItem_new", {
"collapsed": isPanelCollapsed, "collapsed": isPanelCollapsed,
})} })}
role="treeitem" role="treeitem"
@ -169,6 +176,7 @@ const CreateSpaceButton = ({
onClick={onNewClick} onClick={onNewClick}
isNarrow={isPanelCollapsed} isNarrow={isPanelCollapsed}
/> />
{ betaDot }
{ contextMenu } { contextMenu }
</li>; </li>;

View file

@ -799,15 +799,6 @@
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"Change notification settings": "Change notification settings", "Change notification settings": "Change notification settings",
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators", "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
"Spaces": "Spaces",
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.",
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta available for web, desktop and Android. Thank you for trying the beta.",
"%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.",
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Render LaTeX maths in messages": "Render LaTeX maths in messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.", "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
@ -883,6 +874,8 @@
"Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)", "Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)",
"Show all rooms in Home": "Show all rooms in Home", "Show all rooms in Home": "Show all rooms in Home",
"All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.", "All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.",
"Display Communities instead of Spaces": "Display Communities instead of Spaces",
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.",
"Collecting app version information": "Collecting app version information", "Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs", "Collecting logs": "Collecting logs",
"Uploading logs": "Uploading logs", "Uploading logs": "Uploading logs",
@ -1033,14 +1026,15 @@
"e.g. my-space": "e.g. my-space", "e.g. my-space": "e.g. my-space",
"Address": "Address", "Address": "Address",
"Create a space": "Create a space", "Create a space": "Create a space",
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
"What kind of Space do you want to create?": "What kind of Space do you want to create?", "What kind of Space do you want to create?": "What kind of Space do you want to create?",
"You can change this later.": "You can change this later.", "You can change this later.": "You can change this later.",
"Public": "Public", "Public": "Public",
"Open space for anyone, best for communities": "Open space for anyone, best for communities", "Open space for anyone, best for communities": "Open space for anyone, best for communities",
"Private": "Private", "Private": "Private",
"Invite only, best for yourself or teams": "Invite only, best for yourself or teams", "Invite only, best for yourself or teams": "Invite only, best for yourself or teams",
"You can also create a Space from a <a>community</a>.": "You can also create a Space from a <a>community</a>.", "You can also make Spaces from <a>communities</a>.": "You can also make Spaces from <a>communities</a>.",
"To join an existing space you'll need an invite.": "To join an existing space you'll need an invite.", "To join a space you'll need an invite.": "To join a space you'll need an invite.",
"Go back": "Go back", "Go back": "Go back",
"Your public space": "Your public space", "Your public space": "Your public space",
"Your private space": "Your private space", "Your private space": "Your private space",
@ -1052,6 +1046,7 @@
"Show all rooms": "Show all rooms", "Show all rooms": "Show all rooms",
"All rooms": "All rooms", "All rooms": "All rooms",
"Options": "Options", "Options": "Options",
"Spaces": "Spaces",
"Expand space panel": "Expand space panel", "Expand space panel": "Expand space panel",
"Collapse space panel": "Collapse space panel", "Collapse space panel": "Collapse space panel",
"Click to copy": "Click to copy", "Click to copy": "Click to copy",
@ -2788,6 +2783,10 @@
"Create a Group Chat": "Create a Group Chat", "Create a Group Chat": "Create a Group Chat",
"Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s", "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s",
"Open dial pad": "Open dial pad", "Open dial pad": "Open dial pad",
"Public community": "Public community",
"Private community": "Private community",
"To view %(communityName)s, swap to communities in your <a>preferences</a>": "To view %(communityName)s, swap to communities in your <a>preferences</a>",
"To join %(communityName)s, swap to communities in your <a>preferences</a>": "To join %(communityName)s, swap to communities in your <a>preferences</a>",
"Failed to reject invitation": "Failed to reject invitation", "Failed to reject invitation": "Failed to reject invitation",
"Cannot create rooms in this community": "Cannot create rooms in this community", "Cannot create rooms in this community": "Cannot create rooms in this community",
"You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.", "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.",
@ -2818,7 +2817,6 @@
"Error whilst fetching joined communities": "Error whilst fetching joined communities", "Error whilst fetching joined communities": "Error whilst fetching joined communities",
"Create a new community": "Create a new community", "Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
"Communities are changing to Spaces": "Communities are changing to Spaces",
"Youre all caught up": "Youre all caught up", "Youre all caught up": "Youre all caught up",
"You have no visible notifications.": "You have no visible notifications.", "You have no visible notifications.": "You have no visible notifications.",
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
@ -2885,8 +2883,8 @@
"Search names and descriptions": "Search names and descriptions", "Search names and descriptions": "Search names and descriptions",
"Private space": "Private space", "Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you", "<inviter/> invites you": "<inviter/> invites you",
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>", "To view this Space, hide communities in your <a>preferences</a>": "To view this Space, hide communities in your <a>preferences</a>",
"To join %(spaceName)s, turn on the <a>Spaces beta</a>": "To join %(spaceName)s, turn on the <a>Spaces beta</a>", "To join this Space, hide communities in your <a>preferences</a>": "To join this Space, hide communities in your <a>preferences</a>",
"To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite", "To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite",
"Created from <Community />": "Created from <Community />", "Created from <Community />": "Created from <Community />",
"Welcome to <name/>": "Welcome to <name/>", "Welcome to <name/>": "Welcome to <name/>",

View file

@ -16,9 +16,9 @@ limitations under the License.
*/ */
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import React, { ReactNode } from "react"; import { ReactNode } from "react";
import { _t, _td } from '../languageHandler'; import { _td } from '../languageHandler';
import { import {
NotificationBodyEnabledController, NotificationBodyEnabledController,
NotificationsEnabledController, NotificationsEnabledController,
@ -40,7 +40,6 @@ import { OrderedMultiController } from "./controllers/OrderedMultiController";
import { Layout } from "./Layout"; import { Layout } from "./Layout";
import ReducedMotionController from './controllers/ReducedMotionController'; import ReducedMotionController from './controllers/ReducedMotionController';
import IncompatibleController from "./controllers/IncompatibleController"; import IncompatibleController from "./controllers/IncompatibleController";
import SdkConfig from "../SdkConfig";
import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController'; import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController';
import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController'; import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController';
@ -145,44 +144,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: false,
}, },
"feature_spaces": {
isFeature: true,
displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. " +
"Requires compatible homeserver for some features."),
supportedLevels: LEVELS_FEATURE,
default: false,
controller: new ReloadOnChangeController(),
betaInfo: {
title: _td("Spaces"),
caption: _td("Spaces are a new way to group rooms and people."),
disclaimer: (enabled) => {
if (enabled) {
return <>
<p>{ _t("If you leave, %(brand)s will reload with Spaces disabled. " +
"Communities and custom tags will be visible again.", {
brand: SdkConfig.get().brand,
}) }</p>
<p>{ _t("Beta available for web, desktop and Android. Thank you for trying the beta.") }</p>
</>;
}
return <>
<p>{ _t("%(brand)s will reload with Spaces enabled. " +
"Communities and custom tags will be hidden.", {
brand: SdkConfig.get().brand,
}) }</p>
<b>{ _t("You can leave the beta any time from settings or tapping on a beta badge, " +
"like the one above.") }</b>
<p>{ _t("Beta available for web, desktop and Android. " +
"Some features may be unavailable on your homeserver.") }</p>
</>;
},
image: require("../../res/img/betas/spaces.png"),
feedbackSubheading: _td("Your feedback will help make spaces better. " +
"The more detail you can go into, the better."),
feedbackLabel: "spaces-feedback",
},
},
"feature_dnd": { "feature_dnd": {
isFeature: true, isFeature: true,
displayName: _td("Show options to enable 'Do not disturb' mode"), displayName: _td("Show options to enable 'Do not disturb' mode"),
@ -203,7 +164,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
), ),
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: false,
controller: new IncompatibleController("feature_spaces"), controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
}, },
"feature_pinning": { "feature_pinning": {
isFeature: true, isFeature: true,
@ -232,7 +193,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"), displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"),
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: false,
controller: new IncompatibleController("feature_spaces"), controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
}, },
"feature_state_counters": { "feature_state_counters": {
isFeature: true, isFeature: true,
@ -780,6 +741,15 @@ export const SETTINGS: {[setting: string]: ISetting} = {
description: _td("All rooms you're in will appear in Home."), description: _td("All rooms you're in will appear in Home."),
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: false, default: false,
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", null),
},
"showCommunitiesInsteadOfSpaces": {
displayName: _td("Display Communities instead of Spaces"),
description: _td("Temporarily show communities instead of Spaces for this session. " +
"Support for this will be removed in the near future. This will reload Element."),
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: false,
controller: new ReloadOnChangeController(),
}, },
[UIFeature.RoomHistorySettings]: { [UIFeature.RoomHistorySettings]: {
supportedLevels: LEVELS_UI_FEATURE, supportedLevels: LEVELS_UI_FEATURE,
@ -844,7 +814,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
[UIFeature.Communities]: { [UIFeature.Communities]: {
supportedLevels: LEVELS_UI_FEATURE, supportedLevels: LEVELS_UI_FEATURE,
default: true, default: true,
controller: new IncompatibleController("feature_spaces"), controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
}, },
[UIFeature.AdvancedSettings]: { [UIFeature.AdvancedSettings]: {
supportedLevels: LEVELS_UI_FEATURE, supportedLevels: LEVELS_UI_FEATURE,

View file

@ -467,6 +467,10 @@ export default class SettingsStore {
throw new Error("Setting '" + settingName + "' does not appear to be a setting."); throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
} }
if (!SettingsStore.isEnabled(settingName)) {
return false;
}
// When non-beta features are specified in the config.json, we force them as enabled or disabled. // When non-beta features are specified in the config.json, we force them as enabled or disabled.
if (SettingsStore.isFeature(settingName) && !SETTINGS[settingName]?.betaInfo) { if (SettingsStore.isFeature(settingName) && !SETTINGS[settingName]?.betaInfo) {
const configVal = SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, roomId, true, true); const configVal = SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, roomId, true, true);

View file

@ -24,7 +24,11 @@ import SettingsStore from "../SettingsStore";
* labs flags. * labs flags.
*/ */
export default class IncompatibleController extends SettingController { export default class IncompatibleController extends SettingController {
public constructor(private settingName: string, private forcedValue = false) { public constructor(
private settingName: string,
private forcedValue: any = false,
private incompatibleValue: any = true,
) {
super(); super();
} }
@ -34,13 +38,17 @@ export default class IncompatibleController extends SettingController {
calculatedValue: any, calculatedValue: any,
calculatedAtLevel: SettingLevel, calculatedAtLevel: SettingLevel,
): any { ): any {
if (this.incompatibleSettingEnabled) { if (this.incompatibleSetting) {
return this.forcedValue; return this.forcedValue;
} }
return null; // no override return null; // no override
} }
public get incompatibleSettingEnabled(): boolean { public get settingDisabled(): boolean {
return SettingsStore.getValue(this.settingName); return this.incompatibleSetting;
}
public get incompatibleSetting(): boolean {
return SettingsStore.getValue(this.settingName) === this.incompatibleValue;
} }
} }

View file

@ -71,7 +71,7 @@ export interface ISuggestedRoom extends IHierarchyRoom {
const MAX_SUGGESTED_ROOMS = 20; const MAX_SUGGESTED_ROOMS = 20;
// This setting causes the page to reload and can be costly if read frequently, so read it here only // This setting causes the page to reload and can be costly if read frequently, so read it here only
const spacesEnabled = SettingsStore.getValue("feature_spaces"); const spacesEnabled = !SettingsStore.getValue("showCommunitiesInsteadOfSpaces");
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`; const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`;

View file

@ -9,23 +9,16 @@ import sdk from '../../../skinned-sdk';
import dis from '../../../../src/dispatcher/dispatcher'; import dis from '../../../../src/dispatcher/dispatcher';
import DMRoomMap from '../../../../src/utils/DMRoomMap'; import DMRoomMap from '../../../../src/utils/DMRoomMap';
import GroupStore from '../../../../src/stores/GroupStore';
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
import { DefaultTagID } from "../../../../src/stores/room-list/models"; import { DefaultTagID } from "../../../../src/stores/room-list/models";
import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore"; import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore"; import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
function generateRoomId() { function generateRoomId() {
return '!' + Math.random().toString().slice(2, 10) + ':domain'; return '!' + Math.random().toString().slice(2, 10) + ':domain';
} }
function waitForRoomListStoreUpdate() {
return new Promise((resolve) => {
RoomListStore.instance.once(LISTS_UPDATE_EVENT, () => resolve());
});
}
describe('RoomList', () => { describe('RoomList', () => {
function createRoom(opts) { function createRoom(opts) {
const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), { const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
@ -239,73 +232,6 @@ describe('RoomList', () => {
}); });
} }
describe('when no tags are selected', () => {
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles(); itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
});
describe('when tags are selected', () => {
function setupSelectedTag() {
// Simulate a complete sync BEFORE dispatching anything else
dis.dispatch({
action: 'MatrixActions.sync',
prevState: null,
state: 'PREPARED',
matrixClient: client,
}, true);
// Simulate joined groups being received
dis.dispatch({
action: 'GroupActions.fetchJoinedGroups.success',
result: {
groups: ['+group:domain'],
},
}, true);
// Simulate receiving tag ordering account data
dis.dispatch({
action: 'MatrixActions.accountData',
event_type: 'im.vector.web.tag_ordering',
event_content: {
tags: ['+group:domain'],
},
}, true);
// GroupStore is not flux, mock and notify
GroupStore.getGroupRooms = (groupId) => {
return [movingRoom];
};
GroupStore._notifyListeners();
// We also have to mock the client's getGroup function for the room list to filter it.
// It's not smart enough to tell the difference between a real group and a template though.
client.getGroup = (groupId) => {
return { groupId };
};
// Select tag
dis.dispatch({ action: 'select_tag', tag: '+group:domain' }, true);
}
beforeEach(() => {
setupSelectedTag();
});
it('displays the correct rooms when the groups rooms are changed', async () => {
GroupStore.getGroupRooms = (groupId) => {
return [movingRoom, otherRoom];
};
GroupStore._notifyListeners();
await waitForRoomListStoreUpdate();
// XXX: Even though the store updated, it can take a bit before the update makes
// it to the components. This gives it plenty of time to figure out what to do.
await (new Promise(resolve => setTimeout(resolve, 500)));
expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged);
});
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
});
}); });

View file

@ -43,6 +43,9 @@ module.exports = async function scenario(createSession, restCreator) {
console.log("create REST users:"); console.log("create REST users:");
const charlies = await createRestUsers(restCreator); const charlies = await createRestUsers(restCreator);
await lazyLoadingScenarios(alice, bob, charlies); await lazyLoadingScenarios(alice, bob, charlies);
// do spaces scenarios last as the rest of the tests may get confused by spaces
// XXX: disabled for now as fails in CI but succeeds locally
// await spacesScenarios(alice, bob);
}; };
async function createRestUsers(restCreator) { async function createRestUsers(restCreator) {

View file

@ -0,0 +1,32 @@
/*
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.
*/
const { createSpace, inviteSpace } = require("../usecases/create-space");
module.exports = async function spacesScenarios(alice, bob) {
console.log(" creating a space for spaces scenarios:");
await alice.delay(1000); // wait for dialogs to close
await setupSpaceUsingAliceAndInviteBob(alice, bob);
};
const space = "Test Space";
async function setupSpaceUsingAliceAndInviteBob(alice, bob) {
await createSpace(alice, space);
await inviteSpace(alice, space, "@bob:localhost");
await bob.query(`.mx_SpaceButton[aria-label="${space}"]`); // assert invite received
}

View file

@ -0,0 +1,80 @@
/*
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.
*/
async function openSpaceCreateMenu(session) {
const spaceCreateButton = await session.query('.mx_SpaceButton_new');
await spaceCreateButton.click();
}
async function createSpace(session, name, isPublic = false) {
session.log.step(`creates space "${name}"`);
await openSpaceCreateMenu(session);
const className = isPublic ? ".mx_SpaceCreateMenuType_public" : ".mx_SpaceCreateMenuType_private";
const visibilityButton = await session.query(className);
await visibilityButton.click();
const nameInput = await session.query('input[name="spaceName"]');
await session.replaceInputText(nameInput, name);
await session.delay(100);
const createButton = await session.query('.mx_SpaceCreateMenu_wrapper .mx_AccessibleButton_kind_primary');
await createButton.click();
if (!isPublic) {
const justMeButton = await session.query('.mx_SpaceRoomView_privateScope_justMeButton');
await justMeButton.click();
const continueButton = await session.query('.mx_AddExistingToSpace_footer .mx_AccessibleButton_kind_primary');
await continueButton.click();
} else {
for (let i = 0; i < 2; i++) {
const continueButton = await session.query('.mx_SpaceRoomView_buttons .mx_AccessibleButton_kind_primary');
await continueButton.click();
}
}
session.log.done();
}
async function inviteSpace(session, spaceName, userId) {
session.log.step(`invites "${userId}" to space "${spaceName}"`);
const spaceButton = await session.query(`.mx_SpaceButton[aria-label="${spaceName}"]`);
await spaceButton.click({
button: 'right',
});
const inviteButton = await session.query('[aria-label="Invite people"]');
await inviteButton.click();
try {
const button = await session.query('.mx_SpacePublicShare_inviteButton');
await button.click();
} catch (e) {
// ignore
}
const inviteTextArea = await session.query(".mx_InviteDialog_editor input");
await inviteTextArea.type(userId);
const selectUserItem = await session.query(".mx_InviteDialog_roomTile");
await selectUserItem.click();
const confirmButton = await session.query(".mx_InviteDialog_goButton");
await confirmButton.click();
session.log.done();
}
module.exports = { openSpaceCreateMenu, createSpace, inviteSpace };

View file

@ -161,8 +161,8 @@ async function changeRoomSettings(session, settings) {
if (settings.visibility) { if (settings.visibility) {
session.log.step(`sets visibility to ${settings.visibility}`); session.log.step(`sets visibility to ${settings.visibility}`);
const radios = await session.queryAll(".mx_RoomSettingsDialog label"); const radios = await session.queryAll(".mx_RoomSettingsDialog label");
assert.equal(radios.length, 6); assert.equal(radios.length, 7);
const [inviteOnlyRoom, publicRoom] = radios; const [inviteOnlyRoom,, publicRoom] = radios;
if (settings.visibility === "invite_only") { if (settings.visibility === "invite_only") {
await inviteOnlyRoom.click(); await inviteOnlyRoom.click();

View file

@ -1,20 +0,0 @@
/*
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.
*/
// This needs to be executed before the SpaceStore gets imported but due to ES6 import hoisting we have to do this here.
// SpaceStore reads the SettingsStore which needs the localStorage values set at init time.
localStorage.setItem("mx_labs_feature_feature_spaces", "true");

View file

@ -18,7 +18,6 @@ import { EventEmitter } from "events";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import "./SpaceStore-setup"; // enable space lab
import "../skinned-sdk"; // Must be first for skinning to work import "../skinned-sdk"; // Must be first for skinning to work
import SpaceStore, { import SpaceStore, {
UPDATE_HOME_BEHAVIOUR, UPDATE_HOME_BEHAVIOUR,

View file

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import "../SpaceStore-setup"; // enable space lab
import "../../skinned-sdk"; // Must be first for skinning to work import "../../skinned-sdk"; // Must be first for skinning to work
import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher"; import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher";
import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore"; import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";