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();
+ }
+}