Merge pull request #4241 from matrix-org/t3chguy/shortcuts1
Improve Keyboard Shortcuts. Add alt-arrows & alt-shift-arrows
This commit is contained in:
commit
06cc710fb0
8 changed files with 161 additions and 27 deletions
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue