From 8a1c1bbec4a1cf192b29f88da195b1a69a46b62e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Sep 2019 17:45:14 +0200 Subject: [PATCH] implement RoomAliasField component adding a postfix to Field to show the domain name --- res/css/_components.scss | 1 + res/css/views/elements/_Field.scss | 4 + res/css/views/elements/_RoomAliasField.scss | 56 ++++++++ src/components/views/elements/Field.js | 15 ++- .../views/elements/RoomAliasField.js | 125 ++++++++++++++++++ 5 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 res/css/views/elements/_RoomAliasField.scss create mode 100644 src/components/views/elements/RoomAliasField.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 213d0d714c..40a797dc15 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -99,6 +99,7 @@ @import "./views/elements/_ResizeHandle.scss"; @import "./views/elements/_RichText.scss"; @import "./views/elements/_RoleButton.scss"; +@import "./views/elements/_RoomAliasField.scss"; @import "./views/elements/_Spinner.scss"; @import "./views/elements/_SyntaxHighlight.scss"; @import "./views/elements/_TextWithTooltip.scss"; diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 0e8252e89d..da896f947d 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -31,6 +31,10 @@ limitations under the License. border-right: 1px solid $input-border-color; } +.mx_Field_postfix { + border-left: 1px solid $input-border-color; +} + .mx_Field input, .mx_Field select, .mx_Field textarea { diff --git a/res/css/views/elements/_RoomAliasField.scss b/res/css/views/elements/_RoomAliasField.scss new file mode 100644 index 0000000000..0fe53b2766 --- /dev/null +++ b/res/css/views/elements/_RoomAliasField.scss @@ -0,0 +1,56 @@ +/* +Copyright 2019 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. +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. +*/ + +.mx_RoomAliasField { + // if parent is a flex container, this allows the + // width to be as wide as needed, and not 100% + flex: 0 1 auto; + display: flex; + align-items: stretch; + min-width: 0; + max-width: 100%; + + input { + width: 150px; + padding-left: 0; + padding-right: 0; + } + + input::placeholder { + color: $greyed-fg-color; + font-weight: normal; + } + + .mx_Field_prefix, .mx_Field_postfix { + color: $greyed-fg-color; + border-left: none; + border-right: none; + font-weight: 600; + padding: 9px 10px; + flex: 0 0 auto; + } + + .mx_Field_postfix { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + // this allows the domain name to show + // as long as it doesn't make the input shrink + // if it's too big, it shows an ellipsis + // 180: 28 for prefix, 152 for input + max-width: calc(100% - 180px); + } +} diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 08a578b963..0a737d963a 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -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 . }; @@ -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 = {prefix}; } + let postfixContainer = null; + if (postfix) { + postfixContainer = {postfix}; + } 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} + {postfixContainer} {fieldTooltip} ; } diff --git a/src/components/views/elements/RoomAliasField.js b/src/components/views/elements/RoomAliasField.js new file mode 100644 index 0000000000..03f4000e59 --- /dev/null +++ b/src/components/views/elements/RoomAliasField.js @@ -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 = (#); + const aliasPostfix = ":" + this.props.domain; + const domain = ({aliasPostfix}); + const maxlength = 255 - this.props.domain.length - 2; // 2 for # and : + return ( + 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(); + } +}