diff --git a/package.json b/package.json index 60d02c4d46..2001f0d4ad 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "lodash": "^4.13.1", "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "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/component-index.js b/src/component-index.js index d6873c6dfd..090a27d5ed 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -103,10 +103,14 @@ import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/Unknow views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog); import views$elements$AccessibleButton from './components/views/elements/AccessibleButton'; views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton); +import views$elements$ActionButton from './components/views/elements/ActionButton'; +views$elements$ActionButton && (module.exports.components['views.elements.ActionButton'] = views$elements$ActionButton); import views$elements$AddressSelector from './components/views/elements/AddressSelector'; views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector); import views$elements$AddressTile from './components/views/elements/AddressTile'; views$elements$AddressTile && (module.exports.components['views.elements.AddressTile'] = views$elements$AddressTile); +import views$elements$CreateRoomButton from './components/views/elements/CreateRoomButton'; +views$elements$CreateRoomButton && (module.exports.components['views.elements.CreateRoomButton'] = views$elements$CreateRoomButton); import views$elements$DeviceVerifyButtons from './components/views/elements/DeviceVerifyButtons'; views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons); import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox'; @@ -119,12 +123,20 @@ import views$elements$EditableTextContainer from './components/views/elements/Ed views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer); import views$elements$EmojiText from './components/views/elements/EmojiText'; views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText); +import views$elements$HomeButton from './components/views/elements/HomeButton'; +views$elements$HomeButton && (module.exports.components['views.elements.HomeButton'] = views$elements$HomeButton); import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary'; views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary); import views$elements$PowerSelector from './components/views/elements/PowerSelector'; views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector); import views$elements$ProgressBar from './components/views/elements/ProgressBar'; views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar); +import views$elements$RoomDirectoryButton from './components/views/elements/RoomDirectoryButton'; +views$elements$RoomDirectoryButton && (module.exports.components['views.elements.RoomDirectoryButton'] = views$elements$RoomDirectoryButton); +import views$elements$SettingsButton from './components/views/elements/SettingsButton'; +views$elements$SettingsButton && (module.exports.components['views.elements.SettingsButton'] = views$elements$SettingsButton); +import views$elements$StartChatButton from './components/views/elements/StartChatButton'; +views$elements$StartChatButton && (module.exports.components['views.elements.StartChatButton'] = views$elements$StartChatButton); import views$elements$TintableSvg from './components/views/elements/TintableSvg'; views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg); import views$elements$TruncatedList from './components/views/elements/TruncatedList'; diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js new file mode 100644 index 0000000000..267388daf6 --- /dev/null +++ b/src/components/views/elements/ActionButton.js @@ -0,0 +1,80 @@ +/* +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, + 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}); + }, + + _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..e7e526d36b --- /dev/null +++ b/src/components/views/elements/CreateRoomButton.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 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..5e68776a15 --- /dev/null +++ b/src/components/views/elements/RoomDirectoryButton.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 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..02d5677a7c --- /dev/null +++ b/src/components/views/elements/StartChatButton.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 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 49af1560f1..8d396b5536 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. @@ -28,8 +29,16 @@ var Rooms = require('../../../Rooms'); import DMRoomMap from '../../../utils/DMRoomMap'; var Receipt = require('../../../utils/Receipt'); var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); +import AccessibleButton from '../elements/AccessibleButton'; -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', @@ -53,6 +62,7 @@ module.exports = React.createClass({ getInitialState: function() { return { isLoadingLeftRooms: false, + totalRoomCount: null, lists: {}, incomingCall: null, }; @@ -73,8 +83,7 @@ module.exports = React.createClass({ // lookup for which lists a given roomId is currently in. this.listsForRoomId = {}; - var s = this.getRoomLists(); - this.setState(s); + this.refreshRoomList(); // order of the sublists //this.listOrder = []; @@ -317,21 +326,29 @@ module.exports = React.createClass({ // any changes to it incrementally, updating the appropriate sublists // as needed. // Alternatively we'd do something magical with Immutable.js or similar. - this.setState(this.getRoomLists()); - + 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"] = []; this.listsForRoomId = {}; var otherTagNames = {}; @@ -353,7 +370,7 @@ module.exports = React.createClass({ if (me.membership == "invite") { self.listsForRoomId[room.roomId].push("im.vector.fake.invite"); - s.lists["im.vector.fake.invite"].push(room); + 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 @@ -366,8 +383,8 @@ 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[tagName].push(room); + lists[tagName] = lists[tagName] || []; + lists[tagName].push(room); self.listsForRoomId[room.roomId].push(tagName); otherTagNames[tagName] = 1; } @@ -375,16 +392,16 @@ module.exports = React.createClass({ else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { // "Direct Message" rooms (that we're still in and that aren't otherwise tagged) self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); - s.lists["im.vector.fake.direct"].push(room); + lists["im.vector.fake.direct"].push(room); } else { self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); - s.lists["im.vector.fake.recent"].push(room); + lists["im.vector.fake.recent"].push(room); } } else if (me.membership === "leave") { self.listsForRoomId[room.roomId].push("im.vector.fake.archived"); - s.lists["im.vector.fake.archived"].push(room); + lists["im.vector.fake.archived"].push(room); } else { console.error("unrecognised membership: " + me.membership + " - this should never happen"); @@ -408,7 +425,7 @@ module.exports = React.createClass({ ]; */ - return s; + return lists; }, _getScrollNode: function() { @@ -438,6 +455,7 @@ module.exports = React.createClass({ var incomingCallBox = document.getElementById("incomingCallBox"); if (incomingCallBox && incomingCallBox.parentElement) { var scrollArea = this._getScrollNode(); + if (!scrollArea) return; // Use the offset of the top of the scroll area from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; @@ -461,6 +479,7 @@ module.exports = React.createClass({ // properly through React _initAndPositionStickyHeaders: function(initialise, scrollToPosition) { var scrollArea = this._getScrollNode(); + if (!scrollArea) return; // Use the offset of the top of the scroll area from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; @@ -558,6 +577,58 @@ 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 +
; + } + } + + 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; @@ -581,7 +652,7 @@ module.exports = React.createClass({