diff --git a/package.json b/package.json
index 059fdd390f..572dcddeb5 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
"lodash": "^4.13.1",
"matrix-js-sdk": "0.7.8",
"optimist": "^0.6.1",
+ "prop-types": "^15.5.8",
"q": "^1.4.1",
"react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2",
diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js
new file mode 100644
index 0000000000..08fb6faa1d
--- /dev/null
+++ b/src/components/views/elements/ActionButton.js
@@ -0,0 +1,84 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import AccessibleButton from './AccessibleButton';
+import dis from '../../../dispatcher';
+import sdk from '../../../index';
+
+export default React.createClass({
+ displayName: 'RoleButton',
+
+ propTypes: {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+ action: PropTypes.string.isRequired,
+ mouseOverAction: PropTypes.string,
+ label: PropTypes.string.isRequired,
+ iconPath: PropTypes.string.isRequired,
+ },
+
+ getDefaultProps: function() {
+ return {
+ size: "25",
+ tooltip: false,
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ showTooltip: false,
+ };
+ },
+
+ _onClick: function(ev) {
+ ev.stopPropagation();
+ dis.dispatch({action: this.props.action});
+ },
+
+ _onMouseEnter: function() {
+ if (this.props.tooltip) this.setState({showTooltip: true});
+ if (this.props.mouseOverAction) {
+ dis.dispatch({action: this.props.mouseOverAction});
+ }
+ },
+
+ _onMouseLeave: function() {
+ this.setState({showTooltip: false});
+ },
+
+ render: function() {
+ const TintableSvg = sdk.getComponent("elements.TintableSvg");
+
+ let tooltip;
+ if (this.state.showTooltip) {
+ const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
+ tooltip = ;
+ }
+
+ return (
+
+
+ {tooltip}
+
+ );
+ }
+});
diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js
new file mode 100644
index 0000000000..82643559b3
--- /dev/null
+++ b/src/components/views/elements/CreateRoomButton.js
@@ -0,0 +1,39 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import React from 'react';
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+
+const CreateRoomButton = function(props) {
+ const ActionButton = sdk.getComponent('elements.ActionButton');
+ return (
+
+ );
+};
+
+CreateRoomButton.propTypes = {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+};
+
+export default CreateRoomButton;
diff --git a/src/components/views/elements/HomeButton.js b/src/components/views/elements/HomeButton.js
new file mode 100644
index 0000000000..5c446f24c9
--- /dev/null
+++ b/src/components/views/elements/HomeButton.js
@@ -0,0 +1,38 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import React from 'react';
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+
+const HomeButton = function(props) {
+ const ActionButton = sdk.getComponent('elements.ActionButton');
+ return (
+
+ );
+};
+
+HomeButton.propTypes = {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+};
+
+export default HomeButton;
diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js
new file mode 100644
index 0000000000..fc04a666df
--- /dev/null
+++ b/src/components/views/elements/RoomDirectoryButton.js
@@ -0,0 +1,39 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import React from 'react';
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+
+const RoomDirectoryButton = function(props) {
+ const ActionButton = sdk.getComponent('elements.ActionButton');
+ return (
+
+ );
+};
+
+RoomDirectoryButton.propTypes = {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+};
+
+export default RoomDirectoryButton;
diff --git a/src/components/views/elements/SettingsButton.js b/src/components/views/elements/SettingsButton.js
new file mode 100644
index 0000000000..c6438da277
--- /dev/null
+++ b/src/components/views/elements/SettingsButton.js
@@ -0,0 +1,38 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import React from 'react';
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+
+const SettingsButton = function(props) {
+ const ActionButton = sdk.getComponent('elements.ActionButton');
+ return (
+
+ );
+};
+
+SettingsButton.propTypes = {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+};
+
+export default SettingsButton;
diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js
new file mode 100644
index 0000000000..c13b51ee67
--- /dev/null
+++ b/src/components/views/elements/StartChatButton.js
@@ -0,0 +1,39 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import React from 'react';
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+
+const StartChatButton = function(props) {
+ const ActionButton = sdk.getComponent('elements.ActionButton');
+ return (
+
+ );
+};
+
+StartChatButton.propTypes = {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+};
+
+export default StartChatButton;
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 760b0543c6..216dd972cf 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -29,7 +30,14 @@ var Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap';
var Receipt = require('../../../utils/Receipt');
-var HIDE_CONFERENCE_CHANS = true;
+const HIDE_CONFERENCE_CHANS = true;
+
+const VERBS = {
+ 'm.favourite': 'favourite',
+ 'im.vector.fake.direct': 'tag direct chat',
+ 'im.vector.fake.recent': 'restore',
+ 'm.lowpriority': 'demote',
+};
module.exports = React.createClass({
displayName: 'RoomList',
@@ -44,6 +52,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
isLoadingLeftRooms: false,
+ totalRoomCount: null,
lists: {},
incomingCall: null,
};
@@ -63,8 +72,17 @@ module.exports = React.createClass({
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);
- var s = this.getRoomLists();
- this.setState(s);
+ // lookup for which lists a given roomId is currently in.
+ this.listsForRoomId = {};
+
+ this.refreshRoomList();
+
+ // order of the sublists
+ //this.listOrder = [];
+
+ // loop count to stop a stack overflow if the user keeps waggling the
+ // mouse for >30s in a row, or if running under mocha
+ this._delayedRefreshRoomListLoopCount = 0
},
componentDidMount: function() {
@@ -202,31 +220,33 @@ module.exports = React.createClass({
}, 500),
refreshRoomList: function() {
- // console.log("DEBUG: Refresh room list delta=%s ms",
- // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
- // );
-
- // 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());
+ // TODO: ideally we'd calculate this once at start, and then maintain
+ // any changes to it incrementally, updating the appropriate sublists
+ // as needed.
+ // Alternatively we'd do something magical with Immutable.js or similar.
+ const lists = this.getRoomLists();
+ let totalRooms = 0;
+ for (const l of Object.values(lists)) {
+ totalRooms += l.length;
+ }
+ this.setState({
+ lists: this.getRoomLists(),
+ totalRoomCount: totalRooms,
+ });
// this._lastRefreshRoomListTs = Date.now();
},
getRoomLists: function() {
var self = this;
- var s = { lists: {} };
+ const lists = {};
- s.lists["im.vector.fake.invite"] = [];
- s.lists["m.favourite"] = [];
- s.lists["im.vector.fake.recent"] = [];
- s.lists["im.vector.fake.direct"] = [];
- s.lists["m.lowpriority"] = [];
- s.lists["im.vector.fake.archived"] = [];
+ lists["im.vector.fake.invite"] = [];
+ lists["m.favourite"] = [];
+ lists["im.vector.fake.recent"] = [];
+ lists["im.vector.fake.direct"] = [];
+ lists["m.lowpriority"] = [];
+ lists["im.vector.fake.archived"] = [];
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
@@ -240,7 +260,8 @@ module.exports = React.createClass({
// ", prevMembership = " + me.events.member.getPrevContent().membership);
if (me.membership == "invite") {
- s.lists["im.vector.fake.invite"].push(room);
+ self.listsForRoomId[room.roomId].push("im.vector.fake.invite");
+ lists["im.vector.fake.invite"].push(room);
}
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
// skip past this room & don't put it in any lists
@@ -254,48 +275,55 @@ module.exports = React.createClass({
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);
+ lists[tagName] = lists[tagName] || [];
+ lists[tagName].push(room);
+ self.listsForRoomId[room.roomId].push(tagName);
+ otherTagNames[tagName] = 1;
}
}
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
- s.lists["im.vector.fake.direct"].push(room);
+ self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
+ lists["im.vector.fake.direct"].push(room);
}
else {
- s.lists["im.vector.fake.recent"].push(room);
+ self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
+ lists["im.vector.fake.recent"].push(room);
}
}
else if (me.membership === "leave") {
- s.lists["im.vector.fake.archived"].push(room);
+ self.listsForRoomId[room.roomId].push("im.vector.fake.archived");
+ lists["im.vector.fake.archived"].push(room);
}
else {
console.error("unrecognised membership: " + me.membership + " - this should never happen");
}
});
- if (s.lists["im.vector.fake.direct"].length == 0 &&
+ if (lists["im.vector.fake.direct"].length == 0 &&
MatrixClientPeg.get().getAccountData('m.direct') === undefined &&
!MatrixClientPeg.get().isGuest())
{
// scan through the 'recents' list for any rooms which look like DM rooms
// and make them DM rooms
- const oldRecents = s.lists["im.vector.fake.recent"];
- s.lists["im.vector.fake.recent"] = [];
+ const oldRecents = lists["im.vector.fake.recent"];
+ lists["im.vector.fake.recent"] = [];
for (const room of oldRecents) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
- s.lists["im.vector.fake.direct"].push(room);
+ self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
+ lists["im.vector.fake.direct"].push(room);
} else {
- s.lists["im.vector.fake.recent"].push(room);
+ self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
+ lists["im.vector.fake.recent"].push(room);
}
}
// save these new guessed DM rooms into the account data
const newMDirectEvent = {};
- for (const room of s.lists["im.vector.fake.direct"]) {
+ for (const room of lists["im.vector.fake.direct"]) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
const otherPerson = Rooms.getOnlyOtherMember(room, me);
if (!otherPerson) continue;
@@ -313,7 +341,22 @@ module.exports = React.createClass({
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
- return s;
+ // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
+/*
+ this.listOrder = [
+ "im.vector.fake.invite",
+ "m.favourite",
+ "im.vector.fake.recent",
+ "im.vector.fake.direct",
+ Object.keys(otherTagNames).filter(tagName=>{
+ return (!tagName.match(/^m\.(favourite|lowpriority)$/));
+ }).sort(),
+ "m.lowpriority",
+ "im.vector.fake.archived"
+ ];
+*/
+
+ return lists;
},
_getScrollNode: function() {
@@ -467,6 +510,62 @@ module.exports = React.createClass({
this.refs.gemscroll.forceUpdate();
},
+ _getEmptyContent: function(section) {
+ const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
+
+ if (this.props.collapsed) {
+ return ;
+ }
+
+ const StartChatButton = sdk.getComponent('elements.StartChatButton');
+ const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
+ const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
+ if (this.state.totalRoomCount === 0) {
+ const TintableSvg = sdk.getComponent('elements.TintableSvg');
+ switch (section) {
+ case 'im.vector.fake.direct':
+ return
+ Press
+
+ to start a chat with someone
+
;
+ case 'im.vector.fake.recent':
+ return
+ You're not in any rooms yet! Press
+
+ to make a room or
+
+ to browse the directory
+
;
+ }
+ }
+
+ if (this.state.totalRoomCount === 0) {
+ return null;
+ }
+
+ const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
+
+ return ;
+ },
+
+ _getHeaderItems: function(section) {
+ const StartChatButton = sdk.getComponent('elements.StartChatButton');
+ const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
+ const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
+ switch (section) {
+ case 'im.vector.fake.direct':
+ return
+
+ ;
+ case 'im.vector.fake.recent':
+ return
+
+
+ ;
+ }
+ },
+
render: function() {
var RoomSubList = sdk.getComponent('structures.RoomSubList');
var self = this;
@@ -489,7 +588,7 @@ module.exports = React.createClass({