From 9292a46db05d28bdb36e5fbe5ceb7fe3934b098c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Feb 2019 10:24:01 +0000 Subject: [PATCH 1/7] Update comment about Modular server type selection Modular now sets `disable_custom_urls`, so the server type selector is not shown for Modular-hosted Riot. --- src/components/views/auth/ServerTypeSelector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js index 7a28eec0ed..25f5dcee66 100644 --- a/src/components/views/auth/ServerTypeSelector.js +++ b/src/components/views/auth/ServerTypeSelector.js @@ -62,8 +62,8 @@ function getDefaultType(defaultHsUrl) { } 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 + // This is an unlikely case to reach, as Modular defaults to hiding the + // server type selector. return PREMIUM; } else { return ADVANCED; From b846ac580054ba0b0acf2baa9f09a959141df3b4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Feb 2019 11:23:36 +0000 Subject: [PATCH 2/7] Rework `ServerTypeSelector` to only emit changes after initial setup `ServerTypeSelector` would call its `onChange` prop both at construction (because it computed the default selected type and consumers might want to know) as well as on actual user change. This ended up complicating consumer code, as they want to differentiate between initial state and changes made by the user. To simplify things, `ServerTypeSelector` now exports a function to compute the server type from HS URL, which can be useful for setting its initially selected type. The consumer now provides that type via a prop, and `onChange` is now only called for actual user changes, simplifying the logic in `Registration` which uses `ServerTypeSelector`. In addition, some usages of `customHsUrl` vs. `defaultHsUrl` in `Registration` are simplified to be `customHsUrl` only (since it already includes a fallback to the default URL in `MatrixChat`). --- .../structures/auth/Registration.js | 24 ++++++----------- .../views/auth/ServerTypeSelector.js | 26 +++++++------------ 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6b578f0f68..04570df868 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -94,7 +94,7 @@ module.exports = React.createClass({ // If we've been given a session ID, we're resuming // straight back into UI auth doingUIAuth: Boolean(this.props.sessionId), - serverType: null, + serverType: ServerType.getTypeFromHsUrl(this.props.customHsUrl), hsUrl: this.props.customHsUrl, isUrl: this.props.customIsUrl, // Phase of the overall registration dialog. @@ -122,7 +122,7 @@ module.exports = React.createClass({ }); }, - onServerTypeChange(type, initial) { + onServerTypeChange(type) { this.setState({ serverType: type, }); @@ -148,15 +148,10 @@ module.exports = React.createClass({ hsUrl: this.props.defaultHsUrl, isUrl: this.props.defaultIsUrl, }); - // if this is the initial value from the control and we're - // already in the registration phase, don't go back to the - // server details phase (but do if it's actually a change resulting - // from user interaction). - if (!initial || !this.state.phase === PHASE_REGISTRATION) { - this.setState({ - phase: PHASE_SERVER_DETAILS, - }); - } + // Reset back to server details on type change. + this.setState({ + phase: PHASE_SERVER_DETAILS, + }); break; } }, @@ -389,12 +384,9 @@ module.exports = React.createClass({ // 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) { - // if we've been given a custom HS URL we should actually pass that, so - // that the appropriate section is selected at the start to match the - // homeserver URL we're using return
; @@ -436,7 +428,7 @@ module.exports = React.createClass({ return
{serverDetails} diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js index 25f5dcee66..a9311acd7b 100644 --- a/src/components/views/auth/ServerTypeSelector.js +++ b/src/components/views/auth/ServerTypeSelector.js @@ -56,12 +56,12 @@ export const TYPES = { }, }; -function getDefaultType(defaultHsUrl) { - if (!defaultHsUrl) { +export function getTypeFromHsUrl(hsUrl) { + if (!hsUrl) { return null; - } else if (defaultHsUrl === TYPES.FREE.hsUrl) { + } else if (hsUrl === TYPES.FREE.hsUrl) { return FREE; - } else if (new URL(defaultHsUrl).hostname.endsWith('.modular.im')) { + } else if (new URL(hsUrl).hostname.endsWith('.modular.im')) { // This is an unlikely case to reach, as Modular defaults to hiding the // server type selector. return PREMIUM; @@ -72,8 +72,8 @@ function getDefaultType(defaultHsUrl) { 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, + // The default selected type. + selected: PropTypes.string, // Handler called when the selected type changes. onChange: PropTypes.func.isRequired, } @@ -82,20 +82,12 @@ export default class ServerTypeSelector extends React.PureComponent { super(props); const { - defaultHsUrl, - onChange, + selected, } = props; - const type = getDefaultType(defaultHsUrl); + this.state = { - selected: type, + selected, }; - if (onChange) { - // FIXME: Supply a second 'initial' param here to flag that this is - // initialising the value rather than from user interaction - // (which sometimes we'll want to ignore). Must be a better way - // to do this. - onChange(type, true); - } } updateSelectedType(type) { From 91f56a4447492d428a757beca8e76ac883473600 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Feb 2019 12:14:03 +0000 Subject: [PATCH 3/7] Display default server name in registration If a default server name is set and the current HS URL is the default HS URL, we'll display that name in the "your account" text on the registration form. This can be a bit more user friendly, especially when the HS is delegated to somewhere such as Modular, since you'll then see "example.com" instead of "example.modular.im", which you have no direct relationship with as a user. This is the key bit of https://github.com/vector-im/riot-web/issues/8763 for registration. --- src/components/structures/MatrixChat.js | 1 + .../structures/auth/Registration.js | 20 ++++++++++++++++--- src/components/views/auth/RegistrationForm.js | 20 ++++++++++++++----- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8bc1fbdd07..b8d78fc447 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1887,6 +1887,7 @@ export default React.createClass({ sessionId={this.state.register_session_id} idSid={this.state.register_id_sid} email={this.props.startingFragmentQueryParams.email} + defaultServerName={this.getDefaultServerName()} defaultServerDiscoveryError={this.state.defaultServerDiscoveryError} defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 04570df868..f3c6306f79 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -48,15 +48,20 @@ module.exports = React.createClass({ sessionId: PropTypes.string, makeRegistrationUrl: PropTypes.func.isRequired, idSid: PropTypes.string, + // The default server name to use when the user hasn't specified + // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this + // via `.well-known` discovery. The server name is used instead of the + // HS URL when talking about "your account". + defaultServerName: PropTypes.string, + // An error passed along from higher up explaining that something + // went wrong when finding the defaultHsUrl. + defaultServerDiscoveryError: PropTypes.string, customHsUrl: PropTypes.string, customIsUrl: PropTypes.string, defaultHsUrl: PropTypes.string, defaultIsUrl: PropTypes.string, brand: PropTypes.string, email: PropTypes.string, - // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, // registration shouldn't know or care how login is done. onLoginClick: PropTypes.func.isRequired, onServerConfigChange: PropTypes.func.isRequired, @@ -470,6 +475,14 @@ module.exports = React.createClass({ ) { onEditServerDetailsClick = this.onEditServerDetailsClick; } + + // If the current HS URL is the default HS URL, then we can label it + // with the default HS name (if it exists). + let hsName; + if (this.state.hsUrl === this.props.defaultHsUrl) { + hsName = this.props.defaultServerName; + } + return ; } diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index acde4d03fe..910c72ef47 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -50,6 +50,10 @@ module.exports = React.createClass({ onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise onEditServerDetailsClick: PropTypes.func, flows: PropTypes.arrayOf(PropTypes.object).isRequired, + // This is optional and only set if we used a server name to determine + // the HS URL via `.well-known` discovery. The server name is used + // instead of the HS URL when talking about "your account". + hsName: PropTypes.string, hsUrl: PropTypes.string, }, @@ -296,13 +300,19 @@ module.exports = React.createClass({ render: function() { let yourMatrixAccountText = _t('Create your account'); - try { - const parsedHsUrl = new URL(this.props.hsUrl); + if (this.props.hsName) { yourMatrixAccountText = _t('Create your %(serverName)s account', { - serverName: parsedHsUrl.hostname, + serverName: this.props.hsName, }); - } catch (e) { - // ignore + } else { + try { + const parsedHsUrl = new URL(this.props.hsUrl); + yourMatrixAccountText = _t('Create your %(serverName)s account', { + serverName: parsedHsUrl.hostname, + }); + } catch (e) { + // ignore + } } let editLink = null; From f4b718008765a0a0a72ca60f15fcc79f2227319b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Feb 2019 13:40:30 +0000 Subject: [PATCH 4/7] Display default server name in login If a default server name is set and the current HS URL is the default HS URL, we'll display that name in the "sign in to" text on the login form. This can be a bit more user friendly, especially when the HS is delegated to somewhere such as Modular, since you'll then see "example.com" instead of "example.modular.im", which you have no direct relationship with as a user. This is the key bit of https://github.com/vector-im/riot-web/issues/8763 for login. --- src/components/structures/MatrixChat.js | 1 + src/components/structures/auth/Login.js | 23 ++++++++++++++++++---- src/components/views/auth/PasswordLogin.js | 22 ++++++++++++++++----- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b8d78fc447..7de4141192 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1924,6 +1924,7 @@ export default React.createClass({ diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 8db30c6d43..87fbc9c9fb 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -40,6 +40,10 @@ class PasswordLogin extends React.Component { initialPhoneNumber: "", initialPassword: "", loginIncorrect: false, + // This is optional and only set if we used a server name to determine + // the HS URL via `.well-known` discovery. The server name is used + // instead of the HS URL when talking about where to "sign in to". + hsName: null, hsUrl: "", disableSubmit: false, } @@ -250,13 +254,19 @@ class PasswordLogin extends React.Component { } let signInToText = _t('Sign in'); - try { - const parsedHsUrl = new URL(this.props.hsUrl); + if (this.props.hsName) { signInToText = _t('Sign in to %(serverName)s', { - serverName: parsedHsUrl.hostname, + serverName: this.props.hsName, }); - } catch (e) { - // ignore + } else { + try { + const parsedHsUrl = new URL(this.props.hsUrl); + signInToText = _t('Sign in to %(serverName)s', { + serverName: parsedHsUrl.hostname, + }); + } catch (e) { + // ignore + } } let editLink = null; @@ -338,6 +348,8 @@ PasswordLogin.propTypes = { onPhoneNumberChanged: PropTypes.func, onPasswordChanged: PropTypes.func, loginIncorrect: PropTypes.bool, + hsName: PropTypes.string, + hsUrl: PropTypes.string, disableSubmit: PropTypes.bool, }; From 5433feb4d4883c3693cc6118196724cf3bb9e42d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Feb 2019 13:57:21 +0000 Subject: [PATCH 5/7] Display default server name in forgot password If a default server name is set and the current HS URL is the default HS URL, we'll display that name in the "your account" text on the forgot password form. This can be a bit more user friendly, especially when the HS is delegated to somewhere such as Modular, since you'll then see "example.com" instead of "example.modular.im", which you have no direct relationship with as a user. This is the key bit of https://github.com/vector-im/riot-web/issues/8763 for forgot password. --- .../structures/auth/ForgotPassword.js | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 8eb1e9ce70..dad56b798c 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -41,20 +41,22 @@ module.exports = React.createClass({ displayName: 'ForgotPassword', propTypes: { + // The default server name to use when the user hasn't specified + // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this + // via `.well-known` discovery. The server name is used instead of the + // HS URL when talking about "your account". + defaultServerName: PropTypes.string, + // An error passed along from higher up explaining that something + // went wrong when finding the defaultHsUrl. + defaultServerDiscoveryError: PropTypes.string, + defaultHsUrl: PropTypes.string, defaultIsUrl: PropTypes.string, customHsUrl: PropTypes.string, customIsUrl: PropTypes.string, + onLoginClick: PropTypes.func, onComplete: PropTypes.func.isRequired, - - // The default server name to use when the user hasn't specified - // one. This is used when displaying the defaultHsUrl in the UI. - defaultServerName: PropTypes.string, - - // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, }, getInitialState: function() { @@ -235,18 +237,24 @@ module.exports = React.createClass({ } let yourMatrixAccountText = _t('Your account'); - try { - const parsedHsUrl = new URL(this.state.enteredHsUrl); + if (this.state.enteredHsUrl === this.props.defaultHsUrl) { yourMatrixAccountText = _t('Your account on %(serverName)s', { - serverName: parsedHsUrl.hostname, + serverName: this.props.defaultServerName, }); - } catch (e) { - errorText =
{_t( - "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " + - "enter a valid URL including the protocol prefix.", - { - hsUrl: this.state.enteredHsUrl, - })}
; + } else { + try { + const parsedHsUrl = new URL(this.state.enteredHsUrl); + yourMatrixAccountText = _t('Your account on %(serverName)s', { + serverName: parsedHsUrl.hostname, + }); + } catch (e) { + errorText =
{_t( + "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " + + "enter a valid URL including the protocol prefix.", + { + hsUrl: this.state.enteredHsUrl, + })}
; + } } // If custom URLs are allowed, wire up the server details edit link. From d220dd49ef73ce7032c4b756d43d360d1a28fd57 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Feb 2019 14:29:57 +0000 Subject: [PATCH 6/7] Clarify that the account is a Matrix account Now that auth flows can show a server name like `example.com` which might delegate the HS to some other server, it could be confusing to see text like "Sign in to example.com", especially if `example.com` runs an identity service, uses SSO, has its own account system, or other things like this. To clarify that we mean Matrix accounts, all auth flows are updated to talk in terms of " your Matrix account on ". Fixes part of https://github.com/vector-im/riot-web/issues/8763#issuecomment-464823909. --- src/components/structures/auth/ForgotPassword.js | 6 +++--- src/components/views/auth/PasswordLogin.js | 6 +++--- src/components/views/auth/RegistrationForm.js | 6 +++--- src/i18n/strings/en_EN.json | 12 +++++++----- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index dad56b798c..58deb380e3 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -236,15 +236,15 @@ module.exports = React.createClass({ errorText =
{ err }
; } - let yourMatrixAccountText = _t('Your account'); + let yourMatrixAccountText = _t('Your Matrix account'); if (this.state.enteredHsUrl === this.props.defaultHsUrl) { - yourMatrixAccountText = _t('Your account on %(serverName)s', { + yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { serverName: this.props.defaultServerName, }); } else { try { const parsedHsUrl = new URL(this.state.enteredHsUrl); - yourMatrixAccountText = _t('Your account on %(serverName)s', { + yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { serverName: parsedHsUrl.hostname, }); } catch (e) { diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 87fbc9c9fb..fb14629214 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -253,15 +253,15 @@ class PasswordLogin extends React.Component { ; } - let signInToText = _t('Sign in'); + let signInToText = _t('Sign in to your Matrix account'); if (this.props.hsName) { - signInToText = _t('Sign in to %(serverName)s', { + signInToText = _t('Sign in to your Matrix account on %(serverName)s', { serverName: this.props.hsName, }); } else { try { const parsedHsUrl = new URL(this.props.hsUrl); - signInToText = _t('Sign in to %(serverName)s', { + signInToText = _t('Sign in to your Matrix account on %(serverName)s', { serverName: parsedHsUrl.hostname, }); } catch (e) { diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 910c72ef47..056411033b 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -299,15 +299,15 @@ module.exports = React.createClass({ }, render: function() { - let yourMatrixAccountText = _t('Create your account'); + let yourMatrixAccountText = _t('Create your Matrix account'); if (this.props.hsName) { - yourMatrixAccountText = _t('Create your %(serverName)s account', { + yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { serverName: this.props.hsName, }); } else { try { const parsedHsUrl = new URL(this.props.hsUrl); - yourMatrixAccountText = _t('Create your %(serverName)s account', { + yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { serverName: parsedHsUrl.hostname, }); } catch (e) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0f5a875f0d..1b4dd35da8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1252,13 +1252,14 @@ "Username": "Username", "Mobile phone number": "Mobile phone number", "Not sure of your password? Set a new one": "Not sure of your password? Set a new one", - "Sign in to %(serverName)s": "Sign in to %(serverName)s", + "Sign in to your Matrix account": "Sign in to your Matrix account", + "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", "Change": "Change", "Sign in with": "Sign in with", "Phone": "Phone", "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?", - "Create your account": "Create your account", - "Create your %(serverName)s account": "Create your %(serverName)s account", + "Create your Matrix account": "Create your Matrix account", + "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", "Email": "Email", "Email (optional)": "Email (optional)", "Phone (optional)": "Phone (optional)", @@ -1408,8 +1409,8 @@ "A new password must be entered.": "A new password must be entered.", "New passwords must match each other.": "New passwords must match each other.", "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", - "Your account": "Your account", - "Your account on %(serverName)s": "Your account on %(serverName)s", + "Your Matrix account": "Your Matrix account", + "Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s", "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.": "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.", "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.", "Send Reset Email": "Send Reset Email", @@ -1453,6 +1454,7 @@ "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 username.": "You need to enter a username.", "An unknown error occurred.": "An unknown error occurred.", + "Create your account": "Create your account", "Commands": "Commands", "Results from DuckDuckGo": "Results from DuckDuckGo", "Emoji": "Emoji", From 42bb3c4f408767816be4700aded3db4d9658beee Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Feb 2019 16:00:13 +0000 Subject: [PATCH 7/7] Prevent default for forgot password link The forgot password link should prevent default to avoid changing the URL's hash state. --- src/components/views/auth/PasswordLogin.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index fb14629214..1ad93f6075 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -58,6 +58,7 @@ class PasswordLogin extends React.Component { loginType: PasswordLogin.LOGIN_FIELD_MXID, }; + this.onForgotPasswordClick = this.onForgotPasswordClick.bind(this); this.onSubmitForm = this.onSubmitForm.bind(this); this.onUsernameChanged = this.onUsernameChanged.bind(this); this.onUsernameBlur = this.onUsernameBlur.bind(this); @@ -74,6 +75,12 @@ class PasswordLogin extends React.Component { this._loginField = null; } + onForgotPasswordClick(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onForgotPasswordClick(); + } + onSubmitForm(ev) { ev.preventDefault(); @@ -244,7 +251,7 @@ class PasswordLogin extends React.Component { forgotPasswordJsx = {_t('Not sure of your password? Set a new one', {}, { a: sub => {sub}