Merge branch 'develop' into show-room-name

This commit is contained in:
Šimon Brandner 2021-02-06 15:27:04 +01:00
commit 58ab5c36bb
No known key found for this signature in database
GPG Key ID: 9760693FDD98A790
20 changed files with 105 additions and 54 deletions

View File

@ -1,3 +1,10 @@
Changes in [3.13.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.1) (2021-02-04)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0...v3.13.1)
* [Release] Fix z-index of stickerpicker
[\#5618](https://github.com/matrix-org/matrix-react-sdk/pull/5618)
Changes in [3.13.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0) (2021-02-03) Changes in [3.13.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0) (2021-02-03)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0-rc.1...v3.13.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0-rc.1...v3.13.0)

View File

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.13.0", "version": "3.13.1",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {

View File

@ -21,6 +21,11 @@ limitations under the License.
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic $hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
$EventTile_e2e_state_indicator_width: 4px;
$MessageTimestamp_width: 46px; /* 8 + 30 (avatar) + 8 */
$MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e_state_indicator_width);
:root { :root {
font-size: 10px; font-size: 10px;
} }

View File

@ -30,7 +30,7 @@ limitations under the License.
mask-size: contain; mask-size: contain;
content: ''; content: '';
position: absolute; position: absolute;
top: 2px; top: 1px;
left: 0; left: 0;
} }
} }

View File

@ -26,7 +26,7 @@ $left-gutter: 64px;
} }
.mx_EventTile.mx_EventTile_info { .mx_EventTile.mx_EventTile_info {
padding-top: 0px; padding-top: 1px;
} }
.mx_EventTile_avatar { .mx_EventTile_avatar {
@ -37,7 +37,7 @@ $left-gutter: 64px;
} }
.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {
top: $font-8px; top: $font-6px;
left: $left-gutter; left: $left-gutter;
} }
@ -420,15 +420,15 @@ $left-gutter: 64px;
} }
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
border-left: $e2e-verified-color 4px solid; border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
} }
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line {
border-left: $e2e-unverified-color 4px solid; border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
} }
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
border-left: $e2e-unknown-color 4px solid; border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
} }
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
@ -446,8 +446,7 @@ $left-gutter: 64px;
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
left: 3px; width: $MessageTimestamp_width_hover;
width: auto;
} }
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)

View File

