diff --git a/res/css/_common.scss b/res/css/_common.scss index 859c0006a19..8252d5930e5 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -281,6 +281,12 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { box-shadow: 2px 15px 30px 0 $dialog-shadow-color; border-radius: 4px; overflow-y: auto; + + a:link, + a:hover, + a:visited { + @mixin mx_Dialog_link; + } } .mx_Dialog_fixedWidth { diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 49a87d8077f..b05629003ef 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -39,8 +39,7 @@ limitations under the License. a:link, a:hover, a:visited { - color: $accent-color; - text-decoration: none; + @mixin mx_Dialog_link; } input[type=text], diff --git a/res/css/views/dialogs/_AddressPickerDialog.scss b/res/css/views/dialogs/_AddressPickerDialog.scss index 2771ac4052d..168310507c0 100644 --- a/res/css/views/dialogs/_AddressPickerDialog.scss +++ b/res/css/views/dialogs/_AddressPickerDialog.scss @@ -67,3 +67,6 @@ limitations under the License. pointer-events: none; } +.mx_AddressPickerDialog_identityServer { + margin-top: 1em; +} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index f54d25ab29b..ef0b91b41a5 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -202,6 +202,11 @@ $interactive-tooltip-fg-color: #ffffff; background-color: $button-secondary-bg-color; } +@define-mixin mx_Dialog_link { + color: $accent-color; + text-decoration: none; +} + // Nasty hacks to apply a filter to arbitrary monochrome artwork to make it // better match the theme. Typically applied to dark grey 'off' buttons or // light grey 'on' buttons. diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index be46367fbb2..bfaac09761a 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -328,3 +328,8 @@ $interactive-tooltip-fg-color: #ffffff; color: $accent-color; background-color: $button-secondary-bg-color; } + +@define-mixin mx_Dialog_link { + color: $accent-color; + text-decoration: none; +} diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 7a3b59cef8b..856a2ca577e 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -51,7 +51,14 @@ export function showStartChatInviteDialog() { Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, { title: _t('Start a chat'), description: _t("Who would you like to communicate with?"), - placeholder: _t("Email, name or Matrix ID"), + placeholder: (validAddressTypes) => { + // The set of valid address type can be mutated inside the dialog + // when you first have no IS but agree to use one in the dialog. + if (validAddressTypes.includes('email')) { + return _t("Email, name or Matrix ID"); + } + return _t("Name or Matrix ID"); + }, validAddressTypes, button: _t("Start Chat"), onFinished: _onStartDmFinished, @@ -69,7 +76,14 @@ export function showRoomInviteDialog(roomId) { Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, { title: _t('Invite new room members'), button: _t('Send Invites'), - placeholder: _t("Email, name or Matrix ID"), + placeholder: (validAddressTypes) => { + // The set of valid address type can be mutated inside the dialog + // when you first have no IS but agree to use one in the dialog. + if (validAddressTypes.includes('email')) { + return _t("Email, name or Matrix ID"); + } + return _t("Name or Matrix ID"); + }, validAddressTypes, onFinished: (shouldInvite, addrs) => { _onRoomInviteFinished(roomId, shouldInvite, addrs); diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 364db97bf54..8f0a42198e3 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -24,11 +24,14 @@ import createReactClass from 'create-react-class'; import { _t, _td } from '../../../languageHandler'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import dis from '../../../dispatcher'; import Promise from 'bluebird'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStore from '../../../stores/GroupStore'; import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; +import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils'; +import { abbreviateUrl } from '../../../utils/UrlUtils'; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -49,7 +52,7 @@ module.exports = createReactClass({ // Extra node inserted after picker input, dropdown and errors extraNode: PropTypes.node, value: PropTypes.string, - placeholder: PropTypes.string, + placeholder: PropTypes.oneOfType(PropTypes.string, PropTypes.func), roomId: PropTypes.string, button: PropTypes.string, focus: PropTypes.bool, @@ -91,6 +94,9 @@ module.exports = createReactClass({ // List of UserAddressType objects representing the set of // auto-completion results for the current search query. suggestedList: [], + // List of address types initialised from props, but may change while the + // dialog is open. + validAddressTypes: this.props.validAddressTypes, }; }, @@ -101,6 +107,15 @@ module.exports = createReactClass({ } }, + getPlaceholder() { + const { placeholder } = this.props; + if (typeof placeholder === "string") { + return placeholder; + } + // Otherwise it's a function, as checked by prop types. + return placeholder(this.state.validAddressTypes); + }, + onButtonClick: function() { let selectedList = this.state.selectedList.slice(); // Check the text input field to see if user has an unconverted address @@ -434,7 +449,7 @@ module.exports = createReactClass({ // This is important, otherwise there's no way to invite // a perfectly valid address if there are close matches. const addrType = getAddressType(query); - if (this.props.validAddressTypes.includes(addrType)) { + if (this.state.validAddressTypes.includes(addrType)) { if (addrType === 'email' && !Email.looksValid(query)) { this.setState({searchError: _t("That doesn't look like a valid email address")}); return; @@ -470,7 +485,7 @@ module.exports = createReactClass({ isKnown: false, }; - if (!this.props.validAddressTypes.includes(addrType)) { + if (!this.state.validAddressTypes.includes(addrType)) { hasError = true; } else if (addrType === 'mx-user-id') { const user = MatrixClientPeg.get().getUser(addrObj.address); @@ -571,6 +586,24 @@ module.exports = createReactClass({ this._addAddressesToList(text.split(/[\s,]+/)); }, + onUseDefaultIdentityServerClick(e) { + e.preventDefault(); + + // Update the IS in account data. Actually using it may trigger terms. + useDefaultIdentityServer(); + + // Add email as a valid address type. + const { validAddressTypes } = this.state; + validAddressTypes.push('email'); + this.setState({ validAddressTypes }); + }, + + onManageSettingsClick(e) { + e.preventDefault(); + dis.dispatch({ action: 'view_user_settings' }); + this.onCancel(); + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -610,7 +643,7 @@ module.exports = createReactClass({ ref="textinput" className="mx_AddressPickerDialog_input" onChange={this.onQueryChanged} - placeholder={this.props.placeholder} + placeholder={this.getPlaceholder()} defaultValue={this.props.value} autoFocus={this.props.focus}> , @@ -621,7 +654,7 @@ module.exports = createReactClass({ let error; let addressSelector; if (this.state.invalidAddressError) { - const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t])); + const validTypeDescriptions = this.state.validAddressTypes.map((t) => _t(addressTypeName[t])); error =
{ _t("You have entered an invalid address.") }
@@ -644,6 +677,33 @@ module.exports = createReactClass({ ); } + let identityServer; + if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes('email')) { + const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); + if (defaultIdentityServerUrl) { + identityServer =
{_t( + "Use an identity server to invite by email. " + + "Use the default (%(defaultIdentityServerName)s) " + + "or manage in Settings.", + { + defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), + }, + { + default: sub => {sub}, + settings: sub => {sub}, + }, + )}
; + } else { + identityServer =
{_t( + "Use an identity server to invite by email. " + + "Manage in Settings.", + {}, { + settings: sub => {sub}, + }, + )}
; + } + } + return ( @@ -653,6 +713,7 @@ module.exports = createReactClass({ { error } { addressSelector } { this.props.extraNode } + { identityServer }
Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.", + "Use an identity server to invite by email. Manage in Settings.": "Use an identity server to invite by email. Manage in Settings.", "The following users may not exist": "The following users may not exist", "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?", "Invite anyway and never warn me again": "Invite anyway and never warn me again", diff --git a/src/utils/IdentityServerUtils.js b/src/utils/IdentityServerUtils.js new file mode 100644 index 00000000000..883bd521499 --- /dev/null +++ b/src/utils/IdentityServerUtils.js @@ -0,0 +1,30 @@ +/* +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. +*/ + +import SdkConfig from '../SdkConfig'; +import MatrixClientPeg from '../MatrixClientPeg'; + +export function getDefaultIdentityServerUrl() { + return SdkConfig.get()['validated_server_config']['isUrl']; +} + +export function useDefaultIdentityServer() { + const url = getDefaultIdentityServerUrl(); + // Account data change will update localstorage, client, etc through dispatcher + MatrixClientPeg.get().setAccountData("m.identity_server", { + base_url: url, + }); +}