frontend: simplify email list

This commit is contained in:
Quentin Gliech
2025-01-09 09:41:00 +01:00
parent 98efc4813b
commit 14fd660208
17 changed files with 217 additions and 838 deletions

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -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();
});
});

View File

@@ -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;

View File

@@ -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>
`;

View File

@@ -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";

View File

@@ -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>
);

View File

@@ -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}

View File

@@ -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
}
}
}
`);

View File

@@ -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.
*/

View File

@@ -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

View File

@@ -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")}

View File

@@ -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),
]),
});

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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 youre 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 youre 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"