From 8e0247afe5ed1970e653aab6ec7083fb0e09d159 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 7 Jun 2020 22:06:41 -0600 Subject: [PATCH 1/9] Add most of the UI for the new room list's menu button Incomplete implementation: buttons don't work, some text is missing, etc --- res/css/_components.scss | 1 + res/css/structures/_LeftPanel2.scss | 5 + res/css/structures/_UserMenuButton.scss | 157 +++++++++++++ res/img/feather-customised/archive.svg | 1 + .../feather-customised/more-horizontal.svg | 1 + res/img/feather-customised/sun.svg | 1 + res/themes/light/css/_light.scss | 1 + src/components/structures/LeftPanel2.tsx | 5 +- src/components/structures/LoggedInView.tsx | 4 +- .../structures/TopLeftMenuButton.js | 3 +- src/components/structures/UserMenuButton.tsx | 211 ++++++++++++++++++ src/dispatcher/actions.ts | 5 + src/i18n/strings/en_EN.json | 7 + 13 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 res/css/structures/_UserMenuButton.scss create mode 100644 res/img/feather-customised/archive.svg create mode 100644 res/img/feather-customised/more-horizontal.svg create mode 100644 res/img/feather-customised/sun.svg create mode 100644 src/components/structures/UserMenuButton.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 62bec5ad62..de4c1c677c 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -29,6 +29,7 @@ @import "./structures/_ToastContainer.scss"; @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; +@import "./structures/_UserMenuButton.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 822a5ac399..d335df305f 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -73,6 +73,11 @@ $roomListMinimizedWidth: 50px; font-weight: 600; font-size: $font-15px; line-height: $font-20px; + flex: 1; + } + + .mx_LeftPanel2_headerButtons { + // No special styles: the rest of the layout happens to make it work. } .mx_LeftPanel2_breadcrumbsContainer { diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss new file mode 100644 index 0000000000..f7097311c9 --- /dev/null +++ b/res/css/structures/_UserMenuButton.scss @@ -0,0 +1,157 @@ +/* +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. +*/ + +.mx_UserMenuButton { + // No special styles on the button itself +} + +.mx_UserMenuButton_contextMenu { + width: 231px; + + // Put 20px of padding around the whole menu. We do this instead of a + // simple `padding: 20px` rule so the horizontal rules added by the + // optionLists is rendered correctly (full width). + >* { + padding-left: 20px; + padding-right: 20px; + + &:first-child { + padding-top: 20px; + } + + &:last-child { + padding-bottom: 20px; + } + } + + .mx_UserMenuButton_contextMenu_header { + // Create a flexbox to organize the header a bit easier + display: flex; + align-items: center; + + .mx_UserMenuButton_contextMenu_name { + // Create another flexbox of columns to handle large user IDs + display: flex; + flex-direction: column; + + // fit the container + flex: 1; + width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button + + * { + // Automatically grow all subelements to fit the container + flex: 1; + width: 100%; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .mx_UserMenuButton_contextMenu_displayName { + font-weight: bold; + font-size: $font-15px; + line-height: $font-20px; + } + + .mx_UserMenuButton_contextMenu_userId { + font-size: $font-15px; + line-height: $font-24px; + } + } + + .mx_UserMenuButton_contextMenu_themeButton { + min-width: 32px; + max-width: 32px; + width: 32px; + height: 32px; + margin-left: 8px; + border-radius: 32px; + background-color: $theme-button-bg-color; + cursor: pointer; + + // to make alignment easier, create flexbox for the image + display: flex; + align-items: center; + justify-content: center; + } + } + + .mx_UserMenuButton_contextMenu_optionList { + margin-top: 20px; + + // This is a bit of a hack when we could just use a simple border-top property, + // however we have a (kinda) good reason for doing it this way: we need opacity. + // To get the right color, we need an opacity modifier which means we have to work + // around the problem. PostCSS doesn't support the opacity() function, and if we + // use something like postcss-functions we quickly run into an issue where the + // function we would define gets passed a CSS variable for custom themes, which + // can't be converted easily even when considering https://stackoverflow.com/a/41265350/7037379 + // + // Therefore, we just hack in a line and border the thing ourselves + &::before { + border-top: 1px solid $primary-fg-color; + opacity: 0.1; + content: ''; + + // Counteract the padding problems (width: 100% ignores the 40px padding, + // unless we position it absolutely then it does the right thing). + width: 100%; + position: absolute; + left: 0; + } + + ul { + list-style: none; + margin: 0; + padding: 0; + + li { + margin: 0; + padding: 20px 0 0; + + a { + text-decoration: none; + color: $primary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + + // Create a flexbox to more easily define the list items + display: flex; + align-items: center; + + img { // icons + width: 16px; + min-width: 16px; + max-width: 16px; + } + + span { // labels + padding-left: 14px; + width: 100%; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + } + } +} diff --git a/res/img/feather-customised/archive.svg b/res/img/feather-customised/archive.svg new file mode 100644 index 0000000000..428882c87b --- /dev/null +++ b/res/img/feather-customised/archive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/feather-customised/more-horizontal.svg b/res/img/feather-customised/more-horizontal.svg new file mode 100644 index 0000000000..dc6a85564e --- /dev/null +++ b/res/img/feather-customised/more-horizontal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/feather-customised/sun.svg b/res/img/feather-customised/sun.svg new file mode 100644 index 0000000000..7f51b94d1c --- /dev/null +++ b/res/img/feather-customised/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 5aeb125774..a50c34cf03 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -177,6 +177,7 @@ $header-divider-color: #91A1C0; $roomtile2-preview-color: #9e9e9e; $roomtile2-badge-color: #61708b; $roomtile2-selected-bg-color: #FFF; +$theme-button-bg-color: #e3e8f0; $roomtile-name-color: #61708b; $roomtile-badge-fg-color: $accent-fg-color; diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index c66c0a6799..2fd8612ff5 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -27,6 +27,7 @@ import { Action } from "../../dispatcher/actions"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import BaseAvatar from '../views/avatars/BaseAvatar'; import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs"; +import UserMenuButton from "./UserMenuButton"; /******************************************************************* * CAUTION * @@ -49,7 +50,6 @@ export default class LeftPanel2 extends React.Component { // TODO: Properly support TagPanel // TODO: Properly support searching/filtering // TODO: Properly support breadcrumbs - // TODO: Properly support TopLeftMenu (User Settings) // TODO: a11y // TODO: actually make this useful in general (match design proposals) // TODO: Fadable support (is this still needed?) @@ -115,6 +115,9 @@ export default class LeftPanel2 extends React.Component { /> {displayName} + + +
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 0504e3a76a..19edf505e0 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -452,9 +452,7 @@ class LoggedInView extends React.PureComponent { // composer, so CTRL+` it is if (ctrlCmdOnly) { - dis.dispatch({ - action: 'toggle_top_left_menu', - }); + dis.fire(Action.ToggleUserMenu); handled = true; } break; diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js index 234dc661f9..71e7e61406 100644 --- a/src/components/structures/TopLeftMenuButton.js +++ b/src/components/structures/TopLeftMenuButton.js @@ -24,6 +24,7 @@ import * as Avatar from '../../Avatar'; import { _t } from '../../languageHandler'; import dis from "../../dispatcher/dispatcher"; import {ContextMenu, ContextMenuButton} from "./ContextMenu"; +import {Action} from "../../dispatcher/actions"; const AVATAR_SIZE = 28; @@ -75,7 +76,7 @@ export default class TopLeftMenuButton extends React.Component { onAction = (payload) => { // For accessibility - if (payload.action === "toggle_top_left_menu") { + if (payload.action === Action.ToggleUserMenu) { if (this._buttonRef) this._buttonRef.click(); } }; diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx new file mode 100644 index 0000000000..0968e0bdb6 --- /dev/null +++ b/src/components/structures/UserMenuButton.tsx @@ -0,0 +1,211 @@ +/* +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 {User} from "matrix-js-sdk/src/models/user"; +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"; + +interface IProps { +} + +interface IState { + user: User; + menuDisplayed: boolean; +} + +export default class UserMenuButton extends React.Component { + private dispatcherRef: string; + private buttonRef: React.RefObject = createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + menuDisplayed: false, + user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()), + }; + } + + private get displayName(): string { + if (MatrixClientPeg.get().isGuest()) { + return _t("Guest"); + } else if (this.state.user) { + return this.state.user.displayName; + } else { + return MatrixClientPeg.get().getUserId(); + } + } + + public componentDidMount() { + this.dispatcherRef = defaultDispatcher.register(this.onAction); + } + + 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 = () => { + console.log("TODO: Switch theme"); + }; + + private onSettingsOpen = (ev: React.MouseEvent, tabRef: string) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO: Open settings", tabRef); + }; + + private onShowArchived = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO: Show archived rooms"); + }; + + private onProvideFeedback = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO: Show feedback"); + }; + + private onSignOutClick = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO: Sign out"); + }; + + public render() { + let contextMenu; + if (this.state.menuDisplayed) { + const elementRect = this.buttonRef.current.getBoundingClientRect(); + contextMenu = ( + + + + ); + } + + return ( + + + ... + + {contextMenu} + + ) + } +} diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 71493d6e44..60ef61a6e9 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -58,4 +58,9 @@ export enum Action { * Focuses the user's cursor to the composer. No additional payload information required. */ FocusComposer = "focus_composer", + + /** + * Opens the user menu (previously known as the top left menu). No additional payload information required. + */ + ToggleUserMenu = "toggle_user_menu", } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cf6dc2431a..8575b3a258 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2042,6 +2042,13 @@ "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", + "Switch to dark mode": "Switch to dark mode", + "Switch theme": "Switch theme", + "Security & privacy": "Security & privacy", + "All settings": "All settings", + "Archived rooms": "Archived rooms", + "Feedback": "Feedback", + "Account settings": "Account settings", "Could not load user profile": "Could not load user profile", "Verify this login": "Verify this login", "Session verified": "Session verified", From 8b6b117fbfe95f3f896972d30ca9fa08a410807a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 7 Jun 2020 22:15:54 -0600 Subject: [PATCH 2/9] Appease the linter --- res/css/structures/_UserMenuButton.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index f7097311c9..aca5f4253a 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -24,7 +24,7 @@ limitations under the License. // Put 20px of padding around the whole menu. We do this instead of a // simple `padding: 20px` rule so the horizontal rules added by the // optionLists is rendered correctly (full width). - >* { + > * { padding-left: 20px; padding-right: 20px; From f05a1e532b5652d0fe2236b6a2794b889c43149d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 7 Jun 2020 22:17:02 -0600 Subject: [PATCH 3/9] Point buttons at the right functions --- src/components/structures/UserMenuButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 0968e0bdb6..abf4689aa1 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -171,7 +171,7 @@ export default class UserMenuButton extends React.Component {
  • - + {_t("Feedback")} @@ -181,7 +181,7 @@ export default class UserMenuButton extends React.Component {
    • - this.onSettingsOpen(e, 'notifications')}> + {_t("Sign out")} From de18af35ff58b8ffd4f323a47130ff4f05b331ed Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 08:20:15 -0600 Subject: [PATCH 4/9] Support minimum to open user settings to a particular tab Tabs now have IDs, and we use those IDs to open things. This doesn't do any conversion to typescript, and doesn't add the same feature to the room settings out of concern for the size of diff. --- src/components/structures/MatrixChat.tsx | 8 +++-- src/components/structures/TabbedView.tsx | 30 ++++++++----------- src/components/structures/UserMenuButton.tsx | 14 +++++---- .../views/dialogs/RoomSettingsDialog.js | 13 ++++++++ .../views/dialogs/UserSettingsDialog.js | 24 ++++++++++++++- src/dispatcher/actions.ts | 1 + src/dispatcher/payloads/OpenToTabPayload.ts | 27 +++++++++++++++++ 7 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 src/dispatcher/payloads/OpenToTabPayload.ts diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 69f91047b7..e08381d8fa 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -72,6 +72,7 @@ import { hideToast as hideAnalyticsToast } from "../../toasts/AnalyticsToast"; import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast"; +import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; /** constants for MatrixChat.state.view */ export enum Views { @@ -604,9 +605,12 @@ export default class MatrixChat extends React.PureComponent { this.viewIndexedRoom(payload.roomIndex); break; case Action.ViewUserSettings: { + const tabPayload = payload as OpenToTabPayload; const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); - Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + Modal.createTrackedDialog('User settings', '', UserSettingsDialog, + {initialTabId: tabPayload.initialTabId}, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true + ); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index c0e0e58db8..704dbf8832 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -27,25 +27,20 @@ import { ReactNode } from "react"; * Represents a tab for the TabbedView. */ export class Tab { - public label: string; - public icon: string; - public body: React.ReactNode; - /** * Creates a new tab. - * @param {string} tabLabel The untranslated tab label. - * @param {string} tabIconClass The class for the tab icon. This should be a simple mask. - * @param {React.ReactNode} tabJsx The JSX for the tab container. + * @param {string} id The tab's ID. + * @param {string} label The untranslated tab label. + * @param {string} icon The class for the tab icon. This should be a simple mask. + * @param {React.ReactNode} body The JSX for the tab container. */ - constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) { - this.label = tabLabel; - this.icon = tabIconClass; - this.body = tabJsx; + constructor(public id: string, public label: string, public icon: string, public body: React.ReactNode) { } } interface IProps { tabs: Tab[]; + initialTabId?: string; } interface IState { @@ -53,16 +48,17 @@ interface IState { } export default class TabbedView extends React.Component { - static propTypes = { - // The tabs to show - tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired, - }; - constructor(props: IProps) { super(props); + let activeTabIndex = 0; + if (props.initialTabId) { + const tabIndex = props.tabs.findIndex(t => t.id === props.initialTabId); + if (tabIndex >= 0) activeTabIndex = tabIndex; + } + this.state = { - activeTabIndex: 0, + activeTabIndex, }; } diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index abf4689aa1..827a279d98 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -23,6 +23,8 @@ 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"; interface IProps { } @@ -80,11 +82,13 @@ export default class UserMenuButton extends React.Component { console.log("TODO: Switch theme"); }; - private onSettingsOpen = (ev: React.MouseEvent, tabRef: string) => { + private onSettingsOpen = (ev: React.MouseEvent, tabId: string) => { ev.preventDefault(); ev.stopPropagation(); - console.log("TODO: Open settings", tabRef); + const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId}; + defaultDispatcher.dispatch(payload); + this.setState({menuDisplayed: false}); // also close the menu }; private onShowArchived = (ev: React.MouseEvent) => { @@ -147,19 +151,19 @@ export default class UserMenuButton extends React.Component {
      • - this.onSettingsOpen(e, 'notifications')}> + this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> {_t("Notification settings")}
      • - this.onSettingsOpen(e, 'security')}> + this.onSettingsOpen(e, USER_SECURITY_TAB)}> {_t("Security & privacy")}
      • - this.onSettingsOpen(e, 'all')}> + this.onSettingsOpen(e, null)}> {_t("All settings")} diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index c2b98cd9f3..7ad1001f75 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -30,6 +30,13 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; +export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; +export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; +export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB"; +export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; +export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; +export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; + export default class RoomSettingsDialog extends React.Component { static propTypes = { roomId: PropTypes.string.isRequired, @@ -56,21 +63,25 @@ export default class RoomSettingsDialog extends React.Component { const tabs = []; tabs.push(new Tab( + ROOM_GENERAL_TAB, _td("General"), "mx_RoomSettingsDialog_settingsIcon", , )); tabs.push(new Tab( + ROOM_SECURITY_TAB, _td("Security & Privacy"), "mx_RoomSettingsDialog_securityIcon", , )); tabs.push(new Tab( + ROOM_ROLES_TAB, _td("Roles & Permissions"), "mx_RoomSettingsDialog_rolesIcon", , )); tabs.push(new Tab( + ROOM_NOTIFICATIONS_TAB, _td("Notifications"), "mx_RoomSettingsDialog_notificationsIcon", , @@ -78,6 +89,7 @@ export default class RoomSettingsDialog extends React.Component { if (SettingsStore.isFeatureEnabled("feature_bridge_state")) { tabs.push(new Tab( + ROOM_BRIDGES_TAB, _td("Bridges"), "mx_RoomSettingsDialog_bridgesIcon", , @@ -85,6 +97,7 @@ export default class RoomSettingsDialog extends React.Component { } tabs.push(new Tab( + ROOM_ADVANCED_TAB, _td("Advanced"), "mx_RoomSettingsDialog_warningIcon", , diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 4592d921a9..1f1a8d1523 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -33,9 +33,21 @@ import * as sdk from "../../../index"; import SdkConfig from "../../../SdkConfig"; import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab"; +export const USER_GENERAL_TAB = "USER_GENERAL_TAB"; +export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB"; +export const USER_FLAIR_TAB = "USER_FLAIR_TAB"; +export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB"; +export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB"; +export const USER_VOICE_TAB = "USER_VOICE_TAB"; +export const USER_SECURITY_TAB = "USER_SECURITY_TAB"; +export const USER_LABS_TAB = "USER_LABS_TAB"; +export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB"; +export const USER_HELP_TAB = "USER_HELP_TAB"; + export default class UserSettingsDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, + initialTabId: PropTypes.string, }; constructor() { @@ -63,42 +75,50 @@ export default class UserSettingsDialog extends React.Component { const tabs = []; tabs.push(new Tab( + USER_GENERAL_TAB, _td("General"), "mx_UserSettingsDialog_settingsIcon", , )); tabs.push(new Tab( + USER_APPEARANCE_TAB, _td("Appearance"), "mx_UserSettingsDialog_appearanceIcon", , )); tabs.push(new Tab( + USER_FLAIR_TAB, _td("Flair"), "mx_UserSettingsDialog_flairIcon", , )); tabs.push(new Tab( + USER_NOTIFICATIONS_TAB, _td("Notifications"), "mx_UserSettingsDialog_bellIcon", , )); tabs.push(new Tab( + USER_PREFERENCES_TAB, _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", , )); tabs.push(new Tab( + USER_VOICE_TAB, _td("Voice & Video"), "mx_UserSettingsDialog_voiceIcon", , )); tabs.push(new Tab( + USER_SECURITY_TAB, _td("Security & Privacy"), "mx_UserSettingsDialog_securityIcon", , )); if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) { tabs.push(new Tab( + USER_LABS_TAB, _td("Labs"), "mx_UserSettingsDialog_labsIcon", , @@ -106,12 +126,14 @@ export default class UserSettingsDialog extends React.Component { } if (this.state.mjolnirEnabled) { tabs.push(new Tab( + USER_MJOLNIR_TAB, _td("Ignored users"), "mx_UserSettingsDialog_mjolnirIcon", , )); } tabs.push(new Tab( + USER_HELP_TAB, _td("Help & About"), "mx_UserSettingsDialog_helpIcon", , @@ -127,7 +149,7 @@ export default class UserSettingsDialog extends React.Component {
        - +
        ); diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 60ef61a6e9..c9b5d9e3ad 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -36,6 +36,7 @@ export enum Action { /** * Open the user settings. No additional payload information required. + * Optionally can include an OpenToTabPayload. */ ViewUserSettings = "view_user_settings", diff --git a/src/dispatcher/payloads/OpenToTabPayload.ts b/src/dispatcher/payloads/OpenToTabPayload.ts new file mode 100644 index 0000000000..2877ee053e --- /dev/null +++ b/src/dispatcher/payloads/OpenToTabPayload.ts @@ -0,0 +1,27 @@ +/* +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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface OpenToTabPayload extends ActionPayload { + action: Action.ViewUserSettings | string, // TODO: Add room settings action + + /** + * The tab ID to open in the settings view to start, if possible. + */ + initialTabId?: string; +} From acf78ae475799d955eac3b03017a0278c29ce600 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 09:04:43 -0600 Subject: [PATCH 5/9] Wire up the remaining dialogs --- src/components/structures/UserMenuButton.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 827a279d98..35b5cc4d4e 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -25,6 +25,9 @@ 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"; interface IProps { } @@ -95,6 +98,7 @@ export default class UserMenuButton extends React.Component { ev.preventDefault(); ev.stopPropagation(); + // TODO: Archived room view (deferred) console.log("TODO: Show archived rooms"); }; @@ -102,14 +106,16 @@ export default class UserMenuButton extends React.Component { ev.preventDefault(); ev.stopPropagation(); - console.log("TODO: Show feedback"); + Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); + this.setState({menuDisplayed: false}); // also close the menu }; private onSignOutClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); - console.log("TODO: Sign out"); + Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); + this.setState({menuDisplayed: false}); // also close the menu }; public render() { From 94ce23aa4b9e835be45df49d4889f9639592b3b4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 09:24:08 -0600 Subject: [PATCH 6/9] Wire up theme changer --- src/components/structures/UserMenuButton.tsx | 31 ++++++++++++++++++-- src/theme.js | 2 +- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 35b5cc4d4e..40b53a35fb 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -28,6 +28,8 @@ 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"; interface IProps { } @@ -35,10 +37,12 @@ interface IProps { interface IState { user: User; 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) { @@ -47,6 +51,7 @@ export default class UserMenuButton extends React.Component { this.state = { menuDisplayed: false, user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()), + isDarkTheme: this.isUserOnDarkTheme(), }; } @@ -62,8 +67,26 @@ export default class UserMenuButton extends React.Component { 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); + } + + private isUserOnDarkTheme(): boolean { + const theme = SettingsStore.getValue("theme"); + if (theme.startsWith("custom-")) { + return getCustomTheme(theme.substring(0, 7)).is_dark; + } + return theme === "dark"; + } + + private onThemeChanged = () => { + this.setState({isDarkTheme: this.isUserOnDarkTheme()}); + }; + private onAction = (ev: ActionPayload) => { if (ev.action !== Action.ToggleUserMenu) return; // not interested @@ -82,7 +105,11 @@ export default class UserMenuButton extends React.Component { }; private onSwitchThemeClick = () => { - console.log("TODO: Switch theme"); + // 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.ACCOUNT, newTheme); }; private onSettingsOpen = (ev: React.MouseEvent, tabId: string) => { @@ -142,7 +169,7 @@ export default class UserMenuButton extends React.Component {
        Date: Mon, 8 Jun 2020 09:32:16 -0600 Subject: [PATCH 7/9] Add hosting link --- res/css/structures/_UserMenuButton.scss | 5 ++++ src/components/structures/UserMenuButton.tsx | 27 +++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index aca5f4253a..0d35c1cbb2 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -42,6 +42,11 @@ limitations under the License. display: flex; align-items: center; + &:nth-child(n + 1) { + // The first header will have appropriate padding, subsequent ones need a margin. + margin-top: 10px; + } + .mx_UserMenuButton_contextMenu_name { // Create another flexbox of columns to handle large user IDs display: flex; diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 40b53a35fb..dc4415ea54 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -30,6 +30,7 @@ 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"; interface IProps { } @@ -148,6 +149,28 @@ export default class UserMenuButton extends React.Component { 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} + ), + }, + )} +
        + ); + } + const elementRect = this.buttonRef.current.getBoundingClientRect(); contextMenu = ( { />
      -
      - TODO: Upgrade prompt -
      + {hostingLink}
      • From fd8c056200723a3a9e1aa82e5436e93aa1e16b09 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 09:40:03 -0600 Subject: [PATCH 8/9] Fix i18n --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8575b3a258..3520446c2b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2042,6 +2042,7 @@ "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", + "Switch to light mode": "Switch to light mode", "Switch to dark mode": "Switch to dark mode", "Switch theme": "Switch theme", "Security & privacy": "Security & privacy", From de4c2fe3d9b2fb09e07daf0ff9190d7f838a98a6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 11:06:21 -0600 Subject: [PATCH 9/9] Use real buttons in user menu --- res/css/structures/_UserMenuButton.scss | 2 +- src/components/structures/UserMenuButton.tsx | 25 ++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 0d35c1cbb2..1f4183f8d6 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -129,7 +129,7 @@ limitations under the License. margin: 0; padding: 20px 0 0; - a { + .mx_AccessibleButton { text-decoration: none; color: $primary-fg-color; font-size: $font-15px; diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index dc4415ea54..d8f96d4a91 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -31,6 +31,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog"; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import {getCustomTheme} from "../../theme"; import {getHostingLink} from "../../utils/HostingLink"; +import AccessibleButton from "../views/elements/AccessibleButton"; interface IProps { } @@ -205,44 +206,44 @@ export default class UserMenuButton extends React.Component {