Merge pull request #2379 from vector-im/dbkr/join_3p_location
Add native joining of 3p networks to room dirpull/2422/head
commit
ea38968be9
27
README.md
27
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
|
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
|
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.
|
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
|
Running as a Desktop app
|
||||||
========================
|
========================
|
||||||
|
|
|
@ -52,23 +52,48 @@ module.exports = React.createClass({
|
||||||
return {
|
return {
|
||||||
publicRooms: [],
|
publicRooms: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
filterByNetwork: null,
|
network: null,
|
||||||
roomServer: null,
|
roomServer: null,
|
||||||
|
filterString: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
// precompile Regexps
|
// precompile Regexps
|
||||||
this.networkPatterns = {};
|
this.portalRoomPatterns = {};
|
||||||
if (this.props.config.networkPatterns) {
|
this.nativePatterns = {};
|
||||||
for (const network of Object.keys(this.props.config.networkPatterns)) {
|
if (this.props.config.networks) {
|
||||||
this.networkPatterns[network] = new RegExp(this.props.config.networkPatterns[network]);
|
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.nextBatch = null;
|
||||||
this.filterString = null;
|
|
||||||
this.filterTimeout = null;
|
this.filterTimeout = null;
|
||||||
this.scrollPanel = 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({
|
// dis.dispatch({
|
||||||
// action: 'ui_opacity',
|
// action: 'ui_opacity',
|
||||||
|
@ -97,7 +122,7 @@ module.exports = React.createClass({
|
||||||
getMoreRooms: function() {
|
getMoreRooms: function() {
|
||||||
if (!MatrixClientPeg.get()) return q();
|
if (!MatrixClientPeg.get()) return q();
|
||||||
|
|
||||||
const my_filter_string = this.filterString;
|
const my_filter_string = this.state.filterString;
|
||||||
const my_server = this.state.roomServer;
|
const my_server = this.state.roomServer;
|
||||||
// remember the next batch token when we sent the request
|
// remember the next batch token when we sent the request
|
||||||
// too. If it's changed, appending to the list will corrupt it.
|
// too. If it's changed, appending to the list will corrupt it.
|
||||||
|
@ -107,10 +132,10 @@ module.exports = React.createClass({
|
||||||
opts.server = my_server;
|
opts.server = my_server;
|
||||||
}
|
}
|
||||||
if (this.nextBatch) opts.since = this.nextBatch;
|
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) => {
|
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
|
||||||
if (
|
if (
|
||||||
my_filter_string != this.filterString ||
|
my_filter_string != this.state.filterString ||
|
||||||
my_server != this.state.roomServer ||
|
my_server != this.state.roomServer ||
|
||||||
my_next_batch != this.nextBatch)
|
my_next_batch != this.nextBatch)
|
||||||
{
|
{
|
||||||
|
@ -129,7 +154,7 @@ module.exports = React.createClass({
|
||||||
return Boolean(data.next_batch);
|
return Boolean(data.next_batch);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
if (
|
if (
|
||||||
my_filter_string != this.filterString ||
|
my_filter_string != this.state.filterString ||
|
||||||
my_server != this.state.roomServer ||
|
my_server != this.state.roomServer ||
|
||||||
my_next_batch != this.nextBatch)
|
my_next_batch != this.nextBatch)
|
||||||
{
|
{
|
||||||
|
@ -215,7 +240,7 @@ module.exports = React.createClass({
|
||||||
// to clear the list anyway.
|
// to clear the list anyway.
|
||||||
publicRooms: [],
|
publicRooms: [],
|
||||||
roomServer: server,
|
roomServer: server,
|
||||||
filterByNetwork: network,
|
network: network,
|
||||||
}, this.refreshRoomList);
|
}, this.refreshRoomList);
|
||||||
// We also refresh the room list each time even though this
|
// We also refresh the room list each time even though this
|
||||||
// filtering is client-side. It hopefully won't be client side
|
// filtering is client-side. It hopefully won't be client side
|
||||||
|
@ -232,7 +257,9 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onFilterChange: function(alias) {
|
onFilterChange: function(alias) {
|
||||||
this.filterString = alias || null;
|
this.setState({
|
||||||
|
filterString: alias || null,
|
||||||
|
});
|
||||||
|
|
||||||
// don't send the request for a little bit,
|
// don't send the request for a little bit,
|
||||||
// no point hammering the server with a
|
// no point hammering the server with a
|
||||||
|
@ -248,17 +275,52 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onFilterClear: function() {
|
onFilterClear: function() {
|
||||||
this.filterString = null;
|
// update immediately
|
||||||
|
this.setState({
|
||||||
|
filterString: null,
|
||||||
|
}, this.refreshRoomList);
|
||||||
|
|
||||||
if (this.filterTimeout) {
|
if (this.filterTimeout) {
|
||||||
clearTimeout(this.filterTimeout);
|
clearTimeout(this.filterTimeout);
|
||||||
}
|
}
|
||||||
// update immediately
|
|
||||||
this.refreshRoomList();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onJoinClick: function(alias) {
|
onJoinClick: function(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);
|
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) {
|
showRoomAlias: function(alias) {
|
||||||
|
@ -311,8 +373,8 @@ module.exports = React.createClass({
|
||||||
if (!this.state.publicRooms) return [];
|
if (!this.state.publicRooms) return [];
|
||||||
|
|
||||||
var rooms = this.state.publicRooms.filter((a) => {
|
var rooms = this.state.publicRooms.filter((a) => {
|
||||||
if (this.state.filterByNetwork) {
|
if (this.state.network) {
|
||||||
if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.filterByNetwork)) return false;
|
if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.network)) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -382,7 +444,7 @@ module.exports = React.createClass({
|
||||||
* Terrible temporary function that guess what network a public room
|
* Terrible temporary function that guess what network a public room
|
||||||
* entry is in, until synapse is able to tell us
|
* 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
|
// We carve rooms into two categories here. 'portal' rooms are
|
||||||
// rooms created by a user joining a bridge 'portal' alias to
|
// rooms created by a user joining a bridge 'portal' alias to
|
||||||
// participate in that room or a foreign network. A room is a
|
// 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 (room.aliases && room.aliases.length == 1) {
|
||||||
if (this.props.config.serverConfig && this.props.config.serverConfig[server] && this.props.config.serverConfig[server].networks) {
|
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) {
|
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])) {
|
if (pat && pat.test(room.aliases[0])) {
|
||||||
roomNetwork = n;
|
roomNetwork = n;
|
||||||
}
|
}
|
||||||
|
@ -406,6 +468,83 @@ module.exports = React.createClass({
|
||||||
return roomNetwork == network;
|
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() {
|
render: function() {
|
||||||
let content;
|
let content;
|
||||||
if (this.state.loading) {
|
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 SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||||
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
|
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
|
||||||
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
|
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
|
||||||
|
@ -446,6 +602,7 @@ module.exports = React.createClass({
|
||||||
<DirectorySearchBox
|
<DirectorySearchBox
|
||||||
className="mx_RoomDirectory_searchbox"
|
className="mx_RoomDirectory_searchbox"
|
||||||
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick}
|
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick}
|
||||||
|
placeholder={placeholder} showJoinButton={showJoinButton}
|
||||||
/>
|
/>
|
||||||
<NetworkDropdown config={this.props.config} onOptionChange={this.onOptionChange} />
|
<NetworkDropdown config={this.props.config} onOptionChange={this.onOptionChange} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default class NetworkDropdown extends React.Component {
|
||||||
this.props.config.serverConfig &&
|
this.props.config.serverConfig &&
|
||||||
this.props.config.serverConfig[server] &&
|
this.props.config.serverConfig[server] &&
|
||||||
this.props.config.serverConfig[server].networks &&
|
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';
|
defaultNetwork = '_matrix';
|
||||||
}
|
}
|
||||||
|
@ -170,8 +170,22 @@ export default class NetworkDropdown extends React.Component {
|
||||||
icon = <img src="img/network-matrix.svg" width="16" height="16" />;
|
icon = <img src="img/network-matrix.svg" width="16" height="16" />;
|
||||||
span_class = 'mx_NetworkDropdown_menu_network';
|
span_class = 'mx_NetworkDropdown_menu_network';
|
||||||
} else {
|
} else {
|
||||||
name = this.props.config.networkNames[network];
|
if (this.props.config.networks[network] === undefined) {
|
||||||
icon = <img src={this.props.config.networkIcons[network]} />;
|
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';
|
span_class = 'mx_NetworkDropdown_menu_network';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,6 +213,7 @@ export default class NetworkDropdown extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
current_value = <input type="text" className="mx_NetworkDropdown_networkoption"
|
current_value = <input type="text" className="mx_NetworkDropdown_networkoption"
|
||||||
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
|
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
|
||||||
|
placeholder="matrix.org" // 'matrix.org' as an example of an HS name
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
current_value = this._makeMenuOption(
|
current_value = this._makeMenuOption(
|
||||||
|
|
|
@ -15,24 +15,53 @@
|
||||||
"_matrix",
|
"_matrix",
|
||||||
"gitter",
|
"gitter",
|
||||||
"irc:freenode",
|
"irc:freenode",
|
||||||
"irc:mozilla"
|
"irc:mozilla",
|
||||||
|
"irc:snoonet",
|
||||||
|
"irc:oftc"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"networkPatterns": {
|
"networks": {
|
||||||
"gitter": "#gitter_.*:matrix.org",
|
"gitter": {
|
||||||
"irc:freenode": "#freenode_.*:matrix.org",
|
"protocol": "gitter",
|
||||||
"irc:mozilla": "#mozilla_.*:matrix.org"
|
"portalRoomPattern": "#gitter_.*:matrix.org",
|
||||||
|
"name": "Gitter",
|
||||||
|
"icon": "//gitter.im/favicon.ico",
|
||||||
|
"example": "org/community",
|
||||||
|
"nativePattern": "[^\\s]+/[^\\s]+$"
|
||||||
},
|
},
|
||||||
"networkNames": {
|
"irc:freenode": {
|
||||||
"irc:freenode": "Freenode",
|
"portalRoomPattern": "#freenode_.*:matrix.org",
|
||||||
"irc:mozilla": "Mozilla",
|
"name": "Freenode",
|
||||||
"gitter": "Gitter"
|
"icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
|
||||||
|
"example": "#channel",
|
||||||
|
"nativePattern": "^#[^\\s]+$"
|
||||||
},
|
},
|
||||||
"networkIcons": {
|
"irc:mozilla": {
|
||||||
"irc:freenode": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
|
"portalRoomPattern": "#mozilla_.*:matrix.org",
|
||||||
"irc:mozilla": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
|
"name": "Mozilla",
|
||||||
"gitter": "//gitter.im/favicon.ico"
|
"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]+$"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue