Initial attempt to redesign explore servers in room directory

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-03-13 00:02:50 +00:00
parent 5c582037ce
commit 86e53ea2c3
9 changed files with 362 additions and 278 deletions

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
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");
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 PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
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 {
constructor(props) {
super(props);
const SETTING_NAME = "room_directory_servers";
this.dropdownRootElement = null;
this.ignoreEvent = null;
const inPlaceOf = (elementRect) => ({
right: window.innerWidth - elementRect.right,
top: elementRect.top,
chevronOffset: 0,
chevronFace: "none",
});
this.onInputClick = this.onInputClick.bind(this);
this.onRootClick = this.onRootClick.bind(this);
this.onDocumentClick = this.onDocumentClick.bind(this);
this.onMenuOptionClick = this.onMenuOptionClick.bind(this);
this.onInputKeyUp = this.onInputKeyUp.bind(this);
this.collectRoot = this.collectRoot.bind(this);
this.collectInputTextBox = this.collectInputTextBox.bind(this);
// This dropdown sources homeservers from three places:
// + your currently connected homeserver
// + homeservers in config.json["roomDirectory"]
// + homeservers in SettingsStore["room_directory_servers"]
// if a server exists in multiple, only keep the top-most entry.
this.inputTextBox = null;
const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, selectedInstanceId}) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
const userDefinedServers = useSettingValue(SETTING_NAME);
const server = MatrixClientPeg.getHomeserverName();
this.state = {
expanded: false,
selectedServer: server,
selectedInstanceId: null,
includeAllNetworks: false,
const handlerFactory = (server, instanceId) => {
return () => {
onOptionChange(server, instanceId);
closeMenu();
};
}
};
componentWillMount() {
// Listen for all clicks on the document so we can close the
// menu when the user clicks somewhere else
document.addEventListener('click', this.onDocumentClick, false);
// we either show the button or the dropdown in its place.
let content;
if (menuDisplayed) {
const config = SdkConfig.get();
const roomDirectory = config.roomDirectory || {};
// fire this now so the defaults can be set up
const {selectedServer, selectedInstanceId, includeAllNetworks} = this.state;
this.props.onOptionChange(selectedServer, selectedInstanceId, includeAllNetworks);
}
const hsName = MatrixClientPeg.getHomeserverName();
const configServers = new Set(roomDirectory.servers);
componentWillUnmount() {
document.removeEventListener('click', this.onDocumentClick, false);
}
componentDidUpdate() {
if (this.state.expanded && this.inputTextBox) {
this.inputTextBox.focus();
}
}
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());
}
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
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
hsName,
...Array.from(configServers).filter(s => s !== hsName).sort(),
...Array.from(removableServers).sort(),
];
// 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.
// We can't get thirdparty protocols for remote server yet though, so for those
// we can only show the default room list.
for (const server of servers) {
options.push(this._makeMenuOption(server, null, true));
if (server === MatrixClientPeg.getHomeserverName()) {
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 options = servers.map(server => {
const serverSelected = server === selectedServerName;
const entries = [];
const sortedInstances = this.props.protocols[proto].instances;
sortedInstances.sort(function(x, y) {
const a = x.desc;
const b = y.desc;
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
});
for (const instance of sortedInstances) {
if (!instance.instance_id) continue;
options.push(this._makeMenuOption(server, instance, false));
}
}
}
const protocolsList = server === hsName ? Object.values(protocols) : [];
if (protocolsList.length > 0) {
// add a fake protocol with the ALL_ROOMS symbol
protocolsList.push({
instances: [{
instance_id: ALL_ROOMS,
desc: _t("All rooms"),
}],
});
}
}
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) {
if (handleClicks === undefined) handleClicks = true;
let subtitle;
if (server === hsName) {
subtitle = (
<div className="mx_NetworkDropdown_server_subtitle">
{_t("Your server")}
</div>
);
}
let icon;
let name;
let key;
let removeButton;
if (removableServers.has(server)) {
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) {
key = server;
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 [ok] = await finished;
if (!ok) return;
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}>
{icon}
<span className="mx_NetworkDropdown_menu_network">{name}</span>
</div>;
}
// the selected server is being removed, reset to our HS
if (serverSelected === server) {
onOptionChange(hsName, undefined);
}
};
removeButton = <AccessibleButton onClick={onClick} />;
}
render() {
let currentValue;
return (
<div className="mx_NetworkDropdown_server" key={server}>
<div className="mx_NetworkDropdown_server_title">
{ server }
{ removeButton }
</div>
{ subtitle }
let menu;
if (this.state.expanded) {
const menuOptions = this._getMenuOptions();
menu = <div className="mx_NetworkDropdown_menu">
{menuOptions}
</div>;
currentValue = <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 {
const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
currentValue = this._makeMenuOption(
this.state.selectedServer, instance, this.state.includeAllNetworks, false,
<MenuItemRadio
active={serverSelected && !selectedInstanceId}
onClick={handlerFactory(server, undefined)}
label={_t("Matrix")}
className="mx_NetworkDropdown_server_network"
>
{_t("Matrix")}
</MenuItemRadio>
{ entries }
</div>
);
});
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}>
<div className="mx_NetworkDropdown_input mx_no_textinput" onClick={this.onInputClick}>
content = <ContextMenuButton
className="mx_NetworkDropdown_handle"
label={_t("React")}
onClick={openMenu}
isExpanded={menuDisplayed}
>
<span>
{currentValue}
<span className="mx_NetworkDropdown_arrow" />
{menu}
</div>
</div>;
</span> <span>
({selectedServerName})
</span>
</ContextMenuButton>;
}
}
return <div className="mx_NetworkDropdown" ref={handle}>
{content}
</div>;
};
NetworkDropdown.propTypes = {
onOptionChange: PropTypes.func.isRequired,
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 = {
protocols: {},
config: {},
};
export default NetworkDropdown;