diff --git a/README.md b/README.md
index 394e7c35..6b819cce 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,32 @@ You can configure the app by copying `vector/config.sample.json` to
    addresses) to matrix IDs: see http://matrix.org/docs/spec/identity_service/unstable.html
    for more details.  Currently the only public matrix identity servers are https://matrix.org
    and https://vector.im.  In future identity servers will be decentralised.
-
+1. `roomDirectory`: config for the public room directory. This section encodes behaviour
+   on the room directory screen for filtering the list by server / network type and joining
+   third party networks. This config section will disappear once APIs are available to
+   get this information for home servers. This section is optional.
+1. `roomDirectory.servers`: List of other Home Servers' directories to include in the drop
+   down list. Optional.
+1. `roomDirectory.serverConfig`: Config for each server in `roomDirectory.servers`. Optional.
+1. `roomDirectory.serverConfig.<server_name>.networks`: List of networks (named
+   in `roomDirectory.networks`) to include for this server. Optional.
+1. `roomDirectory.networks`: config for each network type. Optional.
+1. `roomDirectory.<network_type>.name`: Human-readable name for the network. Required.
+1. `roomDirectory.<network_type>.protocol`: Protocol as given by the server in
+   `/_matrix/client/unstable/thirdparty/protocols` response. Required to be able to join
+   this type of third party network.
+1. `roomDirectory.<network_type>.domain`: Domain as given by the server in
+   `/_matrix/client/unstable/thirdparty/protocols` response, if present. Required to be
+   able to join this type of third party network, if present in `thirdparty/protocols`.
+1. `roomDirectory.<network_type>.portalRoomPattern`: Regular expression matching aliases
+   for portal rooms to locations on this network. Required.
+1. `roomDirectory.<network_type>.icon`: URL to an icon to be displayed for this network. Required.
+1. `roomDirectory.<network_type>.example`: Textual example of a location on this network,
+   eg. '#channel' for an IRC network. Optional.
+1. `roomDirectory.<network_type>.nativePattern`: Regular expression that matches a
+   valid location on this network. This is used as a hint to the user to indicate
+   when a valid location has been entered so it's not necessary for this to be
+   exactly correct. Optional.
 
 Running as a Desktop app
 ========================
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index a42e76b4..ca698e52 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -52,23 +52,48 @@ module.exports = React.createClass({
         return {
             publicRooms: [],
             loading: true,
-            filterByNetwork: null,
+            network: null,
             roomServer: null,
+            filterString: 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]);
+        this.portalRoomPatterns = {};
+        this.nativePatterns = {};
+        if (this.props.config.networks) {
+            for (const network of Object.keys(this.props.config.networks)) {
+                const network_info = this.props.config.networks[network];
+                if (network_info.portalRoomPattern) {
+                    this.portalRoomPatterns[network] = new RegExp(network_info.portalRoomPattern);
+                }
+                if (network_info.nativePattern) {
+                    this.nativePatterns[network] = new RegExp(network_info.nativePattern);
+                }
             }
         }
+
         this.nextBatch = null;
-        this.filterString = null;
         this.filterTimeout = null;
         this.scrollPanel = null;
+        this.protocols = null;
+
+        MatrixClientPeg.get().getThirdpartyProtocols().done((response) => {
+            this.protocols = response;
+        }, (err) => {
+            if (MatrixClientPeg.get().isGuest()) {
+                // Guests currently aren't allowed to use this API, so
+                // ignore this as otherwise this error is literally the
+                // thing you see when loading the client!
+                return;
+            }
+            const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+            Modal.createDialog(ErrorDialog, {
+                title: "Failed to get protocol list from Home Server",
+                description: "The Home Server may be too old to support third party networks",
+            });
+        });
 
         // dis.dispatch({
         //     action: 'ui_opacity',
@@ -97,7 +122,7 @@ module.exports = React.createClass({
     getMoreRooms: function() {
         if (!MatrixClientPeg.get()) return q();
 
-        const my_filter_string = this.filterString;
+        const my_filter_string = this.state.filterString;
         const my_server = this.state.roomServer;
         // remember the next batch token when we sent the request
         // too. If it's changed, appending to the list will corrupt it.
@@ -107,10 +132,10 @@ module.exports = React.createClass({
             opts.server = my_server;
         }
         if (this.nextBatch) opts.since = this.nextBatch;
-        if (this.filterString) opts.filter = { generic_search_term: my_filter_string } ;
+        if (my_filter_string) opts.filter = { generic_search_term: my_filter_string } ;
         return MatrixClientPeg.get().publicRooms(opts).then((data) => {
             if (
-                my_filter_string != this.filterString ||
+                my_filter_string != this.state.filterString ||
                 my_server != this.state.roomServer ||
                 my_next_batch != this.nextBatch)
             {
@@ -129,7 +154,7 @@ module.exports = React.createClass({
             return Boolean(data.next_batch);
         }, (err) => {
             if (
-                my_filter_string != this.filterString ||
+                my_filter_string != this.state.filterString ||
                 my_server != this.state.roomServer ||
                 my_next_batch != this.nextBatch)
             {
@@ -215,7 +240,7 @@ module.exports = React.createClass({
             // to clear the list anyway.
             publicRooms: [],
             roomServer: server,
-            filterByNetwork: network,
+            network: network,
         }, this.refreshRoomList);
         // We also refresh the room list each time even though this
         // filtering is client-side. It hopefully won't be client side
@@ -232,7 +257,9 @@ module.exports = React.createClass({
     },
 
     onFilterChange: function(alias) {
-        this.filterString = alias || null;
+        this.setState({
+            filterString: alias || null,
+        });
 
         // don't send the request for a little bit,
         // no point hammering the server with a
@@ -248,17 +275,52 @@ module.exports = React.createClass({
     },
 
     onFilterClear: function() {
-        this.filterString = null;
+        // update immediately
+        this.setState({
+            filterString: null,
+        }, this.refreshRoomList);
 
         if (this.filterTimeout) {
             clearTimeout(this.filterTimeout);
         }
-        // update immediately
-        this.refreshRoomList();
     },
 
     onJoinClick: function(alias) {
-        this.showRoomAlias(alias);
+        // If we're on the 'Matrix' network (or all networks),
+        // just show that rooms alias
+        if (this.state.network == null || this.state.network == '_matrix') {
+            this.showRoomAlias(alias);
+        } else {
+            // This is a 3rd party protocol. Let's see if we
+            // can join it
+            const fields = this._getFieldsForThirdPartyLocation(alias, this.state.network);
+            if (!fields) {
+                const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+                Modal.createDialog(ErrorDialog, {
+                    title: "Unable to join network",
+                    description: "Riot does not know how to join a room on this network",
+                });
+                return;
+            }
+            const protocol = this._protocolForThirdPartyNetwork(this.state.network);
+            MatrixClientPeg.get().getThirdpartyLocation(protocol, fields).done((resp) => {
+                if (resp.length > 0 && resp[0].alias) {
+                    this.showRoomAlias(resp[0].alias);
+                } else {
+                    const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+                    Modal.createDialog(ErrorDialog, {
+                        title: "Room not found",
+                        description: "Couldn't find a matching Matrix room",
+                    });
+                }
+            }, (e) => {
+                const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+                Modal.createDialog(ErrorDialog, {
+                    title: "Fetching third party location failed",
+                    description: "Unable to look up room ID from server",
+                });
+            });
+        }
     },
 
     showRoomAlias: function(alias) {
@@ -311,8 +373,8 @@ module.exports = React.createClass({
         if (!this.state.publicRooms) return [];
 
         var rooms = this.state.publicRooms.filter((a) => {
-            if (this.state.filterByNetwork) {
-                if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.filterByNetwork)) return false;
+            if (this.state.network) {
+                if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.network)) return false;
             }
 
             return true;
@@ -382,7 +444,7 @@ 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, server, network) {
+    _isRoomInNetwork: function(room, server, network) {
         // We carve rooms into two categories here. 'portal' rooms are
         // rooms created by a user joining a bridge 'portal' alias to
         // participate in that room or a foreign network. A room is a
@@ -396,7 +458,7 @@ module.exports = React.createClass({
         if (room.aliases && room.aliases.length == 1) {
             if (this.props.config.serverConfig && this.props.config.serverConfig[server] && this.props.config.serverConfig[server].networks) {
                 for (const n of this.props.config.serverConfig[server].networks) {
-                    const pat = this.networkPatterns[n];
+                    const pat = this.portalRoomPatterns[n];
                     if (pat && pat.test(room.aliases[0])) {
                         roomNetwork = n;
                     }
@@ -406,6 +468,83 @@ module.exports = React.createClass({
         return roomNetwork == network;
     },
 
+    _stringLooksLikeId: function(s, network) {
+        let pat = /^#[^\s]+:[^\s]/;
+        if (
+            network && network != '_matrix' &&
+            this.nativePatterns[network]
+        ) {
+            pat = this.nativePatterns[network];
+        }
+
+        return pat.test(s);
+    },
+
+    _protocolForThirdPartyNetwork: function(network) {
+        if (
+            this.props.config.networks &&
+            this.props.config.networks[network] &&
+            this.props.config.networks[network].protocol
+        ) {
+            return this.props.config.networks[network].protocol;
+        }
+    },
+
+    _getFieldsForThirdPartyLocation: function(user_input, network) {
+        if (!this.props.config.networks || !this.props.config.networks[network]) return null;
+
+        const network_info = this.props.config.networks[network];
+        if (!network_info.protocol) return null;
+
+        if (!this.protocols) return null;
+
+        let matched_instance;
+        // Try to find which instance in the 'protocols' response
+        // matches this network. We look for a matching protocol
+        // and the existence of a 'domain' field and if present,
+        // its value.
+        if (this.protocols[network_info.protocol].instances.length == 1) {
+            const the_instance = this.protocols[network_info.protocol].instances[0];
+            // If there's only one instance in this protocol, use it
+            // as long as it has no domain (which we assume to mean it's
+            // there is only one possible instance).
+            if (
+                (
+                    the_instance.fields.domain === undefined &&
+                    network_info.domain === undefined
+                ) ||
+                (
+                    the_instance.fields.domain !== undefined &&
+                    the_instance.fields.domain == network_info.domain
+                )
+            ) {
+                matched_instance = the_instance;
+            }
+        } else if (network_info.domain) {
+            // otherwise, we look for one with a matching domain.
+            for (const this_instance of this.protocols[network_info.protocol].instances) {
+                if (this_instance.fields.domain == network_info.domain) {
+                    matched_instance = this_instance;
+                }
+            }
+        }
+
+        if (matched_instance === undefined) return null;
+
+        // now make an object with the fields specified by that protocol. We
+        // require that the values of all but the last field come from the
+        // instance. The last is the user input.
+        const required_fields = this.protocols[network_info.protocol].location_fields;
+        const fields = {};
+        for (let i = 0; i < required_fields.length - 1; ++i) {
+            const this_field = required_fields[i];
+            if (matched_instance.fields[this_field] === undefined) return null;
+            fields[this_field] = matched_instance.fields[this_field];
+        }
+        fields[required_fields[required_fields.length - 1]] = user_input;
+        return fields;
+    },
+
     render: function() {
         let content;
         if (this.state.loading) {
@@ -435,6 +574,23 @@ module.exports = React.createClass({
             }
         }
 
+        let placeholder = 'Search for a room';
+        if (this.state.network === null || this.state.network === '_matrix') {
+            placeholder = '#example:' + this.state.roomServer;
+        } else if (
+            this.props.config.networks &&
+            this.props.config.networks[this.state.network] &&
+            this.props.config.networks[this.state.network].example &&
+            this._getFieldsForThirdPartyLocation(this.state.filterString, this.state.network)
+        ) {
+            placeholder = this.props.config.networks[this.state.network].example;
+        }
+
+        const showJoinButton = (
+            this._stringLooksLikeId(this.state.filterString, this.state.network) &&
+            this._getFieldsForThirdPartyLocation(this.state.filterString, this.state.network)
+        );
+
         const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
         const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
         const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
@@ -446,6 +602,7 @@ module.exports = React.createClass({
                         <DirectorySearchBox
                             className="mx_RoomDirectory_searchbox"
                             onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick}
+                            placeholder={placeholder} showJoinButton={showJoinButton}
                         />
                         <NetworkDropdown config={this.props.config} onOptionChange={this.onOptionChange} />
                     </div>
diff --git a/src/components/views/directory/NetworkDropdown.js b/src/components/views/directory/NetworkDropdown.js
index e6596319..eb60c4a5 100644
--- a/src/components/views/directory/NetworkDropdown.js
+++ b/src/components/views/directory/NetworkDropdown.js
@@ -40,7 +40,7 @@ export default class NetworkDropdown extends React.Component {
             this.props.config.serverConfig &&
             this.props.config.serverConfig[server] &&
             this.props.config.serverConfig[server].networks &&
-            '_matrix' in this.props.config.serverConfig[server].networks
+            this.props.config.serverConfig[server].networks.indexOf('_matrix') > -1
         ) {
             defaultNetwork = '_matrix';
         }
@@ -170,8 +170,22 @@ export default class NetworkDropdown extends React.Component {
             icon = <img src="img/network-matrix.svg" width="16" height="16" />;
             span_class = 'mx_NetworkDropdown_menu_network';
         } else {
-            name = this.props.config.networkNames[network];
-            icon = <img src={this.props.config.networkIcons[network]} />;
+            if (this.props.config.networks[network] === undefined) {
+                throw new Error(network + ' network missing from config');
+            }
+            if (this.props.config.networks[network].name) {
+                name = this.props.config.networks[network].name;
+            } else {
+                name = network;
+            }
+            if (this.props.config.networks[network].icon) {
+                // omit height here so if people define a non-square logo in the config, it
+                // will keep the aspect when it scales
+                icon = <img src={this.props.config.networks[network].icon} width="16" />;
+            } else {
+                icon = <img src={iconPath} width="16" height="16" />;
+            }
+
             span_class = 'mx_NetworkDropdown_menu_network';
         }
 
@@ -199,6 +213,7 @@ export default class NetworkDropdown extends React.Component {
             </div>;
             current_value = <input type="text" className="mx_NetworkDropdown_networkoption"
                 ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
+                placeholder="matrix.org" // 'matrix.org' as an example of an HS name
             />
         } else {
             current_value = this._makeMenuOption(
diff --git a/vector/config.sample.json b/vector/config.sample.json
index 37043d0f..0e3392a2 100644
--- a/vector/config.sample.json
+++ b/vector/config.sample.json
@@ -15,24 +15,53 @@
                     "_matrix",
                     "gitter",
                     "irc:freenode",
-                    "irc:mozilla"
+                    "irc:mozilla",
+                    "irc:snoonet",
+                    "irc:oftc"
                 ]
             }
         },
-        "networkPatterns": {
-            "gitter": "#gitter_.*:matrix.org",
-            "irc:freenode": "#freenode_.*:matrix.org",
-            "irc:mozilla": "#mozilla_.*:matrix.org"
-        },
-        "networkNames": {
-            "irc:freenode": "Freenode",
-            "irc:mozilla": "Mozilla",
-            "gitter": "Gitter"
-        },
-        "networkIcons": {
-            "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"
+        "networks": {
+            "gitter": {
+                "protocol": "gitter",
+                "portalRoomPattern": "#gitter_.*:matrix.org",
+                "name": "Gitter",
+                "icon": "//gitter.im/favicon.ico",
+                "example": "org/community",
+                "nativePattern": "[^\\s]+/[^\\s]+$"
+            },
+            "irc:freenode": {
+                "portalRoomPattern": "#freenode_.*:matrix.org",
+                "name": "Freenode",
+                "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
+                "example": "#channel",
+                "nativePattern": "^#[^\\s]+$"
+            },
+            "irc:mozilla": {
+                "portalRoomPattern": "#mozilla_.*:matrix.org",
+                "name": "Mozilla",
+                "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
+                "example": "#channel",
+                "nativePattern": "^#[^\\s]+$"
+            },
+            "irc:snoonet": {
+                "protocol": "irc",
+                "domain": "ipv6-irc.snoonet.org",
+                "portalRoomPattern": "#_snoonet_.*:matrix.org",
+                "name": "Snoonet",
+                "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
+                "example": "#channel",
+                "nativePattern": "^#[^\\s]+$"
+            },
+            "irc:oftc": {
+                "protocol": "irc",
+                "domain": "irc.oftc.net",
+                "portalRoomPattern": "#_oftc_.*:matrix.org",
+                "name": "OFTC",
+                "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
+                "example": "#channel",
+                "nativePattern": "^#[^\\s]+$"
+            }
         }
     }
 }