diff --git a/package.json b/package.json
index eb9c3aff95..96176843b3 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,8 @@
"q": "^1.4.1",
"react": "^0.13.3",
"react-loader": "^1.4.0",
- "sanitize-html": "^1.11.1"
+ "react-dnd": "^1.1.8",
+ "sanitize-html": "^1.0.0"
},
"devDependencies": {
"babel": "^5.8.23",
diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js
index 514ae7747b..38a0c652c4 100644
--- a/src/controllers/organisms/RoomList.js
+++ b/src/controllers/organisms/RoomList.js
@@ -23,7 +23,6 @@ var dis = require("matrix-react-sdk/lib/dispatcher");
var sdk = require('matrix-react-sdk');
var VectorConferenceHandler = require("../../modules/VectorConferenceHandler");
-var CallHandler = require("matrix-react-sdk/lib/CallHandler");
var HIDE_CONFERENCE_CHANS = true;
@@ -31,8 +30,7 @@ module.exports = {
getInitialState: function() {
return {
activityMap: null,
- inviteList: [],
- roomList: [],
+ lists: {},
}
},
@@ -41,6 +39,7 @@ module.exports = {
cli.on("Room", this.onRoom);
cli.on("Room.timeline", this.onRoomTimeline);
cli.on("Room.name", this.onRoomName);
+ cli.on("Room.tags", this.onRoomTags);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
@@ -55,11 +54,6 @@ module.exports = {
onAction: function(payload) {
switch (payload.action) {
- // listen for call state changes to prod the render method, which
- // may hide the global CallView if the call it is tracking is dead
- case 'call_state':
- this._recheckCallElement(this.props.selectedRoom);
- break;
case 'view_tooltip':
this.tooltip = payload.tooltip;
this._repositionTooltip();
@@ -80,7 +74,6 @@ module.exports = {
componentWillReceiveProps: function(newProps) {
this.state.activityMap[newProps.selectedRoom] = undefined;
- this._recheckCallElement(newProps.selectedRoom);
this.setState({
activityMap: this.state.activityMap
});
@@ -117,6 +110,10 @@ module.exports = {
this.refreshRoomList();
},
+ onRoomTags: function(room) {
+ this.refreshRoomList();
+ },
+
onRoomStateEvents: function(ev, state) {
setTimeout(this.refreshRoomList, 0);
},
@@ -125,26 +122,31 @@ module.exports = {
setTimeout(this.refreshRoomList, 0);
},
-
refreshRoomList: function() {
+ // TODO: rather than bluntly regenerating and re-sorting everything
+ // every time we see any kind of room change from the JS SDK
+ // we could do incremental updates on our copy of the state
+ // based on the room which has actually changed. This would stop
+ // us re-rendering all the sublists every time anything changes anywhere
+ // in the state of the client.
this.setState(this.getRoomLists());
},
getRoomLists: function() {
- var s = {};
- var inviteList = [];
- s.roomList = RoomListSorter.mostRecentActivityFirst(
- MatrixClientPeg.get().getRooms().filter(function(room) {
- var me = room.getMember(MatrixClientPeg.get().credentials.userId);
+ var s = { lists: {} };
- if (me && me.membership == "invite") {
- inviteList.push(room);
- return false;
- }
+ MatrixClientPeg.get().getRooms().forEach(function(room) {
+ var me = room.getMember(MatrixClientPeg.get().credentials.userId);
+ if (me && me.membership == "invite") {
+ s.lists["invites"] = s.lists["invites"] || [];
+ s.lists["invites"].push(room);
+ }
+ else {
var shouldShowRoom = (
me && (me.membership == "join")
);
+
// hiding conf rooms only ever toggles shouldShowRoom to false
if (shouldShowRoom && HIDE_CONFERENCE_CHANS) {
// we want to hide the 1:1 conf<->user room and not the group chat
@@ -159,23 +161,27 @@ module.exports = {
}
}
}
- return shouldShowRoom;
- })
- );
- s.inviteList = RoomListSorter.mostRecentActivityFirst(inviteList);
- return s;
- },
- _recheckCallElement: function(selectedRoomId) {
- // if we aren't viewing a room with an ongoing call, but there is an
- // active call, show the call element - we need to do this to make
- // audio/video not crap out
- var activeCall = CallHandler.getAnyActiveCall();
- var callForRoom = CallHandler.getCallForRoom(selectedRoomId);
- var showCall = (activeCall && !callForRoom);
- this.setState({
- show_call_element: showCall
+ if (shouldShowRoom) {
+ var tagNames = Object.keys(room.tags);
+ if (tagNames.length) {
+ for (var i = 0; i < tagNames.length; i++) {
+ var tagName = tagNames[i];
+ s.lists[tagName] = s.lists[tagName] || [];
+ s.lists[tagNames[i]].push(room);
+ }
+ }
+ else {
+ s.lists["recents"] = s.lists["recents"] || [];
+ s.lists["recents"].push(room);
+ }
+ }
+ }
});
+
+ // we actually apply the sorting to this when receiving the prop in RoomSubLists.
+
+ return s;
},
_repositionTooltip: function(e) {
@@ -184,23 +190,4 @@ module.exports = {
this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.scrollTop) + "px";
}
},
-
- makeRoomTiles: function(list, isInvite) {
- var self = this;
- var RoomTile = sdk.getComponent("molecules.RoomTile");
- return list.map(function(room) {
- var selected = room.roomId == self.props.selectedRoom;
- return (
-
- );
- });
- }
};
diff --git a/src/skins/vector/css/organisms/LeftPanel.css b/src/skins/vector/css/organisms/LeftPanel.css
index 67f00c358d..d755b2164e 100644
--- a/src/skins/vector/css/organisms/LeftPanel.css
+++ b/src/skins/vector/css/organisms/LeftPanel.css
@@ -34,6 +34,10 @@ limitations under the License.
cursor: pointer;
}
+.mx_LeftPanel_callView {
+
+}
+
.mx_LeftPanel .mx_RoomList {
-webkit-box-ordinal-group: 1;
-moz-box-ordinal-group: 1;
diff --git a/src/skins/vector/css/organisms/RoomList.css b/src/skins/vector/css/organisms/RoomList.css
index 34ebd1dbfb..9512354469 100644
--- a/src/skins/vector/css/organisms/RoomList.css
+++ b/src/skins/vector/css/organisms/RoomList.css
@@ -18,27 +18,9 @@ limitations under the License.
padding-top: 24px;
}
-.mx_RoomList_invites,
-.mx_RoomList_recents {
- display: table;
- table-layout: fixed;
- width: 100%;
-}
-
.mx_RoomList_expandButton {
margin-left: 8px;
cursor: pointer;
padding-left: 12px;
padding-right: 12px;
}
-
-.mx_RoomList h2 {
- text-transform: uppercase;
- color: #3d3b39;
- font-weight: 600;
- font-size: 14px;
- padding-left: 12px;
- padding-right: 12px;
- margin-top: 8px;
- margin-bottom: 4px;
-}
diff --git a/src/skins/vector/css/organisms/RoomSubList.css b/src/skins/vector/css/organisms/RoomSubList.css
new file mode 100644
index 0000000000..43f453fb54
--- /dev/null
+++ b/src/skins/vector/css/organisms/RoomSubList.css
@@ -0,0 +1,32 @@
+/*
+Copyright 2015 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_RoomSubList {
+ display: table;
+ table-layout: fixed;
+ width: 100%;
+}
+
+.mx_RoomSubList_label {
+ text-transform: uppercase;
+ color: #3d3b39;
+ font-weight: 600;
+ font-size: 14px;
+ padding-left: 12px;
+ padding-right: 12px;
+ margin-top: 8px;
+ margin-bottom: 4px;
+}
diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js
index e715656c0e..cf279c872d 100644
--- a/src/skins/vector/skindex.js
+++ b/src/skins/vector/skindex.js
@@ -30,6 +30,7 @@ skin['atoms.LogoutButton'] = require('./views/atoms/LogoutButton');
skin['atoms.MemberAvatar'] = require('./views/atoms/MemberAvatar');
skin['atoms.MessageTimestamp'] = require('./views/atoms/MessageTimestamp');
skin['atoms.RoomAvatar'] = require('./views/atoms/RoomAvatar');
+skin['atoms.Spinner'] = require('./views/atoms/Spinner');
skin['atoms.create_room.CreateRoomButton'] = require('./views/atoms/create_room/CreateRoomButton');
skin['atoms.create_room.Presets'] = require('./views/atoms/create_room/Presets');
skin['atoms.create_room.RoomAlias'] = require('./views/atoms/create_room/RoomAlias');
@@ -80,9 +81,11 @@ skin['organisms.QuestionDialog'] = require('./views/organisms/QuestionDialog');
skin['organisms.RightPanel'] = require('./views/organisms/RightPanel');
skin['organisms.RoomDirectory'] = require('./views/organisms/RoomDirectory');
skin['organisms.RoomList'] = require('./views/organisms/RoomList');
+skin['organisms.RoomSubList'] = require('./views/organisms/RoomSubList');
skin['organisms.RoomView'] = require('./views/organisms/RoomView');
skin['organisms.UserSettings'] = require('./views/organisms/UserSettings');
skin['organisms.ViewSource'] = require('./views/organisms/ViewSource');
+skin['pages.CompatibilityPage'] = require('./views/pages/CompatibilityPage');
skin['pages.MatrixChat'] = require('./views/pages/MatrixChat');
skin['templates.Login'] = require('./views/templates/Login');
skin['templates.Register'] = require('./views/templates/Register');
diff --git a/src/skins/vector/views/organisms/LeftPanel.js b/src/skins/vector/views/organisms/LeftPanel.js
index ec25f9342a..8f4b510a87 100644
--- a/src/skins/vector/views/organisms/LeftPanel.js
+++ b/src/skins/vector/views/organisms/LeftPanel.js
@@ -20,9 +20,51 @@ var React = require('react');
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
+var CallHandler = require("matrix-react-sdk/lib/CallHandler");
+
module.exports = React.createClass({
displayName: 'LeftPanel',
+ getInitialState: function() {
+ return {
+ showCallElement: null,
+ };
+ },
+
+ componentDidMount: function() {
+ this.dispatcherRef = dis.register(this.onAction);
+ },
+
+ componentWillReceiveProps: function(newProps) {
+ this._recheckCallElement(newProps.selectedRoom);
+ },
+
+ componentWillUnmount: function() {
+ dis.unregister(this.dispatcherRef);
+ },
+
+ onAction: function(payload) {
+ switch (payload.action) {
+ // listen for call state changes to prod the render method, which
+ // may hide the global CallView if the call it is tracking is dead
+ case 'call_state':
+ this._recheckCallElement(this.props.selectedRoom);
+ break;
+ }
+ },
+
+ _recheckCallElement: function(selectedRoomId) {
+ // if we aren't viewing a room with an ongoing call, but there is an
+ // active call, show the call element - we need to do this to make
+ // audio/video not crap out
+ var activeCall = CallHandler.getAnyActiveCall();
+ var callForRoom = CallHandler.getCallForRoom(selectedRoomId);
+ var showCall = (activeCall && !callForRoom);
+ this.setState({
+ showCallElement: showCall
+ });
+ },
+
onHideClick: function() {
dis.dispatch({
action: 'hide_left_panel',
@@ -44,10 +86,17 @@ module.exports = React.createClass({
// collapseButton =
}
+ var callPreview;
+ if (this.state.showCallElement) {
+ var CallView = sdk.getComponent('molecules.voip.CallView');
+ callPreview =
+ }
+
return (
diff --git a/src/skins/vector/views/organisms/RoomList.js b/src/skins/vector/views/organisms/RoomList.js
index 1d4ee86b75..81d23867d3 100644
--- a/src/skins/vector/views/organisms/RoomList.js
+++ b/src/skins/vector/views/organisms/RoomList.js
@@ -33,46 +33,57 @@ module.exports = React.createClass({
},
render: function() {
- var CallView = sdk.getComponent('molecules.voip.CallView');
- var RoomDropTarget = sdk.getComponent('molecules.RoomDropTarget');
-
- var callElement;
- if (this.state.show_call_element) {
- callElement =
- }
-
var expandButton = this.props.collapsed ?
:
null;
- var invitesLabel = this.props.collapsed ? null : "Invites";
- var recentsLabel = this.props.collapsed ? null : "Recent";
-
- var invites;
- if (this.state.inviteList.length) {
- invites =
-
{ invitesLabel }
-
- {this.makeRoomTiles(this.state.inviteList, true)}
-
-
- }
+ var RoomSubList = sdk.getComponent('organisms.RoomSubList');
return (
{ expandButton }
- { callElement }
- { invites }
-
Favourites
-
-
{ recentsLabel }
-
- {this.makeRoomTiles(this.state.roomList, false)}
-
+
-
Archive
-
+
+
+
+
+
+
+
);
}
diff --git a/src/skins/vector/views/organisms/RoomSubList.js b/src/skins/vector/views/organisms/RoomSubList.js
new file mode 100644
index 0000000000..b8747ecf22
--- /dev/null
+++ b/src/skins/vector/views/organisms/RoomSubList.js
@@ -0,0 +1,124 @@
+/*
+Copyright 2015 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+var React = require('react');
+var sdk = require('matrix-react-sdk')
+var dis = require('matrix-react-sdk/lib/dispatcher');
+
+module.exports = React.createClass({
+ displayName: 'RoomSubList',
+
+ propTypes: {
+ list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ label: React.PropTypes.string.isRequired,
+ tagname: React.PropTypes.string,
+ editable: React.PropTypes.bool,
+ order: React.PropTypes.string.isRequired,
+ selectedRoom: React.PropTypes.string.isRequired,
+ activityMap: React.PropTypes.object.isRequired,
+ collapsed: React.PropTypes.bool.isRequired
+ },
+
+ getInitialState: function() {
+ return {
+ sortedList: [],
+ };
+ },
+
+ componentWillMount: function() {
+ this.sortList(this.props.list, this.props.order);
+ },
+
+ componentWillReceiveProps: function(newProps) {
+ // order the room list appropriately before we re-render
+ this.sortList(newProps.list, newProps.order);
+ },
+
+ tsOfNewestEvent: function(room) {
+ if (room.timeline.length) {
+ return room.timeline[room.timeline.length - 1].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);
+ },
+
+ manualComparator: function(roomA, roomB) {
+ var a = roomA.tags[this.props.tagname].order;
+ var b = roomB.tags[this.props.tagname].order;
+ return a == b ? this.recentsComparator(roomA, roomB) : ( a > b ? 1 : -1);
+ },
+
+ sortList: function(list, order) {
+ var comparator;
+ list = list || [];
+ if (order === "manual") comparator = this.manualComparator;
+ if (order === "recent") comparator = this.recentsComparator;
+
+ this.setState({ sortedList: list.sort(comparator) });
+ },
+
+ makeRoomTiles: function() {
+ var self = this;
+ var RoomTile = sdk.getComponent("molecules.RoomTile");
+ return this.state.sortedList.map(function(room) {
+ var selected = room.roomId == self.props.selectedRoom;
+ return (
+
+ );
+ });
+ },
+
+ render: function() {
+ var RoomDropTarget = sdk.getComponent('molecules.RoomDropTarget');
+
+ var label = this.props.collapsed ? null : this.props.label;
+
+ if (this.state.sortedList.length > 0 || this.props.editable) {
+ return (
+
+
{ this.props.label }
+
+ { this.makeRoomTiles() }
+
+
+ );
+ }
+ else {
+ return (
+
+
+ );
+ }
+ }
+});
+