forked from matrix/element-web
Merge pull request #2362 from vector-im/dbkr/roomdir_filter_3pnetworks
Room dir: New filtering & 3rd party networks
This commit is contained in:
commit
62344b5194
|
@ -53,6 +53,7 @@ module.exports = React.createClass({
|
||||||
publicRooms: [],
|
publicRooms: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
filterByNetwork: null,
|
filterByNetwork: null,
|
||||||
|
roomServer: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -76,10 +77,6 @@ module.exports = React.createClass({
|
||||||
// });
|
// });
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this.refreshRoomList();
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
// dis.dispatch({
|
// dis.dispatch({
|
||||||
// action: 'ui_opacity',
|
// action: 'ui_opacity',
|
||||||
|
@ -101,12 +98,16 @@ module.exports = React.createClass({
|
||||||
if (!MatrixClientPeg.get()) return q();
|
if (!MatrixClientPeg.get()) return q();
|
||||||
|
|
||||||
const my_filter_string = this.filterString;
|
const my_filter_string = this.filterString;
|
||||||
|
const my_server = this.state.roomServer;
|
||||||
const opts = {limit: 20};
|
const opts = {limit: 20};
|
||||||
|
if (my_server != MatrixClientPeg.getHomeServerName()) {
|
||||||
|
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 (this.filterString) opts.filter = { generic_search_term: my_filter_string } ;
|
||||||
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
|
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
|
||||||
if (my_filter_string != this.filterString) {
|
if (my_filter_string != this.filterString || my_server != this.state.roomServer) {
|
||||||
// if the filter has changed since this request was sent,
|
// if the filter or server has changed since this request was sent,
|
||||||
// throw away the result (don't even clear the busy flag
|
// throw away the result (don't even clear the busy flag
|
||||||
// since we must still have a request in flight)
|
// since we must still have a request in flight)
|
||||||
return;
|
return;
|
||||||
|
@ -120,7 +121,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
return Boolean(data.next_batch);
|
return Boolean(data.next_batch);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
if (my_filter_string != this.filterString) {
|
if (my_filter_string != this.filterString || my_server != this.state.roomServer) {
|
||||||
// as above: we don't care about errors for old
|
// as above: we don't care about errors for old
|
||||||
// requests either
|
// requests either
|
||||||
return;
|
return;
|
||||||
|
@ -194,18 +195,23 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onNetworkChange: function(network) {
|
onOptionChange: function(server, network) {
|
||||||
|
// clear next batch so we don't try to load more rooms
|
||||||
|
this.nextBatch = null;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
// Clear the public rooms out here otherwise we needlessly
|
||||||
|
// spend time filtering lots of rooms when we're about to
|
||||||
|
// to clear the list anyway.
|
||||||
|
publicRooms: [],
|
||||||
|
roomServer: server,
|
||||||
filterByNetwork: network,
|
filterByNetwork: network,
|
||||||
}, () => {
|
}, this.refreshRoomList);
|
||||||
// we just filtered out a bunch of rooms, so check to see if
|
// We also refresh the room list each time even though this
|
||||||
// we need to fill up the scrollpanel again
|
// filtering is client-side. It hopefully won't be client side
|
||||||
// NB. Because we filter the results, the HS can keep giving
|
// for very long, and we may have fetched a thousand rooms to
|
||||||
// us more rooms and we'll keep requesting more if none match
|
// find the five gitter ones, at which point we do not want
|
||||||
// the filter, which is pretty terrible. We need a way
|
// to render all those rooms when switching back to 'all networks'.
|
||||||
// to filter by network on the server.
|
// Easiest to just blow away the state & re-fetch.
|
||||||
if (this.scrollPanel) this.scrollPanel.checkFillState();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onFillRequest: function(backwards) {
|
onFillRequest: function(backwards) {
|
||||||
|
@ -295,7 +301,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var rooms = this.state.publicRooms.filter((a) => {
|
var rooms = this.state.publicRooms.filter((a) => {
|
||||||
if (this.state.filterByNetwork) {
|
if (this.state.filterByNetwork) {
|
||||||
if (!this._isRoomInNetwork(a, this.state.filterByNetwork)) return false;
|
if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.filterByNetwork)) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -365,14 +371,28 @@ 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, network) {
|
_isRoomInNetwork(room, server, network) {
|
||||||
if (room.aliases && this.networkPatterns[network]) {
|
// We carve rooms into two categories here. 'portal' rooms are
|
||||||
for (const alias of room.aliases) {
|
// rooms created by a user joining a bridge 'portal' alias to
|
||||||
if (this.networkPatterns[network].test(alias)) return true;
|
// participate in that room or a foreign network. A room is a
|
||||||
|
// portal room if it has exactly one alias and that alias matches
|
||||||
|
// a pattern defined in the config. Its network is the key
|
||||||
|
// of the pattern that it matches.
|
||||||
|
// All other rooms are considered 'native matrix' rooms, and
|
||||||
|
// go into the special '_matrix' network.
|
||||||
|
|
||||||
|
let roomNetwork = '_matrix';
|
||||||
|
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];
|
||||||
|
if (pat && pat.test(room.aliases[0])) {
|
||||||
|
roomNetwork = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return roomNetwork == network;
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
@ -411,7 +431,7 @@ module.exports = React.createClass({
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
<NetworkDropdown config={this.props.config} onNetworkChange={this.onNetworkChange} />
|
<NetworkDropdown config={this.props.config} onOptionChange={this.onOptionChange} />
|
||||||
</div>
|
</div>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,10 +15,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
|
||||||
|
|
||||||
export default class NetworkDropdown extends React.Component {
|
export default class NetworkDropdown extends React.Component {
|
||||||
constructor() {
|
constructor(props) {
|
||||||
super();
|
super(props);
|
||||||
|
|
||||||
this.dropdownRootElement = null;
|
this.dropdownRootElement = null;
|
||||||
this.ignoreEvent = null;
|
this.ignoreEvent = null;
|
||||||
|
@ -26,12 +27,27 @@ export default class NetworkDropdown extends React.Component {
|
||||||
this.onInputClick = this.onInputClick.bind(this);
|
this.onInputClick = this.onInputClick.bind(this);
|
||||||
this.onRootClick = this.onRootClick.bind(this);
|
this.onRootClick = this.onRootClick.bind(this);
|
||||||
this.onDocumentClick = this.onDocumentClick.bind(this);
|
this.onDocumentClick = this.onDocumentClick.bind(this);
|
||||||
this.onNetworkClick = this.onNetworkClick.bind(this);
|
this.onMenuOptionClick = this.onMenuOptionClick.bind(this);
|
||||||
|
this.onInputKeyUp = this.onInputKeyUp.bind(this);
|
||||||
this.collectRoot = this.collectRoot.bind(this);
|
this.collectRoot = this.collectRoot.bind(this);
|
||||||
|
this.collectInputTextBox = this.collectInputTextBox.bind(this);
|
||||||
|
|
||||||
|
this.inputTextBox = null;
|
||||||
|
|
||||||
|
let defaultNetwork = null;
|
||||||
|
if (
|
||||||
|
this.props.config.serverConfig &&
|
||||||
|
this.props.config.serverConfig[server] &&
|
||||||
|
this.props.config.serverConfig[server].networks &&
|
||||||
|
'_matrix' in this.props.config.serverConfig[server].networks
|
||||||
|
) {
|
||||||
|
defaultNetwork = '_matrix';
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
selectedNetwork: null,
|
selectedServer: MatrixClientPeg.getHomeServerName(),
|
||||||
|
selectedNetwork: defaultNetwork,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,12 +55,21 @@ export default class NetworkDropdown extends React.Component {
|
||||||
// Listen for all clicks on the document so we can close the
|
// Listen for all clicks on the document so we can close the
|
||||||
// menu when the user clicks somewhere else
|
// menu when the user clicks somewhere else
|
||||||
document.addEventListener('click', this.onDocumentClick, false);
|
document.addEventListener('click', this.onDocumentClick, false);
|
||||||
|
|
||||||
|
// fire this now so the defaults can be set up
|
||||||
|
this.props.onOptionChange(this.state.selectedServer, this.state.selectedNetwork);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('click', this.onDocumentClick, false);
|
document.removeEventListener('click', this.onDocumentClick, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.state.expanded && this.inputTextBox) {
|
||||||
|
this.inputTextBox.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onDocumentClick(ev) {
|
onDocumentClick(ev) {
|
||||||
// Close the dropdown if the user clicks anywhere that isn't
|
// Close the dropdown if the user clicks anywhere that isn't
|
||||||
// within our root element
|
// within our root element
|
||||||
|
@ -72,12 +97,24 @@ export default class NetworkDropdown extends React.Component {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
onNetworkClick(network, ev) {
|
onMenuOptionClick(server, network, ev) {
|
||||||
this.setState({
|
this.setState({
|
||||||
expanded: false,
|
expanded: false,
|
||||||
|
selectedServer: server,
|
||||||
selectedNetwork: network,
|
selectedNetwork: network,
|
||||||
});
|
});
|
||||||
this.props.onNetworkChange(network);
|
this.props.onOptionChange(server, network);
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputKeyUp(e) {
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
this.setState({
|
||||||
|
expanded: false,
|
||||||
|
selectedServer: e.target.value,
|
||||||
|
selectedNetwork: null,
|
||||||
|
});
|
||||||
|
this.props.onOptionChange(e.target.value, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
collectRoot(e) {
|
collectRoot(e) {
|
||||||
|
@ -90,41 +127,82 @@ export default class NetworkDropdown extends React.Component {
|
||||||
this.dropdownRootElement = e;
|
this.dropdownRootElement = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
_optionForNetwork(network, wire_onclick) {
|
collectInputTextBox(e) {
|
||||||
|
this.inputTextBox = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getMenuOptions() {
|
||||||
|
const options = [];
|
||||||
|
|
||||||
|
let servers = [];
|
||||||
|
if (this.props.config.servers) {
|
||||||
|
servers = servers.concat(this.props.config.servers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (servers.indexOf(MatrixClientPeg.getHomeServerName()) == -1) {
|
||||||
|
servers.unshift(MatrixClientPeg.getHomeServerName());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const server of servers) {
|
||||||
|
options.push(this._makeMenuOption(server, null));
|
||||||
|
if (this.props.config.serverConfig && this.props.config.serverConfig[server] && this.props.config.serverConfig[server].networks) {
|
||||||
|
for (const network of this.props.config.serverConfig[server].networks) {
|
||||||
|
options.push(this._makeMenuOption(server, network));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeMenuOption(server, network, wire_onclick) {
|
||||||
if (wire_onclick === undefined) wire_onclick = true;
|
if (wire_onclick === undefined) wire_onclick = true;
|
||||||
let icon;
|
let icon;
|
||||||
let name;
|
let name;
|
||||||
let span_class;
|
let span_class;
|
||||||
|
|
||||||
if (network === null) {
|
if (network === null) {
|
||||||
name = 'All networks';
|
name = server;
|
||||||
span_class = 'mx_NetworkDropdown_menu_all';
|
span_class = 'mx_NetworkDropdown_menu_all';
|
||||||
|
} else if (network == '_matrix') {
|
||||||
|
name = 'Matrix';
|
||||||
|
icon = <img src="/img/network-matrix.svg" width="16" height="16" />;
|
||||||
|
span_class = 'mx_NetworkDropdown_menu_network';
|
||||||
} else {
|
} else {
|
||||||
name = this.props.config.networkNames[network];
|
name = this.props.config.networkNames[network];
|
||||||
icon = <img src={this.props.config.networkIcons[network]} />;
|
icon = <img src={this.props.config.networkIcons[network]} />;
|
||||||
span_class = 'mx_NetworkDropdown_menu_network';
|
span_class = 'mx_NetworkDropdown_menu_network';
|
||||||
}
|
}
|
||||||
|
|
||||||
const click_handler = wire_onclick ? this.onNetworkClick.bind(this, network) : null;
|
const click_handler = wire_onclick ? this.onMenuOptionClick.bind(this, server, network) : null;
|
||||||
|
|
||||||
return <div key={network} className="mx_NetworkDropdown_networkoption" onClick={click_handler}>
|
let key = server;
|
||||||
|
if (network !== null) {
|
||||||
|
key += '_' + network;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={click_handler}>
|
||||||
{icon}
|
{icon}
|
||||||
<span className={span_class}>{name}</span>
|
<span className={span_class}>{name}</span>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const current_value = this._optionForNetwork(this.state.selectedNetwork, false);
|
let current_value;
|
||||||
|
|
||||||
let menu;
|
let menu;
|
||||||
if (this.state.expanded) {
|
if (this.state.expanded) {
|
||||||
const menu_options = [this._optionForNetwork(null)];
|
const menu_options = this._getMenuOptions();
|
||||||
for (const network of this.props.config.networks) {
|
|
||||||
menu_options.push(this._optionForNetwork(network));
|
|
||||||
}
|
|
||||||
menu = <div className="mx_NetworkDropdown_menu">
|
menu = <div className="mx_NetworkDropdown_menu">
|
||||||
{menu_options}
|
{menu_options}
|
||||||
</div>;
|
</div>;
|
||||||
|
current_value = <input type="text" className="mx_NetworkDropdown_networkoption"
|
||||||
|
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
current_value = this._makeMenuOption(
|
||||||
|
this.state.selectedServer, this.state.selectedNetwork, false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_NetworkDropdown" ref={this.collectRoot}>
|
return <div className="mx_NetworkDropdown" ref={this.collectRoot}>
|
||||||
|
@ -138,7 +216,7 @@ export default class NetworkDropdown extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkDropdown.propTypes = {
|
NetworkDropdown.propTypes = {
|
||||||
onNetworkChange: React.PropTypes.func.isRequired,
|
onOptionChange: React.PropTypes.func.isRequired,
|
||||||
config: React.PropTypes.object,
|
config: React.PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,10 @@ limitations under the License.
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.mx_NetworkDropdown_networkoption, input.mx_NetworkDropdown_networkoption:focus {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_NetworkDropdown_menu {
|
.mx_NetworkDropdown_menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -1px;
|
left: -1px;
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 520 520" style="enable-background:new 0 0 520 520;" xml:space="preserve">
|
||||||
|
<path d="M13.7,11.9v496.2h35.7V520H0V0h49.4v11.9H13.7z"/>
|
||||||
|
<path d="M166.3,169.2v25.1h0.7c6.7-9.6,14.8-17,24.2-22.2c9.4-5.3,20.3-7.9,32.5-7.9c11.7,0,22.4,2.3,32.1,6.8
|
||||||
|
c9.7,4.5,17,12.6,22.1,24c5.5-8.1,13-15.3,22.4-21.5c9.4-6.2,20.6-9.3,33.5-9.3c9.8,0,18.9,1.2,27.3,3.6c8.4,2.4,15.5,6.2,21.5,11.5
|
||||||
|
c6,5.3,10.6,12.1,14,20.6c3.3,8.5,5,18.7,5,30.7v124.1h-50.9V249.6c0-6.2-0.2-12.1-0.7-17.6c-0.5-5.5-1.8-10.3-3.9-14.3
|
||||||
|
c-2.2-4.1-5.3-7.3-9.5-9.7c-4.2-2.4-9.9-3.6-17-3.6c-7.2,0-13,1.4-17.4,4.1c-4.4,2.8-7.9,6.3-10.4,10.8c-2.5,4.4-4.2,9.4-5,15.1
|
||||||
|
c-0.8,5.6-1.3,11.3-1.3,17v103.3h-50.9v-104c0-5.5-0.1-10.9-0.4-16.3c-0.2-5.4-1.3-10.3-3.1-14.9c-1.8-4.5-4.8-8.2-9-10.9
|
||||||
|
c-4.2-2.7-10.3-4.1-18.5-4.1c-2.4,0-5.6,0.5-9.5,1.6c-3.9,1.1-7.8,3.1-11.5,6.1c-3.7,3-6.9,7.3-9.5,12.9c-2.6,5.6-3.9,13-3.9,22.1
|
||||||
|
v107.6h-50.9V169.2H166.3z"/>
|
||||||
|
<path d="M506.3,508.1V11.9h-35.7V0H520v520h-49.4v-11.9H506.3z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -6,30 +6,30 @@
|
||||||
"integrations_rest_url": "http://localhost:5050",
|
"integrations_rest_url": "http://localhost:5050",
|
||||||
"enableLabs": true,
|
"enableLabs": true,
|
||||||
"roomDirectory": {
|
"roomDirectory": {
|
||||||
"networks": [
|
"servers": [
|
||||||
"matrix:example_com",
|
"matrix.org"
|
||||||
"matrix:matrix_org",
|
|
||||||
"gitter",
|
|
||||||
"irc:freenode",
|
|
||||||
"irc:mozilla"
|
|
||||||
],
|
],
|
||||||
|
"serverConfig": {
|
||||||
|
"matrix.org": {
|
||||||
|
"networks": [
|
||||||
|
"_matrix",
|
||||||
|
"gitter",
|
||||||
|
"irc:freenode",
|
||||||
|
"irc:mozilla"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"networkPatterns": {
|
"networkPatterns": {
|
||||||
"matrix:example_com": "#.*:example.com",
|
|
||||||
"matrix:matrix_org": "#.*:matrix.org",
|
|
||||||
"gitter": "#gitter_.*:matrix.org",
|
"gitter": "#gitter_.*:matrix.org",
|
||||||
"irc:freenode": "#freenode_.*:matrix.org",
|
"irc:freenode": "#freenode_.*:matrix.org",
|
||||||
"irc:mozilla": "#mozilla_.*:matrix.org"
|
"irc:mozilla": "#mozilla_.*:matrix.org"
|
||||||
},
|
},
|
||||||
"networkNames": {
|
"networkNames": {
|
||||||
"matrix:example_com": "example.com",
|
|
||||||
"matrix:matrix_org": "matrix.org",
|
|
||||||
"irc:freenode": "Freenode",
|
"irc:freenode": "Freenode",
|
||||||
"irc:mozilla": "Mozilla",
|
"irc:mozilla": "Mozilla",
|
||||||
"gitter": "Gitter"
|
"gitter": "Gitter"
|
||||||
},
|
},
|
||||||
"networkIcons": {
|
"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:freenode": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
|
||||||
"irc:mozilla": "//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"
|
"gitter": "//gitter.im/favicon.ico"
|
||||||
|
|
Loading…
Reference in New Issue