Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f84ccda
Fix the service worker not loading in dev mode
sandhose Jan 9, 2025
5d69b34
frontend: simplify email list
sandhose Jan 9, 2025
8db3642
Add more stories for the account index page
sandhose Jan 9, 2025
ee33e9c
Remove the primary email address concept
sandhose Jan 9, 2025
75526ff
storage: new email authentication codes
sandhose Jan 9, 2025
1f83b39
Remove the dedicated page to add an email address
sandhose Jan 9, 2025
0513f19
Rip out the email verification codes
sandhose Jan 9, 2025
5f5fc44
Job to send the new email authentication codes
sandhose Jan 10, 2025
23b019c
GraphQL API to use the new email authentication codes
sandhose Jan 10, 2025
e0f4882
Use the new GraphQL APIs in the frontend to add emails
sandhose Jan 10, 2025
dbb5316
Data model and storage layer for storing user registrations
sandhose Jan 13, 2025
3da27af
Move the registration-related views into a sub-module
sandhose Jan 14, 2025
a294b37
Fix the post auth action being lost during the registration flow
sandhose Jan 14, 2025
0bedaf3
Make the password registration create a user_registration
sandhose Jan 14, 2025
f8517a5
Implement email verification in the registration flow
sandhose Jan 14, 2025
621b648
Check that the email isn't used during the registration process
sandhose Jan 14, 2025
36aa1a0
Move the finishing of registration to a dedicated view
sandhose Jan 15, 2025
f50a386
Registration step to set a display name
sandhose Jan 15, 2025
5851584
Link the registration to the browser through a signed cookie
sandhose Jan 15, 2025
02db622
Expire registration sessions after an hour
sandhose Jan 15, 2025
1ec6192
Check with the homeserver the username is still available before regi…
sandhose Jan 15, 2025
ef74d47
Cleanup the unverified emails from the database
sandhose Jan 15, 2025
6092efe
Merge branch 'main' into quenting/optional-email
sandhose Jan 20, 2025
ef077d0
Rate-limit email authentications
sandhose Jan 23, 2025
7e6ab8f
Disclose that email is already in use after verification
sandhose Jan 23, 2025
244ec18
Merge remote-tracking branch 'origin/main' into quenting/optional-email
sandhose Jan 23, 2025
a83cdfb
Clarify that VerifyEmailJob is kept for flushing old jobs
sandhose Jan 23, 2025
8d50088
Apply code style suggestion
sandhose Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions frontend/locales/en.json
Original file line number Diff line number Diff line change
@@ -247,24 +247,13 @@
"title": "Cannot find session: {{deviceId}}"
}
},
"unverified_email_alert": {
"button": "Review and verify",
"text:one": "You have {{count}} unverified email address.",
"text:other": "You have {{count}} unverified email addresses.",
"title": "Unverified email"
},
"user_email": {
"cant_delete_primary": "Choose a different primary email to delete this one.",
"delete_button_confirmation_modal": {
"action": "Delete email",
"body": "Delete this email?"
},
"delete_button_title": "Remove email address",
"email": "Email",
"make_primary_button": "Make primary",
"not_verified": "Not verified",
"primary_email": "Primary email",
"retry_button": "Resend code"
"email": "Email"
},
"user_email_list": {
"no_primary_email_alert": "No primary email address"

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

7 changes: 0 additions & 7 deletions frontend/src/components/UnverifiedEmailAlert/index.ts

This file was deleted.

73 changes: 5 additions & 68 deletions frontend/src/components/UserEmail/UserEmail.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
@@ -13,7 +13,6 @@ import { Translation, useTranslation } from "react-i18next";
import { type FragmentType, graphql, useFragment } from "../../gql";
import { graphqlRequest } from "../../graphql";
import { Close, Description, Dialog, Title } from "../Dialog";
import { Link } from "../Link";
import styles from "./UserEmail.module.css";

// This component shows a single user email address, with controls to verify it,
@@ -23,7 +22,6 @@ export const FRAGMENT = graphql(/* GraphQL */ `
fragment UserEmail_email on UserEmail {
id
email
confirmedAt
}
`);

@@ -45,20 +43,6 @@ const REMOVE_EMAIL_MUTATION = graphql(/* GraphQL */ `
}
`);

const SET_PRIMARY_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation SetPrimaryEmail($id: ID!) {
setPrimaryEmail(input: { userEmailId: $id }) {
status
user {
id
primaryEmail {
id
}
}
}
}
`);

const DeleteButton: React.FC<{ disabled?: boolean; onClick?: () => void }> = ({
disabled,
onClick,
@@ -123,24 +107,13 @@ const DeleteButtonWithConfirmation: React.FC<

const UserEmail: React.FC<{
email: FragmentType<typeof FRAGMENT>;
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
canRemove?: boolean;
onRemove?: () => void;
isPrimary?: boolean;
}> = ({ email, siteConfig, isPrimary, onRemove }) => {
}> = ({ email, canRemove, onRemove }) => {
const { t } = useTranslation();
const data = useFragment(FRAGMENT, email);
const { emailChangeAllowed } = useFragment(CONFIG_FRAGMENT, siteConfig);
const queryClient = useQueryClient();

const setPrimary = useMutation({
mutationFn: (id: string) =>
graphqlRequest({ query: SET_PRIMARY_EMAIL_MUTATION, variables: { id } }),
onSuccess: (_data) => {
queryClient.invalidateQueries({ queryKey: ["currentUserGreeting"] });
queryClient.invalidateQueries({ queryKey: ["userEmails"] });
},
});

const removeEmail = useMutation({
mutationFn: (id: string) =>
graphqlRequest({ query: REMOVE_EMAIL_MUTATION, variables: { id } }),
@@ -155,18 +128,10 @@ const UserEmail: React.FC<{
removeEmail.mutate(data.id);
};

const onSetPrimaryClick = (): void => {
setPrimary.mutate(data.id);
};

return (
<Form.Root>
<Form.Field name="email">
<Form.Label>
{isPrimary
? t("frontend.user_email.primary_email")
: t("frontend.user_email.email")}
</Form.Label>
<Form.Label>{t("frontend.user_email.email")}</Form.Label>

<div className="flex items-center gap-2">
<Form.TextControl
@@ -175,42 +140,14 @@ const UserEmail: React.FC<{
value={data.email}
className={styles.userEmailField}
/>
{!isPrimary && emailChangeAllowed && (
{canRemove && (
<DeleteButtonWithConfirmation
email={data.email}
disabled={removeEmail.isPending}
onClick={onRemoveClick}
/>
)}
</div>

{isPrimary && emailChangeAllowed && (
<Form.HelpMessage>
{t("frontend.user_email.cant_delete_primary")}
</Form.HelpMessage>
)}

{data.confirmedAt && !isPrimary && emailChangeAllowed && (
<Form.HelpMessage>
<button
type="button"
className={styles.link}
disabled={setPrimary.isPending}
onClick={onSetPrimaryClick}
>
{t("frontend.user_email.make_primary_button")}
</button>
</Form.HelpMessage>
)}

{!data.confirmedAt && (
<Form.ErrorMessage>
{t("frontend.user_email.not_verified")} |{" "}
<Link to="/emails/$id/verify" params={{ id: data.id }}>
{t("frontend.user_email.retry_button")}
</Link>
</Form.ErrorMessage>
)}
</Form.Field>
</Form.Root>
);
106 changes: 47 additions & 59 deletions frontend/src/components/UserProfile/UserEmailList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

import { useSuspenseQuery } from "@tanstack/react-query";
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
import { notFound } from "@tanstack/react-router";
import { useTransition } from "react";
import { type FragmentType, graphql, useFragment } from "../../gql";
import { graphqlRequest } from "../../graphql";
@@ -20,78 +21,64 @@ import UserEmail from "../UserEmail";

const QUERY = graphql(/* GraphQL */ `
query UserEmailList(
$userId: ID!
$first: Int
$after: String
$last: Int
$before: String
) {
user(id: $userId) {
id
emails(first: $first, after: $after, last: $last, before: $before) {
edges {
cursor
node {
id
...UserEmail_email
viewer {
__typename
... on User {
emails(first: $first, after: $after, last: $last, before: $before) {
edges {
cursor
node {
...UserEmail_email
}
}
totalCount
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
totalCount
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
`);

const FRAGMENT = graphql(/* GraphQL */ `
fragment UserEmailList_user on User {
id
primaryEmail {
id
}
}
`);
export const query = (pagination: AnyPagination = { first: 6 }) =>
queryOptions({
queryKey: ["userEmails", pagination],
queryFn: ({ signal }) =>
graphqlRequest({
query: QUERY,
variables: pagination,
signal,
}),
});

export const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
fragment UserEmailList_siteConfig on SiteConfig {
...UserEmail_siteConfig
emailChangeAllowed
}
`);

const UserEmailList: React.FC<{
user: FragmentType<typeof FRAGMENT>;
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
}> = ({ user, siteConfig }) => {
const data = useFragment(FRAGMENT, user);
const config = useFragment(CONFIG_FRAGMENT, siteConfig);
}> = ({ siteConfig }) => {
const { emailChangeAllowed } = useFragment(CONFIG_FRAGMENT, siteConfig);
const [pending, startTransition] = useTransition();

const [pagination, setPagination] = usePagination();
const result = useSuspenseQuery({
queryKey: ["userEmails", pagination],
queryFn: ({ signal }) =>
graphqlRequest({
query: QUERY,
variables: {
userId: data.id,
...(pagination as AnyPagination),
},
signal,
}),
});
const emails = result.data.user?.emails;
if (!emails) throw new Error();
const result = useSuspenseQuery(query(pagination));
if (result.data.viewer.__typename !== "User") throw notFound();
const emails = result.data.viewer.emails;

const [prevPage, nextPage] = usePages(pagination, emails.pageInfo);

const primaryEmailId = data.primaryEmail?.id;

const paginate = (pagination: Pagination): void => {
startTransition(() => {
setPagination(pagination);
@@ -105,22 +92,23 @@ const UserEmailList: React.FC<{
});
};

// Is it allowed to remove an email? If there's only one, we can't
const canRemove = emailChangeAllowed && emails.totalCount > 1;

return (
<>
{emails.edges.map((edge) =>
primaryEmailId === edge.node.id ? null : (
<UserEmail
email={edge.node}
key={edge.cursor}
siteConfig={config}
onRemove={onRemove}
/>
),
)}
{emails.edges.map((edge) => (
<UserEmail
email={edge.node}
key={edge.cursor}
canRemove={canRemove}
onRemove={onRemove}
/>
))}

<PaginationControls
autoHide
count={emails.totalCount ?? 0}
count={emails.totalCount}
onPrev={prevPage ? (): void => paginate(prevPage) : null}
onNext={nextPage ? (): void => paginate(nextPage) : null}
disabled={pending}
26 changes: 1 addition & 25 deletions frontend/src/components/VerifyEmail/VerifyEmail.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
@@ -26,18 +26,6 @@ const VERIFY_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation DoVerifyEmail($id: ID!, $code: String!) {
verifyEmail(input: { userEmailId: $id, code: $code }) {
status
user {
id
primaryEmail {
id
}
}
email {
id
...UserEmail_email
}
}
}
`);
@@ -46,18 +34,6 @@ const RESEND_VERIFICATION_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation ResendVerificationEmail($id: ID!) {
sendVerificationEmail(input: { userEmailId: $id }) {
status
user {
id
primaryEmail {
id
}
}
email {
id
...UserEmail_email
}
}
}
`);
43 changes: 14 additions & 29 deletions frontend/src/gql/gql.ts

Large diffs are not rendered by default.

210 changes: 41 additions & 169 deletions frontend/src/gql/graphql.ts

Large diffs are not rendered by default.

51 changes: 20 additions & 31 deletions frontend/src/routes/_account.index.lazy.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
@@ -10,15 +10,12 @@ import {
notFound,
useNavigate,
} from "@tanstack/react-router";
import { Alert, Separator, Text } from "@vector-im/compound-web";
import { Suspense } from "react";
import { Separator, Text } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";

import AccountManagementPasswordPreview from "../components/AccountManagementPasswordPreview";
import { ButtonLink } from "../components/ButtonLink";
import * as Collapsible from "../components/Collapsible";
import LoadingSpinner from "../components/LoadingSpinner";
import UserEmail from "../components/UserEmail";
import AddEmailForm from "../components/UserProfile/AddEmailForm";
import UserEmailList from "../components/UserProfile/UserEmailList";

@@ -43,46 +40,38 @@ function Index(): React.ReactElement {

return (
<div className="flex flex-col gap-4 mb-4">
<Collapsible.Section
defaultOpen
title={t("frontend.account.contact_info")}
>
{viewer.primaryEmail ? (
<UserEmail
email={viewer.primaryEmail}
isPrimary
siteConfig={siteConfig}
/>
) : (
<Alert
type="critical"
title={t("frontend.user_email_list.no_primary_email_alert")}
/>
)}
{/* Only display this section if the user can add email addresses to their
account *or* if they have any existing email addresses */}
{(siteConfig.emailChangeAllowed || viewer.emails.totalCount > 0) && (
<>
<Collapsible.Section
defaultOpen
title={t("frontend.account.contact_info")}
>
<UserEmailList siteConfig={siteConfig} />

<Suspense fallback={<LoadingSpinner mini className="self-center" />}>
<UserEmailList siteConfig={siteConfig} user={viewer} />
</Suspense>
{siteConfig.emailChangeAllowed && (
<AddEmailForm userId={viewer.id} onAdd={onAdd} />
)}
</Collapsible.Section>

{siteConfig.emailChangeAllowed && (
<AddEmailForm userId={viewer.id} onAdd={onAdd} />
)}
</Collapsible.Section>
<Separator kind="section" />
</>
)}

{siteConfig.passwordLoginEnabled && (
<>
<Separator kind="section" />
<Collapsible.Section
defaultOpen
title={t("frontend.account.account_password")}
>
<AccountManagementPasswordPreview siteConfig={siteConfig} />
</Collapsible.Section>

<Separator kind="section" />
</>
)}

<Separator kind="section" />

<Collapsible.Section title={t("common.e2ee")}>
<Text className="text-secondary" size="md">
{t("frontend.reset_cross_signing.description")}
17 changes: 10 additions & 7 deletions frontend/src/routes/_account.index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
@@ -8,6 +8,7 @@ import { queryOptions } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
import * as z from "zod";
import { query as userEmailListQuery } from "../components/UserProfile/UserEmailList";
import { graphql } from "../gql";
import { graphqlRequest } from "../graphql";

@@ -17,12 +18,10 @@ const QUERY = graphql(/* GraphQL */ `
__typename
... on User {
id
primaryEmail {
id
...UserEmail_email
}
...UserEmailList_user
emails(first: 0) {
totalCount
}
}
}
@@ -105,5 +104,9 @@ export const Route = createFileRoute("/_account/")({
}
},

loader: ({ context }) => context.queryClient.ensureQueryData(query),
loader: ({ context }) =>
Promise.all([
context.queryClient.ensureQueryData(userEmailListQuery()),
context.queryClient.ensureQueryData(query),
]),
});
3 changes: 0 additions & 3 deletions frontend/src/routes/_account.lazy.tsx
Original file line number Diff line number Diff line change
@@ -13,7 +13,6 @@ import Layout from "../components/Layout";
import NavBar from "../components/NavBar";
import NavItem from "../components/NavItem";
import EndSessionButton from "../components/Session/EndSessionButton";
import UnverifiedEmailAlert from "../components/UnverifiedEmailAlert";
import UserGreeting from "../components/UserGreeting";

import { useSuspenseQuery } from "@tanstack/react-query";
@@ -45,8 +44,6 @@ function Account(): React.ReactElement {
<div className="flex flex-col gap-4">
<UserGreeting user={session.user} siteConfig={siteConfig} />

<UnverifiedEmailAlert user={session.user} />

<NavBar>
<NavItem to="/">{t("frontend.nav.settings")}</NavItem>
<NavItem to="/sessions">{t("frontend.nav.devices")}</NavItem>
3 changes: 1 addition & 2 deletions frontend/src/routes/_account.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
@@ -18,7 +18,6 @@ const QUERY = graphql(/* GraphQL */ `
id
user {
...UnverifiedEmailAlert_user
...UserGreeting_user
}
}
57 changes: 27 additions & 30 deletions frontend/tests/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Copyright 2024, 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

import { HttpResponse } from "msw";
import { CONFIG_FRAGMENT as PASSWORD_CHANGE_CONFIG_FRAGMENT } from "../../src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview";
import { FRAGMENT as FOOTER_FRAGMENT } from "../../src/components/Footer/Footer";
import { UNVERIFIED_EMAILS_FRAGMENT } from "../../src/components/UnverifiedEmailAlert/UnverifiedEmailAlert";
import {
CONFIG_FRAGMENT as USER_EMAIL_CONFIG_FRAGMENT,
FRAGMENT as USER_EMAIL_FRAGMENT,
@@ -71,15 +75,6 @@ export const handlers = [
},
USER_GREETING_FRAGMENT,
),

makeFragmentData(
{
unverifiedEmails: {
totalCount: 0,
},
},
UNVERIFIED_EMAILS_FRAGMENT,
),
),
},

@@ -99,16 +94,8 @@ export const handlers = [
viewer: {
__typename: "User",
id: "user-id",
primaryEmail: {
id: "primary-email-id",
...makeFragmentData(
{
id: "primary-email-id",
email: "alice@example.com",
confirmedAt: new Date().toISOString(),
},
USER_EMAIL_FRAGMENT,
),
emails: {
totalCount: 1,
},
},

@@ -124,12 +111,9 @@ export const handlers = [
USER_EMAIL_CONFIG_FRAGMENT,
),
makeFragmentData(
makeFragmentData(
{
emailChangeAllowed: true,
},
USER_EMAIL_CONFIG_FRAGMENT,
),
{
emailChangeAllowed: true,
},
USER_EMAIL_LIST_CONFIG_FRAGMENT,
),
makeFragmentData(
@@ -146,11 +130,24 @@ export const handlers = [
mockUserEmailListQuery(() =>
HttpResponse.json({
data: {
user: {
id: "user-id",
viewer: {
__typename: "User",
emails: {
edges: [],
totalCount: 0,
edges: [
{
cursor: "primary-email-id",
node: {
...makeFragmentData(
{
id: "primary-email-id",
email: "alice@example.com",
},
USER_EMAIL_FRAGMENT,
),
},
},
],
totalCount: 1,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
120 changes: 46 additions & 74 deletions frontend/tests/routes/account/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -2,18 +2,18 @@

exports[`Account home page > display name edit box > displays an error if the display name is invalid 1`] = `
<div
aria-describedby="radix-:r79:"
aria-labelledby="radix-:r78:"
aria-describedby="radix-:r75:"
aria-labelledby="radix-:r74:"
class="_body_9cf7b0"
data-state="open"
id="radix-:r77:"
id="radix-:r73:"
role="dialog"
style="pointer-events: auto;"
tabindex="-1"
>
<h2
class="_title_9cf7b0"
id="radix-:r78:"
id="radix-:r74:"
>
Edit profile
</h2>
@@ -40,29 +40,29 @@ exports[`Account home page > display name edit box > displays an error if the di
<label
class="_label_ssths_67"
data-invalid="true"
for="radix-:r8h:"
for="radix-:r8c:"
>
Display name
</label>
<div
class="_container_1qov4_17"
id=":r8i:"
id=":r8d:"
>
<input
aria-describedby="radix-:r8o:"
aria-describedby="radix-:r8j:"
aria-invalid="true"
autocomplete="name"
class="_control_9gon8_18 _control_1qov4_22"
data-invalid="true"
id="radix-:r8h:"
id="radix-:r8c:"
name="displayname"
title=""
type="text"
value="Alice"
/>
<button
aria-controls=":r8i:"
aria-labelledby=":r8j:"
aria-controls=":r8d:"
aria-labelledby=":r8e:"
class="_action_1qov4_33"
type="button"
>
@@ -82,7 +82,7 @@ exports[`Account home page > display name edit box > displays an error if the di
</div>
<span
class="_message_ssths_93 _help-message_ssths_99"
id="radix-:r8o:"
id="radix-:r8j:"
>
This is what others will see wherever you’re signed in.
</span>
@@ -92,13 +92,13 @@ exports[`Account home page > display name edit box > displays an error if the di
>
<label
class="_label_ssths_67"
for="radix-:r8p:"
for="radix-:r8k:"
>
Username
</label>
<input
class="_control_9gon8_18"
id="radix-:r8p:"
id="radix-:r8k:"
name="mxid"
readonly=""
title=""
@@ -129,7 +129,7 @@ exports[`Account home page > display name edit box > displays an error if the di
Cancel
</button>
<button
aria-labelledby=":r8q:"
aria-labelledby=":r8l:"
class="_close_9cf7b0"
type="button"
>
@@ -150,18 +150,18 @@ exports[`Account home page > display name edit box > displays an error if the di

exports[`Account home page > display name edit box > lets edit the display name 1`] = `
<div
aria-describedby="radix-:r1i:"
aria-labelledby="radix-:r1h:"
aria-describedby="radix-:r1h:"
aria-labelledby="radix-:r1g:"
class="_body_9cf7b0"
data-state="open"
id="radix-:r1g:"
id="radix-:r1f:"
role="dialog"
style="pointer-events: auto;"
tabindex="-1"
>
<h2
class="_title_9cf7b0"
id="radix-:r1h:"
id="radix-:r1g:"
>
Edit profile
</h2>
@@ -186,27 +186,27 @@ exports[`Account home page > display name edit box > lets edit the display name
>
<label
class="_label_ssths_67"
for="radix-:r2q:"
for="radix-:r2o:"
>
Display name
</label>
<div
class="_container_1qov4_17"
id=":r2r:"
id=":r2p:"
>
<input
aria-describedby="radix-:r31:"
aria-describedby="radix-:r2v:"
autocomplete="name"
class="_control_9gon8_18 _control_1qov4_22"
id="radix-:r2q:"
id="radix-:r2o:"
name="displayname"
title=""
type="text"
value="Alice"
/>
<button
aria-controls=":r2r:"
aria-labelledby=":r2s:"
aria-controls=":r2p:"
aria-labelledby=":r2q:"
class="_action_1qov4_33"
type="button"
>
@@ -226,7 +226,7 @@ exports[`Account home page > display name edit box > lets edit the display name
</div>
<span
class="_message_ssths_93 _help-message_ssths_99"
id="radix-:r31:"
id="radix-:r2v:"
>
This is what others will see wherever you’re signed in.
</span>
@@ -236,13 +236,13 @@ exports[`Account home page > display name edit box > lets edit the display name
>
<label
class="_label_ssths_67"
for="radix-:r32:"
for="radix-:r30:"
>
Username
</label>
<input
class="_control_9gon8_18"
id="radix-:r32:"
id="radix-:r30:"
name="mxid"
readonly=""
title=""
@@ -273,7 +273,7 @@ exports[`Account home page > display name edit box > lets edit the display name
Cancel
</button>
<button
aria-labelledby=":r33:"
aria-labelledby=":r31:"
class="_close_9cf7b0"
type="button"
>
@@ -495,13 +495,12 @@ exports[`Account home page > renders the page 1`] = `
class="_label_ssths_67"
for="radix-:rj:"
>
Primary email
Email
</label>
<div
class="flex items-center gap-2"
>
<input
aria-describedby="radix-:rk:"
class="_control_9gon8_18 _userEmailField_e2a518"
id="radix-:rj:"
name="email"
@@ -511,35 +510,8 @@ exports[`Account home page > renders the page 1`] = `
value="alice@example.com"
/>
</div>
<span
class="_message_ssths_93 _help-message_ssths_99"
id="radix-:rk:"
>
Choose a different primary email to delete this one.
</span>
</div>
</form>
<div
aria-busy="true"
class="self-center _mini_0c7436"
role="alert"
>
<svg
class="_loadingSpinnerInner_0c7436"
fill="none"
role="img"
viewBox="0 0 100 101"
xmlns="http://www.w3.org/2000/svg"
>
<title>
Loading…
</title>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
<form
class="_root_ssths_24"
>
@@ -548,17 +520,17 @@ exports[`Account home page > renders the page 1`] = `
>
<label
class="_label_ssths_67"
for="radix-:rl:"
for="radix-:rk:"
>
Add email
</label>
<div
class="_controls_1h4nb_17"
>
<input
aria-describedby="radix-:rm:"
aria-describedby="radix-:rl:"
class="_control_9gon8_18"
id="radix-:rl:"
id="radix-:rk:"
name="input"
required=""
title=""
@@ -567,7 +539,7 @@ exports[`Account home page > renders the page 1`] = `
</div>
<span
class="_message_ssths_93 _help-message_ssths_99"
id="radix-:rm:"
id="radix-:rl:"
>
Add an alternative email you can use to access this account.
</span>
@@ -582,7 +554,7 @@ exports[`Account home page > renders the page 1`] = `
role="separator"
/>
<section
aria-labelledby=":rn:"
aria-labelledby=":rm:"
class="_root_f1daaa"
data-state="open"
>
@@ -594,14 +566,14 @@ exports[`Account home page > renders the page 1`] = `
>
<h4
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102 _triggerTitle_f1daaa"
id=":rn:"
id=":rm:"
>
Account password
</h4>
<button
aria-controls="radix-:rp:"
aria-controls="radix-:ro:"
aria-expanded="true"
aria-labelledby=":rq:"
aria-labelledby=":rp:"
class="_icon-button_bh2qc_17 _triggerIcon_f1daaa"
data-state="open"
role="button"
@@ -631,7 +603,7 @@ exports[`Account home page > renders the page 1`] = `
<article
class="_content_f1daaa"
data-state="open"
id="radix-:rp:"
id="radix-:ro:"
style="transition-duration: 0s; animation-name: none;"
>
<form
@@ -642,14 +614,14 @@ exports[`Account home page > renders the page 1`] = `
>
<label
class="_label_ssths_67"
for="radix-:rv:"
for="radix-:ru:"
>
Password
</label>
<input
aria-describedby="radix-:r10:"
aria-describedby="radix-:rv:"
class="_control_9gon8_18"
id="radix-:rv:"
id="radix-:ru:"
name="password_preview"
readonly=""
title=""
@@ -658,7 +630,7 @@ exports[`Account home page > renders the page 1`] = `
/>
<span
class="_message_ssths_93 _help-message_ssths_99"
id="radix-:r10:"
id="radix-:rv:"
>
<a
class="_link_7634c3"
@@ -678,7 +650,7 @@ exports[`Account home page > renders the page 1`] = `
role="separator"
/>
<section
aria-labelledby=":r11:"
aria-labelledby=":r10:"
class="_root_f1daaa"
data-state="closed"
>
@@ -690,14 +662,14 @@ exports[`Account home page > renders the page 1`] = `
>
<h4
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102 _triggerTitle_f1daaa"
id=":r11:"
id=":r10:"
>
End-to-end encryption
</h4>
<button
aria-controls="radix-:r13:"
aria-controls="radix-:r12:"
aria-expanded="false"
aria-labelledby=":r14:"
aria-labelledby=":r13:"
class="_icon-button_bh2qc_17 _triggerIcon_f1daaa"
data-state="closed"
role="button"