Merge pull request #6008 from vector-im/luke/roomlist-beautiful-dnd

Swap RoomList to react-beautiful-dnd
pull/6022/head
Luke Barnard 2018-01-19 14:49:53 +00:00 committed by GitHub
commit a819add7b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 84 additions and 240 deletions

View File

@ -3,7 +3,10 @@ dist: trusty
# we don't need sudo, so can run in a container, which makes startup much
# quicker.
sudo: false
#
# unfortunately we do temporarily require sudo as a workaround for
# https://github.com/travis-ci/travis-ci/issues/8836
sudo: required
language: node_js
node_js:

View File

@ -74,8 +74,7 @@
"pako": "^1.0.5",
"prop-types": "^15.5.10",
"react": "^15.6.0",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-beautiful-dnd": "^4.0.1",
"react-dom": "^15.6.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.11.1",

View File

@ -20,8 +20,8 @@ limitations under the License.
var React = require('react');
var ReactDOM = require('react-dom');
var classNames = require('classnames');
var DropTarget = require('react-dnd').DropTarget;
var sdk = require('matrix-react-sdk');
import { Droppable } from 'react-beautiful-dnd';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
var dis = require('matrix-react-sdk/lib/dispatcher');
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 { KeyCode } from 'matrix-react-sdk/lib/Keyboard';
// turn this on for drop & drag console debugging galore
var debug = false;
@ -326,9 +327,7 @@ var RoomSubList = React.createClass({
});
},
calcManualOrderTagData: function(room) {
const index = this.state.sortedList.indexOf(room);
calcManualOrderTagData: function(index) {
// 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.
@ -375,12 +374,14 @@ var RoomSubList = React.createClass({
makeRoomTiles: function() {
var self = this;
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?
return (
<DNDRoomTile
index={index} // For DND
room={ room }
roomSubList={ self }
tagName={self.props.tagName}
key={ room.roomId }
collapsed={ self.props.collapsed || false}
unread={ Unread.doesRoomHaveUnreadMessages(room) }
@ -566,12 +567,18 @@ var RoomSubList = React.createClass({
</TruncatedList>;
}
return connectDropTarget(
<div>
{ this._getHeaderJsx() }
{ subList }
</div>
);
const subListContent = <div>
{ this._getHeaderJsx() }
{ subList }
</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 {
var Loader = sdk.getComponent("elements.Spinner");
@ -585,11 +592,4 @@ var RoomSubList = 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);
module.exports = RoomSubList;

View File

@ -14,227 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import {DragSource} from 'react-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 { Draggable } from 'react-beautiful-dnd';
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';
/**
* Defines a new Component, DNDRoomTile that wraps RoomTile, making it draggable.
* Requires extra props:
* roomSubList: React.PropTypes.object.isRequired,
* refreshSubList: React.PropTypes.func.isRequired,
*/
import classNames from 'classnames';
/**
* 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
}
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);
}
}
export default class DNDRoomTile extends React.Component {
constructor() {
super();
this.getClassName = this.getClassName.bind(this);
}
};
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();
}
},
};
// 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(),
getClassName(isDragging) {
return classNames({
"mx_DNDRoomTile": true,
"mx_DNDRoomTile_dragging": isDragging,
});
}
})(
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));
render() {
const props = this.props;
return <div>
<Draggable
key={props.room.roomId}
draggableId={props.tagName + '_' + props.room.roomId}
index={props.index}
>
{ (provided, snapshot) => {
return (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className={this.getClassName(snapshot.isDragging)}>
<RoomTile {...props} />
</div>
</div>
{ provided.placeholder }
</div>
);
} }
</Draggable>
</div>;
}
}

View File

@ -20,6 +20,8 @@ limitations under the License.
font-size: 13px;
display: block;
height: 34px;
background-color: $secondary-accent-color;
}
.mx_RoomTile_tooltip {
@ -155,6 +157,15 @@ limitations under the License.
background-color: $roomtile-selected-bg-color;
}
.mx_DNDRoomTile {
transform: none;
transition: transform 0.2s;
}
.mx_DNDRoomTile_dragging {
transform: scale(1.05, 1.05);
}
.mx_RoomTile:focus {
filter: none ! important;
background-color: $roomtile-focused-bg-color;

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-focused-bg-color: rgba(255, 255, 255, 0.9);
$roomsublist-background: #badece;
$roomsublist-label-fg-color: $h3-color;
$roomsublist-label-bg-color: $tertiary-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-selected-bg-color: rgba(255, 255, 255, 0.05);
$roomtile-selected-bg-color: #333;
$roomtile-focused-bg-color: rgba(255, 255, 255, 0.2);
$roomsublist-background: #222;
$roomsublist-label-fg-color: $h3-color;
$roomsublist-label-bg-color: $tertiary-accent-color;
$roomsublist-chevron-color: $accent-color;

View File

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

View File

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