Initial attempt to redesign explore servers in room directory
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
5c582037ce
commit
86e53ea2c3
9 changed files with 362 additions and 278 deletions
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -45,7 +46,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomDirectory_listheader {
|
.mx_RoomDirectory_listheader {
|
||||||
display: flex;
|
display: block;
|
||||||
margin-top: 13px;
|
margin-top: 13px;
|
||||||
margin-bottom: 13px;
|
margin-bottom: 13px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,70 +16,143 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_NetworkDropdown {
|
.mx_NetworkDropdown {
|
||||||
|
height: 32px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
width: max-content;
|
||||||
|
padding-right: 32px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 9px;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
.mx_NetworkDropdown_input {
|
.mx_AccessibleButton {
|
||||||
position: relative;
|
width: max-content;
|
||||||
border-radius: 3px;
|
}
|
||||||
border: 1px solid $strong-input-border-color;
|
|
||||||
font-weight: 300;
|
|
||||||
font-size: 13px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_NetworkDropdown_arrow {
|
|
||||||
border-color: $primary-fg-color transparent transparent;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 5px 5px 0;
|
|
||||||
display: block;
|
|
||||||
height: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
top: 16px;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_NetworkDropdown_networkoption {
|
|
||||||
height: 37px;
|
|
||||||
line-height: 37px;
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_NetworkDropdown_networkoption img {
|
|
||||||
margin: 5px;
|
|
||||||
width: 25px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.mx_NetworkDropdown_networkoption, input.mx_NetworkDropdown_networkoption:focus {
|
|
||||||
border: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NetworkDropdown_menu {
|
.mx_NetworkDropdown_menu {
|
||||||
position: absolute;
|
//position: absolute;
|
||||||
left: -1px;
|
//left: -1px;
|
||||||
right: -1px;
|
//right: -1px;
|
||||||
top: 100%;
|
//top: 100%;
|
||||||
z-index: 2;
|
//z-index: 2;
|
||||||
|
width: 204px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0px;
|
box-sizing: border-box;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
border: 1px solid $accent-color;
|
border: 1px solid $accent-color;
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NetworkDropdown_menu .mx_NetworkDropdown_networkoption:hover {
|
|
||||||
background-color: $focus-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_NetworkDropdown_menu_network {
|
.mx_NetworkDropdown_menu_network {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_NetworkDropdown_server {
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid $input-darker-fg-color;
|
||||||
|
|
||||||
|
.mx_NetworkDropdown_server_title {
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
// remove server button
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
position: absolute;
|
||||||
|
display: inline;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
right: 12px;
|
||||||
|
top: 4px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/x.svg');
|
||||||
|
background-color: $notice-primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NetworkDropdown_server_subtitle {
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 14px;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: $muted-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NetworkDropdown_server_network {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&[aria-checked=true]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
right: 10px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/check.svg');
|
||||||
|
background-color: $input-valid-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NetworkDropdown_server_add,
|
||||||
|
.mx_NetworkDropdown_server_network {
|
||||||
|
&:hover {
|
||||||
|
background-color: $header-panel-bg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NetworkDropdown_server_add {
|
||||||
|
padding: 16px 10px 16px 32px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
left: 7px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/plus.svg');
|
||||||
|
background-color: $muted-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NetworkDropdown_handle {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
right: -28px; // - (24 + 4)
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-left: 9px;
|
padding-left: 9px;
|
||||||
padding-right: 9px;
|
padding-right: 9px;
|
||||||
margin: 0 5px 0 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DirectorySearchBox_joinButton {
|
.mx_DirectorySearchBox_joinButton {
|
||||||
|
|
|
@ -600,9 +600,8 @@ export default createReactClass({
|
||||||
break;
|
break;
|
||||||
case 'view_room_directory': {
|
case 'view_room_directory': {
|
||||||
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
|
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
|
||||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {},
|
||||||
config: this.props.config,
|
'mx_RoomDirectory_dialogWrapper', false, true);
|
||||||
}, 'mx_RoomDirectory_dialogWrapper');
|
|
||||||
|
|
||||||
// View the welcome or home page if we need something to look at
|
// View the welcome or home page if we need something to look at
|
||||||
this._viewSomethingBehindModal();
|
this._viewSomethingBehindModal();
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { _t } from '../../languageHandler';
|
||||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||||
import Analytics from '../../Analytics';
|
import Analytics from '../../Analytics';
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||||
|
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
|
||||||
|
|
||||||
const MAX_NAME_LENGTH = 80;
|
const MAX_NAME_LENGTH = 80;
|
||||||
const MAX_TOPIC_LENGTH = 160;
|
const MAX_TOPIC_LENGTH = 160;
|
||||||
|
@ -40,25 +41,17 @@ export default createReactClass({
|
||||||
displayName: 'RoomDirectory',
|
displayName: 'RoomDirectory',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
config: PropTypes.object,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
config: {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
publicRooms: [],
|
publicRooms: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
protocolsLoading: true,
|
protocolsLoading: true,
|
||||||
error: null,
|
error: null,
|
||||||
instanceId: null,
|
instanceId: undefined,
|
||||||
includeAll: false,
|
roomServer: MatrixClientPeg.getHomeserverName(),
|
||||||
roomServer: null,
|
|
||||||
filterString: null,
|
filterString: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -98,6 +91,10 @@ export default createReactClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.refreshRoomList();
|
||||||
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
if (this.filterTimeout) {
|
if (this.filterTimeout) {
|
||||||
clearTimeout(this.filterTimeout);
|
clearTimeout(this.filterTimeout);
|
||||||
|
@ -130,10 +127,10 @@ export default createReactClass({
|
||||||
if (my_server != MatrixClientPeg.getHomeserverName()) {
|
if (my_server != MatrixClientPeg.getHomeserverName()) {
|
||||||
opts.server = my_server;
|
opts.server = my_server;
|
||||||
}
|
}
|
||||||
if (this.state.instanceId) {
|
if (this.state.instanceId === ALL_ROOMS) {
|
||||||
opts.third_party_instance_id = this.state.instanceId;
|
|
||||||
} else if (this.state.includeAll) {
|
|
||||||
opts.include_all_networks = true;
|
opts.include_all_networks = true;
|
||||||
|
} else if (this.state.instanceId) {
|
||||||
|
opts.third_party_instance_id = this.state.instanceId;
|
||||||
}
|
}
|
||||||
if (this.nextBatch) opts.since = this.nextBatch;
|
if (this.nextBatch) opts.since = this.nextBatch;
|
||||||
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
|
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
|
||||||
|
@ -247,7 +244,7 @@ export default createReactClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onOptionChange: function(server, instanceId, includeAll) {
|
onOptionChange: function(server, instanceId) {
|
||||||
// clear next batch so we don't try to load more rooms
|
// clear next batch so we don't try to load more rooms
|
||||||
this.nextBatch = null;
|
this.nextBatch = null;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -257,7 +254,6 @@ export default createReactClass({
|
||||||
publicRooms: [],
|
publicRooms: [],
|
||||||
roomServer: server,
|
roomServer: server,
|
||||||
instanceId: instanceId,
|
instanceId: instanceId,
|
||||||
includeAll: includeAll,
|
|
||||||
error: null,
|
error: null,
|
||||||
}, 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
|
||||||
|
@ -305,7 +301,7 @@ export default createReactClass({
|
||||||
|
|
||||||
onJoinFromSearchClick: function(alias) {
|
onJoinFromSearchClick: function(alias) {
|
||||||
// If we don't have a particular instance id selected, just show that rooms alias
|
// If we don't have a particular instance id selected, just show that rooms alias
|
||||||
if (!this.state.instanceId) {
|
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||||
// If the user specified an alias without a domain, add on whichever server is selected
|
// If the user specified an alias without a domain, add on whichever server is selected
|
||||||
// in the dropdown
|
// in the dropdown
|
||||||
if (alias.indexOf(':') == -1) {
|
if (alias.indexOf(':') == -1) {
|
||||||
|
@ -587,7 +583,7 @@ export default createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
let placeholder = _t('Find a room…');
|
let placeholder = _t('Find a room…');
|
||||||
if (!this.state.instanceId) {
|
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||||
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
|
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
|
||||||
} else if (instance_expected_field_type) {
|
} else if (instance_expected_field_type) {
|
||||||
placeholder = instance_expected_field_type.placeholder;
|
placeholder = instance_expected_field_type.placeholder;
|
||||||
|
@ -604,10 +600,18 @@ export default createReactClass({
|
||||||
listHeader = <div className="mx_RoomDirectory_listheader">
|
listHeader = <div className="mx_RoomDirectory_listheader">
|
||||||
<DirectorySearchBox
|
<DirectorySearchBox
|
||||||
className="mx_RoomDirectory_searchbox"
|
className="mx_RoomDirectory_searchbox"
|
||||||
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinFromSearchClick}
|
onChange={this.onFilterChange}
|
||||||
placeholder={placeholder} showJoinButton={showJoinButton}
|
onClear={this.onFilterClear}
|
||||||
|
onJoinClick={this.onJoinFromSearchClick}
|
||||||
|
placeholder={placeholder}
|
||||||
|
showJoinButton={showJoinButton}
|
||||||
|
/>
|
||||||
|
<NetworkDropdown
|
||||||
|
protocols={this.protocols}
|
||||||
|
onOptionChange={this.onOptionChange}
|
||||||
|
selectedServerName={this.state.roomServer}
|
||||||
|
selectedInstanceId={this.state.instanceId}
|
||||||
/>
|
/>
|
||||||
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
const explanation =
|
const explanation =
|
||||||
|
|
|
@ -28,9 +28,11 @@ export default createReactClass({
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
]),
|
]),
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
button: PropTypes.string,
|
button: PropTypes.string,
|
||||||
focus: PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
hasCancel: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -39,6 +41,7 @@ export default createReactClass({
|
||||||
value: "",
|
value: "",
|
||||||
description: "",
|
description: "",
|
||||||
focus: true,
|
focus: true,
|
||||||
|
hasCancel: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -80,13 +83,17 @@ export default createReactClass({
|
||||||
className="mx_TextInputDialog_input"
|
className="mx_TextInputDialog_input"
|
||||||
defaultValue={this.props.value}
|
defaultValue={this.props.value}
|
||||||
autoFocus={this.props.focus}
|
autoFocus={this.props.focus}
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
size="64" />
|
size="64" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<DialogButtons primaryButton={this.props.button}
|
<DialogButtons
|
||||||
|
primaryButton={this.props.button}
|
||||||
onPrimaryButtonClick={this.onOk}
|
onPrimaryButtonClick={this.onOk}
|
||||||
onCancel={this.onCancel} />
|
onCancel={this.onCancel}
|
||||||
|
hasCancel={this.props.hasCancel}
|
||||||
|
/>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,239 +18,225 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
|
import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
|
||||||
|
import {ContextMenu, useContextMenu, ContextMenuButton, MenuItemRadio, MenuItem} from "../../structures/ContextMenu";
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
import {useSettingValue} from "../../../hooks/useSettings";
|
||||||
|
import * as sdk from "../../../index";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
|
||||||
const DEFAULT_ICON_URL = require("../../../../res/img/network-matrix.svg");
|
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||||
|
|
||||||
export default class NetworkDropdown extends React.Component {
|
const SETTING_NAME = "room_directory_servers";
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.dropdownRootElement = null;
|
const inPlaceOf = (elementRect) => ({
|
||||||
this.ignoreEvent = null;
|
right: window.innerWidth - elementRect.right,
|
||||||
|
top: elementRect.top,
|
||||||
|
chevronOffset: 0,
|
||||||
|
chevronFace: "none",
|
||||||
|
});
|
||||||
|
|
||||||
this.onInputClick = this.onInputClick.bind(this);
|
// This dropdown sources homeservers from three places:
|
||||||
this.onRootClick = this.onRootClick.bind(this);
|
// + your currently connected homeserver
|
||||||
this.onDocumentClick = this.onDocumentClick.bind(this);
|
// + homeservers in config.json["roomDirectory"]
|
||||||
this.onMenuOptionClick = this.onMenuOptionClick.bind(this);
|
// + homeservers in SettingsStore["room_directory_servers"]
|
||||||
this.onInputKeyUp = this.onInputKeyUp.bind(this);
|
// if a server exists in multiple, only keep the top-most entry.
|
||||||
this.collectRoot = this.collectRoot.bind(this);
|
|
||||||
this.collectInputTextBox = this.collectInputTextBox.bind(this);
|
|
||||||
|
|
||||||
this.inputTextBox = null;
|
const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, selectedInstanceId}) => {
|
||||||
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||||
|
const userDefinedServers = useSettingValue(SETTING_NAME);
|
||||||
|
|
||||||
const server = MatrixClientPeg.getHomeserverName();
|
const handlerFactory = (server, instanceId) => {
|
||||||
this.state = {
|
return () => {
|
||||||
expanded: false,
|
onOptionChange(server, instanceId);
|
||||||
selectedServer: server,
|
closeMenu();
|
||||||
selectedInstanceId: null,
|
|
||||||
includeAllNetworks: false,
|
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
componentWillMount() {
|
// we either show the button or the dropdown in its place.
|
||||||
// Listen for all clicks on the document so we can close the
|
let content;
|
||||||
// menu when the user clicks somewhere else
|
if (menuDisplayed) {
|
||||||
document.addEventListener('click', this.onDocumentClick, false);
|
const config = SdkConfig.get();
|
||||||
|
const roomDirectory = config.roomDirectory || {};
|
||||||
|
|
||||||
// fire this now so the defaults can be set up
|
const hsName = MatrixClientPeg.getHomeserverName();
|
||||||
const {selectedServer, selectedInstanceId, includeAllNetworks} = this.state;
|
const configServers = new Set(roomDirectory.servers);
|
||||||
this.props.onOptionChange(selectedServer, selectedInstanceId, includeAllNetworks);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
|
||||||
document.removeEventListener('click', this.onDocumentClick, false);
|
const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
|
||||||
}
|
const servers = [
|
||||||
|
// we always show our connected HS, this takes precedence over it being configured or user-defined
|
||||||
componentDidUpdate() {
|
hsName,
|
||||||
if (this.state.expanded && this.inputTextBox) {
|
...Array.from(configServers).filter(s => s !== hsName).sort(),
|
||||||
this.inputTextBox.focus();
|
...Array.from(removableServers).sort(),
|
||||||
}
|
];
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMenuOptionClick(server, instance, includeAll) {
|
|
||||||
this.setState({
|
|
||||||
expanded: false,
|
|
||||||
selectedServer: server,
|
|
||||||
selectedInstanceId: instance ? instance.instance_id : null,
|
|
||||||
includeAllNetworks: includeAll,
|
|
||||||
});
|
|
||||||
this.props.onOptionChange(server, instance ? instance.instance_id : null, includeAll);
|
|
||||||
}
|
|
||||||
|
|
||||||
onInputKeyUp(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
this.setState({
|
|
||||||
expanded: false,
|
|
||||||
selectedServer: e.target.value,
|
|
||||||
selectedNetwork: null,
|
|
||||||
includeAllNetworks: false,
|
|
||||||
});
|
|
||||||
this.props.onOptionChange(e.target.value, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
collectRoot(e) {
|
|
||||||
if (this.dropdownRootElement) {
|
|
||||||
this.dropdownRootElement.removeEventListener('click', this.onRootClick, false);
|
|
||||||
}
|
|
||||||
if (e) {
|
|
||||||
e.addEventListener('click', this.onRootClick, false);
|
|
||||||
}
|
|
||||||
this.dropdownRootElement = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
collectInputTextBox(e) {
|
|
||||||
this.inputTextBox = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getMenuOptions() {
|
|
||||||
const options = [];
|
|
||||||
const roomDirectory = this.props.config.roomDirectory || {};
|
|
||||||
|
|
||||||
let servers = [];
|
|
||||||
if (roomDirectory.servers) {
|
|
||||||
servers = servers.concat(roomDirectory.servers);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!servers.includes(MatrixClientPeg.getHomeserverName())) {
|
|
||||||
servers.unshift(MatrixClientPeg.getHomeserverName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// For our own HS, we can use the instance_ids given in the third party protocols
|
// For our own HS, we can use the instance_ids given in the third party protocols
|
||||||
// response to get the server to filter the room list by network for us.
|
// response to get the server to filter the room list by network for us.
|
||||||
// We can't get thirdparty protocols for remote server yet though, so for those
|
// We can't get thirdparty protocols for remote server yet though, so for those
|
||||||
// we can only show the default room list.
|
// we can only show the default room list.
|
||||||
for (const server of servers) {
|
const options = servers.map(server => {
|
||||||
options.push(this._makeMenuOption(server, null, true));
|
const serverSelected = server === selectedServerName;
|
||||||
if (server === MatrixClientPeg.getHomeserverName()) {
|
const entries = [];
|
||||||
options.push(this._makeMenuOption(server, null, false));
|
|
||||||
if (this.props.protocols) {
|
|
||||||
for (const proto of Object.keys(this.props.protocols)) {
|
|
||||||
if (!this.props.protocols[proto].instances) continue;
|
|
||||||
|
|
||||||
const sortedInstances = this.props.protocols[proto].instances;
|
const protocolsList = server === hsName ? Object.values(protocols) : [];
|
||||||
sortedInstances.sort(function(x, y) {
|
if (protocolsList.length > 0) {
|
||||||
const a = x.desc;
|
// add a fake protocol with the ALL_ROOMS symbol
|
||||||
const b = y.desc;
|
protocolsList.push({
|
||||||
if (a < b) {
|
instances: [{
|
||||||
return -1;
|
instance_id: ALL_ROOMS,
|
||||||
} else if (a > b) {
|
desc: _t("All rooms"),
|
||||||
return 1;
|
}],
|
||||||
} else {
|
});
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const instance of sortedInstances) {
|
|
||||||
if (!instance.instance_id) continue;
|
|
||||||
options.push(this._makeMenuOption(server, instance, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
protocolsList.forEach(({instances=[]}) => {
|
||||||
}
|
[...instances].sort((b, a) => {
|
||||||
|
return a.desc.localeCompare(b.desc);
|
||||||
|
}).forEach(({desc, instance_id: instanceId}) => {
|
||||||
|
entries.push(
|
||||||
|
<MenuItemRadio
|
||||||
|
key={String(instanceId)}
|
||||||
|
active={serverSelected && instanceId === selectedInstanceId}
|
||||||
|
onClick={handlerFactory(server, instanceId)}
|
||||||
|
label={desc}
|
||||||
|
className="mx_NetworkDropdown_server_network"
|
||||||
|
>
|
||||||
|
{ desc }
|
||||||
|
</MenuItemRadio>);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
_makeMenuOption(server, instance, includeAll, handleClicks) {
|
let subtitle;
|
||||||
if (handleClicks === undefined) handleClicks = true;
|
if (server === hsName) {
|
||||||
|
subtitle = (
|
||||||
|
<div className="mx_NetworkDropdown_server_subtitle">
|
||||||
|
{_t("Your server")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let icon;
|
let removeButton;
|
||||||
let name;
|
if (removableServers.has(server)) {
|
||||||
let key;
|
const onClick = async () => {
|
||||||
|
closeMenu();
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
|
||||||
|
title: _t("Are you sure?"),
|
||||||
|
description: _t("Are you sure you want to remove <b>%(serverName)s</b>", {
|
||||||
|
serverName: server,
|
||||||
|
}, {
|
||||||
|
b: serverName => <b>{ serverName }</b>,
|
||||||
|
}),
|
||||||
|
button: _t("Remove"),
|
||||||
|
});
|
||||||
|
|
||||||
if (!instance && includeAll) {
|
const [ok] = await finished;
|
||||||
key = server;
|
if (!ok) return;
|
||||||
name = server;
|
|
||||||
} else if (!instance) {
|
|
||||||
key = server + '_all';
|
|
||||||
name = 'Matrix';
|
|
||||||
icon = <img src={require("../../../../res/img/network-matrix.svg")} />;
|
|
||||||
} else {
|
|
||||||
key = server + '_inst_' + instance.instance_id;
|
|
||||||
const imgUrl = instance.icon ?
|
|
||||||
MatrixClientPeg.get().mxcUrlToHttp(instance.icon, 25, 25, 'crop', true) :
|
|
||||||
DEFAULT_ICON_URL;
|
|
||||||
icon = <img src={imgUrl} />;
|
|
||||||
name = instance.desc;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clickHandler = handleClicks ? this.onMenuOptionClick.bind(this, server, instance, includeAll) : null;
|
// delete from setting
|
||||||
|
await SettingsStore.setValue(SETTING_NAME, null, "account", servers.filter(s => s !== server));
|
||||||
|
|
||||||
return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={clickHandler}>
|
// the selected server is being removed, reset to our HS
|
||||||
{icon}
|
if (serverSelected === server) {
|
||||||
<span className="mx_NetworkDropdown_menu_network">{name}</span>
|
onOptionChange(hsName, undefined);
|
||||||
</div>;
|
}
|
||||||
}
|
};
|
||||||
|
removeButton = <AccessibleButton onClick={onClick} />;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
let currentValue;
|
<div className="mx_NetworkDropdown_server" key={server}>
|
||||||
|
<div className="mx_NetworkDropdown_server_title">
|
||||||
|
{ server }
|
||||||
|
{ removeButton }
|
||||||
|
</div>
|
||||||
|
{ subtitle }
|
||||||
|
|
||||||
let menu;
|
<MenuItemRadio
|
||||||
if (this.state.expanded) {
|
active={serverSelected && !selectedInstanceId}
|
||||||
const menuOptions = this._getMenuOptions();
|
onClick={handlerFactory(server, undefined)}
|
||||||
menu = <div className="mx_NetworkDropdown_menu">
|
label={_t("Matrix")}
|
||||||
{menuOptions}
|
className="mx_NetworkDropdown_server_network"
|
||||||
</div>;
|
>
|
||||||
currentValue = <input type="text" className="mx_NetworkDropdown_networkoption"
|
{_t("Matrix")}
|
||||||
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
|
</MenuItemRadio>
|
||||||
placeholder="matrix.org" // 'matrix.org' as an example of an HS name
|
{ entries }
|
||||||
/>;
|
</div>
|
||||||
} else {
|
|
||||||
const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
|
|
||||||
currentValue = this._makeMenuOption(
|
|
||||||
this.state.selectedServer, instance, this.state.includeAllNetworks, false,
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClick = async () => {
|
||||||
|
closeMenu();
|
||||||
|
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||||
|
const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, {
|
||||||
|
title: _t("Add a new server"),
|
||||||
|
description: _t("Enter the address of a new server you want to explore."),
|
||||||
|
button: _t("Add"),
|
||||||
|
hasCancel: false,
|
||||||
|
placeholder: _t("Server address"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ok, newServer] = await finished;
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
if (!userDefinedServers.includes(newServer)) {
|
||||||
|
const servers = [...userDefinedServers, newServer];
|
||||||
|
await SettingsStore.setValue(SETTING_NAME, null, "account", servers);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOptionChange(newServer); // change filter to the new server
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonRect = handle.current.getBoundingClientRect();
|
||||||
|
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu} managed={false}>
|
||||||
|
<div className="mx_NetworkDropdown_menu">
|
||||||
|
{options}
|
||||||
|
<MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
|
||||||
|
{_t("Add a new server...")}
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
</ContextMenu>;
|
||||||
|
} else {
|
||||||
|
let currentValue;
|
||||||
|
if (selectedInstanceId === ALL_ROOMS) {
|
||||||
|
currentValue = _t("All rooms");
|
||||||
|
} else if (selectedInstanceId) {
|
||||||
|
const instance = instanceForInstanceId(protocols, selectedInstanceId);
|
||||||
|
currentValue = _t("%(networkName)s rooms", {
|
||||||
|
networkName: instance.desc,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
currentValue = _t("Matrix rooms");
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_NetworkDropdown" ref={this.collectRoot}>
|
content = <ContextMenuButton
|
||||||
<div className="mx_NetworkDropdown_input mx_no_textinput" onClick={this.onInputClick}>
|
className="mx_NetworkDropdown_handle"
|
||||||
|
label={_t("React")}
|
||||||
|
onClick={openMenu}
|
||||||
|
isExpanded={menuDisplayed}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
{currentValue}
|
{currentValue}
|
||||||
<span className="mx_NetworkDropdown_arrow" />
|
</span> <span>
|
||||||
{menu}
|
({selectedServerName})
|
||||||
</div>
|
</span>
|
||||||
</div>;
|
</ContextMenuButton>;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return <div className="mx_NetworkDropdown" ref={handle}>
|
||||||
|
{content}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
NetworkDropdown.propTypes = {
|
NetworkDropdown.propTypes = {
|
||||||
onOptionChange: PropTypes.func.isRequired,
|
onOptionChange: PropTypes.func.isRequired,
|
||||||
protocols: PropTypes.object,
|
protocols: PropTypes.object,
|
||||||
// The room directory config. May have a 'servers' key that is a list of server names to include in the dropdown
|
|
||||||
config: PropTypes.object,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
NetworkDropdown.defaultProps = {
|
export default NetworkDropdown;
|
||||||
protocols: {},
|
|
||||||
config: {},
|
|
||||||
};
|
|
||||||
|
|
|
@ -1437,6 +1437,15 @@
|
||||||
"And %(count)s more...|other": "And %(count)s more...",
|
"And %(count)s more...|other": "And %(count)s more...",
|
||||||
"ex. @bob:example.com": "ex. @bob:example.com",
|
"ex. @bob:example.com": "ex. @bob:example.com",
|
||||||
"Add User": "Add User",
|
"Add User": "Add User",
|
||||||
|
"All rooms": "All rooms",
|
||||||
|
"Your server": "Your server",
|
||||||
|
"Matrix": "Matrix",
|
||||||
|
"Add a new server": "Add a new server",
|
||||||
|
"Enter the address of a new server you want to explore.": "Enter the address of a new server you want to explore.",
|
||||||
|
"Server address": "Server address",
|
||||||
|
"Add a new server...": "Add a new server...",
|
||||||
|
"%(networkName)s rooms": "%(networkName)s rooms",
|
||||||
|
"Matrix rooms": "Matrix rooms",
|
||||||
"Matrix ID": "Matrix ID",
|
"Matrix ID": "Matrix ID",
|
||||||
"Matrix Room ID": "Matrix Room ID",
|
"Matrix Room ID": "Matrix Room ID",
|
||||||
"email address": "email address",
|
"email address": "email address",
|
||||||
|
|
|
@ -324,6 +324,10 @@ export const SETTINGS = {
|
||||||
supportedLevels: ['account'],
|
supportedLevels: ['account'],
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
|
"room_directory_servers": {
|
||||||
|
supportedLevels: ['account'],
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
"integrationProvisioning": {
|
"integrationProvisioning": {
|
||||||
supportedLevels: ['account'],
|
supportedLevels: ['account'],
|
||||||
default: true,
|
default: true,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue