diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js
index f78835b4..3260c975 100644
--- a/src/components/structures/LeftPanel.js
+++ b/src/components/structures/LeftPanel.js
@@ -17,22 +17,31 @@ limitations under the License.
'use strict';
import React from 'react';
+import PropTypes from 'prop-types';
import classNames from 'classnames';
+import { DragDropContext } from 'react-beautiful-dnd';
+import { MatrixClient } from 'matrix-js-sdk';
import { KeyCode } from 'matrix-react-sdk/lib/Keyboard';
import sdk from 'matrix-react-sdk';
import dis from 'matrix-react-sdk/lib/dispatcher';
-import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
-import CallHandler from 'matrix-react-sdk/lib/CallHandler';
-import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
import VectorConferenceHandler from '../../VectorConferenceHandler';
+import SettingsStore from 'matrix-react-sdk/lib/settings/SettingsStore';
+import TagOrderActions from 'matrix-react-sdk/lib/actions/TagOrderActions';
+import RoomListActions from 'matrix-react-sdk/lib/actions/RoomListActions';
+
+
var LeftPanel = React.createClass({
displayName: 'LeftPanel',
// NB. If you add props, don't forget to update
// shouldComponentUpdate!
propTypes: {
- collapsed: React.PropTypes.bool.isRequired,
+ collapsed: PropTypes.bool.isRequired,
+ },
+
+ contextTypes: {
+ matrixClient: PropTypes.instanceOf(MatrixClient),
},
getInitialState: function() {
@@ -161,13 +170,59 @@ var LeftPanel = React.createClass({
this.setState({ searchFilter: term });
},
+ onDragEnd: function(result) {
+ // Dragged to an invalid destination, not onto a droppable
+ if (!result.destination) {
+ return;
+ }
+
+ const dest = result.destination.droppableId;
+
+ if (dest === 'tag-panel-droppable') {
+ // Dispatch synchronously so that the TagPanel receives an
+ // optimistic update from TagOrderStore before the previous
+ // state is shown.
+ dis.dispatch(TagOrderActions.moveTag(
+ this.context.matrixClient,
+ result.draggableId,
+ result.destination.index,
+ ), true);
+ } else {
+ this.onRoomTileEndDrag(result);
+ }
+ },
+
+ onRoomTileEndDrag: function(result) {
+ let newTag = result.destination.droppableId.split('_')[1];
+ let prevTag = result.source.droppableId.split('_')[1];
+ if (newTag === 'undefined') newTag = undefined;
+ if (prevTag === 'undefined') prevTag = undefined;
+
+ const roomId = result.draggableId.split('_')[1];
+
+ const oldIndex = result.source.index;
+ const newIndex = result.destination.index;
+
+ dis.dispatch(RoomListActions.tagRoom(
+ this.context.matrixClient,
+ this.context.matrixClient.getRoom(roomId),
+ prevTag, newTag,
+ oldIndex, newIndex,
+ ), true);
+ },
+
+ collectRoomList: function(ref) {
+ this._roomList = ref;
+ },
+
render: function() {
const RoomList = sdk.getComponent('rooms.RoomList');
+ const TagPanel = sdk.getComponent('structures.TagPanel');
const BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu');
const CallPreview = sdk.getComponent('voip.CallPreview');
let topBox;
- if (MatrixClientPeg.get().isGuest()) {
+ if (this.context.matrixClient.isGuest()) {
const LoginBox = sdk.getComponent('structures.LoginBox');
topBox = ;
} else {
@@ -184,15 +239,21 @@ var LeftPanel = React.createClass({
);
return (
-
+
+
+ { SettingsStore.isFeatureEnabled("feature_tag_panel") ?
:
}
+
+
+
);
}
});
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
index 15f56c00..d119e271 100644
--- a/src/components/structures/RoomSubList.js
+++ b/src/components/structures/RoomSubList.js
@@ -38,31 +38,6 @@ var debug = false;
const TRUNCATE_AT = 10;
-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',
@@ -110,13 +85,17 @@ var RoomSubList = React.createClass({
},
componentWillMount: function() {
- this.sortList(this.applySearchFilter(this.props.list, this.props.searchFilter), this.props.order);
+ this.setState({
+ sortedList: this.applySearchFilter(this.props.list, this.props.searchFilter),
+ });
},
componentWillReceiveProps: function(newProps) {
// order the room list appropriately before we re-render
//if (debug) console.log("received new props, list = " + newProps.list);
- this.sortList(this.applySearchFilter(newProps.list, newProps.searchFilter), newProps.order);
+ this.setState({
+ sortedList: this.applySearchFilter(newProps.list, newProps.searchFilter),
+ });
},
applySearchFilter: function(list, filter) {
@@ -164,71 +143,6 @@ var RoomSubList = React.createClass({
});
},
- tsOfNewestEvent: function(room) {
- for (var i = room.timeline.length - 1; i >= 0; --i) {
- var ev = room.timeline[i];
- if (ev.getTs() &&
- (Unread.eventTriggersUnreadCount(ev) ||
- (ev.getSender() === MatrixClientPeg.get().credentials.userId))
- ) {
- return ev.getTs();
- }
- }
-
- // we might only have events that don't trigger the unread indicator,
- // in which case use the oldest event even if normally it wouldn't count.
- // This is better than just assuming the last event was forever ago.
- if (room.timeline.length && room.timeline[0].getTs()) {
- return room.timeline[0].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);
- },
-
- lexicographicalComparator: function(roomA, roomB) {
- return roomA.name > roomB.name ? 1 : -1;
- },
-
- // Generates the manual comparator using the given list
- manualComparator: function(roomA, roomB) {
- if (!roomA.tags[this.props.tagName] || !roomB.tags[this.props.tagName]) return 0;
-
- // Make sure the room tag has an order element, if not set it to be the bottom
- var a = roomA.tags[this.props.tagName].order;
- var b = roomB.tags[this.props.tagName].order;
-
- // Order undefined room tag orders to the bottom
- if (a === undefined && b !== undefined) {
- return 1;
- } else if (a !== undefined && b === undefined) {
- return -1;
- }
-
- return a == b ? this.lexicographicalComparator(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;
-
- // Fix undefined orders here, and make sure the backend gets updated as well
- this._fixUndefinedOrder(list);
-
- //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) });
- },
-
_shouldShowNotifBadge: function(roomNotifState) {
const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
return showBadgeInStates.indexOf(roomNotifState) > -1;
@@ -279,98 +193,6 @@ var RoomSubList = React.createClass({
this.setState(this.state);
},
- 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(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.
-
- let orderA = 0.0; // by default we're next to the beginning of the list
- if (index > 0) {
- const 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;
- }
- }
-
- let orderB = 1.0; // by default we're next to the end of the list too
- if (index < this.state.sortedList.length - 1) {
- const 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;
- }
- }
-
- const order = (orderA + orderB) / 2.0;
-
- if (order === orderA || order === orderB) {
- console.error("Cannot describe new list position. This should be incredibly unlikely.");
- this.state.sortedList.forEach((room, index) => {
- MatrixClientPeg.get().setRoomTag(
- room.roomId, this.props.tagName,
- {order: index / this.state.sortedList.length},
- );
- });
- return index / this.state.sortedList.length;
- }
-
- return order;
- },
-
makeRoomTiles: function() {
var self = this;
var DNDRoomTile = sdk.getComponent("rooms.DNDRoomTile");
@@ -497,47 +319,6 @@ var RoomSubList = React.createClass({
this.props.onHeaderClick(false);
},
- // Fix any undefined order elements of a room in a manual ordered list
- // room.tag[tagname].order
- _fixUndefinedOrder: function(list) {
- if (this.props.order === "manual") {
- var order = 0.0;
- var self = this;
-
- // Find the highest (lowest position) order of a room in a manual ordered list
- list.forEach(function(room) {
- if (room.tags.hasOwnProperty(self.props.tagName)) {
- if (order < room.tags[self.props.tagName].order) {
- order = room.tags[self.props.tagName].order;
- }
- }
- });
-
- // Fix any undefined order elements of a room in a manual ordered list
- // Do this one at a time, as each time a rooms tag data is updated the RoomList
- // gets triggered and another list is passed in. Doing it one at a time means that
- // we always correctly calculate the highest order for the list - stops multiple
- // rooms getting the same order. This is only really relevant for the first time this
- // is run with historical room tag data, after that there should only be undefined
- // in the list at a time anyway.
- for (let i = 0; i < list.length; i++) {
- if (list[i].tags[self.props.tagName] && list[i].tags[self.props.tagName].order === undefined) {
- MatrixClientPeg.get().setRoomTag(list[i].roomId, self.props.tagName, {order: (order + 1.0) / 2.0}).finally(function() {
- // Do any final stuff here
- }).catch(function(err) {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- console.error("Failed to add tag " + self.props.tagName + " to room" + err);
- Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
- title: _t('Failed to add tag %(tagName)s to room', {tagName: self.props.tagName}),
- description: ((err && err.message) ? err.message : _t('Operation failed')),
- });
- });
- break;
- };
- };
- }
- },
-
render: function() {
var connectDropTarget = this.props.connectDropTarget;
var TruncatedList = sdk.getComponent('elements.TruncatedList');
@@ -572,13 +353,17 @@ var RoomSubList = React.createClass({
{ subList }
;
- return this.props.editable ?
- { (provided, snapshot) => (
-
- { subListContent }
-
- ) }
- : subListContent;
+ return this.props.editable ?
+
+ { (provided, snapshot) => (
+
+ { subListContent }
+
+ ) }
+ : subListContent;
}
else {
var Loader = sdk.getComponent("elements.Spinner");
diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js
index 36602cfa..06eb347d 100644
--- a/src/components/views/context_menus/RoomTileContextMenu.js
+++ b/src/components/views/context_menus/RoomTileContextMenu.js
@@ -20,6 +20,7 @@ limitations under the License.
import Promise from 'bluebird';
import React from 'react';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
import sdk from 'matrix-react-sdk';
import { _t, _td } from 'matrix-react-sdk/lib/languageHandler';
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
@@ -28,14 +29,15 @@ import DMRoomMap from 'matrix-react-sdk/lib/utils/DMRoomMap';
import * as Rooms from 'matrix-react-sdk/lib/Rooms';
import * as RoomNotifs from 'matrix-react-sdk/lib/RoomNotifs';
import Modal from 'matrix-react-sdk/lib/Modal';
+import RoomListActions from 'matrix-react-sdk/lib/actions/RoomListActions';
module.exports = React.createClass({
displayName: 'RoomTileContextMenu',
propTypes: {
- room: React.PropTypes.object.isRequired,
+ room: PropTypes.object.isRequired,
/* callback called when the menu is dismissed */
- onFinished: React.PropTypes.func,
+ onFinished: PropTypes.func,
},
getInitialState() {
@@ -45,7 +47,7 @@ module.exports = React.createClass({
isFavourite: this.props.room.tags.hasOwnProperty("m.favourite"),
isLowPriority: this.props.room.tags.hasOwnProperty("m.lowpriority"),
isDirectMessage: Boolean(dmRoomMap.getUserIdForRoomId(this.props.room.roomId)),
- }
+ };
},
componentWillMount: function() {
@@ -57,42 +59,16 @@ module.exports = React.createClass({
},
_toggleTag: function(tagNameOn, tagNameOff) {
- var self = this;
- const roomId = this.props.room.roomId;
- var cli = MatrixClientPeg.get();
- if (!cli.isGuest()) {
- Promise.delay(500).then(function() {
- if (tagNameOff !== null && tagNameOff !== undefined) {
- cli.deleteRoomTag(roomId, tagNameOff).finally(function() {
- // Close the context menu
- if (self.props.onFinished) {
- self.props.onFinished();
- };
- }).catch(function(err) {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- Modal.createTrackedDialog('Failed to remove tag from room 1', '', ErrorDialog, {
- title: _t('Failed to remove tag %(tagName)s from room', {tagName: tagNameOff}),
- description: ((err && err.message) ? err.message : _t('Operation failed')),
- });
- });
- }
+ if (!MatrixClientPeg.get().isGuest()) {
+ Promise.delay(500).then(() => {
+ dis.dispatch(RoomListActions.tagRoom(
+ MatrixClientPeg.get(),
+ this.props.room,
+ tagNameOff, tagNameOn,
+ undefined, 0,
+ ), true);
- if (tagNameOn !== null && tagNameOn !== undefined) {
- // If the tag ordering meta data is required, it is added by
- // the RoomSubList when it sorts its rooms
- cli.setRoomTag(roomId, tagNameOn, {}).finally(function() {
- // Close the context menu
- if (self.props.onFinished) {
- self.props.onFinished();
- };
- }).catch(function(err) {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- Modal.createTrackedDialog('Failed to remove tag from room 2', '', ErrorDialog, {
- title: _t('Failed to remove tag %(tagName)s from room', {tagName: tagNameOn}),
- description: ((err && err.message) ? err.message : _t('Operation failed')),
- });
- });
- }
+ this.props.onFinished();
});
}
},
@@ -132,22 +108,22 @@ module.exports = React.createClass({
},
_onClickDM: function() {
+ if (MatrixClientPeg.get().isGuest()) return;
+
const newIsDirectMessage = !this.state.isDirectMessage;
this.setState({
isDirectMessage: newIsDirectMessage,
});
- if (MatrixClientPeg.get().isGuest()) return;
-
Rooms.guessAndSetDMRoom(
- this.props.room, newIsDirectMessage
+ this.props.room, newIsDirectMessage,
).delay(500).finally(() => {
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
- };
+ }
}, (err) => {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to set Direct Message status of room', '', ErrorDialog, {
title: _t('Failed to set Direct Message status of room'),
description: ((err && err.message) ? err.message : _t('Operation failed')),
@@ -165,7 +141,7 @@ module.exports = React.createClass({
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
- };
+ }
},
_onClickReject: function() {
@@ -177,7 +153,7 @@ module.exports = React.createClass({
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
- };
+ }
},
_onClickForget: function() {
@@ -185,8 +161,8 @@ module.exports = React.createClass({
MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' });
}, function(err) {
- var errCode = err.errcode || _td("unknown error code");
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const errCode = err.errcode || _td("unknown error code");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
title: _t('Failed to forget room %(errCode)s', {errCode: errCode}),
description: ((err && err.message) ? err.message : _t('Operation failed')),
@@ -196,20 +172,19 @@ module.exports = React.createClass({
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
- };
+ }
},
_saveNotifState: function(newState) {
+ if (MatrixClientPeg.get().isGuest()) return;
+
const oldState = this.state.roomNotifState;
const roomId = this.props.room.roomId;
- var cli = MatrixClientPeg.get();
-
- if (cli.isGuest()) return;
this.setState({
roomNotifState: newState,
});
- RoomNotifs.setRoomNotifsState(this.props.room.roomId, newState).done(() => {
+ RoomNotifs.setRoomNotifsState(roomId, newState).done(() => {
// delay slightly so that the user can see their state change
// before closing the menu
return Promise.delay(500).then(() => {
@@ -217,7 +192,7 @@ module.exports = React.createClass({
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
- };
+ }
});
}, (error) => {
// TODO: some form of error notification to the user
@@ -247,22 +222,22 @@ module.exports = React.createClass({
},
_renderNotifMenu: function() {
- var alertMeClasses = classNames({
+ const alertMeClasses = classNames({
'mx_RoomTileContextMenu_notif_field': true,
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
});
- var allNotifsClasses = classNames({
+ const allNotifsClasses = classNames({
'mx_RoomTileContextMenu_notif_field': true,
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES,
});
- var mentionsClasses = classNames({
+ const mentionsClasses = classNames({
'mx_RoomTileContextMenu_notif_field': true,
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MENTIONS_ONLY,
});
- var muteNotifsClasses = classNames({
+ const muteNotifsClasses = classNames({
'mx_RoomTileContextMenu_notif_field': true,
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MUTE,
});
@@ -272,22 +247,22 @@ module.exports = React.createClass({
-
+
{ _t('All messages (noisy)') }
-
+
{ _t('All messages') }
-
+
{ _t('Mentions only') }
-
+
{ _t('Mute') }
@@ -322,7 +297,7 @@ module.exports = React.createClass({
return (
-
+
{ leaveText }
@@ -351,17 +326,17 @@ module.exports = React.createClass({
return (
-
+
{ _t('Favourite') }
-
+
{ _t('Low Priority') }
-
+
{ _t('Direct Chat') }
@@ -372,7 +347,7 @@ module.exports = React.createClass({
render: function() {
const myMember = this.props.room.getMember(
- MatrixClientPeg.get().credentials.userId
+ MatrixClientPeg.get().credentials.userId,
);
// Can't set notif level or tags on non-join rooms
@@ -389,5 +364,5 @@ module.exports = React.createClass({
{ this._renderRoomTagMenu() }
);
- }
+ },
});
diff --git a/src/components/views/rooms/DNDRoomTile.js b/src/components/views/rooms/DNDRoomTile.js
index 129e3f45..b8f8b402 100644
--- a/src/components/views/rooms/DNDRoomTile.js
+++ b/src/components/views/rooms/DNDRoomTile.js
@@ -41,6 +41,7 @@ export default class DNDRoomTile extends React.Component {
key={props.room.roomId}
draggableId={props.tagName + '_' + props.room.roomId}
index={props.index}
+ type="draggable-RoomTile"
>
{ (provided, snapshot) => {
return (
diff --git a/src/skins/vector/css/vector-web/structures/_LeftPanel.scss b/src/skins/vector/css/vector-web/structures/_LeftPanel.scss
index 8ae1fe15..a2147a02 100644
--- a/src/skins/vector/css/vector-web/structures/_LeftPanel.scss
+++ b/src/skins/vector/css/vector-web/structures/_LeftPanel.scss
@@ -21,6 +21,10 @@ limitations under the License.
flex-direction: column;
}
+.mx_LeftPanel_container {
+ display: flex;
+}
+
.mx_LeftPanel_hideButton {
position: absolute;
top: 10px;