From 81784964572457da8a3ba763e80152bda0369173 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 5 Dec 2017 14:38:17 +0000 Subject: [PATCH 01/39] Implement Store for ordering tags in the tag panel --- src/components/structures/TagPanel.js | 17 +++++- src/settings/Settings.js | 4 ++ src/stores/TagOrderStore.js | 80 +++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/stores/TagOrderStore.js diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 0107ad1db15..a853a592129 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -20,6 +20,7 @@ import { MatrixClient } from 'matrix-js-sdk'; import classNames from 'classnames'; import FilterStore from '../../stores/FilterStore'; import FlairStore from '../../stores/FlairStore'; +import TagOrderStore from '../../stores/TagOrderStore'; import sdk from '../../index'; import dis from '../../dispatcher'; import { isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; @@ -115,6 +116,14 @@ export default React.createClass({ selectedTags: FilterStore.getSelectedTags(), }); }); + this._tagOrderStoreToken = TagOrderStore.addListener(() => { + if (this.unmounted) { + return; + } + this.setState({ + orderedTags: TagOrderStore.getOrderedTags(), + }); + }); this._fetchJoinedRooms(); }, @@ -157,7 +166,13 @@ export default React.createClass({ render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); - const tags = this.state.joinedGroupProfiles.map((groupProfile, index) => { + + const orderedGroupProfiles = this.state.orderedTags ? + this.state.joinedGroupProfiles.sort((a, b) => + this.state.orderedTags.indexOf(a.groupId) - + this.state.orderedTags.indexOf(b.groupId), + ) : this.state.joinedGroupProfiles; + const tags = orderedGroupProfiles.map((groupProfile, index) => { return t !== payload.tag); + const tagPrevIx = orderedTags.indexOf(payload.prevTag); + orderedTags = [ + ...orderedTags.slice(0, tagPrevIx + 1), + payload.tag, + ...orderedTags.slice(tagPrevIx + 1), + ]; + this._setState({orderedTags}); + SettingsStore.setValue("TagOrderStore.orderedTags", null, "account", orderedTags); + } + break; + } + } + + getOrderedTags() { + return this._state.orderedTags || SettingsStore.getValue("TagOrderStore.orderedTags"); + } +} + +if (global.singletonTagOrderStore === undefined) { + global.singletonTagOrderStore = new TagOrderStore(); +} +export default global.singletonTagOrderStore; From 82a95f0793448e855a3449d1ab638cec8659c1d9 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 6 Dec 2017 11:22:06 +0000 Subject: [PATCH 02/39] Simplify order_tag in TagOrderStore such that: - it takes a targetTag to be replaced instead the previous tag to insert after - it optionally displaces the targetTag before or after the inserted tag --- src/stores/TagOrderStore.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index ce150a60b0f..4364ec2683b 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -53,19 +53,24 @@ class TagOrderStore extends Store { this._setState({allTags: payload.tags}); break; case 'order_tag': { - // Puts payload.tag below payload.prevTag in the orderedTags state + if (!payload.tag || !payload.targetTag || payload.tag === payload.targetTag) break; + + // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag const tags = SettingsStore.getValue("TagOrderStore.orderedTags") || this._state.allTags; + let orderedTags = tags.filter((t) => t !== payload.tag); - const tagPrevIx = orderedTags.indexOf(payload.prevTag); + const newIndex = orderedTags.indexOf(payload.targetTag) + (payload.after ? 1 : 0); orderedTags = [ - ...orderedTags.slice(0, tagPrevIx + 1), + ...orderedTags.slice(0, newIndex), payload.tag, - ...orderedTags.slice(tagPrevIx + 1), + ...orderedTags.slice(newIndex), ]; this._setState({orderedTags}); - SettingsStore.setValue("TagOrderStore.orderedTags", null, "account", orderedTags); } break; + case 'commit_tags': + SettingsStore.setValue("TagOrderStore.orderedTags", null, "account", this._state.orderedTags); + break; } } From a8a650c24a2873dd9c1504bca66630c9c50dc479 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 6 Dec 2017 11:25:46 +0000 Subject: [PATCH 03/39] Move TagTile to separate file, and make it dragable --- package.json | 2 + src/components/structures/TagPanel.js | 76 ++---------------- src/components/views/elements/DNDTagTile.js | 86 ++++++++++++++++++++ src/components/views/elements/TagTile.js | 88 +++++++++++++++++++++ 4 files changed, 183 insertions(+), 69 deletions(-) create mode 100644 src/components/views/elements/DNDTagTile.js create mode 100644 src/components/views/elements/TagTile.js diff --git a/package.json b/package.json index 5c81db2153a..943c443c596 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,8 @@ "querystring": "^0.2.0", "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", + "react-dnd": "^2.1.4", + "react-dnd-html5-backend": "^2.1.2", "react-dom": "^15.4.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.14.1", diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index a853a592129..2c498e9eeef 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -15,82 +15,17 @@ limitations under the License. */ import React from 'react'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; -import classNames from 'classnames'; import FilterStore from '../../stores/FilterStore'; import FlairStore from '../../stores/FlairStore'; import TagOrderStore from '../../stores/TagOrderStore'; import sdk from '../../index'; import dis from '../../dispatcher'; -import { isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; -const TagTile = React.createClass({ - displayName: 'TagTile', - - propTypes: { - groupProfile: PropTypes.object, - }, - - contextTypes: { - matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, - }, - - getInitialState() { - return { - hover: false, - }; - }, - - onClick: function(e) { - e.preventDefault(); - e.stopPropagation(); - dis.dispatch({ - action: 'select_tag', - tag: this.props.groupProfile.groupId, - ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e), - shiftKey: e.shiftKey, - }); - }, - - onMouseOver: function() { - this.setState({hover: true}); - }, - - onMouseOut: function() { - this.setState({hover: false}); - }, - - render: function() { - const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); - const profile = this.props.groupProfile || {}; - const name = profile.name || profile.groupId; - const avatarHeight = 35; - - const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( - profile.avatarUrl, avatarHeight, avatarHeight, "crop", - ) : null; - - const className = classNames({ - mx_TagTile: true, - mx_TagTile_selected: this.props.selected, - }); - - const tip = this.state.hover ? - : -
; - return -
- - { tip } -
-
; - }, -}); - -export default React.createClass({ +const TagPanel = React.createClass({ displayName: 'TagPanel', contextTypes: { @@ -166,14 +101,16 @@ export default React.createClass({ render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); + const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const orderedGroupProfiles = this.state.orderedTags ? this.state.joinedGroupProfiles.sort((a, b) => this.state.orderedTags.indexOf(a.groupId) - this.state.orderedTags.indexOf(b.groupId), ) : this.state.joinedGroupProfiles; + const tags = orderedGroupProfiles.map((groupProfile, index) => { - return ; }, }); +export default DragDropContext(HTML5Backend)(TagPanel); diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js new file mode 100644 index 00000000000..6715bd8dd34 --- /dev/null +++ b/src/components/views/elements/DNDTagTile.js @@ -0,0 +1,86 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { DragSource, DropTarget } from 'react-dnd'; + +import TagTile from './TagTile'; +import dis from '../../../dispatcher'; +import { findDOMNode } from 'react-dom'; + +const tagTileSource = { + canDrag: function(props, monitor) { + return true; + }, + + beginDrag: function(props) { + // Return the data describing the dragged item + return { + tag: props.groupProfile.groupId, + }; + }, + + endDrag: function(props, monitor, component) { + const dropResult = monitor.getDropResult(); + if (!monitor.didDrop() || !dropResult) { + return; + } + dis.dispatch({ + action: 'commit_tags', + }); + }, +}; + +const tagTileTarget = { + canDrop(props, monitor) { + return true; + }, + + hover(props, monitor, component) { + if (!monitor.canDrop()) return; + const draggedY = monitor.getClientOffset().y; + const {top, bottom} = findDOMNode(component).getBoundingClientRect(); + const targetY = (top + bottom) / 2; + dis.dispatch({ + action: 'order_tag', + tag: monitor.getItem().tag, + targetTag: props.groupProfile.groupId, + // Note: we indicate that the tag should be after the target when + // it's being dragged over the top half of the target. + after: draggedY < targetY, + }); + }, + + drop(props) { + // Return the data to be returned by getDropResult + return { + tag: props.groupProfile.groupId, + }; + }, +}; + +export default + DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + }))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + }))((props) => { + const { connectDropTarget, connectDragSource, ...otherProps } = props; + return connectDropTarget(connectDragSource( +
+ +
, + )); + })); diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js new file mode 100644 index 00000000000..124559a838c --- /dev/null +++ b/src/components/views/elements/TagTile.js @@ -0,0 +1,88 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { MatrixClient } from 'matrix-js-sdk'; +import sdk from '../../../index'; +import dis from '../../../dispatcher'; +import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard'; + +export default React.createClass({ + displayName: 'TagTile', + + propTypes: { + groupProfile: PropTypes.object, + }, + + contextTypes: { + matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, + }, + + getInitialState() { + return { + hover: false, + }; + }, + + onClick: function(e) { + e.preventDefault(); + e.stopPropagation(); + dis.dispatch({ + action: 'select_tag', + tag: this.props.groupProfile.groupId, + ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e), + shiftKey: e.shiftKey, + }); + }, + + onMouseOver: function() { + this.setState({hover: true}); + }, + + onMouseOut: function() { + this.setState({hover: false}); + }, + + render: function() { + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); + const profile = this.props.groupProfile || {}; + const name = profile.name || profile.groupId; + const avatarHeight = 35; + + const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( + profile.avatarUrl, avatarHeight, avatarHeight, "crop", + ) : null; + + const className = classNames({ + mx_TagTile: true, + mx_TagTile_selected: this.props.selected, + }); + + const tip = this.state.hover ? + : +
; + return +
+ + { tip } +
+
; + }, +}); From 7aa5dcef69f820abd4a66b92d5469e08bba66163 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 6 Dec 2017 13:10:58 +0000 Subject: [PATCH 04/39] Move DragDropContext to wrap entire app --- src/components/structures/MatrixChat.js | 6 +++++- src/components/structures/TagPanel.js | 4 +--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index cd75ad87981..d880c839529 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -19,6 +19,8 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; import Matrix from "matrix-js-sdk"; import Analytics from "../../Analytics"; @@ -84,7 +86,7 @@ const ONBOARDING_FLOW_STARTERS = [ 'view_create_group', ]; -module.exports = React.createClass({ +const MatrixChat = React.createClass({ // we export this so that the integration tests can use it :-S statics: { VIEWS: VIEWS, @@ -1584,3 +1586,5 @@ module.exports = React.createClass({ console.error(`Unknown view ${this.state.view}`); }, }); + +export default DragDropContext(HTML5Backend)(MatrixChat); diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 2c498e9eeef..ff3fd65f0c0 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -15,8 +15,6 @@ limitations under the License. */ import React from 'react'; -import { DragDropContext } from 'react-dnd'; -import HTML5Backend from 'react-dnd-html5-backend'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import FilterStore from '../../stores/FilterStore'; @@ -126,4 +124,4 @@ const TagPanel = React.createClass({
; }, }); -export default DragDropContext(HTML5Backend)(TagPanel); +export default TagPanel; From 4af7def20ef66493317466a7c79c4cc93280f0d0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 6 Dec 2017 14:13:08 +0000 Subject: [PATCH 05/39] Use AccountData im.vector.web.tag_ordering Also, make defaults sensible --- src/stores/TagOrderStore.js | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 4364ec2683b..4814bd1c985 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -16,10 +16,11 @@ limitations under the License. import {Store} from 'flux/utils'; import dis from '../dispatcher'; // import Analytics from '../Analytics'; -import SettingsStore from "../settings/SettingsStore"; +import MatrixClientPeg from "../MatrixClientPeg"; const INITIAL_STATE = { orderedTags: null, + allTags: null, }; /** @@ -40,23 +41,28 @@ class TagOrderStore extends Store { __onDispatch(payload) { switch (payload.action) { + // Get ordering from account data, once the client has synced case 'sync_state': - // Get ordering from account data, once the client has synced if (payload.prevState === "PREPARED" && payload.state === "SYNCING") { - this._setState({ - orderedTags: SettingsStore.getValue("TagOrderStore.orderedTags"), - }); + const accountDataEvent = MatrixClientPeg.get().getAccountData('im.vector.web.tag_ordering'); + + const orderedTags = accountDataEvent && accountDataEvent.getContent() ? + accountDataEvent.getContent().tags : null; + + this._setState({orderedTags}); } break; + // Initialise the state such that if account data is unset, default to the existing ordering case 'all_tags': - // Initialise the state with all known tags displayed in the TagPanel - this._setState({allTags: payload.tags}); + this._setState({ + allTags: payload.tags.sort(), // Sort lexically + }); break; + // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag case 'order_tag': { - if (!payload.tag || !payload.targetTag || payload.tag === payload.targetTag) break; + if (!payload.tag || !payload.targetTag || payload.tag === payload.targetTag) return; - // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag - const tags = SettingsStore.getValue("TagOrderStore.orderedTags") || this._state.allTags; + const tags = this._state.orderedTags || this._state.allTags; let orderedTags = tags.filter((t) => t !== payload.tag); const newIndex = orderedTags.indexOf(payload.targetTag) + (payload.after ? 1 : 0); @@ -69,13 +75,17 @@ class TagOrderStore extends Store { } break; case 'commit_tags': - SettingsStore.setValue("TagOrderStore.orderedTags", null, "account", this._state.orderedTags); + MatrixClientPeg.get().setAccountData('im.vector.web.tag_ordering', {tags: this._state.orderedTags}); break; } } getOrderedTags() { - return this._state.orderedTags || SettingsStore.getValue("TagOrderStore.orderedTags"); + return this._state.orderedTags; + } + + getAllTags() { + return this._state.allTags; } } From 35a108eecc68af4a40e8f8ba15e2fa2dc53816b1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 6 Dec 2017 14:17:26 +0000 Subject: [PATCH 06/39] Simplify render of TagPanel - remove sorting --- src/components/structures/TagPanel.js | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index ff3fd65f0c0..d488d57b4d4 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -32,7 +32,7 @@ const TagPanel = React.createClass({ getInitialState() { return { - joinedGroupProfiles: [], + orderedGroupTagProfiles: [], selectedTags: [], }; }, @@ -53,8 +53,13 @@ const TagPanel = React.createClass({ if (this.unmounted) { return; } - this.setState({ - orderedTags: TagOrderStore.getOrderedTags(), + + const orderedTags = TagOrderStore.getOrderedTags() || TagOrderStore.getAllTags(); + const orderedGroupTags = orderedTags.filter((t) => t[0] === '+'); + Promise.all(orderedGroupTags.map( + (groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId), + )).then((orderedGroupTagProfiles) => { + this.setState({orderedGroupTagProfiles}); }); }); @@ -84,16 +89,13 @@ const TagPanel = React.createClass({ }, async _fetchJoinedRooms() { + // This could be done by anything with a matrix client (, see TagOrderStore). const joinedGroupResponse = await this.context.matrixClient.getJoinedGroups(); const joinedGroupIds = joinedGroupResponse.groups; - const joinedGroupProfiles = await Promise.all(joinedGroupIds.map( - (groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId), - )); dis.dispatch({ action: 'all_tags', tags: joinedGroupIds, }); - this.setState({joinedGroupProfiles}); }, render() { @@ -101,13 +103,7 @@ const TagPanel = React.createClass({ const TintableSvg = sdk.getComponent('elements.TintableSvg'); const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); - const orderedGroupProfiles = this.state.orderedTags ? - this.state.joinedGroupProfiles.sort((a, b) => - this.state.orderedTags.indexOf(a.groupId) - - this.state.orderedTags.indexOf(b.groupId), - ) : this.state.joinedGroupProfiles; - - const tags = orderedGroupProfiles.map((groupProfile, index) => { + const tags = this.state.orderedGroupTagProfiles.map((groupProfile, index) => { return Date: Wed, 6 Dec 2017 14:20:16 +0000 Subject: [PATCH 07/39] Remove redundant TagOrderStore.orderedTags setting --- src/settings/Settings.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 9a67c444b4b..07de17ccfdc 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -255,8 +255,4 @@ export const SETTINGS = { default: true, controller: new AudioNotificationsEnabledController(), }, - "TagOrderStore.orderedTags": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: null, - }, }; From 8f88995b3d5455bd8f5c28706b05ba7acf5dcc92 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 6 Dec 2017 14:22:11 +0000 Subject: [PATCH 08/39] Add analytics to TagOrderStore --- src/stores/TagOrderStore.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 4814bd1c985..f2f5ca55693 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -15,7 +15,7 @@ limitations under the License. */ import {Store} from 'flux/utils'; import dis from '../dispatcher'; -// import Analytics from '../Analytics'; +import Analytics from '../Analytics'; import MatrixClientPeg from "../MatrixClientPeg"; const INITIAL_STATE = { @@ -76,6 +76,7 @@ class TagOrderStore extends Store { break; case 'commit_tags': MatrixClientPeg.get().setAccountData('im.vector.web.tag_ordering', {tags: this._state.orderedTags}); + Analytics.trackEvent('TagOrderStore', 'commit_tags'); break; } } From 7e1f1cdbd9da799559f931ae0918396a4bb97a90 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 6 Dec 2017 15:01:16 +0000 Subject: [PATCH 09/39] Move DragDropContext to wrap LoggedInView Becuase the tests rely on being able to inspect the state of MatrixChat --- src/components/structures/LoggedInView.js | 6 +++++- src/components/structures/MatrixChat.js | 4 +--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 01abf966f9f..38b7634edb9 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -18,6 +18,8 @@ limitations under the License. import * as Matrix from 'matrix-js-sdk'; import React from 'react'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import Notifier from '../../Notifier'; @@ -38,7 +40,7 @@ import SettingsStore from "../../settings/SettingsStore"; * * Components mounted below us can access the matrix client via the react context. */ -export default React.createClass({ +const LoggedInView = React.createClass({ displayName: 'LoggedInView', propTypes: { @@ -344,3 +346,5 @@ export default React.createClass({ ); }, }); + +export default DragDropContext(HTML5Backend)(LoggedInView); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d880c839529..7c41a67fcaa 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -19,8 +19,6 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; -import { DragDropContext } from 'react-dnd'; -import HTML5Backend from 'react-dnd-html5-backend'; import Matrix from "matrix-js-sdk"; import Analytics from "../../Analytics"; @@ -1587,4 +1585,4 @@ const MatrixChat = React.createClass({ }, }); -export default DragDropContext(HTML5Backend)(MatrixChat); +export default MatrixChat; From 65d88334a9eabd4dfe8261f40f6962c0f36ef54c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 6 Dec 2017 16:48:18 +0000 Subject: [PATCH 10/39] Fix linting React DnD specifies functions with upper-case first letters --- src/components/views/elements/DNDTagTile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js index 6715bd8dd34..55c09a37200 100644 --- a/src/components/views/elements/DNDTagTile.js +++ b/src/components/views/elements/DNDTagTile.js @@ -1,3 +1,4 @@ +/* eslint new-cap: "off" */ /* Copyright 2017 New Vector Ltd. From ee6df105feb7ab43c87383de9fca20cc00449f21 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 7 Dec 2017 14:17:32 +0000 Subject: [PATCH 11/39] Introduce action creators These can be used to dispatch actions immediately, or after some asynchronous work has been done. Also, create GroupActions.fetchJoinedGroups as an example. The concept of async action creators can be used in the following cases: - stores or views that do async work, dispatching based on the results - actions that have complicated payloads, would make more sense as functions with documentation that dispatch created actions. --- src/actions/GroupActions.js | 27 ++++++++++++++++++++++++++ src/actions/actionCreators.js | 28 +++++++++++++++++++++++++++ src/components/structures/TagPanel.js | 19 ++++++------------ src/stores/TagOrderStore.js | 6 +++--- 4 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 src/actions/GroupActions.js create mode 100644 src/actions/actionCreators.js diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js new file mode 100644 index 00000000000..30249f846c4 --- /dev/null +++ b/src/actions/GroupActions.js @@ -0,0 +1,27 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + +import { createPromiseActionCreator } from './actionCreators'; + +const GroupActions = {}; + +GroupActions.fetchJoinedGroups = createPromiseActionCreator( + 'GroupActions.fetchJoinedGroups', + (matrixClient) => matrixClient.getJoinedGroups(), +); + +export default GroupActions; diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js new file mode 100644 index 00000000000..c4e20a0a3f2 --- /dev/null +++ b/src/actions/actionCreators.js @@ -0,0 +1,28 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import dis from '../dispatcher'; + +export function createPromiseActionCreator(id, fn) { + return (...args) => { + dis.dispatch({action: id + '.pending'}); + fn(...args).then((result) => { + dis.dispatch({action: id + '.success', result}); + }).catch((err) => { + dis.dispatch({action: id + '.failure', err}); + }) + } +} diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index d488d57b4d4..ae86bef4c16 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -20,6 +20,9 @@ import { MatrixClient } from 'matrix-js-sdk'; import FilterStore from '../../stores/FilterStore'; import FlairStore from '../../stores/FlairStore'; import TagOrderStore from '../../stores/TagOrderStore'; + +import GroupActions from '../../actions/GroupActions'; + import sdk from '../../index'; import dis from '../../dispatcher'; @@ -62,8 +65,8 @@ const TagPanel = React.createClass({ this.setState({orderedGroupTagProfiles}); }); }); - - this._fetchJoinedRooms(); + // This could be done by anything with a matrix client + GroupActions.fetchJoinedGroups(this.context.matrixClient); }, componentWillUnmount() { @@ -76,7 +79,7 @@ const TagPanel = React.createClass({ _onGroupMyMembership() { if (this.unmounted) return; - this._fetchJoinedRooms(); + GroupActions.fetchJoinedGroups(this.context.matrixClient); }, onClick() { @@ -88,16 +91,6 @@ const TagPanel = React.createClass({ dis.dispatch({action: 'view_create_group'}); }, - async _fetchJoinedRooms() { - // This could be done by anything with a matrix client (, see TagOrderStore). - const joinedGroupResponse = await this.context.matrixClient.getJoinedGroups(); - const joinedGroupIds = joinedGroupResponse.groups; - dis.dispatch({ - action: 'all_tags', - tags: joinedGroupIds, - }); - }, - render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index f2f5ca55693..885eaafb387 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -52,10 +52,10 @@ class TagOrderStore extends Store { this._setState({orderedTags}); } break; - // Initialise the state such that if account data is unset, default to the existing ordering - case 'all_tags': + // Initialise the state such that if account data is unset, default to joined groups + case 'GroupActions.fetchJoinedGroups.success': this._setState({ - allTags: payload.tags.sort(), // Sort lexically + allTags: payload.result.groups.sort(), // Sort lexically }); break; // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag From 12515441cdf68caa3a5d05a4d446419a5a73d63a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 7 Dec 2017 17:10:45 +0000 Subject: [PATCH 12/39] Handle accountData events from TagOrderStore This introduces a generic way to register certain events emitted by the js-sdk as those that should be propagated through as dispatched actions. This allows the store to treat the js-sdk as the "Server" in the Flux data flow model. It also allows for stores to not be aware specifically of the matrix client if they are only reading from it. --- src/MatrixClientPeg.js | 6 ++++ src/actions/GroupActions.js | 1 - src/actions/MatrixActionCreators.js | 39 +++++++++++++++++++++++++ src/actions/actionCreators.js | 44 +++++++++++++++++++++++++++++ src/stores/TagOrderStore.js | 19 ++++++------- 5 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 src/actions/MatrixActionCreators.js diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index a6012f5213a..fdd06b38c34 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -22,6 +22,7 @@ import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; import createMatrixClient from './utils/createMatrixClient'; import SettingsStore from './settings/SettingsStore'; +import MatrixActionCreators from './actions/MatrixActionCreators'; interface MatrixClientCreds { homeserverUrl: string, @@ -68,6 +69,8 @@ class MatrixClientPeg { unset() { this.matrixClient = null; + + MatrixActionCreators.stop(); } /** @@ -108,6 +111,9 @@ class MatrixClientPeg { // regardless of errors, start the client. If we did error out, we'll // just end up doing a full initial /sync. + // Connect the matrix client to the dispatcher + MatrixActionCreators.start(this.matrixClient); + console.log(`MatrixClientPeg: really starting MatrixClient`); this.get().startClient(opts); console.log(`MatrixClientPeg: MatrixClient started`); diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js index 30249f846c4..da321af18bc 100644 --- a/src/actions/GroupActions.js +++ b/src/actions/GroupActions.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ - import { createPromiseActionCreator } from './actionCreators'; const GroupActions = {}; diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js new file mode 100644 index 00000000000..86d4a3456c1 --- /dev/null +++ b/src/actions/MatrixActionCreators.js @@ -0,0 +1,39 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { createMatrixActionCreator } from './actionCreators'; + +// Events emitted from the matrixClient that we want to dispatch as actions +// via MatrixActionCreators. See createMatrixActionCreator. +const REGISTERED_EVENTS = [ + "accountData", +]; + +export default { + actionCreators: [], + actionCreatorsStop: [], + + start(matrixClient) { + this.actionCreators = REGISTERED_EVENTS.map((eventId) => + createMatrixActionCreator(matrixClient, eventId), + ); + this.actionCreatorsStop = this.actionCreators.map((ac) => ac()); + }, + + stop() { + this.actionCreatorsStop.map((stop) => stop()); + }, +}; diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index c4e20a0a3f2..5b2c25d023d 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -16,6 +16,16 @@ limitations under the License. import dis from '../dispatcher'; +/** + * Create an action creator that will dispatch actions asynchronously that + * indicate the current status of promise returned by the given function, fn. + * @param {string} id the id to give the dispatched actions. This is given a + * suffix determining whether it is pending, successful or + * a failure. + * @param {function} fn the function to call with arguments given to the + * returned function. This function should return a Promise. + * @returns a function that dispatches asynchronous actions when called. + */ export function createPromiseActionCreator(id, fn) { return (...args) => { dis.dispatch({action: id + '.pending'}); @@ -26,3 +36,37 @@ export function createPromiseActionCreator(id, fn) { }) } } + +/** + * Create an action creator that will listen to events of type eventId emitted + * by matrixClient and dispatch a corresponding action of the following shape: + * { + * action: 'MatrixActions.' + eventId, + * event: matrixEvent, + * event_type: matrixEvent.getType(), + * event_content: matrixEvent.getContent(), + * } + * @param matrixClient{MatrixClient} the matrix client with which to register + * a listener. + * @param eventId{string} the ID of the event that hen emitted will cause the + * an action to be dispatched. + * @returns a function that, when called, will begin to listen to dispatches + * from matrixClient. The result from that function can be called to + * stop listening. + */ +export function createMatrixActionCreator(matrixClient, eventId) { + const listener = (matrixEvent) => { + dis.dispatch({ + action: 'MatrixActions.' + eventId, + event: matrixEvent, + event_type: matrixEvent.getType(), + event_content: matrixEvent.getContent(), + }); + }; + return () => { + matrixClient.on(eventId, listener); + return () => { + matrixClient.removeListener(listener); + } + } +} diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 885eaafb387..a5eae3806ed 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -41,17 +41,14 @@ class TagOrderStore extends Store { __onDispatch(payload) { switch (payload.action) { - // Get ordering from account data, once the client has synced - case 'sync_state': - if (payload.prevState === "PREPARED" && payload.state === "SYNCING") { - const accountDataEvent = MatrixClientPeg.get().getAccountData('im.vector.web.tag_ordering'); - - const orderedTags = accountDataEvent && accountDataEvent.getContent() ? - accountDataEvent.getContent().tags : null; - - this._setState({orderedTags}); - } - break; + // Get ordering from account data + case 'MatrixActions.accountData': { + if (payload.event_type !== 'im.vector.web.tag_ordering') break; + this._setState({ + orderedTags: payload.event_content ? payload.event_content.tags : null, + }); + break; + } // Initialise the state such that if account data is unset, default to joined groups case 'GroupActions.fetchJoinedGroups.success': this._setState({ From 72550961e54b44295098af10d652e627e4b9a941 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 8 Dec 2017 10:52:20 +0000 Subject: [PATCH 13/39] Move 'commit_tags' to action creator --- src/actions/TagOrderActions.js | 33 +++++++++++++++++++++ src/components/structures/TagPanel.js | 6 ++++ src/components/views/elements/DNDTagTile.js | 4 +-- src/stores/TagOrderStore.js | 6 ---- 4 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 src/actions/TagOrderActions.js diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js new file mode 100644 index 00000000000..d98cf28ca84 --- /dev/null +++ b/src/actions/TagOrderActions.js @@ -0,0 +1,33 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Analytics from '../Analytics'; +import { createPromiseActionCreator } from './actionCreators'; +import TagOrderStore from '../stores/TagOrderStore'; + +const TagOrderActions = {}; + +TagOrderActions.commitTagOrdering = createPromiseActionCreator( + 'TagOrderActions.commitTagOrdering', + (matrixClient) => { + Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); + return matrixClient.setAccountData('im.vector.web.tag_ordering', { + tags: TagOrderStore.getOrderedTags(), + }); + }, +); + +export default TagOrderActions; diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index ae86bef4c16..127a220b261 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -22,6 +22,7 @@ import FlairStore from '../../stores/FlairStore'; import TagOrderStore from '../../stores/TagOrderStore'; import GroupActions from '../../actions/GroupActions'; +import TagOrderActions from '../../actions/TagOrderActions'; import sdk from '../../index'; import dis from '../../dispatcher'; @@ -91,6 +92,10 @@ const TagPanel = React.createClass({ dis.dispatch({action: 'view_create_group'}); }, + onTagTileEndDrag() { + TagOrderActions.commitTagOrdering(this.context.matrixClient); + }, + render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); @@ -101,6 +106,7 @@ const TagPanel = React.createClass({ key={groupProfile.groupId + '_' + index} groupProfile={groupProfile} selected={this.state.selectedTags.includes(groupProfile.groupId)} + onEndDrag={this.onTagTileEndDrag} />; }); return
diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js index 55c09a37200..4d035349805 100644 --- a/src/components/views/elements/DNDTagTile.js +++ b/src/components/views/elements/DNDTagTile.js @@ -38,9 +38,7 @@ const tagTileSource = { if (!monitor.didDrop() || !dropResult) { return; } - dis.dispatch({ - action: 'commit_tags', - }); + props.onEndDrag(); }, }; diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index a5eae3806ed..960839fa063 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -15,8 +15,6 @@ limitations under the License. */ import {Store} from 'flux/utils'; import dis from '../dispatcher'; -import Analytics from '../Analytics'; -import MatrixClientPeg from "../MatrixClientPeg"; const INITIAL_STATE = { orderedTags: null, @@ -71,10 +69,6 @@ class TagOrderStore extends Store { this._setState({orderedTags}); } break; - case 'commit_tags': - MatrixClientPeg.get().setAccountData('im.vector.web.tag_ordering', {tags: this._state.orderedTags}); - Analytics.trackEvent('TagOrderStore', 'commit_tags'); - break; } } From 31a52c15bd88927b0f15ae49bb327b4757f85a53 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 8 Dec 2017 10:55:29 +0000 Subject: [PATCH 14/39] Fix bug with removing matrix listeners --- src/actions/actionCreators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index 5b2c25d023d..547e45260c7 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -66,7 +66,7 @@ export function createMatrixActionCreator(matrixClient, eventId) { return () => { matrixClient.on(eventId, listener); return () => { - matrixClient.removeListener(listener); + matrixClient.removeListener(eventId, listener); } } } From 53e7232a9720e9d007e80272e19f467492bf22c5 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 8 Dec 2017 11:08:57 +0000 Subject: [PATCH 15/39] Linting --- src/actions/actionCreators.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index 547e45260c7..18a452f7f74 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -24,7 +24,7 @@ import dis from '../dispatcher'; * a failure. * @param {function} fn the function to call with arguments given to the * returned function. This function should return a Promise. - * @returns a function that dispatches asynchronous actions when called. + * @returns {function} a function that dispatches asynchronous actions when called. */ export function createPromiseActionCreator(id, fn) { return (...args) => { @@ -33,8 +33,8 @@ export function createPromiseActionCreator(id, fn) { dis.dispatch({action: id + '.success', result}); }).catch((err) => { dis.dispatch({action: id + '.failure', err}); - }) - } + }); + }; } /** @@ -46,13 +46,13 @@ export function createPromiseActionCreator(id, fn) { * event_type: matrixEvent.getType(), * event_content: matrixEvent.getContent(), * } - * @param matrixClient{MatrixClient} the matrix client with which to register - * a listener. - * @param eventId{string} the ID of the event that hen emitted will cause the - * an action to be dispatched. - * @returns a function that, when called, will begin to listen to dispatches - * from matrixClient. The result from that function can be called to - * stop listening. + * @param {MatrixClient} matrixClient the matrix client with which to register + * a listener. + * @param {string} eventId the ID of the event that hen emitted will cause the + * an action to be dispatched. + * @returns {function} a function that, when called, will begin to listen to + * dispatches from matrixClient. The result from that + * function can be called to stop listening. */ export function createMatrixActionCreator(matrixClient, eventId) { const listener = (matrixEvent) => { @@ -67,6 +67,6 @@ export function createMatrixActionCreator(matrixClient, eventId) { matrixClient.on(eventId, listener); return () => { matrixClient.removeListener(eventId, listener); - } - } + }; + }; } From 8f0774496f887670770cf8fd0ee8b46d1d30b70f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 8 Dec 2017 11:29:21 +0000 Subject: [PATCH 16/39] Remove redundant MatrixChat --- src/components/structures/MatrixChat.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7c41a67fcaa..802da74bcf1 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -84,7 +84,7 @@ const ONBOARDING_FLOW_STARTERS = [ 'view_create_group', ]; -const MatrixChat = React.createClass({ +export default React.createClass({ // we export this so that the integration tests can use it :-S statics: { VIEWS: VIEWS, @@ -1584,5 +1584,3 @@ const MatrixChat = React.createClass({ console.error(`Unknown view ${this.state.view}`); }, }); - -export default MatrixChat; From df88b71dbb9899fd834f3bd7c7fc45f1b288d4ef Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 8 Dec 2017 16:47:52 +0000 Subject: [PATCH 17/39] Comment typo --- src/actions/actionCreators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index 18a452f7f74..d3c626c64ca 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -48,7 +48,7 @@ export function createPromiseActionCreator(id, fn) { * } * @param {MatrixClient} matrixClient the matrix client with which to register * a listener. - * @param {string} eventId the ID of the event that hen emitted will cause the + * @param {string} eventId the ID of the event that when emitted will cause the * an action to be dispatched. * @returns {function} a function that, when called, will begin to listen to * dispatches from matrixClient. The result from that From 991ea4ebe539cd62b610552f7b8be04cc32df7c1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 11 Dec 2017 17:07:31 +0000 Subject: [PATCH 18/39] Fix a few bugs with TagOrderStore: - Have TagOrderStore listen for MatrixSync actions so that it can initialise tag ordering state. - Expose an empty list until the client has done its first sync and has fetched list of joined groups --- src/actions/MatrixActionCreators.js | 7 ++++- src/actions/actionCreators.js | 33 +++++++++++++++++++++++ src/components/structures/TagPanel.js | 2 +- src/stores/TagOrderStore.js | 38 +++++++++++++++++++++------ 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index 86d4a3456c1..0f02e7730e8 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -14,7 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { createMatrixActionCreator } from './actionCreators'; +import { + createMatrixActionCreator, + createMatrixSyncActionCreator, +} from './actionCreators'; // Events emitted from the matrixClient that we want to dispatch as actions // via MatrixActionCreators. See createMatrixActionCreator. @@ -30,6 +33,8 @@ export default { this.actionCreators = REGISTERED_EVENTS.map((eventId) => createMatrixActionCreator(matrixClient, eventId), ); + this.actionCreators.push(createMatrixSyncActionCreator(matrixClient)); + this.actionCreatorsStop = this.actionCreators.map((ac) => ac()); }, diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index d3c626c64ca..60ff368fc83 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -70,3 +70,36 @@ export function createMatrixActionCreator(matrixClient, eventId) { }; }; } + +// TODO: migrate from sync_state to MatrixSync so that more js-sdk events +// become dispatches in the same place. +/** + * Create an action creator that will listen to `sync` events emitted + * by matrixClient and dispatch a corresponding MatrixSync action. E.g: + * { + * action: 'MatrixSync', + * state: 'SYNCING', + * prevState: 'PREPARED' + * } + * @param {MatrixClient} matrixClient the matrix client with which to register + * a listener. + * @returns {function} a function that, when called, will begin to listen to + * dispatches from matrixClient. The result from that + * function can be called to stop listening. + */ +export function createMatrixSyncActionCreator(matrixClient) { + const listener = (state, prevState) => { + dis.dispatch({ + action: 'MatrixSync', + state, + prevState, + matrixClient, + }); + }; + return () => { + matrixClient.on('sync', listener); + return () => { + matrixClient.removeListener('sync', listener); + }; + }; +} diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 127a220b261..88bb39406ed 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -58,7 +58,7 @@ const TagPanel = React.createClass({ return; } - const orderedTags = TagOrderStore.getOrderedTags() || TagOrderStore.getAllTags(); + const orderedTags = TagOrderStore.getOrderedTags(); const orderedGroupTags = orderedTags.filter((t) => t[0] === '+'); Promise.all(orderedGroupTags.map( (groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId), diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 960839fa063..9741d59d4e3 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -18,7 +18,9 @@ import dis from '../dispatcher'; const INITIAL_STATE = { orderedTags: null, - allTags: null, + orderedTagsAccountData: null, + hasSynced: false, + joinedGroupIds: null, }; /** @@ -39,25 +41,42 @@ class TagOrderStore extends Store { __onDispatch(payload) { switch (payload.action) { + // Initialise state after initial sync + case 'MatrixSync': { + if (!(payload.prevState === 'PREPARED' && payload.state === 'SYNCING')) { + break; + } + const tagOrderingEvent = payload.matrixClient.getAccountData('im.vector.web.tag_ordering'); + const tagOrderingEventContent = tagOrderingEvent ? tagOrderingEvent.getContent() : {}; + this._setState({ + orderedTagsAccountData: tagOrderingEventContent.tags || null, + hasSynced: true, + }); + this._updateOrderedTags(); + break; + } // Get ordering from account data case 'MatrixActions.accountData': { if (payload.event_type !== 'im.vector.web.tag_ordering') break; this._setState({ - orderedTags: payload.event_content ? payload.event_content.tags : null, + orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null, }); + this._updateOrderedTags(); break; } // Initialise the state such that if account data is unset, default to joined groups case 'GroupActions.fetchJoinedGroups.success': this._setState({ - allTags: payload.result.groups.sort(), // Sort lexically + joinedGroupIds: payload.result.groups.sort(), // Sort lexically + hasFetchedJoinedGroups: true, }); + this._updateOrderedTags(); break; // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag case 'order_tag': { if (!payload.tag || !payload.targetTag || payload.tag === payload.targetTag) return; - const tags = this._state.orderedTags || this._state.allTags; + const tags = this._state.orderedTags; let orderedTags = tags.filter((t) => t !== payload.tag); const newIndex = orderedTags.indexOf(payload.targetTag) + (payload.after ? 1 : 0); @@ -72,12 +91,15 @@ class TagOrderStore extends Store { } } - getOrderedTags() { - return this._state.orderedTags; + _updateOrderedTags() { + this._setState({ + orderedTags: this._state.hasSynced && this._state.hasFetchedJoinedGroups ? + this._state.orderedTagsAccountData || this._state.joinedGroupIds : [], + }); } - getAllTags() { - return this._state.allTags; + getOrderedTags() { + return this._state.orderedTags; } } From aa914098dc4f1ec343cdbbf3be34a7c73386776c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 11 Dec 2017 17:19:29 +0000 Subject: [PATCH 19/39] Return null if TagOrderStore is loading The view should decide the default state. --- src/components/structures/TagPanel.js | 2 +- src/stores/TagOrderStore.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 88bb39406ed..59a658c1b4f 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -58,7 +58,7 @@ const TagPanel = React.createClass({ return; } - const orderedTags = TagOrderStore.getOrderedTags(); + const orderedTags = TagOrderStore.getOrderedTags() || []; const orderedGroupTags = orderedTags.filter((t) => t[0] === '+'); Promise.all(orderedGroupTags.map( (groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId), diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 9741d59d4e3..108eb434a26 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -94,7 +94,7 @@ class TagOrderStore extends Store { _updateOrderedTags() { this._setState({ orderedTags: this._state.hasSynced && this._state.hasFetchedJoinedGroups ? - this._state.orderedTagsAccountData || this._state.joinedGroupIds : [], + this._state.orderedTagsAccountData || this._state.joinedGroupIds : null, }); } From 0b38bf5e7b1cd783c0bbf5dc045fe1e4f0f2681c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 11 Dec 2017 17:24:33 +0000 Subject: [PATCH 20/39] Do not allow ordering until TagOrderStore has loaded --- src/stores/TagOrderStore.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 108eb434a26..cc1ce8625e5 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -74,7 +74,11 @@ class TagOrderStore extends Store { break; // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag case 'order_tag': { - if (!payload.tag || !payload.targetTag || payload.tag === payload.targetTag) return; + if (!this._state.orderedTags || + !payload.tag || + !payload.targetTag || + payload.tag === payload.targetTag + ) return; const tags = this._state.orderedTags; From 8d2d3e62cd5283eeadce8d32b2ef4dda280f7f26 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 11 Dec 2017 17:30:10 +0000 Subject: [PATCH 21/39] Only commit a non-falsy tags list --- src/actions/TagOrderActions.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js index d98cf28ca84..76d48bff996 100644 --- a/src/actions/TagOrderActions.js +++ b/src/actions/TagOrderActions.js @@ -23,10 +23,14 @@ const TagOrderActions = {}; TagOrderActions.commitTagOrdering = createPromiseActionCreator( 'TagOrderActions.commitTagOrdering', (matrixClient) => { + // Only commit tags if the state is ready, i.e. not null + const tags = TagOrderStore.getOrderedTags(); + if (!tags) { + return; + } + Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); - return matrixClient.setAccountData('im.vector.web.tag_ordering', { - tags: TagOrderStore.getOrderedTags(), - }); + return matrixClient.setAccountData('im.vector.web.tag_ordering', {tags}); }, ); From a12033513056838a0ef22ae92057c70f93ed0279 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 11 Dec 2017 18:03:19 +0000 Subject: [PATCH 22/39] Handle groups being joined and left --- src/components/structures/TagPanel.js | 1 + src/stores/TagOrderStore.js | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 59a658c1b4f..9bf5a127317 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -60,6 +60,7 @@ const TagPanel = React.createClass({ const orderedTags = TagOrderStore.getOrderedTags() || []; const orderedGroupTags = orderedTags.filter((t) => t[0] === '+'); + // XXX: One profile lookup failing will bring the whole lot down Promise.all(orderedGroupTags.map( (groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId), )).then((orderedGroupTagProfiles) => { diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index cc1ce8625e5..2ed6ff6e75f 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -97,11 +97,28 @@ class TagOrderStore extends Store { _updateOrderedTags() { this._setState({ - orderedTags: this._state.hasSynced && this._state.hasFetchedJoinedGroups ? - this._state.orderedTagsAccountData || this._state.joinedGroupIds : null, + orderedTags: + this._state.hasSynced && + this._state.hasFetchedJoinedGroups ? + this._mergeGroupsAndTags() : null, }); } + _mergeGroupsAndTags() { + const groupIds = this._state.joinedGroupIds || []; + const tags = this._state.orderedTagsAccountData || []; + + const tagsToKeep = tags.filter( + (t) => t[0] !== '+' || groupIds.includes(t), + ); + + const groupIdsToAdd = groupIds.filter( + (groupId) => !tags.includes(groupId), + ); + + return tagsToKeep.concat(groupIdsToAdd); + } + getOrderedTags() { return this._state.orderedTags; } From 3e532e3722126a541258888aa1453c7259894fed Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 12 Dec 2017 14:10:39 +0000 Subject: [PATCH 23/39] Use consistent indentation and break;s in TagOrderStore switch --- src/stores/TagOrderStore.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 2ed6ff6e75f..819ad5abfe4 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -65,13 +65,14 @@ class TagOrderStore extends Store { break; } // Initialise the state such that if account data is unset, default to joined groups - case 'GroupActions.fetchJoinedGroups.success': + case 'GroupActions.fetchJoinedGroups.success': { this._setState({ joinedGroupIds: payload.result.groups.sort(), // Sort lexically hasFetchedJoinedGroups: true, }); this._updateOrderedTags(); - break; + break; + } // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag case 'order_tag': { if (!this._state.orderedTags || @@ -90,8 +91,8 @@ class TagOrderStore extends Store { ...orderedTags.slice(newIndex), ]; this._setState({orderedTags}); + break; } - break; } } From 60d8ebb914f3c44fa0db5a98ca977c8b4a2aecd4 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 12 Dec 2017 16:05:18 +0000 Subject: [PATCH 24/39] Refactor MatrixActions to something much easier to grok. --- src/actions/MatrixActionCreators.js | 68 +++++++++++++++++++++-------- src/actions/actionCreators.js | 67 ---------------------------- src/stores/TagOrderStore.js | 2 +- 3 files changed, 52 insertions(+), 85 deletions(-) diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index 0f02e7730e8..0b42938cafd 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -14,31 +14,65 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { - createMatrixActionCreator, - createMatrixSyncActionCreator, -} from './actionCreators'; +import dis from '../dispatcher'; -// Events emitted from the matrixClient that we want to dispatch as actions -// via MatrixActionCreators. See createMatrixActionCreator. -const REGISTERED_EVENTS = [ - "accountData", -]; +// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events +// become dispatches in the same place. +/** + * An action creator that will map a `sync` event to a MatrixActions.sync action, + * each parameter mapping to a key-value in the action. + * + * @param {MatrixClient} matrixClient the matrix client + * @param {string} state the current sync state. + * @param {string} prevState the previous sync state. + * @returns {Object} an action of type MatrixActions.sync. + */ +function createSyncAction(matrixClient, state, prevState) { + return { + action: 'MatrixActions.sync', + state, + prevState, + matrixClient, + }; +} + +/** + * An action creator that will map an account data matrix event to a + * MatrixActions.accountData action. + * + * @param {MatrixClient} matrixClient the matrix client with which to + * register a listener. + * @param {MatrixEvent} accountDataEvent the account data event. + * @returns {Object} an action of type MatrixActions.accountData. + */ +function createAccountDataAction(matrixClient, accountDataEvent) { + return { + action: 'MatrixActions.accountData', + event: accountDataEvent, + event_type: accountDataEvent.getType(), + event_content: accountDataEvent.getContent(), + }; +} export default { - actionCreators: [], - actionCreatorsStop: [], + _matrixClientListenersStop: [], start(matrixClient) { - this.actionCreators = REGISTERED_EVENTS.map((eventId) => - createMatrixActionCreator(matrixClient, eventId), - ); - this.actionCreators.push(createMatrixSyncActionCreator(matrixClient)); + this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); + this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); + }, - this.actionCreatorsStop = this.actionCreators.map((ac) => ac()); + _addMatrixClientListener(matrixClient, eventName, actionCreator) { + const listener = (...args) => { + dis.dispatch(actionCreator(matrixClient, ...args)); + }; + matrixClient.on(eventName, listener); + this._matrixClientListenersStop.push(() => { + matrixClient.removeListener(eventName, listener); + }); }, stop() { - this.actionCreatorsStop.map((stop) => stop()); + this._matrixClientListenersStop.forEach((stopListener) => stopListener()); }, }; diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index 60ff368fc83..b92e8ed9503 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -36,70 +36,3 @@ export function createPromiseActionCreator(id, fn) { }); }; } - -/** - * Create an action creator that will listen to events of type eventId emitted - * by matrixClient and dispatch a corresponding action of the following shape: - * { - * action: 'MatrixActions.' + eventId, - * event: matrixEvent, - * event_type: matrixEvent.getType(), - * event_content: matrixEvent.getContent(), - * } - * @param {MatrixClient} matrixClient the matrix client with which to register - * a listener. - * @param {string} eventId the ID of the event that when emitted will cause the - * an action to be dispatched. - * @returns {function} a function that, when called, will begin to listen to - * dispatches from matrixClient. The result from that - * function can be called to stop listening. - */ -export function createMatrixActionCreator(matrixClient, eventId) { - const listener = (matrixEvent) => { - dis.dispatch({ - action: 'MatrixActions.' + eventId, - event: matrixEvent, - event_type: matrixEvent.getType(), - event_content: matrixEvent.getContent(), - }); - }; - return () => { - matrixClient.on(eventId, listener); - return () => { - matrixClient.removeListener(eventId, listener); - }; - }; -} - -// TODO: migrate from sync_state to MatrixSync so that more js-sdk events -// become dispatches in the same place. -/** - * Create an action creator that will listen to `sync` events emitted - * by matrixClient and dispatch a corresponding MatrixSync action. E.g: - * { - * action: 'MatrixSync', - * state: 'SYNCING', - * prevState: 'PREPARED' - * } - * @param {MatrixClient} matrixClient the matrix client with which to register - * a listener. - * @returns {function} a function that, when called, will begin to listen to - * dispatches from matrixClient. The result from that - * function can be called to stop listening. - */ -export function createMatrixSyncActionCreator(matrixClient) { - const listener = (state, prevState) => { - dis.dispatch({ - action: 'MatrixSync', - state, - prevState, - matrixClient, - }); - }; - return () => { - matrixClient.on('sync', listener); - return () => { - matrixClient.removeListener('sync', listener); - }; - }; -} diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 819ad5abfe4..2fdfbe5381b 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -42,7 +42,7 @@ class TagOrderStore extends Store { __onDispatch(payload) { switch (payload.action) { // Initialise state after initial sync - case 'MatrixSync': { + case 'MatrixActions.sync': { if (!(payload.prevState === 'PREPARED' && payload.state === 'SYNCING')) { break; } From 13925db2514d896acdca019c54e20883bfc47c59 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 12 Dec 2017 17:32:43 +0000 Subject: [PATCH 25/39] Refactor to allow dispatching of two kinds of Actions They are: 1. The existing type of Action, Objects with an `action` type. 1. Asyncronous Actions, functions that accept a `dispatch` argument, which can be used to dispatch Actions asyncronously. --- src/actions/GroupActions.js | 9 ++++----- src/actions/TagOrderActions.js | 11 +++++------ src/actions/actionCreators.js | 28 +++++++++++++-------------- src/components/structures/TagPanel.js | 6 +++--- src/dispatcher.js | 14 ++++++++++++-- 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js index da321af18bc..7ee5a40cdb8 100644 --- a/src/actions/GroupActions.js +++ b/src/actions/GroupActions.js @@ -14,13 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { createPromiseActionCreator } from './actionCreators'; +import { asyncAction } from './actionCreators'; const GroupActions = {}; -GroupActions.fetchJoinedGroups = createPromiseActionCreator( - 'GroupActions.fetchJoinedGroups', - (matrixClient) => matrixClient.getJoinedGroups(), -); +GroupActions.fetchJoinedGroups = function(matrixClient) { + return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups()); +}; export default GroupActions; diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js index 76d48bff996..cda28991049 100644 --- a/src/actions/TagOrderActions.js +++ b/src/actions/TagOrderActions.js @@ -15,14 +15,13 @@ limitations under the License. */ import Analytics from '../Analytics'; -import { createPromiseActionCreator } from './actionCreators'; +import { asyncAction } from './actionCreators'; import TagOrderStore from '../stores/TagOrderStore'; const TagOrderActions = {}; -TagOrderActions.commitTagOrdering = createPromiseActionCreator( - 'TagOrderActions.commitTagOrdering', - (matrixClient) => { +TagOrderActions.commitTagOrdering = function(matrixClient) { + return asyncAction('TagOrderActions.commitTagOrdering', () => { // Only commit tags if the state is ready, i.e. not null const tags = TagOrderStore.getOrderedTags(); if (!tags) { @@ -31,7 +30,7 @@ TagOrderActions.commitTagOrdering = createPromiseActionCreator( Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); return matrixClient.setAccountData('im.vector.web.tag_ordering', {tags}); - }, -); + }); +}; export default TagOrderActions; diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index b92e8ed9503..8e1423d6c34 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -14,25 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from '../dispatcher'; - /** - * Create an action creator that will dispatch actions asynchronously that - * indicate the current status of promise returned by the given function, fn. + * Create an asynchronous action creator that will dispatch actions indicating + * the current status of the promise returned by fn. * @param {string} id the id to give the dispatched actions. This is given a - * suffix determining whether it is pending, successful or - * a failure. - * @param {function} fn the function to call with arguments given to the - * returned function. This function should return a Promise. - * @returns {function} a function that dispatches asynchronous actions when called. + * suffix determining whether it is pending, successful or + * a failure. + * @param {function} fn a function that returns a Promise. + * @returns {function} a function that uses its single argument as a dispatch + * function. */ -export function createPromiseActionCreator(id, fn) { - return (...args) => { - dis.dispatch({action: id + '.pending'}); - fn(...args).then((result) => { - dis.dispatch({action: id + '.success', result}); +export function asyncAction(id, fn) { + return (dispatch) => { + dispatch({action: id + '.pending'}); + fn().then((result) => { + dispatch({action: id + '.success', result}); }).catch((err) => { - dis.dispatch({action: id + '.failure', err}); + dispatch({action: id + '.failure', err}); }); }; } diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 9bf5a127317..e3b8c700279 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -68,7 +68,7 @@ const TagPanel = React.createClass({ }); }); // This could be done by anything with a matrix client - GroupActions.fetchJoinedGroups(this.context.matrixClient); + dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient)); }, componentWillUnmount() { @@ -81,7 +81,7 @@ const TagPanel = React.createClass({ _onGroupMyMembership() { if (this.unmounted) return; - GroupActions.fetchJoinedGroups(this.context.matrixClient); + dis.dispatch(GroupActions.fetchJoinedGroups.bind(this.context.matrixClient)); }, onClick() { @@ -94,7 +94,7 @@ const TagPanel = React.createClass({ }, onTagTileEndDrag() { - TagOrderActions.commitTagOrdering(this.context.matrixClient); + dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient)); }, render() { diff --git a/src/dispatcher.js b/src/dispatcher.js index be74dc856eb..50585b969d2 100644 --- a/src/dispatcher.js +++ b/src/dispatcher.js @@ -20,14 +20,24 @@ const flux = require("flux"); class MatrixDispatcher extends flux.Dispatcher { /** - * @param {Object} payload Required. The payload to dispatch. - * Must contain at least an 'action' key. + * @param {Object|function} payload Required. The payload to dispatch. + * If an Object, must contain at least an 'action' key. + * If a function, must have the signature (dispatch) => {...}. * @param {boolean=} sync Optional. Pass true to dispatch * synchronously. This is useful for anything triggering * an operation that the browser requires user interaction * for. */ dispatch(payload, sync) { + // Allow for asynchronous dispatching by accepting payloads that have the + // type `function (dispatch) {...}` + if (typeof payload === 'function') { + payload((action) => { + this.dispatch(action, sync); + }); + return; + } + if (sync) { super.dispatch(payload); } else { From d5534a9ecec45d1020f0dcf11b1a5918b2969946 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 13 Dec 2017 10:17:38 +0000 Subject: [PATCH 26/39] Copyright --- src/MatrixClientPeg.js | 1 + src/actions/GroupActions.js | 2 +- src/actions/MatrixActionCreators.js | 2 +- src/actions/TagOrderActions.js | 2 +- src/actions/actionCreators.js | 2 +- src/dispatcher.js | 1 + src/stores/TagOrderStore.js | 2 +- 7 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index fdd06b38c34..14dfa91fa48 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations 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. diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js index 7ee5a40cdb8..8e5484bb8cc 100644 --- a/src/actions/GroupActions.js +++ b/src/actions/GroupActions.js @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations 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. diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index 0b42938cafd..3473d0ca2f5 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations 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. diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js index cda28991049..8b79f882762 100644 --- a/src/actions/TagOrderActions.js +++ b/src/actions/TagOrderActions.js @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations 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. diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index 8e1423d6c34..60c5031f956 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations 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. diff --git a/src/dispatcher.js b/src/dispatcher.js index 50585b969d2..48c8dc86e94 100644 --- a/src/dispatcher.js +++ b/src/dispatcher.js @@ -1,5 +1,6 @@ /* Copyright 2015, 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. diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 2fdfbe5381b..820634b90e8 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations 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. From cc30b8fb09c1e4242bf122a811d6e510b7dabc0d Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 13 Dec 2017 10:37:14 +0000 Subject: [PATCH 27/39] Doc MatrixActionCreators properly --- src/actions/MatrixActionCreators.js | 32 +++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index 3473d0ca2f5..d1be8abc610 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -19,8 +19,7 @@ import dis from '../dispatcher'; // TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events // become dispatches in the same place. /** - * An action creator that will map a `sync` event to a MatrixActions.sync action, - * each parameter mapping to a key-value in the action. + * Create a MatrixActions.sync action that represents a MatrixClient `sync` event. * * @param {MatrixClient} matrixClient the matrix client * @param {string} state the current sync state. @@ -37,11 +36,10 @@ function createSyncAction(matrixClient, state, prevState) { } /** - * An action creator that will map an account data matrix event to a - * MatrixActions.accountData action. + * Create a MatrixActions.accountData action that represents a MatrixClient `accountData` + * matrix event. * - * @param {MatrixClient} matrixClient the matrix client with which to - * register a listener. + * @param {MatrixClient} matrixClient the matrix client. * @param {MatrixEvent} accountDataEvent the account data event. * @returns {Object} an action of type MatrixActions.accountData. */ @@ -54,14 +52,33 @@ function createAccountDataAction(matrixClient, accountDataEvent) { }; } +/** + * This object is responsible for dispatching actions when certain events are emitted by + * the given MatrixClient. + */ export default { + // A list of callbacks to call to unregister all listeners added _matrixClientListenersStop: [], + /** + * Start listening to certain events from the MatrixClient and dispatch actions when + * they are emitted. + * @param {MatrixClient} matrixClient the MatrixClient to listen to events from + */ start(matrixClient) { this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); }, + /** + * Start listening to events emitted by matrixClient, dispatch an action created by the + * actionCreator function. + * @param {MatrixClient} matrixClient a MatrixClient to register a listener with. + * @param {string} eventName the event to listen to on MatrixClient. + * @param {function} actionCreator a function that should return an action to dispatch + * when given the arguments emitted in the MatrixClient + * event. + */ _addMatrixClientListener(matrixClient, eventName, actionCreator) { const listener = (...args) => { dis.dispatch(actionCreator(matrixClient, ...args)); @@ -72,6 +89,9 @@ export default { }); }, + /** + * Stop listening to events. + */ stop() { this._matrixClientListenersStop.forEach((stopListener) => stopListener()); }, From 5de05591923fb30900ef419f56fd3acb6a0f2e80 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 13 Dec 2017 10:39:45 +0000 Subject: [PATCH 28/39] Adjust actionCreators doc --- src/actions/actionCreators.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index 60c5031f956..624401b9ce6 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -21,8 +21,8 @@ limitations under the License. * suffix determining whether it is pending, successful or * a failure. * @param {function} fn a function that returns a Promise. - * @returns {function} a function that uses its single argument as a dispatch - * function. + * @returns {function} an asyncronous action creator - a function that uses its + * single argument as a dispatch function. */ export function asyncAction(id, fn) { return (dispatch) => { From 42c1f3cfe24219eadd603a1ad550341b6daf79e0 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 13 Dec 2017 10:41:24 +0000 Subject: [PATCH 29/39] Fix incorrect bind --- src/components/structures/TagPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index e3b8c700279..89bb0a8605e 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -81,7 +81,7 @@ const TagPanel = React.createClass({ _onGroupMyMembership() { if (this.unmounted) return; - dis.dispatch(GroupActions.fetchJoinedGroups.bind(this.context.matrixClient)); + dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient)); }, onClick() { From a8b245d0cf0538c22a144f44a72de17437fdb159 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 13 Dec 2017 10:42:11 +0000 Subject: [PATCH 30/39] Add unmounted guard --- src/components/structures/TagPanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 89bb0a8605e..dbb75a28792 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -64,6 +64,7 @@ const TagPanel = React.createClass({ Promise.all(orderedGroupTags.map( (groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId), )).then((orderedGroupTagProfiles) => { + if (this.unmounted) return; this.setState({orderedGroupTagProfiles}); }); }); From f38690f265f3d72cc02e8b5f92f6b6a12ce091e7 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 13 Dec 2017 10:51:04 +0000 Subject: [PATCH 31/39] Doc orderedGroupTagProfiles --- src/components/structures/TagPanel.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index dbb75a28792..74383bfea58 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -36,7 +36,15 @@ const TagPanel = React.createClass({ getInitialState() { return { - orderedGroupTagProfiles: [], + // A list of group profiles for group tags + orderedGroupTagProfiles: [ + // { + // groupId: '+awesome:foo.bar',{ + // name: 'My Awesome Community', + // avatarUrl: 'mxc://...', + // shortDescription: 'Some description...', + // }, + ], selectedTags: [], }; }, From e1ea8f0a780efda638046f6ef5c662c02972ed03 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 13 Dec 2017 10:57:47 +0000 Subject: [PATCH 32/39] Copy state when initialisng, reset state when logging out --- src/stores/TagOrderStore.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 820634b90e8..633ffc7e9c5 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -31,7 +31,7 @@ class TagOrderStore extends Store { super(dis); // Initialise state - this._state = INITIAL_STATE; + this._state = Object.assign({}, INITIAL_STATE); } _setState(newState) { @@ -93,6 +93,12 @@ class TagOrderStore extends Store { this._setState({orderedTags}); break; } + case 'on_logged_out': { + // Reset state without pushing an update to the view, which generally assumes that + // the matrix client isn't `null` and so causing a re-render will cause NPEs. + this._state = Object.assign({}, INITIAL_STATE); + break; + } } } From a653ece99e089c602b861f07d7c2c717606e5905 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 13 Dec 2017 11:03:21 +0000 Subject: [PATCH 33/39] Doc commitTagOrdering --- src/actions/TagOrderActions.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js index 8b79f882762..00f724d9a22 100644 --- a/src/actions/TagOrderActions.js +++ b/src/actions/TagOrderActions.js @@ -20,6 +20,16 @@ import TagOrderStore from '../stores/TagOrderStore'; const TagOrderActions = {}; +/** + * Create a TagOrderActions.commitTagOrdering action that represents an + * asyncronous request to commit TagOrderStore.getOrderedTags() to account + * data. + * + * @param {MatrixClient} matrixClient the matrix client to set the account + * data on. + * @returns {function} an asyncronous action of type + * TagOrderActions.commitTagOrdering. + */ TagOrderActions.commitTagOrdering = function(matrixClient) { return asyncAction('TagOrderActions.commitTagOrdering', () => { // Only commit tags if the state is ready, i.e. not null From ddf5dbad89a4991cad0a8208cd7a55c821775e43 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 13 Dec 2017 11:05:23 +0000 Subject: [PATCH 34/39] Doc fetchJoinedGroups --- src/actions/GroupActions.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js index 8e5484bb8cc..f3337eda25f 100644 --- a/src/actions/GroupActions.js +++ b/src/actions/GroupActions.js @@ -18,6 +18,14 @@ import { asyncAction } from './actionCreators'; const GroupActions = {}; +/** + * Create a GroupActions.fetchJoinedGroups action that represents an + * asyncronous request to fetch the groups to which a user is joined. + * + * @param {MatrixClient} matrixClient the matrix client to query. + * @returns {function} an asyncronous action of type + * GroupActions.fetchJoinedGroups. + */ GroupActions.fetchJoinedGroups = function(matrixClient) { return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups()); }; From 31ea092d996f72bed567998307774ab894539230 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 13 Dec 2017 15:39:17 +0000 Subject: [PATCH 35/39] Improve createAccountDataAction docs --- src/actions/MatrixActionCreators.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index d1be8abc610..30276e518f5 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -19,7 +19,8 @@ import dis from '../dispatcher'; // TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events // become dispatches in the same place. /** - * Create a MatrixActions.sync action that represents a MatrixClient `sync` event. + * Create a MatrixActions.sync action that represents a MatrixClient `sync` event, + * each parameter mapping to a key-value in the action. * * @param {MatrixClient} matrixClient the matrix client * @param {string} state the current sync state. @@ -35,13 +36,22 @@ function createSyncAction(matrixClient, state, prevState) { }; } +/** + * @typedef AccountDataAction + * @type {Object} + * @property {string} action 'MatrixActions.accountData'. + * @property {MatrixEvent} event the MatrixEvent that triggered the dispatch. + * @property {string} event_type the type of the MatrixEvent, e.g. "m.direct". + * @property {Object} event_content the content of the MatrixEvent. + */ + /** * Create a MatrixActions.accountData action that represents a MatrixClient `accountData` * matrix event. * * @param {MatrixClient} matrixClient the matrix client. * @param {MatrixEvent} accountDataEvent the account data event. - * @returns {Object} an action of type MatrixActions.accountData. + * @returns {AccountDataAction} an action of type MatrixActions.accountData. */ function createAccountDataAction(matrixClient, accountDataEvent) { return { From fe6b7c0ea2b3ad518d3bbd6f14dbec2c0a31103b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 13 Dec 2017 15:43:39 +0000 Subject: [PATCH 36/39] Improve _addMatrixClientListener docs --- src/actions/MatrixActionCreators.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index 30276e518f5..e82ca41d700 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -81,8 +81,8 @@ export default { }, /** - * Start listening to events emitted by matrixClient, dispatch an action created by the - * actionCreator function. + * Start listening to events of type eventName on matrixClient and when they are emitted, + * dispatch an action created by the actionCreator function. * @param {MatrixClient} matrixClient a MatrixClient to register a listener with. * @param {string} eventName the event to listen to on MatrixClient. * @param {function} actionCreator a function that should return an action to dispatch From 950f591b3f1a2c8bcb9a25380880b29e2cb06b65 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 13 Dec 2017 15:50:20 +0000 Subject: [PATCH 37/39] Clarify more docs --- src/actions/MatrixActionCreators.js | 4 ++-- src/components/structures/TagPanel.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index e82ca41d700..33bdb53799f 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -86,8 +86,8 @@ export default { * @param {MatrixClient} matrixClient a MatrixClient to register a listener with. * @param {string} eventName the event to listen to on MatrixClient. * @param {function} actionCreator a function that should return an action to dispatch - * when given the arguments emitted in the MatrixClient - * event. + * when given the MatrixClient as an argument as well as + * arguments emitted in the MatrixClient event. */ _addMatrixClientListener(matrixClient, eventName, actionCreator) { const listener = (...args) => { diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 74383bfea58..49d22d8e529 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -36,7 +36,9 @@ const TagPanel = React.createClass({ getInitialState() { return { - // A list of group profiles for group tags + // A list of group profiles for tags that are group IDs. The intention in future + // is to allow arbitrary tags to be selected in the TagPanel, not just groups. + // For now, it suffices to maintain a list of ordered group profiles. orderedGroupTagProfiles: [ // { // groupId: '+awesome:foo.bar',{ From 6b02f59fb789056509069562a527ccb3f63b1c01 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 13 Dec 2017 17:32:46 +0000 Subject: [PATCH 38/39] Spelling --- src/actions/GroupActions.js | 6 +++--- src/actions/TagOrderActions.js | 2 +- src/actions/actionCreators.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js index f3337eda25f..d41d82d8bfa 100644 --- a/src/actions/GroupActions.js +++ b/src/actions/GroupActions.js @@ -19,11 +19,11 @@ import { asyncAction } from './actionCreators'; const GroupActions = {}; /** - * Create a GroupActions.fetchJoinedGroups action that represents an - * asyncronous request to fetch the groups to which a user is joined. + * Create a GroupActions.fetchJoinedGroups action that represents an + * asynchronous request to fetch the groups to which a user is joined. * * @param {MatrixClient} matrixClient the matrix client to query. - * @returns {function} an asyncronous action of type + * @returns {function} an asynchronous action of type * GroupActions.fetchJoinedGroups. */ GroupActions.fetchJoinedGroups = function(matrixClient) { diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js index 00f724d9a22..038cda1b356 100644 --- a/src/actions/TagOrderActions.js +++ b/src/actions/TagOrderActions.js @@ -27,7 +27,7 @@ const TagOrderActions = {}; * * @param {MatrixClient} matrixClient the matrix client to set the account * data on. - * @returns {function} an asyncronous action of type + * @returns {function} an asynchronous action of type * TagOrderActions.commitTagOrdering. */ TagOrderActions.commitTagOrdering = function(matrixClient) { diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index 624401b9ce6..bebd368f95f 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -21,8 +21,8 @@ limitations under the License. * suffix determining whether it is pending, successful or * a failure. * @param {function} fn a function that returns a Promise. - * @returns {function} an asyncronous action creator - a function that uses its - * single argument as a dispatch function. + * @returns {function} an asynchronous action creator - a function that uses + * its single argument as a dispatch function. */ export function asyncAction(id, fn) { return (dispatch) => { From 629cd13319ef5da567d12f09ffc00188a6581a92 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 13 Dec 2017 18:28:43 +0000 Subject: [PATCH 39/39] Even better docs --- src/actions/GroupActions.js | 9 +++++---- src/actions/TagOrderActions.js | 15 ++++++++------- src/actions/actionCreators.js | 13 +++++++++---- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js index d41d82d8bfa..006c2da5b8b 100644 --- a/src/actions/GroupActions.js +++ b/src/actions/GroupActions.js @@ -19,12 +19,13 @@ import { asyncAction } from './actionCreators'; const GroupActions = {}; /** - * Create a GroupActions.fetchJoinedGroups action that represents an - * asynchronous request to fetch the groups to which a user is joined. + * Creates an action thunk that will do an asynchronous request to fetch + * the groups to which a user is joined. * * @param {MatrixClient} matrixClient the matrix client to query. - * @returns {function} an asynchronous action of type - * GroupActions.fetchJoinedGroups. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction */ GroupActions.fetchJoinedGroups = function(matrixClient) { return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups()); diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js index 038cda1b356..60946ea7f1b 100644 --- a/src/actions/TagOrderActions.js +++ b/src/actions/TagOrderActions.js @@ -21,14 +21,15 @@ import TagOrderStore from '../stores/TagOrderStore'; const TagOrderActions = {}; /** - * Create a TagOrderActions.commitTagOrdering action that represents an - * asyncronous request to commit TagOrderStore.getOrderedTags() to account - * data. + * Creates an action thunk that will do an asynchronous request to + * commit TagOrderStore.getOrderedTags() to account data and dispatch + * actions to indicate the status of the request. * - * @param {MatrixClient} matrixClient the matrix client to set the account - * data on. - * @returns {function} an asynchronous action of type - * TagOrderActions.commitTagOrdering. + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction */ TagOrderActions.commitTagOrdering = function(matrixClient) { return asyncAction('TagOrderActions.commitTagOrdering', () => { diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index bebd368f95f..bddfbc7c63c 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -15,14 +15,19 @@ limitations under the License. */ /** - * Create an asynchronous action creator that will dispatch actions indicating - * the current status of the promise returned by fn. + * Create an action thunk that will dispatch actions indicating the current + * status of the Promise returned by fn. + * * @param {string} id the id to give the dispatched actions. This is given a * suffix determining whether it is pending, successful or * a failure. * @param {function} fn a function that returns a Promise. - * @returns {function} an asynchronous action creator - a function that uses - * its single argument as a dispatch function. + * @returns {function} an action thunk - a function that uses its single + * argument as a dispatch function to dispatch the + * following actions: + * `${id}.pending` and either + * `${id}.success` or + * `${id}.failure`. */ export function asyncAction(id, fn) { return (dispatch) => {