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:",