diff --git a/package.json b/package.json
index eb2cabf854..dbc27b5a08 100644
--- a/package.json
+++ b/package.json
@@ -77,6 +77,7 @@
"querystring": "^0.2.0",
"react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2",
+ "react-beautiful-dnd": "^4.0.0",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-dom": "^15.4.0",
diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js
index 60946ea7f1..db9230e964 100644
--- a/src/actions/TagOrderActions.js
+++ b/src/actions/TagOrderActions.js
@@ -31,16 +31,22 @@ const TagOrderActions = {};
* indicating the status of the request.
* @see asyncAction
*/
-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) {
- return;
- }
+TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
+ // Only commit tags if the state is ready, i.e. not null
+ let tags = TagOrderStore.getOrderedTags();
+ if (!tags) {
+ return;
+ }
+ tags = tags.filter((t) => t !== tag);
+ tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
+
+ return asyncAction('TagOrderActions.moveTag', () => {
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
return matrixClient.setAccountData('im.vector.web.tag_ordering', {tags});
+ }, () => {
+ // For an optimistic update
+ return {tags};
});
};
diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js
index bddfbc7c63..0238eee8c0 100644
--- a/src/actions/actionCreators.js
+++ b/src/actions/actionCreators.js
@@ -22,6 +22,9 @@ limitations under the License.
* suffix determining whether it is pending, successful or
* a failure.
* @param {function} fn a function that returns a Promise.
+ * @param {function?} pendingFn a function that returns an object to assign
+ * to the `request` key of the ${id}.pending
+ * payload.
* @returns {function} an action thunk - a function that uses its single
* argument as a dispatch function to dispatch the
* following actions:
@@ -29,9 +32,13 @@ limitations under the License.
* `${id}.success` or
* `${id}.failure`.
*/
-export function asyncAction(id, fn) {
+export function asyncAction(id, fn, pendingFn) {
return (dispatch) => {
- dispatch({action: id + '.pending'});
+ dispatch({
+ action: id + '.pending',
+ request:
+ typeof pendingFn === 'function' ? pendingFn() : undefined,
+ });
fn().then((result) => {
dispatch({action: id + '.success', result});
}).catch((err) => {
diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js
index 531c247ea6..323528e1f1 100644
--- a/src/components/structures/TagPanel.js
+++ b/src/components/structures/TagPanel.js
@@ -25,6 +25,8 @@ import TagOrderActions from '../../actions/TagOrderActions';
import sdk from '../../index';
import dis from '../../dispatcher';
+import { DragDropContext, Droppable } from 'react-beautiful-dnd';
+
const TagPanel = React.createClass({
displayName: 'TagPanel',
@@ -69,7 +71,9 @@ const TagPanel = React.createClass({
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
},
- onClick() {
+ onClick(e) {
+ // Ignore clicks on children
+ if (e.target !== e.currentTarget) return;
dis.dispatch({action: 'deselect_tags'});
},
@@ -78,8 +82,20 @@ const TagPanel = React.createClass({
dis.dispatch({action: 'view_create_group'});
},
- onTagTileEndDrag() {
- dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient));
+ onTagTileEndDrag(result) {
+ // Dragged to an invalid destination, not onto a droppable
+ if (!result.destination) {
+ return;
+ }
+
+ // Dispatch synchronously so that the TagPanel receives an
+ // optimistic update from TagOrderStore before the previous
+ // state is shown.
+ dis.dispatch(TagOrderActions.moveTag(
+ this.context.matrixClient,
+ result.draggableId,
+ result.destination.index,
+ ), true);
},
render() {
@@ -89,16 +105,26 @@ const TagPanel = React.createClass({
const tags = this.state.orderedTags.map((tag, index) => {
return