+ );
+ }
}
});
diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js
index ece48043..471bd8a1 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,178 @@ 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 = {
+ canDrag: function(props, monitor) {
+ return props.roomSubList.props.editable;
+ },
+
+ 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
+ lastTargetRoom: null,
+ lastYOffset: null,
+ lastYDelta: null,
+ };
+
+ if (props.roomSubList.debug) console.log("roomTile beginDrag for " + item.room.roomId);
+
+ // doing this 'correctly' with state causes react-dnd to break seemingly due to the state transitions
+ props.room._dragging = true;
+
+ return item;
+ },
+
+ endDrag: function (props, monitor, component) {
+ var item = monitor.getItem();
+
+ if (props.roomSubList.debug) console.log("roomTile endDrag for " + item.room.roomId + " with didDrop=" + monitor.didDrop());
+
+ props.room._dragging = false;
+ if (monitor.didDrop()) {
+ if (props.roomSubList.debug) console.log("force updating component " + item.targetList.props.label);
+ item.targetList.forceUpdate(); // as we're not using state
+ }
+
+ if (monitor.didDrop() && item.targetList.props.editable) {
+ // if we moved lists, remove the old tag
+ if (item.targetList !== item.originalList) {
+ // commented out attempts to set a spinner on our target component as component is actually
+ // the original source component being dragged, not our target. To fix we just need to
+ // move all of this to endDrop in the target instead. FIXME later.
+
+ //component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 });
+ MatrixClientPeg.get().deleteRoomTag(item.room.roomId, item.originalList.props.tagName).finally(function() {
+ //component.state.set({ spinner: component.state.spinner-- });
+ }).fail(function(err) {
+ var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
+ Modal.createDialog(ErrorDialog, {
+ title: "Failed to remove tag " + item.originalList.props.tagName + " from room",
+ description: err.toString()
+ });
+ });
+ }
+
+ var newOrder= {};
+ if (item.targetList.props.order === 'manual') {
+ newOrder['order'] = item.targetList.calcManualOrderTagData(item.room);
+ }
+
+ // if we moved lists or the ordering changed, add the new tag
+ if (item.targetList.props.tagName && (item.targetList !== item.originalList || newOrder)) {
+ //component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 });
+ MatrixClientPeg.get().setRoomTag(item.room.roomId, item.targetList.props.tagName, newOrder).finally(function() {
+ //component.state.set({ spinner: component.state.spinner-- });
+ }).fail(function(err) {
+ var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
+ Modal.createDialog(ErrorDialog, {
+ title: "Failed to add tag " + item.targetList.props.tagName + " to room",
+ description: err.toString()
+ });
+ });
+ }
+ }
+ else {
+ // cancel the drop and reset our original position
+ if (props.roomSubList.debug) console.log("cancelling drop & drag");
+ props.roomSubList.moveRoomTile(item.room, item.originalIndex);
+ if (item.targetList && item.targetList !== item.originalList) {
+ item.targetList.removeRoomTile(item.room);
+ }
+ }
+ }
+};
+
+var roomTileTarget = {
+ canDrop: function() {
+ return false;
+ },
+
+ hover: function(props, monitor) {
+ var item = monitor.getItem();
+ var off = monitor.getClientOffset();
+ // console.log("hovering on room " + props.room.roomId + ", isOver=" + monitor.isOver());
+
+ //console.log("item.targetList=" + item.targetList + ", roomSubList=" + props.roomSubList);
+
+ var switchedTarget = false;
+ 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.
+ if (props.roomSubList.debug) console.log("switched target sublist");
+ switchedTarget = true;
+ item.targetList.removeRoomTile(item.room);
+ item.targetList = props.roomSubList;
+ }
+
+ if (!item.targetList.props.editable) return;
+
+ if (item.targetList.props.order === 'manual') {
+ if (item.room.roomId !== props.room.roomId && props.room !== item.lastTargetRoom) {
+ // find the offset of the target tile in the list.
+ var roomTile = props.roomSubList.findRoomTile(props.room);
+ // shuffle the list to add our tile to that position.
+ props.roomSubList.moveRoomTile(item.room, roomTile.index);
+ }
+
+ // stop us from flickering between our droptarget and the previous room.
+ // whenever the cursor changes direction we have to reset the flicker-damping.
+
+ var yDelta = off.y - item.lastYOffset;
+
+ if ((yDelta > 0 && item.lastYDelta < 0) ||
+ (yDelta < 0 && item.lastYDelta > 0))
+ {
+ // the cursor changed direction - forget our previous room
+ item.lastTargetRoom = null;
+ }
+ else {
+ // track the last room we were hovering over so we can stop
+ // bouncing back and forth if the droptarget is narrower than
+ // the other list items. The other way to do this would be
+ // to reduce the size of the hittarget on the list items, but
+ // can't see an easy way to do that.
+ item.lastTargetRoom = props.room;
+ }
+
+ if (yDelta) item.lastYDelta = yDelta;
+ item.lastYOffset = off.y;
+ }
+ else if (switchedTarget) {
+ if (!props.roomSubList.findRoomTile(item.room).room) {
+ // add to the list in the right place
+ props.roomSubList.moveRoomTile(item.room, 0);
+ }
+ // we have to sort the list whatever to recalculate it
+ props.roomSubList.sortList();
+ }
+ },
+};
+
+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 });
},
@@ -42,18 +212,28 @@ module.exports = React.createClass({
},
render: function() {
+ // if (this.props.clientOffset) {
+ // //console.log("room " + this.props.room.roomId + " has dropTarget clientOffset " + this.props.clientOffset.x + "," + this.props.clientOffset.y);
+ // }
+
+ if (this.props.room._dragging) {
+ var RoomDropTarget = sdk.getComponent("molecules.RoomDropTarget");
+ return ;
+ }
+
var myUserId = MatrixClientPeg.get().credentials.userId;
+ var me = this.props.room.currentState.members[myUserId];
var classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_highlight': this.props.highlight,
- 'mx_RoomTile_invited': this.props.room.currentState.members[myUserId].membership == 'invite'
+ 'mx_RoomTile_invited': (me && me.membership == 'invite'),
});
var name;
if (this.props.isInvite) {
- name = this.props.room.getMember(MatrixClientPeg.get().credentials.userId).events.member.getSender();
+ name = this.props.room.getMember(myUserId).events.member.getSender();
}
else {
// XXX: We should never display raw room IDs, but sometimes the room name js sdk gives is undefined
@@ -92,7 +272,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(
);
}
diff --git a/src/skins/vector/views/organisms/RoomSubList.js b/src/skins/vector/views/organisms/RoomSubList.js
new file mode 100644
index 00000000..a4fd5e99
--- /dev/null
+++ b/src/skins/vector/views/organisms/RoomSubList.js
@@ -0,0 +1,290 @@
+/*
+Copyright 2015 OpenMarket 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.
+*/
+
+'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');
+
+// turn this on for drop & drag console debugging galore
+var debug = false;
+
+var roomListTarget = {
+ canDrop: function() {
+ return true;
+ },
+
+ drop: function(props, monitor, component) {
+ if (debug) console.log("dropped on sublist")
+ },
+
+ hover: function(props, monitor, component) {
+ var item = monitor.getItem();
+
+ if (component.state.sortedList.length == 0 && props.editable) {
+ if (debug) 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',
+
+ debug: debug,
+
+ propTypes: {
+ list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ label: React.PropTypes.string.isRequired,
+ tagName: React.PropTypes.string,
+ editable: React.PropTypes.bool,
+ order: React.PropTypes.string.isRequired,
+ bottommost: React.PropTypes.bool,
+ selectedRoom: React.PropTypes.string.isRequired,
+ activityMap: React.PropTypes.object.isRequired,
+ collapsed: React.PropTypes.bool.isRequired
+ },
+
+ getInitialState: function() {
+ return {
+ hidden: false,
+ sortedList: [],
+ };
+ },
+
+ componentWillMount: function() {
+ this.sortList(this.props.list, this.props.order);
+ },
+
+ componentWillReceiveProps: function(newProps) {
+ // order the room list appropriately before we re-render
+ //if (debug) console.log("received new props, list = " + newProps.list);
+ this.sortList(newProps.list, newProps.order);
+ },
+
+ onClick: function(ev) {
+ this.setState({ hidden : !this.state.hidden });
+ },
+
+ tsOfNewestEvent: function(room) {
+ if (room.timeline.length) {
+ return room.timeline[room.timeline.length - 1].getTs();
+ }
+ else {
+ return Number.MAX_SAFE_INTEGER;
+ }
+ },
+
+ // TODO: factor the comparators back out into a generic comparator
+ // so that view_prev_room and view_next_room can do the right thing
+
+ recentsComparator: function(roomA, roomB) {
+ return this.tsOfNewestEvent(roomB) - this.tsOfNewestEvent(roomA);
+ },
+
+ manualComparator: function(roomA, roomB) {
+ if (!roomA.tags[this.props.tagName] || !roomB.tags[this.props.tagName]) return 0;
+ var a = roomA.tags[this.props.tagName].order;
+ var b = roomB.tags[this.props.tagName].order;
+ return a == b ? this.recentsComparator(roomA, roomB) : ( a > b ? 1 : -1);
+ },
+
+ sortList: function(list, order) {
+ if (list === undefined) list = this.state.sortedList;
+ if (order === undefined) order = this.props.order;
+ var comparator;
+ list = list || [];
+ if (order === "manual") comparator = this.manualComparator;
+ if (order === "recent") comparator = this.recentsComparator;
+
+ //if (debug) console.log("sorting list for sublist " + this.props.label + " with length " + list.length + ", this.props.list = " + this.props.list);
+ this.setState({ sortedList: list.sort(comparator) });
+ },
+
+ moveRoomTile: function(room, atIndex) {
+ if (debug) 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) {
+ if (debug) console.log("removing at index " + found.index + " and adding at index " + atIndex);
+ rooms.splice(found.index, 1);
+ rooms.splice(atIndex, 0, found.room);
+ }
+ else {
+ if (debug) 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) {
+ if (debug) 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.warn("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.roomId + " with index " + index);
+ }
+ else {
+ if (debug) console.log("didn't find room");
+ room = null;
+ }
+ return ({
+ room: room,
+ index: index,
+ });
+ },
+
+ calcManualOrderTagData: function(room) {
+ var index = this.state.sortedList.indexOf(room);
+
+ // we sort rooms by the lexicographic ordering of the 'order' metadata on their tags.
+ // for convenience, we calculate this for now a floating point number between 0.0 and 1.0.
+
+ var orderA = 0.0; // by default we're next to the beginning of the list
+ if (index > 0) {
+ var prevTag = this.state.sortedList[index - 1].tags[this.props.tagName];
+ if (!prevTag) {
+ console.error("Previous room in sublist is not tagged to be in this list. This should never happen.")
+ }
+ else if (prevTag.order === undefined) {
+ console.error("Previous room in sublist has no ordering metadata. This should never happen.");
+ }
+ else {
+ orderA = prevTag.order;
+ }
+ }
+
+ var orderB = 1.0; // by default we're next to the end of the list too
+ if (index < this.state.sortedList.length - 1) {
+ var nextTag = this.state.sortedList[index + 1].tags[this.props.tagName];
+ if (!nextTag) {
+ console.error("Next room in sublist is not tagged to be in this list. This should never happen.")
+ }
+ else if (nextTag.order === undefined) {
+ console.error("Next room in sublist has no ordering metadata. This should never happen.");
+ }
+ else {
+ orderB = nextTag.order;
+ }
+ }
+
+ var order = (orderA + orderB) / 2.0;
+ if (order === orderA || order === orderB) {
+ console.error("Cannot describe new list position. This should be incredibly unlikely.");
+ // TODO: renumber the list
+ }
+
+ return order;
+ },
+
+ 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 (
+
+ );
+ });
+ },
+
+ 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));
+
+ var target;
+ if (this.state.sortedList.length == 0 && this.props.editable) {
+ target = ;
+ }
+
+ if (this.state.sortedList.length > 0 || this.props.editable) {
+ var subList;
+ var classes = "mx_RoomSubList" +
+ (this.props.bottommost ? " mx_RoomSubList_bottommost" : "");
+
+ if (!this.state.hidden) {
+ subList =