diff --git a/package.json b/package.json index eb9c3aff9..96176843b 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 514ae7747..38a0c652c 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 67f00c358..d755b2164 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 34ebd1dbf..951235446 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 000000000..43f453fb5 --- /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 e715656c0..cf279c872 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 ec25f9342..8f4b510a8 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 1d4ee86b7..81d23867d 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 000000000..b8747ecf2 --- /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 ( +
+
+ ); + } + } +}); +