diff --git a/res/css/_components.scss b/res/css/_components.scss index 66eb98ea9d..afc40ca0d6 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -30,7 +30,7 @@ @import "./structures/_ToastContainer.scss"; @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; -@import "./structures/_UserMenuButton.scss"; +@import "./structures/_UserMenu.scss"; @import "./structures/_ViewSource.scss"; @import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 0765b628f6..98f23a058b 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -54,7 +54,7 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations display: flex; flex-direction: column; - // There's 2 rows when breadcrumbs are present: the top bit and the breadcrumbs + // This is basically just breadcrumbs. The row above that is handled by the UserMenu .mx_LeftPanel2_headerRow { // Create yet another flexbox, this time within the row, to ensure items stay // aligned correctly. This is also a row-based flexbox. @@ -62,32 +62,6 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations align-items: center; } - .mx_LeftPanel2_userAvatarContainer { - position: relative; // to make default avatars work - margin-right: 8px; - height: 32px; // to remove the unknown 4px gap the browser puts below it - - .mx_LeftPanel2_userAvatar { - border-radius: 32px; // should match avatar size - } - } - - .mx_LeftPanel2_userName { - font-weight: 600; - font-size: $font-15px; - line-height: $font-20px; - flex: 1; - - // Ellipsize any text overflow - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - - .mx_LeftPanel2_headerButtons { - // No special styles: the rest of the layout happens to make it work. - } - .mx_LeftPanel2_breadcrumbsContainer { width: 100%; overflow: hidden; @@ -158,16 +132,6 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations .mx_LeftPanel2_roomListContainer { width: 68px; - .mx_LeftPanel2_userHeader { - .mx_LeftPanel2_headerRow { - justify-content: center; - } - - .mx_LeftPanel2_userAvatarContainer { - margin-right: 0; - } - } - .mx_LeftPanel2_filterContainer { // Organize the flexbox into a centered column layout flex-direction: column; diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenu.scss similarity index 66% rename from res/css/structures/_UserMenuButton.scss rename to res/css/structures/_UserMenu.scss index f1dffbd1f5..bbb1e1cc7b 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenu.scss @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_UserMenuButton { - > span { +.mx_UserMenu { + .mx_UserMenu_headerButtons { width: 16px; height: 16px; position: relative; @@ -35,12 +35,56 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/more-horizontal.svg'); } } + + .mx_UserMenu_row { + // Create a row-based flexbox to ensure items stay aligned correctly. + display: flex; + align-items: center; + + .mx_UserMenu_userAvatarContainer { + position: relative; // to make default avatars work + margin-right: 8px; + height: 32px; // to remove the unknown 4px gap the browser puts below it + + .mx_UserMenu_userAvatar { + border-radius: 32px; // should match avatar size + } + } + + .mx_UserMenu_userName { + font-weight: 600; + font-size: $font-15px; + line-height: $font-20px; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .mx_UserMenu_headerButtons { + // No special styles: the rest of the layout happens to make it work. + } + } + + &.mx_UserMenu_minimized { + .mx_UserMenu_userHeader { + .mx_UserMenu_row { + justify-content: center; + } + + .mx_UserMenu_userAvatarContainer { + margin-right: 0; + } + } + } } -.mx_UserMenuButton_contextMenu { +.mx_UserMenu_contextMenu { width: 247px; - .mx_UserMenuButton_contextMenu_redRow { + .mx_UserMenu_contextMenu_redRow { .mx_AccessibleButton { color: $warning-color !important; // !important to override styles from context menu } @@ -50,12 +94,12 @@ limitations under the License. } } - .mx_UserMenuButton_contextMenu_header { + .mx_UserMenu_contextMenu_header { // Create a flexbox to organize the header a bit easier display: flex; align-items: center; - .mx_UserMenuButton_contextMenu_name { + .mx_UserMenu_contextMenu_name { // Create another flexbox of columns to handle large user IDs display: flex; flex-direction: column; @@ -72,19 +116,19 @@ limitations under the License. white-space: nowrap; } - .mx_UserMenuButton_contextMenu_displayName { + .mx_UserMenu_contextMenu_displayName { font-weight: bold; font-size: $font-15px; line-height: $font-20px; } - .mx_UserMenuButton_contextMenu_userId { + .mx_UserMenu_contextMenu_userId { font-size: $font-15px; line-height: $font-24px; } } - .mx_UserMenuButton_contextMenu_themeButton { + .mx_UserMenu_contextMenu_themeButton { min-width: 32px; max-width: 32px; width: 32px; @@ -118,31 +162,31 @@ limitations under the License. } } - .mx_UserMenuButton_iconHome::before { + .mx_UserMenu_iconHome::before { mask-image: url('$(res)/img/feather-customised/home.svg'); } - .mx_UserMenuButton_iconBell::before { + .mx_UserMenu_iconBell::before { mask-image: url('$(res)/img/feather-customised/notifications.svg'); } - .mx_UserMenuButton_iconLock::before { + .mx_UserMenu_iconLock::before { mask-image: url('$(res)/img/feather-customised/lock.svg'); } - .mx_UserMenuButton_iconSettings::before { + .mx_UserMenu_iconSettings::before { mask-image: url('$(res)/img/feather-customised/settings.svg'); } - .mx_UserMenuButton_iconArchive::before { + .mx_UserMenu_iconArchive::before { mask-image: url('$(res)/img/feather-customised/archive.svg'); } - .mx_UserMenuButton_iconMessage::before { + .mx_UserMenu_iconMessage::before { mask-image: url('$(res)/img/feather-customised/message-circle.svg'); } - .mx_UserMenuButton_iconSignOut::before { + .mx_UserMenu_iconSignOut::before { mask-image: url('$(res)/img/feather-customised/sign-out.svg'); } } diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 27583f26ee..32d6748f94 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -22,18 +22,13 @@ import dis from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import RoomList2 from "../views/rooms/RoomList2"; import { Action } from "../../dispatcher/actions"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import BaseAvatar from '../views/avatars/BaseAvatar'; -import UserMenuButton from "./UserMenuButton"; +import UserMenu from "./UserMenu"; import RoomSearch from "./RoomSearch"; import AccessibleButton from "../views/elements/AccessibleButton"; import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import ResizeNotifier from "../../utils/ResizeNotifier"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { throttle } from 'lodash'; -import { OwnProfileStore } from "../../stores/OwnProfileStore"; /******************************************************************* * CAUTION * @@ -76,32 +71,13 @@ export default class LeftPanel2 extends React.Component { // We watch the middle panel because we don't actually get resized, the middle panel does. // We listen to the noisy channel to avoid choppy reaction times. this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); - - OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); } public componentWillUnmount() { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); - OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); } - // TSLint wants this to be a member, but we don't want that. - // tslint:disable-next-line - private onRoomStateUpdate = throttle((ev: MatrixEvent) => { - const myUserId = MatrixClientPeg.get().getUserId(); - if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) { - // noinspection JSIgnoredPromiseFromCall - this.onProfileUpdate(); - } - }, 200, {trailing: true, leading: true}); - - private onProfileUpdate = async () => { - // the store triggered an update, so force a layout update. We don't - // have any state to store here for that to magically happen. - this.forceUpdate(); - }; - private onSearch = (term: string): void => { this.setState({searchFilter: term}); }; @@ -170,7 +146,6 @@ export default class LeftPanel2 extends React.Component { // TODO: Presence // TODO: Breadcrumbs toggle // TODO: Menu button - const avatarSize = 32; // should match border-radius of the avatar let breadcrumbs; if (this.state.showBreadcrumbs) { @@ -181,34 +156,9 @@ export default class LeftPanel2 extends React.Component { ); } - let name = {OwnProfileStore.instance.displayName}; - let buttons = ( - - - - ); - if (this.props.isMinimized) { - name = null; - buttons = null; - } - return (
-
- - - - {name} - {buttons} -
+ {breadcrumbs}
); diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx new file mode 100644 index 0000000000..19e57ac51b --- /dev/null +++ b/src/components/structures/UserMenu.tsx @@ -0,0 +1,332 @@ +/* +Copyright 2020 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 * as React from "react"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { ActionPayload } from "../../dispatcher/payloads"; +import { Action } from "../../dispatcher/actions"; +import { createRef } from "react"; +import { _t } from "../../languageHandler"; +import {ContextMenu, ContextMenuButton} from "./ContextMenu"; +import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog"; +import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; +import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog"; +import Modal from "../../Modal"; +import LogoutDialog from "../views/dialogs/LogoutDialog"; +import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; +import {getCustomTheme} from "../../theme"; +import {getHostingLink} from "../../utils/HostingLink"; +import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; +import SdkConfig from "../../SdkConfig"; +import {getHomePageUrl} from "../../utils/pages"; +import { OwnProfileStore } from "../../stores/OwnProfileStore"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import BaseAvatar from '../views/avatars/BaseAvatar'; +import classNames from "classnames"; + +interface IProps { + isMinimized: boolean; +} + +interface IState { + menuDisplayed: boolean; + isDarkTheme: boolean; +} + +export default class UserMenu extends React.Component { + private dispatcherRef: string; + private themeWatcherRef: string; + private buttonRef: React.RefObject = createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + menuDisplayed: false, + isDarkTheme: this.isUserOnDarkTheme(), + }; + + OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); + } + + private get hasHomePage(): boolean { + return !!getHomePageUrl(SdkConfig.get()); + } + + public componentDidMount() { + this.dispatcherRef = defaultDispatcher.register(this.onAction); + this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); + } + + public componentWillUnmount() { + if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); + if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); + OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); + } + + private isUserOnDarkTheme(): boolean { + const theme = SettingsStore.getValue("theme"); + if (theme.startsWith("custom-")) { + return getCustomTheme(theme.substring("custom-".length)).is_dark; + } + return theme === "dark"; + } + + private onProfileUpdate = async () => { + // the store triggered an update, so force a layout update. We don't + // have any state to store here for that to magically happen. + this.forceUpdate(); + }; + + private onThemeChanged = () => { + this.setState({isDarkTheme: this.isUserOnDarkTheme()}); + }; + + private onAction = (ev: ActionPayload) => { + if (ev.action !== Action.ToggleUserMenu) return; // not interested + + // For accessibility + if (this.buttonRef.current) this.buttonRef.current.click(); + }; + + private onOpenMenuClick = (ev: InputEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({menuDisplayed: true}); + }; + + private onCloseMenu = (ev: InputEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({menuDisplayed: false}); + }; + + private onSwitchThemeClick = () => { + // Disable system theme matching if the user hits this button + SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); + + const newTheme = this.state.isDarkTheme ? "light" : "dark"; + SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab + }; + + private onSettingsOpen = (ev: ButtonEvent, tabId: string) => { + ev.preventDefault(); + ev.stopPropagation(); + + const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId}; + defaultDispatcher.dispatch(payload); + this.setState({menuDisplayed: false}); // also close the menu + }; + + private onShowArchived = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + // TODO: Archived room view (deferred) + console.log("TODO: Show archived rooms"); + }; + + private onProvideFeedback = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); + this.setState({menuDisplayed: false}); // also close the menu + }; + + private onSignOutClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); + this.setState({menuDisplayed: false}); // also close the menu + }; + + private onHomeClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({action: 'view_home_page'}); + }; + + private renderContextMenu = (): React.ReactNode => { + if (!this.state.menuDisplayed) return null; + + let hostingLink; + const signupLink = getHostingLink("user-context-menu"); + if (signupLink) { + hostingLink = ( +
+ {_t( + "Upgrade to your own domain", {}, + { + a: sub => ( + {sub} + ), + }, + )} +
+ ); + } + + let homeButton = null; + if (this.hasHomePage) { + homeButton = ( +
  • + + + {_t("Home")} + +
  • + ); + } + + const elementRect = this.buttonRef.current.getBoundingClientRect(); + return ( + +
    +
    +
    + + {OwnProfileStore.instance.displayName} + + + {MatrixClientPeg.get().getUserId()} + +
    +
    + {_t("Switch +
    +
    + {hostingLink} +
    +
      + {homeButton} +
    • + this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> + + {_t("Notification settings")} + +
    • +
    • + this.onSettingsOpen(e, USER_SECURITY_TAB)}> + + {_t("Security & privacy")} + +
    • +
    • + this.onSettingsOpen(e, null)}> + + {_t("All settings")} + +
    • +
    • + + + {_t("Archived rooms")} + +
    • +
    • + + + {_t("Feedback")} + +
    • +
    +
    +
    +
      +
    • + + + {_t("Sign out")} + +
    • +
    +
    +
    +
    + ); + }; + + public render() { + const avatarSize = 32; // should match border-radius of the avatar + + let name = {OwnProfileStore.instance.displayName}; + let buttons = ( + + {/* masked image in CSS */} + + ); + if (this.props.isMinimized) { + name = null; + buttons = null; + } + + const classes = classNames({ + 'mx_UserMenu': true, + 'mx_UserMenu_minimized': this.props.isMinimized, + }); + + return ( + + +
    + + + + {name} + {buttons} +
    + {this.renderContextMenu()} +
    +
    + ); + } +} diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx deleted file mode 100644 index 7613a4a9ae..0000000000 --- a/src/components/structures/UserMenuButton.tsx +++ /dev/null @@ -1,294 +0,0 @@ -/* -Copyright 2020 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 * as React from "react"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import defaultDispatcher from "../../dispatcher/dispatcher"; -import { ActionPayload } from "../../dispatcher/payloads"; -import { Action } from "../../dispatcher/actions"; -import { createRef } from "react"; -import { _t } from "../../languageHandler"; -import {ContextMenu, ContextMenuButton} from "./ContextMenu"; -import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog"; -import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; -import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog"; -import Modal from "../../Modal"; -import LogoutDialog from "../views/dialogs/LogoutDialog"; -import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; -import {getCustomTheme} from "../../theme"; -import {getHostingLink} from "../../utils/HostingLink"; -import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; -import SdkConfig from "../../SdkConfig"; -import {getHomePageUrl} from "../../utils/pages"; -import { OwnProfileStore } from "../../stores/OwnProfileStore"; -import { UPDATE_EVENT } from "../../stores/AsyncStore"; - -interface IProps { -} - -interface IState { - menuDisplayed: boolean; - isDarkTheme: boolean; -} - -export default class UserMenuButton extends React.Component { - private dispatcherRef: string; - private themeWatcherRef: string; - private buttonRef: React.RefObject = createRef(); - - constructor(props: IProps) { - super(props); - - this.state = { - menuDisplayed: false, - isDarkTheme: this.isUserOnDarkTheme(), - }; - - OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); - } - - private get hasHomePage(): boolean { - return !!getHomePageUrl(SdkConfig.get()); - } - - public componentDidMount() { - this.dispatcherRef = defaultDispatcher.register(this.onAction); - this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); - } - - public componentWillUnmount() { - if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); - if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); - OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); - } - - private isUserOnDarkTheme(): boolean { - const theme = SettingsStore.getValue("theme"); - if (theme.startsWith("custom-")) { - return getCustomTheme(theme.substring("custom-".length)).is_dark; - } - return theme === "dark"; - } - - private onProfileUpdate = async () => { - // the store triggered an update, so force a layout update. We don't - // have any state to store here for that to magically happen. - this.forceUpdate(); - }; - - private onThemeChanged = () => { - this.setState({isDarkTheme: this.isUserOnDarkTheme()}); - }; - - private onAction = (ev: ActionPayload) => { - if (ev.action !== Action.ToggleUserMenu) return; // not interested - - // For accessibility - if (this.buttonRef.current) this.buttonRef.current.click(); - }; - - private onOpenMenuClick = (ev: InputEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - this.setState({menuDisplayed: true}); - }; - - private onCloseMenu = () => { - this.setState({menuDisplayed: false}); - }; - - private onSwitchThemeClick = () => { - // Disable system theme matching if the user hits this button - SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); - - const newTheme = this.state.isDarkTheme ? "light" : "dark"; - SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab - }; - - private onSettingsOpen = (ev: ButtonEvent, tabId: string) => { - ev.preventDefault(); - ev.stopPropagation(); - - const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId}; - defaultDispatcher.dispatch(payload); - this.setState({menuDisplayed: false}); // also close the menu - }; - - private onShowArchived = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - // TODO: Archived room view (deferred) - console.log("TODO: Show archived rooms"); - }; - - private onProvideFeedback = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); - this.setState({menuDisplayed: false}); // also close the menu - }; - - private onSignOutClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); - this.setState({menuDisplayed: false}); // also close the menu - }; - - private onHomeClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - defaultDispatcher.dispatch({action: 'view_home_page'}); - }; - - public render() { - let contextMenu; - if (this.state.menuDisplayed) { - let hostingLink; - const signupLink = getHostingLink("user-context-menu"); - if (signupLink) { - hostingLink = ( -
    - {_t( - "Upgrade to your own domain", {}, - { - a: sub => ( - {sub} - ), - }, - )} -
    - ); - } - - let homeButton = null; - if (this.hasHomePage) { - homeButton = ( -
  • - - - {_t("Home")} - -
  • - ); - } - - const elementRect = this.buttonRef.current.getBoundingClientRect(); - contextMenu = ( - -
    -
    -
    - - {OwnProfileStore.instance.displayName} - - - {MatrixClientPeg.get().getUserId()} - -
    -
    - {_t("Switch -
    -
    - {hostingLink} -
    -
      - {homeButton} -
    • - this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> - - {_t("Notification settings")} - -
    • -
    • - this.onSettingsOpen(e, USER_SECURITY_TAB)}> - - {_t("Security & privacy")} - -
    • -
    • - this.onSettingsOpen(e, null)}> - - {_t("All settings")} - -
    • -
    • - - - {_t("Archived rooms")} - -
    • -
    • - - - {_t("Feedback")} - -
    • -
    -
    -
    -
      -
    • - - - {_t("Sign out")} - -
    • -
    -
    -
    -
    - ); - } - - return ( - - - {/* masked image in CSS */} - - {contextMenu} - - ); - } -}