Swap RoomList to react-beautiful-dnd
Includes themeing See matrix-org/matrix-react-sdk#1711
This commit is contained in:
parent
a94208e230
commit
3481283af8
8 changed files with 68 additions and 235 deletions
|
@ -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",
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
|
@ -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));
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue