1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2026-01-03 17:02:28 +03:00

move unverified email alert to header

This commit is contained in:
Kerry Archibald
2023-08-30 20:50:57 +12:00
committed by Quentin Gliech
parent a3b7cc27bf
commit 7bc13ab536
11 changed files with 483 additions and 84 deletions

View File

@@ -0,0 +1,18 @@
/* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.alert {
margin-top: var(--cpd-space-4x);
}

View File

@@ -0,0 +1,95 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// @vitest-environment happy-dom
import { render, cleanup, fireEvent } from "@testing-library/react";
import { describe, it, expect, afterEach } from "vitest";
import { makeFragmentData } from "../../gql/fragment-masking";
import { WithLocation } from "../../test-utils/WithLocation";
import UnverifiedEmailAlert, {
UNVERIFIED_EMAILS_FRAGMENT,
} from "./UnverifiedEmailAlert";
describe("<UnverifiedEmailAlert />", () => {
afterEach(cleanup);
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(
<WithLocation>
<UnverifiedEmailAlert unverifiedEmails={data} />
</WithLocation>,
);
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(
<WithLocation>
<UnverifiedEmailAlert unverifiedEmails={data} />
</WithLocation>,
);
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(
<WithLocation>
<UnverifiedEmailAlert unverifiedEmails={data} />
</WithLocation>,
);
// warning is rendered
expect(getByText("Unverified email")).toBeTruthy();
fireEvent.click(getByLabelText("Close"));
// no more warning
expect(container).toMatchInlineSnapshot("<div />");
});
});

View File

@@ -0,0 +1,60 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Alert } from "@vector-im/compound-web";
import { useState } from "react";
import { Link } from "../../Router";
import { FragmentType, useFragment } from "../../gql/fragment-masking";
import { graphql } from "../../gql/gql";
import styles from "./UnverifiedEmailAlert.module.css";
export const UNVERIFIED_EMAILS_FRAGMENT = graphql(/* GraphQL */ `
fragment UnverifiedEmailAlert on User {
id
unverifiedEmails: emails(first: 0, state: PENDING) {
totalCount
}
}
`);
const UnverifiedEmailAlert: React.FC<{
unverifiedEmails?: FragmentType<typeof UNVERIFIED_EMAILS_FRAGMENT>;
}> = ({ unverifiedEmails }) => {
const data = useFragment(UNVERIFIED_EMAILS_FRAGMENT, unverifiedEmails);
const [dismiss, setDismiss] = useState(false);
const doDismiss = (): void => setDismiss(true);
if (!data?.unverifiedEmails?.totalCount || dismiss) {
return null;
}
return (
<Alert
type="critical"
title="Unverified email"
onClose={doDismiss}
className={styles.alert}
>
You have {data.unverifiedEmails.totalCount} unverified email address(es).{" "}
<Link kind="button" route={{ type: "profile" }}>
Review and verify
</Link>
</Alert>
);
};
export default UnverifiedEmailAlert;

View File

