Swap RoomList to react-beautiful-dnd

Includes themeing

See matrix-org/matrix-react-sdk#1711
This commit is contained in:
lukebarnard 2018-01-18 17:54:12 +00:00
parent a94208e230
commit 3481283af8
8 changed files with 68 additions and 235 deletions

View file

@ -74,6 +74,7 @@
"pako": "^1.0.5", "pako": "^1.0.5",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"react": "^15.6.0", "react": "^15.6.0",
"react-beautiful-dnd": "^4.0.1",
"react-dnd": "^2.1.4", "react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2", "react-dnd-html5-backend": "^2.1.2",
"react-dom": "^15.6.0", "react-dom": "^15.6.0",

View file

@ -20,8 +20,8 @@ limitations under the License.
var React = require('react'); var React = require('react');
var ReactDOM = require('react-dom'); var ReactDOM = require('react-dom');
var classNames = require('classnames'); var classNames = require('classnames');
var DropTarget = require('react-dnd').DropTarget;
var sdk = require('matrix-react-sdk'); var sdk = require('matrix-react-sdk');
import { Droppable } from 'react-beautiful-dnd';
import { _t } from 'matrix-react-sdk/lib/languageHandler'; import { _t } from 'matrix-react-sdk/lib/languageHandler';
var dis = require('matrix-react-sdk/lib/dispatcher'); var dis = require('matrix-react-sdk/lib/dispatcher');
var Unread = require('matrix-react-sdk/lib/Unread'); var Unread = require('matrix-react-sdk/lib/Unread');
@ -32,6 +32,7 @@ var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/A
import Modal from 'matrix-react-sdk/lib/Modal'; import Modal from 'matrix-react-sdk/lib/Modal';
import { KeyCode } from 'matrix-react-sdk/lib/Keyboard'; import { KeyCode } from 'matrix-react-sdk/lib/Keyboard';
// turn this on for drop & drag console debugging galore // turn this on for drop & drag console debugging galore
var debug = false; var debug = false;
@ -326,9 +327,7 @@ var RoomSubList = React.createClass({
}); });
}, },
calcManualOrderTagData: function(room) { calcManualOrderTagData: function(index) {
const index = this.state.sortedList.indexOf(room);
// we sort rooms by the lexicographic ordering of the 'order' metadata on their tags. // 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. // for convenience, we calculate this for now a floating point number between 0.0 and 1.0.
@ -375,12 +374,14 @@ var RoomSubList = React.createClass({
makeRoomTiles: function() { makeRoomTiles: function() {
var self = this; var self = this;
var DNDRoomTile = sdk.getComponent("rooms.DNDRoomTile"); var DNDRoomTile = sdk.getComponent("rooms.DNDRoomTile");
return this.state.sortedList.map(function(room) { return this.state.sortedList.map(function(room, index) {
// XXX: is it evil to pass in self as a prop to RoomTile? // XXX: is it evil to pass in self as a prop to RoomTile?
return ( return (
<DNDRoomTile <DNDRoomTile
index={index} // For DND
room={ room } room={ room }
roomSubList={ self } roomSubList={ self }
tagName={self.props.tagName}
key={ room.roomId } key={ room.roomId }
collapsed={ self.props.collapsed || false} collapsed={ self.props.collapsed || false}
unread={ Unread.doesRoomHaveUnreadMessages(room) } unread={ Unread.doesRoomHaveUnreadMessages(room) }
@ -566,12 +567,18 @@ var RoomSubList = React.createClass({
</TruncatedList>; </TruncatedList>;
} }
return connectDropTarget( const subListContent = <div>
<div> { this._getHeaderJsx() }
{ this._getHeaderJsx() } { subList }
{ subList } </div>;
</div>
); return this.props.editable ? <Droppable droppableId={"room-sub-list-droppable_" + this.props.tagName}>
{ (provided, snapshot) => (
<div ref={provided.innerRef}>
{ subListContent }
</div>
) }
</Droppable> : subListContent;
} }
else { else {
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");
@ -585,11 +592,4 @@ var RoomSubList = React.createClass({
} }
}); });
// Export the wrapped version, inlining the 'collect' functions module.exports = RoomSubList;
// to more closely resemble the ES7
module.exports =
DropTarget('RoomTile', roomListTarget, function(connect) {
return {
connectDropTarget: connect.dropTarget(),
}
})(RoomSubList);

View file

@ -14,227 +14,50 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import React from 'react'; import React from 'react';
import {DragSource} from 'react-dnd'; import { Draggable } from 'react-beautiful-dnd';
import {DropTarget} from 'react-dnd';
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
import sdk from 'matrix-react-sdk';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
import RoomTile from 'matrix-react-sdk/lib/components/views/rooms/RoomTile'; import RoomTile from 'matrix-react-sdk/lib/components/views/rooms/RoomTile';
import * as Rooms from 'matrix-react-sdk/lib/Rooms';
import Modal from 'matrix-react-sdk/lib/Modal';
/** export default class DNDRoomTile extends React.Component {
* Defines a new Component, DNDRoomTile that wraps RoomTile, making it draggable. constructor() {
* Requires extra props: super();
* roomSubList: React.PropTypes.object.isRequired, this.getStyle = this.getStyle.bind(this);
* refreshSubList: React.PropTypes.func.isRequired, }
*/
/** getStyle(isDragging) {
* Specifies the drag source contract. const result = {
* Only `beginDrag` function is required. transform: isDragging ? "scale(1.05, 1.05)" : "none",
*/ transition: "transform 0.2s",
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,
}; };
return result;
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
}
const prevTag = item.originalList.props.tagName;
const newTag = item.targetList.props.tagName;
if (monitor.didDrop() && item.targetList.props.editable) {
// Evil hack to get DMs behaving
if ((prevTag === undefined && newTag === 'im.vector.fake.direct') ||
(prevTag === 'im.vector.fake.direct' && newTag === undefined)
) {
Rooms.guessAndSetDMRoom(
item.room, newTag === 'im.vector.fake.direct',
).done(() => {
item.originalList.removeRoomTile(item.room);
}, (err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to set direct chat tag " + err);
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
title: _t('Failed to set direct chat tag'),
description: ((err && err.message) ? err.message : _t('Operation failed')),
});
});
return;
}
// More evilness: We will still be dealing with moving to favourites/low prio,
// but we avoid ever doing a request with 'im.vector.fake.direct`.
// if we moved lists, remove the old tag
if (prevTag && prevTag !== 'im.vector.fake.direct' &&
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, prevTag).finally(function() {
//component.state.set({ spinner: component.state.spinner-- });
}).catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to remove tag " + prevTag + " from room: " + err);
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
title: _t('Failed to remove tag %(tagName)s from room', {tagName: prevTag}),
description: ((err && err.message) ? err.message : _t('Operation failed')),
});
});
}
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 (newTag && newTag !== 'im.vector.fake.direct' &&
(item.targetList !== item.originalList || newOrder)
) {
MatrixClientPeg.get().setRoomTag(item.room.roomId, newTag, newOrder).catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to add tag " + newTag + " to room: " + err);
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}),
description: ((err && err.message) ? err.message : _t('Operation failed')),
});
});
}
}
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 = { render() {
canDrop: function() { const props = this.props;
return false;
},
hover: function(props, monitor) { return <div>
var item = monitor.getItem(); <Draggable
//var off = monitor.getClientOffset(); key={props.room.roomId}
// console.log("hovering on room " + props.room.roomId + ", isOver=" + monitor.isOver()); draggableId={props.tagName + '_' + props.room.roomId}
index={props.index}
//console.log("item.targetList=" + item.targetList + ", roomSubList=" + props.roomSubList); >
{ (provided, snapshot) => {
var switchedTarget = false; return (
if (item.targetList !== props.roomSubList) { <div>
// we've switched target, so remove the tile from the previous target. <div
// n.b. the previous target might actually be the source list. ref={provided.innerRef}
if (props.roomSubList.debug) console.log("switched target sublist"); {...provided.draggableProps}
switchedTarget = true; {...provided.dragHandleProps}
item.targetList.removeRoomTile(item.room); >
item.targetList = props.roomSubList; <div style={this.getStyle(snapshot.isDragging)}>
} <RoomTile {...props} />
</div>
if (!item.targetList.props.editable) return; </div>
{ provided.placeholder }
if (item.targetList.props.order === 'manual') { </div>
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); </Draggable>
// shuffle the list to add our tile to that position. </div>;
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();
}
},
};
// Export the wrapped version, inlining the 'collect' functions
// to more closely resemble the ES7
module.exports =
DropTarget('RoomTile', roomTileTarget, function(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
} }
})( }
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));

