From 61e55b3ca3a3f3d8dc0740384c3b7a007fb536c1 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Wed, 4 Nov 2015 02:25:08 +0000
Subject: [PATCH] implement most of drag & drop.

---
 src/skins/vector/views/molecules/RoomTile.js  | 114 +++++++++++++++++-
 .../vector/views/organisms/RoomSubList.js     |  89 +++++++++++++-
 src/skins/vector/views/pages/MatrixChat.js    |   6 +-
 3 files changed, 202 insertions(+), 7 deletions(-)

diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js
index bdaa621d..383c08d1 100644
--- a/src/skins/vector/views/molecules/RoomTile.js
+++ b/src/skins/vector/views/molecules/RoomTile.js
@@ -17,6 +17,8 @@ limitations under the License.
 'use strict';
 
 var React = require('react');
+var DragSource = require('react-dnd').DragSource;
+var DropTarget = require('react-dnd').DropTarget;
 var classNames = require('classnames');
 
 var RoomTileController = require('matrix-react-sdk/lib/controllers/molecules/RoomTile')
@@ -25,10 +27,89 @@ var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
 
 var sdk = require('matrix-react-sdk')
 
-module.exports = React.createClass({
+/**
+ * Specifies the drag source contract.
+ * Only `beginDrag` function is required.
+ */
+var roomTileSource = {
+    beginDrag: function (props) {
+        // Return the data describing the dragged item
+        var item = {
+            room: props.room,
+            originalList: props.roomSubList,            
+            originalIndex: props.roomSubList.findRoomTile(props.room).index,
+            targetList: props.roomSubList, // at first target is same as original
+        };
+
+        console.log("roomTile beginDrag for " + item.room.roomId);
+
+        return item;
+    },
+
+    endDrag: function (props, monitor, component) {
+        var item = monitor.getItem();
+        var dropResult = monitor.getDropResult();
+
+        console.log("roomTile endDrag for " + item.room.roomId + " with didDrop=" + monitor.didDrop());
+
+        if (!monitor.didDrop() || !item.targetList.props.editable) {
+            props.roomSubList.moveRoomTile(item.room, item.originalIndex);
+            if (item.targetList && item.targetList !== item.originalList) {
+                item.targetList.removeRoomTile(item.room);
+            }
+            return;
+        }
+        else {
+            // if it's not manual ordering, we'll need to position the tile correctly here according to the right ordering
+
+            // When dropped on a compatible target, actually set the right tags for the new ordering
+            // persistNewOrder(item.room, dropResult.listId);
+        }
+    }
+};
+
+var roomTileTarget = {
+    canDrop: function() {
+        return false;
+    },
+
+    hover: function(props, monitor) {
+        var item = monitor.getItem();
+        console.log("hovering on room " + props.room.roomId + ", isOver=" + monitor.isOver());
+
+        //console.log("item.targetList=" + item.targetList + ", roomSubList=" + props.roomSubList);
+
+        if (item.targetList !== props.roomSubList) {
+            // we've switched target, so remove the tile from the previous target.
+            // n.b. the previous target might actually be the source list.
+            item.targetList.removeRoomTile(item.room);
+            item.targetList = props.roomSubList;
+        }
+
+        if (item.targetList.props.order === 'manual' && item.room.roomId !== props.room.roomId) {
+            var roomTile = props.roomSubList.findRoomTile(props.room);
+            props.roomSubList.moveRoomTile(item.room, roomTile.index);
+        }
+    },
+};
+
+var RoomTile = React.createClass({
     displayName: 'RoomTile',
     mixins: [RoomTileController],
 
+    propTypes: {
+        connectDragSource: React.PropTypes.func.isRequired,
+        connectDropTarget: React.PropTypes.func.isRequired,
+        isDragging: React.PropTypes.bool.isRequired,
+        room: React.PropTypes.object.isRequired,
+        collapsed: React.PropTypes.bool.isRequired,
+        selected: React.PropTypes.bool.isRequired,
+        unread: React.PropTypes.bool.isRequired,
+        highlight: React.PropTypes.bool.isRequired,
+        isInvite: React.PropTypes.bool.isRequired,
+        roomSubList: React.PropTypes.object.isRequired,
+    },
+
     getInitialState: function() {
         return( { hover : false });
     },
@@ -92,7 +173,14 @@ module.exports = React.createClass({
         }
 
         var RoomAvatar = sdk.getComponent('atoms.RoomAvatar');
-        return (
+
+        // These props are injected by React DnD,
+        // as defined by your `collect` function above:
+        var isDragging = this.props.isDragging;
+        var connectDragSource = this.props.connectDragSource;
+        var connectDropTarget = this.props.connectDropTarget;
+
+        return connectDragSource(connectDropTarget(
             <div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
                 <div className="mx_RoomTile_avatar">
                     <RoomAvatar room={this.props.room} width="24" height="24" />
@@ -100,6 +188,26 @@ module.exports = React.createClass({
                 </div>
                 { label }
             </div>
-        );
+        ));
     }
 });
+
+// Export the wrapped version, inlining the 'collect' functions
+// to more closely resemble the ES7
+module.exports = 
+DropTarget('RoomTile', roomTileTarget, function(connect) {
+    return {
+        // Call this function inside render()
+        // to let React DnD handle the drag events:
+        connectDropTarget: connect.dropTarget(),
+    }
+})(
+DragSource('RoomTile', roomTileSource, function(connect, monitor) {
+    return {
+        // Call this function inside render()
+        // to let React DnD handle the drag events:
+        connectDragSource: connect.dragSource(),
+        // You can ask the monitor about the current drag state:
+        isDragging: monitor.isDragging()
+    };
+})(RoomTile));
\ No newline at end of file
diff --git a/src/skins/vector/views/organisms/RoomSubList.js b/src/skins/vector/views/organisms/RoomSubList.js
index b8747ecf..68d55c7b 100644
--- a/src/skins/vector/views/organisms/RoomSubList.js
+++ b/src/skins/vector/views/organisms/RoomSubList.js
@@ -17,10 +17,32 @@ limitations under the License.
 'use strict';
 
 var React = require('react');
+var DropTarget = require('react-dnd').DropTarget;
 var sdk = require('matrix-react-sdk')
 var dis = require('matrix-react-sdk/lib/dispatcher');
 
-module.exports = React.createClass({
+var roomListTarget = {
+    canDrop: function() {
+        return true;
+    },
+
+    hover: function(props, monitor, component) {
+        var item = monitor.getItem();
+
+        if (component.state.sortedList.length == 0 && props.editable) {
+            console.log("hovering on sublist " + props.label + ", isOver=" + monitor.isOver());
+
+            if (item.targetList !== component) {
+                 item.targetList.removeRoomTile(item.room);
+                 item.targetList = component;
+            }
+
+            component.moveRoomTile(item.room, 0);
+        }
+    },
+};
+
+var RoomSubList = React.createClass({
     displayName: 'RoomSubList',
 
     propTypes: {
@@ -80,14 +102,64 @@ module.exports = React.createClass({
         this.setState({ sortedList: list.sort(comparator) });
     },
 
+    moveRoomTile: function(room, atIndex) {
+        console.log("moveRoomTile: id " + room.roomId + ", atIndex " + atIndex);
+        //console.log("moveRoomTile before: " + JSON.stringify(this.state.rooms));
+        var found = this.findRoomTile(room);
+        var rooms = this.state.sortedList;
+        if (found.room) {
+            console.log("removing at index " + found.index + " and adding at index " + atIndex);
+            rooms.splice(found.index, 1);
+            rooms.splice(atIndex, 0, found.room);
+        }
+        else {
+            console.log("Adding at index " + atIndex);
+            rooms.splice(atIndex, 0, room);
+        }
+        this.setState({ sortedList: rooms });
+        // console.log("moveRoomTile after: " + JSON.stringify(this.state.rooms));
+    },
+
+    // XXX: this isn't invoked via a property method but indirectly via
+    // the roomList property method.  Unsure how evil this is.
+    removeRoomTile: function(room) {
+        console.log("remove room " + room.roomId);
+        var found = this.findRoomTile(room);
+        var rooms = this.state.sortedList;
+        if (found.room) {
+            rooms.splice(found.index, 1);
+        }        
+        else {
+            console.log*("Can't remove room " + room.roomId + " - can't find it");
+        }
+        this.setState({ sortedList: rooms });
+    },
+
+    findRoomTile: function(room) {        
+        var index = this.state.sortedList.indexOf(room); 
+        if (index >= 0) {
+            console.log("found: room: " + room + " with id " + room.roomId);
+        }
+        else {
+            console.log("didn't find room");
+            room = null;
+        }
+        return ({
+            room: room,
+            index: index,
+        });
+    },    
+
     makeRoomTiles: function() {
         var self = this;
         var RoomTile = sdk.getComponent("molecules.RoomTile");
         return this.state.sortedList.map(function(room) {
             var selected = room.roomId == self.props.selectedRoom;
+            // XXX: is it evil to pass in self as a prop to RoomTile?
             return (
                 <RoomTile
                     room={room}
+                    roomSubList={self}
                     key={room.roomId}
                     collapsed={self.props.collapsed}
                     selected={selected}
@@ -99,14 +171,17 @@ module.exports = React.createClass({
     },
 
     render: function() {
+        var connectDropTarget = this.props.connectDropTarget;
         var RoomDropTarget = sdk.getComponent('molecules.RoomDropTarget');
 
         var label = this.props.collapsed ? null : this.props.label;
 
+        //console.log("render: " + JSON.stringify(this.state.sortedList));
+
         if (this.state.sortedList.length > 0 || this.props.editable) {
-            return (
+            return connectDropTarget(
                 <div>
-                    <h2 className="mx_RoomSubList_label">{ this.props.label }</h2>
+                    <h2 className="mx_RoomSubList_label">{ this.props.collapsed ? '' : this.props.label }</h2>
                     <div className="mx_RoomSubList">
                         { this.makeRoomTiles() }
                     </div>
@@ -122,3 +197,11 @@ module.exports = React.createClass({
     }
 });
 
+// Export the wrapped version, inlining the 'collect' functions
+// to more closely resemble the ES7
+module.exports = 
+DropTarget('RoomTile', roomListTarget, function(connect) {
+    return {
+        connectDropTarget: connect.dropTarget(),
+    }
+})(RoomSubList);
diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js
index 0cf754c2..f34b6d4f 100644
--- a/src/skins/vector/views/pages/MatrixChat.js
+++ b/src/skins/vector/views/pages/MatrixChat.js
@@ -17,6 +17,8 @@ limitations under the License.
 'use strict';
 
 var React = require('react');
+var DragDropContext = require('react-dnd').DragDropContext;
+var HTML5Backend = require('react-dnd/modules/backends/HTML5');
 var sdk = require('matrix-react-sdk')
 
 var MatrixChatController = require('matrix-react-sdk/lib/controllers/pages/MatrixChat')
@@ -28,7 +30,7 @@ var dis = require('matrix-react-sdk/lib/dispatcher');
 var Matrix = require("matrix-js-sdk");
 var ContextualMenu = require("../../../../ContextualMenu");
 
-module.exports = React.createClass({
+var MatrixChat = React.createClass({
     displayName: 'MatrixChat',
     mixins: [MatrixChatController],
 
@@ -172,3 +174,5 @@ module.exports = React.createClass({
         }
     }
 });
+
+module.exports = DragDropContext(HTML5Backend)(MatrixChat);
\ No newline at end of file