+
{ancestors}
-
-
-
+
+
+
{descendants}
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 0e4796fcb5..21f2395ba0 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -8,7 +8,7 @@ import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom';
import { isMobile } from '../../is_mobile';
import { debounce } from 'lodash';
-import { uploadCompose } from '../../actions/compose';
+import { uploadCompose, resetCompose } from '../../actions/compose';
import { refreshHomeTimeline } from '../../actions/timelines';
import { refreshNotifications } from '../../actions/notifications';
import { clearHeight } from '../../actions/height_cache';
@@ -37,15 +37,43 @@ import {
Mutes,
PinnedStatuses,
} from './util/async-components';
+import { HotKeys } from 'react-hotkeys';
// Dummy import, to make sure that
ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
import '../../components/status';
const mapStateToProps = state => ({
+ me: state.getIn(['meta', 'me']),
isComposing: state.getIn(['compose', 'is_composing']),
});
+const keyMap = {
+ new: 'n',
+ search: 's',
+ forceNew: 'option+n',
+ focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ reply: 'r',
+ favourite: 'f',
+ boost: 'b',
+ mention: 'm',
+ open: ['enter', 'o'],
+ openProfile: 'p',
+ moveDown: ['down', 'j'],
+ moveUp: ['up', 'k'],
+ back: 'backspace',
+ goToHome: 'g h',
+ goToNotifications: 'g n',
+ goToLocal: 'g l',
+ goToFederated: 'g t',
+ goToStart: 'g s',
+ goToFavourites: 'g f',
+ goToPinned: 'g p',
+ goToProfile: 'g u',
+ goToBlocked: 'g b',
+ goToMuted: 'g m',
+};
+
@connect(mapStateToProps)
@withRouter
export default class UI extends React.Component {
@@ -58,6 +86,7 @@ export default class UI extends React.Component {
dispatch: PropTypes.func.isRequired,
children: PropTypes.node,
isComposing: PropTypes.bool,
+ me: PropTypes.string,
location: PropTypes.object,
};
@@ -155,6 +184,12 @@ export default class UI extends React.Component {
this.props.dispatch(refreshNotifications());
}
+ componentDidMount () {
+ this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
+ return !(e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) && ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
+ };
+ }
+
shouldComponentUpdate (nextProps) {
if (nextProps.isComposing !== this.props.isComposing) {
// Avoid expensive update just to toggle a class
@@ -191,52 +226,160 @@ export default class UI extends React.Component {
this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance();
}
- setOverlayRef = c => {
- this.overlay = c;
+ handleHotkeyNew = e => {
+ e.preventDefault();
+
+ const element = this.node.querySelector('.compose-form__autosuggest-wrapper textarea');
+
+ if (element) {
+ element.focus();
+ }
+ }
+
+ handleHotkeySearch = e => {
+ e.preventDefault();
+
+ const element = this.node.querySelector('.search__input');
+
+ if (element) {
+ element.focus();
+ }
+ }
+
+ handleHotkeyForceNew = e => {
+ this.handleHotkeyNew(e);
+ this.props.dispatch(resetCompose());
+ }
+
+ handleHotkeyFocusColumn = e => {
+ const index = (e.key * 1) + 1; // First child is drawer, skip that
+ const column = this.node.querySelector(`.column:nth-child(${index})`);
+
+ if (column) {
+ const status = column.querySelector('.focusable');
+
+ if (status) {
+ status.focus();
+ }
+ }
+ }
+
+ handleHotkeyBack = () => {
+ if (window.history && window.history.length === 1) {
+ this.context.router.history.push('/');
+ } else {
+ this.context.router.history.goBack();
+ }
+ }
+
+ setHotkeysRef = c => {
+ this.hotkeys = c;
+ }
+
+ handleHotkeyGoToHome = () => {
+ this.context.router.history.push('/timelines/home');
+ }
+
+ handleHotkeyGoToNotifications = () => {
+ this.context.router.history.push('/notifications');
+ }
+
+ handleHotkeyGoToLocal = () => {
+ this.context.router.history.push('/timelines/public/local');
+ }
+
+ handleHotkeyGoToFederated = () => {
+ this.context.router.history.push('/timelines/public');
+ }
+
+ handleHotkeyGoToStart = () => {
+ this.context.router.history.push('/getting-started');
+ }
+
+ handleHotkeyGoToFavourites = () => {
+ this.context.router.history.push('/favourites');
+ }
+
+ handleHotkeyGoToPinned = () => {
+ this.context.router.history.push('/pinned');
+ }
+
+ handleHotkeyGoToProfile = () => {
+ this.context.router.history.push(`/accounts/${this.props.me}`);
+ }
+
+ handleHotkeyGoToBlocked = () => {
+ this.context.router.history.push('/blocks');
+ }
+
+ handleHotkeyGoToMuted = () => {
+ this.context.router.history.push('/mutes');
}
render () {
const { width, draggingOver } = this.state;
const { children } = this.props;
+ const handlers = {
+ new: this.handleHotkeyNew,
+ search: this.handleHotkeySearch,
+ forceNew: this.handleHotkeyForceNew,
+ focusColumn: this.handleHotkeyFocusColumn,
+ back: this.handleHotkeyBack,
+ goToHome: this.handleHotkeyGoToHome,
+ goToNotifications: this.handleHotkeyGoToNotifications,
+ goToLocal: this.handleHotkeyGoToLocal,
+ goToFederated: this.handleHotkeyGoToFederated,
+ goToStart: this.handleHotkeyGoToStart,
+ goToFavourites: this.handleHotkeyGoToFavourites,
+ goToPinned: this.handleHotkeyGoToPinned,
+ goToProfile: this.handleHotkeyGoToProfile,
+ goToBlocked: this.handleHotkeyGoToBlocked,
+ goToMuted: this.handleHotkeyGoToMuted,
+ };
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 082d4d3705..3e9310f164 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -25,6 +25,7 @@ import {
COMPOSE_UPLOAD_CHANGE_REQUEST,
COMPOSE_UPLOAD_CHANGE_SUCCESS,
COMPOSE_UPLOAD_CHANGE_FAIL,
+ COMPOSE_RESET,
} from '../actions/compose';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STORE_HYDRATE } from '../actions/store';
@@ -214,6 +215,7 @@ export default function compose(state = initialState, action) {
}
});
case COMPOSE_REPLY_CANCEL:
+ case COMPOSE_RESET:
return state.withMutations(map => {
map.set('in_reply_to', null);
map.set('text', '');
diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss
index 96f0023c3d..0018c9a5d1 100644
--- a/app/javascript/styles/basics.scss
+++ b/app/javascript/styles/basics.scss
@@ -94,9 +94,12 @@ button {
}
.app-holder {
- display: flex;
- width: 100%;
- height: 100%;
- align-items: center;
- justify-content: center;
+ &,
+ & > div {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+ }
}
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 6ef4e38663..7609ac005d 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -587,6 +587,22 @@
position: absolute;
}
+.focusable {
+ &:focus {
+ outline: 0;
+ background: lighten($ui-base-color, 4%);
+
+ &.status-direct {
+ background: lighten($ui-base-color, 12%);
+ }
+
+ .detailed-status,
+ .detailed-status__action-bar {
+ background: lighten($ui-base-color, 8%);
+ }
+ }
+}
+
.status {
padding: 8px 10px;
padding-left: 68px;
@@ -1046,11 +1062,11 @@
strong {
color: $primary-text-color;
}
+}
- &.muted {
- .emojione {
- opacity: 0.5;
- }
+.muted {
+ .emojione {
+ opacity: 0.5;
}
}
diff --git a/package.json b/package.json
index 11de3c636f..d94186cf27 100644
--- a/package.json
+++ b/package.json
@@ -80,6 +80,7 @@
"rails-ujs": "^5.1.2",
"react": "^16.0.0",
"react-dom": "^16.0.0",
+ "react-hotkeys": "^0.10.0",
"react-immutable-proptypes": "^2.1.0",
"react-immutable-pure-component": "^1.0.0",
"react-intl": "^2.4.0",
diff --git a/yarn.lock b/yarn.lock
index 3aa39a4159..4f085ff2c9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1684,6 +1684,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
+create-react-class@^15.5.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a"
+ dependencies:
+ fbjs "^0.8.9"
+ loose-envify "^1.3.1"
+ object-assign "^4.1.1"
+
cross-env@^5.0.1:
version "5.0.5"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.0.5.tgz#4383d364d9660873dd185b398af3bfef5efffef3"
@@ -4209,6 +4217,10 @@ mocha@^3.4.1:
mkdirp "0.5.1"
supports-color "3.1.2"
+mousetrap@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9"
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -5553,6 +5565,15 @@ react-event-listener@^0.5.0:
prop-types "^15.5.10"
warning "^3.0.0"
+react-hotkeys@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-0.10.0.tgz#d1e78bd63f16d6db58d550d33c8eb071f35d94fb"
+ dependencies:
+ create-react-class "^15.5.2"
+ lodash "^4.13.1"
+ mousetrap "^1.5.2"
+ prop-types "^15.5.8"
+
react-immutable-proptypes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4"