Merge pull request #4241 from matrix-org/t3chguy/shortcuts1
Improve Keyboard Shortcuts. Add alt-arrows & alt-shift-arrowspull/21833/head
						commit
						06cc710fb0
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -87,12 +87,6 @@ const shortcuts: Record<Categories, IShortcut[]> = { | |||
|                 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<Categories, IShortcut[]> = { | |||
|                 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<Categories, IShortcut[]> = { | |||
|                 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<Categories, IShortcut[]> = { | |||
|     ], | ||||
| }; | ||||
| 
 | ||||
| const categoryOrder = [ | ||||
|     Categories.COMPOSER, | ||||
|     Categories.CALLS, | ||||
|     Categories.ROOM_LIST, | ||||
|     Categories.AUTOCOMPLETE, | ||||
|     Categories.NAVIGATION, | ||||
| ]; | ||||
| 
 | ||||
| interface IModal { | ||||
|     close: () => void; | ||||
|     finished: Promise<any[]>; | ||||
|  | @ -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 <div className="mx_KeyboardShortcutsDialog_category" key={category}> | ||||
|             <h3>{_t(category)}</h3> | ||||
|             <div>{list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />)}</div> | ||||
|  |  | |||
|  | @ -380,11 +380,23 @@ const LoggedInView = createReactClass({ | |||
|                 break; | ||||
| 
 | ||||
|             case Key.SLASH: | ||||
|                 if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { | ||||
|                 if (ctrlCmdOnly) { | ||||
|                     KeyboardShortcuts.toggleDialog(); | ||||
|                     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) { | ||||
|  |  | |||
|  | @ -111,21 +111,30 @@ export default class RoomSubList extends React.PureComponent { | |||
|     } | ||||
| 
 | ||||
|     onAction = (payload) => { | ||||
|         // XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
 | ||||
|         // but this is no longer true, so we must do it here (and can apply the small
 | ||||
|         // optimisation of checking that we care about the room being read).
 | ||||
|         //
 | ||||
|         // Ultimately we need to transition to a state pushing flow where something
 | ||||
|         // explicitly notifies the components concerned that the notif count for a room
 | ||||
|         // has change (e.g. a Flux store).
 | ||||
|         if (payload.action === 'on_room_read' && | ||||
|             this.props.list.some((r) => r.roomId === payload.roomId) | ||||
|         ) { | ||||
|             this.forceUpdate(); | ||||
|         switch (payload.action) { | ||||
|             case 'on_room_read': | ||||
|                 // XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
 | ||||
|                 // but this is no longer true, so we must do it here (and can apply the small
 | ||||
|                 // optimisation of checking that we care about the room being read).
 | ||||
|                 //
 | ||||
|                 // Ultimately we need to transition to a state pushing flow where something
 | ||||
|                 // explicitly notifies the components concerned that the notif count for a room
 | ||||
|                 // has change (e.g. a Flux store).
 | ||||
|                 if (this.props.list.some((r) => r.roomId === payload.roomId)) { | ||||
|                     this.forceUpdate(); | ||||
|                 } | ||||
|                 break; | ||||
| 
 | ||||
|             case 'view_room': | ||||
|                 if (this.state.hidden && !this.props.forceExpand && | ||||
|                     this.props.list.some((r) => r.roomId === payload.room_id) | ||||
|                 ) { | ||||
|                     this.toggle(); | ||||
|                 } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     onClick = (ev) => { | ||||
|     toggle = () => { | ||||
|         if (this.isCollapsibleOnClick()) { | ||||
|             // The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
 | ||||
|             const isHidden = !this.state.hidden; | ||||
|  | @ -138,6 +147,10 @@ export default class RoomSubList extends React.PureComponent { | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     onClick = (ev) => { | ||||
|         this.toggle(); | ||||
|     }; | ||||
| 
 | ||||
|     onHeaderKeyDown = (ev) => { | ||||
|         switch (ev.key) { | ||||
|             case Key.ARROW_LEFT: | ||||
|  |  | |||
|  | @ -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,54 @@ 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, | ||||
|                     ...[].concat.apply([], shownCustomTagRooms), // eslint-disable-line prefer-spread
 | ||||
|                     ...lowPriorityRooms, | ||||
|                     ...historicalRooms, | ||||
|                     ...serverNoticeRooms, | ||||
|                 ], this.props.searchFilter); | ||||
| 
 | ||||
|                 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); | ||||
|                 // use slice to account for looping around the start
 | ||||
|                 const [room] = rooms.slice((currentIndex + payload.delta) % rooms.length); | ||||
|                 if (room) { | ||||
|                     dis.dispatch({ | ||||
|                         action: 'view_room', | ||||
|                         room_id: room.roomId, | ||||
|                     }); | ||||
|                 } | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|  |  | |||
|  | @ -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,15 +225,34 @@ 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 shortcut access
 | ||||
|                 if (payload.room_id === this.props.room.roomId) { | ||||
|                     this._scrollIntoView(); | ||||
|                 } | ||||
|                 break; | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _scrollIntoView: function() { | ||||
|         if (!this._roomTile.current) return; | ||||
|         this._roomTile.current.scrollIntoView({ | ||||
|             block: "nearest", | ||||
|             behavior: "auto", | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     _onActiveRoomChange: function() { | ||||
|         this.setState({ | ||||
|             selected: this.props.room.roomId === RoomViewStore.getRoomId(), | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     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 */ | ||||
|  | @ -257,6 +276,11 @@ export default createReactClass({ | |||
|                 statusUser.on("User._unstable_statusMessage", this._onStatusMessageCommitted); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // when we're first rendered (or our sublist is expanded) make sure we are visible if we're active
 | ||||
|         if (this.state.selected) { | ||||
|             this._scrollIntoView(); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     componentWillUnmount: function() { | ||||
|  | @ -538,7 +562,7 @@ export default createReactClass({ | |||
|         } | ||||
| 
 | ||||
|         return <React.Fragment> | ||||
|             <RovingTabIndexWrapper> | ||||
|             <RovingTabIndexWrapper inputRef={this._roomTile}> | ||||
|                 {({onFocus, isActive, ref}) => | ||||
|                     <AccessibleButton | ||||
|                         onFocus={onFocus} | ||||
|  |  | |||
|  | @ -135,10 +135,12 @@ export default class SendMessageComposer extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     onVerticalArrow(e, up) { | ||||
|         if (e.ctrlKey || e.shiftKey || e.metaKey) return; | ||||
|         // arrows from an initial-caret composer navigates recent messages to edit
 | ||||
|         // ctrl-alt-arrows navigate send history
 | ||||
|         if (e.shiftKey || e.metaKey) return; | ||||
| 
 | ||||
|         const shouldSelectHistory = e.altKey; | ||||
|         const shouldEditLastMessage = !e.altKey && up && !RoomViewStore.getQuotingEvent(); | ||||
|         const shouldSelectHistory = e.altKey && e.ctrlKey; | ||||
|         const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !RoomViewStore.getQuotingEvent(); | ||||
| 
 | ||||
|         if (shouldSelectHistory) { | ||||
|             // Try select composer history
 | ||||
|  |  | |||
|  | @ -2195,10 +2195,10 @@ | |||
|     "Toggle Bold": "Toggle Bold", | ||||
|     "Toggle Italics": "Toggle Italics", | ||||
|     "Toggle Quote": "Toggle Quote", | ||||
|     "Toggle Markdown": "Toggle Markdown", | ||||
|     "New line": "New line", | ||||
|     "Navigate recent messages to edit": "Navigate recent messages to edit", | ||||
|     "Jump to start/end of the composer": "Jump to start/end of the composer", | ||||
|     "Navigate composer history": "Navigate composer history", | ||||
|     "Toggle microphone mute": "Toggle microphone mute", | ||||
|     "Toggle video on/off": "Toggle video on/off", | ||||
|     "Jump to room search": "Jump to room search", | ||||
|  | @ -2208,6 +2208,8 @@ | |||
|     "Expand room list section": "Expand room list section", | ||||
|     "Clear room list filter field": "Clear room list filter field", | ||||
|     "Scroll up/down in the timeline": "Scroll up/down in the timeline", | ||||
|     "Previous/next unread room or DM": "Previous/next unread room or DM", | ||||
|     "Previous/next room or DM": "Previous/next room or DM", | ||||
|     "Toggle the top left menu": "Toggle the top left menu", | ||||
|     "Close dialog or context menu": "Close dialog or context menu", | ||||
|     "Activate selected button": "Activate selected button", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski