Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 06cc710

Browse files
authored
Merge pull request #4241 from matrix-org/t3chguy/shortcuts1
Improve Keyboard Shortcuts. Add alt-arrows & alt-shift-arrows
2 parents c9de12e + 8a0ee2f commit 06cc710

File tree

8 files changed

+161
-27
lines changed

8 files changed

+161
-27
lines changed

res/css/views/dialogs/_KeyboardShortcutsDialog.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ limitations under the License.
2121
-webkit-box-direction: normal;
2222
flex-direction: column;
2323
margin-bottom: -50px;
24-
max-height: 700px; // XXX: this may need adjusting when adding new shortcuts
24+
max-height: 1100px; // XXX: this may need adjusting when adding new shortcuts
2525

2626
.mx_KeyboardShortcutsDialog_category {
2727
width: 33.3333%; // 3 columns

src/accessibility/KeyboardShortcuts.tsx

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,6 @@ const shortcuts: Record<Categories, IShortcut[]> = {
8787
key: Key.GREATER_THAN,
8888
}],
8989
description: _td("Toggle Quote"),
90-
}, {
91-
keybinds: [{
92-
modifiers: [CMD_OR_CTRL],
93-
key: Key.M,
94-
}],
95-
description: _td("Toggle Markdown"),
9690
}, {
9791
keybinds: [{
9892
modifiers: [Modifiers.SHIFT],
@@ -115,6 +109,15 @@ const shortcuts: Record<Categories, IShortcut[]> = {
115109
key: Key.END,
116110
}],
117111
description: _td("Jump to start/end of the composer"),
112+
}, {
113+
keybinds: [{
114+
modifiers: [Modifiers.CONTROL, Modifiers.ALT],
115+
key: Key.ARROW_UP,
116+
}, {
117+
modifiers: [Modifiers.CONTROL, Modifiers.ALT],
118+
key: Key.ARROW_DOWN,
119+
}],
120+
description: _td("Navigate composer history"),
118121
},
119122
],
120123

