Merge pull request #6008 from vector-im/luke/roomlist-beautiful-dnd
Swap RoomList to react-beautiful-dnd
This commit is contained in:
commit
a819add7b0
9 changed files with 84 additions and 240 deletions
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
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;
|
||||
|
|
|
@ -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
|
||||
export default class DNDRoomTile extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.getClassName = this.getClassName.bind(this);
|
||||
}
|
||||
|
||||
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')),
|
||||
});
|
||||
getClassName(isDragging) {
|
||||
return classNames({
|
||||
"mx_DNDRoomTile": true,
|
||||
"mx_DNDRoomTile_dragging": isDragging,
|
||||
});
|
||||
}
|
||||
|
||||
var newOrder= {};
|
||||
if (item.targetList.props.order === 'manual') {
|
||||
newOrder['order'] = item.targetList.calcManualOrderTagData(item.room);
|
||||
}
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
// 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')),
|
||||
});
|
||||
});
|
||||
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>;
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue