diff --git a/res/css/_components.scss b/res/css/_components.scss index 953f12d73e..8aa2d70954 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -58,6 +58,7 @@ @import "./structures/_ViewSource.scss"; @import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; +@import "./structures/auth/_Registration.scss"; @import "./structures/auth/_SetupEncryptionBody.scss"; @import "./views/audio_messages/_AudioPlayer.scss"; @import "./views/audio_messages/_PlayPauseButton.scss"; diff --git a/res/css/structures/auth/_Registration.scss b/res/css/structures/auth/_Registration.scss new file mode 100644 index 0000000000..b415e78f10 --- /dev/null +++ b/res/css/structures/auth/_Registration.scss @@ -0,0 +1,53 @@ +/* +Copyright 2022 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_Register_mainContent { + display: flex; + flex-direction: column; + flex-grow: 1; + min-height: 270px; + + p { + font-size: $font-14px; + color: $authpage-primary-color; + + &.secondary { + color: $authpage-secondary-color; + } + } + + > img:first-child { + margin-bottom: 16px; + width: max-content; + } + + .mx_Login_submit { + margin-bottom: 0; + } +} + +.mx_Register_footerActions { + display: flex; + flex-direction: row; + justify-content: space-between; + padding-top: 16px; + margin-top: 16px; + border-top: 1px solid rgba(141, 151, 165, 0.2); + + > * { + flex-basis: content; + } +} diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 8c6b6bdff0..203e3ef61f 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -24,6 +24,11 @@ limitations under the License. padding: 25px 60px; box-sizing: border-box; + &.mx_AuthBody_flex { + display: flex; + flex-direction: column; + } + h2 { font-size: $font-24px; font-weight: 600; @@ -139,7 +144,6 @@ limitations under the License. .mx_AuthBody_changeFlow { display: block; text-align: center; - width: 100%; > a { font-weight: $font-semi-bold; diff --git a/res/css/views/auth/_AuthPage.scss b/res/css/views/auth/_AuthPage.scss index e3409792f0..b1399444df 100644 --- a/res/css/views/auth/_AuthPage.scss +++ b/res/css/views/auth/_AuthPage.scss @@ -28,10 +28,12 @@ limitations under the License. border-radius: 4px; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.33); background-color: $authpage-modal-bg-color; -} -@media only screen and (max-width: 480px) { - .mx_AuthPage_modal { + @media only screen and (max-height: 768px) { + margin-top: 50px; + } + + @media only screen and (max-width: 480px) { margin-top: 0; } } diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index a37683935a..43e0062eb3 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -14,35 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_InteractiveAuthEntryComponents_emailWrapper { - padding-right: 100px; - position: relative; - margin-top: 32px; - margin-bottom: 32px; - - &::before, &::after { - position: absolute; - width: 116px; - height: 116px; - content: ""; - right: -10px; - } - - &::before { - background-color: rgba(244, 246, 250, 0.91); - border-radius: 50%; - top: -20px; - } - - &::after { - background-image: url('$(res)/img/element-icons/email-prompt.svg'); - background-repeat: no-repeat; - background-position: center; - background-size: contain; - top: -25px; - } -} - .mx_InteractiveAuthEntryComponents_msisdnWrapper { text-align: center; } @@ -103,3 +74,21 @@ limitations under the License. margin-left: 5px; } } + +.mx_InteractiveAuthEntryComponents_emailWrapper { + // "Resend" button/link + .mx_AccessibleButton_kind_link_inline { + // We need this to be an inline-block so positioning works correctly + display: inline-block !important; + + // Spinner as end adornment of the "resend" button/link + .mx_Spinner { + // Spinners are usually block elements, but we need it as inline element + display: inline-flex !important; + // Spinners by default fill all available width, but we don't want that + width: auto !important; + // We need to center the spinner relative to the button/link + vertical-align: middle !important; + } + } +} diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 40073f8f64..893aa605f1 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -76,6 +76,11 @@ limitations under the License. border: 0; text-align: center; + &:not(.mx_Tooltip_noMargin) { + margin-left: 6px; + margin-right: 6px; + } + .mx_Tooltip_chevron { display: none; } diff --git a/res/img/element-icons/email-prompt.svg b/res/img/element-icons/email-prompt.svg index 19b8f82449..126fff6dd3 100644 --- a/res/img/element-icons/email-prompt.svg +++ b/res/img/element-icons/email-prompt.svg @@ -1,13 +1,6 @@ - - - - - - - - - - - - + + + + + diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 94c54b32e3..b42e65d57f 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -269,6 +269,7 @@ export default class InteractiveAuthComponent extends React.Component { { regDoneText } ; } else { - body =
-

{ _t('Create account') }

- { errorText } - { serverDeadSection } - - { this.renderRegisterComponent() } - { goBack } - { signIn } -
; + body = +
+ } + > + { errorText } + { serverDeadSection } + + { this.renderRegisterComponent() } +
+
+ { goBack } + { signIn } +
+
; } return ( - - { body } - + + + { body } + + ); } diff --git a/src/components/structures/auth/header/AuthHeaderContext.tsx b/src/components/structures/auth/header/AuthHeaderContext.tsx new file mode 100644 index 0000000000..347b26252d --- /dev/null +++ b/src/components/structures/auth/header/AuthHeaderContext.tsx @@ -0,0 +1,26 @@ +/* +Copyright 2022 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 { createContext, Dispatch, ReducerAction, ReducerState } from "react"; + +import type { AuthHeaderReducer } from "./AuthHeaderProvider"; + +interface AuthHeaderContextType { + state: ReducerState; + dispatch: Dispatch>; +} + +export const AuthHeaderContext = createContext(undefined); diff --git a/src/components/structures/auth/header/AuthHeaderDisplay.tsx b/src/components/structures/auth/header/AuthHeaderDisplay.tsx new file mode 100644 index 0000000000..fd5b65a1eb --- /dev/null +++ b/src/components/structures/auth/header/AuthHeaderDisplay.tsx @@ -0,0 +1,41 @@ +/* +Copyright 2022 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 React, { Fragment, PropsWithChildren, ReactNode, useContext } from "react"; + +import { AuthHeaderContext } from "./AuthHeaderContext"; + +interface Props { + title: ReactNode; + icon?: ReactNode; + serverPicker: ReactNode; +} + +export function AuthHeaderDisplay({ title, icon, serverPicker, children }: PropsWithChildren) { + const context = useContext(AuthHeaderContext); + if (!context) { + return null; + } + const current = context.state.length ? context.state[0] : null; + return ( + + { current?.icon ?? icon } +

{ current?.title ?? title }

+ { children } + { current?.hideServerPicker !== true && serverPicker } +
+ ); +} diff --git a/src/components/structures/auth/header/AuthHeaderModifier.tsx b/src/components/structures/auth/header/AuthHeaderModifier.tsx new file mode 100644 index 0000000000..a5646ff4f1 --- /dev/null +++ b/src/components/structures/auth/header/AuthHeaderModifier.tsx @@ -0,0 +1,39 @@ +/* +Copyright 2022 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 { ReactNode, useContext, useEffect } from "react"; + +import { AuthHeaderContext } from "./AuthHeaderContext"; +import { AuthHeaderActionType } from "./AuthHeaderProvider"; + +interface Props { + title: ReactNode; + icon?: ReactNode; + hideServerPicker?: boolean; +} + +export function AuthHeaderModifier(props: Props) { + const context = useContext(AuthHeaderContext); + const dispatch = context ? context.dispatch : null; + useEffect(() => { + if (!dispatch) { + return; + } + dispatch({ type: AuthHeaderActionType.Add, value: props }); + return () => dispatch({ type: AuthHeaderActionType.Remove, value: props }); + }, [props, dispatch]); + return null; +} diff --git a/src/components/structures/auth/header/AuthHeaderProvider.tsx b/src/components/structures/auth/header/AuthHeaderProvider.tsx new file mode 100644 index 0000000000..6c2bc5a750 --- /dev/null +++ b/src/components/structures/auth/header/AuthHeaderProvider.tsx @@ -0,0 +1,52 @@ +/* +Copyright 2022 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 { isEqual } from "lodash"; +import React, { ComponentProps, PropsWithChildren, Reducer, useReducer } from "react"; + +import { AuthHeaderContext } from "./AuthHeaderContext"; +import { AuthHeaderModifier } from "./AuthHeaderModifier"; + +export enum AuthHeaderActionType { + Add, + Remove +} + +interface AuthHeaderAction { + type: AuthHeaderActionType; + value: ComponentProps; +} + +export type AuthHeaderReducer = Reducer[], AuthHeaderAction>; + +export function AuthHeaderProvider({ children }: PropsWithChildren<{}>) { + const [state, dispatch] = useReducer( + (state: ComponentProps[], action: AuthHeaderAction) => { + switch (action.type) { + case AuthHeaderActionType.Add: + return [action.value, ...state]; + case AuthHeaderActionType.Remove: + return (state.length && isEqual(state[0], action.value)) ? state.slice(1) : state; + } + }, + [] as ComponentProps[], + ); + return ( + + { children } + + ); +} diff --git a/src/components/views/auth/AuthBody.tsx b/src/components/views/auth/AuthBody.tsx index 4532ceeaf4..cacab416f6 100644 --- a/src/components/views/auth/AuthBody.tsx +++ b/src/components/views/auth/AuthBody.tsx @@ -14,12 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import classNames from "classnames"; +import React, { PropsWithChildren } from 'react'; -export default class AuthBody extends React.PureComponent { - public render(): React.ReactNode { - return
- { this.props.children } -
; - } +interface Props { + flex?: boolean; +} + +export default function AuthBody({ flex, children }: PropsWithChildren) { + return
+ { children } +
; } diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 11a28d1e05..97c45999bd 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -14,18 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react'; import classNames from 'classnames'; import { MatrixClient } from "matrix-js-sdk/src/client"; import { AuthType, IAuthDict, IInputs, IStageStatus } from 'matrix-js-sdk/src/interactive-auth'; import { logger } from "matrix-js-sdk/src/logger"; +import React, { ChangeEvent, createRef, FormEvent, Fragment, MouseEvent } from 'react'; +import EmailPromptIcon from '../../../../res/img/element-icons/email-prompt.svg'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; -import AccessibleButton from "../elements/AccessibleButton"; -import Spinner from "../elements/Spinner"; import { LocalisedPolicy, Policies } from '../../../Terms'; +import { AuthHeaderModifier } from "../../structures/auth/header/AuthHeaderModifier"; +import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import Field from '../elements/Field'; +import Spinner from "../elements/Spinner"; +import { Alignment } from "../elements/Tooltip"; import CaptchaForm from "./CaptchaForm"; /* This file contains a collection of components which are used by the @@ -86,6 +90,7 @@ interface IAuthEntryProps { busy?: boolean; onPhaseChange: (phase: number) => void; submitAuthDict: (auth: IAuthDict) => void; + requestEmailToken?: () => Promise; } interface IPasswordAuthEntryState { @@ -205,7 +210,9 @@ export class RecaptchaAuthEntry extends React.Component; + return ( + + ); } let errorText = this.props.errorText; @@ -349,7 +356,9 @@ export class TermsAuthEntry extends React.Component; + return ( + + ); } const checkboxes = []; @@ -405,9 +414,24 @@ interface IEmailIdentityAuthEntryProps extends IAuthEntryProps { }; } -export class EmailIdentityAuthEntry extends React.Component { +interface IEmailIdentityAuthEntryState { + requested: boolean; + requesting: boolean; +} + +export class EmailIdentityAuthEntry extends + React.Component { static LOGIN_TYPE = AuthType.Email; + constructor(props: IEmailIdentityAuthEntryProps) { + super(props); + + this.state = { + requested: false, + requesting: false, + }; + } + componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); } @@ -440,11 +464,51 @@ export class EmailIdentityAuthEntry extends React.Component -

{ _t("A confirmation email has been sent to %(emailAddress)s", + } + hideServerPicker={true} + /> +

{ _t("To create your account, open the link in the email we just sent to %(emailAddress)s.", { emailAddress: { this.props.inputs.emailAddress } }, - ) } -

-

{ _t("Open the link in the email to continue registration.") }

+ ) }

+ { this.state.requesting ? ( +

{ _t("Did not receive it? Resend it", {}, { + a: (text: string) => + null} + disabled + >{ text } + , + }) }

+ ) :

{ _t("Did not receive it? Resend it", {}, { + a: (text: string) => this.setState({ requested: false }) + : undefined} + onClick={async () => { + this.setState({ requesting: true }); + try { + await this.props.requestEmailToken?.(); + } catch (e) { + logger.warn("Email token request failed: ", e); + } finally { + this.setState({ requested: true, requesting: false }); + } + }} + >{ text }, + }) }

} { errorSection } ); @@ -560,7 +624,9 @@ export class MsisdnAuthEntry extends React.Component; + return ( + + ); } else { const enableSubmit = Boolean(this.state.token); const submitClasses = classNames({ @@ -726,13 +792,15 @@ export class SSOAuthEntry extends React.Component - { errorSection } -
- { cancelButton } - { continueButton } -
- ; + return ( + + { errorSection } +
+ { cancelButton } + { continueButton } +
+
+ ); } } @@ -817,6 +885,7 @@ export interface IStageComponentProps extends IAuthEntryProps { fail?(e: Error): void; setEmailSid?(sid: string): void; onCancel?(): void; + requestEmailToken?(): Promise; } export interface IStageComponent extends React.ComponentClass> { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d3f1935d4d..6f051db481 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2976,8 +2976,11 @@ "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.", "Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies", "Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:", - "A confirmation email has been sent to %(emailAddress)s": "A confirmation email has been sent to %(emailAddress)s", - "Open the link in the email to continue registration.": "Open the link in the email to continue registration.", + "Check your email to continue": "Check your email to continue", + "Unread email icon": "Unread email icon", + "To create your account, open the link in the email we just sent to %(emailAddress)s.": "To create your account, open the link in the email we just sent to %(emailAddress)s.", + "Did not receive it? Resend it": "Did not receive it? Resend it", + "Resent!": "Resent!", "Token incorrect": "Token incorrect", "A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s", "Please enter the code it contains:": "Please enter the code it contains:",