You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-11-23 11:02:35 +03:00
Finish replacing jotai-urql with bare urql
Mostly remaining was pagination related stuff & fixing tests
This commit is contained in:
@@ -39,7 +39,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app_sessions_list": {
|
"app_sessions_list": {
|
||||||
"error": "Failed to load app sessions",
|
|
||||||
"heading": "Apps"
|
"heading": "Apps"
|
||||||
},
|
},
|
||||||
"browser_session_details": {
|
"browser_session_details": {
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
"jotai": "^2.6.4",
|
"jotai": "^2.6.4",
|
||||||
"jotai-devtools": "^0.7.1",
|
"jotai-devtools": "^0.7.1",
|
||||||
"jotai-location": "^0.5.2",
|
"jotai-location": "^0.5.2",
|
||||||
"jotai-urql": "^0.7.1",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^14.0.5",
|
"react-i18next": "^14.0.5",
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
// 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 { AnyVariables, CombinedError, OperationContext } from "@urql/core";
|
|
||||||
import { atom, WritableAtom } from "jotai";
|
|
||||||
import { useHydrateAtoms } from "jotai/utils";
|
|
||||||
import { AtomWithQuery, clientAtom } from "jotai-urql";
|
|
||||||
import type { ReactElement } from "react";
|
|
||||||
import { useQuery } from "urql";
|
|
||||||
|
|
||||||
import { graphql } from "./gql";
|
|
||||||
import { client } from "./graphql";
|
|
||||||
import { err, ok, Result } from "./result";
|
|
||||||
|
|
||||||
export type GqlResult<T> = Result<T, CombinedError>;
|
|
||||||
export type GqlAtom<T> = WritableAtom<
|
|
||||||
Promise<GqlResult<T>>,
|
|
||||||
[context?: Partial<OperationContext>],
|
|
||||||
void
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map the result of a query atom to a new value, making it a GqlResult
|
|
||||||
*
|
|
||||||
* @param queryAtom: An atom got from atomWithQuery
|
|
||||||
* @param mapper: A function that takes the data from the query and returns a new value
|
|
||||||
*/
|
|
||||||
export const mapQueryAtom = <Data, Variables extends AnyVariables, NewData>(
|
|
||||||
queryAtom: AtomWithQuery<Data, Variables>,
|
|
||||||
mapper: (data: Data) => NewData,
|
|
||||||
): GqlAtom<NewData> => {
|
|
||||||
return atom(
|
|
||||||
async (get): Promise<GqlResult<NewData>> => {
|
|
||||||
const result = await get(queryAtom);
|
|
||||||
if (result.error) {
|
|
||||||
return err(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.data === undefined) {
|
|
||||||
throw new Error("Query result is undefined");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok(mapper(result.data));
|
|
||||||
},
|
|
||||||
|
|
||||||
(_get, set, context) => {
|
|
||||||
set(queryAtom, context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HydrateAtoms: React.FC<{ children: ReactElement }> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
useHydrateAtoms([[clientAtom, client]]);
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
|
||||||
query CurrentViewerQuery {
|
|
||||||
viewer {
|
|
||||||
__typename
|
|
||||||
... on User {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
export const useCurrentUserId = (): string | null => {
|
|
||||||
const [result] = useQuery({ query: CURRENT_VIEWER_QUERY });
|
|
||||||
if (result.error) throw result.error;
|
|
||||||
if (!result.data) throw new Error(); // Suspense mode is enabled
|
|
||||||
return result.data.viewer.__typename === "User"
|
|
||||||
? result.data.viewer.id
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -12,21 +12,12 @@
|
|||||||
// 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 { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { useState, useTransition } from "react";
|
||||||
import { atomFamily } from "jotai/utils";
|
import { useQuery } from "urql";
|
||||||
import { atomWithQuery } from "jotai-urql";
|
|
||||||
import { useTransition } from "react";
|
|
||||||
|
|
||||||
import { mapQueryAtom } from "../atoms";
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
import { SessionState, PageInfo } from "../gql/graphql";
|
import { SessionState } from "../gql/graphql";
|
||||||
import {
|
import { FIRST_PAGE, Pagination, usePages, usePagination } from "../pagination";
|
||||||
atomForCurrentPagination,
|
|
||||||
atomWithPagination,
|
|
||||||
FIRST_PAGE,
|
|
||||||
Pagination,
|
|
||||||
} from "../pagination";
|
|
||||||
import { isOk, unwrap, unwrapOk } from "../result";
|
|
||||||
|
|
||||||
import BlockList from "./BlockList";
|
import BlockList from "./BlockList";
|
||||||
import BrowserSession from "./BrowserSession";
|
import BrowserSession from "./BrowserSession";
|
||||||
@@ -72,52 +63,22 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const filterAtom = atom<SessionState | null>(SessionState.Active);
|
|
||||||
const currentPaginationAtom = atomForCurrentPagination();
|
|
||||||
|
|
||||||
const browserSessionListFamily = atomFamily((userId: string) => {
|
|
||||||
const browserSessionListQuery = atomWithQuery({
|
|
||||||
query: QUERY,
|
|
||||||
getVariables: (get) => ({
|
|
||||||
userId,
|
|
||||||
state: get(filterAtom),
|
|
||||||
...get(currentPaginationAtom),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const browserSessionList = mapQueryAtom(
|
|
||||||
browserSessionListQuery,
|
|
||||||
(data) => data.user?.browserSessions || null,
|
|
||||||
);
|
|
||||||
|
|
||||||
return browserSessionList;
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageInfoFamily = atomFamily((userId: string) => {
|
|
||||||
const pageInfoAtom = atom(async (get): Promise<PageInfo | null> => {
|
|
||||||
const result = await get(browserSessionListFamily(userId));
|
|
||||||
return (isOk(result) && unwrapOk(result)?.pageInfo) || null;
|
|
||||||
});
|
|
||||||
return pageInfoAtom;
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginationFamily = atomFamily((userId: string) => {
|
|
||||||
const paginationAtom = atomWithPagination(
|
|
||||||
currentPaginationAtom,
|
|
||||||
pageInfoFamily(userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
return paginationAtom;
|
|
||||||
});
|
|
||||||
|
|
||||||
const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||||
|
const [pagination, setPagination] = usePagination();
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const result = useAtomValue(browserSessionListFamily(userId));
|
const [filter, setFilter] = useState<SessionState | null>(
|
||||||
const setPagination = useSetAtom(currentPaginationAtom);
|
SessionState.Active,
|
||||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
);
|
||||||
const [filter, setFilter] = useAtom(filterAtom);
|
const [result] = useQuery({
|
||||||
|
query: QUERY,
|
||||||
|
variables: { userId, state: filter, ...pagination },
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
const browserSessions = result.data?.user?.browserSessions;
|
||||||
|
if (!browserSessions) throw new Error(); // Suspense mode is enabled
|
||||||
|
|
||||||
|
const [prevPage, nextPage] = usePages(pagination, browserSessions.pageInfo);
|
||||||
|
|
||||||
const browserSessions = unwrap(result);
|
|
||||||
if (browserSessions === null) return <>Failed to load browser sessions</>;
|
if (browserSessions === null) return <>Failed to load browser sessions</>;
|
||||||
|
|
||||||
const paginate = (pagination: Pagination): void => {
|
const paginate = (pagination: Pagination): void => {
|
||||||
@@ -145,6 +106,7 @@ const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
|||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
disabled={pending}
|
||||||
checked={filter === SessionState.Active}
|
checked={filter === SessionState.Active}
|
||||||
onChange={toggleFilter}
|
onChange={toggleFilter}
|
||||||
/>{" "}
|
/>{" "}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
// @vitest-environment happy-dom
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
import { create } from "react-test-renderer";
|
import { create } from "react-test-renderer";
|
||||||
|
import { Provider } from "urql";
|
||||||
import { describe, expect, it, beforeAll } from "vitest";
|
import { describe, expect, it, beforeAll } from "vitest";
|
||||||
|
import { never } from "wonka";
|
||||||
|
|
||||||
import { makeFragmentData } from "../gql";
|
import { makeFragmentData } from "../gql";
|
||||||
import { WithLocation } from "../test-utils/WithLocation";
|
import { WithLocation } from "../test-utils/WithLocation";
|
||||||
@@ -24,6 +26,10 @@ import { mockLocale } from "../test-utils/mockLocale";
|
|||||||
import CompatSession, { FRAGMENT } from "./CompatSession";
|
import CompatSession, { FRAGMENT } from "./CompatSession";
|
||||||
|
|
||||||
describe("<CompatSession />", () => {
|
describe("<CompatSession />", () => {
|
||||||
|
const mockClient = {
|
||||||
|
executeQuery: (): typeof never => never,
|
||||||
|
};
|
||||||
|
|
||||||
const baseSession = {
|
const baseSession = {
|
||||||
id: "session-id",
|
id: "session-id",
|
||||||
deviceId: "abcd1234",
|
deviceId: "abcd1234",
|
||||||
@@ -42,9 +48,11 @@ describe("<CompatSession />", () => {
|
|||||||
it("renders an active session", () => {
|
it("renders an active session", () => {
|
||||||
const session = makeFragmentData(baseSession, FRAGMENT);
|
const session = makeFragmentData(baseSession, FRAGMENT);
|
||||||
const component = create(
|
const component = create(
|
||||||
<WithLocation>
|
<Provider value={mockClient}>
|
||||||
<CompatSession session={session} />
|
<WithLocation>
|
||||||
</WithLocation>,
|
<CompatSession session={session} />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
expect(component.toJSON()).toMatchSnapshot();
|
expect(component.toJSON()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@@ -58,9 +66,11 @@ describe("<CompatSession />", () => {
|
|||||||
FRAGMENT,
|
FRAGMENT,
|
||||||
);
|
);
|
||||||
const component = create(
|
const component = create(
|
||||||
<WithLocation>
|
<Provider value={mockClient}>
|
||||||
<CompatSession session={session} />
|
<WithLocation>
|
||||||
</WithLocation>,
|
<CompatSession session={session} />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
expect(component.toJSON()).toMatchSnapshot();
|
expect(component.toJSON()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
// @vitest-environment happy-dom
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
|
import { Provider } from "urql";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { never } from "wonka";
|
||||||
|
|
||||||
import { WithLocation } from "../../test-utils/WithLocation";
|
import { WithLocation } from "../../test-utils/WithLocation";
|
||||||
|
|
||||||
@@ -23,10 +25,16 @@ import Layout from "./Layout";
|
|||||||
|
|
||||||
describe("<Layout />", () => {
|
describe("<Layout />", () => {
|
||||||
it("renders app navigation correctly", async () => {
|
it("renders app navigation correctly", async () => {
|
||||||
|
const mockClient = {
|
||||||
|
executeQuery: (): typeof never => never,
|
||||||
|
};
|
||||||
|
|
||||||
const component = render(
|
const component = render(
|
||||||
<WithLocation path="/">
|
<Provider value={mockClient}>
|
||||||
<Layout userId="abc123" />
|
<WithLocation path="/">
|
||||||
</WithLocation>,
|
<Layout userId="abc123" />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await component.findByText("Profile")).toMatchSnapshot();
|
expect(await component.findByText("Profile")).toMatchSnapshot();
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
// @vitest-environment happy-dom
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
import { create } from "react-test-renderer";
|
import { create } from "react-test-renderer";
|
||||||
|
import { Provider } from "urql";
|
||||||
import { describe, expect, it, beforeAll } from "vitest";
|
import { describe, expect, it, beforeAll } from "vitest";
|
||||||
|
import { never } from "wonka";
|
||||||
|
|
||||||
import { makeFragmentData } from "../gql";
|
import { makeFragmentData } from "../gql";
|
||||||
import { Oauth2ApplicationType } from "../gql/graphql";
|
import { Oauth2ApplicationType } from "../gql/graphql";
|
||||||
@@ -25,6 +27,10 @@ import { mockLocale } from "../test-utils/mockLocale";
|
|||||||
import OAuth2Session, { FRAGMENT } from "./OAuth2Session";
|
import OAuth2Session, { FRAGMENT } from "./OAuth2Session";
|
||||||
|
|
||||||
describe("<OAuth2Session />", () => {
|
describe("<OAuth2Session />", () => {
|
||||||
|
const mockClient = {
|
||||||
|
executeQuery: (): typeof never => never,
|
||||||
|
};
|
||||||
|
|
||||||
const defaultSession = {
|
const defaultSession = {
|
||||||
id: "session-id",
|
id: "session-id",
|
||||||
scope:
|
scope:
|
||||||
@@ -48,9 +54,11 @@ describe("<OAuth2Session />", () => {
|
|||||||
const session = makeFragmentData(defaultSession, FRAGMENT);
|
const session = makeFragmentData(defaultSession, FRAGMENT);
|
||||||
|
|
||||||
const component = create(
|
const component = create(
|
||||||
<WithLocation>
|
<Provider value={mockClient}>
|
||||||
<OAuth2Session session={session} />
|
<WithLocation>
|
||||||
</WithLocation>,
|
<OAuth2Session session={session} />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
expect(component.toJSON()).toMatchSnapshot();
|
expect(component.toJSON()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@@ -64,9 +72,11 @@ describe("<OAuth2Session />", () => {
|
|||||||
FRAGMENT,
|
FRAGMENT,
|
||||||
);
|
);
|
||||||
const component = create(
|
const component = create(
|
||||||
<WithLocation>
|
<Provider value={mockClient}>
|
||||||
<OAuth2Session session={session} />
|
<WithLocation>
|
||||||
</WithLocation>,
|
<OAuth2Session session={session} />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
expect(component.toJSON()).toMatchSnapshot();
|
expect(component.toJSON()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@@ -84,9 +94,11 @@ describe("<OAuth2Session />", () => {
|
|||||||
FRAGMENT,
|
FRAGMENT,
|
||||||
);
|
);
|
||||||
const component = create(
|
const component = create(
|
||||||
<WithLocation>
|
<Provider value={mockClient}>
|
||||||
<OAuth2Session session={session} />
|
<WithLocation>
|
||||||
</WithLocation>,
|
<OAuth2Session session={session} />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
expect(component.toJSON()).toMatchSnapshot();
|
expect(component.toJSON()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
// @vitest-environment happy-dom
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
import { render, cleanup } from "@testing-library/react";
|
import { render, cleanup } from "@testing-library/react";
|
||||||
|
import { Provider } from "urql";
|
||||||
import { describe, expect, it, afterEach, beforeAll } from "vitest";
|
import { describe, expect, it, afterEach, beforeAll } from "vitest";
|
||||||
|
import { never } from "wonka";
|
||||||
|
|
||||||
import { makeFragmentData } from "../../gql";
|
import { makeFragmentData } from "../../gql";
|
||||||
import { WithLocation } from "../../test-utils/WithLocation";
|
import { WithLocation } from "../../test-utils/WithLocation";
|
||||||
@@ -24,6 +26,10 @@ import { mockLocale } from "../../test-utils/mockLocale";
|
|||||||
import CompatSessionDetail, { FRAGMENT } from "./CompatSessionDetail";
|
import CompatSessionDetail, { FRAGMENT } from "./CompatSessionDetail";
|
||||||
|
|
||||||
describe("<CompatSessionDetail>", () => {
|
describe("<CompatSessionDetail>", () => {
|
||||||
|
const mockClient = {
|
||||||
|
executeQuery: (): typeof never => never,
|
||||||
|
};
|
||||||
|
|
||||||
const baseSession = {
|
const baseSession = {
|
||||||
id: "session-id",
|
id: "session-id",
|
||||||
deviceId: "abcd1234",
|
deviceId: "abcd1234",
|
||||||
@@ -43,9 +49,11 @@ describe("<CompatSessionDetail>", () => {
|
|||||||
const data = makeFragmentData(baseSession, FRAGMENT);
|
const data = makeFragmentData(baseSession, FRAGMENT);
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<WithLocation>
|
<Provider value={mockClient}>
|
||||||
<CompatSessionDetail session={data} />
|
<WithLocation>
|
||||||
</WithLocation>,
|
<CompatSessionDetail session={data} />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
@@ -61,9 +69,11 @@ describe("<CompatSessionDetail>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<WithLocation>
|
<Provider value={mockClient}>
|
||||||
<CompatSessionDetail session={data} />
|
<WithLocation>
|
||||||
</WithLocation>,
|
<CompatSessionDetail session={data} />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
@@ -79,9 +89,11 @@ describe("<CompatSessionDetail>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { getByText, queryByText } = render(
|
const { getByText, queryByText } = render(
|
||||||
<WithLocation>
|
<Provider value={mockClient}>
|
||||||
<CompatSessionDetail session={data} />
|
<WithLocation>
|
||||||
</WithLocation>,
|
<CompatSessionDetail session={data} />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getByText("Finished")).toBeTruthy();
|
expect(getByText("Finished")).toBeTruthy();
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
// @vitest-environment happy-dom
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
import { render, cleanup } from "@testing-library/react";
|
import { render, cleanup } from "@testing-library/react";
|
||||||
|
import { Provider } from "urql";
|
||||||
import { describe, expect, it, afterEach, beforeAll } from "vitest";
|
import { describe, expect, it, afterEach, beforeAll } from "vitest";
|
||||||
|
import { never } from "wonka";
|
||||||
|
|
||||||
import { makeFragmentData } from "../../gql";
|
import { makeFragmentData } from "../../gql";
|
||||||
import { WithLocation } from "../../test-utils/WithLocation";
|
import { WithLocation } from "../../test-utils/WithLocation";
|
||||||
@@ -24,6 +26,10 @@ import { mockLocale } from "../../test-utils/mockLocale";
|
|||||||
import OAuth2SessionDetail, { FRAGMENT } from "./OAuth2SessionDetail";
|
import OAuth2SessionDetail, { FRAGMENT } from "./OAuth2SessionDetail";
|
||||||
|
|
||||||
describe("<OAuth2SessionDetail>", () => {
|
describe("<OAuth2SessionDetail>", () => {
|
||||||
|
const mockClient = {
|
||||||
|
executeQuery: (): typeof never => never,
|
||||||
|
};
|
||||||
|
|
||||||
const baseSession = {
|
const baseSession = {
|
||||||
id: "session-id",
|
id: "session-id",
|
||||||
scope:
|
scope:
|
||||||
@@ -46,9 +52,11 @@ describe("<OAuth2SessionDetail>", () => {
|
|||||||
const data = makeFragmentData(baseSession, FRAGMENT);
|
const data = makeFragmentData(baseSession, FRAGMENT);
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<WithLocation>
|
<Provider value={mockClient}>
|
||||||
<OAuth2SessionDetail session={data} />
|
<WithLocation>
|
||||||
</WithLocation>,
|
<OAuth2SessionDetail session={data} />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
@@ -64,9 +72,11 @@ describe("<OAuth2SessionDetail>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { getByText, queryByText } = render(
|
const { getByText, queryByText } = render(
|
||||||
<WithLocation>
|
<Provider value={mockClient}>
|
||||||
<OAuth2SessionDetail session={data} />
|
<WithLocation>
|
||||||
</WithLocation>,
|
<OAuth2SessionDetail session={data} />
|
||||||
|
</WithLocation>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getByText("Finished")).toBeTruthy();
|
expect(getByText("Finished")).toBeTruthy();
|
||||||
|
|||||||
@@ -13,19 +13,17 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Alert, H3 } from "@vector-im/compound-web";
|
import { Alert, H3 } from "@vector-im/compound-web";
|
||||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { atomFamily } from "jotai/utils";
|
|
||||||
import { atomWithQuery } from "jotai-urql";
|
|
||||||
import { useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
|
||||||
import { graphql } from "../../gql";
|
import { graphql } from "../../gql";
|
||||||
import { PageInfo } from "../../gql/graphql";
|
|
||||||
import {
|
import {
|
||||||
atomForCurrentPagination,
|
|
||||||
atomWithPagination,
|
|
||||||
FIRST_PAGE,
|
FIRST_PAGE,
|
||||||
Pagination,
|
Pagination,
|
||||||
|
usePages,
|
||||||
|
usePagination,
|
||||||
} from "../../pagination";
|
} from "../../pagination";
|
||||||
import { routeAtom } from "../../routing";
|
import { routeAtom } from "../../routing";
|
||||||
import BlockList from "../BlockList";
|
import BlockList from "../BlockList";
|
||||||
@@ -76,67 +74,31 @@ const PRIMARY_EMAIL_QUERY = graphql(/* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
export const primaryEmailResultFamily = atomFamily((userId: string) => {
|
|
||||||
const primaryEmailResult = atomWithQuery({
|
|
||||||
query: PRIMARY_EMAIL_QUERY,
|
|
||||||
getVariables: () => ({ userId }),
|
|
||||||
});
|
|
||||||
return primaryEmailResult;
|
|
||||||
});
|
|
||||||
|
|
||||||
const primaryEmailIdFamily = atomFamily((userId: string) => {
|
|
||||||
const primaryEmailIdAtom = atom(
|
|
||||||
async (get) => {
|
|
||||||
const result = await get(primaryEmailResultFamily(userId));
|
|
||||||
return result.data?.user?.primaryEmail?.id ?? null;
|
|
||||||
},
|
|
||||||
(get, set) => {
|
|
||||||
set(primaryEmailResultFamily(userId));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return primaryEmailIdAtom;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const currentPaginationAtom = atomForCurrentPagination();
|
|
||||||
|
|
||||||
export const emailPageResultFamily = atomFamily((userId: string) => {
|
|
||||||
const emailPageResult = atomWithQuery({
|
|
||||||
query: QUERY,
|
|
||||||
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
|
|
||||||
});
|
|
||||||
return emailPageResult;
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageInfoFamily = atomFamily((userId: string) => {
|
|
||||||
const pageInfoAtom = atom(async (get): Promise<PageInfo | null> => {
|
|
||||||
const result = await get(emailPageResultFamily(userId));
|
|
||||||
return result.data?.user?.emails?.pageInfo ?? null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return pageInfoAtom;
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginationFamily = atomFamily((userId: string) => {
|
|
||||||
const paginationAtom = atomWithPagination(
|
|
||||||
currentPaginationAtom,
|
|
||||||
pageInfoFamily(userId),
|
|
||||||
);
|
|
||||||
return paginationAtom;
|
|
||||||
});
|
|
||||||
|
|
||||||
const UserEmailList: React.FC<{
|
const UserEmailList: React.FC<{
|
||||||
userId: string;
|
userId: string;
|
||||||
}> = ({ userId }) => {
|
}> = ({ userId }) => {
|
||||||
const [pending, startTransition] = useTransition();
|
|
||||||
const [result, refreshList] = useAtom(emailPageResultFamily(userId));
|
|
||||||
const setPagination = useSetAtom(currentPaginationAtom);
|
|
||||||
const setRoute = useSetAtom(routeAtom);
|
|
||||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
|
||||||
const [primaryEmailId, refreshPrimaryEmailId] = useAtom(
|
|
||||||
primaryEmailIdFamily(userId),
|
|
||||||
);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [pending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const [pagination, setPagination] = usePagination();
|
||||||
|
const [result, refreshList] = useQuery({
|
||||||
|
query: QUERY,
|
||||||
|
variables: { userId, ...pagination },
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
const emails = result.data?.user?.emails;
|
||||||
|
if (!emails) throw new Error(); // Suspense mode is enabled
|
||||||
|
|
||||||
|
const setRoute = useSetAtom(routeAtom);
|
||||||
|
const [prevPage, nextPage] = usePages(pagination, emails.pageInfo);
|
||||||
|
|
||||||
|
const [primaryEmailResult, refreshPrimaryEmail] = useQuery({
|
||||||
|
query: PRIMARY_EMAIL_QUERY,
|
||||||
|
variables: { userId },
|
||||||
|
});
|
||||||
|
if (primaryEmailResult.error) throw primaryEmailResult.error;
|
||||||
|
if (!result.data) throw new Error(); // Suspense mode is enabled
|
||||||
|
const primaryEmailId = primaryEmailResult.data?.user?.primaryEmail?.id;
|
||||||
|
|
||||||
const paginate = (pagination: Pagination): void => {
|
const paginate = (pagination: Pagination): void => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
@@ -168,19 +130,19 @@ const UserEmailList: React.FC<{
|
|||||||
title={t("frontend.user_email_list.no_primary_email_alert")}
|
title={t("frontend.user_email_list.no_primary_email_alert")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{result.data?.user?.emails?.edges?.map((edge) => (
|
{emails.edges.map((edge) => (
|
||||||
<UserEmail
|
<UserEmail
|
||||||
email={edge.node}
|
email={edge.node}
|
||||||
key={edge.cursor}
|
key={edge.cursor}
|
||||||
isPrimary={primaryEmailId === edge.node.id}
|
isPrimary={primaryEmailId === edge.node.id}
|
||||||
onSetPrimary={refreshPrimaryEmailId}
|
onSetPrimary={refreshPrimaryEmail}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<PaginationControls
|
<PaginationControls
|
||||||
autoHide
|
autoHide
|
||||||
count={result.data?.user?.emails?.totalCount ?? 0}
|
count={emails.totalCount ?? 0}
|
||||||
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
||||||
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
|
|||||||
@@ -13,17 +13,26 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Alert, Button, Form } from "@vector-im/compound-web";
|
import { Alert, Button, Form } from "@vector-im/compound-web";
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { useState, ChangeEventHandler } from "react";
|
import { useState, ChangeEventHandler } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "urql";
|
import { useMutation, useQuery } from "urql";
|
||||||
|
|
||||||
import { graphql } from "../../gql";
|
import { graphql } from "../../gql";
|
||||||
import LoadingSpinner from "../LoadingSpinner/LoadingSpinner";
|
import LoadingSpinner from "../LoadingSpinner/LoadingSpinner";
|
||||||
import { userGreetingFamily } from "../UserGreeting";
|
|
||||||
|
|
||||||
import styles from "./UserName.module.css";
|
import styles from "./UserName.module.css";
|
||||||
|
|
||||||
|
const QUERY = graphql(/* GraphQL */ `
|
||||||
|
query UserDisplayName($userId: ID!) {
|
||||||
|
user(id: $userId) {
|
||||||
|
id
|
||||||
|
matrix {
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
const SET_DISPLAYNAME_MUTATION = graphql(/* GraphQL */ `
|
const SET_DISPLAYNAME_MUTATION = graphql(/* GraphQL */ `
|
||||||
mutation SetDisplayName($userId: ID!, $displayName: String) {
|
mutation SetDisplayName($userId: ID!, $displayName: String) {
|
||||||
setDisplayName(input: { userId: $userId, displayName: $displayName }) {
|
setDisplayName(input: { userId: $userId, displayName: $displayName }) {
|
||||||
@@ -51,10 +60,11 @@ const getErrorMessage = (result: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UserName: React.FC<{ userId: string }> = ({ userId }) => {
|
const UserName: React.FC<{ userId: string }> = ({ userId }) => {
|
||||||
const [userGreeting, refreshUserGreeting] = useAtom(
|
const [result, refreshUserGreeting] = useQuery({
|
||||||
userGreetingFamily(userId),
|
query: QUERY,
|
||||||
);
|
variables: { userId },
|
||||||
const displayName = userGreeting.data?.user?.matrix.displayName || "";
|
});
|
||||||
|
const displayName = result.data?.user?.matrix.displayName || "";
|
||||||
|
|
||||||
const [setDisplayNameResult, setDisplayName] = useMutation(
|
const [setDisplayNameResult, setDisplayName] = useMutation(
|
||||||
SET_DISPLAYNAME_MUTATION,
|
SET_DISPLAYNAME_MUTATION,
|
||||||
|
|||||||
@@ -13,21 +13,12 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { H5 } from "@vector-im/compound-web";
|
import { H5 } from "@vector-im/compound-web";
|
||||||
import { atom, useAtomValue, useSetAtom } from "jotai";
|
|
||||||
import { atomFamily } from "jotai/utils";
|
|
||||||
import { atomWithQuery } from "jotai-urql";
|
|
||||||
import { useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
|
||||||
import { mapQueryAtom } from "../../atoms";
|
|
||||||
import { graphql } from "../../gql";
|
import { graphql } from "../../gql";
|
||||||
import { SessionState, PageInfo } from "../../gql/graphql";
|
import { Pagination, usePages, usePagination } from "../../pagination";
|
||||||
import {
|
|
||||||
atomForCurrentPagination,
|
|
||||||
atomWithPagination,
|
|
||||||
Pagination,
|
|
||||||
} from "../../pagination";
|
|
||||||
import { isOk, unwrap, unwrapOk } from "../../result";
|
|
||||||
import BlockList from "../BlockList";
|
import BlockList from "../BlockList";
|
||||||
import CompatSession from "../CompatSession";
|
import CompatSession from "../CompatSession";
|
||||||
import OAuth2Session from "../OAuth2Session";
|
import OAuth2Session from "../OAuth2Session";
|
||||||
@@ -73,44 +64,6 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const filterAtom = atom<SessionState | null>(SessionState.Active);
|
|
||||||
const currentPaginationAtom = atomForCurrentPagination();
|
|
||||||
|
|
||||||
export const appSessionListFamily = atomFamily((userId: string) => {
|
|
||||||
const appSessionListQuery = atomWithQuery({
|
|
||||||
query: QUERY,
|
|
||||||
getVariables: (get) => ({
|
|
||||||
userId,
|
|
||||||
state: get(filterAtom),
|
|
||||||
...get(currentPaginationAtom),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const appSessionList = mapQueryAtom(
|
|
||||||
appSessionListQuery,
|
|
||||||
(data) => data.user?.appSessions || null,
|
|
||||||
);
|
|
||||||
|
|
||||||
return appSessionList;
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageInfoFamily = atomFamily((userId: string) => {
|
|
||||||
const pageInfoAtom = atom(async (get): Promise<PageInfo | null> => {
|
|
||||||
const result = await get(appSessionListFamily(userId));
|
|
||||||
return (isOk(result) && unwrapOk(result)?.pageInfo) || null;
|
|
||||||
});
|
|
||||||
return pageInfoAtom;
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginationFamily = atomFamily((userId: string) => {
|
|
||||||
const paginationAtom = atomWithPagination(
|
|
||||||
currentPaginationAtom,
|
|
||||||
pageInfoFamily(userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
return paginationAtom;
|
|
||||||
});
|
|
||||||
|
|
||||||
// A type-safe way to ensure we've handled all session types
|
// A type-safe way to ensure we've handled all session types
|
||||||
const unknownSessionType = (type: never): never => {
|
const unknownSessionType = (type: never): never => {
|
||||||
throw new Error(`Unknown session type: ${type}`);
|
throw new Error(`Unknown session type: ${type}`);
|
||||||
@@ -118,14 +71,17 @@ const unknownSessionType = (type: never): never => {
|
|||||||
|
|
||||||
const AppSessionsList: React.FC<{ userId: string }> = ({ userId }) => {
|
const AppSessionsList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const result = useAtomValue(appSessionListFamily(userId));
|
const [pagination, setPagination] = usePagination();
|
||||||
const setPagination = useSetAtom(currentPaginationAtom);
|
const [result] = useQuery({
|
||||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
query: QUERY,
|
||||||
|
variables: { userId, ...pagination },
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
const appSessions = result.data?.user?.appSessions;
|
||||||
|
if (!appSessions) throw new Error(); // Suspense mode is enabled
|
||||||
|
const [prevPage, nextPage] = usePages(pagination, appSessions.pageInfo);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const appSessions = unwrap(result);
|
|
||||||
if (!appSessions) return <>{t("frontend.app_sessions_list.error")}</>;
|
|
||||||
|
|
||||||
const paginate = (pagination: Pagination): void => {
|
const paginate = (pagination: Pagination): void => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setPagination(pagination);
|
setPagination(pagination);
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/
|
|||||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||||
*/
|
*/
|
||||||
const documents = {
|
const documents = {
|
||||||
"\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n":
|
|
||||||
types.CurrentViewerQueryDocument,
|
|
||||||
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n":
|
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n":
|
||||||
types.BrowserSession_SessionFragmentDoc,
|
types.BrowserSession_SessionFragmentDoc,
|
||||||
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n":
|
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n":
|
||||||
@@ -57,6 +55,8 @@ const documents = {
|
|||||||
types.UserEmailListQueryDocument,
|
types.UserEmailListQueryDocument,
|
||||||
"\n query UserPrimaryEmail($userId: ID!) {\n user(id: $userId) {\n id\n primaryEmail {\n id\n }\n }\n }\n":
|
"\n query UserPrimaryEmail($userId: ID!) {\n user(id: $userId) {\n id\n primaryEmail {\n id\n }\n }\n }\n":
|
||||||
types.UserPrimaryEmailDocument,
|
types.UserPrimaryEmailDocument,
|
||||||
|
"\n query UserDisplayName($userId: ID!) {\n user(id: $userId) {\n id\n matrix {\n displayName\n }\n }\n }\n":
|
||||||
|
types.UserDisplayNameDocument,
|
||||||
"\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n user {\n id\n matrix {\n displayName\n }\n }\n }\n }\n":
|
"\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n user {\n id\n matrix {\n displayName\n }\n }\n }\n }\n":
|
||||||
types.SetDisplayNameDocument,
|
types.SetDisplayNameDocument,
|
||||||
"\n query AppSessionList(\n $userId: ID!\n $state: SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n appSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: $state\n ) {\n totalCount\n\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
|
"\n query AppSessionList(\n $userId: ID!\n $state: SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n appSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: $state\n ) {\n totalCount\n\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
|
||||||
@@ -79,6 +79,8 @@ const documents = {
|
|||||||
types.VerifyEmailQueryDocument,
|
types.VerifyEmailQueryDocument,
|
||||||
"\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n":
|
"\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n":
|
||||||
types.CurrentViewerSessionQueryDocument,
|
types.CurrentViewerSessionQueryDocument,
|
||||||
|
"\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n":
|
||||||
|
types.CurrentViewerQueryDocument,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,12 +97,6 @@ const documents = {
|
|||||||
*/
|
*/
|
||||||
export function graphql(source: string): unknown;
|
export function graphql(source: string): unknown;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n",
|
|
||||||
): (typeof documents)["\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\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.
|
||||||
*/
|
*/
|
||||||
@@ -227,6 +223,12 @@ export function graphql(
|
|||||||
export function graphql(
|
export function graphql(
|
||||||
source: "\n query UserPrimaryEmail($userId: ID!) {\n user(id: $userId) {\n id\n primaryEmail {\n id\n }\n }\n }\n",
|
source: "\n query UserPrimaryEmail($userId: ID!) {\n user(id: $userId) {\n id\n primaryEmail {\n id\n }\n }\n }\n",
|
||||||
): (typeof documents)["\n query UserPrimaryEmail($userId: ID!) {\n user(id: $userId) {\n id\n primaryEmail {\n id\n }\n }\n }\n"];
|
): (typeof documents)["\n query UserPrimaryEmail($userId: ID!) {\n user(id: $userId) {\n id\n primaryEmail {\n id\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 query UserDisplayName($userId: ID!) {\n user(id: $userId) {\n id\n matrix {\n displayName\n }\n }\n }\n",
|
||||||
|
): (typeof documents)["\n query UserDisplayName($userId: ID!) {\n user(id: $userId) {\n id\n matrix {\n displayName\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.
|
||||||
*/
|
*/
|
||||||
@@ -293,6 +295,12 @@ export function graphql(
|
|||||||
export function graphql(
|
export function graphql(
|
||||||
source: "\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n",
|
source: "\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n",
|
||||||
): (typeof documents)["\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n"];
|
): (typeof documents)["\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\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 query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n",
|
||||||
|
): (typeof documents)["\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n"];
|
||||||
|
|
||||||
export function graphql(source: string) {
|
export function graphql(source: string) {
|
||||||
return (documents as any)[source] ?? {};
|
return (documents as any)[source] ?? {};
|
||||||
|
|||||||
@@ -1107,13 +1107,6 @@ export type Viewer = Anonymous | User;
|
|||||||
/** Represents the current viewer's session */
|
/** Represents the current viewer's session */
|
||||||
export type ViewerSession = Anonymous | BrowserSession | Oauth2Session;
|
export type ViewerSession = Anonymous | BrowserSession | Oauth2Session;
|
||||||
|
|
||||||
export type CurrentViewerQueryQueryVariables = Exact<{ [key: string]: never }>;
|
|
||||||
|
|
||||||
export type CurrentViewerQueryQuery = {
|
|
||||||
__typename?: "Query";
|
|
||||||
viewer: { __typename: "Anonymous" } | { __typename: "User"; id: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BrowserSession_SessionFragment = {
|
export type BrowserSession_SessionFragment = {
|
||||||
__typename?: "BrowserSession";
|
__typename?: "BrowserSession";
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1485,6 +1478,19 @@ export type UserPrimaryEmailQuery = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserDisplayNameQueryVariables = Exact<{
|
||||||
|
userId: Scalars["ID"]["input"];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type UserDisplayNameQuery = {
|
||||||
|
__typename?: "Query";
|
||||||
|
user?: {
|
||||||
|
__typename?: "User";
|
||||||
|
id: string;
|
||||||
|
matrix: { __typename?: "MatrixUser"; displayName?: string | null };
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type SetDisplayNameMutationVariables = Exact<{
|
export type SetDisplayNameMutationVariables = Exact<{
|
||||||
userId: Scalars["ID"]["input"];
|
userId: Scalars["ID"]["input"];
|
||||||
displayName?: InputMaybe<Scalars["String"]["input"]>;
|
displayName?: InputMaybe<Scalars["String"]["input"]>;
|
||||||
@@ -1678,6 +1684,13 @@ export type CurrentViewerSessionQueryQuery = {
|
|||||||
| { __typename: "Oauth2Session" };
|
| { __typename: "Oauth2Session" };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CurrentViewerQueryQueryVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
|
export type CurrentViewerQueryQuery = {
|
||||||
|
__typename?: "Query";
|
||||||
|
viewer: { __typename: "Anonymous" } | { __typename: "User"; id: string };
|
||||||
|
};
|
||||||
|
|
||||||
export const BrowserSession_SessionFragmentDoc = {
|
export const BrowserSession_SessionFragmentDoc = {
|
||||||
kind: "Document",
|
kind: "Document",
|
||||||
definitions: [
|
definitions: [
|
||||||
@@ -2058,47 +2071,6 @@ export const UserEmail_VerifyEmailFragmentDoc = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as unknown as DocumentNode<UserEmail_VerifyEmailFragment, unknown>;
|
} as unknown as DocumentNode<UserEmail_VerifyEmailFragment, unknown>;
|
||||||
export const CurrentViewerQueryDocument = {
|
|
||||||
kind: "Document",
|
|
||||||
definitions: [
|
|
||||||
{
|
|
||||||
kind: "OperationDefinition",
|
|
||||||
operation: "query",
|
|
||||||
name: { kind: "Name", value: "CurrentViewerQuery" },
|
|
||||||
selectionSet: {
|
|
||||||
kind: "SelectionSet",
|
|
||||||
selections: [
|
|
||||||
{
|
|
||||||
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" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as unknown as DocumentNode<
|
|
||||||
CurrentViewerQueryQuery,
|
|
||||||
CurrentViewerQueryQueryVariables
|
|
||||||
>;
|
|
||||||
export const EndBrowserSessionDocument = {
|
export const EndBrowserSessionDocument = {
|
||||||
kind: "Document",
|
kind: "Document",
|
||||||
definitions: [
|
definitions: [
|
||||||
@@ -3442,6 +3414,70 @@ export const UserPrimaryEmailDocument = {
|
|||||||
UserPrimaryEmailQuery,
|
UserPrimaryEmailQuery,
|
||||||
UserPrimaryEmailQueryVariables
|
UserPrimaryEmailQueryVariables
|
||||||
>;
|
>;
|
||||||
|
export const UserDisplayNameDocument = {
|
||||||
|
kind: "Document",
|
||||||
|
definitions: [
|
||||||
|
{
|
||||||
|
kind: "OperationDefinition",
|
||||||
|
operation: "query",
|
||||||
|
name: { kind: "Name", value: "UserDisplayName" },
|
||||||
|
variableDefinitions: [
|
||||||
|
{
|
||||||
|
kind: "VariableDefinition",
|
||||||
|
variable: {
|
||||||
|
kind: "Variable",
|
||||||
|
name: { kind: "Name", value: "userId" },
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
kind: "NonNullType",
|
||||||
|
type: { kind: "NamedType", name: { kind: "Name", value: "ID" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "user" },
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
kind: "Argument",
|
||||||
|
name: { kind: "Name", value: "id" },
|
||||||
|
value: {
|
||||||
|
kind: "Variable",
|
||||||
|
name: { kind: "Name", value: "userId" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "matrix" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "displayName" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as DocumentNode<
|
||||||
|
UserDisplayNameQuery,
|
||||||
|
UserDisplayNameQueryVariables
|
||||||
|
>;
|
||||||
export const SetDisplayNameDocument = {
|
export const SetDisplayNameDocument = {
|
||||||
kind: "Document",
|
kind: "Document",
|
||||||
definitions: [
|
definitions: [
|
||||||
@@ -4407,3 +4443,44 @@ export const CurrentViewerSessionQueryDocument = {
|
|||||||
CurrentViewerSessionQueryQuery,
|
CurrentViewerSessionQueryQuery,
|
||||||
CurrentViewerSessionQueryQueryVariables
|
CurrentViewerSessionQueryQueryVariables
|
||||||
>;
|
>;
|
||||||
|
export const CurrentViewerQueryDocument = {
|
||||||
|
kind: "Document",
|
||||||
|
definitions: [
|
||||||
|
{
|
||||||
|
kind: "OperationDefinition",
|
||||||
|
operation: "query",
|
||||||
|
name: { kind: "Name", value: "CurrentViewerQuery" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{
|
||||||
|
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" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as DocumentNode<
|
||||||
|
CurrentViewerQueryQuery,
|
||||||
|
CurrentViewerQueryQueryVariables
|
||||||
|
>;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { createRoot } from "react-dom/client";
|
|||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
import { Provider as UrqlProvider } from "urql";
|
import { Provider as UrqlProvider } from "urql";
|
||||||
|
|
||||||
import { HydrateAtoms, useCurrentUserId } from "./atoms";
|
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import Layout from "./components/Layout";
|
import Layout from "./components/Layout";
|
||||||
import LoadingScreen from "./components/LoadingScreen";
|
import LoadingScreen from "./components/LoadingScreen";
|
||||||
@@ -30,6 +29,7 @@ import { client } from "./graphql";
|
|||||||
import i18n from "./i18n";
|
import i18n from "./i18n";
|
||||||
import { Router } from "./routing";
|
import { Router } from "./routing";
|
||||||
import "./main.css";
|
import "./main.css";
|
||||||
|
import { useCurrentUserId } from "./utils/useCurrentUserId";
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const userId = useCurrentUserId();
|
const userId = useCurrentUserId();
|
||||||
@@ -50,15 +50,13 @@ createRoot(document.getElementById("root") as HTMLElement).render(
|
|||||||
<UrqlProvider value={client}>
|
<UrqlProvider value={client}>
|
||||||
<Provider>
|
<Provider>
|
||||||
{import.meta.env.DEV && <DevTools />}
|
{import.meta.env.DEV && <DevTools />}
|
||||||
<HydrateAtoms>
|
<Suspense fallback={<LoadingScreen />}>
|
||||||
<Suspense fallback={<LoadingScreen />}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<TooltipProvider>
|
||||||
<TooltipProvider>
|
<App />
|
||||||
<App />
|
</TooltipProvider>
|
||||||
</TooltipProvider>
|
</I18nextProvider>
|
||||||
</I18nextProvider>
|
</Suspense>
|
||||||
</Suspense>
|
|
||||||
</HydrateAtoms>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
</UrqlProvider>
|
</UrqlProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -12,13 +12,12 @@
|
|||||||
// 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 { atom, Atom, WritableAtom } from "jotai";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { PageInfo } from "./gql/graphql";
|
import { PageInfo } from "./gql/graphql";
|
||||||
|
|
||||||
export const FIRST_PAGE = Symbol("FIRST_PAGE");
|
export const FIRST_PAGE = Symbol("FIRST_PAGE");
|
||||||
export const LAST_PAGE = Symbol("LAST_PAGE");
|
export const LAST_PAGE = Symbol("LAST_PAGE");
|
||||||
const EMPTY = Symbol("EMPTY");
|
|
||||||
|
|
||||||
export type ForwardPagination = {
|
export type ForwardPagination = {
|
||||||
first: number;
|
first: number;
|
||||||
@@ -36,99 +35,73 @@ export type Pagination = ForwardPagination | BackwardPagination;
|
|||||||
export const isForwardPagination = (
|
export const isForwardPagination = (
|
||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
): pagination is ForwardPagination => {
|
): pagination is ForwardPagination => {
|
||||||
return pagination.hasOwnProperty("first");
|
return Object.hasOwn(pagination, "first");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if the pagination is backward pagination.
|
// Check if the pagination is backward pagination.
|
||||||
export const isBackwardPagination = (
|
export const isBackwardPagination = (
|
||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
): pagination is BackwardPagination => {
|
): pagination is BackwardPagination => {
|
||||||
return pagination.hasOwnProperty("last");
|
return Object.hasOwn(pagination, "last");
|
||||||
};
|
};
|
||||||
|
|
||||||
// This atom sets the default page size for pagination.
|
|
||||||
export const pageSizeAtom = atom(6);
|
|
||||||
|
|
||||||
type Action = typeof FIRST_PAGE | typeof LAST_PAGE | Pagination;
|
type Action = typeof FIRST_PAGE | typeof LAST_PAGE | Pagination;
|
||||||
|
|
||||||
export const atomForCurrentPagination = (): WritableAtom<
|
// Hook to handle pagination state.
|
||||||
Pagination,
|
export const usePagination = (
|
||||||
[Action],
|
pageSize = 6,
|
||||||
void
|
): [Pagination, (action: Action) => void] => {
|
||||||
> => {
|
const [pagination, setPagination] = useState<Pagination>({
|
||||||
const dataAtom = atom<typeof EMPTY | Pagination>(EMPTY);
|
first: pageSize,
|
||||||
|
after: null,
|
||||||
|
});
|
||||||
|
|
||||||
const currentPaginationAtom = atom(
|
const handlePagination = (action: Action): void => {
|
||||||
(get) => {
|
if (action === FIRST_PAGE) {
|
||||||
const data = get(dataAtom);
|
setPagination({
|
||||||
if (data === EMPTY) {
|
first: pageSize,
|
||||||
return {
|
after: null,
|
||||||
first: get(pageSizeAtom),
|
});
|
||||||
after: null,
|
} else if (action === LAST_PAGE) {
|
||||||
};
|
setPagination({
|
||||||
}
|
last: pageSize,
|
||||||
|
before: null,
|
||||||
return data;
|
});
|
||||||
},
|
} else {
|
||||||
(get, set, action: Action) => {
|
setPagination(action);
|
||||||
if (action === FIRST_PAGE) {
|
}
|
||||||
set(dataAtom, EMPTY);
|
|
||||||
} else if (action === LAST_PAGE) {
|
|
||||||
set(dataAtom, {
|
|
||||||
last: get(pageSizeAtom),
|
|
||||||
before: null,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
set(dataAtom, action);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
currentPaginationAtom.onMount = (setAtom): void => {
|
|
||||||
setAtom(FIRST_PAGE);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return currentPaginationAtom;
|
return [pagination, handlePagination];
|
||||||
};
|
};
|
||||||
|
|
||||||
// This atom is used to create a pagination atom that gives the previous and
|
// Compute the previous and next pagination based on the current pagination and the page info.
|
||||||
// next pagination objects, given the current pagination and the page info.
|
export const usePages = (
|
||||||
export const atomWithPagination = (
|
currentPagination: Pagination,
|
||||||
currentPaginationAtom: Atom<Pagination>,
|
pageInfo: PageInfo | null,
|
||||||
pageInfoAtom: Atom<Promise<PageInfo | null>>,
|
pageSize = 6,
|
||||||
): Atom<Promise<[BackwardPagination | null, ForwardPagination | null]>> => {
|
): [BackwardPagination | null, ForwardPagination | null] => {
|
||||||
const paginationAtom = atom(
|
const hasProbablyPreviousPage =
|
||||||
async (
|
isForwardPagination(currentPagination) && currentPagination.after !== null;
|
||||||
get,
|
const hasProbablyNextPage =
|
||||||
): Promise<[BackwardPagination | null, ForwardPagination | null]> => {
|
isBackwardPagination(currentPagination) &&
|
||||||
const currentPagination = get(currentPaginationAtom);
|
currentPagination.before !== null;
|
||||||
const pageInfo = await get(pageInfoAtom);
|
|
||||||
const hasProbablyPreviousPage =
|
|
||||||
isForwardPagination(currentPagination) &&
|
|
||||||
currentPagination.after !== null;
|
|
||||||
const hasProbablyNextPage =
|
|
||||||
isBackwardPagination(currentPagination) &&
|
|
||||||
currentPagination.before !== null;
|
|
||||||
|
|
||||||
let previousPagination: BackwardPagination | null = null;
|
let previousPagination: BackwardPagination | null = null;
|
||||||
let nextPagination: ForwardPagination | null = null;
|
let nextPagination: ForwardPagination | null = null;
|
||||||
if (pageInfo?.hasPreviousPage || hasProbablyPreviousPage) {
|
if (pageInfo?.hasPreviousPage || hasProbablyPreviousPage) {
|
||||||
previousPagination = {
|
previousPagination = {
|
||||||
last: get(pageSizeAtom),
|
last: pageSize,
|
||||||
before: pageInfo?.startCursor ?? null,
|
before: pageInfo?.startCursor ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageInfo?.hasNextPage || hasProbablyNextPage) {
|
if (pageInfo?.hasNextPage || hasProbablyNextPage) {
|
||||||
nextPagination = {
|
nextPagination = {
|
||||||
first: get(pageSizeAtom),
|
first: pageSize,
|
||||||
after: pageInfo?.endCursor ?? null,
|
after: pageInfo?.endCursor ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return [previousPagination, nextPagination];
|
return [previousPagination, nextPagination];
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return paginationAtom;
|
|
||||||
};
|
};
|
||||||
|
|||||||
37
frontend/src/utils/useCurrentUserId.tsx
Normal file
37
frontend/src/utils/useCurrentUserId.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// 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 { useQuery } from "urql";
|
||||||
|
|
||||||
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
|
const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||||
|
query CurrentViewerQuery {
|
||||||
|
viewer {
|
||||||
|
__typename
|
||||||
|
... on User {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const useCurrentUserId = (): string | null => {
|
||||||
|
const [result] = useQuery({ query: CURRENT_VIEWER_QUERY });
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
if (!result.data) throw new Error(); // Suspense mode is enabled
|
||||||
|
return result.data.viewer.__typename === "User"
|
||||||
|
? result.data.viewer.id
|
||||||
|
: null;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user