aboutsummaryrefslogtreecommitdiffstats
path: root/packages/shared-react
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--packages/shared-react/hooks/assets.ts99
-rw-r--r--packages/shared-react/hooks/bookmarks.ts250
-rw-r--r--packages/shared-react/hooks/highlights.ts93
-rw-r--r--packages/shared-react/hooks/lists.ts196
-rw-r--r--packages/shared-react/hooks/reader-settings.tsx290
-rw-r--r--packages/shared-react/hooks/rules.ts69
-rw-r--r--packages/shared-react/hooks/tags.ts203
-rw-r--r--packages/shared-react/hooks/users.ts51
-rw-r--r--packages/shared-react/package.json5
-rw-r--r--packages/shared-react/providers/trpc-provider.tsx18
-rw-r--r--packages/shared-react/trpc.ts4
11 files changed, 899 insertions, 379 deletions
diff --git a/packages/shared-react/hooks/assets.ts b/packages/shared-react/hooks/assets.ts
index 5367e97c..8be21304 100644
--- a/packages/shared-react/hooks/assets.ts
+++ b/packages/shared-react/hooks/assets.ts
@@ -1,49 +1,74 @@
-import { api } from "../trpc";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { useTRPC } from "../trpc";
+
+type TRPCApi = ReturnType<typeof useTRPC>;
export function useAttachBookmarkAsset(
- ...opts: Parameters<typeof api.assets.attachAsset.useMutation>
+ opts?: Parameters<TRPCApi["assets"]["attachAsset"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.assets.attachAsset.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
- apiUtils.assets.list.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.assets.attachAsset.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());
+ queryClient.invalidateQueries(
+ api.bookmarks.searchBookmarks.pathFilter(),
+ );
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),
+ );
+ queryClient.invalidateQueries(api.assets.list.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useReplaceBookmarkAsset(
- ...opts: Parameters<typeof api.assets.replaceAsset.useMutation>
+ opts?: Parameters<TRPCApi["assets"]["replaceAsset"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.assets.replaceAsset.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
- apiUtils.assets.list.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.assets.replaceAsset.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());
+ queryClient.invalidateQueries(
+ api.bookmarks.searchBookmarks.pathFilter(),
+ );
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),
+ );
+ queryClient.invalidateQueries(api.assets.list.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useDetachBookmarkAsset(
- ...opts: Parameters<typeof api.assets.detachAsset.useMutation>
+ opts?: Parameters<TRPCApi["assets"]["detachAsset"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.assets.detachAsset.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
- apiUtils.assets.list.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.assets.detachAsset.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());
+ queryClient.invalidateQueries(
+ api.bookmarks.searchBookmarks.pathFilter(),
+ );
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),
+ );
+ queryClient.invalidateQueries(api.assets.list.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
diff --git a/packages/shared-react/hooks/bookmarks.ts b/packages/shared-react/hooks/bookmarks.ts
index aea2d185..6ff75d7e 100644
--- a/packages/shared-react/hooks/bookmarks.ts
+++ b/packages/shared-react/hooks/bookmarks.ts
@@ -1,132 +1,196 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
import { getBookmarkRefreshInterval } from "@karakeep/shared/utils/bookmarkUtils";
-import { api } from "../trpc";
+import { useTRPC } from "../trpc";
import { useBookmarkGridContext } from "./bookmark-grid-context";
import { useAddBookmarkToList } from "./lists";
+type TRPCApi = ReturnType<typeof useTRPC>;
+
export function useAutoRefreshingBookmarkQuery(
- input: Parameters<typeof api.bookmarks.getBookmark.useQuery>[0],
+ input: Parameters<TRPCApi["bookmarks"]["getBookmark"]["queryOptions"]>[0],
) {
- return api.bookmarks.getBookmark.useQuery(input, {
- refetchInterval: (query) => {
- const data = query.state.data;
- if (!data) {
- return false;
- }
- return getBookmarkRefreshInterval(data);
- },
- });
+ const api = useTRPC();
+ return useQuery(
+ api.bookmarks.getBookmark.queryOptions(input, {
+ refetchInterval: (query) => {
+ const data = query.state.data;
+ if (!data) {
+ return false;
+ }
+ return getBookmarkRefreshInterval(data);
+ },
+ }),
+ );
}
export function useCreateBookmark(
- ...opts: Parameters<typeof api.bookmarks.createBookmark.useMutation>
+ opts?: Parameters<
+ TRPCApi["bookmarks"]["createBookmark"]["mutationOptions"]
+ >[0],
) {
- const apiUtils = api.useUtils();
- return api.bookmarks.createBookmark.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- apiUtils.lists.stats.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.bookmarks.createBookmark.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());
+ queryClient.invalidateQueries(
+ api.bookmarks.searchBookmarks.pathFilter(),
+ );
+ queryClient.invalidateQueries(api.lists.stats.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useCreateBookmarkWithPostHook(
- ...opts: Parameters<typeof api.bookmarks.createBookmark.useMutation>
+ opts?: Parameters<
+ TRPCApi["bookmarks"]["createBookmark"]["mutationOptions"]
+ >[0],
) {
- const apiUtils = api.useUtils();
+ const api = useTRPC();
+ const queryClient = useQueryClient();
const postCreationCB = useBookmarkPostCreationHook();
- return api.bookmarks.createBookmark.useMutation({
- ...opts[0],
- onSuccess: async (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- await postCreationCB(res.id);
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ return useMutation(
+ api.bookmarks.createBookmark.mutationOptions({
+ ...opts,
+ onSuccess: async (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());
+ queryClient.invalidateQueries(
+ api.bookmarks.searchBookmarks.pathFilter(),
+ );
+ await postCreationCB(res.id);
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useDeleteBookmark(
- ...opts: Parameters<typeof api.bookmarks.deleteBookmark.useMutation>
+ opts?: Parameters<
+ TRPCApi["bookmarks"]["deleteBookmark"]["mutationOptions"]
+ >[0],
) {
- const apiUtils = api.useUtils();
- return api.bookmarks.deleteBookmark.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
- apiUtils.lists.stats.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.bookmarks.deleteBookmark.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());
+ queryClient.invalidateQueries(
+ api.bookmarks.searchBookmarks.pathFilter(),
+ );
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),
+ );
+ queryClient.invalidateQueries(api.lists.stats.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useUpdateBookmark(
- ...opts: Parameters<typeof api.bookmarks.updateBookmark.useMutation>
+ opts?: Parameters<
+ TRPCApi["bookmarks"]["updateBookmark"]["mutationOptions"]
+ >[0],
) {
- const apiUtils = api.useUtils();
- return api.bookmarks.updateBookmark.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
- apiUtils.lists.stats.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.bookmarks.updateBookmark.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());
+ queryClient.invalidateQueries(
+ api.bookmarks.searchBookmarks.pathFilter(),
+ );
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),
+ );
+ queryClient.invalidateQueries(api.lists.stats.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useSummarizeBookmark(
- ...opts: Parameters<typeof api.bookmarks.summarizeBookmark.useMutation>
+ opts?: Parameters<
+ TRPCApi["bookmarks"]["summarizeBookmark"]["mutationOptions"]
+ >[0],
) {
- const apiUtils = api.useUtils();
- return api.bookmarks.summarizeBookmark.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.bookmarks.summarizeBookmark.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());
+ queryClient.invalidateQueries(
+ api.bookmarks.searchBookmarks.pathFilter(),
+ );
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),
+ );
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useRecrawlBookmark(
- ...opts: Parameters<typeof api.bookmarks.recrawlBookmark.useMutation>
+ opts?: Parameters<
+ TRPCApi["bookmarks"]["recrawlBookmark"]["mutationOptions"]
+ >[0],
) {
- const apiUtils = api.useUtils();
- return api.bookmarks.recrawlBookmark.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.bookmarks.recrawlBookmark.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),
+ );
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useUpdateBookmarkTags(
- ...opts: Parameters<typeof api.bookmarks.updateTags.useMutation>
+ opts?: Parameters<TRPCApi["bookmarks"]["updateTags"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.bookmarks.updateTags.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
-
- [...res.attached, ...res.detached].forEach((id) => {
- apiUtils.tags.get.invalidate({ tagId: id });
- apiUtils.bookmarks.getBookmarks.invalidate({ tagId: id });
- });
- apiUtils.tags.list.invalidate();
- apiUtils.lists.stats.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.bookmarks.updateTags.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),
+ );
+
+ [...res.attached, ...res.detached].forEach((id) => {
+ queryClient.invalidateQueries(
+ api.tags.get.queryFilter({ tagId: id }),
+ );
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmarks.queryFilter({ tagId: id }),
+ );
+ });
+ queryClient.invalidateQueries(api.tags.list.pathFilter());
+ queryClient.invalidateQueries(api.lists.stats.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
/**
diff --git a/packages/shared-react/hooks/highlights.ts b/packages/shared-react/hooks/highlights.ts
index e642f878..3f6a6e01 100644
--- a/packages/shared-react/hooks/highlights.ts
+++ b/packages/shared-react/hooks/highlights.ts
@@ -1,49 +1,68 @@
-import { api } from "../trpc";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { useTRPC } from "../trpc";
+
+type TRPCApi = ReturnType<typeof useTRPC>;
export function useCreateHighlight(
- ...opts: Parameters<typeof api.highlights.create.useMutation>
+ opts?: Parameters<TRPCApi["highlights"]["create"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.highlights.create.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.highlights.getForBookmark.invalidate({
- bookmarkId: req.bookmarkId,
- });
- apiUtils.highlights.getAll.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.highlights.create.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(
+ api.highlights.getForBookmark.queryFilter({
+ bookmarkId: req.bookmarkId,
+ }),
+ );
+ queryClient.invalidateQueries(api.highlights.getAll.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useUpdateHighlight(
- ...opts: Parameters<typeof api.highlights.update.useMutation>
+ opts?: Parameters<TRPCApi["highlights"]["update"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.highlights.update.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.highlights.getForBookmark.invalidate({
- bookmarkId: res.bookmarkId,
- });
- apiUtils.highlights.getAll.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.highlights.update.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(
+ api.highlights.getForBookmark.queryFilter({
+ bookmarkId: res.bookmarkId,
+ }),
+ );
+ queryClient.invalidateQueries(api.highlights.getAll.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useDeleteHighlight(
- ...opts: Parameters<typeof api.highlights.delete.useMutation>
+ opts?: Parameters<TRPCApi["highlights"]["delete"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.highlights.delete.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.highlights.getForBookmark.invalidate({
- bookmarkId: res.bookmarkId,
- });
- apiUtils.highlights.getAll.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.highlights.delete.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(
+ api.highlights.getForBookmark.queryFilter({
+ bookmarkId: res.bookmarkId,
+ }),
+ );
+ queryClient.invalidateQueries(api.highlights.getAll.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
diff --git a/packages/shared-react/hooks/lists.ts b/packages/shared-react/hooks/lists.ts
index d269efe3..231e43a7 100644
--- a/packages/shared-react/hooks/lists.ts
+++ b/packages/shared-react/hooks/lists.ts
@@ -1,113 +1,155 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
import { ZBookmarkList } from "@karakeep/shared/types/lists";
import {
listsToTree,
ZBookmarkListRoot,
} from "@karakeep/shared/utils/listUtils";
-import { api } from "../trpc";
+import { useTRPC } from "../trpc";
+
+type TRPCApi = ReturnType<typeof useTRPC>;
export function useCreateBookmarkList(
- ...opts: Parameters<typeof api.lists.create.useMutation>
+ opts?: Parameters<TRPCApi["lists"]["create"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.lists.create.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.lists.list.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.lists.create.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.lists.list.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useEditBookmarkList(
- ...opts: Parameters<typeof api.lists.edit.useMutation>
+ opts?: Parameters<TRPCApi["lists"]["edit"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.lists.edit.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.lists.list.invalidate();
- apiUtils.lists.get.invalidate({ listId: req.listId });
- if (res.type === "smart") {
- apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId });
- }
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.lists.edit.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.lists.list.pathFilter());
+ queryClient.invalidateQueries(
+ api.lists.get.queryFilter({ listId: req.listId }),
+ );
+ if (res.type === "smart") {
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }),
+ );
+ }
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useMergeLists(
- ...opts: Parameters<typeof api.lists.merge.useMutation>
+ opts?: Parameters<TRPCApi["lists"]["merge"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.lists.merge.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.lists.list.invalidate();
- apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.targetId });
- apiUtils.lists.stats.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.lists.merge.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.lists.list.pathFilter());
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmarks.queryFilter({ listId: req.targetId }),
+ );
+ queryClient.invalidateQueries(api.lists.stats.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useAddBookmarkToList(
- ...opts: Parameters<typeof api.lists.addToList.useMutation>
+ opts?: Parameters<TRPCApi["lists"]["addToList"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.lists.addToList.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId });
- apiUtils.lists.getListsOfBookmark.invalidate({
- bookmarkId: req.bookmarkId,
- });
- apiUtils.lists.stats.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.lists.addToList.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }),
+ );
+ queryClient.invalidateQueries(
+ api.lists.getListsOfBookmark.queryFilter({
+ bookmarkId: req.bookmarkId,
+ }),
+ );
+ queryClient.invalidateQueries(api.lists.stats.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useRemoveBookmarkFromList(
- ...opts: Parameters<typeof api.lists.removeFromList.useMutation>
+ opts?: Parameters<TRPCApi["lists"]["removeFromList"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.lists.removeFromList.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId });
- apiUtils.lists.getListsOfBookmark.invalidate({
- bookmarkId: req.bookmarkId,
- });
- apiUtils.lists.stats.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.lists.removeFromList.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }),
+ );
+ queryClient.invalidateQueries(
+ api.lists.getListsOfBookmark.queryFilter({
+ bookmarkId: req.bookmarkId,
+ }),
+ );
+ queryClient.invalidateQueries(api.lists.stats.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useDeleteBookmarkList(
- ...opts: Parameters<typeof api.lists.delete.useMutation>
+ opts?: Parameters<TRPCApi["lists"]["delete"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.lists.delete.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.lists.list.invalidate();
- apiUtils.lists.get.invalidate({ listId: req.listId });
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.lists.delete.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.lists.list.pathFilter());
+ queryClient.invalidateQueries(
+ api.lists.get.queryFilter({ listId: req.listId }),
+ );
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useBookmarkLists(
- ...opts: Parameters<typeof api.lists.list.useQuery>
+ input?: Parameters<TRPCApi["lists"]["list"]["queryOptions"]>[0],
+ opts?: Parameters<TRPCApi["lists"]["list"]["queryOptions"]>[1],
) {
- return api.lists.list.useQuery(opts[0], {
- ...opts[1],
- select: (data) => {
- return { data: data.lists, ...listsToTree(data.lists) };
- },
- });
+ const api = useTRPC();
+ return useQuery(
+ api.lists.list.queryOptions(input, {
+ ...opts,
+ select: (data) => {
+ return { data: data.lists, ...listsToTree(data.lists) };
+ },
+ }),
+ );
}
export function augmentBookmarkListsWithInitialData(
diff --git a/packages/shared-react/hooks/reader-settings.tsx b/packages/shared-react/hooks/reader-settings.tsx
new file mode 100644
index 00000000..7258ebe9
--- /dev/null
+++ b/packages/shared-react/hooks/reader-settings.tsx
@@ -0,0 +1,290 @@
+"use client";
+
+import {
+ createContext,
+ ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+import {
+ READER_DEFAULTS,
+ ReaderSettings,
+ ReaderSettingsPartial,
+} from "@karakeep/shared/types/readers";
+
+import { useTRPC } from "../trpc";
+
+export interface UseReaderSettingsOptions {
+ /**
+ * Get local overrides (device-specific settings stored locally)
+ */
+ getLocalOverrides: () => ReaderSettingsPartial;
+ /**
+ * Save local overrides to local storage
+ */
+ saveLocalOverrides: (overrides: ReaderSettingsPartial) => void;
+ /**
+ * Optional session overrides (for live preview in web).
+ * If provided, these take highest precedence.
+ */
+ sessionOverrides?: ReaderSettingsPartial;
+ /**
+ * Callback when session overrides should be cleared (after successful server save)
+ */
+ onClearSessionOverrides?: () => void;
+}
+
+export function useReaderSettings(options: UseReaderSettingsOptions) {
+ const api = useTRPC();
+ const {
+ getLocalOverrides,
+ saveLocalOverrides,
+ sessionOverrides = {},
+ onClearSessionOverrides,
+ } = options;
+
+ const [localOverrides, setLocalOverrides] = useState<ReaderSettingsPartial>(
+ {},
+ );
+ const [pendingServerSave, setPendingServerSave] =
+ useState<ReaderSettings | null>(null);
+
+ const { data: serverSettings } = useQuery(api.users.settings.queryOptions());
+ const queryClient = useQueryClient();
+
+ // Load local overrides on mount
+ useEffect(() => {
+ setLocalOverrides(getLocalOverrides());
+ }, [getLocalOverrides]);
+
+ // Clear pending state when server settings match what we saved
+ useEffect(() => {
+ if (pendingServerSave && serverSettings) {
+ const serverMatches =
+ serverSettings.readerFontSize === pendingServerSave.fontSize &&
+ // Tolerate minor float normalization differences for lineHeight
+ Math.abs(
+ (serverSettings.readerLineHeight ?? 0) - pendingServerSave.lineHeight,
+ ) < 1e-6 &&
+ serverSettings.readerFontFamily === pendingServerSave.fontFamily;
+ if (serverMatches) {
+ setPendingServerSave(null);
+ }
+ }
+ }, [serverSettings, pendingServerSave]);
+
+ const { mutate: updateServerSettings, isPending: isSaving } = useMutation(
+ api.users.updateSettings.mutationOptions({
+ onSettled: async () => {
+ await queryClient.refetchQueries(api.users.settings.pathFilter());
+ },
+ }),
+ );
+
+ // Separate mutation for saving defaults (clears local overrides on success)
+ const { mutate: saveServerSettings, isPending: isSavingDefaults } =
+ useMutation(
+ api.users.updateSettings.mutationOptions({
+ onSuccess: () => {
+ // Clear local and session overrides after successful server save
+ setLocalOverrides({});
+ saveLocalOverrides({});
+ onClearSessionOverrides?.();
+ },
+ onError: () => {
+ // Clear pending state so we don't show values that failed to persist
+ setPendingServerSave(null);
+ },
+ onSettled: async () => {
+ await queryClient.refetchQueries(api.users.settings.pathFilter());
+ },
+ }),
+ );
+
+ // Compute effective settings with precedence: session → local → pendingSave → server → default
+ const settings: ReaderSettings = useMemo(
+ () => ({
+ fontSize:
+ sessionOverrides.fontSize ??
+ localOverrides.fontSize ??
+ pendingServerSave?.fontSize ??
+ serverSettings?.readerFontSize ??
+ READER_DEFAULTS.fontSize,
+ lineHeight:
+ sessionOverrides.lineHeight ??
+ localOverrides.lineHeight ??
+ pendingServerSave?.lineHeight ??
+ serverSettings?.readerLineHeight ??
+ READER_DEFAULTS.lineHeight,
+ fontFamily:
+ sessionOverrides.fontFamily ??
+ localOverrides.fontFamily ??
+ pendingServerSave?.fontFamily ??
+ serverSettings?.readerFontFamily ??
+ READER_DEFAULTS.fontFamily,
+ }),
+ [sessionOverrides, localOverrides, pendingServerSave, serverSettings],
+ );
+
+ // Get the server setting values (for UI indicators)
+ const serverDefaults: ReaderSettingsPartial = useMemo(
+ () => ({
+ fontSize: serverSettings?.readerFontSize ?? undefined,
+ lineHeight: serverSettings?.readerLineHeight ?? undefined,
+ fontFamily: serverSettings?.readerFontFamily ?? undefined,
+ }),
+ [serverSettings],
+ );
+
+ // Update local override (per-device, immediate)
+ const updateLocal = useCallback(
+ (updates: ReaderSettingsPartial) => {
+ setLocalOverrides((prev) => {
+ const newOverrides = { ...prev, ...updates };
+ saveLocalOverrides(newOverrides);
+ return newOverrides;
+ });
+ },
+ [saveLocalOverrides],
+ );
+
+ // Clear a specific local override
+ const clearLocal = useCallback(
+ (key: keyof ReaderSettings) => {
+ setLocalOverrides((prev) => {
+ const { [key]: _, ...rest } = prev;
+ saveLocalOverrides(rest);
+ return rest;
+ });
+ },
+ [saveLocalOverrides],
+ );
+
+ // Clear all local overrides
+ const clearAllLocal = useCallback(() => {
+ setLocalOverrides({});
+ saveLocalOverrides({});
+ }, [saveLocalOverrides]);
+
+ // Save current effective settings as server default (syncs across devices)
+ const saveAsDefault = useCallback(
+ (settingsToSave?: ReaderSettingsPartial) => {
+ const toSave: ReaderSettings = {
+ fontSize: settingsToSave?.fontSize ?? settings.fontSize,
+ lineHeight: settingsToSave?.lineHeight ?? settings.lineHeight,
+ fontFamily: settingsToSave?.fontFamily ?? settings.fontFamily,
+ };
+ // Set pending state to prevent flicker while server syncs
+ setPendingServerSave(toSave);
+ saveServerSettings({
+ readerFontSize: toSave.fontSize,
+ readerLineHeight: toSave.lineHeight,
+ readerFontFamily: toSave.fontFamily,
+ });
+ },
+ [settings, saveServerSettings],
+ );
+
+ // Clear a specific server default (set to null)
+ const clearDefault = useCallback(
+ (key: keyof ReaderSettings) => {
+ const serverKeyMap = {
+ fontSize: "readerFontSize",
+ lineHeight: "readerLineHeight",
+ fontFamily: "readerFontFamily",
+ } as const;
+ updateServerSettings({ [serverKeyMap[key]]: null });
+ },
+ [updateServerSettings],
+ );
+
+ // Clear all server defaults
+ const clearAllDefaults = useCallback(() => {
+ updateServerSettings({
+ readerFontSize: null,
+ readerLineHeight: null,
+ readerFontFamily: null,
+ });
+ }, [updateServerSettings]);
+
+ // Check if there are any local overrides
+ const hasLocalOverrides = Object.keys(localOverrides).length > 0;
+
+ // Check if there are any server defaults
+ const hasServerDefaults =
+ serverSettings?.readerFontSize != null ||
+ serverSettings?.readerLineHeight != null ||
+ serverSettings?.readerFontFamily != null;
+
+ return {
+ // Current effective settings (what should be displayed)
+ settings,
+
+ // Raw values for UI indicators
+ localOverrides,
+ serverDefaults,
+
+ // Status flags
+ hasLocalOverrides,
+ hasServerDefaults,
+ isSaving: isSaving || isSavingDefaults,
+
+ // Internal state setters (for web's context-based approach)
+ setLocalOverrides,
+
+ // Actions
+ updateLocal,
+ clearLocal,
+ clearAllLocal,
+ saveAsDefault,
+ clearDefault,
+ clearAllDefaults,
+ };
+}
+
+// Context for sharing reader settings state across components
+export type ReaderSettingsContextValue = ReturnType<typeof useReaderSettings>;
+
+const ReaderSettingsContext = createContext<ReaderSettingsContextValue | null>(
+ null,
+);
+
+export interface ReaderSettingsProviderProps extends UseReaderSettingsOptions {
+ children: ReactNode;
+}
+
+/**
+ * Provider that creates a single instance of reader settings state
+ * and shares it across all child components.
+ */
+export function ReaderSettingsProvider({
+ children,
+ ...options
+}: ReaderSettingsProviderProps) {
+ const value = useReaderSettings(options);
+
+ return (
+ <ReaderSettingsContext.Provider value={value}>
+ {children}
+ </ReaderSettingsContext.Provider>
+ );
+}
+
+/**
+ * Hook to access shared reader settings from context.
+ * Must be used within a ReaderSettingsProvider.
+ */
+export function useReaderSettingsContext() {
+ const context = useContext(ReaderSettingsContext);
+ if (!context) {
+ throw new Error(
+ "useReaderSettingsContext must be used within a ReaderSettingsProvider",
+ );
+ }
+ return context;
+}
diff --git a/packages/shared-react/hooks/rules.ts b/packages/shared-react/hooks/rules.ts
index 8428f883..8bca9d69 100644
--- a/packages/shared-react/hooks/rules.ts
+++ b/packages/shared-react/hooks/rules.ts
@@ -1,40 +1,53 @@
-import { api } from "../trpc";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { useTRPC } from "../trpc";
+
+type TRPCApi = ReturnType<typeof useTRPC>;
export function useCreateRule(
- ...opts: Parameters<typeof api.rules.create.useMutation>
+ opts?: Parameters<TRPCApi["rules"]["create"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.rules.create.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.rules.list.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.rules.create.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.rules.list.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useUpdateRule(
- ...opts: Parameters<typeof api.rules.update.useMutation>
+ opts?: Parameters<TRPCApi["rules"]["update"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.rules.update.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.rules.list.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.rules.update.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.rules.list.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useDeleteRule(
- ...opts: Parameters<typeof api.rules.delete.useMutation>
+ opts?: Parameters<TRPCApi["rules"]["delete"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.rules.delete.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.rules.list.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.rules.delete.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.rules.list.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
diff --git a/packages/shared-react/hooks/tags.ts b/packages/shared-react/hooks/tags.ts
index 617c6933..a616b88a 100644
--- a/packages/shared-react/hooks/tags.ts
+++ b/packages/shared-react/hooks/tags.ts
@@ -1,118 +1,155 @@
-import { keepPreviousData } from "@tanstack/react-query";
+import {
+ keepPreviousData,
+ useInfiniteQuery,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from "@tanstack/react-query";
import { ZTagListResponse } from "@karakeep/shared/types/tags";
-import { api } from "../trpc";
+import { useTRPC } from "../trpc";
+
+type TRPCApi = ReturnType<typeof useTRPC>;
export function usePaginatedSearchTags(
- input: Parameters<typeof api.tags.list.useInfiniteQuery>[0],
+ input: Parameters<TRPCApi["tags"]["list"]["infiniteQueryOptions"]>[0],
) {
- return api.tags.list.useInfiniteQuery(input, {
- placeholderData: keepPreviousData,
- getNextPageParam: (lastPage) => lastPage.nextCursor,
+ const api = useTRPC();
+ return useInfiniteQuery({
+ ...api.tags.list.infiniteQueryOptions(input, {
+ placeholderData: keepPreviousData,
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
+ gcTime: 60_000,
+ }),
select: (data) => ({
tags: data.pages.flatMap((page) => page.tags),
}),
- gcTime: 60_000,
});
}
-export function useTagAutocomplete<T>(opts: {
+export function useTagAutocomplete<T = ZTagListResponse>(opts: {
nameContains: string;
- select?: (tags: ZTagListResponse) => T;
+ select?: (data: ZTagListResponse) => T;
+ enabled?: boolean;
}) {
- return api.tags.list.useQuery(
- {
- nameContains: opts.nameContains,
- limit: 50,
- sortBy: opts.nameContains ? "relevance" : "usage",
- },
- {
- select: opts.select,
- placeholderData: keepPreviousData,
- gcTime: opts.nameContains?.length > 0 ? 60_000 : 3_600_000,
- },
- );
+ const api = useTRPC();
+ return useQuery({
+ ...api.tags.list.queryOptions(
+ {
+ nameContains: opts.nameContains,
+ limit: 50,
+ sortBy: opts.nameContains ? "relevance" : "usage",
+ },
+ {
+ placeholderData: keepPreviousData,
+ gcTime: opts.nameContains?.length > 0 ? 60_000 : 3_600_000,
+ enabled: opts.enabled,
+ },
+ ),
+ select: opts.select,
+ });
}
export function useCreateTag(
- ...opts: Parameters<typeof api.tags.create.useMutation>
+ opts?: Parameters<TRPCApi["tags"]["create"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
-
- return api.tags.create.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.tags.list.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+
+ return useMutation(
+ api.tags.create.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.tags.list.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useUpdateTag(
- ...opts: Parameters<typeof api.tags.update.useMutation>
+ opts?: Parameters<TRPCApi["tags"]["update"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
-
- return api.tags.update.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.tags.list.invalidate();
- apiUtils.tags.get.invalidate({ tagId: res.id });
- apiUtils.bookmarks.getBookmarks.invalidate({ tagId: res.id });
-
- // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks
- apiUtils.bookmarks.getBookmark.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+
+ return useMutation(
+ api.tags.update.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.tags.list.pathFilter());
+ queryClient.invalidateQueries(
+ api.tags.get.queryFilter({ tagId: res.id }),
+ );
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmarks.queryFilter({ tagId: res.id }),
+ );
+
+ // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks
+ queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useMergeTag(
- ...opts: Parameters<typeof api.tags.merge.useMutation>
+ opts?: Parameters<TRPCApi["tags"]["merge"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
-
- return api.tags.merge.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.tags.list.invalidate();
- [res.mergedIntoTagId, ...res.deletedTags].forEach((tagId) => {
- apiUtils.tags.get.invalidate({ tagId });
- apiUtils.bookmarks.getBookmarks.invalidate({ tagId });
- });
- // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks
- apiUtils.bookmarks.getBookmark.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+
+ return useMutation(
+ api.tags.merge.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.tags.list.pathFilter());
+ [res.mergedIntoTagId, ...res.deletedTags].forEach((tagId) => {
+ queryClient.invalidateQueries(api.tags.get.queryFilter({ tagId }));
+ queryClient.invalidateQueries(
+ api.bookmarks.getBookmarks.queryFilter({ tagId }),
+ );
+ });
+ // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks
+ queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useDeleteTag(
- ...opts: Parameters<typeof api.tags.delete.useMutation>
+ opts?: Parameters<TRPCApi["tags"]["delete"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
-
- return api.tags.delete.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.tags.list.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+
+ return useMutation(
+ api.tags.delete.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.tags.list.pathFilter());
+ queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useDeleteUnusedTags(
- ...opts: Parameters<typeof api.tags.deleteUnused.useMutation>
+ opts?: Parameters<TRPCApi["tags"]["deleteUnused"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
-
- return api.tags.deleteUnused.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.tags.list.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+
+ return useMutation(
+ api.tags.deleteUnused.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.tags.list.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
diff --git a/packages/shared-react/hooks/users.ts b/packages/shared-react/hooks/users.ts
index eecde3f1..221681a4 100644
--- a/packages/shared-react/hooks/users.ts
+++ b/packages/shared-react/hooks/users.ts
@@ -1,24 +1,49 @@
-import { api } from "../trpc";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+import { useTRPC } from "../trpc";
+
+type TRPCApi = ReturnType<typeof useTRPC>;
export function useUpdateUserSettings(
- ...opts: Parameters<typeof api.users.updateSettings.useMutation>
+ opts?: Parameters<TRPCApi["users"]["updateSettings"]["mutationOptions"]>[0],
+) {
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.users.updateSettings.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.users.settings.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
+}
+
+export function useUpdateUserAvatar(
+ opts?: Parameters<TRPCApi["users"]["updateAvatar"]["mutationOptions"]>[0],
) {
- const apiUtils = api.useUtils();
- return api.users.updateSettings.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta, context) => {
- apiUtils.users.settings.invalidate();
- return opts[0]?.onSuccess?.(res, req, meta, context);
- },
- });
+ const api = useTRPC();
+ const queryClient = useQueryClient();
+ return useMutation(
+ api.users.updateAvatar.mutationOptions({
+ ...opts,
+ onSuccess: (res, req, meta, context) => {
+ queryClient.invalidateQueries(api.users.whoami.pathFilter());
+ return opts?.onSuccess?.(res, req, meta, context);
+ },
+ }),
+ );
}
export function useDeleteAccount(
- ...opts: Parameters<typeof api.users.deleteAccount.useMutation>
+ opts?: Parameters<TRPCApi["users"]["deleteAccount"]["mutationOptions"]>[0],
) {
- return api.users.deleteAccount.useMutation(opts[0]);
+ const api = useTRPC();
+ return useMutation(api.users.deleteAccount.mutationOptions(opts));
}
export function useWhoAmI() {
- return api.users.whoami.useQuery();
+ const api = useTRPC();
+ return useQuery(api.users.whoami.queryOptions());
}
diff --git a/packages/shared-react/package.json b/packages/shared-react/package.json
index 1c8a2f85..ce29a24e 100644
--- a/packages/shared-react/package.json
+++ b/packages/shared-react/package.json
@@ -8,7 +8,8 @@
"@karakeep/shared": "workspace:^0.1.0",
"@karakeep/trpc": "workspace:^0.1.0",
"@tanstack/react-query": "5.90.2",
- "@trpc/client": "^11.4.3",
+ "@trpc/client": "^11.9.0",
+ "@trpc/tanstack-react-query": "^11.9.0",
"superjson": "^2.2.1"
},
"devDependencies": {
@@ -16,7 +17,7 @@
"@karakeep/tsconfig": "workspace:^0.1.0"
},
"peerDependencies": {
- "react": "^19.0.0",
+ "react": "^19.2.1",
"react-native": "0.79.5"
},
"peerDependenciesMeta": {
diff --git a/packages/shared-react/providers/trpc-provider.tsx b/packages/shared-react/providers/trpc-provider.tsx
index 696bf195..2c41aa11 100644
--- a/packages/shared-react/providers/trpc-provider.tsx
+++ b/packages/shared-react/providers/trpc-provider.tsx
@@ -1,9 +1,11 @@
import { useMemo } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { httpBatchLink } from "@trpc/client";
+import { createTRPCClient, httpBatchLink } from "@trpc/client";
import superjson from "superjson";
-import { api } from "../trpc";
+import type { AppRouter } from "@karakeep/trpc/routers/_app";
+
+import { TRPCProvider } from "../trpc";
interface Settings {
apiKey?: string;
@@ -12,7 +14,7 @@ interface Settings {
}
function getTRPCClient(settings: Settings) {
- return api.createClient({
+ return createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: `${settings.address}/api/trpc`,
@@ -31,7 +33,7 @@ function getTRPCClient(settings: Settings) {
});
}
-export function TRPCProvider({
+export function TRPCSettingsProvider({
settings,
children,
}: {
@@ -42,8 +44,10 @@ export function TRPCProvider({
const trpcClient = useMemo(() => getTRPCClient(settings), [settings]);
return (
- <api.Provider client={trpcClient} queryClient={queryClient}>
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
- </api.Provider>
+ <QueryClientProvider client={queryClient}>
+ <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
+ {children}
+ </TRPCProvider>
+ </QueryClientProvider>
);
}
diff --git a/packages/shared-react/trpc.ts b/packages/shared-react/trpc.ts
index 1478684f..2a36b257 100644
--- a/packages/shared-react/trpc.ts
+++ b/packages/shared-react/trpc.ts
@@ -1,7 +1,7 @@
"use client";
-import { createTRPCReact } from "@trpc/react-query";
+import { createTRPCContext } from "@trpc/tanstack-react-query";
import type { AppRouter } from "@karakeep/trpc/routers/_app";
-export const api = createTRPCReact<AppRouter>();
+export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();