From 3bcb3195c4bc5b59fe3a792605caa5b58f9094a5 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 30 Nov 2017 14:48:54 +0000 Subject: [PATCH 1/4] Implement shift-click and ctrl-click semantics for TP --- src/components/structures/TagPanel.js | 6 +++ src/stores/FilterStore.js | 60 ++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 242b71cd16..a275f5864a 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -50,6 +50,8 @@ const TagTile = React.createClass({ dis.dispatch({ action: 'select_tag', tag: this.props.groupProfile.groupId, + ctrlOrCmdKey: e.metaKey || e.ctrlKey, + shiftKey: e.shiftKey, }); }, @@ -148,6 +150,10 @@ export default React.createClass({ const joinedGroupProfiles = await Promise.all(joinedGroupIds.map( (groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId), )); + dis.dispatch({ + action: 'all_tags', + tags: joinedGroupIds, + }); this.setState({joinedGroupProfiles}); }, diff --git a/src/stores/FilterStore.js b/src/stores/FilterStore.js index 6e2a7f4739..8078a13ffd 100644 --- a/src/stores/FilterStore.js +++ b/src/stores/FilterStore.js @@ -18,7 +18,10 @@ import dis from '../dispatcher'; import Analytics from '../Analytics'; const INITIAL_STATE = { - tags: [], + allTags: [], + selectedTags: [], + // Last selected tag when shift was not being pressed + anchorTag: null, }; /** @@ -39,15 +42,62 @@ class FilterStore extends Store { __onDispatch(payload) { switch (payload.action) { - case 'select_tag': + case 'all_tags' : this._setState({ - tags: [payload.tag], + allTags: payload.tags, }); + break; + case 'select_tag': { + let newTags = []; + // Shift-click semantics + if (payload.shiftKey) { + // Select range of tags + let start = this._state.allTags.indexOf(this._state.anchorTag); + let end = this._state.allTags.indexOf(payload.tag); + + if (start === -1) { + start = end; + } + if (start > end) { + const temp = start; + start = end; + end = temp; + } + newTags = payload.ctrlOrCmdKey ? this._state.selectedTags : []; + newTags = [...new Set( + this._state.allTags.slice(start, end + 1).concat(newTags), + )]; + } else { + if (payload.ctrlOrCmdKey) { + // Toggle individual tag + if (this._state.selectedTags.includes(payload.tag)) { + newTags = this._state.selectedTags.filter((t) => t !== payload.tag); + } else { + newTags = [...this._state.selectedTags, payload.tag]; + } + } else { + // Select individual tag + newTags = [payload.tag]; + } + // Only set the anchor tag if the tag was previously unselected, otherwise + // the next range starts with an unselected tag. + if (!this._state.selectedTags.includes(payload.tag)) { + this._setState({ + anchorTag: payload.tag, + }); + } + } + + this._setState({ + selectedTags: newTags, + }); + Analytics.trackEvent('FilterStore', 'select_tag'); + } break; case 'deselect_tags': this._setState({ - tags: [], + selectedTags: [], }); Analytics.trackEvent('FilterStore', 'deselect_tags'); break; @@ -55,7 +105,7 @@ class FilterStore extends Store { } getSelectedTags() { - return this._state.tags; + return this._state.selectedTags; } } From fe81fcb8c65e1959a91c23cd2340544e8f06e8bb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 1 Dec 2017 10:30:49 +0000 Subject: [PATCH 2/4] Factor out isCtrlOrCmdKeyEvent, use that in TagPanel as opposed to the incorrect ctrl || meta --- src/{KeyCode.js => Keyboard.js} | 12 +++++++++++- src/components/structures/LoggedInView.js | 10 ++-------- src/components/structures/RoomView.js | 10 ++-------- src/components/structures/TagPanel.js | 3 ++- src/components/views/dialogs/BaseDialog.js | 2 +- src/components/views/dialogs/SetMxIdDialog.js | 2 +- src/components/views/rooms/ForwardMessage.js | 2 +- src/components/views/rooms/MessageComposerInput.js | 10 ++-------- 8 files changed, 22 insertions(+), 29 deletions(-) rename src/{KeyCode.js => Keyboard.js} (77%) diff --git a/src/KeyCode.js b/src/Keyboard.js similarity index 77% rename from src/KeyCode.js rename to src/Keyboard.js index ec5595b71b..50ed78e063 100644 --- a/src/KeyCode.js +++ b/src/Keyboard.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ limitations under the License. */ /* a selection of key codes, as used in KeyboardEvent.keyCode */ -module.exports = { +export const KeyCode = { BACKSPACE: 8, TAB: 9, ENTER: 13, @@ -58,3 +59,12 @@ module.exports = { KEY_Y: 89, KEY_Z: 90, }; + +export function isCtrlOrCmdKeyEvent(ev) { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + if (isMac) { + return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; + } else { + return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey; + } +} diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 2f0ee5e47d..89971eb5eb 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -19,7 +19,7 @@ limitations under the License. import * as Matrix from 'matrix-js-sdk'; import React from 'react'; -import KeyCode from '../../KeyCode'; +import { KeyCode, isCtrlOrCmdKeyEvent } from '../../Keyboard'; import Notifier from '../../Notifier'; import PageTypes from '../../PageTypes'; import CallMediaHandler from '../../CallMediaHandler'; @@ -153,13 +153,7 @@ export default React.createClass({ */ let handled = false; - const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; - let ctrlCmdOnly; - if (isMac) { - ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; - } else { - ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey; - } + const ctrlCmdOnly = isCtrlOrCmdKeyEvent(ev); switch (ev.keyCode) { case KeyCode.UP: diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 1381b4fce0..e506a3fd00 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -41,7 +41,7 @@ const rate_limited_func = require('../../ratelimitedfunc'); const ObjectUtils = require('../../ObjectUtils'); const Rooms = require('../../Rooms'); -import KeyCode from '../../KeyCode'; +import { KeyCode, isCtrlOrCmdKeyEvent } from '../../Keyboard'; import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; @@ -433,13 +433,7 @@ module.exports = React.createClass({ onKeyDown: function(ev) { let handled = false; - const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; - let ctrlCmdOnly; - if (isMac) { - ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; - } else { - ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey; - } + const ctrlCmdOnly = isCtrlOrCmdKeyEvent(ev); switch (ev.keyCode) { case KeyCode.KEY_D: diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index a275f5864a..fb92ec52d6 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -22,6 +22,7 @@ import FilterStore from '../../stores/FilterStore'; import FlairStore from '../../stores/FlairStore'; import sdk from '../../index'; import dis from '../../dispatcher'; +import { isCtrlOrCmdKeyEvent } from '../../Keyboard'; const TagTile = React.createClass({ displayName: 'TagTile', @@ -50,7 +51,7 @@ const TagTile = React.createClass({ dis.dispatch({ action: 'select_tag', tag: this.props.groupProfile.groupId, - ctrlOrCmdKey: e.metaKey || e.ctrlKey, + ctrlOrCmdKey: isCtrlOrCmdKeyEvent(e), shiftKey: e.shiftKey, }); }, diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 295bb21ea1..b88a6c026e 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; -import * as KeyCode from '../../../KeyCode'; +import { KeyCode } from '../../../Keyboard'; import AccessibleButton from '../elements/AccessibleButton'; import sdk from '../../../index'; diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 6fc1d77682..3ffafb0659 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -20,7 +20,7 @@ import React from 'react'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import classnames from 'classnames'; -import KeyCode from '../../../KeyCode'; +import { KeyCode } from '../../../Keyboard'; import { _t } from '../../../languageHandler'; // The amount of time to wait for further changes to the input username before diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js index b0fba12865..6f9d1d0b69 100644 --- a/src/components/views/rooms/ForwardMessage.js +++ b/src/components/views/rooms/ForwardMessage.js @@ -18,7 +18,7 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher'; -import KeyCode from '../../../KeyCode'; +import { KeyCode } from '../../../Keyboard'; module.exports = React.createClass({ diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 4e8228beec..8b222d1ef2 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -28,7 +28,7 @@ import Promise from 'bluebird'; import MatrixClientPeg from '../../../MatrixClientPeg'; import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; import SlashCommands from '../../../SlashCommands'; -import KeyCode from '../../../KeyCode'; +import { KeyCode, isCtrlOrCmdKeyEvent } from '../../../Keyboard'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; @@ -105,13 +105,7 @@ export default class MessageComposerInput extends React.Component { }; static getKeyBinding(ev: SyntheticKeyboardEvent): string { - const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; - let ctrlCmdOnly; - if (isMac) { - ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; - } else { - ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey; - } + const ctrlCmdOnly = isCtrlOrCmdKeyEvent(ev); // Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and // importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not From ddd0e161c082792561eec0fa628cdb4798595f92 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 1 Dec 2017 10:41:56 +0000 Subject: [PATCH 3/4] Fix broken imports --- src/components/structures/ScrollPanel.js | 2 +- src/components/structures/TimelinePanel.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index dfc6b0f7a1..37cb2977aa 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -18,7 +18,7 @@ const React = require("react"); const ReactDOM = require("react-dom"); const GeminiScrollbar = require('react-gemini-scrollbar'); import Promise from 'bluebird'; -const KeyCode = require('../../KeyCode'); +import { KeyCode } from '../../Keyboard'; const DEBUG_SCROLL = false; // var DEBUG_SCROLL = true; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index aeb6cce6c3..98f57a60b5 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -31,7 +31,7 @@ const dis = require("../../dispatcher"); const ObjectUtils = require('../../ObjectUtils'); const Modal = require("../../Modal"); const UserActivity = require("../../UserActivity"); -const KeyCode = require('../../KeyCode'); +import { KeyCode } from '../../Keyboard'; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; From 363fe04a102046260eea5d6abf13da964960a0a0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 1 Dec 2017 10:44:00 +0000 Subject: [PATCH 4/4] isCtrlOrCmdKeyEvent -> isOnlyCtrlOrCmdKeyEvent --- src/Keyboard.js | 2 +- src/components/structures/LoggedInView.js | 4 ++-- src/components/structures/RoomView.js | 4 ++-- src/components/structures/TagPanel.js | 4 ++-- src/components/views/rooms/MessageComposerInput.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Keyboard.js b/src/Keyboard.js index 50ed78e063..9c872e1c66 100644 --- a/src/Keyboard.js +++ b/src/Keyboard.js @@ -60,7 +60,7 @@ export const KeyCode = { KEY_Z: 90, }; -export function isCtrlOrCmdKeyEvent(ev) { +export function isOnlyCtrlOrCmdKeyEvent(ev) { const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; if (isMac) { return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 89971eb5eb..01abf966f9 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -19,7 +19,7 @@ limitations under the License. import * as Matrix from 'matrix-js-sdk'; import React from 'react'; -import { KeyCode, isCtrlOrCmdKeyEvent } from '../../Keyboard'; +import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import Notifier from '../../Notifier'; import PageTypes from '../../PageTypes'; import CallMediaHandler from '../../CallMediaHandler'; @@ -153,7 +153,7 @@ export default React.createClass({ */ let handled = false; - const ctrlCmdOnly = isCtrlOrCmdKeyEvent(ev); + const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); switch (ev.keyCode) { case KeyCode.UP: diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e506a3fd00..1fda05fb76 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -41,7 +41,7 @@ const rate_limited_func = require('../../ratelimitedfunc'); const ObjectUtils = require('../../ObjectUtils'); const Rooms = require('../../Rooms'); -import { KeyCode, isCtrlOrCmdKeyEvent } from '../../Keyboard'; +import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; @@ -433,7 +433,7 @@ module.exports = React.createClass({ onKeyDown: function(ev) { let handled = false; - const ctrlCmdOnly = isCtrlOrCmdKeyEvent(ev); + const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); switch (ev.keyCode) { case KeyCode.KEY_D: diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index fb92ec52d6..e9b7657345 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -22,7 +22,7 @@ import FilterStore from '../../stores/FilterStore'; import FlairStore from '../../stores/FlairStore'; import sdk from '../../index'; import dis from '../../dispatcher'; -import { isCtrlOrCmdKeyEvent } from '../../Keyboard'; +import { isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; const TagTile = React.createClass({ displayName: 'TagTile', @@ -51,7 +51,7 @@ const TagTile = React.createClass({ dis.dispatch({ action: 'select_tag', tag: this.props.groupProfile.groupId, - ctrlOrCmdKey: isCtrlOrCmdKeyEvent(e), + ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e), shiftKey: e.shiftKey, }); }, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 8b222d1ef2..cd30f20645 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -28,7 +28,7 @@ import Promise from 'bluebird'; import MatrixClientPeg from '../../../MatrixClientPeg'; import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; import SlashCommands from '../../../SlashCommands'; -import { KeyCode, isCtrlOrCmdKeyEvent } from '../../../Keyboard'; +import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; @@ -105,7 +105,7 @@ export default class MessageComposerInput extends React.Component { }; static getKeyBinding(ev: SyntheticKeyboardEvent): string { - const ctrlCmdOnly = isCtrlOrCmdKeyEvent(ev); + const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); // Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and // importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not