Merge branch 'experimental' into travis/rsettings/tab/general
This commit is contained in:
commit
ad276c1fda
37 changed files with 919 additions and 347 deletions
|
@ -66,7 +66,7 @@ const customVariables = {
|
|||
},
|
||||
'User Type': {
|
||||
id: 3,
|
||||
expl: _td('Whether or not you\'re logged in (we don\'t record your user name)'),
|
||||
expl: _td('Whether or not you\'re logged in (we don\'t record your username)'),
|
||||
example: 'Logged In',
|
||||
},
|
||||
'Chosen Language': {
|
||||
|
|
|
@ -20,6 +20,7 @@ import React from "react";
|
|||
// Copyright (c) Noel Delgado <pixelia.me@gmail.com> (pixelia.me)
|
||||
function getScrollbarWidth(alternativeOverflow) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'mx_AutoHideScrollbar'; //to get width of css scrollbar
|
||||
div.style.position = 'absolute';
|
||||
div.style.top = '-9999px';
|
||||
div.style.width = '100px';
|
||||
|
|
|
@ -84,7 +84,7 @@ const RoomSubList = React.createClass({
|
|||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||
isCollapsableOnClick: function() {
|
||||
const stuck = this.refs.header.dataset.stuck;
|
||||
if (this.state.hidden || stuck === undefined || stuck === "none") {
|
||||
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -238,7 +238,7 @@ const RoomSubList = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_getHeaderJsx: function() {
|
||||
_getHeaderJsx: function(isCollapsed) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const subListNotifications = this.roomNotificationCount();
|
||||
const subListNotifCount = subListNotifications[0];
|
||||
|
@ -287,8 +287,8 @@ const RoomSubList = React.createClass({
|
|||
if (len) {
|
||||
const chevronClasses = classNames({
|
||||
'mx_RoomSubList_chevron': true,
|
||||
'mx_RoomSubList_chevronRight': this.state.hidden,
|
||||
'mx_RoomSubList_chevronDown': !this.state.hidden,
|
||||
'mx_RoomSubList_chevronRight': isCollapsed,
|
||||
'mx_RoomSubList_chevronDown': !isCollapsed,
|
||||
});
|
||||
chevron = (<div className={chevronClasses}></div>);
|
||||
}
|
||||
|
@ -321,21 +321,23 @@ const RoomSubList = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const len = this.props.list.length + this.props.extraTiles.length;
|
||||
const isCollapsed = this.state.hidden && !this.props.forceExpand;
|
||||
if (len) {
|
||||
const subListClasses = classNames({
|
||||
"mx_RoomSubList": true,
|
||||
"mx_RoomSubList_hidden": this.state.hidden,
|
||||
"mx_RoomSubList_nonEmpty": len && !this.state.hidden,
|
||||
"mx_RoomSubList_hidden": isCollapsed,
|
||||
"mx_RoomSubList_nonEmpty": len && !isCollapsed,
|
||||
});
|
||||
if (this.state.hidden) {
|
||||
|
||||
if (isCollapsed) {
|
||||
return <div ref="subList" className={subListClasses}>
|
||||
{this._getHeaderJsx()}
|
||||
{this._getHeaderJsx(isCollapsed)}
|
||||
</div>;
|
||||
} else {
|
||||
const tiles = this.makeRoomTiles();
|
||||
tiles.push(...this.props.extraTiles);
|
||||
return <div ref="subList" className={subListClasses}>
|
||||
{this._getHeaderJsx()}
|
||||
{this._getHeaderJsx(isCollapsed)}
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll">
|
||||
{ tiles }
|
||||
</IndicatorScrollbar>
|
||||
|
@ -344,13 +346,13 @@ const RoomSubList = React.createClass({
|
|||
} else {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
let content;
|
||||
if (this.props.showSpinner && !this.state.hidden) {
|
||||
if (this.props.showSpinner && !isCollapsed) {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref="subList" className="mx_RoomSubList">
|
||||
{ this._getHeaderJsx() }
|
||||
{ this._getHeaderJsx(isCollapsed) }
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -211,7 +211,6 @@ module.exports = React.createClass({
|
|||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
|
|
|
@ -26,10 +26,20 @@ import Login from '../../../Login';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import { AutoDiscovery } from "matrix-js-sdk";
|
||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
const PHASE_SERVER_DETAILS = 0;
|
||||
// Show the appropriate login flow(s) for the server
|
||||
const PHASE_LOGIN = 1;
|
||||
|
||||
// Disable phases for now, pending UX discussion on WK discovery
|
||||
const PHASES_ENABLED = false;
|
||||
|
||||
// These are used in several places, and come from the js-sdk's autodiscovery
|
||||
// stuff. We define them here so that they'll be picked up by i18n.
|
||||
_td("Invalid homeserver discovery response");
|
||||
|
@ -80,6 +90,8 @@ module.exports = React.createClass({
|
|||
busy: false,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
|
||||
serverType: null,
|
||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
|
||||
|
@ -87,6 +99,10 @@ module.exports = React.createClass({
|
|||
username: "",
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
|
||||
// Phase of the overall login dialog.
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
// The current login flow, such as password, SSO, etc.
|
||||
currentFlow: "m.login.password",
|
||||
|
||||
// .well-known discovery
|
||||
|
@ -299,12 +315,60 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onServerTypeChange(type) {
|
||||
this.setState({
|
||||
serverType: type,
|
||||
});
|
||||
|
||||
// When changing server types, set the HS / IS URLs to reasonable defaults for the
|
||||
// the new type.
|
||||
switch (type) {
|
||||
case ServerType.FREE: {
|
||||
const { hsUrl, isUrl } = ServerType.TYPES.FREE;
|
||||
this.onServerConfigChange({
|
||||
hsUrl,
|
||||
isUrl,
|
||||
});
|
||||
// Move directly to the login phase since the server details are fixed.
|
||||
this.setState({
|
||||
phase: PHASE_LOGIN,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ServerType.PREMIUM:
|
||||
case ServerType.ADVANCED:
|
||||
this.onServerConfigChange({
|
||||
hsUrl: this.props.defaultHsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onRegisterClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onRegisterClick();
|
||||
},
|
||||
|
||||
onServerDetailsNextPhaseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
phase: PHASE_LOGIN,
|
||||
});
|
||||
},
|
||||
|
||||
onEditServerDetailsClick(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
});
|
||||
},
|
||||
|
||||
_tryWellKnownDiscovery: async function(serverName) {
|
||||
if (!serverName.trim()) {
|
||||
// Nothing to discover
|
||||
|
@ -475,7 +539,79 @@ module.exports = React.createClass({
|
|||
return errorText;
|
||||
},
|
||||
|
||||
componentForStep: function(step) {
|
||||
serverComponentForStep() {
|
||||
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
// TODO: May need to adjust the behavior of this config option
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we're on a different phase, we only show the server type selector,
|
||||
// which is always shown if we allow custom URLs at all.
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let serverDetails = null;
|
||||
switch (this.state.serverType) {
|
||||
case ServerType.FREE:
|
||||
break;
|
||||
case ServerType.PREMIUM:
|
||||
serverDetails = <ModularServerConfig
|
||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}
|
||||
/>;
|
||||
break;
|
||||
case ServerType.ADVANCED:
|
||||
serverDetails = <ServerConfig
|
||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
||||
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}
|
||||
/>;
|
||||
break;
|
||||
}
|
||||
|
||||
let nextButton = null;
|
||||
if (PHASES_ENABLED) {
|
||||
nextButton = <AccessibleButton className="mx_Login_submit"
|
||||
onClick={this.onServerDetailsNextPhaseClick}
|
||||
>
|
||||
{_t("Next")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
{serverDetails}
|
||||
{nextButton}
|
||||
</div>;
|
||||
},
|
||||
|
||||
loginComponentForStep() {
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_LOGIN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const step = this.state.currentFlow;
|
||||
|
||||
if (!step) {
|
||||
return null;
|
||||
}
|
||||
|
@ -491,10 +627,21 @@ module.exports = React.createClass({
|
|||
|
||||
_renderPasswordStep: function() {
|
||||
const PasswordLogin = sdk.getComponent('auth.PasswordLogin');
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed and we haven't selected the Free server type, wire
|
||||
// up the server details edit link.
|
||||
if (
|
||||
PHASES_ENABLED &&
|
||||
!SdkConfig.get()['disable_custom_urls'] &&
|
||||
this.state.serverType !== ServerType.FREE
|
||||
) {
|
||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onError={this.onPasswordLoginError}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
|
@ -521,7 +668,7 @@ module.exports = React.createClass({
|
|||
// user's browser, let them log into their SSO provider, then redirect their browser
|
||||
// to vector://vector which, of course, will not work.
|
||||
return (
|
||||
<a href={url} className="mx_Login_sso_link">{ _t('Sign in with single sign-on') }</a>
|
||||
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a>
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -530,7 +677,6 @@ module.exports = React.createClass({
|
|||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText;
|
||||
|
@ -543,19 +689,6 @@ module.exports = React.createClass({
|
|||
</a>;
|
||||
}
|
||||
|
||||
let serverConfig;
|
||||
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfig = <ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
||||
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000} />;
|
||||
}
|
||||
|
||||
let errorTextSection;
|
||||
if (errorText) {
|
||||
errorTextSection = (
|
||||
|
@ -574,8 +707,8 @@ module.exports = React.createClass({
|
|||
{loader}
|
||||
</h2>
|
||||
{ errorTextSection }
|
||||
{ this.componentForStep(this.state.currentFlow) }
|
||||
{ serverConfig }
|
||||
{ this.serverComponentForStep() }
|
||||
{ this.loginComponentForStep() }
|
||||
<a className="mx_Auth_changeFlow" onClick={this.onRegisterClick} href="#">
|
||||
{ _t('Create account') }
|
||||
</a>
|
||||
|
|
|
@ -255,7 +255,7 @@ module.exports = React.createClass({
|
|||
errMsg = _t("Only use lower case letters, numbers and '=_-./'");
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_BLANK":
|
||||
errMsg = _t('You need to enter a user name.');
|
||||
errMsg = _t('You need to enter a username.');
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown error code: %s", errCode);
|
||||
|
@ -330,7 +330,6 @@ module.exports = React.createClass({
|
|||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
|
|
|
@ -27,17 +27,17 @@ module.exports = React.createClass({
|
|||
{ _t("Custom Server Options") }
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<span>
|
||||
{ _t("You can use the custom server options to sign into other Matrix " +
|
||||
"servers by specifying a different Home server URL.") }
|
||||
<br />
|
||||
{ _t("This allows you to use this app with an existing Matrix account on " +
|
||||
"a different home server.") }
|
||||
<br />
|
||||
<br />
|
||||
{ _t("You can also set a custom identity server but this will typically prevent " +
|
||||
"interaction with users based on email address.") }
|
||||
</span>
|
||||
<p>{_t(
|
||||
"You can use the custom server options to sign into other " +
|
||||
"Matrix servers by specifying a different homeserver URL. This " +
|
||||
"allows you to use this app with an existing Matrix account on a " +
|
||||
"different homeserver.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"You can also set a custom identity server, but you won't be " +
|
||||
"able to invite users by email address, or be invited by email " +
|
||||
"address yourself.",
|
||||
)}</p>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onFinished} autoFocus={true}>
|
||||
|
|
126
src/components/views/auth/ModularServerConfig.js
Normal file
126
src/components/views/auth/ModularServerConfig.js
Normal file
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
|
||||
/*
|
||||
* Configure the Modular server name.
|
||||
*
|
||||
* This is a variant of ServerConfig with only the HS field and different body
|
||||
* text that is specific to the Modular case.
|
||||
*/
|
||||
export default class ModularServerConfig extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onServerConfigChange: PropTypes.func,
|
||||
|
||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
||||
// they are used if the user has not overridden them with a custom URL.
|
||||
// In other words, if the custom URL is blank, the default is used.
|
||||
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
|
||||
|
||||
// This component always uses the default IS URL and doesn't allow it
|
||||
// to be changed. We still receive it as a prop here to simplify
|
||||
// consumers by still passing the IS URL via onServerConfigChange.
|
||||
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
|
||||
|
||||
// custom URLs are explicitly provided by the user and override the
|
||||
// default URLs. The user enters them via the component's input fields,
|
||||
// which is reflected on these properties whenever on..UrlChanged fires.
|
||||
// They are persisted in localStorage by MatrixClientPeg, and so can
|
||||
// override the default URLs when the component initially loads.
|
||||
customHsUrl: PropTypes.string,
|
||||
|
||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onServerConfigChange: function() {},
|
||||
customHsUrl: "",
|
||||
delayTimeMs: 0,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hsUrl: props.customHsUrl,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.customHsUrl === this.state.hsUrl) return;
|
||||
|
||||
this.setState({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
});
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
}
|
||||
|
||||
onHomeserverChanged = (ev) => {
|
||||
this.setState({hsUrl: ev.target.value}, () => {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
||||
let hsUrl = this.state.hsUrl.trim().replace(/\/$/, "");
|
||||
if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.state.hsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_waitThenInvoke(existingTimeoutId, fn) {
|
||||
if (existingTimeoutId) {
|
||||
clearTimeout(existingTimeoutId);
|
||||
}
|
||||
return setTimeout(fn.bind(this), this.props.delayTimeMs);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
return (
|
||||
<div className="mx_ServerConfig">
|
||||
<h3>{_t("Your Modular server")}</h3>
|
||||
{_t(
|
||||
"Enter the location of your Modular homeserver. It may use your own " +
|
||||
"domain name or be a subdomain of <a>modular.im</a>.",
|
||||
{}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
)}
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Server Name")}
|
||||
placeholder={this.props.defaultHsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onChange={this.onHomeserverChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
class PasswordLogin extends React.Component {
|
||||
static defaultProps = {
|
||||
onError: function() {},
|
||||
onEditServerDetailsClick: null,
|
||||
onUsernameChanged: function() {},
|
||||
onUsernameBlur: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
|
@ -93,7 +94,7 @@ class PasswordLogin extends React.Component {
|
|||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
username = this.state.username;
|
||||
if (!username) {
|
||||
error = _t('The user name field must not be blank.');
|
||||
error = _t('The username field must not be blank.');
|
||||
}
|
||||
break;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
|
@ -158,18 +159,16 @@ class PasswordLogin extends React.Component {
|
|||
this.props.onPasswordChanged(ev.target.value);
|
||||
}
|
||||
|
||||
renderLoginField(loginType, disabled) {
|
||||
renderLoginField(loginType) {
|
||||
const classes = {
|
||||
mx_Login_field: true,
|
||||
mx_Login_field_disabled: disabled,
|
||||
};
|
||||
|
||||
switch (loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
classes.mx_Login_email = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <input
|
||||
className={classNames(classes)}
|
||||
className="mx_Login_field"
|
||||
ref={(e) => {this._loginField = e;}}
|
||||
key="email_input"
|
||||
type="text"
|
||||
|
@ -179,10 +178,8 @@ class PasswordLogin extends React.Component {
|
|||
placeholder="joe@example.com"
|
||||
value={this.state.username}
|
||||
autoFocus
|
||||
disabled={disabled}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
classes.mx_Login_username = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <input
|
||||
className={classNames(classes)}
|
||||
|
@ -195,14 +192,12 @@ class PasswordLogin extends React.Component {
|
|||
placeholder={SdkConfig.get().disable_custom_urls ?
|
||||
_t("Username on %(hs)s", {
|
||||
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
|
||||
}) : _t("User name")}
|
||||
}) : _t("Username")}
|
||||
value={this.state.username}
|
||||
autoFocus
|
||||
disabled={disabled}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE: {
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
classes.mx_Login_phoneNumberField = true;
|
||||
classes.mx_Login_field_has_prefix = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
|
||||
return <div className="mx_Login_phoneSection">
|
||||
|
@ -212,7 +207,6 @@ class PasswordLogin extends React.Component {
|
|||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<input
|
||||
className={classNames(classes)}
|
||||
|
@ -224,7 +218,6 @@ class PasswordLogin extends React.Component {
|
|||
placeholder={_t("Mobile phone number")}
|
||||
value={this.state.phoneNumber}
|
||||
autoFocus
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
@ -257,27 +250,35 @@ class PasswordLogin extends React.Component {
|
|||
</span>;
|
||||
}
|
||||
|
||||
let matrixIdText = _t('Matrix ID');
|
||||
let yourMatrixAccountText = _t('Your account');
|
||||
if (this.props.hsName) {
|
||||
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName});
|
||||
yourMatrixAccountText = _t('Your %(serverName)s account', {serverName: this.props.hsName});
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname});
|
||||
yourMatrixAccountText = _t('Your %(serverName)s account', {serverName: parsedHsUrl.hostname});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
if (this.props.onEditServerDetailsClick) {
|
||||
editLink = <a className="mx_Auth_editServerDetails"
|
||||
href="#" onClick={this.props.onEditServerDetailsClick}
|
||||
>
|
||||
{_t('Edit')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
const pwFieldClass = classNames({
|
||||
mx_Login_field: true,
|
||||
mx_Login_field_disabled: matrixIdText === '',
|
||||
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||
});
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
|
||||
const loginField = this.renderLoginField(this.state.loginType);
|
||||
|
||||
let loginType;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
|
@ -287,9 +288,8 @@ class PasswordLogin extends React.Component {
|
|||
<Dropdown
|
||||
className="mx_Login_type_dropdown"
|
||||
value={this.state.loginType}
|
||||
disabled={matrixIdText === ''}
|
||||
onOptionChange={this.onLoginTypeChange}>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ matrixIdText }</span>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ _t('Username') }</span>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
|
||||
</Dropdown>
|
||||
|
@ -297,22 +297,27 @@ class PasswordLogin extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const disableSubmit = this.props.disableSubmit || matrixIdText === '';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>
|
||||
{yourMatrixAccountText}
|
||||
{editLink}
|
||||
</h3>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
{ loginType }
|
||||
{ loginField }
|
||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||
name="password"
|
||||
value={this.state.password} onChange={this.onPasswordChanged}
|
||||
placeholder={_t('Password')}
|
||||
disabled={matrixIdText === ''}
|
||||
/>
|
||||
<br />
|
||||
{ forgotPasswordJsx }
|
||||
<input className="mx_Login_submit" type="submit" value={_t('Sign in')} disabled={disableSubmit} />
|
||||
{ loginType }
|
||||
{ loginField }
|
||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||
name="password"
|
||||
value={this.state.password} onChange={this.onPasswordChanged}
|
||||
placeholder={_t('Password')}
|
||||
/>
|
||||
<br />
|
||||
{ forgotPasswordJsx }
|
||||
<input className="mx_Login_submit"
|
||||
type="submit"
|
||||
value={_t('Sign in')}
|
||||
disabled={this.props.disableSubmit}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -305,7 +305,7 @@ module.exports = React.createClass({
|
|||
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
|
||||
);
|
||||
|
||||
const placeholderUserName = _t("User name");
|
||||
const placeholderUsername = _t("Username");
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -313,7 +313,7 @@ module.exports = React.createClass({
|
|||
{ emailSection }
|
||||
{ phoneSection }
|
||||
<input type="text" ref="username"
|
||||
placeholder={placeholderUserName} defaultValue={this.props.defaultUsername}
|
||||
placeholder={placeholderUsername} defaultValue={this.props.defaultUsername}
|
||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
|
||||
<br />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,21 +15,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
const Modal = require('../../../Modal');
|
||||
const sdk = require('../../../index');
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/**
|
||||
/*
|
||||
* A pure UI component which displays the HS and IS to use.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ServerConfig',
|
||||
|
||||
propTypes: {
|
||||
export default class ServerConfig extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onServerConfigChange: PropTypes.func,
|
||||
|
||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
||||
|
@ -45,155 +43,103 @@ module.exports = React.createClass({
|
|||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
|
||||
withToggleButton: PropTypes.bool,
|
||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
||||
},
|
||||
}
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onServerConfigChange: function() {},
|
||||
customHsUrl: "",
|
||||
customIsUrl: "",
|
||||
withToggleButton: false,
|
||||
delayTimeMs: 0,
|
||||
static defaultProps = {
|
||||
onServerConfigChange: function() {},
|
||||
customHsUrl: "",
|
||||
customIsUrl: "",
|
||||
delayTimeMs: 0,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hsUrl: props.customHsUrl,
|
||||
isUrl: props.customIsUrl,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
hs_url: this.props.customHsUrl,
|
||||
is_url: this.props.customIsUrl,
|
||||
// if withToggleButton is false, then show the config all the time given we have no way otherwise of making it visible
|
||||
configVisible: !this.props.withToggleButton ||
|
||||
(this.props.customHsUrl !== this.props.defaultHsUrl) ||
|
||||
(this.props.customIsUrl !== this.props.defaultIsUrl),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
if (newProps.customHsUrl === this.state.hs_url &&
|
||||
newProps.customIsUrl === this.state.is_url) return;
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.customHsUrl === this.state.hsUrl &&
|
||||
newProps.customIsUrl === this.state.isUrl) return;
|
||||
|
||||
this.setState({
|
||||
hs_url: newProps.customHsUrl,
|
||||
is_url: newProps.customIsUrl,
|
||||
configVisible: !newProps.withToggleButton ||
|
||||
(newProps.customHsUrl !== newProps.defaultHsUrl) ||
|
||||
(newProps.customIsUrl !== newProps.defaultIsUrl),
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: newProps.customIsUrl,
|
||||
});
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: newProps.customIsUrl,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onHomeserverChanged: function(ev) {
|
||||
this.setState({hs_url: ev.target.value}, () => {
|
||||
onHomeserverChanged = (ev) => {
|
||||
this.setState({hsUrl: ev.target.value}, () => {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
||||
let hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
|
||||
let hsUrl = this.state.hsUrl.trim().replace(/\/$/, "");
|
||||
if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.state.hs_url,
|
||||
isUrl: this.state.is_url,
|
||||
hsUrl: this.state.hsUrl,
|
||||
isUrl: this.state.isUrl,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onIdentityServerChanged: function(ev) {
|
||||
this.setState({is_url: ev.target.value}, () => {
|
||||
onIdentityServerChanged = (ev) => {
|
||||
this.setState({isUrl: ev.target.value}, () => {
|
||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
|
||||
let isUrl = this.state.is_url.trim().replace(/\/$/, "");
|
||||
let isUrl = this.state.isUrl.trim().replace(/\/$/, "");
|
||||
if (isUrl === "") isUrl = this.props.defaultIsUrl;
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.state.hs_url,
|
||||
isUrl: this.state.is_url,
|
||||
hsUrl: this.state.hsUrl,
|
||||
isUrl: this.state.isUrl,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_waitThenInvoke: function(existingTimeoutId, fn) {
|
||||
_waitThenInvoke(existingTimeoutId, fn) {
|
||||
if (existingTimeoutId) {
|
||||
clearTimeout(existingTimeoutId);
|
||||
}
|
||||
return setTimeout(fn.bind(this), this.props.delayTimeMs);
|
||||
},
|
||||
}
|
||||
|
||||
onServerConfigVisibleChange: function(visible, ev) {
|
||||
this.setState({
|
||||
configVisible: visible,
|
||||
});
|
||||
if (!visible) {
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.props.defaultHsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
} else {
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.state.hs_url,
|
||||
isUrl: this.state.is_url,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showHelpPopup: function() {
|
||||
showHelpPopup = () => {
|
||||
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
|
||||
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
const serverConfigStyle = {};
|
||||
serverConfigStyle.display = this.state.configVisible ? 'block' : 'none';
|
||||
|
||||
let toggleButton;
|
||||
if (this.props.withToggleButton) {
|
||||
toggleButton = (
|
||||
<div className="mx_ServerConfig_selector">
|
||||
<input className="mx_Login_radio" id="basic" name="configVisible" type="radio"
|
||||
checked={!this.state.configVisible}
|
||||
onChange={this.onServerConfigVisibleChange.bind(this, false)} />
|
||||
<label className="mx_Login_label" htmlFor="basic">
|
||||
{ _t("Default server") }
|
||||
</label>
|
||||
|
||||
<input className="mx_Login_radio" id="advanced" name="configVisible" type="radio"
|
||||
checked={this.state.configVisible}
|
||||
onChange={this.onServerConfigVisibleChange.bind(this, true)} />
|
||||
<label className="mx_Login_label" htmlFor="advanced">
|
||||
{ _t("Custom server") }
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ toggleButton }
|
||||
<div style={serverConfigStyle}>
|
||||
<div className="mx_ServerConfig">
|
||||
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">
|
||||
{ _t("Home server URL") }
|
||||
</label>
|
||||
<input className="mx_Login_field" id="hsurl" type="text"
|
||||
<div className="mx_ServerConfig">
|
||||
<h3>{_t("Other servers")}</h3>
|
||||
{_t("Enter custom server URLs <a>What does this mean?</a>", {}, {
|
||||
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
{ sub }
|
||||
</a>,
|
||||
})}
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Homeserver URL")}
|
||||
placeholder={this.props.defaultHsUrl}
|
||||
disabled={!this.props.withToggleButton}
|
||||
value={this.state.hs_url}
|
||||
onChange={this.onHomeserverChanged} />
|
||||
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">
|
||||
{ _t("Identity server URL") }
|
||||
</label>
|
||||
<input className="mx_Login_field" id="isurl" type="text"
|
||||
value={this.state.hsUrl}
|
||||
onChange={this.onHomeserverChanged}
|
||||
/>
|
||||
<Field id="mx_ServerConfig_isUrl"
|
||||
label={_t("Identity Server URL")}
|
||||
placeholder={this.props.defaultIsUrl}
|
||||
disabled={!this.props.withToggleButton}
|
||||
value={this.state.is_url}
|
||||
onChange={this.onIdentityServerChanged} />
|
||||
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
{ _t("What does this mean?") }
|
||||
</a>
|
||||
value={this.state.isUrl}
|
||||
onChange={this.onIdentityServerChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
148
src/components/views/auth/ServerTypeSelector.js
Normal file
148
src/components/views/auth/ServerTypeSelector.js
Normal file
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
|
||||
export const FREE = 'Free';
|
||||
export const PREMIUM = 'Premium';
|
||||
export const ADVANCED = 'Advanced';
|
||||
|
||||
export const TYPES = {
|
||||
FREE: {
|
||||
id: FREE,
|
||||
label: () => _t('Free'),
|
||||
logo: () => <img src={require('../../../../res/img/feather-icons/matrix-org-bw-logo.svg')} />,
|
||||
description: () => _t('Join millions for free on the largest public server'),
|
||||
hsUrl: 'https://matrix.org',
|
||||
isUrl: 'https://vector.im',
|
||||
},
|
||||
PREMIUM: {
|
||||
id: PREMIUM,
|
||||
label: () => _t('Premium'),
|
||||
logo: () => <img src={require('../../../../res/img/feather-icons/modular-bw-logo.svg')} />,
|
||||
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
}),
|
||||
},
|
||||
ADVANCED: {
|
||||
id: ADVANCED,
|
||||
label: () => _t('Advanced'),
|
||||
logo: () => <div>
|
||||
<img src={require('../../../../res/img/feather-icons/globe.svg')} />
|
||||
{_t('Other')}
|
||||
</div>,
|
||||
description: () => _t('Find other public servers or use a custom server'),
|
||||
},
|
||||
};
|
||||
|
||||
function getDefaultType(defaultHsUrl) {
|
||||
if (!defaultHsUrl) {
|
||||
return null;
|
||||
} else if (defaultHsUrl === TYPES.FREE.hsUrl) {
|
||||
return FREE;
|
||||
} else if (new URL(defaultHsUrl).hostname.endsWith('.modular.im')) {
|
||||
// TODO: Use a Riot config parameter to detect Modular-ness.
|
||||
// https://github.com/vector-im/riot-web/issues/8253
|
||||
return PREMIUM;
|
||||
} else {
|
||||
return ADVANCED;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ServerTypeSelector extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The default HS URL as another way to set the initially selected type.
|
||||
defaultHsUrl: PropTypes.string,
|
||||
// Handler called when the selected type changes.
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
defaultHsUrl,
|
||||
onChange,
|
||||
} = props;
|
||||
const type = getDefaultType(defaultHsUrl);
|
||||
this.state = {
|
||||
selected: type,
|
||||
};
|
||||
if (onChange) {
|
||||
onChange(type);
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedType(type) {
|
||||
if (this.state.selected === type) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selected: type,
|
||||
});
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(type);
|
||||
}
|
||||
}
|
||||
|
||||
onClick = (e) => {
|
||||
e.stopPropagation();
|
||||
const type = e.currentTarget.dataset.id;
|
||||
this.updateSelectedType(type);
|
||||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const serverTypes = [];
|
||||
for (const type of Object.values(TYPES)) {
|
||||
const { id, label, logo, description } = type;
|
||||
const classes = classnames(
|
||||
"mx_ServerTypeSelector_type",
|
||||
`mx_ServerTypeSelector_type_${id}`,
|
||||
{
|
||||
"mx_ServerTypeSelector_type_selected": id === this.state.selected,
|
||||
},
|
||||
);
|
||||
|
||||
serverTypes.push(<div className={classes} key={id} >
|
||||
<div className="mx_ServerTypeSelector_label">
|
||||
{label()}
|
||||
</div>
|
||||
<AccessibleButton onClick={this.onClick} data-id={id}>
|
||||
<div className="mx_ServerTypeSelector_logo">
|
||||
{logo()}
|
||||
</div>
|
||||
<div className="mx_ServerTypeSelector_description">
|
||||
{description()}
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return <div className="mx_ServerTypeSelector">
|
||||
{serverTypes}
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab";
|
|||
import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab";
|
||||
import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab";
|
||||
import HelpSettingsTab from "../settings/tabs/HelpSettingsTab";
|
||||
import FlairSettingsTab from "../settings/tabs/FlairSettingsTab";
|
||||
|
||||
// TODO: Ditch this whole component
|
||||
export class TempTab extends React.Component {
|
||||
|
@ -58,6 +59,11 @@ export default class UserSettingsDialog extends React.Component {
|
|||
"mx_UserSettingsDialog_settingsIcon",
|
||||
<GeneralUserSettingsTab />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Flair"),
|
||||
"mx_UserSettingsDialog_flairIcon",
|
||||
<FlairSettingsTab />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Notifications"),
|
||||
"mx_UserSettingsDialog_bellIcon",
|
||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import { _t } from '../../../languageHandler.js';
|
||||
import ToggleSwitch from "../elements/ToggleSwitch";
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupPublicityToggle',
|
||||
|
@ -52,8 +52,7 @@ export default React.createClass({
|
|||
if (this._groupStoreToken) this._groupStoreToken.unregister();
|
||||
},
|
||||
|
||||
_onPublicityToggle: function(e) {
|
||||
e.stopPropagation();
|
||||
_onPublicityToggle: function() {
|
||||
this.setState({
|
||||
busy: true,
|
||||
// Optimistic early update
|
||||
|
@ -68,21 +67,11 @@ export default React.createClass({
|
|||
|
||||
render() {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
const input = <input type="checkbox"
|
||||
onChange={this._onPublicityToggle}
|
||||
checked={this.state.isGroupPublicised}
|
||||
/>;
|
||||
const labelText = !this.state.ready ? _t("Loading...") :
|
||||
(this.state.isGroupPublicised ?
|
||||
_t("Flair will appear if enabled in room settings") :
|
||||
_t("Flair will not appear")
|
||||
);
|
||||
return <div className="mx_GroupPublicity_toggle">
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
|
||||
<label onClick={this._onPublicityToggle}>
|
||||
{ input }
|
||||
{ labelText }
|
||||
</label>
|
||||
<ToggleSwitch checked={this.state.isGroupPublicised}
|
||||
disabled={!this.state.ready || this.state.busy}
|
||||
onChange={this._onPublicityToggle} />
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -43,9 +43,9 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_renderGroupPublicity() {
|
||||
render() {
|
||||
let text = "";
|
||||
let scrollbox = <div />;
|
||||
let groupPublicityToggles = null;
|
||||
const groups = this.state.groups;
|
||||
|
||||
if (this.state.error) {
|
||||
|
@ -54,16 +54,10 @@ export default React.createClass({
|
|||
text = _t('Loading...');
|
||||
} else if (groups.length > 0) {
|
||||
const GroupPublicityToggle = sdk.getComponent('groups.GroupPublicityToggle');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
|
||||
const groupPublicityToggles = groups.map((groupId, index) => {
|
||||
groupPublicityToggles = groups.map((groupId, index) => {
|
||||
return <GroupPublicityToggle key={index} groupId={groupId} />;
|
||||
});
|
||||
text = _t('Display your community flair in rooms configured to show it.');
|
||||
scrollbox = <div className="mx_GroupUserSettings_groupPublicity_scrollbox">
|
||||
<GeminiScrollbarWrapper>
|
||||
{ groupPublicityToggles }
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>;
|
||||
} else {
|
||||
text = _t("You're not currently a member of any communities.");
|
||||
}
|
||||
|
@ -71,16 +65,10 @@ export default React.createClass({
|
|||
return (
|
||||
<div>
|
||||
<p className="mx_SettingsTab_subsectionText">{ text }</p>
|
||||
{ scrollbox }
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{ groupPublicityToggles }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render() {
|
||||
const groupPublicity = this._renderGroupPublicity();
|
||||
|
||||
return <div>
|
||||
{ groupPublicity }
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -82,7 +82,11 @@ module.exports = React.createClass({
|
|||
this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {};
|
||||
this._layoutSections = [];
|
||||
|
||||
this._layout = new Layout((key, size) => {
|
||||
const unfilteredOptions = {
|
||||
allowWhitespace: true,
|
||||
handleHeight: 1,
|
||||
};
|
||||
this._unfilteredlayout = new Layout((key, size) => {
|
||||
const subList = this._subListRefs[key];
|
||||
if (subList) {
|
||||
subList.setHeight(size);
|
||||
|
@ -95,7 +99,19 @@ module.exports = React.createClass({
|
|||
window.localStorage.setItem("mx_roomlist_sizes",
|
||||
JSON.stringify(this.subListSizes));
|
||||
}
|
||||
}, this.subListSizes, this.collapsedState);
|
||||
}, this.subListSizes, this.collapsedState, unfilteredOptions);
|
||||
|
||||
this._filteredLayout = new Layout((key, size) => {
|
||||
const subList = this._subListRefs[key];
|
||||
if (subList) {
|
||||
subList.setHeight(size);
|
||||
}
|
||||
}, null, null, {
|
||||
allowWhitespace: false,
|
||||
handleHeight: 0,
|
||||
});
|
||||
|
||||
this._layout = this._unfilteredlayout;
|
||||
|
||||
return {
|
||||
isLoadingLeftRooms: false,
|
||||
|
@ -187,15 +203,21 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidUpdate: function(prevProps) {
|
||||
let forceLayoutUpdate = false;
|
||||
this._repositionIncomingCallBox(undefined, false);
|
||||
// if (this.props.searchFilter !== prevProps.searchFilter) {
|
||||
// this._checkSubListsOverflow();
|
||||
// }
|
||||
if (!this.props.searchFilter && prevProps.searchFilter) {
|
||||
this._layout = this._unfilteredlayout;
|
||||
forceLayoutUpdate = true;
|
||||
} else if (this.props.searchFilter && !prevProps.searchFilter) {
|
||||
this._layout = this._filteredLayout;
|
||||
forceLayoutUpdate = true;
|
||||
}
|
||||
this._layout.update(
|
||||
this._layoutSections,
|
||||
this.resizeContainer && this.resizeContainer.clientHeight,
|
||||
forceLayoutUpdate,
|
||||
);
|
||||
// TODO: call layout.setAvailableHeight, window height was changed when bannerShown prop was changed
|
||||
this._checkSubListsOverflow();
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
|
@ -617,7 +639,7 @@ module.exports = React.createClass({
|
|||
onHeaderClick(collapsed);
|
||||
}
|
||||
};
|
||||
const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
|
||||
let startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
|
||||
this._layoutSections.push({
|
||||
id: chosenKey,
|
||||
count: len,
|
||||
|
@ -625,6 +647,7 @@ module.exports = React.createClass({
|
|||
let subList = (<RoomSubList
|
||||
ref={this._subListRef.bind(this, chosenKey)}
|
||||
startAsHidden={startAsHidden}
|
||||
forceExpand={!!this.props.searchFilter}
|
||||
onHeaderClick={onSubListHeaderClick}
|
||||
key={chosenKey}
|
||||
label={label}
|
||||
|
|
52
src/components/views/settings/tabs/FlairSettingsTab.js
Normal file
52
src/components/views/settings/tabs/FlairSettingsTab.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import {DragDropContext} from "react-beautiful-dnd";
|
||||
import GroupUserSettings from "../../groups/GroupUserSettings";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
|
||||
export default class FlairSettingsTab extends React.Component {
|
||||
static childContextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<span className="mx_SettingsTab_heading">{_t("Flair")}</span>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<DragDropContext>
|
||||
<GroupUserSettings />
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
"Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email",
|
||||
"The platform you're on": "The platform you're on",
|
||||
"The version of Riot.im": "The version of Riot.im",
|
||||
"Whether or not you're logged in (we don't record your user name)": "Whether or not you're logged in (we don't record your user name)",
|
||||
"Whether or not you're logged in (we don't record your username)": "Whether or not you're logged in (we don't record your username)",
|
||||
"Your language of choice": "Your language of choice",
|
||||
"Which officially provided instance you are using, if any": "Which officially provided instance you are using, if any",
|
||||
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Whether or not you're using the Richtext mode of the Rich Text Editor",
|
||||
|
@ -305,7 +305,7 @@
|
|||
"Uploading report": "Uploading report",
|
||||
"Waiting for response from server": "Waiting for response from server",
|
||||
"Messages containing my display name": "Messages containing my display name",
|
||||
"Messages containing my user name": "Messages containing my user name",
|
||||
"Messages containing my username": "Messages containing my username",
|
||||
"Messages containing @room": "Messages containing @room",
|
||||
"Messages in one-to-one chats": "Messages in one-to-one chats",
|
||||
"Encrypted messages in one-to-one chats": "Encrypted messages in one-to-one chats",
|
||||
|
@ -1170,9 +1170,8 @@
|
|||
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
|
||||
"This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot",
|
||||
"Custom Server Options": "Custom Server Options",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.",
|
||||
"This allows you to use this app with an existing Matrix account on a different home server.": "This allows you to use this app with an existing Matrix account on a different home server.",
|
||||
"You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "You can also set a custom identity server but this will typically prevent interaction with users based on email address.",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.",
|
||||
"You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.",
|
||||
"To continue, please enter your password.": "To continue, please enter your password.",
|
||||
"Password:": "Password:",
|
||||
"Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies",
|
||||
|
@ -1184,25 +1183,34 @@
|
|||
"Please enter the code it contains:": "Please enter the code it contains:",
|
||||
"Code": "Code",
|
||||
"Start authentication": "Start authentication",
|
||||
"Your Modular server": "Your Modular server",
|
||||
"Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.",
|
||||
"Server Name": "Server Name",
|
||||
"The email field must not be blank.": "The email field must not be blank.",
|
||||
"The user name field must not be blank.": "The user name field must not be blank.",
|
||||
"The username field must not be blank.": "The username field must not be blank.",
|
||||
"The phone number field must not be blank.": "The phone number field must not be blank.",
|
||||
"The password field must not be blank.": "The password field must not be blank.",
|
||||
"Username on %(hs)s": "Username on %(hs)s",
|
||||
"User name": "User name",
|
||||
"Username": "Username",
|
||||
"Mobile phone number": "Mobile phone number",
|
||||
"Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
|
||||
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
|
||||
"Your account": "Your account",
|
||||
"Your %(serverName)s account": "Your %(serverName)s account",
|
||||
"Sign in with": "Sign in with",
|
||||
"Sign in": "Sign in",
|
||||
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
|
||||
"Email address (optional)": "Email address (optional)",
|
||||
"Mobile phone number (optional)": "Mobile phone number (optional)",
|
||||
"Default server": "Default server",
|
||||
"Custom server": "Custom server",
|
||||
"Home server URL": "Home server URL",
|
||||
"Identity server URL": "Identity server URL",
|
||||
"What does this mean?": "What does this mean?",
|
||||
"Other servers": "Other servers",
|
||||
"Enter custom server URLs <a>What does this mean?</a>": "Enter custom server URLs <a>What does this mean?</a>",
|
||||
"Homeserver URL": "Homeserver URL",
|
||||
"Identity Server URL": "Identity Server URL",
|
||||
"Free": "Free",
|
||||
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
|
||||
"Premium": "Premium",
|
||||
"Premium hosting for organisations <a>Learn more</a>": "Premium hosting for organisations <a>Learn more</a>",
|
||||
"Other": "Other",
|
||||
"Find other public servers or use a custom server": "Find other public servers or use a custom server",
|
||||
"Sorry, your browser is <b>not</b> able to run Riot.": "Sorry, your browser is <b>not</b> able to run Riot.",
|
||||
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.",
|
||||
"Please install <chromeLink>Chrome</chromeLink> or <firefoxLink>Firefox</firefoxLink> for the best experience.": "Please install <chromeLink>Chrome</chromeLink> or <firefoxLink>Firefox</firefoxLink> for the best experience.",
|
||||
|
@ -1405,7 +1413,7 @@
|
|||
"This doesn't look like a valid phone number.": "This doesn't look like a valid phone number.",
|
||||
"An email address is required to register on this homeserver.": "An email address is required to register on this homeserver.",
|
||||
"A phone number is required to register on this homeserver.": "A phone number is required to register on this homeserver.",
|
||||
"You need to enter a user name.": "You need to enter a user name.",
|
||||
"You need to enter a username.": "You need to enter a username.",
|
||||
"An unknown error occurred.": "An unknown error occurred.",
|
||||
"Create your account": "Create your account",
|
||||
"Commands": "Commands",
|
||||
|
|
|
@ -83,7 +83,7 @@ module.exports = {
|
|||
// Messages containing user's username (localpart/MXID)
|
||||
".m.rule.contains_user_name": new VectorPushRuleDefinition({
|
||||
kind: "override",
|
||||
description: _td("Messages containing my user name"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||
description: _td("Messages containing my username"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||
|
|
|
@ -16,9 +16,6 @@ limitations under the License.
|
|||
|
||||
import FixedDistributor from "./fixed";
|
||||
|
||||
// const allowWhitespace = true;
|
||||
const handleHeight = 1;
|
||||
|
||||
function clamp(height, min, max) {
|
||||
if (height > max) return max;
|
||||
if (height < min) return min;
|
||||
|
@ -26,7 +23,7 @@ function clamp(height, min, max) {
|
|||
}
|
||||
|
||||
export class Layout {
|
||||
constructor(applyHeight, initialSizes, collapsedState) {
|
||||
constructor(applyHeight, initialSizes, collapsedState, options) {
|
||||
// callback to set height of section
|
||||
this._applyHeight = applyHeight;
|
||||
// list of {id, count} objects,
|
||||
|
@ -41,6 +38,17 @@ export class Layout {
|
|||
this._sectionHeights = Object.assign({}, initialSizes);
|
||||
// in-progress heights, while dragging. Committed on mouse-up.
|
||||
this._heights = [];
|
||||
// use while manually resizing to cancel
|
||||
// the resize for a given mouse position
|
||||
// when the previous resize made the layout
|
||||
// constrained
|
||||
this._clampedOffset = 0;
|
||||
// used while manually resizing, to clear
|
||||
// _clampedOffset when the direction of resizing changes
|
||||
this._lastOffset = 0;
|
||||
|
||||
this._allowWhitespace = options && options.allowWhitespace;
|
||||
this._handleHeight = (options && options.handleHeight) || 0;
|
||||
}
|
||||
|
||||
setAvailableHeight(newSize) {
|
||||
|
@ -60,7 +68,7 @@ export class Layout {
|
|||
this._applyNewSize();
|
||||
}
|
||||
|
||||
update(sections, availableHeight) {
|
||||
update(sections, availableHeight, force = false) {
|
||||
let heightChanged = false;
|
||||
|
||||
if (Number.isFinite(availableHeight) && availableHeight !== this._availableHeight) {
|
||||
|
@ -75,7 +83,7 @@ export class Layout {
|
|||
return a.id !== b.id || a.count !== b.count;
|
||||
});
|
||||
|
||||
if (!heightChanged && !sectionsChanged) {
|
||||
if (!heightChanged && !sectionsChanged && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -104,7 +112,7 @@ export class Layout {
|
|||
const collapsed = this._collapsedState[section.id];
|
||||
return count + (collapsed ? 0 : 1);
|
||||
}, 0);
|
||||
return this._availableHeight - ((nonCollapsedSectionCount - 1) * handleHeight);
|
||||
return this._availableHeight - ((nonCollapsedSectionCount - 1) * this._handleHeight);
|
||||
}
|
||||
|
||||
_applyNewSize() {
|
||||
|
@ -130,9 +138,10 @@ export class Layout {
|
|||
|
||||
if (collapsed) {
|
||||
return this._sectionHeight(0);
|
||||
} else if (!this._allowWhitespace) {
|
||||
return this._sectionHeight(section.count);
|
||||
} else {
|
||||
return 100000;
|
||||
// return this._sectionHeight(section.count);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,6 +277,22 @@ export class Layout {
|
|||
this._sectionHeights[section.id] = this._heights[i];
|
||||
});
|
||||
}
|
||||
|
||||
_setUncommittedSectionHeight(sectionIndex, offset) {
|
||||
if (Math.sign(offset) != Math.sign(this._lastOffset)) {
|
||||
this._clampedOffset = undefined;
|
||||
}
|
||||
if (this._clampedOffset !== undefined) {
|
||||
if (offset < 0 && offset < this._clampedOffset) {
|
||||
return;
|
||||
}
|
||||
if (offset > 0 && offset > this._clampedOffset) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._clampedOffset = this._relayout(sectionIndex, offset);
|
||||
this._lastOffset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
class Handle {
|
||||
|
@ -278,7 +303,10 @@ class Handle {
|
|||
}
|
||||
|
||||
setHeight(height) {
|
||||
this._layout._relayout(this._sectionIndex, height - this._initialHeight);
|
||||
this._layout._setUncommittedSectionHeight(
|
||||
this._sectionIndex,
|
||||
height - this._initialHeight,
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue