diff --git a/frontend/locales/en.json b/frontend/locales/en.json index e139cb724..697123442 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -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" diff --git a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.module.css b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.module.css deleted file mode 100644 index 89636451a..000000000 --- a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.module.css +++ /dev/null @@ -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; -} diff --git a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.test.tsx b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.test.tsx deleted file mode 100644 index cd65e2be2..000000000 --- a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.test.tsx +++ /dev/null @@ -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("", () => { - 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( - - - , - ); - - expect(container).toMatchInlineSnapshot("
"); - }); - - it("renders a warning when there are unverified emails", () => { - const data = makeFragmentData( - { - id: "abc123", - unverifiedEmails: { - totalCount: 2, - }, - }, - UNVERIFIED_EMAILS_FRAGMENT, - ); - - const { container } = render( - - - , - ); - - 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( - - - , - ); - - // warning is rendered - expect(getByText("Unverified email")).toBeTruthy(); - - fireEvent.click(getByLabelText("Close")); - - // no more warning - expect(container).toMatchInlineSnapshot("
"); - }); - - 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( - - - , - ); - - // warning is rendered - expect(getByText("Unverified email")).toBeTruthy(); - - const newData = makeFragmentData( - { - id: "abc123", - unverifiedEmails: { - totalCount: 0, - }, - }, - UNVERIFIED_EMAILS_FRAGMENT, - ); - rerender( - - - , - ); - - // warning removed - expect(container).toMatchInlineSnapshot("
"); - }); - - 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( - - - , - ); - - // warning is rendered - expect(getByText("Unverified email")).toBeTruthy(); - - fireEvent.click(getByLabelText("Close")); - - // no more warning - expect(container).toMatchInlineSnapshot("
"); - - const newData = makeFragmentData( - { - id: "abc123", - unverifiedEmails: { - totalCount: 3, - }, - }, - UNVERIFIED_EMAILS_FRAGMENT, - ); - rerender( - - - , - ); - - // warning is rendered - expect(getByText("Unverified email")).toBeTruthy(); - }); -}); diff --git a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx deleted file mode 100644 index 319b0f5da..000000000 --- a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx +++ /dev/null @@ -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; -}> = ({ user }) => { - const data = useFragment(UNVERIFIED_EMAILS_FRAGMENT, user); - const [dismiss, setDismiss] = useState(false); - const { t } = useTranslation(); - const currentCount = useRef(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 ( - - {t("frontend.unverified_email_alert.text", { - count: data.unverifiedEmails.totalCount, - })}{" "} - - {t("frontend.unverified_email_alert.button")} - - - ); -}; - -export default UnverifiedEmailAlert; diff --git a/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap b/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap deleted file mode 100644 index 040c86a8c..000000000 --- a/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap +++ /dev/null @@ -1,78 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[` > renders a warning when there are unverified emails 1`] = ` -
-
- -
-
-

- Unverified email -

-

- You have 2 unverified email addresses. - - - Review and verify - -

-
-
- -
-
-`; diff --git a/frontend/src/components/UnverifiedEmailAlert/index.ts b/frontend/src/components/UnverifiedEmailAlert/index.ts deleted file mode 100644 index 47176edfd..000000000 --- a/frontend/src/components/UnverifiedEmailAlert/index.ts +++ /dev/null @@ -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"; diff --git a/frontend/src/components/UserEmail/UserEmail.tsx b/frontend/src/components/UserEmail/UserEmail.tsx index 88dedbb6d..02771412e 100644 --- a/frontend/src/components/UserEmail/UserEmail.tsx +++ b/frontend/src/components/UserEmail/UserEmail.tsx @@ -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; - siteConfig: FragmentType; + 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 ( - - {isPrimary - ? t("frontend.user_email.primary_email") - : t("frontend.user_email.email")} - + {t("frontend.user_email.email")}
- {!isPrimary && emailChangeAllowed && ( + {canRemove && ( )}
- - {isPrimary && emailChangeAllowed && ( - - {t("frontend.user_email.cant_delete_primary")} - - )} - - {data.confirmedAt && !isPrimary && emailChangeAllowed && ( - - - - )} - - {!data.confirmedAt && ( - - {t("frontend.user_email.not_verified")} |{" "} - - {t("frontend.user_email.retry_button")} - - - )}
); diff --git a/frontend/src/components/UserProfile/UserEmailList.tsx b/frontend/src/components/UserProfile/UserEmailList.tsx index 9af5b0fb0..6db4adcf3 100644 --- a/frontend/src/components/UserProfile/UserEmailList.tsx +++ b/frontend/src/components/UserProfile/UserEmailList.tsx @@ -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; - siteConfig: FragmentType; -}> = ({ 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; +}> = ({ 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 : ( - - ), - )} + {emails.edges.map((edge) => ( + + ))} paginate(prevPage) : null} onNext={nextPage ? (): void => paginate(nextPage) : null} disabled={pending} diff --git a/frontend/src/components/VerifyEmail/VerifyEmail.tsx b/frontend/src/components/VerifyEmail/VerifyEmail.tsx index c7c696b92..42ee6bc93 100644 --- a/frontend/src/components/VerifyEmail/VerifyEmail.tsx +++ b/frontend/src/components/VerifyEmail/VerifyEmail.tsx @@ -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 - } } } `); diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index d6334121d..89d6d8e47 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -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. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 3980ea976..7a67fafc6 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -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; after?: InputMaybe; last?: InputMaybe; @@ -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; -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; export const UserEmail_EmailFragmentDoc = new TypedDocumentString(` fragment UserEmail_email on UserEmail { id email - confirmedAt } `, {"fragmentName":"UserEmail_email"}) as unknown as TypedDocumentString; +export const UserEmail_SiteConfigFragmentDoc = new TypedDocumentString(` + fragment UserEmail_siteConfig on SiteConfig { + emailChangeAllowed +} + `, {"fragmentName":"UserEmail_siteConfig"}) as unknown as TypedDocumentString; 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; -export const UserEmailList_UserFragmentDoc = new TypedDocumentString(` - fragment UserEmailList_user on User { - id - primaryEmail { - id - } -} - `, {"fragmentName":"UserEmailList_user"}) as unknown as TypedDocumentString; -export const UserEmail_SiteConfigFragmentDoc = new TypedDocumentString(` - fragment UserEmail_siteConfig on SiteConfig { - emailChangeAllowed -} - `, {"fragmentName":"UserEmail_siteConfig"}) as unknown as TypedDocumentString; 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; +} + `, {"fragmentName":"UserEmailList_siteConfig"}) as unknown as TypedDocumentString; 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; -export const SetPrimaryEmailDocument = new TypedDocumentString(` - mutation SetPrimaryEmail($id: ID!) { - setPrimaryEmail(input: {userEmailId: $id}) { - status - user { - id - primaryEmail { - id - } - } - } -} - `) as unknown as TypedDocumentString; 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; 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; 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; + `) as unknown as TypedDocumentString; 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; + `) as unknown as TypedDocumentString; 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; 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 { - * const { id } = variables; - * return HttpResponse.json({ - * data: { setPrimaryEmail } - * }) - * }, - * requestOptions - * ) - */ -export const mockSetPrimaryEmailMutation = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) => - graphql.mutation( - '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 { - * const { userId, first, after, last, before } = variables; + * const { first, after, last, before } = variables; * return HttpResponse.json({ - * data: { user } + * data: { viewer } * }) * }, * requestOptions diff --git a/frontend/src/routes/_account.index.lazy.tsx b/frontend/src/routes/_account.index.lazy.tsx index fe63e0906..81792e216 100644 --- a/frontend/src/routes/_account.index.lazy.tsx +++ b/frontend/src/routes/_account.index.lazy.tsx @@ -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 (
- - {viewer.primaryEmail ? ( - - ) : ( - - )} + {/* 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) && ( + <> + + - }> - - + {siteConfig.emailChangeAllowed && ( + + )} + - {siteConfig.emailChangeAllowed && ( - - )} - + + + )} {siteConfig.passwordLoginEnabled && ( <> - + + )} - - {t("frontend.reset_cross_signing.description")} diff --git a/frontend/src/routes/_account.index.tsx b/frontend/src/routes/_account.index.tsx index 2addf320b..6422b749b 100644 --- a/frontend/src/routes/_account.index.tsx +++ b/frontend/src/routes/_account.index.tsx @@ -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), + ]), }); diff --git a/frontend/src/routes/_account.lazy.tsx b/frontend/src/routes/_account.lazy.tsx index d5390874d..3acf066fd 100644 --- a/frontend/src/routes/_account.lazy.tsx +++ b/frontend/src/routes/_account.lazy.tsx @@ -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 {
- - {t("frontend.nav.settings")} {t("frontend.nav.devices")} diff --git a/frontend/src/routes/_account.tsx b/frontend/src/routes/_account.tsx index 11121bef9..e61b98aaa 100644 --- a/frontend/src/routes/_account.tsx +++ b/frontend/src/routes/_account.tsx @@ -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 } } diff --git a/frontend/tests/mocks/handlers.ts b/frontend/tests/mocks/handlers.ts index 35ba2407d..a7b110c60 100644 --- a/frontend/tests/mocks/handlers.ts +++ b/frontend/tests/mocks/handlers.ts @@ -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, diff --git a/frontend/tests/routes/account/__snapshots__/index.test.tsx.snap b/frontend/tests/routes/account/__snapshots__/index.test.tsx.snap index 7bbdb9738..48fa1e3be 100644 --- a/frontend/tests/routes/account/__snapshots__/index.test.tsx.snap +++ b/frontend/tests/routes/account/__snapshots__/index.test.tsx.snap @@ -2,18 +2,18 @@ exports[`Account home page > display name edit box > displays an error if the display name is invalid 1`] = ` This is what others will see wherever you’re signed in. @@ -236,13 +236,13 @@ exports[`Account home page > display name edit box > lets edit the display name > display name edit box > lets edit the display name Cancel
-
@@ -548,7 +520,7 @@ exports[`Account home page > renders the page 1`] = ` > @@ -556,9 +528,9 @@ exports[`Account home page > renders the page 1`] = ` class="_controls_1h4nb_17" > renders the page 1`] = `
Add an alternative email you can use to access this account. @@ -582,7 +554,7 @@ exports[`Account home page > renders the page 1`] = ` role="separator" />
@@ -594,14 +566,14 @@ exports[`Account home page > renders the page 1`] = ` >

Account password