Implement hotkeys for web UI (#5164)
* Fix #2102 - Implement hotkeys Hotkeys on status list: - r to reply - m to mention author - f to favourite - b to boost - enter to open status - p to open author's profile - up or k to move up in the list - down or j to move down in the list - 1-9 to focus a status in one of the columns - n to focus the compose textarea - alt+n to start a brand new toot - backspace to navigate back * Add navigational hotkeys The key g followed by: - s: start - h: home - n: notifications - l: local timeline - t: federated timeline - f: favourites - u: own profile - p: pinned toots - b: blocked users - m: muted users * Add hotkey for focusing search, make escape un-focus compose/search * Fix focusing notifications column, fix hotkeys in compose textarea
This commit is contained in:
parent
49cc0eb3e7
commit
7db0f8dcb2
16 changed files with 627 additions and 150 deletions
|
@ -28,6 +28,7 @@ import StatusContainer from '../../containers/status_container';
|
|||
import { openModal } from '../../actions/modal';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
|
@ -151,8 +152,100 @@ export default class Status extends ImmutablePureComponent {
|
|||
this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
|
||||
}
|
||||
|
||||
handleHotkeyMoveUp = () => {
|
||||
this.handleMoveUp(this.props.status.get('id'));
|
||||
}
|
||||
|
||||
handleHotkeyMoveDown = () => {
|
||||
this.handleMoveDown(this.props.status.get('id'));
|
||||
}
|
||||
|
||||
handleHotkeyReply = e => {
|
||||
e.preventDefault();
|
||||
this.handleReplyClick(this.props.status);
|
||||
}
|
||||
|
||||
handleHotkeyFavourite = () => {
|
||||
this.handleFavouriteClick(this.props.status);
|
||||
}
|
||||
|
||||
handleHotkeyBoost = () => {
|
||||
this.handleReblogClick(this.props.status);
|
||||
}
|
||||
|
||||
handleHotkeyMention = e => {
|
||||
e.preventDefault();
|
||||
this.handleMentionClick(this.props.status);
|
||||
}
|
||||
|
||||
handleHotkeyOpenProfile = () => {
|
||||
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
|
||||
}
|
||||
|
||||
handleMoveUp = id => {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
|
||||
if (id === status.get('id')) {
|
||||
this._selectChild(ancestorsIds.size - 1);
|
||||
} else {
|
||||
let index = ancestorsIds.indexOf(id);
|
||||
|
||||
if (index === -1) {
|
||||
index = descendantsIds.indexOf(id);
|
||||
this._selectChild(ancestorsIds.size + index);
|
||||
} else {
|
||||
this._selectChild(index - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMoveDown = id => {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
|
||||
if (id === status.get('id')) {
|
||||
this._selectChild(ancestorsIds.size + 1);
|
||||
} else {
|
||||
let index = ancestorsIds.indexOf(id);
|
||||
|
||||
if (index === -1) {
|
||||
index = descendantsIds.indexOf(id);
|
||||
this._selectChild(ancestorsIds.size + index + 2);
|
||||
} else {
|
||||
this._selectChild(index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_selectChild (index) {
|
||||
const element = this.node.querySelectorAll('.focusable')[index];
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
renderChildren (list) {
|
||||
return list.map(id => <StatusContainer key={id} id={id} />);
|
||||
return list.map(id => (
|
||||
<StatusContainer
|
||||
key={id}
|
||||
id={id}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const { ancestorsIds } = this.props;
|
||||
|
||||
if (ancestorsIds) {
|
||||
const element = this.node.querySelectorAll('.focusable')[this.props.ancestorsIds.size];
|
||||
element.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -176,34 +269,48 @@ export default class Status extends ImmutablePureComponent {
|
|||
descendants = <div>{this.renderChildren(descendantsIds)}</div>;
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
moveUp: this.handleHotkeyMoveUp,
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
reply: this.handleHotkeyReply,
|
||||
favourite: this.handleHotkeyFavourite,
|
||||
boost: this.handleHotkeyBoost,
|
||||
mention: this.handleHotkeyMention,
|
||||
openProfile: this.handleHotkeyOpenProfile,
|
||||
};
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
|
||||
<ScrollContainer scrollKey='thread'>
|
||||
<div className='scrollable detailed-status__wrapper'>
|
||||
<div className='scrollable detailed-status__wrapper' ref={this.setRef}>
|
||||
{ancestors}
|
||||
|
||||
<DetailedStatus
|
||||
status={status}
|
||||
autoPlayGif={autoPlayGif}
|
||||
me={me}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
/>
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className='focusable' tabIndex='0'>
|
||||
<DetailedStatus
|
||||
status={status}
|
||||
autoPlayGif={autoPlayGif}
|
||||
me={me}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
/>
|
||||
|
||||
<ActionBar
|
||||
status={status}
|
||||
me={me}
|
||||
onReply={this.handleReplyClick}
|
||||
onFavourite={this.handleFavouriteClick}
|
||||
onReblog={this.handleReblogClick}
|
||||
onDelete={this.handleDeleteClick}
|
||||
onMention={this.handleMentionClick}
|
||||
onReport={this.handleReport}
|
||||
onPin={this.handlePin}
|
||||
onEmbed={this.handleEmbed}
|
||||
/>
|
||||
<ActionBar
|
||||
status={status}
|
||||
me={me}
|
||||
onReply={this.handleReplyClick}
|
||||
onFavourite={this.handleFavouriteClick}
|
||||
onReblog={this.handleReblogClick}
|
||||
onDelete={this.handleDeleteClick}
|
||||
onMention={this.handleMentionClick}
|
||||
onReport={this.handleReport}
|
||||
onPin={this.handlePin}
|
||||
onEmbed={this.handleEmbed}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
|
||||
{descendants}
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue