diff --git a/package.json b/package.json
index 5c81db2153..943c443c59 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/MatrixClientPeg.js b/src/MatrixClientPeg.js
index a6012f5213..14dfa91fa4 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.
@@ -22,6 +23,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 +70,8 @@ class MatrixClientPeg {
unset() {
this.matrixClient = null;
+
+ MatrixActionCreators.stop();
}
/**
@@ -108,6 +112,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
new file mode 100644
index 0000000000..006c2da5b8
--- /dev/null
+++ b/src/actions/GroupActions.js
@@ -0,0 +1,34 @@
+/*
+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 { asyncAction } from './actionCreators';
+
+const GroupActions = {};
+
+/**
+ * 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 action thunk that will dispatch actions
+ * indicating the status of the request.
+ * @see asyncAction
+ */
+GroupActions.fetchJoinedGroups = function(matrixClient) {
+ return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups());
+};
+
+export default GroupActions;
diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js
new file mode 100644
index 0000000000..33bdb53799
--- /dev/null
+++ b/src/actions/MatrixActionCreators.js
@@ -0,0 +1,108 @@
+/*
+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 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,
+ * 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,
+ };
+}
+
+/**
+ * @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 {AccountDataAction} an action of type MatrixActions.accountData.
+ */
+function createAccountDataAction(matrixClient, accountDataEvent) {
+ return {
+ action: 'MatrixActions.accountData',
+ event: accountDataEvent,
+ event_type: accountDataEvent.getType(),
+ event_content: accountDataEvent.getContent(),
+ };
+}
+
+/**
+ * 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 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
+ * when given the MatrixClient as an argument as well as
+ * arguments emitted in the MatrixClient event.
+ */
+ _addMatrixClientListener(matrixClient, eventName, actionCreator) {
+ const listener = (...args) => {
+ dis.dispatch(actionCreator(matrixClient, ...args));
+ };
+ matrixClient.on(eventName, listener);
+ this._matrixClientListenersStop.push(() => {
+ matrixClient.removeListener(eventName, listener);
+ });
+ },
+
+ /**
+ * Stop listening to events.
+ */
+ stop() {
+ this._matrixClientListenersStop.forEach((stopListener) => stopListener());
+ },
+};
diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js
new file mode 100644
index 0000000000..60946ea7f1
--- /dev/null
+++ b/src/actions/TagOrderActions.js
@@ -0,0 +1,47 @@
+/*
+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 Analytics from '../Analytics';
+import { asyncAction } from './actionCreators';
+import TagOrderStore from '../stores/TagOrderStore';
+
+const TagOrderActions = {};
+
+/**
+ * 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 action thunk that will dispatch actions
+ * 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;
+ }
+
+ 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
new file mode 100644
index 0000000000..bddfbc7c63
--- /dev/null
+++ b/src/actions/actionCreators.js
@@ -0,0 +1,41 @@
+/*
+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.
+*/
+
+/**
+ * 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 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) => {
+ dispatch({action: id + '.pending'});
+ fn().then((result) => {
+ dispatch({action: id + '.success', result});
+ }).catch((err) => {
+ dispatch({action: id + '.failure', err});
+ });
+ };
+}
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index 01abf966f9..38b7634edb 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 ba7251b603..69b737cb7e 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -83,7 +83,7 @@ const ONBOARDING_FLOW_STARTERS = [
'view_create_group',
];
-module.exports = React.createClass({
+export default React.createClass({
// we export this so that the integration tests can use it :-S
statics: {
VIEWS: VIEWS,
diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js
index 0107ad1db1..49d22d8e52 100644
--- a/src/components/structures/TagPanel.js
+++ b/src/components/structures/TagPanel.js
@@ -17,79 +17,17 @@ limitations under the License.
import React from 'react';
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 GroupActions from '../../actions/GroupActions';
+import TagOrderActions from '../../actions/TagOrderActions';
+
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 ?
-