diff --git a/res/css/_common.scss b/res/css/_common.scss index 0b2b861617..47aa295540 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -585,93 +585,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { max-width: 120px; } -// A context menu that largely fits the | [icon] [label] | format. -.mx_IconizedContextMenu { - min-width: 146px; - - .mx_IconizedContextMenu_optionList { - & > * { - padding-left: 20px; - padding-right: 20px; - } - - // the notFirst class is for cases where the optionList might be under a header of sorts. - &:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst { - // 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; - } - } - - // round the top corners of the top button for the hover effect to be bounded - &:first-child .mx_AccessibleButton:first-child { - border-radius: 8px 8px 0 0; // radius matches .mx_ContextualMenu - } - - // round the bottom corners of the bottom button for the hover effect to be bounded - &:last-child .mx_AccessibleButton:last-child { - border-radius: 0 0 8px 8px; // radius matches .mx_ContextualMenu - } - - .mx_AccessibleButton { - // pad the inside of the button so that the hover background is padded too - padding-top: 12px; - padding-bottom: 12px; - 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; - - &:hover { - background-color: $menu-selected-color; - } - - img, .mx_IconizedContextMenu_icon { // icons - width: 16px; - min-width: 16px; - max-width: 16px; - } - - span.mx_IconizedContextMenu_label { // labels - padding-left: 14px; - width: 100%; - flex: 1; - - // Ellipsize any text overflow - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - } - } - - &.mx_IconizedContextMenu_compact { - .mx_IconizedContextMenu_optionList > * { - padding: 8px 16px 8px 11px; - } - } -} - @define-mixin ProgressBarColour $colour { color: $colour; &::-moz-progress-bar { diff --git a/res/css/_components.scss b/res/css/_components.scss index fcc87e2061..0dc267e130 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -50,6 +50,7 @@ @import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/avatars/_PulsedAvatar.scss"; +@import "./views/context_menus/_IconizedContextMenu.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_RoomTileContextMenu.scss"; @import "./views/context_menus/_StatusMessageContextMenu.scss"; diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 81a10ee1d0..78795c85a2 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -89,15 +89,10 @@ limitations under the License. .mx_UserMenu_contextMenu { width: 247px; - .mx_UserMenu_contextMenu_redRow { + &.mx_IconizedContextMenu .mx_IconizedContextMenu_optionList_red { .mx_AccessibleButton { padding-top: 16px; padding-bottom: 16px; - color: $warning-color !important; // !important to override styles from context menu - } - - .mx_IconizedContextMenu_icon::before { - background-color: $warning-color; } } diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss new file mode 100644 index 0000000000..7913058995 --- /dev/null +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -0,0 +1,148 @@ +/* +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. +*/ + +// A context menu that largely fits the | [icon] [label] | format. +.mx_IconizedContextMenu { + min-width: 146px; + + .mx_IconizedContextMenu_optionList { + & > * { + padding-left: 20px; + padding-right: 20px; + } + + // the notFirst class is for cases where the optionList might be under a header of sorts. + &:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst { + // 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; + } + } + + // round the top corners of the top button for the hover effect to be bounded + &:first-child .mx_AccessibleButton:first-child { + border-radius: 8px 8px 0 0; // radius matches .mx_ContextualMenu + } + + // round the bottom corners of the bottom button for the hover effect to be bounded + &:last-child .mx_AccessibleButton:last-child { + border-radius: 0 0 8px 8px; // radius matches .mx_ContextualMenu + } + + .mx_AccessibleButton { + // pad the inside of the button so that the hover background is padded too + padding-top: 12px; + padding-bottom: 12px; + 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; + + &:hover { + background-color: $menu-selected-color; + } + + img, .mx_IconizedContextMenu_icon { // icons + width: 16px; + min-width: 16px; + max-width: 16px; + } + + span.mx_IconizedContextMenu_label { // labels + padding-left: 14px; + width: 100%; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + + .mx_IconizedContextMenu_icon { + position: relative; + width: 16px; + height: 16px; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } + + .mx_IconizedContextMenu_optionList_red { + .mx_AccessibleButton { + color: $warning-color !important; + } + + .mx_IconizedContextMenu_icon::before { + background-color: $warning-color; + } + } + + .mx_IconizedContextMenu_active { + &.mx_AccessibleButton, .mx_AccessibleButton { + color: $accent-color !important; + } + + .mx_IconizedContextMenu_icon::before { + background-color: $accent-color; + } + } + + &.mx_IconizedContextMenu_compact { + .mx_IconizedContextMenu_optionList > * { + padding: 8px 16px 8px 11px; + } + } + + .mx_IconizedContextMenu_checked { + margin-left: 16px; + margin-right: -5px; + + &::before { + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + } +} diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index f22228602d..8eca3f1efa 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -175,48 +175,8 @@ limitations under the License. .mx_RoomTile_iconBellMentions::before { mask-image: url('$(res)/img/element-icons/roomlist/notifications-dm.svg'); } -.mx_RoomTile_iconCheck::before { - mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); -} .mx_RoomTile_contextMenu { - .mx_RoomTile_contextMenu_redRow { - .mx_AccessibleButton { - color: $warning-color !important; // !important to override styles from context menu - } - - .mx_IconizedContextMenu_icon::before { - background-color: $warning-color; - } - } - - .mx_RoomTile_contextMenu_activeRow { - &.mx_AccessibleButton, .mx_AccessibleButton { - color: $accent-color !important; // !important to override styles from context menu - } - - .mx_IconizedContextMenu_icon::before { - background-color: $accent-color; - } - } - - .mx_IconizedContextMenu_icon { - position: relative; - width: 16px; - height: 16px; - - &::before { - content: ''; - width: 16px; - height: 16px; - position: absolute; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background: $primary-fg-color; - } - } - .mx_RoomTile_iconStar::before { mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg'); } diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index f1bd297730..587ae2cb6b 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -58,7 +58,7 @@ export enum ChevronFace { None = "none", } -interface IProps extends IPosition { +export interface IProps extends IPosition { menuWidth?: number; menuHeight?: number; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 3b3dde6f91..e782618872 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -20,7 +20,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { _t } from "../../languageHandler"; -import { ChevronFace, ContextMenu, ContextMenuButton, MenuItem } from "./ContextMenu"; +import { 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"; @@ -38,6 +38,10 @@ import BaseAvatar from '../views/avatars/BaseAvatar'; import classNames from "classnames"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { SettingLevel } from "../../settings/SettingLevel"; +import IconizedContextMenu, { + IconizedContextMenuOption, + IconizedContextMenuOptionList +} from "../views/context_menus/IconizedContextMenu"; interface IProps { isMinimized: boolean; @@ -50,19 +54,6 @@ interface IState { isDarkTheme: boolean; } -interface IMenuButtonProps { - iconClassName: string; - label: string; - onClick(ev: ButtonEvent); -} - -const MenuButton: React.FC = ({iconClassName, label, onClick}) => { - return - - {label} - ; -}; - export default class UserMenu extends React.Component { private dispatcherRef: string; private themeWatcherRef: string; @@ -226,7 +217,7 @@ export default class UserMenu extends React.Component { let homeButton = null; if (this.hasHomePage) { homeButton = ( - { ); } - return ( - -
-
-
- - {OwnProfileStore.instance.displayName} - - - {MatrixClientPeg.get().getUserId()} - -
- - {_t("Switch - -
- {hostingLink} -
- {homeButton} - this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} - /> - this.onSettingsOpen(e, USER_SECURITY_TAB)} - /> - this.onSettingsOpen(e, null)} - /> - {/* */} - -
-
- -
+ return +
+
+ + {OwnProfileStore.instance.displayName} + + + {MatrixClientPeg.get().getUserId()} +
- - ); + + {_t("Switch + +
+ {hostingLink} + + {homeButton} + this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} + /> + this.onSettingsOpen(e, USER_SECURITY_TAB)} + /> + this.onSettingsOpen(e, null)} + /> + {/* */} + + + + + +
; }; public render() { diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx new file mode 100644 index 0000000000..b3ca9fde6f --- /dev/null +++ b/src/components/views/context_menus/IconizedContextMenu.tsx @@ -0,0 +1,124 @@ +/* +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 React from "react"; +import classNames from "classnames"; + +import { + ChevronFace, + ContextMenu, + IProps as IContextMenuProps, + MenuItem, + MenuItemCheckbox, MenuItemRadio, +} from "../../structures/ContextMenu"; + +interface IProps extends IContextMenuProps { + className?: string; + compact?: boolean; +} + +interface IOptionListProps { + first?: boolean; + red?: boolean; + className?: string; +} + +interface IOptionProps extends React.ComponentProps { + iconClassName: string; +} + +interface ICheckboxProps extends React.ComponentProps { + iconClassName: string; +} + +interface IRadioProps extends React.ComponentProps { + iconClassName: string; +} + +export const IconizedContextMenuRadio: React.FC = ({ + label, + iconClassName, + active, + className, + ...props +}) => { + return + + {label} + {active && } + ; +}; + +export const IconizedContextMenuCheckbox: React.FC = ({ + label, + iconClassName, + active, + className, + ...props +}) => { + return + + {label} + {active && } + ; +}; + +export const IconizedContextMenuOption: React.FC = ({label, iconClassName, ...props}) => { + return + + {label} + ; +}; + +export const IconizedContextMenuOptionList: React.FC = ({first, red, className, children}) => { + const classes = classNames("mx_IconizedContextMenu_optionList", className, { + mx_IconizedContextMenu_optionList_notFirst: !first, + mx_IconizedContextMenu_optionList_red: red, + }); + + return
+ {children} +
; +}; + +const IconizedContextMenu: React.FC = ({className, children, compact, ...props}) => { + const classes = classNames("mx_IconizedContextMenu", className, { + mx_IconizedContextMenu_compact: compact, + }); + + return +
+ { children } +
+
; +}; + +export default IconizedContextMenu; + diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 02aa915fa5..7c5c5b469b 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -27,14 +27,7 @@ import defaultDispatcher from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import ActiveRoomObserver from "../../../ActiveRoomObserver"; import { _t } from "../../../languageHandler"; -import { - ChevronFace, - ContextMenu, - ContextMenuTooltipButton, - MenuItem, - MenuItemCheckbox, - MenuItemRadio, -} from "../../structures/ContextMenu"; +import { ChevronFace, ContextMenuTooltipButton, MenuItemRadio } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; @@ -51,6 +44,11 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { EchoChamber } from "../../../stores/local-echo/EchoChamber"; import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber"; import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber"; +import IconizedContextMenu, { + IconizedContextMenuCheckbox, + IconizedContextMenuOption, + IconizedContextMenuOptionList, IconizedContextMenuRadio +} from "../context_menus/IconizedContextMenu"; interface IProps { room: Room; @@ -78,32 +76,6 @@ const contextMenuBelow = (elementRect: PartialDOMRect) => { return {left, top, chevronFace}; }; -interface INotifOptionProps { - active: boolean; - iconClassName: string; - label: string; - onClick(ev: ButtonEvent); -} - -const NotifOption: React.FC = ({active, onClick, iconClassName, label}) => { - const classes = classNames({ - mx_RoomTile_contextMenu_activeRow: active, - }); - - let activeIcon; - if (active) { - activeIcon = ; - } - - return ( - - - { label } - { activeIcon } - - ); -}; - export default class RoomTile extends React.PureComponent { private dispatcherRef: string; private roomTileRef = createRef(); @@ -335,38 +307,39 @@ export default class RoomTile extends React.PureComponent { let contextMenu = null; if (this.state.notificationsMenuPosition) { - contextMenu = ( - -
-
- - - - -
-
-
- ); + contextMenu = + + + + + + + ; } const classes = classNames("mx_RoomTile_notificationsButton", { @@ -400,18 +373,20 @@ export default class RoomTile extends React.PureComponent { let contextMenu = null; if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) { - contextMenu = ( - -
-
- - - {_t("Forget Room")} - -
-
-
- ); + contextMenu = + + + + ; } else if (this.state.generalMenuPosition) { const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room); @@ -421,42 +396,40 @@ export default class RoomTile extends React.PureComponent { const isLowPriority = roomTags.includes(DefaultTagID.LowPriority); const lowPriorityLabel = _t("Low Priority"); - contextMenu = ( - -
-
- this.onTagRoom(e, DefaultTagID.Favourite)} - active={isFavorite} - label={favouriteLabel} - > - - {favouriteLabel} - - this.onTagRoom(e, DefaultTagID.LowPriority)} - active={isLowPriority} - label={lowPriorityLabel} - > - - {lowPriorityLabel} - - - - {_t("Settings")} - -
-
- - - {_t("Leave Room")} - -
-
-
- ); + contextMenu = + + this.onTagRoom(e, DefaultTagID.Favourite)} + active={isFavorite} + label={favouriteLabel} + iconClassName="mx_RoomTile_iconStar" + /> + this.onTagRoom(e, DefaultTagID.LowPriority)} + active={isLowPriority} + label={lowPriorityLabel} + iconClassName="mx_RoomTile_iconArrowDown" + /> + + + + + + + ; } return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d25e136747..974a96406f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1179,11 +1179,11 @@ "All messages": "All messages", "Mentions & Keywords": "Mentions & Keywords", "Notification options": "Notification options", - "Leave Room": "Leave Room", "Forget Room": "Forget Room", "Favourited": "Favourited", "Favourite": "Favourite", "Low Priority": "Low Priority", + "Leave Room": "Leave Room", "Room options": "Room options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", "%(count)s unread messages including mentions.|one": "1 unread mention.",