diff --git a/frontend/locales/en.json b/frontend/locales/en.json index adeb55d7b..a1a757159 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -39,7 +39,6 @@ } }, "app_sessions_list": { - "error": "Failed to load app sessions", "heading": "Apps" }, "browser_session_details": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index eb1963cb5..4bd99c454 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,7 +34,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", - "ua-parser-js": "^1.0.37" + "ua-parser-js": "^1.0.37", + "urql": "^4.0.6" }, "devDependencies": { "@graphql-codegen/cli": "^5.0.2", @@ -23549,6 +23550,18 @@ "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==", "dev": true }, + "node_modules/urql": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/urql/-/urql-4.0.6.tgz", + "integrity": "sha512-meXJ2puOd64uCGKh7Fse2R7gPa8+ZpBOoA62jN7CPXXUt7SVZSdeXWSpB3HvlfzLUkEqsWbvshwrgeWRYNNGaQ==", + "dependencies": { + "@urql/core": "^4.2.0", + "wonka": "^6.3.2" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 548526af7..975e6ba00 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,11 +38,11 @@ "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", - "ua-parser-js": "^1.0.37" + "ua-parser-js": "^1.0.37", + "urql": "^4.0.6" }, "devDependencies": { "@graphql-codegen/cli": "^5.0.2", diff --git a/frontend/src/atoms.ts b/frontend/src/atoms.ts index 7ecef58af..e69de29bb 100644 --- a/frontend/src/atoms.ts +++ b/frontend/src/atoms.ts @@ -1,123 +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, atomWithQuery, clientAtom } from "jotai-urql"; -import type { ReactElement } from "react"; - -import { graphql } from "./gql"; -import { client } from "./graphql"; -import { err, ok, Result } from "./result"; - -export type GqlResult = Result; -export type GqlAtom = WritableAtom< - Promise>, - [context?: Partial], - 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 = ( - queryAtom: AtomWithQuery, - mapper: (data: Data) => NewData, -): GqlAtom => { - return atom( - async (get): Promise> => { - 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 - } - - ... on Anonymous { - id - } - } - } -`); - -const currentViewerAtom = atomWithQuery({ query: CURRENT_VIEWER_QUERY }); - -export const currentUserIdAtom: GqlAtom = mapQueryAtom( - currentViewerAtom, - (data) => { - if (data.viewer.__typename === "User") { - return data.viewer.id; - } - return null; - }, -); - -const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ ` - query CurrentViewerSessionQuery { - viewerSession { - __typename - ... on BrowserSession { - id - } - - ... on Anonymous { - id - } - } - } -`); - -const currentViewerSessionAtom = atomWithQuery({ - query: CURRENT_VIEWER_SESSION_QUERY, -}); - -export const currentBrowserSessionIdAtom: GqlAtom = mapQueryAtom( - currentViewerSessionAtom, - (data) => { - if (data.viewerSession.__typename === "BrowserSession") { - return data.viewerSession.id; - } - return null; - }, -); diff --git a/frontend/src/components/BrowserSession.tsx b/frontend/src/components/BrowserSession.tsx index 79e996208..8d0e3884b 100644 --- a/frontend/src/components/BrowserSession.tsx +++ b/frontend/src/components/BrowserSession.tsx @@ -13,12 +13,9 @@ // limitations under the License. import { parseISO } from "date-fns"; -import { atom, useSetAtom } from "jotai"; -import { atomFamily } from "jotai/utils"; -import { atomWithMutation } from "jotai-urql"; import { useCallback } from "react"; +import { useMutation } from "urql"; -import { currentBrowserSessionIdAtom, currentUserIdAtom } from "../atoms"; import { FragmentType, graphql, useFragment } from "../gql"; import { parseUserAgent, @@ -56,39 +53,18 @@ const END_SESSION_MUTATION = graphql(/* GraphQL */ ` } `); -export const endBrowserSessionFamily = atomFamily((id: string) => { - const endSession = atomWithMutation(END_SESSION_MUTATION); - - // A proxy atom which pre-sets the id variable in the mutation - const endSessionAtom = atom( - (get) => get(endSession), - (get, set) => set(endSession, { id }), - ); - - return endSessionAtom; -}); - export const useEndBrowserSession = ( sessionId: string, isCurrent: boolean, ): (() => Promise) => { - const endSession = useSetAtom(endBrowserSessionFamily(sessionId)); - - // Pull those atoms to reset them when the current session is ended - const currentUserId = useSetAtom(currentUserIdAtom); - const currentBrowserSessionId = useSetAtom(currentBrowserSessionIdAtom); + const [, endSession] = useMutation(END_SESSION_MUTATION); const onSessionEnd = useCallback(async (): Promise => { - await endSession(); + await endSession({ id: sessionId }); if (isCurrent) { - currentBrowserSessionId({ - requestPolicy: "network-only", - }); - currentUserId({ - requestPolicy: "network-only", - }); + window.location.reload(); } - }, [isCurrent, endSession, currentBrowserSessionId, currentUserId]); + }, [isCurrent, endSession, sessionId]); return onSessionEnd; }; diff --git a/frontend/src/components/BrowserSessionList.tsx b/frontend/src/components/BrowserSessionList.tsx index d659f8c1b..fb688e34f 100644 --- a/frontend/src/components/BrowserSessionList.tsx +++ b/frontend/src/components/BrowserSessionList.tsx @@ -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.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, +const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => { + const [pagination, setPagination] = usePagination(); + const [pending, startTransition] = useTransition(); + const [filter, setFilter] = useState( + SessionState.Active, ); - - return browserSessionList; -}); - -const pageInfoFamily = atomFamily((userId: string) => { - const pageInfoAtom = atom(async (get): Promise => { - const result = await get(browserSessionListFamily(userId)); - return (isOk(result) && unwrapOk(result)?.pageInfo) || null; + const [result] = useQuery({ + query: QUERY, + variables: { userId, state: filter, ...pagination }, }); - return pageInfoAtom; -}); - -const paginationFamily = atomFamily((userId: string) => { - const paginationAtom = atomWithPagination( - currentPaginationAtom, - pageInfoFamily(userId), - ); + if (result.error) throw result.error; + const browserSessions = result.data?.user?.browserSessions; + if (!browserSessions) throw new Error(); // Suspense mode is enabled - return paginationAtom; -}); - -const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => { - 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 [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 }) => {