frontend: simplify email list
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/* Copyright 2024 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.
|
||||
*/
|
||||
|
||||
.alert > * {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { makeFragmentData } from "../../gql/fragment-masking";
|
||||
import { DummyRouter } from "../../test-utils/router";
|
||||
|
||||
import UnverifiedEmailAlert, {
|
||||
UNVERIFIED_EMAILS_FRAGMENT,
|
||||
} from "./UnverifiedEmailAlert";
|
||||
|
||||
describe("<UnverifiedEmailAlert />", () => {
|
||||
it("does not render a warning when there are no unverified emails", () => {
|
||||
const data = makeFragmentData(
|
||||
{
|
||||
id: "abc123",
|
||||
unverifiedEmails: {
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
UNVERIFIED_EMAILS_FRAGMENT,
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<DummyRouter>
|
||||
<UnverifiedEmailAlert user={data} />
|
||||
</DummyRouter>,
|
||||
);
|
||||
|
||||
expect(container).toMatchInlineSnapshot("<div />");
|
||||
});
|
||||
|
||||
it("renders a warning when there are unverified emails", () => {
|
||||
const data = makeFragmentData(
|
||||
{
|
||||
id: "abc123",
|
||||
unverifiedEmails: {
|
||||
totalCount: 2,
|
||||
},
|
||||
},
|
||||
UNVERIFIED_EMAILS_FRAGMENT,
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<DummyRouter>
|
||||
<UnverifiedEmailAlert user={data} />
|
||||
</DummyRouter>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("hides warning after it has been dismissed", () => {
|
||||
const data = makeFragmentData(
|
||||
{
|
||||
id: "abc123",
|
||||
unverifiedEmails: {
|
||||
totalCount: 2,
|
||||
},
|
||||
},
|
||||
UNVERIFIED_EMAILS_FRAGMENT,
|
||||
);
|
||||
|
||||
const { container, getByText, getByLabelText } = render(
|
||||
<DummyRouter>
|
||||
<UnverifiedEmailAlert user={data} />
|
||||
</DummyRouter>,
|
||||
);
|
||||
|
||||
// warning is rendered
|
||||
expect(getByText("Unverified email")).toBeTruthy();
|
||||
|
||||
fireEvent.click(getByLabelText("Close"));
|
||||
|
||||
// no more warning
|
||||
expect(container).toMatchInlineSnapshot("<div />");
|
||||
});
|
||||
|
||||
it("hides warning when count of unverified emails becomes 0", () => {
|
||||
const data = makeFragmentData(
|
||||
{
|
||||
id: "abc123",
|
||||
unverifiedEmails: {
|
||||
totalCount: 2,
|
||||
},
|
||||
},
|
||||
UNVERIFIED_EMAILS_FRAGMENT,
|
||||
);
|
||||
|
||||
const { container, getByText, rerender } = render(
|
||||
<DummyRouter>
|
||||
<UnverifiedEmailAlert user={data} />
|
||||
</DummyRouter>,
|
||||
);
|
||||
|
||||
// warning is rendered
|
||||
expect(getByText("Unverified email")).toBeTruthy();
|
||||
|
||||
const newData = makeFragmentData(
|
||||
{
|
||||
id: "abc123",
|
||||
unverifiedEmails: {
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
UNVERIFIED_EMAILS_FRAGMENT,
|
||||
);
|
||||
rerender(
|
||||
<DummyRouter>
|
||||
<UnverifiedEmailAlert user={newData} />
|
||||
</DummyRouter>,
|
||||
);
|
||||
|
||||
// warning removed
|
||||
expect(container).toMatchInlineSnapshot("<div />");
|
||||
});
|
||||
|
||||
it("shows a dismissed warning again when there are new unverified emails", () => {
|
||||
const data = makeFragmentData(
|
||||
{
|
||||
id: "abc123",
|
||||
unverifiedEmails: {
|
||||
totalCount: 2,
|
||||
},
|
||||
},
|
||||
UNVERIFIED_EMAILS_FRAGMENT,
|
||||
);
|
||||
|
||||
const { container, getByText, getByLabelText, rerender } = render(
|
||||
<DummyRouter>
|
||||
<UnverifiedEmailAlert user={data} />
|
||||
</DummyRouter>,
|
||||
);
|
||||
|
||||
// warning is rendered
|
||||
expect(getByText("Unverified email")).toBeTruthy();
|
||||
|
||||
fireEvent.click(getByLabelText("Close"));
|
||||
|
||||
// no more warning
|
||||
expect(container).toMatchInlineSnapshot("<div />");
|
||||
|
||||
const newData = makeFragmentData(
|
||||
{
|
||||
id: "abc123",
|
||||
unverifiedEmails: {
|
||||
totalCount: 3,
|
||||
},
|
||||
},
|
||||
UNVERIFIED_EMAILS_FRAGMENT,
|
||||
);
|
||||
rerender(
|
||||
<DummyRouter>
|
||||
<UnverifiedEmailAlert user={newData} />
|
||||
</DummyRouter>,
|
||||
);
|
||||
|
||||
// warning is rendered
|
||||
expect(getByText("Unverified email")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright 2024 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 { Alert } from "@vector-im/compound-web";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { type FragmentType, graphql, useFragment } from "../../gql";
|
||||
import { Link } from "../Link";
|
||||
|
||||
import styles from "./UnverifiedEmailAlert.module.css";
|
||||
|
||||
export const UNVERIFIED_EMAILS_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UnverifiedEmailAlert_user on User {
|
||||
unverifiedEmails: emails(first: 0, state: PENDING) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const UnverifiedEmailAlert: React.FC<{
|
||||
user: FragmentType<typeof UNVERIFIED_EMAILS_FRAGMENT>;
|
||||
}> = ({ user }) => {
|
||||
const data = useFragment(UNVERIFIED_EMAILS_FRAGMENT, user);
|
||||
const [dismiss, setDismiss] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const currentCount = useRef<number>(data.unverifiedEmails.totalCount);
|
||||
|
||||
const doDismiss = (): void => setDismiss(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentCount.current !== data.unverifiedEmails.totalCount) {
|
||||
currentCount.current = data.unverifiedEmails.totalCount;
|
||||
setDismiss(false);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (!data.unverifiedEmails.totalCount || dismiss) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
type="critical"
|
||||
title={t("frontend.unverified_email_alert.title")}
|
||||
onClose={doDismiss}
|
||||
className={styles.alert}
|
||||
>
|
||||
{t("frontend.unverified_email_alert.text", {
|
||||
count: data.unverifiedEmails.totalCount,
|
||||
})}{" "}
|
||||
<Link to="/" hash="emails">
|
||||
{t("frontend.unverified_email_alert.button")}
|
||||
</Link>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnverifiedEmailAlert;
|
||||
@@ -1,78 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<UnverifiedEmailAlert /> > renders a warning when there are unverified emails 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="_alert_1bz08_19 _alert_d86cd2"
|
||||
data-type="critical"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_1bz08_57"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="_content_1bz08_46"
|
||||
>
|
||||
<div
|
||||
class="_text-content_1bz08_53"
|
||||
>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64"
|
||||
>
|
||||
Unverified email
|
||||
</p>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
|
||||
>
|
||||
You have 2 unverified email addresses.
|
||||
|
||||
<a
|
||||
aria-current="page"
|
||||
class="_link_ue21z_17 active"
|
||||
data-kind="primary"
|
||||
data-size="medium"
|
||||
data-status="active"
|
||||
href="/#emails"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Review and verify
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
export { default } from "./UnverifiedEmailAlert";
|
||||
@@ -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,7 +140,7 @@ const UserEmail: React.FC<{
|
||||
value={data.email}
|
||||
className={styles.userEmailField}
|
||||
/>
|
||||
{!isPrimary && emailChangeAllowed && (
|
||||
{canRemove && (
|
||||
<DeleteButtonWithConfirmation
|
||||
email={data.email}
|
||||
disabled={removeEmail.isPending}
|
||||
@@ -183,34 +148,6 @@ const UserEmail: React.FC<{
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -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 CONFIG_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserEmailList_siteConfig on SiteConfig {
|
||||
...UserEmail_siteConfig
|
||||
}
|
||||
`);
|
||||
|
||||
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);
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const [pagination, setPagination] = usePagination();
|
||||
const result = useSuspenseQuery({
|
||||
export const query = (pagination: AnyPagination = { first: 6 }) =>
|
||||
queryOptions({
|
||||
queryKey: ["userEmails", pagination],
|
||||
queryFn: ({ signal }) =>
|
||||
graphqlRequest({
|
||||
query: QUERY,
|
||||
variables: {
|
||||
userId: data.id,
|
||||
...(pagination as AnyPagination),
|
||||
},
|
||||
variables: pagination,
|
||||
signal,
|
||||
}),
|
||||
});
|
||||
const emails = result.data.user?.emails;
|
||||
if (!emails) throw new Error();
|
||||
|
||||
export const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserEmailList_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
}
|
||||
`);
|
||||
|
||||
const UserEmailList: React.FC<{
|
||||
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
|
||||
}> = ({ siteConfig }) => {
|
||||
const { emailChangeAllowed } = useFragment(CONFIG_FRAGMENT, siteConfig);
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const [pagination, setPagination] = usePagination();
|
||||
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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -29,28 +29,25 @@ const documents = {
|
||||
"\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": types.BrowserSession_DetailFragmentDoc,
|
||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc,
|
||||
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc,
|
||||
"\n fragment UnverifiedEmailAlert_user on User {\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n": types.UnverifiedEmailAlert_UserFragmentDoc,
|
||||
"\n fragment UserEmail_email on UserEmail {\n id\n email\n confirmedAt\n }\n": types.UserEmail_EmailFragmentDoc,
|
||||
"\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": types.UserEmail_EmailFragmentDoc,
|
||||
"\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmail_SiteConfigFragmentDoc,
|
||||
"\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument,
|
||||
"\n mutation SetPrimaryEmail($id: ID!) {\n setPrimaryEmail(input: { userEmailId: $id }) {\n status\n user {\n id\n primaryEmail {\n id\n }\n }\n }\n }\n": types.SetPrimaryEmailDocument,
|
||||
"\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n": types.UserGreeting_UserFragmentDoc,
|
||||
"\n fragment UserGreeting_siteConfig on SiteConfig {\n displayNameChangeAllowed\n }\n": types.UserGreeting_SiteConfigFragmentDoc,
|
||||
"\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n }\n }\n": types.SetDisplayNameDocument,
|
||||
"\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": types.AddEmailDocument,
|
||||
"\n query UserEmailList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": types.UserEmailListDocument,
|
||||
"\n fragment UserEmailList_user on User {\n id\n primaryEmail {\n id\n }\n }\n": types.UserEmailList_UserFragmentDoc,
|
||||
"\n fragment UserEmailList_siteConfig on SiteConfig {\n ...UserEmail_siteConfig\n }\n": types.UserEmailList_SiteConfigFragmentDoc,
|
||||
"\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": types.UserEmailListDocument,
|
||||
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmailList_SiteConfigFragmentDoc,
|
||||
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc,
|
||||
"\n fragment UserEmail_verifyEmail on UserEmail {\n id\n email\n }\n": types.UserEmail_VerifyEmailFragmentDoc,
|
||||
"\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": types.DoVerifyEmailDocument,
|
||||
"\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": types.ResendVerificationEmailDocument,
|
||||
"\n query UserProfile {\n viewer {\n __typename\n ... on User {\n id\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n ...UserEmailList_user\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument,
|
||||
"\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n }\n }\n": types.DoVerifyEmailDocument,
|
||||
"\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n }\n }\n": types.ResendVerificationEmailDocument,
|
||||
"\n query UserProfile {\n viewer {\n __typename\n ... on User {\n id\n\n emails(first: 0) {\n totalCount\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument,
|
||||
"\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": types.SessionDetailDocument,
|
||||
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": types.BrowserSessionListDocument,
|
||||
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": types.SessionsOverviewDocument,
|
||||
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": types.AppSessionsListDocument,
|
||||
"\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n ...UnverifiedEmailAlert_user\n ...UserGreeting_user\n }\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": types.CurrentUserGreetingDocument,
|
||||
"\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n ...UserGreeting_user\n }\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": types.CurrentUserGreetingDocument,
|
||||
"\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": types.OAuth2ClientDocument,
|
||||
"\n query CurrentViewer {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.CurrentViewerDocument,
|
||||
"\n query DeviceRedirect($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.DeviceRedirectDocument,
|
||||
@@ -124,11 +121,7 @@ export function graphql(source: "\n fragment OAuth2Session_detail on Oauth2Sess
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UnverifiedEmailAlert_user on User {\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n"): typeof import('./graphql').UnverifiedEmailAlert_UserFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserEmail_email on UserEmail {\n id\n email\n confirmedAt\n }\n"): typeof import('./graphql').UserEmail_EmailFragmentDoc;
|
||||
export function graphql(source: "\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n"): typeof import('./graphql').UserEmail_EmailFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -137,10 +130,6 @@ export function graphql(source: "\n fragment UserEmail_siteConfig on SiteConfig
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n"): typeof import('./graphql').RemoveEmailDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation SetPrimaryEmail($id: ID!) {\n setPrimaryEmail(input: { userEmailId: $id }) {\n status\n user {\n id\n primaryEmail {\n id\n }\n }\n }\n }\n"): typeof import('./graphql').SetPrimaryEmailDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -160,15 +149,11 @@ export function graphql(source: "\n mutation AddEmail($userId: ID!, $email: Str
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query UserEmailList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"): typeof import('./graphql').UserEmailListDocument;
|
||||
export function graphql(source: "\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n"): typeof import('./graphql').UserEmailListDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserEmailList_user on User {\n id\n primaryEmail {\n id\n }\n }\n"): typeof import('./graphql').UserEmailList_UserFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserEmailList_siteConfig on SiteConfig {\n ...UserEmail_siteConfig\n }\n"): typeof import('./graphql').UserEmailList_SiteConfigFragmentDoc;
|
||||
export function graphql(source: "\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n"): typeof import('./graphql').UserEmailList_SiteConfigFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -180,15 +165,15 @@ export function graphql(source: "\n fragment UserEmail_verifyEmail on UserEmail
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"): typeof import('./graphql').DoVerifyEmailDocument;
|
||||
export function graphql(source: "\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n }\n }\n"): typeof import('./graphql').DoVerifyEmailDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"): typeof import('./graphql').ResendVerificationEmailDocument;
|
||||
export function graphql(source: "\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n }\n }\n"): typeof import('./graphql').ResendVerificationEmailDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query UserProfile {\n viewer {\n __typename\n ... on User {\n id\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n ...UserEmailList_user\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument;
|
||||
export function graphql(source: "\n query UserProfile {\n viewer {\n __typename\n ... on User {\n id\n\n emails(first: 0) {\n totalCount\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -208,7 +193,7 @@ export function graphql(source: "\n query AppSessionsList(\n $before: String
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n ...UnverifiedEmailAlert_user\n ...UserGreeting_user\n }\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n"): typeof import('./graphql').CurrentUserGreetingDocument;
|
||||
export function graphql(source: "\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n ...UserGreeting_user\n }\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n"): typeof import('./graphql').CurrentUserGreetingDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -1551,9 +1551,7 @@ export type CompatSession_DetailFragment = { __typename?: 'CompatSession', id: s
|
||||
|
||||
export type OAuth2Session_DetailFragment = { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null } } & { ' $fragmentName'?: 'OAuth2Session_DetailFragment' };
|
||||
|
||||
export type UnverifiedEmailAlert_UserFragment = { __typename?: 'User', unverifiedEmails: { __typename?: 'UserEmailConnection', totalCount: number } } & { ' $fragmentName'?: 'UnverifiedEmailAlert_UserFragment' };
|
||||
|
||||
export type UserEmail_EmailFragment = { __typename?: 'UserEmail', id: string, email: string, confirmedAt?: string | null } & { ' $fragmentName'?: 'UserEmail_EmailFragment' };
|
||||
export type UserEmail_EmailFragment = { __typename?: 'UserEmail', id: string, email: string } & { ' $fragmentName'?: 'UserEmail_EmailFragment' };
|
||||
|
||||
export type UserEmail_SiteConfigFragment = { __typename?: 'SiteConfig', emailChangeAllowed: boolean } & { ' $fragmentName'?: 'UserEmail_SiteConfigFragment' };
|
||||
|
||||
@@ -1564,13 +1562,6 @@ export type RemoveEmailMutationVariables = Exact<{
|
||||
|
||||
export type RemoveEmailMutation = { __typename?: 'Mutation', removeEmail: { __typename?: 'RemoveEmailPayload', status: RemoveEmailStatus, user?: { __typename?: 'User', id: string } | null } };
|
||||
|
||||
export type SetPrimaryEmailMutationVariables = Exact<{
|
||||
id: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SetPrimaryEmailMutation = { __typename?: 'Mutation', setPrimaryEmail: { __typename?: 'SetPrimaryEmailPayload', status: SetPrimaryEmailStatus, user?: { __typename?: 'User', id: string, primaryEmail?: { __typename?: 'UserEmail', id: string } | null } | null } };
|
||||
|
||||
export type UserGreeting_UserFragment = { __typename?: 'User', id: string, matrix: { __typename?: 'MatrixUser', mxid: string, displayName?: string | null } } & { ' $fragmentName'?: 'UserGreeting_UserFragment' };
|
||||
|
||||
export type UserGreeting_SiteConfigFragment = { __typename?: 'SiteConfig', displayNameChangeAllowed: boolean } & { ' $fragmentName'?: 'UserGreeting_SiteConfigFragment' };
|
||||
@@ -1595,7 +1586,6 @@ export type AddEmailMutation = { __typename?: 'Mutation', addEmail: { __typename
|
||||
) | null } };
|
||||
|
||||
export type UserEmailListQueryVariables = Exact<{
|
||||
userId: Scalars['ID']['input'];
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
last?: InputMaybe<Scalars['Int']['input']>;
|
||||
@@ -1603,17 +1593,12 @@ export type UserEmailListQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UserEmailListQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, emails: { __typename?: 'UserEmailConnection', totalCount: number, edges: Array<{ __typename?: 'UserEmailEdge', cursor: string, node: (
|
||||
{ __typename?: 'UserEmail', id: string }
|
||||
export type UserEmailListQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous' } | { __typename: 'User', emails: { __typename?: 'UserEmailConnection', totalCount: number, edges: Array<{ __typename?: 'UserEmailEdge', cursor: string, node: (
|
||||
{ __typename?: 'UserEmail' }
|
||||
& { ' $fragmentRefs'?: { 'UserEmail_EmailFragment': UserEmail_EmailFragment } }
|
||||
) }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } } | null };
|
||||
) }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } } };
|
||||
|
||||
export type UserEmailList_UserFragment = { __typename?: 'User', id: string, primaryEmail?: { __typename?: 'UserEmail', id: string } | null } & { ' $fragmentName'?: 'UserEmailList_UserFragment' };
|
||||
|
||||
export type UserEmailList_SiteConfigFragment = (
|
||||
{ __typename?: 'SiteConfig' }
|
||||
& { ' $fragmentRefs'?: { 'UserEmail_SiteConfigFragment': UserEmail_SiteConfigFragment } }
|
||||
) & { ' $fragmentName'?: 'UserEmailList_SiteConfigFragment' };
|
||||
export type UserEmailList_SiteConfigFragment = { __typename?: 'SiteConfig', emailChangeAllowed: boolean } & { ' $fragmentName'?: 'UserEmailList_SiteConfigFragment' };
|
||||
|
||||
export type BrowserSessionsOverview_UserFragment = { __typename?: 'User', id: string, browserSessions: { __typename?: 'BrowserSessionConnection', totalCount: number } } & { ' $fragmentName'?: 'BrowserSessionsOverview_UserFragment' };
|
||||
|
||||
@@ -1625,31 +1610,19 @@ export type DoVerifyEmailMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type DoVerifyEmailMutation = { __typename?: 'Mutation', verifyEmail: { __typename?: 'VerifyEmailPayload', status: VerifyEmailStatus, user?: { __typename?: 'User', id: string, primaryEmail?: { __typename?: 'UserEmail', id: string } | null } | null, email?: (
|
||||
{ __typename?: 'UserEmail', id: string }
|
||||
& { ' $fragmentRefs'?: { 'UserEmail_EmailFragment': UserEmail_EmailFragment } }
|
||||
) | null } };
|
||||
export type DoVerifyEmailMutation = { __typename?: 'Mutation', verifyEmail: { __typename?: 'VerifyEmailPayload', status: VerifyEmailStatus } };
|
||||
|
||||
export type ResendVerificationEmailMutationVariables = Exact<{
|
||||
id: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ResendVerificationEmailMutation = { __typename?: 'Mutation', sendVerificationEmail: { __typename?: 'SendVerificationEmailPayload', status: SendVerificationEmailStatus, user: { __typename?: 'User', id: string, primaryEmail?: { __typename?: 'UserEmail', id: string } | null }, email: (
|
||||
{ __typename?: 'UserEmail', id: string }
|
||||
& { ' $fragmentRefs'?: { 'UserEmail_EmailFragment': UserEmail_EmailFragment } }
|
||||
) } };
|
||||
export type ResendVerificationEmailMutation = { __typename?: 'Mutation', sendVerificationEmail: { __typename?: 'SendVerificationEmailPayload', status: SendVerificationEmailStatus } };
|
||||
|
||||
export type UserProfileQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type UserProfileQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous' } | (
|
||||
{ __typename: 'User', id: string, primaryEmail?: (
|
||||
{ __typename?: 'UserEmail', id: string }
|
||||
& { ' $fragmentRefs'?: { 'UserEmail_EmailFragment': UserEmail_EmailFragment } }
|
||||
) | null }
|
||||
& { ' $fragmentRefs'?: { 'UserEmailList_UserFragment': UserEmailList_UserFragment } }
|
||||
), siteConfig: (
|
||||
export type UserProfileQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous' } | { __typename: 'User', id: string, emails: { __typename?: 'UserEmailConnection', totalCount: number } }, siteConfig: (
|
||||
{ __typename?: 'SiteConfig', emailChangeAllowed: boolean, passwordLoginEnabled: boolean }
|
||||
& { ' $fragmentRefs'?: { 'UserEmailList_SiteConfigFragment': UserEmailList_SiteConfigFragment;'UserEmail_SiteConfigFragment': UserEmail_SiteConfigFragment;'PasswordChange_SiteConfigFragment': PasswordChange_SiteConfigFragment } }
|
||||
) };
|
||||
@@ -1714,7 +1687,7 @@ export type CurrentUserGreetingQueryVariables = Exact<{ [key: string]: never; }>
|
||||
|
||||
export type CurrentUserGreetingQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: (
|
||||
{ __typename?: 'User' }
|
||||
& { ' $fragmentRefs'?: { 'UnverifiedEmailAlert_UserFragment': UnverifiedEmailAlert_UserFragment;'UserGreeting_UserFragment': UserGreeting_UserFragment } }
|
||||
& { ' $fragmentRefs'?: { 'UserGreeting_UserFragment': UserGreeting_UserFragment } }
|
||||
) } | { __typename: 'Oauth2Session' }, siteConfig: (
|
||||
{ __typename?: 'SiteConfig' }
|
||||
& { ' $fragmentRefs'?: { 'UserGreeting_SiteConfigFragment': UserGreeting_SiteConfigFragment } }
|
||||
@@ -1972,20 +1945,17 @@ export const OAuth2Session_DetailFragmentDoc = new TypedDocumentString(`
|
||||
}
|
||||
}
|
||||
`, {"fragmentName":"OAuth2Session_detail"}) as unknown as TypedDocumentString<OAuth2Session_DetailFragment, unknown>;
|
||||
export const UnverifiedEmailAlert_UserFragmentDoc = new TypedDocumentString(`
|
||||
fragment UnverifiedEmailAlert_user on User {
|
||||
unverifiedEmails: emails(first: 0, state: PENDING) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`, {"fragmentName":"UnverifiedEmailAlert_user"}) as unknown as TypedDocumentString<UnverifiedEmailAlert_UserFragment, unknown>;
|
||||
export const UserEmail_EmailFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmail_email on UserEmail {
|
||||
id
|
||||
email
|
||||
confirmedAt
|
||||
}
|
||||
`, {"fragmentName":"UserEmail_email"}) as unknown as TypedDocumentString<UserEmail_EmailFragment, unknown>;
|
||||
export const UserEmail_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmail_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
}
|
||||
`, {"fragmentName":"UserEmail_siteConfig"}) as unknown as TypedDocumentString<UserEmail_SiteConfigFragment, unknown>;
|
||||
export const UserGreeting_UserFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserGreeting_user on User {
|
||||
id
|
||||
@@ -2000,26 +1970,11 @@ export const UserGreeting_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
displayNameChangeAllowed
|
||||
}
|
||||
`, {"fragmentName":"UserGreeting_siteConfig"}) as unknown as TypedDocumentString<UserGreeting_SiteConfigFragment, unknown>;
|
||||
export const UserEmailList_UserFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmailList_user on User {
|
||||
id
|
||||
primaryEmail {
|
||||
id
|
||||
}
|
||||
}
|
||||
`, {"fragmentName":"UserEmailList_user"}) as unknown as TypedDocumentString<UserEmailList_UserFragment, unknown>;
|
||||
export const UserEmail_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmail_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
}
|
||||
`, {"fragmentName":"UserEmail_siteConfig"}) as unknown as TypedDocumentString<UserEmail_SiteConfigFragment, unknown>;
|
||||
export const UserEmailList_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmailList_siteConfig on SiteConfig {
|
||||
...UserEmail_siteConfig
|
||||
}
|
||||
fragment UserEmail_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
}`, {"fragmentName":"UserEmailList_siteConfig"}) as unknown as TypedDocumentString<UserEmailList_SiteConfigFragment, unknown>;
|
||||
}
|
||||
`, {"fragmentName":"UserEmailList_siteConfig"}) as unknown as TypedDocumentString<UserEmailList_SiteConfigFragment, unknown>;
|
||||
export const BrowserSessionsOverview_UserFragmentDoc = new TypedDocumentString(`
|
||||
fragment BrowserSessionsOverview_user on User {
|
||||
id
|
||||
@@ -2125,19 +2080,6 @@ export const RemoveEmailDocument = new TypedDocumentString(`
|
||||
}
|
||||
}
|
||||
`) as unknown as TypedDocumentString<RemoveEmailMutation, RemoveEmailMutationVariables>;
|
||||
export const SetPrimaryEmailDocument = new TypedDocumentString(`
|
||||
mutation SetPrimaryEmail($id: ID!) {
|
||||
setPrimaryEmail(input: {userEmailId: $id}) {
|
||||
status
|
||||
user {
|
||||
id
|
||||
primaryEmail {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`) as unknown as TypedDocumentString<SetPrimaryEmailMutation, SetPrimaryEmailMutationVariables>;
|
||||
export const SetDisplayNameDocument = new TypedDocumentString(`
|
||||
mutation SetDisplayName($userId: ID!, $displayName: String) {
|
||||
setDisplayName(input: {userId: $userId, displayName: $displayName}) {
|
||||
@@ -2159,26 +2101,26 @@ export const AddEmailDocument = new TypedDocumentString(`
|
||||
fragment UserEmail_email on UserEmail {
|
||||
id
|
||||
email
|
||||
confirmedAt
|
||||
}`) as unknown as TypedDocumentString<AddEmailMutation, AddEmailMutationVariables>;
|
||||
export const UserEmailListDocument = new TypedDocumentString(`
|
||||
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
|
||||
query UserEmailList($first: Int, $after: String, $last: Int, $before: String) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2186,61 +2128,30 @@ export const UserEmailListDocument = new TypedDocumentString(`
|
||||
fragment UserEmail_email on UserEmail {
|
||||
id
|
||||
email
|
||||
confirmedAt
|
||||
}`) as unknown as TypedDocumentString<UserEmailListQuery, UserEmailListQueryVariables>;
|
||||
export const DoVerifyEmailDocument = new TypedDocumentString(`
|
||||
mutation DoVerifyEmail($id: ID!, $code: String!) {
|
||||
verifyEmail(input: {userEmailId: $id, code: $code}) {
|
||||
status
|
||||
user {
|
||||
id
|
||||
primaryEmail {
|
||||
id
|
||||
}
|
||||
}
|
||||
email {
|
||||
id
|
||||
...UserEmail_email
|
||||
}
|
||||
}
|
||||
}
|
||||
fragment UserEmail_email on UserEmail {
|
||||
id
|
||||
email
|
||||
confirmedAt
|
||||
}`) as unknown as TypedDocumentString<DoVerifyEmailMutation, DoVerifyEmailMutationVariables>;
|
||||
`) as unknown as TypedDocumentString<DoVerifyEmailMutation, DoVerifyEmailMutationVariables>;
|
||||
export const ResendVerificationEmailDocument = new TypedDocumentString(`
|
||||
mutation ResendVerificationEmail($id: ID!) {
|
||||
sendVerificationEmail(input: {userEmailId: $id}) {
|
||||
status
|
||||
user {
|
||||
id
|
||||
primaryEmail {
|
||||
id
|
||||
}
|
||||
}
|
||||
email {
|
||||
id
|
||||
...UserEmail_email
|
||||
}
|
||||
}
|
||||
}
|
||||
fragment UserEmail_email on UserEmail {
|
||||
id
|
||||
email
|
||||
confirmedAt
|
||||
}`) as unknown as TypedDocumentString<ResendVerificationEmailMutation, ResendVerificationEmailMutationVariables>;
|
||||
`) as unknown as TypedDocumentString<ResendVerificationEmailMutation, ResendVerificationEmailMutationVariables>;
|
||||
export const UserProfileDocument = new TypedDocumentString(`
|
||||
query UserProfile {
|
||||
viewer {
|
||||
__typename
|
||||
... on User {
|
||||
id
|
||||
primaryEmail {
|
||||
id
|
||||
...UserEmail_email
|
||||
emails(first: 0) {
|
||||
totalCount
|
||||
}
|
||||
...UserEmailList_user
|
||||
}
|
||||
}
|
||||
siteConfig {
|
||||
@@ -2254,22 +2165,11 @@ export const UserProfileDocument = new TypedDocumentString(`
|
||||
fragment PasswordChange_siteConfig on SiteConfig {
|
||||
passwordChangeAllowed
|
||||
}
|
||||
fragment UserEmail_email on UserEmail {
|
||||
id
|
||||
email
|
||||
confirmedAt
|
||||
}
|
||||
fragment UserEmail_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
}
|
||||
fragment UserEmailList_user on User {
|
||||
id
|
||||
primaryEmail {
|
||||
id
|
||||
}
|
||||
}
|
||||
fragment UserEmailList_siteConfig on SiteConfig {
|
||||
...UserEmail_siteConfig
|
||||
emailChangeAllowed
|
||||
}`) as unknown as TypedDocumentString<UserProfileQuery, UserProfileQueryVariables>;
|
||||
export const SessionDetailDocument = new TypedDocumentString(`
|
||||
query SessionDetail($id: ID!) {
|
||||
@@ -2486,7 +2386,6 @@ export const CurrentUserGreetingDocument = new TypedDocumentString(`
|
||||
... on BrowserSession {
|
||||
id
|
||||
user {
|
||||
...UnverifiedEmailAlert_user
|
||||
...UserGreeting_user
|
||||
}
|
||||
}
|
||||
@@ -2495,12 +2394,7 @@ export const CurrentUserGreetingDocument = new TypedDocumentString(`
|
||||
...UserGreeting_siteConfig
|
||||
}
|
||||
}
|
||||
fragment UnverifiedEmailAlert_user on User {
|
||||
unverifiedEmails: emails(first: 0, state: PENDING) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
fragment UserGreeting_user on User {
|
||||
fragment UserGreeting_user on User {
|
||||
id
|
||||
matrix {
|
||||
mxid
|
||||
@@ -2736,28 +2630,6 @@ export const mockRemoveEmailMutation = (resolver: GraphQLResponseResolver<Remove
|
||||
options
|
||||
)
|
||||
|
||||
/**
|
||||
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
|
||||
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
|
||||
* @see https://mswjs.io/docs/basics/response-resolver
|
||||
* @example
|
||||
* mockSetPrimaryEmailMutation(
|
||||
* ({ query, variables }) => {
|
||||
* const { id } = variables;
|
||||
* return HttpResponse.json({
|
||||
* data: { setPrimaryEmail }
|
||||
* })
|
||||
* },
|
||||
* requestOptions
|
||||
* )
|
||||
*/
|
||||
export const mockSetPrimaryEmailMutation = (resolver: GraphQLResponseResolver<SetPrimaryEmailMutation, SetPrimaryEmailMutationVariables>, options?: RequestHandlerOptions) =>
|
||||
graphql.mutation<SetPrimaryEmailMutation, SetPrimaryEmailMutationVariables>(
|
||||
'SetPrimaryEmail',
|
||||
resolver,
|
||||
options
|
||||
)
|
||||
|
||||
/**
|
||||
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
|
||||
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
|
||||
@@ -2809,9 +2681,9 @@ export const mockAddEmailMutation = (resolver: GraphQLResponseResolver<AddEmailM
|
||||
* @example
|
||||
* mockUserEmailListQuery(
|
||||
* ({ query, variables }) => {
|
||||
* const { userId, first, after, last, before } = variables;
|
||||
* const { first, after, last, before } = variables;
|
||||
* return HttpResponse.json({
|
||||
* data: { user }
|
||||
* data: { viewer }
|
||||
* })
|
||||
* },
|
||||
* requestOptions
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,7 +520,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_ssths_67"
|
||||
for="radix-:rl:"
|
||||
for="radix-:rk:"
|
||||
>
|
||||
Add email
|
||||
</label>
|
||||
@@ -556,9 +528,9 @@ exports[`Account home page > renders the page 1`] = `
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user