Merge pull request #4241 from matrix-org/t3chguy/shortcuts1

Improve Keyboard Shortcuts. Add alt-arrows & alt-shift-arrows
This commit is contained in:
Michael Telatynski 2020-03-20 16:09:46 +00:00 committed by GitHub
commit 06cc710fb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 161 additions and 27 deletions

View file

@ -21,7 +21,7 @@ limitations under the License.
-webkit-box-direction: normal; -webkit-box-direction: normal;
flex-direction: column; flex-direction: column;
margin-bottom: -50px; 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 { .mx_KeyboardShortcutsDialog_category {
width: 33.3333%; // 3 columns width: 33.3333%; // 3 columns

View file

@ -87,12 +87,6 @@ const shortcuts: Record<Categories, IShortcut[]> = {
key: Key.GREATER_THAN, key: Key.GREATER_THAN,
}], }],
description: _td("Toggle Quote"), description: _td("Toggle Quote"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.M,
}],
description: _td("Toggle Markdown"),
}, { }, {
keybinds: [{ keybinds: [{
modifiers: [Modifiers.SHIFT], modifiers: [Modifiers.SHIFT],
@ -115,6 +109,15 @@ const shortcuts: Record<Categories, IShortcut[]> = {
key: Key.END, key: Key.END,
}], }],
description: _td("Jump to start/end of the composer"), 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, key: Key.PAGE_DOWN,
}], }],
description: _td("Scroll up/down in the timeline"), 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: [{ keybinds: [{
modifiers: [CMD_OR_CTRL], 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 { interface IModal {
close: () => void; close: () => void;
finished: Promise<any[]>; finished: Promise<any[]>;
@ -289,7 +318,8 @@ export const toggleDialog = () => {
return; 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}> return <div className="mx_KeyboardShortcutsDialog_category" key={category}>
<h3>{_t(category)}</h3> <h3>{_t(category)}</h3>
<div>{list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />)}</div> <div>{list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />)}</div>

View file

@ -380,11 +380,23 @@ const LoggedInView = createReactClass({
break; break;
case Key.SLASH: case Key.SLASH:
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { if (ctrlCmdOnly) {
KeyboardShortcuts.toggleDialog(); KeyboardShortcuts.toggleDialog();
handled = true; handled = true;
} }
break; 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) { if (handled) {

View file

@ -111,21 +111,30 @@ export default class RoomSubList extends React.PureComponent {
} }
onAction = (payload) => { onAction = (payload) => {
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched, switch (payload.action) {
// but this is no longer true, so we must do it here (and can apply the small case 'on_room_read':
// optimisation of checking that we care about the room being 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
// Ultimately we need to transition to a state pushing flow where something // optimisation of checking that we care about the room being read).
// explicitly notifies the components concerned that the notif count for a room //
// has change (e.g. a Flux store). // Ultimately we need to transition to a state pushing flow where something
if (payload.action === 'on_room_read' && // explicitly notifies the components concerned that the notif count for a room
this.props.list.some((r) => r.roomId === payload.roomId) // has change (e.g. a Flux store).
) { if (this.props.list.some((r) => r.roomId === payload.roomId)) {
this.forceUpdate(); 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()) { if (this.isCollapsibleOnClick()) {
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic // The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
const isHidden = !this.state.hidden; const isHidden = !this.state.hidden;
@ -138,6 +147,10 @@ export default class RoomSubList extends React.PureComponent {
} }
}; };
onClick = (ev) => {
this.toggle();
};
onHeaderKeyDown = (ev) => { onHeaderKeyDown = (ev) => {
switch (ev.key) { switch (ev.key) {
case Key.ARROW_LEFT: case Key.ARROW_LEFT:

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 Vector Creations 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 {Resizer} from '../../../resizer';
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex"; import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex";
import * as Unread from "../../../Unread";
import RoomViewStore from "../../../stores/RoomViewStore";
const HIDE_CONFERENCE_CHANS = true; const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@ -242,6 +245,54 @@ export default createReactClass({
}); });
} }
break; 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;
}
} }
}, },

View file

@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, {createRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import classNames from 'classnames'; import classNames from 'classnames';
@ -225,15 +225,34 @@ export default createReactClass({
case 'feature_custom_status_changed': case 'feature_custom_status_changed':
this.forceUpdate(); this.forceUpdate();
break; 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() { _onActiveRoomChange: function() {
this.setState({ this.setState({
selected: this.props.room.roomId === RoomViewStore.getRoomId(), selected: this.props.room.roomId === RoomViewStore.getRoomId(),
}); });
}, },
UNSAFE_componentWillMount: function() {
this._roomTile = createRef();
},
componentDidMount: function() { componentDidMount: function() {
/* We bind here rather than in the definition because otherwise we wind up with the /* 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 */ 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); 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() { componentWillUnmount: function() {
@ -538,7 +562,7 @@ export default createReactClass({
} }
return <React.Fragment> return <React.Fragment>
<RovingTabIndexWrapper> <RovingTabIndexWrapper inputRef={this._roomTile}>
{({onFocus, isActive, ref}) => {({onFocus, isActive, ref}) =>
<AccessibleButton <AccessibleButton
onFocus={onFocus} onFocus={onFocus}

View file

@ -135,10 +135,12 @@ export default class SendMessageComposer extends React.Component {
} }
onVerticalArrow(e, up) { 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 shouldSelectHistory = e.altKey && e.ctrlKey;
const shouldEditLastMessage = !e.altKey && up && !RoomViewStore.getQuotingEvent(); const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !RoomViewStore.getQuotingEvent();
if (shouldSelectHistory) { if (shouldSelectHistory) {
// Try select composer history // Try select composer history

View file

@ -2195,10 +2195,10 @@
"Toggle Bold": "Toggle Bold", "Toggle Bold": "Toggle Bold",
"Toggle Italics": "Toggle Italics", "Toggle Italics": "Toggle Italics",
"Toggle Quote": "Toggle Quote", "Toggle Quote": "Toggle Quote",
"Toggle Markdown": "Toggle Markdown",
"New line": "New line", "New line": "New line",
"Navigate recent messages to edit": "Navigate recent messages to edit", "Navigate recent messages to edit": "Navigate recent messages to edit",
"Jump to start/end of the composer": "Jump to start/end of the composer", "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 microphone mute": "Toggle microphone mute",
"Toggle video on/off": "Toggle video on/off", "Toggle video on/off": "Toggle video on/off",
"Jump to room search": "Jump to room search", "Jump to room search": "Jump to room search",
@ -2208,6 +2208,8 @@
"Expand room list section": "Expand room list section", "Expand room list section": "Expand room list section",
"Clear room list filter field": "Clear room list filter field", "Clear room list filter field": "Clear room list filter field",
"Scroll up/down in the timeline": "Scroll up/down in the timeline", "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", "Toggle the top left menu": "Toggle the top left menu",
"Close dialog or context menu": "Close dialog or context menu", "Close dialog or context menu": "Close dialog or context menu",
"Activate selected button": "Activate selected button", "Activate selected button": "Activate selected button",