@@ -179,6 +182,24 @@ const shortcuts: Record<Categories, IShortcut[]> = {
179182
key: Key.PAGE_DOWN,
180183
}],
181184
description: _td("Scroll up/down in the timeline"),
185+
}, {
186+
keybinds: [{
187+
modifiers: [Modifiers.ALT, Modifiers.SHIFT],
188+
key: Key.ARROW_UP,
189+
}, {
190+
modifiers: [Modifiers.ALT, Modifiers.SHIFT],
191+
key: Key.ARROW_DOWN,
192+
}],
193+
description: _td("Previous/next unread room or DM"),
194+
}, {
195+
keybinds: [{
196+
modifiers: [Modifiers.ALT],
197+
key: Key.ARROW_UP,
198+
}, {
199+
modifiers: [Modifiers.ALT],
200+
key: Key.ARROW_DOWN,
201+
}],
202+
description: _td("Previous/next room or DM"),
182203
}, {
183204
keybinds: [{
184205
modifiers: [CMD_OR_CTRL],
@@ -223,6 +244,14 @@ const shortcuts: Record<Categories, IShortcut[]> = {
223244
],
224245
};
225246

247+
const categoryOrder = [
248+
Categories.COMPOSER,
249+
Categories.CALLS,
250+
Categories.ROOM_LIST,
251+
Categories.AUTOCOMPLETE,
252+
Categories.NAVIGATION,
253+
];
254+
226255
interface IModal {
227256
close: () => void;
228257
finished: Promise<any[]>;
@@ -289,7 +318,8 @@ export const toggleDialog = () => {
289318
return;
290319
}
291320

292-
const sections = Object.entries(shortcuts).map(([category, list]) => {
321+
const sections = categoryOrder.map(category => {
322+
const list = shortcuts[category];
293323
return <div className="mx_KeyboardShortcutsDialog_category" key={category}>
294324
<h3>{_t(category)}</h3>
295325
<div>{list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />)}</div>

src/components/structures/LoggedInView.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,11 +380,23 @@ const LoggedInView = createReactClass({
380380
break;
381381

382382
case Key.SLASH:
383-
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
383+
if (ctrlCmdOnly) {
384384
KeyboardShortcuts.toggleDialog();
385385
handled = true;
386386
}
387387
break;
388+
389+
case Key.ARROW_UP:
390+
case Key.ARROW_DOWN:
391+
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
392+
dis.dispatch({
393+
action: 'view_room_delta',
394+
delta: ev.key === Key.ARROW_UP ? -1 : 1,
395+
unread: ev.shiftKey,
396+
});
397+
handled = true;
398+
}
399+
break;
388400
}
389401

390402
if (handled) {

src/components/structures/RoomSubList.js

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,21 +111,30 @@ export default class RoomSubList extends React.PureComponent {
111111
}
112112

113113
onAction = (payload) => {
114-
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
115-
// but this is no longer true, so we must do it here (and can apply the small
116-
// optimisation of checking that we care about the room being read).
117-
//
118-
// Ultimately we need to transition to a state pushing flow where something
119-
// explicitly notifies the components concerned that the notif count for a room
120-
// has change (e.g. a Flux store).
121-
if (payload.action === 'on_room_read' &&
122-
this.props.list.some((r) => r.roomId === payload.roomId)
123-
) {
124-
this.forceUpdate();
114+
switch (payload.action) {
115+
case 'on_room_read':
116+
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
117+
// but this is no longer true, so we must do it here (and can apply the small
118+
// optimisation of checking that we care about the room being read).
119+
//
120+
// Ultimately we need to transition to a state pushing flow where something
121+
// explicitly notifies the components concerned that the notif count for a room
122+
// has change (e.g. a Flux store).
123+
if (this.props.list.some((r) => r.roomId === payload.roomId)) {
124+
this.forceUpdate();
125+
}
126+
break;
127+
128+
case 'view_room':
129+
if (this.state.hidden && !this.props.forceExpand &&
130+
this.props.list.some((r) => r.roomId === payload.room_id)
131+
) {
132+
this.toggle();
133+
}
125134
}
126135
};
127136

128-
onClick = (ev) => {
137+
toggle = () => {
129138
if (this.isCollapsibleOnClick()) {
130139
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
131140
const isHidden = !this.state.hidden;
@@ -138,6 +147,10 @@ export default class RoomSubList extends React.PureComponent {
138147
}
139148
};
140149

150+
onClick = (ev) => {
151+
this.toggle();
152+
};
153+
141154
onHeaderKeyDown = (ev) => {
142155
switch (ev.key) {
143156
case Key.ARROW_LEFT:

src/components/views/rooms/RoomList.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
Copyright 2015, 2016 OpenMarket Ltd
33
Copyright 2017, 2018 Vector Creations Ltd
4+
Copyright 2020 The Matrix.org Foundation C.I.C.
45
56
Licensed under the Apache License, Version 2.0 (the "License");
67
you may not use this file except in compliance with the License.
@@ -40,6 +41,8 @@ import * as Receipt from "../../../utils/Receipt";
4041
import {Resizer} from '../../../resizer';
4142
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
4243
import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex";
44+
import * as Unread from "../../../Unread";
45+
import RoomViewStore from "../../../stores/RoomViewStore";
4346

4447
const HIDE_CONFERENCE_CHANS = true;
4548
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@@ -242,6 +245,54 @@ export default createReactClass({
242245
});
243246
}
244247
break;
248+
case 'view_room_delta': {
249+
const currentRoomId = RoomViewStore.getRoomId();
250+
const {
251+
"im.vector.fake.invite": inviteRooms,
252+
"m.favourite": favouriteRooms,
253+
[TAG_DM]: dmRooms,
254+
"im.vector.fake.recent": recentRooms,
255+
"m.lowpriority": lowPriorityRooms,
256+
"im.vector.fake.archived": historicalRooms,
257+
"m.server_notice": serverNoticeRooms,
258+
...tags
259+
} = this.state.lists;
260+
261+
const shownCustomTagRooms = Object.keys(tags).filter(tagName => {
262+
return (!this.state.customTags || this.state.customTags[tagName]) &&
263+
!tagName.match(STANDARD_TAGS_REGEX);
264+
}).map(tagName => tags[tagName]);
265+
266+
// this order matches the one when generating the room sublists below.
267+
let rooms = this._applySearchFilter([
268+
...inviteRooms,
269+
...favouriteRooms,
270+
...dmRooms,
271+
...recentRooms,
272+
...[].concat.apply([], shownCustomTagRooms), // eslint-disable-line prefer-spread
273+
...lowPriorityRooms,
274+
...historicalRooms,
275+
...serverNoticeRooms,
276+
], this.props.searchFilter);
277+
278+
if (payload.unread) {
279+
// filter to only notification rooms (and our current active room so we can index properly)
280+
rooms = rooms.filter(room => {
281+
return room.roomId === currentRoomId || Unread.doesRoomHaveUnreadMessages(room);
282+
});
283+
}
284+
285+
const currentIndex = rooms.findIndex(room => room.roomId === currentRoomId);
286+
// use slice to account for looping around the start
287+
const [room] = rooms.slice((currentIndex + payload.delta) % rooms.length);
288+
if (room) {
289+
dis.dispatch({
290+
action: 'view_room',
291+
room_id: room.roomId,
292+
});
293+
}
294+
break;
295+
}
245296
}
246297
},
247298

src/components/views/rooms/RoomTile.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
1717
limitations under the License.
1818
*/
1919

20-
import React from 'react';
20+
import React, {createRef} from 'react';
2121
import PropTypes from 'prop-types';
2222
import createReactClass from 'create-react-class';
2323
import classNames from 'classnames';
@@ -225,15 +225,34 @@ export default createReactClass({
225225
case 'feature_custom_status_changed':
226226
this.forceUpdate();
227227
break;
228+
229+
case 'view_room':
230+
// when the room is selected make sure its tile is visible, for breadcrumbs/keyboard shortcut access
231+
if (payload.room_id === this.props.room.roomId) {
232+
this._scrollIntoView();
233+
}
234+
break;
228235
}
229236
},
230237

238+
_scrollIntoView: function() {
239+
if (!this._roomTile.current) return;
240+
this._roomTile.current.scrollIntoView({
241+
block: "nearest",
242+
behavior: "auto",
243+
});
244+
},
245+
231246
_onActiveRoomChange: function() {
232247
this.setState({
233248
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
234249
});
235250
},
236251

252+
UNSAFE_componentWillMount: function() {
253+
this._roomTile = createRef();
254+
},
255+
237256
componentDidMount: function() {
238257
/* We bind here rather than in the definition because otherwise we wind up with the
239258
method only being callable once every 500ms across all instances, which would be wrong */
@@ -257,6 +276,11 @@ export default createReactClass({
257276
statusUser.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
258277
}
259278
}
279+
280+
// when we're first rendered (or our sublist is expanded) make sure we are visible if we're active
281+
if (this.state.selected) {
282+
this._scrollIntoView();
283+
}
260284
},
261285

262286
componentWillUnmount: function() {
@@ -538,7 +562,7 @@ export default createReactClass({
538562
}
539563

540564
return <React.Fragment>
541-
<RovingTabIndexWrapper>
565+
<RovingTabIndexWrapper inputRef={this._roomTile}>
542566
{({onFocus, isActive, ref}) =>
543567
<AccessibleButton
544568
onFocus={onFocus}

src/components/views/rooms/SendMessageComposer.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,12 @@ export default class SendMessageComposer extends React.Component {
135135
}
136136

137137
onVerticalArrow(e, up) {
138-
if (e.ctrlKey || e.shiftKey || e.metaKey) return;
138+
// arrows from an initial-caret composer navigates recent messages to edit
139+
// ctrl-alt-arrows navigate send history
140+
if (e.shiftKey || e.metaKey) return;
139141

140-
const shouldSelectHistory = e.altKey;
141-
const shouldEditLastMessage = !e.altKey && up && !RoomViewStore.getQuotingEvent();
142+
const shouldSelectHistory = e.altKey && e.ctrlKey;
143+
const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !RoomViewStore.getQuotingEvent();
142144

143145
if (shouldSelectHistory) {
144146
// Try select composer history

src/i18n/strings/en_EN.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2195,10 +2195,10 @@
21952195
"Toggle Bold": "Toggle Bold",
21962196
"Toggle Italics": "Toggle Italics",
21972197
"Toggle Quote": "Toggle Quote",
2198-
"Toggle Markdown": "Toggle Markdown",
21992198
"New line": "New line",
22002199
"Navigate recent messages to edit": "Navigate recent messages to edit",
22012200
"Jump to start/end of the composer": "Jump to start/end of the composer",
2201+
"Navigate composer history": "Navigate composer history",
22022202
"Toggle microphone mute": "Toggle microphone mute",
22032203
"Toggle video on/off": "Toggle video on/off",
22042204
"Jump to room search": "Jump to room search",
@@ -2208,6 +2208,8 @@
22082208
"Expand room list section": "Expand room list section",
22092209
"Clear room list filter field": "Clear room list filter field",
22102210
"Scroll up/down in the timeline": "Scroll up/down in the timeline",
2211+
"Previous/next unread room or DM": "Previous/next unread room or DM",
2212+
"Previous/next room or DM": "Previous/next room or DM",
22112213
"Toggle the top left menu": "Toggle the top left menu",
22122214
"Close dialog or context menu": "Close dialog or context menu",
22132215
"Activate selected button": "Activate selected button",

0 commit comments

Comments
 (0)