diff --git a/res/css/_common.scss b/res/css/_common.scss index 77a8ff9f4a..e83c6aaeda 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -581,3 +581,118 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { // So it fits in the space provided by the page max-width: 120px; } + +// A context menu that largely fits the | [icon] [label] | format. +.mx_IconizedContextMenu { + // 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_IconizedContextMenu_optionList { + // the notFirst class is for cases where the optionList might be under a header of sorts. + &:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst { + 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; + + .mx_AccessibleButton { + 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, .mx_IconizedContextMenu_icon { // icons + width: 16px; + min-width: 16px; + max-width: 16px; + } + + span:last-child { // labels + padding-left: 14px; + width: 100%; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + } + } + + &.mx_IconizedContextMenu_compact { + > * { + padding-left: 11px; + padding-right: 16px; + + &:first-child { + padding-top: 13px; + } + + &:last-child { + padding-bottom: 13px; + } + } + + .mx_IconizedContextMenu_optionList { + &:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst { + margin-top: 10px; + + li:first-child { + padding-top: 10px; + } + } + + li:first-child { + padding-top: 0; + } + } + } +} diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 1f4183f8d6..a173e468e1 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -21,22 +21,6 @@ limitations under the License. .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; @@ -95,68 +79,4 @@ limitations under the License. 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; - - .mx_AccessibleButton { - 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/css/views/rooms/_NotificationBadge.scss b/res/css/views/rooms/_NotificationBadge.scss index 609e41c583..500a7b7e05 100644 --- a/res/css/views/rooms/_NotificationBadge.scss +++ b/res/css/views/rooms/_NotificationBadge.scss @@ -46,7 +46,7 @@ limitations under the License. width: 6px; height: 6px; border-radius: 6px; - margin-right: 18px; + margin-right: 8px; } &.mx_NotificationBadge_2char { diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index cfb9bc3b6d..b92200500a 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -25,9 +25,10 @@ limitations under the License. display: flex; flex-direction: column; - margin-left: 8px; + padding-left: 8px; margin-top: 12px; margin-bottom: 12px; + width: 100%; .mx_RoomSublist2_headerContainer { // Create a flexbox to make ordering easy diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 41c9469bc1..48389adfc8 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -18,7 +18,7 @@ limitations under the License. // Note: the room tile expects to be in a flexbox column container .mx_RoomTile2 { - width: calc(100% - 11px); // 8px for padding (4px on either side), 3px for margin + width: calc(100% - 21px); // 8px for padding (4px on either side), 3px for margin margin-bottom: 4px; margin-right: 3px; padding: 4px; @@ -27,7 +27,7 @@ limitations under the License. display: flex; flex-wrap: wrap; - &.mx_RoomTile2_selected { + &.mx_RoomTile2_selected, &:hover, &.mx_RoomTile2_hasMenuOpen { background-color: $roomtile2-selected-bg-color; border-radius: 32px; } @@ -37,6 +37,9 @@ limitations under the License. } .mx_RoomTile2_nameContainer { + flex-grow: 1; + max-width: calc(100% - 58px); // 32px avatar, 18px badge area, 8px margin on avatar + // Create a new column layout flexbox for the name parts display: flex; flex-direction: column; @@ -45,9 +48,13 @@ limitations under the License. .mx_RoomTile2_name, .mx_RoomTile2_messagePreview { margin: 0 2px; - } + width: 100%; - // TODO: Ellipsis on the name and preview + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } .mx_RoomTile2_name { font-size: $font-14px; @@ -66,7 +73,8 @@ limitations under the License. } .mx_RoomTile2_badgeContainer { - flex-grow: 1; + width: 18px; + height: 32px; // Create another flexbox row because it's super easy to position the badge at // the end this way. @@ -74,4 +82,97 @@ limitations under the License. align-items: center; justify-content: flex-end; } + + // The menu button is hidden by default + // TODO: [Notifications] Add your bell icon class here, similar to the following approach: + // https://github.com/matrix-org/matrix-react-sdk/blob/2180a56074f3698fc0241c309a72ba6cad802d1c/res/css/views/rooms/_RoomSublist2.scss#L48-L76 + // You'll need to do the same down below on the &:hover selector for the tile. + // ... also remove this 4 line TODO comment. + .mx_RoomTile2_menuButton { + width: 0; + height: 0; + visibility: hidden; + position: relative; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } + + .mx_RoomTile2_menuButton::before { + top: 8px; + left: -1px; // this is off-center to align it with the badges + mask-image: url('$(res)/img/feather-customised/more-horizontal.svg'); + } + + &:hover, &.mx_RoomTile2_hasMenuOpen { + // Hide the badge container on hover because it'll be a menu button + .mx_RoomTile2_badgeContainer { + width: 0; + height: 0; + visibility: hidden; + } + + .mx_RoomTile2_menuButton { + width: 18px; + height: 32px; + visibility: visible; + } + } +} + +.mx_RoomTile2_contextMenu { + .mx_RoomTile2_contextMenu_redRow { + .mx_AccessibleButton { + color: $warning-color !important; // !important to override styles from context menu + } + + .mx_IconizedContextMenu_icon::before { + background-color: $warning-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_RoomTile2_iconStar::before { + mask-image: url('$(res)/img/feather-customised/star.svg'); + } + + .mx_RoomTile2_iconArrowDown::before { + mask-image: url('$(res)/img/feather-customised/arrow-down.svg'); + } + + .mx_RoomTile2_iconUser::before { + mask-image: url('$(res)/img/feather-customised/user.svg'); + } + + .mx_RoomTile2_iconSettings::before { + mask-image: url('$(res)/img/feather-customised/settings.svg'); + } + + .mx_RoomTile2_iconSignOut::before { + mask-image: url('$(res)/img/feather-customised/sign-out.svg'); + } } diff --git a/res/img/feather-customised/arrow-down.svg b/res/img/feather-customised/arrow-down.svg new file mode 100644 index 0000000000..4f84f627bd --- /dev/null +++ b/res/img/feather-customised/arrow-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/feather-customised/star.svg b/res/img/feather-customised/star.svg new file mode 100644 index 0000000000..bcdc31aa47 --- /dev/null +++ b/res/img/feather-customised/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index d8f96d4a91..ff8491864a 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -180,7 +180,7 @@ export default class UserMenuButton extends React.Component { top={elementRect.top + elementRect.height} onFinished={this.onCloseMenu} > -
+
@@ -203,7 +203,7 @@ export default class UserMenuButton extends React.Component {
{hostingLink} -
+
  • this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> @@ -237,7 +237,7 @@ export default class UserMenuButton extends React.Component {
-
+
  • diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index d4f64e4571..7778b5be4b 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -27,6 +27,9 @@ import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import ActiveRoomObserver from "../../../ActiveRoomObserver"; import NotificationBadge, { INotificationState, NotificationColor, RoomNotificationState } from "./NotificationBadge"; +import { _t } from "../../../languageHandler"; +import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; +import { DefaultTagID, TagID } from "../../../stores/room-list/models"; /******************************************************************* * CAUTION * @@ -49,10 +52,12 @@ interface IState { hover: boolean; notificationState: INotificationState; selected: boolean; + generalMenuDisplayed: boolean; } export default class RoomTile2 extends React.Component { - private roomTile = createRef(); + private roomTileRef: React.RefObject = createRef(); + private generalMenuButtonRef: React.RefObject = createRef(); // TODO: Custom status // TODO: Lock icon @@ -72,6 +77,7 @@ export default class RoomTile2 extends React.Component { hover: false, notificationState: new RoomNotificationState(this.props.room), selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, + generalMenuDisplayed: false, }; ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); @@ -105,6 +111,124 @@ export default class RoomTile2 extends React.Component { this.setState({selected: isActive}); }; + private onGeneralMenuOpenClick = (ev: InputEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({generalMenuDisplayed: true}); + }; + + private onCloseGeneralMenu = (ev: InputEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({generalMenuDisplayed: false}); + }; + + private onTagRoom = (ev: React.MouseEvent, tagId: TagID) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (tagId === DefaultTagID.DM) { + // TODO: DM Flagging + } else { + // TODO: XOR favourites and low priority + } + }; + + private onLeaveRoomClick = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + dis.dispatch({ + action: 'leave_room', + room_id: this.props.room.roomId, + }); + this.setState({generalMenuDisplayed: false}); // hide the menu + }; + + private onOpenRoomSettings = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + dis.dispatch({ + action: 'open_room_settings', + room_id: this.props.room.roomId, + }); + this.setState({generalMenuDisplayed: false}); // hide the menu + }; + + private renderGeneralMenu(): React.ReactElement { + let contextMenu = null; + if (this.state.generalMenuDisplayed) { + // The context menu appears within the list, so use the room tile as a reference point + const elementRect = this.roomTileRef.current.getBoundingClientRect(); + contextMenu = ( + +
    +
    +
      +
    • + this.onTagRoom(e, DefaultTagID.Favourite)}> + + {_t("Favourite")} + +
    • +
    • + this.onTagRoom(e, DefaultTagID.LowPriority)}> + + {_t("Low Priority")} + +
    • +
    • + this.onTagRoom(e, DefaultTagID.DM)}> + + {_t("Direct Chat")} + +
    • +
    • + + + {_t("Settings")} + +
    • +
    +
    +
    +
      +
    • + + + {_t("Leave Room")} + +
    • +
    +
    +
    +
    + ); + } + + return ( + + + {contextMenu} + + ) + } + public render(): React.ReactElement { // TODO: Collapsed state // TODO: Invites @@ -114,6 +238,7 @@ export default class RoomTile2 extends React.Component { const classes = classNames({ 'mx_RoomTile2': true, 'mx_RoomTile2_selected': this.state.selected, + 'mx_RoomTile2_hasMenuOpen': this.state.generalMenuDisplayed, }); const badge = ; @@ -141,7 +266,7 @@ export default class RoomTile2 extends React.Component { const avatarSize = 32; return ( - + {({onFocus, isActive, ref}) => {
    {badge}
    + {this.renderGeneralMenu()}
    }
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8a1d112e5d..aca7795c4e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1142,6 +1142,11 @@ "%(count)s unread messages.|one": "1 unread message.", "Unread mentions.": "Unread mentions.", "Unread messages.": "Unread messages.", + "Favourite": "Favourite", + "Low Priority": "Low Priority", + "Direct Chat": "Direct Chat", + "Leave Room": "Leave Room", + "Room options": "Room options", "Add a topic": "Add a topic", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", @@ -1817,9 +1822,6 @@ "Mentions only": "Mentions only", "Leave": "Leave", "Forget": "Forget", - "Favourite": "Favourite", - "Low Priority": "Low Priority", - "Direct Chat": "Direct Chat", "Clear status": "Clear status", "Update status": "Update status", "Set status": "Set status",