@ -34,7 +34,7 @@ $left-gutter: 64px;
.mx_MessageTimestamp { .mx_MessageTimestamp {
position: absolute; position: absolute;
width: 46px; /* 8 + 30 (avatar) + 8 */ width: $MessageTimestamp_width;
} }
.mx_EventTile_line, .mx_EventTile_reply { .mx_EventTile_line, .mx_EventTile_reply {

View File

@ -755,6 +755,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break; break;
case 'on_logged_in': case 'on_logged_in':
if ( if (
// Skip this handling for token login as that always calls onLoggedIn itself
!this.tokenLogin &&
!Lifecycle.isSoftLogout() && !Lifecycle.isSoftLogout() &&
this.state.view !== Views.LOGIN && this.state.view !== Views.LOGIN &&
this.state.view !== Views.REGISTER && this.state.view !== Views.REGISTER &&
@ -1652,10 +1654,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149 // TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
let threepidInvite: IThreepidInvite; let threepidInvite: IThreepidInvite;
// if we landed here from a 3PID invite, persist it
if (params.signurl && params.email) { if (params.signurl && params.email) {
threepidInvite = ThreepidInviteStore.instance threepidInvite = ThreepidInviteStore.instance
.storeInvite(roomString, params as IThreepidInviteWireFormat); .storeInvite(roomString, params as IThreepidInviteWireFormat);
} }
// otherwise check that this room doesn't already have a known invite
if (!threepidInvite) {
const invites = ThreepidInviteStore.instance.getInvites();
threepidInvite = invites.find(invite => invite.roomId === roomString);
}
// on our URLs there might be a ?via=matrix.org or similar to help // on our URLs there might be a ?via=matrix.org or similar to help
// joins to the room succeed. We'll pass these through as an array // joins to the room succeed. We'll pass these through as an array

View File

@ -229,7 +229,7 @@ export default class MessagePanel extends React.Component {
onAction = (payload) => { onAction = (payload) => {
switch (payload.action) { switch (payload.action) {
case "message_sent": case "scroll_to_bottom":
this.scrollToBottom(); this.scrollToBottom();
break; break;
} }

View File

@ -30,7 +30,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions"; import {Action} from "../../dispatcher/actions";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard"; import WidgetCard from "../views/right_panel/WidgetCard";
import defaultDispatcher from "../../dispatcher/dispatcher";
export default class RightPanel extends React.Component { export default class RightPanel extends React.Component {
static get propTypes() { static get propTypes() {
@ -186,7 +185,7 @@ export default class RightPanel extends React.Component {
} }
} }
onCloseUserInfo = () => { onClose = () => {
// XXX: There are three different ways of 'closing' this panel depending on what state // XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest // things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly. // of the app and is generally a bit silly.
@ -198,29 +197,19 @@ export default class RightPanel extends React.Component {
dis.dispatch({ dis.dispatch({
action: "view_home_page", action: "view_home_page",
}); });
} else if (this.state.phase === RightPanelPhases.EncryptionPanel && } else if (
this.state.phase === RightPanelPhases.EncryptionPanel &&
this.state.verificationRequest && this.state.verificationRequest.pending this.state.verificationRequest && this.state.verificationRequest.pending
) { ) {
// When the user clicks close on the encryption panel cancel the pending request first if any // When the user clicks close on the encryption panel cancel the pending request first if any
this.state.verificationRequest.cancel(); this.state.verificationRequest.cancel();
} else { } else {
// Otherwise we have got our user from RoomViewStore which means we're being shown
// within a room/group, so go back to the member panel if we were in the encryption panel,
// or the member list if we were in the member panel... phew.
const isEncryptionPhase = this.state.phase === RightPanelPhases.EncryptionPanel;
dis.dispatch({
action: Action.ViewUser,
member: isEncryptionPhase ? this.state.member : null,
});
}
};
onClose = () => {
// the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here // the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
defaultDispatcher.dispatch({ dis.dispatch({
action: Action.ToggleRightPanel, action: Action.ToggleRightPanel,
type: this.props.groupId ? "group" : "room", type: this.props.groupId ? "group" : "room",
}); });
}
}; };
render() { render() {
@ -260,7 +249,7 @@ export default class RightPanel extends React.Component {
user={this.state.member} user={this.state.member}
room={this.props.room} room={this.props.room}
key={roomId || this.state.member.userId} key={roomId || this.state.member.userId}
onClose={this.onCloseUserInfo} onClose={this.onClose}
phase={this.state.phase} phase={this.state.phase}
verificationRequest={this.state.verificationRequest} verificationRequest={this.state.verificationRequest}
verificationRequestPromise={this.state.verificationRequestPromise} verificationRequestPromise={this.state.verificationRequestPromise}
@ -276,7 +265,7 @@ export default class RightPanel extends React.Component {
user={this.state.member} user={this.state.member}
groupId={this.props.groupId} groupId={this.props.groupId}
key={this.state.member.userId} key={this.state.member.userId}
onClose={this.onCloseUserInfo} />; onClose={this.onClose} />;
break; break;
case RightPanelPhases.GroupRoomInfo: case RightPanelPhases.GroupRoomInfo:

View File

@ -23,6 +23,7 @@ import ResizeObserver from 'resize-observer-polyfill';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
// Shamelessly ripped off Modal.js. There's probably a better way // Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
@ -61,6 +62,9 @@ export default class PersistedElement extends React.Component {
// Any PersistedElements with the same persistKey will use // Any PersistedElements with the same persistKey will use
// the same DOM container. // the same DOM container.
persistKey: PropTypes.string.isRequired, persistKey: PropTypes.string.isRequired,
// z-index for the element. Defaults to 9.
zIndex: PropTypes.number,
}; };
constructor() { constructor() {
@ -165,7 +169,7 @@ export default class PersistedElement extends React.Component {
const parentRect = parent.getBoundingClientRect(); const parentRect = parent.getBoundingClientRect();
Object.assign(child.style, { Object.assign(child.style, {
zIndex: 9, zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
position: 'absolute', position: 'absolute',
top: parentRect.top + 'px', top: parentRect.top + 'px',
left: parentRect.left + 'px', left: parentRect.left + 'px',

View File

@ -33,6 +33,7 @@ interface IProps {
previousPhase?: RightPanelPhases; previousPhase?: RightPanelPhases;
closeLabel?: string; closeLabel?: string;
onClose?(): void; onClose?(): void;
refireParams?;
} }
interface IGroupProps { interface IGroupProps {
@ -56,6 +57,7 @@ const BaseCard: React.FC<IProps> = ({
withoutScrollContainer, withoutScrollContainer,
previousPhase, previousPhase,
children, children,
refireParams,
}) => { }) => {
let backButton; let backButton;
if (previousPhase) { if (previousPhase) {
@ -63,6 +65,7 @@ const BaseCard: React.FC<IProps> = ({
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({ defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase, action: Action.SetRightPanelPhase,
phase: previousPhase, phase: previousPhase,
refireParams: refireParams,
}); });
}; };
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={_t("Back")} />; backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={_t("Back")} />;

View File

@ -60,6 +60,7 @@ import QuestionDialog from "../dialogs/QuestionDialog";
import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog"; import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
import InfoDialog from "../dialogs/InfoDialog"; import InfoDialog from "../dialogs/InfoDialog";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
interface IDevice { interface IDevice {
deviceId: string; deviceId: string;
@ -1534,6 +1535,24 @@ const UserInfo: React.FC<Props> = ({
const classes = ["mx_UserInfo"]; const classes = ["mx_UserInfo"];
let refireParams;
let previousPhase: RightPanelPhases;
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
if (room && phase === RightPanelPhases.EncryptionPanel) {
previousPhase = RightPanelPhases.RoomMemberInfo;
refireParams = {member: member};
} else if (room) {
previousPhase = RightPanelPhases.RoomMemberList;
}
const onEncryptionPanelClose = () => {
dis.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: previousPhase,
refireParams: refireParams,
});
}
let content; let content;
switch (phase) { switch (phase) {
case RightPanelPhases.RoomMemberInfo: case RightPanelPhases.RoomMemberInfo:
@ -1553,19 +1572,13 @@ const UserInfo: React.FC<Props> = ({
<EncryptionPanel <EncryptionPanel
{...props as React.ComponentProps<typeof EncryptionPanel>} {...props as React.ComponentProps<typeof EncryptionPanel>}
member={member} member={member}
onClose={onClose} onClose={onEncryptionPanelClose}
isRoomEncrypted={isRoomEncrypted} isRoomEncrypted={isRoomEncrypted}
/> />
); );
break; break;
} }
let previousPhase: RightPanelPhases;
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
if (room) {
previousPhase = RightPanelPhases.RoomMemberList;
}
let closeLabel = undefined; let closeLabel = undefined;
if (phase === RightPanelPhases.EncryptionPanel) { if (phase === RightPanelPhases.EncryptionPanel) {
const verificationRequest = (props as React.ComponentProps<typeof EncryptionPanel>).verificationRequest; const verificationRequest = (props as React.ComponentProps<typeof EncryptionPanel>).verificationRequest;
@ -1581,6 +1594,7 @@ const UserInfo: React.FC<Props> = ({
onClose={onClose} onClose={onClose}
closeLabel={closeLabel} closeLabel={closeLabel}
previousPhase={previousPhase} previousPhase={previousPhase}
refireParams={refireParams}
> >
{ content } { content }
</BaseCard>; </BaseCard>;

View File

@ -69,19 +69,24 @@ export default class RoomProfileSettings extends React.Component {
// clear file upload field so same file can be selected // clear file upload field so same file can be selected
this._avatarUpload.current.value = ""; this._avatarUpload.current.value = "";
this.setState({ this.setState({
avatarUrl: undefined, avatarUrl: null,
avatarFile: undefined, avatarFile: null,
enableProfileSave: true, enableProfileSave: true,
}); });
}; };
_clearProfile = async (e) => { _cancelProfileChanges = async (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (!this.state.enableProfileSave) return; if (!this.state.enableProfileSave) return;
this._removeAvatar(); this.setState({
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName}); enableProfileSave: false,
displayName: this.state.originalDisplayName,
topic: this.state.originalTopic,
avatarUrl: this.state.originalAvatarUrl,
avatarFile: null,
});
}; };
_saveProfile = async (e) => { _saveProfile = async (e) => {
@ -108,7 +113,7 @@ export default class RoomProfileSettings extends React.Component {
newState.originalAvatarUrl = newState.avatarUrl; newState.originalAvatarUrl = newState.avatarUrl;
newState.avatarFile = null; newState.avatarFile = null;
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: undefined}, ''); await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
} }
if (this.state.originalTopic !== this.state.topic) { if (this.state.originalTopic !== this.state.topic) {
@ -164,11 +169,15 @@ export default class RoomProfileSettings extends React.Component {
const AvatarSetting = sdk.getComponent('settings.AvatarSetting'); const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
let profileSettingsButtons; let profileSettingsButtons;
if (this.state.canSetTopic && this.state.canSetName) { if (
this.state.canSetName ||
this.state.canSetTopic ||
this.state.canSetAvatar
) {
profileSettingsButtons = ( profileSettingsButtons = (
<div className="mx_ProfileSettings_buttons"> <div className="mx_ProfileSettings_buttons">
<AccessibleButton <AccessibleButton
onClick={this._clearProfile} onClick={this._cancelProfileChanges}
kind="link" kind="link"
disabled={!this.state.enableProfileSave} disabled={!this.state.enableProfileSave}
> >

View File

@ -426,7 +426,8 @@ export default class MessageComposer extends React.Component {
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />, <EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
); );
if (SettingsStore.getValue(UIFeature.Widgets)) { if (SettingsStore.getValue(UIFeature.Widgets) &&
SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />); controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
} }

View File

@ -403,6 +403,7 @@ export default class SendMessageComposer extends React.Component {
this._editorRef.clearUndoHistory(); this._editorRef.clearUndoHistory();
this._editorRef.focus(); this._editorRef.focus();
this._clearStoredEditorState(); this._clearStoredEditorState();
dis.dispatch({action: "scroll_to_bottom"});
} }
componentWillUnmount() { componentWillUnmount() {

View File

@ -264,7 +264,7 @@ export default class Stickerpicker extends React.Component {
width: this.popoverWidth, width: this.popoverWidth,
}} }}
> >
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}> <PersistedElement persistKey={PERSISTED_ELEMENT_KEY} zIndex={STICKERPICKER_Z_INDEX}>
<AppTile <AppTile
app={stickerApp} app={stickerApp}
room={this.props.room} room={this.props.room}

View File

@ -52,19 +52,23 @@ export default class ProfileSettings extends React.Component {
// clear file upload field so same file can be selected // clear file upload field so same file can be selected
this._avatarUpload.current.value = ""; this._avatarUpload.current.value = "";
this.setState({ this.setState({
avatarUrl: undefined, avatarUrl: null,
avatarFile: undefined, avatarFile: null,
enableProfileSave: true, enableProfileSave: true,
}); });
}; };
_clearProfile = async (e) => { _cancelProfileChanges = async (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (!this.state.enableProfileSave) return; if (!this.state.enableProfileSave) return;
this._removeAvatar(); this.setState({
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName}); enableProfileSave: false,
displayName: this.state.originalDisplayName,
avatarUrl: this.state.originalAvatarUrl,
avatarFile: null,
});
}; };
_saveProfile = async (e) => { _saveProfile = async (e) => {
@ -186,7 +190,7 @@ export default class ProfileSettings extends React.Component {
</div> </div>
<div className="mx_ProfileSettings_buttons"> <div className="mx_ProfileSettings_buttons">
<AccessibleButton <AccessibleButton
onClick={this._clearProfile} onClick={this._cancelProfileChanges}
kind="link" kind="link"
disabled={!this.state.enableProfileSave} disabled={!this.state.enableProfileSave}
> >

View File

@ -34,6 +34,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
'MessageComposerInput.suggestEmoji', 'MessageComposerInput.suggestEmoji',
'sendTypingNotifications', 'sendTypingNotifications',
'MessageComposerInput.ctrlEnterToSend', 'MessageComposerInput.ctrlEnterToSend',
'MessageComposerInput.showStickersButton',
]; ];
static TIMELINE_SETTINGS = [ static TIMELINE_SETTINGS = [

View File

@ -795,6 +795,7 @@
"Font size": "Font size", "Font size": "Font size",
"Use custom size": "Use custom size", "Use custom size": "Use custom size",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
"Show stickers button": "Show stickers button",
"Use a more compact Modern layout": "Use a more compact Modern layout", "Use a more compact Modern layout": "Use a more compact Modern layout",
"Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show a placeholder for removed messages": "Show a placeholder for removed messages",
"Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)", "Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)",

View File

@ -240,6 +240,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: true, default: true,
invertedSettingName: 'MessageComposerInput.dontSuggestEmoji', invertedSettingName: 'MessageComposerInput.dontSuggestEmoji',
}, },
"MessageComposerInput.showStickersButton": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Show stickers button'),
default: true,
},
// TODO: Wire up appropriately to UI (FTUE notifications) // TODO: Wire up appropriately to UI (FTUE notifications)
"Notifications.alwaysShowBadgeCounts": { "Notifications.alwaysShowBadgeCounts": {
supportedLevels: LEVELS_ROOM_OR_ACCOUNT, supportedLevels: LEVELS_ROOM_OR_ACCOUNT,