View file

@ -20,6 +20,8 @@ limitations under the License.
font-size: 13px; font-size: 13px;
display: block; display: block;
height: 34px; height: 34px;
background-color: $secondary-accent-color;
} }
.mx_RoomTile_tooltip { .mx_RoomTile_tooltip {

View file

@ -104,6 +104,7 @@ $roomtile-name-color: rgba(69, 69, 69, 0.8);
$roomtile-selected-bg-color: rgba(255, 255, 255, 0.8); $roomtile-selected-bg-color: rgba(255, 255, 255, 0.8);
$roomtile-focused-bg-color: rgba(255, 255, 255, 0.9); $roomtile-focused-bg-color: rgba(255, 255, 255, 0.9);
$roomsublist-background: #badece;
$roomsublist-label-fg-color: $h3-color; $roomsublist-label-fg-color: $h3-color;
$roomsublist-label-bg-color: $tertiary-accent-color; $roomsublist-label-bg-color: $tertiary-accent-color;
$roomsublist-chevron-color: $accent-color; $roomsublist-chevron-color: $accent-color;

View file

@ -100,9 +100,10 @@ $rte-code-bg-color: #000;
// ******************** // ********************
$roomtile-name-color: rgba(186, 186, 186, 0.8); $roomtile-name-color: rgba(186, 186, 186, 0.8);
$roomtile-selected-bg-color: rgba(255, 255, 255, 0.05); $roomtile-selected-bg-color: #333;
$roomtile-focused-bg-color: rgba(255, 255, 255, 0.2); $roomtile-focused-bg-color: rgba(255, 255, 255, 0.2);
$roomsublist-background: #222;
$roomsublist-label-fg-color: $h3-color; $roomsublist-label-fg-color: $h3-color;
$roomsublist-label-bg-color: $tertiary-accent-color; $roomsublist-label-bg-color: $tertiary-accent-color;
$roomsublist-chevron-color: $accent-color; $roomsublist-chevron-color: $accent-color;

View file

@ -18,6 +18,8 @@ limitations under the License.
display: table; display: table;
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
background-color: $roomsublist-background;
} }
.mx_RoomSubList_labelContainer { .mx_RoomSubList_labelContainer {
@ -155,6 +157,8 @@ limitations under the License.
position: relative; position: relative;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 13px;
background-color: $secondary-accent-color;
} }
.collapsed .mx_RoomSubList_ellipsis { .collapsed .mx_RoomSubList_ellipsis {

View file

@ -160,6 +160,7 @@ $roomtile-name-color: #ffffff;
$roomtile-selected-bg-color: #465561; $roomtile-selected-bg-color: #465561;
$roomtile-focused-bg-color: #6d8597; $roomtile-focused-bg-color: #6d8597;
$roomsublist-background: #465561;
$roomsublist-label-fg-color: #ffffff; $roomsublist-label-fg-color: #ffffff;
$roomsublist-label-bg-color: $secondary-accent-color; $roomsublist-label-bg-color: $secondary-accent-color;
$roomsublist-chevron-color: #ffffff; $roomsublist-chevron-color: #ffffff;