diff --git a/res/css/views/dialogs/_KeyboardShortcutsDialog.scss b/res/css/views/dialogs/_KeyboardShortcutsDialog.scss index f529b11059..638cacd41f 100644 --- a/res/css/views/dialogs/_KeyboardShortcutsDialog.scss +++ b/res/css/views/dialogs/_KeyboardShortcutsDialog.scss @@ -21,7 +21,7 @@ limitations under the License. -webkit-box-direction: normal; flex-direction: column; margin-bottom: -50px; - max-height: 700px; // XXX: this may need adjusting when adding new shortcuts + max-height: 1100px; // XXX: this may need adjusting when adding new shortcuts .mx_KeyboardShortcutsDialog_category { width: 33.3333%; // 3 columns diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index 618ed4755a..9ffeb9f72c 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -87,12 +87,6 @@ const shortcuts: Record = { key: Key.GREATER_THAN, }], description: _td("Toggle Quote"), - }, { - keybinds: [{ - modifiers: [CMD_OR_CTRL], - key: Key.M, - }], - description: _td("Toggle Markdown"), }, { keybinds: [{ modifiers: [Modifiers.SHIFT], @@ -115,6 +109,15 @@ const shortcuts: Record = { key: Key.END, }], description: _td("Jump to start/end of the composer"), + }, { + keybinds: [{ + modifiers: [Modifiers.CONTROL, Modifiers.ALT], + key: Key.ARROW_UP, + }, { + modifiers: [Modifiers.CONTROL, Modifiers.ALT], + key: Key.ARROW_DOWN, + }], + description: _td("Navigate composer history"), }, ], @@ -179,6 +182,24 @@ const shortcuts: Record = { key: Key.PAGE_DOWN, }], description: _td("Scroll up/down in the timeline"), + }, { + keybinds: [{ + modifiers: [Modifiers.ALT, Modifiers.SHIFT], + key: Key.ARROW_UP, + }, { + modifiers: [Modifiers.ALT, Modifiers.SHIFT], + key: Key.ARROW_DOWN, + }], + description: _td("Previous/next unread room or DM"), + }, { + keybinds: [{ + modifiers: [Modifiers.ALT], + key: Key.ARROW_UP, + }, { + modifiers: [Modifiers.ALT], + key: Key.ARROW_DOWN, + }], + description: _td("Previous/next room or DM"), }, { keybinds: [{ modifiers: [CMD_OR_CTRL], @@ -223,6 +244,14 @@ const shortcuts: Record = { ], }; +const categoryOrder = [ + Categories.COMPOSER, + Categories.CALLS, + Categories.ROOM_LIST, + Categories.AUTOCOMPLETE, + Categories.NAVIGATION, +]; + interface IModal { close: () => void; finished: Promise; @@ -289,7 +318,8 @@ export const toggleDialog = () => { return; } - const sections = Object.entries(shortcuts).map(([category, list]) => { + const sections = categoryOrder.map(category => { + const list = shortcuts[category]; return

{_t(category)}

{list.map(shortcut => )}
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index f923c50b44..41fbd54991 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -385,6 +385,18 @@ const LoggedInView = createReactClass({ handled = true; } break; + + case Key.ARROW_UP: + case Key.ARROW_DOWN: + if (ev.altKey && !ev.ctrlKey && !ev.metaKey) { + dis.dispatch({ + action: 'view_room_delta', + delta: ev.key === Key.ARROW_UP ? -1 : 1, + unread: ev.shiftKey, + }); + handled = true; + } + break; } if (handled) { diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 2143c7f1a8..aa4c106bb6 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017, 2018 Vector Creations Ltd +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. @@ -40,6 +41,8 @@ import * as Receipt from "../../../utils/Receipt"; import {Resizer} from '../../../resizer'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex"; +import * as Unread from "../../../Unread"; +import RoomViewStore from "../../../stores/RoomViewStore"; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -242,6 +245,56 @@ export default createReactClass({ }); } break; + case 'view_room_delta': { + const currentRoomId = RoomViewStore.getRoomId(); + const { + "im.vector.fake.invite": inviteRooms, + "m.favourite": favouriteRooms, + [TAG_DM]: dmRooms, + "im.vector.fake.recent": recentRooms, + "m.lowpriority": lowPriorityRooms, + "im.vector.fake.archived": historicalRooms, + "m.server_notice": serverNoticeRooms, + ...tags + } = this.state.lists; + + const shownCustomTagRooms = Object.keys(tags).filter(tagName => { + return (!this.state.customTags || this.state.customTags[tagName]) && + !tagName.match(STANDARD_TAGS_REGEX); + }).map(tagName => tags[tagName]); + + // this order matches the one when generating the room sublists below. + let rooms = this._applySearchFilter([ + ...inviteRooms, + ...favouriteRooms, + ...dmRooms, + ...recentRooms, + // eslint-disable-next-line prefer-spread + ...[].concat.apply([], shownCustomTagRooms), + ...lowPriorityRooms, + ...historicalRooms, + ...serverNoticeRooms, + ], this.props.searchFilter); // TODO optimize + + if (payload.unread) { + // filter to only notification rooms (and our current active room so we can index properly) + rooms = rooms.filter(room => { + return room.roomId === currentRoomId || Unread.doesRoomHaveUnreadMessages(room); + }); + } + + const currentIndex = rooms.findIndex(room => room.roomId === currentRoomId); + + const [room] = rooms.slice((currentIndex + payload.delta) % rooms.length); + // console.log("DEBUG", currentIndex, room, rooms); + if (room) { + dis.dispatch({ + action: 'view_room', + room_id: room.roomId, + }); + } + break; + } } }, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index e1624602f1..d4ccd243cf 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; @@ -225,6 +225,16 @@ export default createReactClass({ case 'feature_custom_status_changed': this.forceUpdate(); break; + + case 'view_room': + // when the room is selected make sure its tile is visible, for breadcrumbs/keyboard shortcuts + if (payload.room_id === this.props.room.roomId && this._roomTile.current) { + this._roomTile.current.scrollIntoView({ + block: "nearest", + behavior: "auto", + }); + } + break; } }, @@ -234,6 +244,10 @@ export default createReactClass({ }); }, + UNSAFE_componentWillMount: function() { + this._roomTile = createRef(); + }, + componentDidMount: function() { /* We bind here rather than in the definition because otherwise we wind up with the method only being callable once every 500ms across all instances, which would be wrong */ @@ -538,7 +552,7 @@ export default createReactClass({ } return - + {({onFocus, isActive, ref}) =>