@@ -0,0 +1,61 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<UnverifiedEmailAlert /> > renders a warning when there are unverified emails 1`] = `
<div>
<div
class="_alert_fxw8y_19 _alert_d86cd2"
data-type="critical"
>
<svg
aria-hidden="true"
class="_icon_fxw8y_52"
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"
fill="currentColor"
/>
</svg>
<div
class="_content_fxw8y_44"
>
<p
class="_title_fxw8y_48"
>
Unverified email
</p>
<p>
You have
2
unverified email address(es).
<a
class="_linkButton_4f14fc"
href="/profile"
>
Review and verify
</a>
</p>
</div>
<svg
aria-label="Close"
class="_close_fxw8y_68"
fill="currentColor"
height="16"
role="button"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12 13.4-4.9 4.9a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l4.9-4.9-4.9-4.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.9 4.9 4.9-4.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L13.4 12l4.9 4.9a.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275L12 13.4Z"
fill="currentColor"
/>
</svg>
</div>
</div>
`;

View File

@@ -0,0 +1,15 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export { default } from "./UnverifiedEmailAlert";

View File

@@ -18,6 +18,11 @@ import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql"; import { atomWithQuery } from "jotai-urql";
import { graphql } from "../gql"; import { graphql } from "../gql";
import { FragmentType } from "../gql/fragment-masking";
import UnverifiedEmailAlert, {
UNVERIFIED_EMAILS_FRAGMENT,
} from "./UnverifiedEmailAlert/UnverifiedEmailAlert";
const QUERY = graphql(/* GraphQL */ ` const QUERY = graphql(/* GraphQL */ `
query UserGreeting($userId: ID!) { query UserGreeting($userId: ID!) {
@@ -29,6 +34,14 @@ const QUERY = graphql(/* GraphQL */ `
displayName displayName
} }
} }
viewer {
__typename
... on User {
id
...UnverifiedEmailAlert
}
}
} }
`); `);
@@ -47,17 +60,26 @@ const UserGreeting: React.FC<{ userId: string }> = ({ userId }) => {
if (result.data?.user) { if (result.data?.user) {
const user = result.data.user; const user = result.data.user;
return ( return (
<header className="text-center"> <>
<Avatar <header className="text-center">
size="var(--cpd-space-24x)" <Avatar
id={user.matrix.mxid} size="var(--cpd-space-24x)"
name={user.matrix.displayName || user.matrix.mxid} id={user.matrix.mxid}
name={user.matrix.displayName || user.matrix.mxid}
/>
<Heading size="xl" weight="semibold">
{user.matrix.displayName || user.username}
</Heading>
<Body size="lg">{user.matrix.mxid}</Body>
</header>
<UnverifiedEmailAlert
unverifiedEmails={
result.data?.viewer as FragmentType<
typeof UNVERIFIED_EMAILS_FRAGMENT
>
}
/> />
<Heading size="xl" weight="semibold"> </>
{user.matrix.displayName || user.username}
</Heading>
<Body size="lg">{user.matrix.mxid}</Body>
</header>
); );
} }

View File

@@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Alert, Body, H3, H6 } from "@vector-im/compound-web"; import { Body, H3, H6 } from "@vector-im/compound-web";
import { useState } from "react";
import { Link } from "../../Router"; import { Link } from "../../Router";
import { FragmentType, graphql, useFragment } from "../../gql"; import { FragmentType, graphql, useFragment } from "../../gql";
@@ -35,10 +34,6 @@ export const FRAGMENT = graphql(/* GraphQL */ `
totalCount totalCount
} }
unverifiedEmails: emails(first: 0, state: PENDING) {
totalCount
}
browserSessions(first: 0, state: ACTIVE) { browserSessions(first: 0, state: ACTIVE) {
totalCount totalCount
} }
@@ -57,11 +52,6 @@ const UserHome: React.FC<{
user: FragmentType<typeof FRAGMENT>; user: FragmentType<typeof FRAGMENT>;
}> = ({ user }) => { }> = ({ user }) => {
const data = useFragment(FRAGMENT, user); const data = useFragment(FRAGMENT, user);
const [dismiss, setDismiss] = useState(false);
const doDismiss = (): void => {
setDismiss(true);
};
// allow this until we get i18n // allow this until we get i18n
const pluraliseSession = (count: number): string => const pluraliseSession = (count: number): string =>
@@ -74,14 +64,7 @@ const UserHome: React.FC<{
return ( return (
<BlockList> <BlockList>
{data.unverifiedEmails.totalCount > 0 && !dismiss && (
<Alert type="critical" title="Unverified email" onClose={doDismiss}>
You have {data.unverifiedEmails.totalCount} unverified email
address(es). <Link route={{ type: "profile" }}>Check</Link>
</Alert>
)}
{/* This is a short term solution, so I won't bother extracting these blocks into components */} {/* This is a short term solution, so I won't bother extracting these blocks into components */}
<H3>Where you're signed in</H3> <H3>Where you're signed in</H3>
<Block className={styles.sessionListBlock}> <Block className={styles.sessionListBlock}>
<div className={styles.sessionListBlockInfo}> <div className={styles.sessionListBlockInfo}>
@@ -91,7 +74,9 @@ const UserHome: React.FC<{
{pluraliseSession(data.browserSessions.totalCount)} {pluraliseSession(data.browserSessions.totalCount)}
</Body> </Body>
</div> </div>
<Link route={{ type: "browser-session-list" }}>View all</Link> <Link kind="button" route={{ type: "browser-session-list" }}>
View all
</Link>
</Block> </Block>
<Block className={styles.sessionListBlock}> <Block className={styles.sessionListBlock}>
<div className={styles.sessionListBlockInfo}> <div className={styles.sessionListBlockInfo}>
@@ -101,7 +86,9 @@ const UserHome: React.FC<{
{pluraliseSession(data.oauth2Sessions.totalCount)} {pluraliseSession(data.oauth2Sessions.totalCount)}
</Body> </Body>
</div> </div>
<Link route={{ type: "oauth2-session-list" }}>View all</Link> <Link kind="button" route={{ type: "oauth2-session-list" }}>
View all
</Link>
</Block> </Block>
<Block className={styles.sessionListBlock}> <Block className={styles.sessionListBlock}>
<div className={styles.sessionListBlockInfo}> <div className={styles.sessionListBlockInfo}>
@@ -111,7 +98,9 @@ const UserHome: React.FC<{
{pluraliseSession(data.compatSessions.totalCount)} {pluraliseSession(data.compatSessions.totalCount)}
</Body> </Body>
</div> </div>
<Link route={{ type: "compat-session-list" }}>View all</Link> <Link kind="button" route={{ type: "compat-session-list" }}>
View all
</Link>
</Block> </Block>
</BlockList> </BlockList>
); );

View File

@@ -30,6 +30,7 @@ exports[`UserHome > render a <UserHome /> with sessions 1`] = `
</p> </p>
</div> </div>
<a <a
className="_linkButton_4f14fc"
href="/sessions" href="/sessions"
onClick={[Function]} onClick={[Function]}
> >
@@ -57,6 +58,7 @@ exports[`UserHome > render a <UserHome /> with sessions 1`] = `
</p> </p>
</div> </div>
<a <a
className="_linkButton_4f14fc"
href="/oauth2-sessions" href="/oauth2-sessions"
onClick={[Function]} onClick={[Function]}
> >
@@ -84,6 +86,7 @@ exports[`UserHome > render a <UserHome /> with sessions 1`] = `
</p> </p>
</div> </div>
<a <a
className="_linkButton_4f14fc"
href="/compat-sessions" href="/compat-sessions"
onClick={[Function]} onClick={[Function]}
> >
@@ -123,6 +126,7 @@ exports[`UserHome > render an simple <UserHome /> 1`] = `
</p> </p>
</div> </div>
<a <a
className="_linkButton_4f14fc"
href="/sessions" href="/sessions"
onClick={[Function]} onClick={[Function]}
> >
@@ -150,6 +154,7 @@ exports[`UserHome > render an simple <UserHome /> 1`] = `
</p> </p>
</div> </div>
<a <a
className="_linkButton_4f14fc"
href="/oauth2-sessions" href="/oauth2-sessions"
onClick={[Function]} onClick={[Function]}
> >
@@ -177,6 +182,7 @@ exports[`UserHome > render an simple <UserHome /> 1`] = `
</p> </p>
</div> </div>
<a <a
className="_linkButton_4f14fc"
href="/compat-sessions" href="/compat-sessions"
onClick={[Function]} onClick={[Function]}
> >

View File

@@ -37,15 +37,17 @@ const documents = {
types.EndOAuth2SessionDocument, types.EndOAuth2SessionDocument,
"\n query OAuth2SessionListQuery(\n $userId: ID!\n $state: Oauth2SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n state: $state\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": "\n query OAuth2SessionListQuery(\n $userId: ID!\n $state: Oauth2SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n state: $state\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
types.OAuth2SessionListQueryDocument, types.OAuth2SessionListQueryDocument,
"\n fragment UnverifiedEmailAlert on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n":
types.UnverifiedEmailAlertFragmentDoc,
"\n fragment UserEmail_email on UserEmail {\n id\n email\n confirmedAt\n }\n": "\n fragment UserEmail_email on UserEmail {\n id\n email\n confirmedAt\n }\n":
types.UserEmail_EmailFragmentDoc, types.UserEmail_EmailFragmentDoc,
"\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": "\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n":
types.RemoveEmailDocument, 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": "\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, types.SetPrimaryEmailDocument,
"\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n": "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n viewer {\n __typename\n\n ... on User {\n id\n ...UnverifiedEmailAlert\n }\n }\n }\n":
types.UserGreetingDocument, types.UserGreetingDocument,
"\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": "\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n":
types.UserHome_UserFragmentDoc, types.UserHome_UserFragmentDoc,
"\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": "\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, types.AddEmailDocument,
@@ -157,6 +159,12 @@ export function graphql(
export function graphql( export function graphql(
source: "\n query OAuth2SessionListQuery(\n $userId: ID!\n $state: Oauth2SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n state: $state\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n", source: "\n query OAuth2SessionListQuery(\n $userId: ID!\n $state: Oauth2SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n state: $state\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n",
): (typeof documents)["\n query OAuth2SessionListQuery(\n $userId: ID!\n $state: Oauth2SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n state: $state\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"]; ): (typeof documents)["\n query OAuth2SessionListQuery(\n $userId: ID!\n $state: Oauth2SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n state: $state\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"];
/**
* 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 on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n",
): (typeof documents)["\n fragment UnverifiedEmailAlert on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -179,14 +187,14 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n", source: "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n viewer {\n __typename\n\n ... on User {\n id\n ...UnverifiedEmailAlert\n }\n }\n }\n",
): (typeof documents)["\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n"]; ): (typeof documents)["\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n viewer {\n __typename\n\n ... on User {\n id\n ...UnverifiedEmailAlert\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n", source: "\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n",
): (typeof documents)["\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n"]; ): (typeof documents)["\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

View File

@@ -1146,6 +1146,12 @@ export type OAuth2SessionListQueryQuery = {
} | null; } | null;
}; };
export type UnverifiedEmailAlertFragment = {
__typename?: "User";
id: string;
unverifiedEmails: { __typename?: "UserEmailConnection"; totalCount: number };
} & { " $fragmentName"?: "UnverifiedEmailAlertFragment" };
export type UserEmail_EmailFragment = { export type UserEmail_EmailFragment = {
__typename?: "UserEmail"; __typename?: "UserEmail";
id: string; id: string;
@@ -1199,6 +1205,13 @@ export type UserGreetingQuery = {
displayName?: string | null; displayName?: string | null;
}; };
} | null; } | null;
viewer:
| { __typename: "Anonymous" }
| ({ __typename: "User"; id: string } & {
" $fragmentRefs"?: {
UnverifiedEmailAlertFragment: UnverifiedEmailAlertFragment;
};
});
}; };
export type UserHome_UserFragment = { export type UserHome_UserFragment = {
@@ -1210,7 +1223,6 @@ export type UserHome_UserFragment = {
}) })
| null; | null;
confirmedEmails: { __typename?: "UserEmailConnection"; totalCount: number }; confirmedEmails: { __typename?: "UserEmailConnection"; totalCount: number };
unverifiedEmails: { __typename?: "UserEmailConnection"; totalCount: number };
browserSessions: { browserSessions: {
__typename?: "BrowserSessionConnection"; __typename?: "BrowserSessionConnection";
totalCount: number; totalCount: number;
@@ -1565,6 +1577,48 @@ export const OAuth2Session_SessionFragmentDoc = {
}, },
], ],
} as unknown as DocumentNode<OAuth2Session_SessionFragment, unknown>; } as unknown as DocumentNode<OAuth2Session_SessionFragment, unknown>;
export const UnverifiedEmailAlertFragmentDoc = {
kind: "Document",
definitions: [
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UnverifiedEmailAlert" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "User" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "Field",
alias: { kind: "Name", value: "unverifiedEmails" },
name: { kind: "Name", value: "emails" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "PENDING" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<UnverifiedEmailAlertFragment, unknown>;
export const UserEmail_EmailFragmentDoc = { export const UserEmail_EmailFragmentDoc = {
kind: "Document", kind: "Document",
definitions: [ definitions: [
@@ -1637,29 +1691,6 @@ export const UserHome_UserFragmentDoc = {
], ],
}, },
}, },
{
kind: "Field",
alias: { kind: "Name", value: "unverifiedEmails" },
name: { kind: "Name", value: "emails" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "PENDING" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
{ {
kind: "Field", kind: "Field",
name: { kind: "Name", value: "browserSessions" }, name: { kind: "Name", value: "browserSessions" },
@@ -3058,6 +3089,70 @@ export const UserGreetingDocument = {
], ],
}, },
}, },
{
kind: "Field",
name: { kind: "Name", value: "viewer" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "__typename" } },
{
kind: "InlineFragment",
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "User" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "FragmentSpread",
name: { kind: "Name", value: "UnverifiedEmailAlert" },
},
],
},
},
],
},
},
],
},
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UnverifiedEmailAlert" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "User" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "Field",
alias: { kind: "Name", value: "unverifiedEmails" },
name: { kind: "Name", value: "emails" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "PENDING" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
], ],
}, },
}, },
@@ -3951,29 +4046,6 @@ export const HomeQueryDocument = {
], ],
}, },
}, },
{
kind: "Field",
alias: { kind: "Name", value: "unverifiedEmails" },
name: { kind: "Name", value: "emails" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "PENDING" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
{ {
kind: "Field", kind: "Field",
name: { kind: "Name", value: "browserSessions" }, name: { kind: "Name", value: "browserSessions" },

View File

@@ -0,0 +1,53 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// @vitest-environment happy-dom
import { Provider } from "jotai";
import { useHydrateAtoms } from "jotai/utils";
import { appConfigAtom, locationAtom } from "../Router";
const HydrateLocation: React.FC<React.PropsWithChildren<{ path: string }>> = ({
children,
path,
}) => {
useHydrateAtoms([
[appConfigAtom, { root: "/" }],
[locationAtom, { pathname: path }],
]);
return <>{children}</>;
};
/**
* Utility for testing components that rely on routing or location
* For example any component that includes a <Link />
* Eg:
* ```
* const component = create(
<WithLocation path="/">
<NavItem route={{ type: "home" }}>Active</NavItem>
</WithLocation>,
);
* ```
*/
export const WithLocation: React.FC<
React.PropsWithChildren<{ path?: string }>
> = ({ children, path }) => {
return (
<Provider>
<HydrateLocation path={path || "/"}>{children}</HydrateLocation>
</Provider>
);
};