diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2017-10-06 01:07:59 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-06 01:07:59 +0200 |
commit | 7db0f8dcb2110b4ec8815bedc965cfbd01a59798 (patch) | |
tree | ff3153c334c12a75aa2875284012cded2a82d49d /app/javascript/mastodon/features/notifications | |
parent | 49cc0eb3e7d1521079e33a60216df46679082547 (diff) |
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
Diffstat (limited to 'app/javascript/mastodon/features/notifications')
3 files changed, 125 insertions, 27 deletions
diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index a608a5223..9d170cad5 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ b/app/javascript/mastodon/features/notifications/components/notification.js @@ -6,61 +6,126 @@ import AccountContainer from '../../../containers/account_container'; import { FormattedMessage } from 'react-intl'; import Permalink from '../../../components/permalink'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { HotKeys } from 'react-hotkeys'; export default class Notification extends ImmutablePureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { notification: ImmutablePropTypes.map.isRequired, hidden: PropTypes.bool, + onMoveUp: PropTypes.func.isRequired, + onMoveDown: PropTypes.func.isRequired, + onMention: PropTypes.func.isRequired, }; + handleMoveUp = () => { + const { notification, onMoveUp } = this.props; + onMoveUp(notification.get('id')); + } + + handleMoveDown = () => { + const { notification, onMoveDown } = this.props; + onMoveDown(notification.get('id')); + } + + handleOpen = () => { + const { notification } = this.props; + + if (notification.get('status')) { + this.context.router.history.push(`/statuses/${notification.get('status')}`); + } else { + this.handleOpenProfile(); + } + } + + handleOpenProfile = () => { + const { notification } = this.props; + this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`); + } + + handleMention = e => { + e.preventDefault(); + + const { notification, onMention } = this.props; + onMention(notification.get('account'), this.context.router.history); + } + + getHandlers () { + return { + moveUp: this.handleMoveUp, + moveDown: this.handleMoveDown, + open: this.handleOpen, + openProfile: this.handleOpenProfile, + mention: this.handleMention, + reply: this.handleMention, + }; + } + renderFollow (account, link) { return ( - <div className='notification notification-follow'> - <div className='notification__message'> - <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-user-plus' /> + <HotKeys handlers={this.getHandlers()}> + <div className='notification notification-follow focusable' tabIndex='0'> + <div className='notification__message'> + <div className='notification__favourite-icon-wrapper'> + <i className='fa fa-fw fa-user-plus' /> + </div> + + <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} /> </div> - <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} /> + <AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} /> </div> - - <AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} /> - </div> + </HotKeys> ); } renderMention (notification) { - return <StatusContainer id={notification.get('status')} withDismiss hidden={this.props.hidden} />; + return ( + <StatusContainer + id={notification.get('status')} + withDismiss + hidden={this.props.hidden} + onMoveDown={this.handleMoveDown} + onMoveUp={this.handleMoveUp} + /> + ); } renderFavourite (notification, link) { return ( - <div className='notification notification-favourite'> - <div className='notification__message'> - <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-star star-icon' /> + <HotKeys handlers={this.getHandlers()}> + <div className='notification notification-favourite focusable' tabIndex='0'> + <div className='notification__message'> + <div className='notification__favourite-icon-wrapper'> + <i className='fa fa-fw fa-star star-icon' /> + </div> + <FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} /> </div> - <FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} /> - </div> - <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={!!this.props.hidden} /> - </div> + <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={!!this.props.hidden} /> + </div> + </HotKeys> ); } renderReblog (notification, link) { return ( - <div className='notification notification-reblog'> - <div className='notification__message'> - <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-retweet' /> + <HotKeys handlers={this.getHandlers()}> + <div className='notification notification-reblog focusable' tabIndex='0'> + <div className='notification__message'> + <div className='notification__favourite-icon-wrapper'> + <i className='fa fa-fw fa-retweet' /> + </div> + <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} /> </div> - <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} /> - </div> - <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={this.props.hidden} /> - </div> + <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={this.props.hidden} /> + </div> + </HotKeys> ); } diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js index 786222967..921aa460f 100644 --- a/app/javascript/mastodon/features/notifications/containers/notification_container.js +++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import { makeGetNotification } from '../../../selectors'; import Notification from '../components/notification'; +import { mentionCompose } from '../../../actions/compose'; const makeMapStateToProps = () => { const getNotification = makeGetNotification(); @@ -12,4 +13,10 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -export default connect(makeMapStateToProps)(Notification); +const mapDispatchToProps = dispatch => ({ + onMention: (account, router) => { + dispatch(mentionCompose(account, router)); + }, +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(Notification); diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index b74473b9f..35b430bfb 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -86,6 +86,24 @@ export default class Notifications extends React.PureComponent { this.column = c; } + handleMoveUp = id => { + const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) - 1; + this._selectChild(elementIndex); + } + + handleMoveDown = id => { + const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) + 1; + this._selectChild(elementIndex); + } + + _selectChild (index) { + const element = this.column.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); + + if (element) { + element.focus(); + } + } + render () { const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props; const pinned = !!columnId; @@ -96,7 +114,15 @@ export default class Notifications extends React.PureComponent { if (isLoading && this.scrollableContent) { scrollableContent = this.scrollableContent; } else if (notifications.size > 0 || hasMore) { - scrollableContent = notifications.map((item) => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />); + scrollableContent = notifications.map((item) => ( + <NotificationContainer + key={item.get('id')} + notification={item} + accountId={item.get('account')} + onMoveUp={this.handleMoveUp} + onMoveDown={this.handleMoveDown} + /> + )); } else { scrollableContent = null; } |