Merge branches 'develop' and 't3chguy/accesibility' of github.com:matrix-org/matrix-react-sdk into t3chguy/accesibility
This commit is contained in:
commit
ab3e5c3b87
27 changed files with 596 additions and 124 deletions
|
@ -420,6 +420,7 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._submitUrl = null;
|
||||
this._sid = null;
|
||||
this._msisdn = null;
|
||||
this._tokenBox = null;
|
||||
|
@ -442,6 +443,7 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
this.props.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
).then((result) => {
|
||||
this._submitUrl = result.submit_url;
|
||||
this._sid = result.sid;
|
||||
this._msisdn = result.msisdn;
|
||||
});
|
||||
|
@ -453,45 +455,52 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onFormSubmit: function(e) {
|
||||
_onFormSubmit: async function(e) {
|
||||
e.preventDefault();
|
||||
if (this.state.token == '') return;
|
||||
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
|
||||
this.props.matrixClient.submitMsisdnToken(
|
||||
this._sid, this.props.clientSecret, this.state.token,
|
||||
).then((result) => {
|
||||
if (result.success) {
|
||||
const idServerParsedUrl = url.parse(
|
||||
this.props.matrixClient.getIdentityServerUrl(),
|
||||
try {
|
||||
let result;
|
||||
if (this._submitUrl) {
|
||||
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
||||
this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
|
||||
);
|
||||
} else {
|
||||
result = await this.props.matrixClient.submitMsisdnToken(
|
||||
this._sid, this.props.clientSecret, this.state.token,
|
||||
);
|
||||
}
|
||||
if (result.success) {
|
||||
const creds = {
|
||||
sid: this._sid,
|
||||
client_secret: this.props.clientSecret,
|
||||
};
|
||||
if (await this.props.matrixClient.doesServerRequireIdServerParam()) {
|
||||
const idServerParsedUrl = url.parse(
|
||||
this.props.matrixClient.getIdentityServerUrl(),
|
||||
);
|
||||
creds.id_server = idServerParsedUrl.host;
|
||||
}
|
||||
this.props.submitAuthDict({
|
||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/riot-web/issues/10312
|
||||
threepid_creds: {
|
||||
sid: this._sid,
|
||||
client_secret: this.props.clientSecret,
|
||||
id_server: idServerParsedUrl.host,
|
||||
},
|
||||
threepidCreds: {
|
||||
sid: this._sid,
|
||||
client_secret: this.props.clientSecret,
|
||||
id_server: idServerParsedUrl.host,
|
||||
},
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: _t("Token incorrect"),
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
} catch (e) {
|
||||
this.props.fail(e);
|
||||
console.log("Failed to submit msisdn token");
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -19,7 +19,9 @@ import createReactClass from 'create-react-class';
|
|||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import withValidation from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
|
@ -27,47 +29,164 @@ export default createReactClass({
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
getInitialState() {
|
||||
const config = SdkConfig.get();
|
||||
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
|
||||
this.defaultNoFederate = config.default_federate === false;
|
||||
return {
|
||||
isPublic: false,
|
||||
name: "",
|
||||
topic: "",
|
||||
alias: "",
|
||||
detailsOpen: false,
|
||||
noFederate: config.default_federate === false,
|
||||
nameIsValid: false,
|
||||
};
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked);
|
||||
_roomCreateOptions() {
|
||||
const createOpts = {};
|
||||
createOpts.name = this.state.name;
|
||||
if (this.state.isPublic) {
|
||||
createOpts.visibility = "public";
|
||||
createOpts.preset = "public_chat";
|
||||
// to prevent createRoom from enabling guest access
|
||||
createOpts['initial_state'] = [];
|
||||
const {alias} = this.state;
|
||||
const localPart = alias.substr(1, alias.indexOf(":") - 1);
|
||||
createOpts['room_alias_name'] = localPart;
|
||||
}
|
||||
if (this.state.topic) {
|
||||
createOpts.topic = this.state.topic;
|
||||
}
|
||||
if (this.state.noFederate) {
|
||||
createOpts.creation_content = {'m.federate': false};
|
||||
}
|
||||
return createOpts;
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this._detailsRef.addEventListener("toggle", this.onDetailsToggled);
|
||||
// move focus to first field when showing dialog
|
||||
this._nameFieldRef.focus();
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this._detailsRef.removeEventListener("toggle", this.onDetailsToggled);
|
||||
},
|
||||
|
||||
onOk: async function() {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
await this._nameFieldRef.validate({allowEmpty: false});
|
||||
if (this._aliasFieldRef) {
|
||||
await this._aliasFieldRef.validate({allowEmpty: false});
|
||||
}
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
if (this.state.nameIsValid && (!this._aliasFieldRef || this._aliasFieldRef.isValid)) {
|
||||
this.props.onFinished(true, this._roomCreateOptions());
|
||||
} else {
|
||||
let field;
|
||||
if (!this.state.nameIsValid) {
|
||||
field = this._nameFieldRef;
|
||||
} else if (this._aliasFieldRef && !this._aliasFieldRef.isValid) {
|
||||
field = this._aliasFieldRef;
|
||||
}
|
||||
if (field) {
|
||||
field.focus();
|
||||
field.validate({ allowEmpty: false, focused: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
onNameChange(ev) {
|
||||
this.setState({name: ev.target.value});
|
||||
},
|
||||
|
||||
onTopicChange(ev) {
|
||||
this.setState({topic: ev.target.value});
|
||||
},
|
||||
|
||||
onPublicChange(isPublic) {
|
||||
this.setState({isPublic});
|
||||
},
|
||||
|
||||
onAliasChange(alias) {
|
||||
this.setState({alias});
|
||||
},
|
||||
|
||||
onDetailsToggled(ev) {
|
||||
this.setState({detailsOpen: ev.target.open});
|
||||
},
|
||||
|
||||
onNoFederateChange(noFederate) {
|
||||
this.setState({noFederate});
|
||||
},
|
||||
|
||||
collectDetailsRef(ref) {
|
||||
this._detailsRef = ref;
|
||||
},
|
||||
|
||||
async onNameValidate(fieldState) {
|
||||
const result = await this._validateRoomName(fieldState);
|
||||
this.setState({nameIsValid: result.valid});
|
||||
return result;
|
||||
},
|
||||
|
||||
_validateRoomName: withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: async ({ value }) => !!value,
|
||||
invalid: () => _t("Please enter a name for the room"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
||||
|
||||
let privateLabel;
|
||||
let publicLabel;
|
||||
let aliasField;
|
||||
if (this.state.isPublic) {
|
||||
publicLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
aliasField = (
|
||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
|
||||
}
|
||||
|
||||
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
|
||||
return (
|
||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||
title={_t('Create Room')}
|
||||
title={title}
|
||||
>
|
||||
<form onSubmit={this.onOk}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateRoomDialog_label">
|
||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||
</div>
|
||||
<div className="mx_CreateRoomDialog_input_container">
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<details className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
||||
<div>
|
||||
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
||||
<label htmlFor="checkbox">
|
||||
{ _t('Block users on other matrix homeservers from joining this room') }
|
||||
<br />
|
||||
({ _t('This setting cannot be changed later!') })
|
||||
</label>
|
||||
</div>
|
||||
<Field id="name" ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
|
||||
<Field id="topic" label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} />
|
||||
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
|
||||
{ privateLabel }
|
||||
{ publicLabel }
|
||||
{ aliasField }
|
||||
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
|
||||
<LabelledToggleSwitch label={ _t('Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)')} onChange={this.onNoFederateChange} value={this.state.noFederate} />
|
||||
</details>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -41,6 +41,8 @@ export default class Field extends React.PureComponent {
|
|||
value: PropTypes.string.isRequired,
|
||||
// Optional component to include inside the field before the input.
|
||||
prefix: PropTypes.node,
|
||||
// Optional component to include inside the field after the input.
|
||||
postfix: PropTypes.node,
|
||||
// The callback called whenever the contents of the field
|
||||
// changes. Returns an object with `valid` boolean field
|
||||
// and a `feedback` react component field to provide feedback
|
||||
|
@ -54,6 +56,8 @@ export default class Field extends React.PureComponent {
|
|||
// If specified alongside tooltipContent, the class name to apply to the
|
||||
// tooltip itself.
|
||||
tooltipClassName: PropTypes.string,
|
||||
// If specified, an additional class name to apply to the field container
|
||||
className: PropTypes.string,
|
||||
// All other props pass through to the <input>.
|
||||
};
|
||||
|
||||
|
@ -143,8 +147,8 @@ export default class Field extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const {
|
||||
element, prefix, onValidate, children, tooltipContent, flagInvalid,
|
||||
tooltipClassName, ...inputProps} = this.props;
|
||||
element, prefix, postfix, className, onValidate, children,
|
||||
tooltipContent, flagInvalid, tooltipClassName, ...inputProps} = this.props;
|
||||
|
||||
const inputElement = element || "input";
|
||||
|
||||
|
@ -163,9 +167,13 @@ export default class Field extends React.PureComponent {
|
|||
if (prefix) {
|
||||
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
|
||||
}
|
||||
let postfixContainer = null;
|
||||
if (postfix) {
|
||||
postfixContainer = <span className="mx_Field_postfix">{postfix}</span>;
|
||||
}
|
||||
|
||||
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
|
||||
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, {
|
||||
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, {
|
||||
// If we have a prefix element, leave the label always at the top left and
|
||||
// don't animate it, as it looks a bit clunky and would add complexity to do
|
||||
// properly.
|
||||
|
@ -192,6 +200,7 @@ export default class Field extends React.PureComponent {
|
|||
{prefixContainer}
|
||||
{fieldInput}
|
||||
<label htmlFor={this.props.id}>{this.props.label}</label>
|
||||
{postfixContainer}
|
||||
{fieldTooltip}
|
||||
</div>;
|
||||
}
|
||||
|
|
125
src/components/views/elements/RoomAliasField.js
Normal file
125
src/components/views/elements/RoomAliasField.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
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 { _t } from '../../../languageHandler';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import withValidation from './Validation';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
export default class RoomAliasField extends React.PureComponent {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
domain: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isValid: true};
|
||||
}
|
||||
|
||||
_asFullAlias(localpart) {
|
||||
return `#${localpart}:${this.props.domain}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
const poundSign = (<span>#</span>);
|
||||
const aliasPostfix = ":" + this.props.domain;
|
||||
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
|
||||
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
||||
return (
|
||||
<Field
|
||||
label={_t("Room alias")}
|
||||
className="mx_RoomAliasField"
|
||||
prefix={poundSign}
|
||||
postfix={domain}
|
||||
id={this.props.id}
|
||||
ref={ref => this._fieldRef = ref}
|
||||
onValidate={this._onValidate}
|
||||
placeholder={_t("e.g. my-room")}
|
||||
onChange={this._onChange}
|
||||
maxLength={maxlength} />
|
||||
);
|
||||
}
|
||||
|
||||
_onChange = (ev) => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this._asFullAlias(ev.target.value));
|
||||
}
|
||||
}
|
||||
|
||||
_onValidate = async (fieldState) => {
|
||||
const result = await this._validationRules(fieldState);
|
||||
this.setState({isValid: result.valid});
|
||||
return result;
|
||||
};
|
||||
|
||||
_validationRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "safeLocalpart",
|
||||
test: async ({ value }) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
const fullAlias = this._asFullAlias(value);
|
||||
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
|
||||
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
|
||||
encodeURI(fullAlias) === fullAlias;
|
||||
},
|
||||
invalid: () => _t("Some characters not allowed"),
|
||||
}, {
|
||||
key: "required",
|
||||
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Please provide a room alias"),
|
||||
}, {
|
||||
key: "taken",
|
||||
test: async ({value}) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
const client = MatrixClientPeg.get();
|
||||
try {
|
||||
await client.getRoomIdForAlias(this._asFullAlias(value));
|
||||
// we got a room id, so the alias is taken
|
||||
return false;
|
||||
} catch (err) {
|
||||
// any server error code will do,
|
||||
// either it M_NOT_FOUND or the alias is invalid somehow,
|
||||
// in which case we don't want to show the invalid message
|
||||
return !!err.errcode;
|
||||
}
|
||||
},
|
||||
valid: () => _t("This alias is available to use"),
|
||||
invalid: () => _t("This alias is already in use"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
get isValid() {
|
||||
return this.state.isValid;
|
||||
}
|
||||
|
||||
validate(options) {
|
||||
return this._fieldRef.validate(options);
|
||||
}
|
||||
|
||||
focus() {
|
||||
this._fieldRef.focus();
|
||||
}
|
||||
}
|
|
@ -169,9 +169,32 @@ export default class BasicMessageEditor extends React.Component {
|
|||
|
||||
_onCompositionEnd = (event) => {
|
||||
this._isIMEComposing = false;
|
||||
// some browsers (chromium) don't fire an input event after ending a composition
|
||||
// so trigger a model update after the composition is done by calling the input handler
|
||||
this._onInput({inputType: "insertCompositionText"});
|
||||
// some browsers (Chrome) don't fire an input event after ending a composition,
|
||||
// so trigger a model update after the composition is done by calling the input handler.
|
||||
|
||||
// however, modifying the DOM (caused by the editor model update) from the compositionend handler seems
|
||||
// to confuse the IME in Chrome, likely causing https://github.com/vector-im/riot-web/issues/10913 ,
|
||||
// so we do it async
|
||||
|
||||
// however, doing this async seems to break things in Safari for some reason, so browser sniff.
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isSafari = ua.includes('safari/') && !ua.includes('chrome/');
|
||||
|
||||
if (isSafari) {
|
||||
this._onInput({inputType: "insertCompositionText"});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this._onInput({inputType: "insertCompositionText"});
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
isComposing(event) {
|
||||
// checking the event.isComposing flag just in case any browser out there
|
||||
// emits events related to the composition after compositionend
|
||||
// has been fired
|
||||
return !!(this._isIMEComposing || (event.nativeEvent && event.nativeEvent.isComposing));
|
||||
}
|
||||
|
||||
_onPaste = (event) => {
|
||||
|
|
|
@ -127,6 +127,10 @@ export default class EditMessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
_onKeyDown = (event) => {
|
||||
// ignore any keypress while doing IME compositions
|
||||
if (this._editorRef.isComposing(event)) {
|
||||
return;
|
||||
}
|
||||
if (event.metaKey || event.altKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -104,6 +104,10 @@ export default class SendMessageComposer extends React.Component {
|
|||
};
|
||||
|
||||
_onKeyDown = (event) => {
|
||||
// ignore any keypress while doing IME compositions
|
||||
if (this._editorRef.isComposing(event)) {
|
||||
return;
|
||||
}
|
||||
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
if (event.key === "Enter" && !hasModifier) {
|
||||
this._sendMessage();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue