refactor out the sections of the RoomList into RoomSubLists. Start wiring up tags

This commit is contained in:
Matthew Hodgson 2015-11-04 00:19:37 +00:00
parent 8b9b268ec0
commit 7fe7af6026
9 changed files with 295 additions and 102 deletions

View File

@ -34,7 +34,8 @@
"q": "^1.4.1", "q": "^1.4.1",
"react": "^0.13.3", "react": "^0.13.3",
"react-loader": "^1.4.0", "react-loader": "^1.4.0",
"sanitize-html": "^1.11.1" "react-dnd": "^1.1.8",
"sanitize-html": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"babel": "^5.8.23", "babel": "^5.8.23",

View File

@ -23,7 +23,6 @@ var dis = require("matrix-react-sdk/lib/dispatcher");
var sdk = require('matrix-react-sdk'); var sdk = require('matrix-react-sdk');
var VectorConferenceHandler = require("../../modules/VectorConferenceHandler"); var VectorConferenceHandler = require("../../modules/VectorConferenceHandler");
var CallHandler = require("matrix-react-sdk/lib/CallHandler");
var HIDE_CONFERENCE_CHANS = true; var HIDE_CONFERENCE_CHANS = true;
@ -31,8 +30,7 @@ module.exports = {
getInitialState: function() { getInitialState: function() {
return { return {
activityMap: null, activityMap: null,
inviteList: [], lists: {},
roomList: [],
} }
}, },
@ -41,6 +39,7 @@ module.exports = {
cli.on("Room", this.onRoom); cli.on("Room", this.onRoom);
cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.timeline", this.onRoomTimeline);
cli.on("Room.name", this.onRoomName); cli.on("Room.name", this.onRoomName);
cli.on("Room.tags", this.onRoomTags);
cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName); cli.on("RoomMember.name", this.onRoomMemberName);
@ -55,11 +54,6 @@ module.exports = {
onAction: function(payload) { onAction: function(payload) {
switch (payload.action) { 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': case 'view_tooltip':
this.tooltip = payload.tooltip; this.tooltip = payload.tooltip;
this._repositionTooltip(); this._repositionTooltip();
@ -80,7 +74,6 @@ module.exports = {
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
this.state.activityMap[newProps.selectedRoom] = undefined; this.state.activityMap[newProps.selectedRoom] = undefined;
this._recheckCallElement(newProps.selectedRoom);
this.setState({ this.setState({
activityMap: this.state.activityMap activityMap: this.state.activityMap
}); });
@ -117,6 +110,10 @@ module.exports = {
this.refreshRoomList(); this.refreshRoomList();
}, },
onRoomTags: function(room) {
this.refreshRoomList();
},
onRoomStateEvents: function(ev, state) { onRoomStateEvents: function(ev, state) {
setTimeout(this.refreshRoomList, 0); setTimeout(this.refreshRoomList, 0);
}, },
@ -125,26 +122,31 @@ module.exports = {
setTimeout(this.refreshRoomList, 0); setTimeout(this.refreshRoomList, 0);
}, },
refreshRoomList: function() { 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()); this.setState(this.getRoomLists());
}, },
getRoomLists: function() { getRoomLists: function() {
var s = {}; var s = { lists: {} };
var inviteList = [];
s.roomList = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms().filter(function(room) {
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && me.membership == "invite") { MatrixClientPeg.get().getRooms().forEach(function(room) {
inviteList.push(room); var me = room.getMember(MatrixClientPeg.get().credentials.userId);
return false;
}
if (me && me.membership == "invite") {
s.lists["invites"] = s.lists["invites"] || [];
s.lists["invites"].push(room);
}
else {
var shouldShowRoom = ( var shouldShowRoom = (
me && (me.membership == "join") me && (me.membership == "join")
); );
// hiding conf rooms only ever toggles shouldShowRoom to false // hiding conf rooms only ever toggles shouldShowRoom to false
if (shouldShowRoom && HIDE_CONFERENCE_CHANS) { if (shouldShowRoom && HIDE_CONFERENCE_CHANS) {
// we want to hide the 1:1 conf<->user room and not the group chat // 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 (shouldShowRoom) {
// if we aren't viewing a room with an ongoing call, but there is an var tagNames = Object.keys(room.tags);
// active call, show the call element - we need to do this to make if (tagNames.length) {
// audio/video not crap out for (var i = 0; i < tagNames.length; i++) {
var activeCall = CallHandler.getAnyActiveCall(); var tagName = tagNames[i];
var callForRoom = CallHandler.getCallForRoom(selectedRoomId); s.lists[tagName] = s.lists[tagName] || [];
var showCall = (activeCall && !callForRoom); s.lists[tagNames[i]].push(room);
this.setState({ }
show_call_element: showCall }
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) { _repositionTooltip: function(e) {
@ -184,23 +190,4 @@ module.exports = {
this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.scrollTop) + "px"; 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 (
<RoomTile
room={room}
key={room.roomId}
collapsed={self.props.collapsed}
selected={selected}
unread={self.state.activityMap[room.roomId] === 1}
highlight={self.state.activityMap[room.roomId] === 2}
isInvite={isInvite}
/>
);
});
}
}; };

View File

@ -34,6 +34,10 @@ limitations under the License.
cursor: pointer; cursor: pointer;
} }
.mx_LeftPanel_callView {
}
.mx_LeftPanel .mx_RoomList { .mx_LeftPanel .mx_RoomList {
-webkit-box-ordinal-group: 1; -webkit-box-ordinal-group: 1;
-moz-box-ordinal-group: 1; -moz-box-ordinal-group: 1;

View File

@ -18,27 +18,9 @@ limitations under the License.
padding-top: 24px; padding-top: 24px;
} }
.mx_RoomList_invites,
.mx_RoomList_recents {
display: table;
table-layout: fixed;
width: 100%;
}
.mx_RoomList_expandButton { .mx_RoomList_expandButton {
margin-left: 8px; margin-left: 8px;
cursor: pointer; cursor: pointer;
padding-left: 12px; padding-left: 12px;
padding-right: 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;
}

View File

@ -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;
}

View File

@ -30,6 +30,7 @@ skin['atoms.LogoutButton'] = require('./views/atoms/LogoutButton');
skin['atoms.MemberAvatar'] = require('./views/atoms/MemberAvatar'); skin['atoms.MemberAvatar'] = require('./views/atoms/MemberAvatar');
skin['atoms.MessageTimestamp'] = require('./views/atoms/MessageTimestamp'); skin['atoms.MessageTimestamp'] = require('./views/atoms/MessageTimestamp');
skin['atoms.RoomAvatar'] = require('./views/atoms/RoomAvatar'); 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.CreateRoomButton'] = require('./views/atoms/create_room/CreateRoomButton');
skin['atoms.create_room.Presets'] = require('./views/atoms/create_room/Presets'); skin['atoms.create_room.Presets'] = require('./views/atoms/create_room/Presets');
skin['atoms.create_room.RoomAlias'] = require('./views/atoms/create_room/RoomAlias'); 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.RightPanel'] = require('./views/organisms/RightPanel');
skin['organisms.RoomDirectory'] = require('./views/organisms/RoomDirectory'); skin['organisms.RoomDirectory'] = require('./views/organisms/RoomDirectory');
skin['organisms.RoomList'] = require('./views/organisms/RoomList'); skin['organisms.RoomList'] = require('./views/organisms/RoomList');
skin['organisms.RoomSubList'] = require('./views/organisms/RoomSubList');
skin['organisms.RoomView'] = require('./views/organisms/RoomView'); skin['organisms.RoomView'] = require('./views/organisms/RoomView');
skin['organisms.UserSettings'] = require('./views/organisms/UserSettings'); skin['organisms.UserSettings'] = require('./views/organisms/UserSettings');
skin['organisms.ViewSource'] = require('./views/organisms/ViewSource'); skin['organisms.ViewSource'] = require('./views/organisms/ViewSource');
skin['pages.CompatibilityPage'] = require('./views/pages/CompatibilityPage');
skin['pages.MatrixChat'] = require('./views/pages/MatrixChat'); skin['pages.MatrixChat'] = require('./views/pages/MatrixChat');
skin['templates.Login'] = require('./views/templates/Login'); skin['templates.Login'] = require('./views/templates/Login');
skin['templates.Register'] = require('./views/templates/Register'); skin['templates.Register'] = require('./views/templates/Register');

View File

@ -20,9 +20,51 @@ var React = require('react');
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher'); var dis = require('matrix-react-sdk/lib/dispatcher');
var CallHandler = require("matrix-react-sdk/lib/CallHandler");
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'LeftPanel', 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() { onHideClick: function() {
dis.dispatch({ dis.dispatch({
action: 'hide_left_panel', action: 'hide_left_panel',
@ -44,10 +86,17 @@ module.exports = React.createClass({
// collapseButton = <img className="mx_LeftPanel_hideButton" onClick={ this.onHideClick } src="img/hide.png" width="12" height="20" alt="<"/> // collapseButton = <img className="mx_LeftPanel_hideButton" onClick={ this.onHideClick } src="img/hide.png" width="12" height="20" alt="<"/>
} }
var callPreview;
if (this.state.showCallElement) {
var CallView = sdk.getComponent('molecules.voip.CallView');
callPreview = <CallView className="mx_LeftPanel_callView"/>
}
return ( return (
<aside className={classes}> <aside className={classes}>
{ collapseButton } { collapseButton }
<IncomingCallBox /> <IncomingCallBox />
{ callPreview }
<RoomList selectedRoom={this.props.selectedRoom} collapsed={this.props.collapsed}/> <RoomList selectedRoom={this.props.selectedRoom} collapsed={this.props.collapsed}/>
<BottomLeftMenu collapsed={this.props.collapsed}/> <BottomLeftMenu collapsed={this.props.collapsed}/>
</aside> </aside>

View File

@ -33,46 +33,57 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var CallView = sdk.getComponent('molecules.voip.CallView');
var RoomDropTarget = sdk.getComponent('molecules.RoomDropTarget');
var callElement;
if (this.state.show_call_element) {
callElement = <CallView className="mx_MatrixChat_callView"/>
}
var expandButton = this.props.collapsed ? var expandButton = this.props.collapsed ?
<img className="mx_RoomList_expandButton" onClick={ this.onShowClick } src="img/menu.png" width="20" alt=">"/> : <img className="mx_RoomList_expandButton" onClick={ this.onShowClick } src="img/menu.png" width="20" alt=">"/> :
null; null;
var invitesLabel = this.props.collapsed ? null : "Invites"; var RoomSubList = sdk.getComponent('organisms.RoomSubList');
var recentsLabel = this.props.collapsed ? null : "Recent";
var invites;
if (this.state.inviteList.length) {
invites = <div>
<h2 className="mx_RoomList_invitesLabel">{ invitesLabel }</h2>
<div className="mx_RoomList_invites">
{this.makeRoomTiles(this.state.inviteList, true)}
</div>
</div>
}
return ( return (
<div className="mx_RoomList" onScroll={this._repositionTooltip}> <div className="mx_RoomList" onScroll={this._repositionTooltip}>
{ expandButton } { expandButton }
{ callElement }
{ invites }
<h2 className="mx_RoomList_favouritesLabel">Favourites</h2>
<RoomDropTarget text="Drop here to favourite"/>
<h2 className="mx_RoomList_recentsLabel">{ recentsLabel }</h2> <RoomSubList list={ this.state.lists['invites'] }
<div className="mx_RoomList_recents"> label="Invites"
{this.makeRoomTiles(this.state.roomList, false)} editable={ false }
</div> order="recent"
activityMap={ this.state.activityMap }
selectedRoom={ this.props.selectedRoom }
collapsed={ this.props.collapsed } />
<h2 className="mx_RoomList_archiveLabel">Archive</h2> <RoomSubList list={ this.state.lists['favourites'] }
<RoomDropTarget text="Drop here to archive"/> label="Favourites"
tagname="favourites"
editable={ true }
order="manual"
activityMap={ this.state.activityMap }
selectedRoom={ this.props.selectedRoom }
collapsed={ this.props.collapsed } />
<RoomSubList list={ this.state.lists['recents'] }
label="Recents"
editable={ true }
order="recent"
activityMap={ this.state.activityMap }
selectedRoom={ this.props.selectedRoom }
collapsed={ this.props.collapsed } />
<RoomSubList list={ this.state.lists['lurking'] }
label="Others"
tagname="secondary"
editable={ true }
order="recent"
activityMap={ this.state.activityMap }
selectedRoom={ this.props.selectedRoom }
collapsed={ this.props.collapsed } />
<RoomSubList list={ this.state.lists['archived'] }
label="Historical"
editable={ false }
order="recent"
activityMap={ this.state.activityMap }
selectedRoom={ this.props.selectedRoom }
collapsed={ this.props.collapsed } />
</div> </div>
); );
} }

View File

@ -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 (
<RoomTile
room={room}
key={room.roomId}
collapsed={self.props.collapsed}
selected={selected}
unread={self.props.activityMap[room.roomId] === 1}
highlight={self.props.activityMap[room.roomId] === 2}
isInvite={self.props.label === 'Invites'} />
);
});
},
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 (
<div>
<h2 className="mx_RoomSubList_label">{ this.props.label }</h2>
<div className="mx_RoomSubList">
{ this.makeRoomTiles() }
</div>
</div>
);
}
else {
return (
<div className="mx_RoomSubList">
</div>
);
}
}
});