diff --git a/typescript/client/src/app/pages/dashboard/users/UserIntegrationsPage.tsx b/typescript/client/src/app/pages/dashboard/users/UserIntegrationsPage.tsx index 3ec936583..33a96d9a2 100644 --- a/typescript/client/src/app/pages/dashboard/users/UserIntegrationsPage.tsx +++ b/typescript/client/src/app/pages/dashboard/users/UserIntegrationsPage.tsx @@ -14,9 +14,10 @@ import SelectLinkButton from "#components/util/SelectLinkButton"; import { TachiConfig } from "#lib/config"; import { type SetState } from "#types/react"; import { APIFetchV1 } from "#util/api"; -import { allPermissions, DelayedPageReload } from "#util/misc"; +import { allPermissions } from "#util/misc"; import React, { useEffect, useReducer, useState } from "react"; import { Alert, Button, Col, Form, Modal, Row } from "react-bootstrap"; +import { useQueryClient } from "react-query"; import { Link, Route, Switch } from "react-router-dom"; import { type APIPermissions, @@ -167,7 +168,7 @@ function CreateNewOAuthClient({ setClients }: { setClients: SetState { e.preventDefault(); - const res = await APIFetchV1( + const res = await APIFetchV1( "/clients/create", { method: "POST", @@ -188,7 +189,8 @@ function CreateNewOAuthClient({ setClients }: { setClients: SetState [...prev, res.body]); + setShow(false); } }} > @@ -696,9 +698,9 @@ function KAIIntegrationStatus({ kaiType: "eag" | "flo" | "min"; userID: integer; }) { - const { data, error } = useApiQuery<{ authStatus: boolean }>( - `/users/${userID}/integrations/kai/${kaiType}`, - ); + const queryClient = useQueryClient(); + const kaiQueryKey = `/users/${userID}/integrations/kai/${kaiType}`; + const { data, error } = useApiQuery<{ authStatus: boolean }>(kaiQueryKey); if (error) { return ; @@ -730,7 +732,7 @@ function KAIIntegrationStatus({ ); if (res.success) { - window.location.reload(); + await queryClient.invalidateQueries([kaiQueryKey]); } }} variant="danger" diff --git a/typescript/client/src/app/pages/dashboard/users/UserInvitesPage.tsx b/typescript/client/src/app/pages/dashboard/users/UserInvitesPage.tsx index 9f7c716cb..f18e8ff97 100644 --- a/typescript/client/src/app/pages/dashboard/users/UserInvitesPage.tsx +++ b/typescript/client/src/app/pages/dashboard/users/UserInvitesPage.tsx @@ -7,10 +7,11 @@ import Loading from "#components/util/Loading"; import Muted from "#components/util/Muted"; import useApiQuery from "#components/util/query/useApiQuery"; import { APIFetchV1 } from "#util/api"; -import { CopyToClipboard, DelayedPageReload } from "#util/misc"; +import { CopyToClipboard } from "#util/misc"; import { FormatTime } from "#util/time"; import React from "react"; import { Button, Col, Row } from "react-bootstrap"; +import { useQueryClient } from "react-query"; import { Link } from "react-router-dom"; import { type integer, @@ -26,6 +27,8 @@ export default function UserInvitesPage({ reqUser }: { reqUser: UserDocument }) `${reqUser.username}'s Invites`, ); + const queryClient = useQueryClient(); + const { data, error } = useApiQuery<{ invites: integer; limit: integer }>( `/users/${reqUser.id}/invites/limit`, ); @@ -72,7 +75,14 @@ export default function UserInvitesPage({ reqUser }: { reqUser: UserDocument }) ); if (r.success) { - DelayedPageReload(); + await Promise.all([ + queryClient.invalidateQueries([ + `/users/${reqUser.id}/invites`, + ]), + queryClient.invalidateQueries([ + `/users/${reqUser.id}/invites/limit`, + ]), + ]); } }} > diff --git a/typescript/client/src/app/pages/dashboard/users/UserSettingsPage.tsx b/typescript/client/src/app/pages/dashboard/users/UserSettingsPage.tsx index ed0c1a887..2884a296f 100644 --- a/typescript/client/src/app/pages/dashboard/users/UserSettingsPage.tsx +++ b/typescript/client/src/app/pages/dashboard/users/UserSettingsPage.tsx @@ -6,10 +6,12 @@ import Icon from "#components/util/Icon"; import Muted from "#components/util/Muted"; import useApiQuery from "#components/util/query/useApiQuery"; import SelectButton from "#components/util/SelectButton"; +import { BackgroundContext } from "#context/BackgroundContext"; +import { UserContext } from "#context/UserContext"; import { UserSettingsContext } from "#context/UserSettingsContext"; import { type SetState } from "#types/react"; import { APIFetchV1, ToAPIURL } from "#util/api"; -import { DelayedPageReload, FetchJSONBody, UppercaseFirst } from "#util/misc"; +import { FetchJSONBody, UppercaseFirst } from "#util/misc"; import { getStoredTheme, mediaQueryPrefers, setTheme, type Themes } from "#util/themeUtils"; import { useFormik } from "formik"; import React, { useContext, useEffect, useRef, useState } from "react"; @@ -19,6 +21,7 @@ import Form from "react-bootstrap/Form"; import InputGroup from "react-bootstrap/InputGroup"; import Stack from "react-bootstrap/Stack"; import toast from "react-hot-toast"; +import { useQueryClient } from "react-query"; import { type integer, type UserDocument, type UserSettingsDocument } from "tachi-common"; interface Props { @@ -537,6 +540,13 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) { const pfpInput = useRef(null); const [banner, setBanner] = useState(); const bannerInput = useRef(null); + const [mediaNonce, setMediaNonce] = useState(0); + + const queryClient = useQueryClient(); + const { setBackground } = useContext(BackgroundContext); + const { user: ctxUser, setUser } = useContext(UserContext); + + const mediaQs = mediaNonce > 0 ? `?v=${mediaNonce}` : ""; const handleReset = (ref: React.MutableRefObject) => { if (ref.current) { @@ -544,6 +554,23 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) { } }; + const afterMediaMutation = async (uploadType: "banner" | "pfp") => { + setMediaNonce((n) => n + 1); + if (uploadType === "banner") { + setBackground(ToAPIURL(`/users/${reqUser.id}/banner?v=${Date.now()}`)); + } + await Promise.all([ + queryClient.invalidateQueries([`/users/${reqUser.username}`]), + queryClient.invalidateQueries([`/users/${reqUser.id}`]), + ]); + if (ctxUser?.id === reqUser.id) { + const r = await APIFetchV1("/users/me", {}, true, true); + if (r.success) { + setUser(r.body); + } + } + }; + return ( @@ -563,12 +590,17 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) { />
handleReset(pfpInput)} setFile={setPfp} @@ -592,11 +624,12 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) { src={ banner ? URL.createObjectURL(banner) - : ToAPIURL(`/users/${reqUser.id}/banner`) + : ToAPIURL(`/users/${reqUser.id}/banner`) + mediaQs } /> handleReset(bannerInput)} setFile={setBanner} @@ -608,6 +641,7 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) { } function SocialMediaForm({ reqUser }: { reqUser: UserDocument }) { + const queryClient = useQueryClient(); const placeholders = { discord: "Discord Username", twitter: "Twitter Handle", @@ -648,7 +682,10 @@ function SocialMediaForm({ reqUser }: { reqUser: UserDocument }) { ); if (rj.success) { - DelayedPageReload(); + await Promise.all([ + queryClient.invalidateQueries([`/users/${reqUser.username}`]), + queryClient.invalidateQueries([`/users/${reqUser.id}`]), + ]); } }, }); @@ -710,8 +747,10 @@ function FileUploadController({ reqUser, setFile, reset, + onMutationSuccess, }: { file?: File; + onMutationSuccess: (uploadType: "banner" | "pfp") => Promise | void; reqUser: UserDocument; reset: () => void; setFile: SetState; @@ -750,7 +789,7 @@ function FileUploadController({ ); if (res.success) { - DelayedPageReload(); + await onMutationSuccess(type); } } }} @@ -782,7 +821,9 @@ function FileUploadController({ ); if (res.success) { - window.location.reload(); + setFile(undefined); + reset(); + await onMutationSuccess(type); } }} variant="success" diff --git a/typescript/client/src/components/tables/dropdowns/components/DeleteScoreBtn.tsx b/typescript/client/src/components/tables/dropdowns/components/DeleteScoreBtn.tsx index 200f2e37c..6ba662c9c 100644 --- a/typescript/client/src/components/tables/dropdowns/components/DeleteScoreBtn.tsx +++ b/typescript/client/src/components/tables/dropdowns/components/DeleteScoreBtn.tsx @@ -1,11 +1,12 @@ import Icon from "#components/util/Icon"; +import { useInvalidateUseApiQueryCache } from "#components/util/query/useApiQuery"; import { APIFetchV1 } from "#util/api"; -import { DelayedPageReload } from "#util/misc"; import React, { useMemo, useReducer } from "react"; import { type ScoreDocument } from "tachi-common"; export default function DeleteScoreBtn({ score }: { score: ScoreDocument }) { const [warn, upgWarn] = useReducer((r) => r + 1, 0); + const invalidateApiQueries = useInvalidateUseApiQueryCache(); const message = useMemo(() => { if (warn === 0) { return ; @@ -36,7 +37,7 @@ export default function DeleteScoreBtn({ score }: { score: ScoreDocument }) { }, true, true, - ).then(() => DelayedPageReload()); + ).then(() => invalidateApiQueries()); } }} > diff --git a/typescript/client/src/components/tables/dropdowns/components/ManageImport.tsx b/typescript/client/src/components/tables/dropdowns/components/ManageImport.tsx index 2d09fee40..f27b0c470 100644 --- a/typescript/client/src/components/tables/dropdowns/components/ManageImport.tsx +++ b/typescript/client/src/components/tables/dropdowns/components/ManageImport.tsx @@ -1,11 +1,12 @@ +import { useInvalidateUseApiQueryCache } from "#components/util/query/useApiQuery"; import { APIFetchV1 } from "#util/api"; -import { DelayedPageReload } from "#util/misc"; import React, { useMemo, useReducer } from "react"; import { Button } from "react-bootstrap"; import { type ImportDocument } from "tachi-common"; export default function ManageImport({ importDoc }: { importDoc: ImportDocument }) { const [warn, upgWarn] = useReducer((r) => r + 1, 0); + const invalidateApiQueries = useInvalidateUseApiQueryCache(); const message = useMemo(() => { if (warn === 0) { return "Undo Import (Requires Further Confirmation)"; @@ -39,7 +40,7 @@ export default function ManageImport({ importDoc }: { importDoc: ImportDocument }, true, true, - ).then(() => DelayedPageReload()); + ).then(() => invalidateApiQueries()); } }} variant="danger" diff --git a/typescript/client/src/components/user/UserHeader.tsx b/typescript/client/src/components/user/UserHeader.tsx index 87b2a1db7..34c67eefe 100644 --- a/typescript/client/src/components/user/UserHeader.tsx +++ b/typescript/client/src/components/user/UserHeader.tsx @@ -11,6 +11,7 @@ import { APIFetchV1 } from "#util/api"; import { FormatDate } from "#util/time"; import React, { useContext, useState } from "react"; import { Button, Form, InputGroup, Modal } from "react-bootstrap"; +import { useQueryClient } from "react-query"; import { UserAuthLevels, type UserDocument } from "tachi-common"; import ProfileBadges from "./ProfileBadges"; @@ -42,12 +43,20 @@ export function UserHeaderBody({ reqUser }: { reqUser: UserDocument }) { } const { user: loggedInUser } = useContext(UserContext); + const queryClient = useQueryClient(); + + async function invalidateProfileQueries() { + await Promise.all([ + queryClient.invalidateQueries([`/users/${reqUser.username}`]), + queryClient.invalidateQueries([`/users/${reqUser.id}`]), + ]); + } return ( <>
- +
@@ -69,7 +78,11 @@ export function UserHeaderBody({ reqUser }: { reqUser: UserDocument }) { { method: "DELETE" }, true, true, - ).then(() => window.location.reload()) + ).then(async (res) => { + if (res.success) { + await invalidateProfileQueries(); + } + }) } variant="danger" > @@ -83,7 +96,11 @@ export function UserHeaderBody({ reqUser }: { reqUser: UserDocument }) { { method: "POST" }, true, true, - ).then(() => window.location.reload()) + ).then(async (res) => { + if (res.success) { + await invalidateProfileQueries(); + } + }) } variant="primary" > diff --git a/typescript/client/src/components/util/query/useApiQuery.tsx b/typescript/client/src/components/util/query/useApiQuery.tsx index af5552df7..6bd696741 100644 --- a/typescript/client/src/components/util/query/useApiQuery.tsx +++ b/typescript/client/src/components/util/query/useApiQuery.tsx @@ -1,11 +1,26 @@ import { APIFetchV1, type UnsuccessfulAPIFetchResponse } from "#util/api"; -import { type QueryKey, useQuery } from "react-query"; +import { type QueryClient, type QueryKey, useQuery, useQueryClient } from "react-query"; import { type SuccessfulAPIResponse } from "tachi-common"; export function UnfuckURLsV3GameToV2Format(url: string) { return url; } +/** Invalidate data fetched via useApiQuery (keys are URL path strings) without a full page reload. */ +export function invalidateUseApiQueryCache(queryClient: QueryClient) { + return queryClient.invalidateQueries({ + predicate: (q) => + Array.isArray(q.queryKey) && + q.queryKey.length > 0 && + q.queryKey.every((part) => typeof part === "string" && part.startsWith("/")), + }); +} + +export function useInvalidateUseApiQueryCache() { + const queryClient = useQueryClient(); + return () => invalidateUseApiQueryCache(queryClient); +} + export default function useApiQuery( url: string | string[], options?: RequestInit, diff --git a/typescript/client/src/util/misc.ts b/typescript/client/src/util/misc.ts index 23b37d52f..984d996ca 100644 --- a/typescript/client/src/util/misc.ts +++ b/typescript/client/src/util/misc.ts @@ -146,10 +146,6 @@ export function FormatMillions(v: number) { return v.toLocaleString(); } -export function DelayedPageReload(delay = 300) { - setTimeout(() => window.location.reload(), delay); -} - export function ShortDelayify(fn: () => void, delay = 300) { setTimeout(fn, delay); }