about summary refs log tree commit diff
path: root/app/javascript/mastodon/features/status/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/mastodon/features/status/index.js')
-rw-r--r--app/javascript/mastodon/features/status/index.js153
1 files changed, 130 insertions, 23 deletions
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index fc45d5f21..fff5f529c 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -28,6 +28,7 @@ import StatusContainer from '../../../glitch/components/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' },
@@ -153,8 +154,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 { status, ancestorsIds } = this.props;
+
+    if (status && ancestorsIds && ancestorsIds.size > 0) {
+      const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size];
+      element.scrollIntoView();
+    }
   }
 
   render () {
@@ -178,35 +271,49 @@ 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}
-              settings={settings}
-              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}
-            />
+            <HotKeys handlers={handlers}>
+              <div className='focusable' tabIndex='0'>
+                <DetailedStatus
+                  status={status}
+                  settings={settings}
+                  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}
+                />
+              </div>
+            </HotKeys>
 
             {descendants}
           </div>