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": {
|
||||
"error": "Failed to load app sessions",
|
||||
"heading": "Apps"
|
||||
},
|
||||
"browser_session_details": {
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"jotai": "^2.6.4",
|
||||
"jotai-devtools": "^0.7.1",
|
||||
"jotai-location": "^0.5.2",
|
||||
"jotai-urql": "^0.7.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"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
|
||||
// limitations under the License.
|
||||
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
import { useTransition } from "react";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import { graphql } from "../gql";
|
||||
import { SessionState, PageInfo } from "../gql/graphql";
|
||||
import {
|
||||
atomForCurrentPagination,
|
||||
atomWithPagination,
|
||||
FIRST_PAGE,
|
||||
Pagination,
|
||||
} from "../pagination";
|
||||
import { isOk, unwrap, unwrapOk } from "../result";
|
||||
import { SessionState } from "../gql/graphql";
|
||||
import { FIRST_PAGE, Pagination, usePages, usePagination } from "../pagination";
|
||||
|
||||
import BlockList from "./BlockList";
|
||||
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 [pagination, setPagination] = usePagination();
|
||||
const [pending, startTransition] = useTransition();
|
||||
const result = useAtomValue(browserSessionListFamily(userId));
|
||||
const setPagination = useSetAtom(currentPaginationAtom);
|
||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
||||
const [filter, setFilter] = useAtom(filterAtom);
|
||||
const [filter, setFilter] = useState<SessionState | null>(
|
||||
SessionState.Active,
|
||||
);
|
||||
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</>;
|
||||
|
||||
const paginate = (pagination: Pagination): void => {
|
||||
@@ -145,6 +106,7 @@ const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={pending}
|
||||
checked={filter === SessionState.Active}
|
||||
onChange={toggleFilter}
|
||||
/>{" "}
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { create } from "react-test-renderer";
|
||||
import { Provider } from "urql";
|
||||
import { describe, expect, it, beforeAll } from "vitest";
|
||||
import { never } from "wonka";
|
||||
|
||||
import { makeFragmentData } from "../gql";
|
||||
import { WithLocation } from "../test-utils/WithLocation";
|
||||
@@ -24,6 +26,10 @@ import { mockLocale } from "../test-utils/mockLocale";
|
||||
import CompatSession, { FRAGMENT } from "./CompatSession";
|
||||
|
||||
describe("<CompatSession />", () => {
|
||||
const mockClient = {
|
||||
executeQuery: (): typeof never => never,
|
||||
};
|
||||
|
||||
const baseSession = {
|
||||
id: "session-id",
|
||||
deviceId: "abcd1234",
|
||||
@@ -42,9 +48,11 @@ describe("<CompatSession />", () => {
|
||||
it("renders an active session", () => {
|
||||
const session = makeFragmentData(baseSession, FRAGMENT);
|
||||
const component = create(
|
||||
<WithLocation>
|
||||
<CompatSession session={session} />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation>
|
||||
<CompatSession session={session} />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
expect(component.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
@@ -58,9 +66,11 @@ describe("<CompatSession />", () => {
|
||||
FRAGMENT,
|
||||
);
|
||||
const component = create(
|
||||
<WithLocation>
|
||||
<CompatSession session={session} />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation>
|
||||
<CompatSession session={session} />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
expect(component.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { Provider } from "urql";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { never } from "wonka";
|
||||
|
||||
import { WithLocation } from "../../test-utils/WithLocation";
|
||||
|
||||
@@ -23,10 +25,16 @@ import Layout from "./Layout";
|
||||
|
||||
describe("<Layout />", () => {
|
||||
it("renders app navigation correctly", async () => {
|
||||
const mockClient = {
|
||||
executeQuery: (): typeof never => never,
|
||||
};
|
||||
|
||||
const component = render(
|
||||
<WithLocation path="/">
|
||||
<Layout userId="abc123" />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation path="/">
|
||||
<Layout userId="abc123" />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(await component.findByText("Profile")).toMatchSnapshot();
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { create } from "react-test-renderer";
|
||||
import { Provider } from "urql";
|
||||
import { describe, expect, it, beforeAll } from "vitest";
|
||||
import { never } from "wonka";
|
||||
|
||||
import { makeFragmentData } from "../gql";
|
||||
import { Oauth2ApplicationType } from "../gql/graphql";
|
||||
@@ -25,6 +27,10 @@ import { mockLocale } from "../test-utils/mockLocale";
|
||||
import OAuth2Session, { FRAGMENT } from "./OAuth2Session";
|
||||
|
||||
describe("<OAuth2Session />", () => {
|
||||
const mockClient = {
|
||||
executeQuery: (): typeof never => never,
|
||||
};
|
||||
|
||||
const defaultSession = {
|
||||
id: "session-id",
|
||||
scope:
|
||||
@@ -48,9 +54,11 @@ describe("<OAuth2Session />", () => {
|
||||
const session = makeFragmentData(defaultSession, FRAGMENT);
|
||||
|
||||
const component = create(
|
||||
<WithLocation>
|
||||
<OAuth2Session session={session} />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation>
|
||||
<OAuth2Session session={session} />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
expect(component.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
@@ -64,9 +72,11 @@ describe("<OAuth2Session />", () => {
|
||||
FRAGMENT,
|
||||
);
|
||||
const component = create(
|
||||
<WithLocation>
|
||||
<OAuth2Session session={session} />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation>
|
||||
<OAuth2Session session={session} />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
expect(component.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
@@ -84,9 +94,11 @@ describe("<OAuth2Session />", () => {
|
||||
FRAGMENT,
|
||||
);
|
||||
const component = create(
|
||||
<WithLocation>
|
||||
<OAuth2Session session={session} />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation>
|
||||
<OAuth2Session session={session} />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
expect(component.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { render, cleanup } from "@testing-library/react";
|
||||
import { Provider } from "urql";
|
||||
import { describe, expect, it, afterEach, beforeAll } from "vitest";
|
||||
import { never } from "wonka";
|
||||
|
||||
import { makeFragmentData } from "../../gql";
|
||||
import { WithLocation } from "../../test-utils/WithLocation";
|
||||
@@ -24,6 +26,10 @@ import { mockLocale } from "../../test-utils/mockLocale";
|
||||
import CompatSessionDetail, { FRAGMENT } from "./CompatSessionDetail";
|
||||
|
||||
describe("<CompatSessionDetail>", () => {
|
||||
const mockClient = {
|
||||
executeQuery: (): typeof never => never,
|
||||
};
|
||||
|
||||
const baseSession = {
|
||||
id: "session-id",
|
||||
deviceId: "abcd1234",
|
||||
@@ -43,9 +49,11 @@ describe("<CompatSessionDetail>", () => {
|
||||
const data = makeFragmentData(baseSession, FRAGMENT);
|
||||
|
||||
const { container } = render(
|
||||
<WithLocation>
|
||||
<CompatSessionDetail session={data} />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation>
|
||||
<CompatSessionDetail session={data} />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
@@ -61,9 +69,11 @@ describe("<CompatSessionDetail>", () => {
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<WithLocation>
|
||||
<CompatSessionDetail session={data} />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation>
|
||||
<CompatSessionDetail session={data} />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
@@ -79,9 +89,11 @@ describe("<CompatSessionDetail>", () => {
|
||||
);
|
||||
|
||||
const { getByText, queryByText } = render(
|
||||
<WithLocation>
|
||||
<CompatSessionDetail session={data} />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation>
|
||||
<CompatSessionDetail session={data} />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(getByText("Finished")).toBeTruthy();
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { render, cleanup } from "@testing-library/react";
|
||||
import { Provider } from "urql";
|
||||
import { describe, expect, it, afterEach, beforeAll } from "vitest";
|
||||
import { never } from "wonka";
|
||||
|
||||
import { makeFragmentData } from "../../gql";
|
||||
import { WithLocation } from "../../test-utils/WithLocation";
|
||||
@@ -24,6 +26,10 @@ import { mockLocale } from "../../test-utils/mockLocale";
|
||||
import OAuth2SessionDetail, { FRAGMENT } from "./OAuth2SessionDetail";
|
||||
|
||||
describe("<OAuth2SessionDetail>", () => {
|
||||
const mockClient = {
|
||||
executeQuery: (): typeof never => never,
|
||||
};
|
||||
|
||||
const baseSession = {
|
||||
id: "session-id",
|
||||
scope:
|
||||
@@ -46,9 +52,11 @@ describe("<OAuth2SessionDetail>", () => {
|
||||
const data = makeFragmentData(baseSession, FRAGMENT);
|
||||
|
||||
const { container } = render(
|
||||
<WithLocation>
|
||||
<OAuth2SessionDetail session={data} />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation>
|
||||
<OAuth2SessionDetail session={data} />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
@@ -64,9 +72,11 @@ describe("<OAuth2SessionDetail>", () => {
|
||||
);
|
||||
|
||||
const { getByText, queryByText } = render(
|
||||
<WithLocation>
|
||||
<OAuth2SessionDetail session={data} />
|
||||
</WithLocation>,
|
||||
<Provider value={mockClient}>
|
||||
<WithLocation>
|
||||
<OAuth2SessionDetail session={data} />
|
||||
</WithLocation>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(getByText("Finished")).toBeTruthy();
|
||||
|
||||
@@ -13,19 +13,17 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { Alert, H3 } from "@vector-im/compound-web";
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useTransition } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { graphql } from "../../gql";
|
||||
import { PageInfo } from "../../gql/graphql";
|
||||
import {
|
||||
atomForCurrentPagination,
|
||||
atomWithPagination,
|
||||
FIRST_PAGE,
|
||||
Pagination,
|
||||
usePages,
|
||||
usePagination,
|
||||
} from "../../pagination";
|
||||
import { routeAtom } from "../../routing";
|
||||
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<{
|
||||
userId: string;
|
||||
}> = ({ 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 [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 => {
|
||||
startTransition(() => {
|
||||
@@ -168,19 +130,19 @@ const UserEmailList: React.FC<{
|
||||
title={t("frontend.user_email_list.no_primary_email_alert")}
|
||||
/>
|
||||
)}
|
||||
{result.data?.user?.emails?.edges?.map((edge) => (
|
||||
{emails.edges.map((edge) => (
|
||||
<UserEmail
|
||||
email={edge.node}
|
||||
key={edge.cursor}
|
||||
isPrimary={primaryEmailId === edge.node.id}
|
||||
onSetPrimary={refreshPrimaryEmailId}
|
||||
onSetPrimary={refreshPrimaryEmail}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
|
||||
<PaginationControls
|
||||
autoHide
|
||||
count={result.data?.user?.emails?.totalCount ?? 0}
|
||||
count={emails.totalCount ?? 0}
|
||||
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
||||
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
||||
disabled={pending}
|
||||
|
||||
@@ -13,17 +13,26 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { Alert, Button, Form } from "@vector-im/compound-web";
|
||||
import { useAtom } from "jotai";
|
||||
import { useState, ChangeEventHandler } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation } from "urql";
|
||||
import { useMutation, useQuery } from "urql";
|
||||
|
||||
import { graphql } from "../../gql";
|
||||
import LoadingSpinner from "../LoadingSpinner/LoadingSpinner";
|
||||
import { userGreetingFamily } from "../UserGreeting";
|
||||
|
||||
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 */ `
|
||||
mutation SetDisplayName($userId: ID!, $displayName: String) {
|
||||
setDisplayName(input: { userId: $userId, displayName: $displayName }) {
|
||||
@@ -51,10 +60,11 @@ const getErrorMessage = (result: {
|
||||
};
|
||||
|
||||
const UserName: React.FC<{ userId: string }> = ({ userId }) => {
|
||||
const [userGreeting, refreshUserGreeting] = useAtom(
|
||||
userGreetingFamily(userId),
|
||||
);
|
||||
const displayName = userGreeting.data?.user?.matrix.displayName || "";
|
||||
const [result, refreshUserGreeting] = useQuery({
|
||||
query: QUERY,
|
||||
variables: { userId },
|
||||
});
|
||||
const displayName = result.data?.user?.matrix.displayName || "";
|
||||
|
||||
const [setDisplayNameResult, setDisplayName] = useMutation(
|
||||
SET_DISPLAYNAME_MUTATION,
|
||||
|
||||
@@ -13,21 +13,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
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 { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { mapQueryAtom } from "../../atoms";
|
||||
import { graphql } from "../../gql";
|
||||
import { SessionState, PageInfo } from "../../gql/graphql";
|
||||
import {
|
||||
atomForCurrentPagination,
|
||||
atomWithPagination,
|
||||
Pagination,
|
||||
} from "../../pagination";
|
||||
import { isOk, unwrap, unwrapOk } from "../../result";
|
||||
import { Pagination, usePages, usePagination } from "../../pagination";
|
||||
import BlockList from "../BlockList";
|
||||
import CompatSession from "../CompatSession";
|
||||
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
|
||||
const unknownSessionType = (type: never): never => {
|
||||
throw new Error(`Unknown session type: ${type}`);
|
||||
@@ -118,14 +71,17 @@ const unknownSessionType = (type: never): never => {
|
||||
|
||||
const AppSessionsList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const result = useAtomValue(appSessionListFamily(userId));
|
||||
const setPagination = useSetAtom(currentPaginationAtom);
|
||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
||||
const [pagination, setPagination] = usePagination();
|
||||
const [result] = useQuery({
|
||||
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 appSessions = unwrap(result);
|
||||
if (!appSessions) return <>{t("frontend.app_sessions_list.error")}</>;
|
||||
|
||||
const paginate = (pagination: Pagination): void => {
|
||||
startTransition(() => {
|
||||
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.
|
||||
*/
|
||||
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":
|
||||
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":
|
||||
@@ -57,6 +55,8 @@ const documents = {
|
||||
types.UserEmailListQueryDocument,
|
||||
"\n query UserPrimaryEmail($userId: ID!) {\n user(id: $userId) {\n id\n primaryEmail {\n id\n }\n }\n }\n":
|
||||
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":
|
||||
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":
|
||||
@@ -79,6 +79,8 @@ const documents = {
|
||||
types.VerifyEmailQueryDocument,
|
||||
"\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n":
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -227,6 +223,12 @@ 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",
|
||||
): (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.
|
||||
*/
|
||||
@@ -293,6 +295,12 @@ export function graphql(
|
||||
export function graphql(
|
||||
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"];
|
||||
/**
|
||||
* 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) {
|
||||
return (documents as any)[source] ?? {};
|
||||
|
||||
@@ -1107,13 +1107,6 @@ export type Viewer = Anonymous | User;
|
||||
/** Represents the current viewer's session */
|
||||
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 = {
|
||||
__typename?: "BrowserSession";
|
||||
id: string;
|
||||
@@ -1485,6 +1478,19 @@ export type UserPrimaryEmailQuery = {
|
||||
} | 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<{
|
||||
userId: Scalars["ID"]["input"];
|
||||
displayName?: InputMaybe<Scalars["String"]["input"]>;
|
||||
@@ -1678,6 +1684,13 @@ export type CurrentViewerSessionQueryQuery = {
|
||||
| { __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 = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
@@ -2058,47 +2071,6 @@ export const UserEmail_VerifyEmailFragmentDoc = {
|
||||
},
|
||||
],
|
||||
} 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 = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
@@ -3442,6 +3414,70 @@ export const UserPrimaryEmailDocument = {
|
||||
UserPrimaryEmailQuery,
|
||||
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 = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
@@ -4407,3 +4443,44 @@ export const CurrentViewerSessionQueryDocument = {
|
||||
CurrentViewerSessionQueryQuery,
|
||||
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 { Provider as UrqlProvider } from "urql";
|
||||
|
||||
import { HydrateAtoms, useCurrentUserId } from "./atoms";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import Layout from "./components/Layout";
|
||||
import LoadingScreen from "./components/LoadingScreen";
|
||||
@@ -30,6 +29,7 @@ import { client } from "./graphql";
|
||||
import i18n from "./i18n";
|
||||
import { Router } from "./routing";
|
||||
import "./main.css";
|
||||
import { useCurrentUserId } from "./utils/useCurrentUserId";
|
||||
|
||||
const App: React.FC = () => {
|
||||
const userId = useCurrentUserId();
|
||||
@@ -50,15 +50,13 @@ createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<UrqlProvider value={client}>
|
||||
<Provider>
|
||||
{import.meta.env.DEV && <DevTools />}
|
||||
<HydrateAtoms>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<TooltipProvider>
|
||||
<App />
|
||||
</TooltipProvider>
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
</HydrateAtoms>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<TooltipProvider>
|
||||
<App />
|
||||
</TooltipProvider>
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
</Provider>
|
||||
</UrqlProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -12,13 +12,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { atom, Atom, WritableAtom } from "jotai";
|
||||
import { useState } from "react";
|
||||
|
||||
import { PageInfo } from "./gql/graphql";
|
||||
|
||||
export const FIRST_PAGE = Symbol("FIRST_PAGE");
|
||||
export const LAST_PAGE = Symbol("LAST_PAGE");
|
||||
const EMPTY = Symbol("EMPTY");
|
||||
|
||||
export type ForwardPagination = {
|
||||
first: number;
|
||||
@@ -36,99 +35,73 @@ export type Pagination = ForwardPagination | BackwardPagination;
|
||||
export const isForwardPagination = (
|
||||
pagination: Pagination,
|
||||
): pagination is ForwardPagination => {
|
||||
return pagination.hasOwnProperty("first");
|
||||
return Object.hasOwn(pagination, "first");
|
||||
};
|
||||
|
||||
// Check if the pagination is backward pagination.
|
||||
export const isBackwardPagination = (
|
||||
pagination: Pagination,
|
||||
): 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;
|
||||
|
||||
export const atomForCurrentPagination = (): WritableAtom<
|
||||
Pagination,
|
||||
[Action],
|
||||
void
|
||||
> => {
|
||||
const dataAtom = atom<typeof EMPTY | Pagination>(EMPTY);
|
||||
// Hook to handle pagination state.
|
||||
export const usePagination = (
|
||||
pageSize = 6,
|
||||
): [Pagination, (action: Action) => void] => {
|
||||
const [pagination, setPagination] = useState<Pagination>({
|
||||
first: pageSize,
|
||||
after: null,
|
||||
});
|
||||
|
||||
const currentPaginationAtom = atom(
|
||||
(get) => {
|
||||
const data = get(dataAtom);
|
||||
if (data === EMPTY) {
|
||||
return {
|
||||
first: get(pageSizeAtom),
|
||||
after: null,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
(get, set, action: 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);
|
||||
const handlePagination = (action: Action): void => {
|
||||
if (action === FIRST_PAGE) {
|
||||
setPagination({
|
||||
first: pageSize,
|
||||
after: null,
|
||||
});
|
||||
} else if (action === LAST_PAGE) {
|
||||
setPagination({
|
||||
last: pageSize,
|
||||
before: null,
|
||||
});
|
||||
} else {
|
||||
setPagination(action);
|
||||
}
|
||||
};
|
||||
|
||||
return currentPaginationAtom;
|
||||
return [pagination, handlePagination];
|
||||
};
|
||||
|
||||
// This atom is used to create a pagination atom that gives the previous and
|
||||
// next pagination objects, given the current pagination and the page info.
|
||||
export const atomWithPagination = (
|
||||
currentPaginationAtom: Atom<Pagination>,
|
||||
pageInfoAtom: Atom<Promise<PageInfo | null>>,
|
||||
): Atom<Promise<[BackwardPagination | null, ForwardPagination | null]>> => {
|
||||
const paginationAtom = atom(
|
||||
async (
|
||||
get,
|
||||
): Promise<[BackwardPagination | null, ForwardPagination | null]> => {
|
||||
const currentPagination = get(currentPaginationAtom);
|
||||
const pageInfo = await get(pageInfoAtom);
|
||||
const hasProbablyPreviousPage =
|
||||
isForwardPagination(currentPagination) &&
|
||||
currentPagination.after !== null;
|
||||
const hasProbablyNextPage =
|
||||
isBackwardPagination(currentPagination) &&
|
||||
currentPagination.before !== null;
|
||||
// Compute the previous and next pagination based on the current pagination and the page info.
|
||||
export const usePages = (
|
||||
currentPagination: Pagination,
|
||||
pageInfo: PageInfo | null,
|
||||
pageSize = 6,
|
||||
): [BackwardPagination | null, ForwardPagination | null] => {
|
||||
const hasProbablyPreviousPage =
|
||||
isForwardPagination(currentPagination) && currentPagination.after !== null;
|
||||
const hasProbablyNextPage =
|
||||
isBackwardPagination(currentPagination) &&
|
||||
currentPagination.before !== null;
|
||||
|
||||
let previousPagination: BackwardPagination | null = null;
|
||||
let nextPagination: ForwardPagination | null = null;
|
||||
if (pageInfo?.hasPreviousPage || hasProbablyPreviousPage) {
|
||||
previousPagination = {
|
||||
last: get(pageSizeAtom),
|
||||
before: pageInfo?.startCursor ?? null,
|
||||
};
|
||||
}
|
||||
let previousPagination: BackwardPagination | null = null;
|
||||
let nextPagination: ForwardPagination | null = null;
|
||||
if (pageInfo?.hasPreviousPage || hasProbablyPreviousPage) {
|
||||
previousPagination = {
|
||||
last: pageSize,
|
||||
before: pageInfo?.startCursor ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (pageInfo?.hasNextPage || hasProbablyNextPage) {
|
||||
nextPagination = {
|
||||
first: get(pageSizeAtom),
|
||||
after: pageInfo?.endCursor ?? null,
|
||||
};
|
||||
}
|
||||
if (pageInfo?.hasNextPage || hasProbablyNextPage) {
|
||||
nextPagination = {
|
||||
first: pageSize,
|
||||
after: pageInfo?.endCursor ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return [previousPagination, nextPagination];
|
||||
},
|
||||
);
|
||||
|
||||
return paginationAtom;
|
||||
return [previousPagination, nextPagination];
|
||||
};
|
||||
|
||||
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