diff --git a/src/component-index.js b/src/component-index.js index a2ee1ad4e..ec597609a 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -38,6 +38,7 @@ module.exports.components['views.context_menus.MessageContextMenu'] = require('. module.exports.components['views.context_menus.NotificationStateContextMenu'] = require('./components/views/context_menus/NotificationStateContextMenu'); module.exports.components['views.context_menus.RoomTagContextMenu'] = require('./components/views/context_menus/RoomTagContextMenu'); module.exports.components['views.dialogs.ChangelogDialog'] = require('./components/views/dialogs/ChangelogDialog'); +module.exports.components['views.directory.NetworkDropdown'] = require('./components/views/directory/NetworkDropdown'); module.exports.components['views.elements.ImageView'] = require('./components/views/elements/ImageView'); module.exports.components['views.elements.Spinner'] = require('./components/views/elements/Spinner'); module.exports.components['views.globals.GuestWarningBar'] = require('./components/views/globals/GuestWarningBar'); diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index e4a8524b9..3d8249968 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -35,15 +35,36 @@ linkifyMatrix(linkify); module.exports = React.createClass({ displayName: 'RoomDirectory', + propTypes: { + config: React.PropTypes.object, + }, + + getDefaultProps: function() { + return { + config: { + networks: [], + }, + } + }, + getInitialState: function() { return { publicRooms: [], roomAlias: '', loading: true, + filterByNetwork: null, } }, componentWillMount: function() { + // precompile Regexps + this.networkPatterns = {}; + if (this.props.config.networkPatterns) { + for (const network of Object.keys(this.props.config.networkPatterns)) { + this.networkPatterns[network] = new RegExp(this.props.config.networkPatterns[network]); + } + } + // dis.dispatch({ // action: 'ui_opacity', // sideOpacity: 0.3, @@ -143,6 +164,12 @@ module.exports = React.createClass({ } }, + onNetworkChange: function(network) { + this.setState({ + filterByNetwork: network, + }); + }, + showRoomAlias: function(alias) { this.showRoom(null, alias); }, @@ -192,9 +219,13 @@ module.exports = React.createClass({ if (!this.state.publicRooms) return []; - var rooms = this.state.publicRooms.filter(function(a) { + var rooms = this.state.publicRooms.filter((a) => { // FIXME: if incrementally typing, keep narrowing down the search set // incrementally rather than starting over each time. + if (this.state.filterByNetwork) { + if (!this._isRoomInNetwork(a, this.state.filterByNetwork)) return false; + } + return (((a.name && a.name.toLowerCase().search(filter.toLowerCase()) >= 0) || (a.aliases && a.aliases[0].toLowerCase().search(filter.toLowerCase()) >= 0)) && a.num_joined_members > 0); @@ -266,6 +297,20 @@ module.exports = React.createClass({ } }, + /** + * Terrible temporary function that guess what network a public room + * entry is in, until synapse is able to tell us + */ + _isRoomInNetwork(room, network) { + if (room.aliases && this.networkPatterns[network]) { + for (const alias of room.aliases) { + if (this.networkPatterns[network].test(alias)) return true; + } + } + + return false; + }, + render: function() { if (this.state.loading) { var Loader = sdk.getComponent("elements.Spinner"); @@ -276,12 +321,16 @@ module.exports = React.createClass({ ); } - var SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); + const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); + const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown'); return (
- +
+ + +
diff --git a/src/components/views/directory/NetworkDropdown.js b/src/components/views/directory/NetworkDropdown.js new file mode 100644 index 000000000..e1de4ffea --- /dev/null +++ b/src/components/views/directory/NetworkDropdown.js @@ -0,0 +1,150 @@ +/* +Copyright 2016 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. +*/ + +import React from 'react'; + +export default class NetworkDropdown extends React.Component { + constructor() { + super(); + + this.dropdownRootElement = null; + this.ignoreEvent = null; + + this.onInputClick = this.onInputClick.bind(this); + this.onRootClick = this.onRootClick.bind(this); + this.onDocumentClick = this.onDocumentClick.bind(this); + this.onNetworkClick = this.onNetworkClick.bind(this); + this.collectRoot = this.collectRoot.bind(this); + + this.state = { + expanded: false, + selectedNetwork: null, + }; + } + + componentWillMount() { + // Listen for all clicks on the document so we can close the + // menu when the user clicks somewhere else + document.addEventListener('click', this.onDocumentClick, false); + } + + componentWillUnmount() { + document.removeEventListener('click', this.onDocumentClick, false); + } + + onDocumentClick(ev) { + // Close the dropdown if the user clicks anywhere that isn't + // within our root element + if (ev !== this.ignoreEvent) { + this.setState({ + expanded: false, + }); + } + } + + onRootClick(ev) { + // This captures any clicks that happen within our elements, + // such that we can then ignore them when they're seen by the + // click listener on the document handler, ie. not close the + // dropdown immediately after opening it. + // NB. We can't just stopPropagation() because then the event + // doesn't reach the React onClick(). + this.ignoreEvent = ev; + } + + onInputClick(ev) { + this.setState({ + expanded: !this.state.expanded, + }); + ev.preventDefault(); + } + + onNetworkClick(network, ev) { + this.setState({ + expanded: false, + selectedNetwork: network, + }); + this.props.onNetworkChange(network); + } + + collectRoot(e) { + if (this.dropdownRootElement) { + this.dropdownRootElement.removeEventListener('click', this.onRootClick, false); + } + if (e) { + e.addEventListener('click', this.onRootClick, false); + } + this.dropdownRootElement = e; + } + + _optionForNetwork(network, wire_onclick) { + if (wire_onclick === undefined) wire_onclick = true; + let icon; + let name; + let span_class; + + if (network === null) { + name = 'All networks'; + span_class = 'mx_NetworkDropdown_menu_all'; + } else { + name = this.props.config.networkNames[network]; + icon = ; + span_class = 'mx_NetworkDropdown_menu_network'; + } + + const click_handler = wire_onclick ? this.onNetworkClick.bind(this, network) : null; + + return
+ {icon} + {name} +
; + } + + render() { + const current_value = this._optionForNetwork(this.state.selectedNetwork, false); + + let menu; + if (this.state.expanded) { + const menu_options = [this._optionForNetwork(null)]; + for (const network of this.props.config.networks) { + menu_options.push(this._optionForNetwork(network)); + } + menu =
+ {menu_options} +
; + } + + return
+
+ {current_value} + + {menu} +
+
; + } +} + +NetworkDropdown.propTypes = { + onNetworkChange: React.PropTypes.func.isRequired, + config: React.PropTypes.object, +}; + +NetworkDropdown.defaultProps = { + config: { + networks: [], + } +}; + diff --git a/src/skins/vector/css/vector-web/structures/RoomDirectory.css b/src/skins/vector/css/vector-web/structures/RoomDirectory.css index 2f75724d5..b4a239459 100644 --- a/src/skins/vector/css/vector-web/structures/RoomDirectory.css +++ b/src/skins/vector/css/vector-web/structures/RoomDirectory.css @@ -46,15 +46,26 @@ limitations under the License. -webkit-flex-direction: column; } +.mx_RoomDirectory_listheader { + display: table; + width: 100%; + margin-top: 12px; + margin-bottom: 12px; + border-spacing: 5px; +} + .mx_RoomDirectory_input { - margin: auto; + display: table-cell; border-radius: 3px; border: 1px solid #c7c7c7; font-weight: 300; font-size: 13px; padding: 9px; - margin-top: 12px; - margin-bottom: 12px; +} + +.mx_RoomDirectory_listheader .mx_NetworkDropdown { + display: table-cell; + width: 100%; } .mx_RoomDirectory_tableWrapper { diff --git a/src/skins/vector/css/vector-web/views/directory/NetworkDropdown.css b/src/skins/vector/css/vector-web/views/directory/NetworkDropdown.css new file mode 100644 index 000000000..3bf4bb347 --- /dev/null +++ b/src/skins/vector/css/vector-web/views/directory/NetworkDropdown.css @@ -0,0 +1,77 @@ +/* +Copyright 2015, 2016 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_NetworkDropdown { + position: relative; +} + +.mx_NetworkDropdown_input { + position: relative; + border-radius: 3px; + border: 1px solid #c7c7c7; + font-weight: 300; + font-size: 13px; + margin-top: 12px; + margin-bottom: 12px; + user-select: none; +} + +.mx_NetworkDropdown_arrow { + border-color: #4a4a4a transparent transparent; + border-style: solid; + border-width: 5px 5px 0; + display: block; + height: 0; + position: absolute; + right: 10px; + top: 14px; + width: 0 +} + +.mx_NetworkDropdown_networkoption { + height: 35px; + line-height: 35px; + padding-left: 8px; + padding-right: 8px; +} + +.mx_NetworkDropdown_networkoption img { + margin: 5px; + width: 25px; + vertical-align: middle; +} + +.mx_NetworkDropdown_menu { + position: absolute; + left: -1px; + right: -1px; + top: 100%; + z-index: 2; + margin: 0; + padding: 0px; + border-radius: 3px; + border: 1px solid #76cfa6; + background-color: white; +} + +.mx_NetworkDropdown_menu .mx_NetworkDropdown_networkoption:hover { + background-color: #ddd; +} + +.mx_NetworkDropdown_menu_network { + font-weight: bold; +} + diff --git a/vector/config.sample.json b/vector/config.sample.json index 5b80ede38..e4fd34698 100644 --- a/vector/config.sample.json +++ b/vector/config.sample.json @@ -4,5 +4,35 @@ "brand": "Vector", "integrations_ui_url": "http://localhost:8081/", "integrations_rest_url": "http://localhost:5050", - "enableLabs": true + "enableLabs": true, + "roomDirectory": { + "networks": [ + "matrix:example_com", + "matrix:matrix_org", + "gitter", + "irc:freenode", + "irc:mozilla" + ], + "networkPatterns": { + "matrix:example_com": "#.*:example.com", + "matrix:matrix_org": "#.*:matrix.org", + "gitter": "#gitter_.*:matrix.org", + "irc:freenode": "#freenode_.*:matrix.org", + "irc:mozilla": "#mozilla_.*:matrix.org" + }, + "networkNames": { + "matrix:example_com": "example.com", + "matrix:matrix_org": "matrix.org", + "irc:freenode": "Freenode", + "irc:mozilla": "Mozilla", + "gitter": "Gitter" + }, + "networkIcons": { + "matrix:example_com": "//matrix.org/favicon.ico", + "matrix:matrix_org": "//matrix.org/favicon.ico", + "irc:freenode": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", + "irc:mozilla": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", + "gitter": "//gitter.im/favicon.ico" + } + } }