@btst/stack 2.7.0 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/packages/stack/src/plugins/blog/client/components/loading/post-navigation-skeleton.cjs +13 -0
- package/dist/packages/stack/src/plugins/blog/client/components/loading/post-navigation-skeleton.mjs +11 -0
- package/dist/packages/stack/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.cjs +17 -0
- package/dist/packages/stack/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.mjs +15 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -7
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -7
- package/dist/packages/stack/src/plugins/blog/client/components/shared/post-navigation.cjs +48 -52
- package/dist/packages/stack/src/plugins/blog/client/components/shared/post-navigation.mjs +49 -53
- package/dist/packages/stack/src/plugins/blog/client/components/shared/recent-posts-carousel.cjs +34 -37
- package/dist/packages/stack/src/plugins/blog/client/components/shared/recent-posts-carousel.mjs +35 -38
- package/dist/packages/stack/src/plugins/blog/client/hooks/blog-hooks.cjs +4 -21
- package/dist/packages/stack/src/plugins/blog/client/hooks/blog-hooks.mjs +4 -21
- package/dist/packages/stack/src/plugins/comments/api/getters.cjs +284 -0
- package/dist/packages/stack/src/plugins/comments/api/getters.mjs +280 -0
- package/dist/packages/stack/src/plugins/comments/api/mutations.cjs +118 -0
- package/dist/packages/stack/src/plugins/comments/api/mutations.mjs +112 -0
- package/dist/packages/stack/src/plugins/comments/api/plugin.cjs +335 -0
- package/dist/packages/stack/src/plugins/comments/api/plugin.mjs +333 -0
- package/dist/packages/stack/src/plugins/comments/api/query-key-defs.cjs +60 -0
- package/dist/packages/stack/src/plugins/comments/api/query-key-defs.mjs +55 -0
- package/dist/packages/stack/src/plugins/comments/api/serializers.cjs +23 -0
- package/dist/packages/stack/src/plugins/comments/api/serializers.mjs +21 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-count.cjs +46 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-count.mjs +44 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-form.cjs +86 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-form.mjs +84 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.cjs +540 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.mjs +538 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.cjs +64 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.internal.cjs +426 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.internal.mjs +424 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.mjs +62 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.cjs +66 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.internal.cjs +256 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.internal.mjs +254 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.mjs +64 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.cjs +86 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.internal.cjs +191 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.internal.mjs +189 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.mjs +84 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/page-wrapper.cjs +27 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/page-wrapper.mjs +25 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/pagination.cjs +37 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/pagination.mjs +35 -0
- package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.cjs +476 -0
- package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.mjs +464 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-moderation.cjs +67 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-moderation.mjs +65 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-my.cjs +27 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-my.mjs +25 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-thread.cjs +30 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-thread.mjs +28 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/index.cjs +13 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/index.mjs +11 -0
- package/dist/packages/stack/src/plugins/comments/client/plugin.cjs +116 -0
- package/dist/packages/stack/src/plugins/comments/client/plugin.mjs +114 -0
- package/dist/packages/stack/src/plugins/comments/client/utils.cjs +41 -0
- package/dist/packages/stack/src/plugins/comments/client/utils.mjs +37 -0
- package/dist/packages/stack/src/plugins/comments/db.cjs +75 -0
- package/dist/packages/stack/src/plugins/comments/db.mjs +73 -0
- package/dist/packages/stack/src/plugins/comments/schemas.cjs +45 -0
- package/dist/packages/stack/src/plugins/comments/schemas.mjs +38 -0
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +0 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +0 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/pages/board-page.internal.cjs +39 -22
- package/dist/packages/stack/src/plugins/kanban/client/components/pages/board-page.internal.mjs +40 -23
- package/dist/packages/ui/src/components/avatar.mjs +1 -1
- package/dist/packages/ui/src/components/pagination-controls.cjs +64 -0
- package/dist/packages/ui/src/components/pagination-controls.mjs +62 -0
- package/dist/packages/ui/src/components/when-visible.cjs +39 -0
- package/dist/packages/ui/src/components/when-visible.mjs +37 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +1 -1
- package/dist/plugins/blog/client/hooks/index.d.mts +1 -1
- package/dist/plugins/blog/client/hooks/index.d.ts +1 -1
- package/dist/plugins/blog/client/index.d.cts +24 -2
- package/dist/plugins/blog/client/index.d.mts +24 -2
- package/dist/plugins/blog/client/index.d.ts +24 -2
- package/dist/plugins/comments/api/index.cjs +21 -0
- package/dist/plugins/comments/api/index.d.cts +126 -0
- package/dist/plugins/comments/api/index.d.mts +126 -0
- package/dist/plugins/comments/api/index.d.ts +126 -0
- package/dist/plugins/comments/api/index.mjs +5 -0
- package/dist/plugins/comments/client/components/index.cjs +15 -0
- package/dist/plugins/comments/client/components/index.d.cts +125 -0
- package/dist/plugins/comments/client/components/index.d.mts +125 -0
- package/dist/plugins/comments/client/components/index.d.ts +125 -0
- package/dist/plugins/comments/client/components/index.mjs +5 -0
- package/dist/plugins/comments/client/hooks/index.cjs +17 -0
- package/dist/plugins/comments/client/hooks/index.d.cts +200 -0
- package/dist/plugins/comments/client/hooks/index.d.mts +200 -0
- package/dist/plugins/comments/client/hooks/index.d.ts +200 -0
- package/dist/plugins/comments/client/hooks/index.mjs +1 -0
- package/dist/plugins/comments/client/index.cjs +9 -0
- package/dist/plugins/comments/client/index.d.cts +262 -0
- package/dist/plugins/comments/client/index.d.mts +262 -0
- package/dist/plugins/comments/client/index.d.ts +262 -0
- package/dist/plugins/comments/client/index.mjs +2 -0
- package/dist/plugins/comments/client.css +2 -0
- package/dist/plugins/comments/query-keys.cjs +113 -0
- package/dist/plugins/comments/query-keys.d.cts +71 -0
- package/dist/plugins/comments/query-keys.d.mts +71 -0
- package/dist/plugins/comments/query-keys.d.ts +71 -0
- package/dist/plugins/comments/query-keys.mjs +111 -0
- package/dist/plugins/comments/style.css +15 -0
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/shared/{stack.FeaWkglm.d.ts → stack.BxFl46lB.d.cts} +24 -1
- package/dist/shared/stack.C-b3Sn8j.d.cts +142 -0
- package/dist/shared/stack.C-b3Sn8j.d.mts +142 -0
- package/dist/shared/stack.C-b3Sn8j.d.ts +142 -0
- package/dist/shared/stack.CJE9sAjV.d.ts +335 -0
- package/dist/shared/stack.CmHRdhl8.d.cts +335 -0
- package/dist/shared/{stack.CNLHlv7r.d.mts → stack.DOZ1EXjM.d.mts} +6 -12
- package/dist/shared/{stack.FeaWkglm.d.mts → stack.DRpeDS6X.d.ts} +24 -1
- package/dist/shared/{stack.CQAZwXhV.d.cts → stack.DX-tQ93o.d.cts} +6 -12
- package/dist/shared/stack.Dcz6636A.d.mts +335 -0
- package/dist/shared/{stack.FeaWkglm.d.cts → stack.Jb0kQDJC.d.mts} +24 -1
- package/dist/shared/stack.Ldfkr5b2.d.cts +112 -0
- package/dist/shared/stack.Ldfkr5b2.d.mts +112 -0
- package/dist/shared/stack.Ldfkr5b2.d.ts +112 -0
- package/dist/shared/{stack.D3BsrpAz.d.ts → stack.VF6FhyZw.d.ts} +6 -12
- package/package.json +69 -4
- package/src/plugins/blog/client/components/loading/post-navigation-skeleton.tsx +10 -0
- package/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.tsx +18 -0
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +23 -8
- package/src/plugins/blog/client/components/shared/post-navigation.tsx +0 -5
- package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +1 -5
- package/src/plugins/blog/client/hooks/blog-hooks.tsx +8 -33
- package/src/plugins/blog/client/overrides.ts +26 -1
- package/src/plugins/cms/client/components/shared/pagination.tsx +14 -42
- package/src/plugins/comments/api/getters.ts +444 -0
- package/src/plugins/comments/api/index.ts +21 -0
- package/src/plugins/comments/api/mutations.ts +206 -0
- package/src/plugins/comments/api/plugin.ts +628 -0
- package/src/plugins/comments/api/query-key-defs.ts +143 -0
- package/src/plugins/comments/api/serializers.ts +37 -0
- package/src/plugins/comments/client/components/comment-count.tsx +66 -0
- package/src/plugins/comments/client/components/comment-form.tsx +112 -0
- package/src/plugins/comments/client/components/comment-thread.tsx +799 -0
- package/src/plugins/comments/client/components/index.tsx +11 -0
- package/src/plugins/comments/client/components/pages/moderation-page.internal.tsx +550 -0
- package/src/plugins/comments/client/components/pages/moderation-page.tsx +70 -0
- package/src/plugins/comments/client/components/pages/my-comments-page.internal.tsx +367 -0
- package/src/plugins/comments/client/components/pages/my-comments-page.tsx +72 -0
- package/src/plugins/comments/client/components/pages/resource-comments-page.internal.tsx +225 -0
- package/src/plugins/comments/client/components/pages/resource-comments-page.tsx +97 -0
- package/src/plugins/comments/client/components/shared/page-wrapper.tsx +32 -0
- package/src/plugins/comments/client/components/shared/pagination.tsx +44 -0
- package/src/plugins/comments/client/hooks/index.tsx +13 -0
- package/src/plugins/comments/client/hooks/use-comments.tsx +717 -0
- package/src/plugins/comments/client/index.ts +14 -0
- package/src/plugins/comments/client/localization/comments-moderation.ts +75 -0
- package/src/plugins/comments/client/localization/comments-my.ts +32 -0
- package/src/plugins/comments/client/localization/comments-thread.ts +32 -0
- package/src/plugins/comments/client/localization/index.ts +11 -0
- package/src/plugins/comments/client/overrides.ts +164 -0
- package/src/plugins/comments/client/plugin.tsx +195 -0
- package/src/plugins/comments/client/utils.ts +67 -0
- package/src/plugins/comments/client.css +2 -0
- package/src/plugins/comments/db.ts +77 -0
- package/src/plugins/comments/query-keys.ts +189 -0
- package/src/plugins/comments/schemas.ts +72 -0
- package/src/plugins/comments/style.css +15 -0
- package/src/plugins/comments/types.ts +73 -0
- package/src/plugins/kanban/client/components/forms/task-form.tsx +0 -1
- package/src/plugins/kanban/client/components/pages/board-page.internal.tsx +46 -27
- package/src/plugins/kanban/client/overrides.ts +27 -1
- package/dist/shared/{stack.Rtcvl8sS.d.cts → stack.BOokfhZD.d.cts} +3 -3
- package/dist/shared/{stack.D4Cea8II.d.ts → stack.BvCR4-9H.d.ts} +3 -3
- package/dist/shared/{stack.HE_IvqV5.d.mts → stack.CWxAl9K3.d.mts} +3 -3
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useQuery,
|
|
5
|
+
useInfiniteQuery,
|
|
6
|
+
useMutation,
|
|
7
|
+
useQueryClient,
|
|
8
|
+
useSuspenseQuery,
|
|
9
|
+
type InfiniteData,
|
|
10
|
+
} from "@tanstack/react-query";
|
|
11
|
+
import { createApiClient } from "@btst/stack/plugins/client";
|
|
12
|
+
import { createCommentsQueryKeys } from "../../query-keys";
|
|
13
|
+
import type { CommentsApiRouter } from "../../api";
|
|
14
|
+
import type { SerializedComment, CommentListResult } from "../../types";
|
|
15
|
+
import { toError } from "../utils";
|
|
16
|
+
|
|
17
|
+
interface CommentsClientConfig {
|
|
18
|
+
apiBaseURL: string;
|
|
19
|
+
apiBasePath: string;
|
|
20
|
+
headers?: HeadersInit;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getClient(config: CommentsClientConfig) {
|
|
24
|
+
return createApiClient<CommentsApiRouter>({
|
|
25
|
+
baseURL: config.apiBaseURL,
|
|
26
|
+
basePath: config.apiBasePath,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetch a paginated list of comments for a resource.
|
|
32
|
+
* Returns approved comments by default.
|
|
33
|
+
*/
|
|
34
|
+
export function useComments(
|
|
35
|
+
config: CommentsClientConfig,
|
|
36
|
+
params: {
|
|
37
|
+
resourceId?: string;
|
|
38
|
+
resourceType?: string;
|
|
39
|
+
parentId?: string | null;
|
|
40
|
+
status?: "pending" | "approved" | "spam";
|
|
41
|
+
currentUserId?: string;
|
|
42
|
+
authorId?: string;
|
|
43
|
+
sort?: "asc" | "desc";
|
|
44
|
+
limit?: number;
|
|
45
|
+
offset?: number;
|
|
46
|
+
},
|
|
47
|
+
options?: { enabled?: boolean },
|
|
48
|
+
) {
|
|
49
|
+
const client = getClient(config);
|
|
50
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
51
|
+
|
|
52
|
+
const query = useQuery({
|
|
53
|
+
...queries.comments.list(params),
|
|
54
|
+
staleTime: 30_000,
|
|
55
|
+
retry: false,
|
|
56
|
+
enabled: options?.enabled ?? true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
data: query.data,
|
|
61
|
+
comments: query.data?.items ?? [],
|
|
62
|
+
total: query.data?.total ?? 0,
|
|
63
|
+
isLoading: query.isLoading,
|
|
64
|
+
isFetching: query.isFetching,
|
|
65
|
+
error: query.error,
|
|
66
|
+
refetch: query.refetch,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* useSuspenseQuery version — for use in .internal.tsx files.
|
|
72
|
+
*/
|
|
73
|
+
export function useSuspenseComments(
|
|
74
|
+
config: CommentsClientConfig,
|
|
75
|
+
params: {
|
|
76
|
+
resourceId?: string;
|
|
77
|
+
resourceType?: string;
|
|
78
|
+
parentId?: string | null;
|
|
79
|
+
status?: "pending" | "approved" | "spam";
|
|
80
|
+
currentUserId?: string;
|
|
81
|
+
authorId?: string;
|
|
82
|
+
sort?: "asc" | "desc";
|
|
83
|
+
limit?: number;
|
|
84
|
+
offset?: number;
|
|
85
|
+
},
|
|
86
|
+
) {
|
|
87
|
+
const client = getClient(config);
|
|
88
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
89
|
+
|
|
90
|
+
const { data, refetch, error, isFetching } = useSuspenseQuery({
|
|
91
|
+
...queries.comments.list(params),
|
|
92
|
+
staleTime: 30_000,
|
|
93
|
+
retry: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (error && !isFetching) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
comments: data?.items ?? [],
|
|
102
|
+
total: data?.total ?? 0,
|
|
103
|
+
refetch,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Page-based variant for the moderation dashboard.
|
|
109
|
+
* Uses useSuspenseQuery with explicit offset so the table always shows exactly
|
|
110
|
+
* one page of results and navigation is handled by Prev / Next controls.
|
|
111
|
+
*/
|
|
112
|
+
export function useSuspenseModerationComments(
|
|
113
|
+
config: CommentsClientConfig,
|
|
114
|
+
params: {
|
|
115
|
+
status?: "pending" | "approved" | "spam";
|
|
116
|
+
limit?: number;
|
|
117
|
+
page?: number;
|
|
118
|
+
},
|
|
119
|
+
) {
|
|
120
|
+
const limit = params.limit ?? 20;
|
|
121
|
+
const page = params.page ?? 1;
|
|
122
|
+
const offset = (page - 1) * limit;
|
|
123
|
+
|
|
124
|
+
const client = getClient(config);
|
|
125
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
126
|
+
|
|
127
|
+
const { data, refetch, error, isFetching } = useSuspenseQuery({
|
|
128
|
+
...queries.comments.list({ status: params.status, limit, offset }),
|
|
129
|
+
staleTime: 30_000,
|
|
130
|
+
retry: false,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (error && !isFetching) {
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const comments = data?.items ?? [];
|
|
138
|
+
const total = data?.total ?? 0;
|
|
139
|
+
const totalPages = Math.max(1, Math.ceil(total / limit));
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
comments,
|
|
143
|
+
total,
|
|
144
|
+
limit,
|
|
145
|
+
offset,
|
|
146
|
+
totalPages,
|
|
147
|
+
refetch,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Infinite-scroll variant for the CommentThread component.
|
|
153
|
+
* Uses the "commentsThread" factory namespace (separate from the plain
|
|
154
|
+
* useComments / useSuspenseComments queries) to avoid InfiniteData shape conflicts.
|
|
155
|
+
*
|
|
156
|
+
* Mirrors the blog's usePosts pattern: spread the factory base query into
|
|
157
|
+
* useInfiniteQuery, drive pages via pageParam, and derive hasMore from server total.
|
|
158
|
+
*/
|
|
159
|
+
export function useInfiniteComments(
|
|
160
|
+
config: CommentsClientConfig,
|
|
161
|
+
params: {
|
|
162
|
+
resourceId: string;
|
|
163
|
+
resourceType: string;
|
|
164
|
+
parentId?: string | null;
|
|
165
|
+
status?: "pending" | "approved" | "spam";
|
|
166
|
+
currentUserId?: string;
|
|
167
|
+
pageSize?: number;
|
|
168
|
+
},
|
|
169
|
+
options?: { enabled?: boolean },
|
|
170
|
+
) {
|
|
171
|
+
const pageSize = params.pageSize ?? 10;
|
|
172
|
+
const client = getClient(config);
|
|
173
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
174
|
+
|
|
175
|
+
const baseQuery = queries.commentsThread.list({
|
|
176
|
+
resourceId: params.resourceId,
|
|
177
|
+
resourceType: params.resourceType,
|
|
178
|
+
parentId: params.parentId ?? null,
|
|
179
|
+
status: params.status,
|
|
180
|
+
currentUserId: params.currentUserId,
|
|
181
|
+
limit: pageSize,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const query = useInfiniteQuery<
|
|
185
|
+
CommentListResult,
|
|
186
|
+
Error,
|
|
187
|
+
InfiniteData<CommentListResult>,
|
|
188
|
+
typeof baseQuery.queryKey,
|
|
189
|
+
number
|
|
190
|
+
>({
|
|
191
|
+
...baseQuery,
|
|
192
|
+
initialPageParam: 0,
|
|
193
|
+
getNextPageParam: (lastPage) => {
|
|
194
|
+
const nextOffset = lastPage.offset + lastPage.limit;
|
|
195
|
+
return nextOffset < lastPage.total ? nextOffset : undefined;
|
|
196
|
+
},
|
|
197
|
+
staleTime: 30_000,
|
|
198
|
+
retry: false,
|
|
199
|
+
enabled: options?.enabled ?? true,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const comments = query.data?.pages.flatMap((p) => p.items) ?? [];
|
|
203
|
+
const total = query.data?.pages[0]?.total ?? 0;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
comments,
|
|
207
|
+
total,
|
|
208
|
+
queryKey: baseQuery.queryKey,
|
|
209
|
+
isLoading: query.isLoading,
|
|
210
|
+
isFetching: query.isFetching,
|
|
211
|
+
loadMore: query.fetchNextPage,
|
|
212
|
+
hasMore: !!query.hasNextPage,
|
|
213
|
+
isLoadingMore: query.isFetchingNextPage,
|
|
214
|
+
error: query.error,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Fetch the approved comment count for a resource.
|
|
220
|
+
*/
|
|
221
|
+
export function useCommentCount(
|
|
222
|
+
config: CommentsClientConfig,
|
|
223
|
+
params: {
|
|
224
|
+
resourceId: string;
|
|
225
|
+
resourceType: string;
|
|
226
|
+
status?: "pending" | "approved" | "spam";
|
|
227
|
+
},
|
|
228
|
+
) {
|
|
229
|
+
const client = getClient(config);
|
|
230
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
231
|
+
|
|
232
|
+
const query = useQuery({
|
|
233
|
+
...queries.commentCount.byResource(params),
|
|
234
|
+
staleTime: 60_000,
|
|
235
|
+
retry: false,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
count: query.data ?? 0,
|
|
240
|
+
isLoading: query.isLoading,
|
|
241
|
+
error: query.error,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Post a new comment with optimistic update.
|
|
247
|
+
* When autoApprove is false the optimistic entry shows as "pending" — visible
|
|
248
|
+
* only to the comment's own author via the `currentUserId` match in the UI.
|
|
249
|
+
*
|
|
250
|
+
* Pass `infiniteKey` (from `useInfiniteComments`) when the thread uses an
|
|
251
|
+
* infinite query so the optimistic update targets InfiniteData<CommentListResult>
|
|
252
|
+
* instead of a plain CommentListResult cache entry.
|
|
253
|
+
*/
|
|
254
|
+
export function usePostComment(
|
|
255
|
+
config: CommentsClientConfig,
|
|
256
|
+
params: {
|
|
257
|
+
resourceId: string;
|
|
258
|
+
resourceType: string;
|
|
259
|
+
currentUserId?: string;
|
|
260
|
+
/** When provided, optimistic updates target this infinite-query cache key. */
|
|
261
|
+
infiniteKey?: readonly unknown[];
|
|
262
|
+
/**
|
|
263
|
+
* Page size used by the corresponding `useInfiniteComments` call.
|
|
264
|
+
* Used only when the infinite-query cache is empty at the time of the
|
|
265
|
+
* optimistic update — ensures `getNextPageParam` computes the correct
|
|
266
|
+
* `nextOffset` from `lastPage.limit` instead of a hardcoded fallback.
|
|
267
|
+
*/
|
|
268
|
+
pageSize?: number;
|
|
269
|
+
},
|
|
270
|
+
) {
|
|
271
|
+
const queryClient = useQueryClient();
|
|
272
|
+
const client = getClient(config);
|
|
273
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
274
|
+
|
|
275
|
+
// Compute the list key for a given parentId so optimistic updates always
|
|
276
|
+
// target the exact cache entry the component is subscribed to.
|
|
277
|
+
// parentId must be normalised to null (not undefined) because useComments
|
|
278
|
+
// passes `parentId: null` explicitly — null and undefined produce different
|
|
279
|
+
// discriminator objects and therefore different React Query cache keys.
|
|
280
|
+
const getListKey = (
|
|
281
|
+
parentId: string | null | undefined,
|
|
282
|
+
offset?: number,
|
|
283
|
+
limit?: number,
|
|
284
|
+
) => {
|
|
285
|
+
// Top-level posts for a thread using useInfiniteComments get the infinite key.
|
|
286
|
+
if (params.infiniteKey && (parentId ?? null) === null) {
|
|
287
|
+
return params.infiniteKey;
|
|
288
|
+
}
|
|
289
|
+
return queries.comments.list({
|
|
290
|
+
resourceId: params.resourceId,
|
|
291
|
+
resourceType: params.resourceType,
|
|
292
|
+
parentId: parentId ?? null,
|
|
293
|
+
status: "approved",
|
|
294
|
+
currentUserId: params.currentUserId,
|
|
295
|
+
limit,
|
|
296
|
+
offset,
|
|
297
|
+
}).queryKey;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const isInfinitePost = (parentId: string | null | undefined) =>
|
|
301
|
+
!!params.infiniteKey && (parentId ?? null) === null;
|
|
302
|
+
|
|
303
|
+
return useMutation({
|
|
304
|
+
mutationFn: async (input: {
|
|
305
|
+
body: string;
|
|
306
|
+
parentId?: string | null;
|
|
307
|
+
limit?: number;
|
|
308
|
+
offset?: number;
|
|
309
|
+
}) => {
|
|
310
|
+
const response = await client("@post/comments", {
|
|
311
|
+
method: "POST",
|
|
312
|
+
body: {
|
|
313
|
+
resourceId: params.resourceId,
|
|
314
|
+
resourceType: params.resourceType,
|
|
315
|
+
parentId: input.parentId ?? null,
|
|
316
|
+
body: input.body,
|
|
317
|
+
},
|
|
318
|
+
headers: config.headers,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const data = (response as { data?: unknown }).data;
|
|
322
|
+
if (!data) throw toError((response as { error?: unknown }).error);
|
|
323
|
+
return data as SerializedComment;
|
|
324
|
+
},
|
|
325
|
+
onMutate: async (input) => {
|
|
326
|
+
const listKey = getListKey(input.parentId, input.offset, input.limit);
|
|
327
|
+
await queryClient.cancelQueries({ queryKey: listKey });
|
|
328
|
+
|
|
329
|
+
// Optimistic comment — shows to own author with "pending" badge
|
|
330
|
+
const optimisticId = `optimistic-${Date.now()}`;
|
|
331
|
+
const optimistic: SerializedComment = {
|
|
332
|
+
id: optimisticId,
|
|
333
|
+
resourceId: params.resourceId,
|
|
334
|
+
resourceType: params.resourceType,
|
|
335
|
+
parentId: input.parentId ?? null,
|
|
336
|
+
authorId: params.currentUserId ?? "",
|
|
337
|
+
resolvedAuthorName: "You",
|
|
338
|
+
resolvedAvatarUrl: null,
|
|
339
|
+
body: input.body,
|
|
340
|
+
status: "pending",
|
|
341
|
+
likes: 0,
|
|
342
|
+
isLikedByCurrentUser: false,
|
|
343
|
+
editedAt: null,
|
|
344
|
+
createdAt: new Date().toISOString(),
|
|
345
|
+
updatedAt: new Date().toISOString(),
|
|
346
|
+
replyCount: 0,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
if (isInfinitePost(input.parentId)) {
|
|
350
|
+
const previous =
|
|
351
|
+
queryClient.getQueryData<InfiniteData<CommentListResult>>(listKey);
|
|
352
|
+
|
|
353
|
+
queryClient.setQueryData<InfiniteData<CommentListResult>>(
|
|
354
|
+
listKey,
|
|
355
|
+
(old) => {
|
|
356
|
+
if (!old) {
|
|
357
|
+
return {
|
|
358
|
+
pages: [
|
|
359
|
+
{
|
|
360
|
+
items: [optimistic],
|
|
361
|
+
total: 1,
|
|
362
|
+
limit: params.pageSize ?? 10,
|
|
363
|
+
offset: 0,
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
pageParams: [0],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const lastIdx = old.pages.length - 1;
|
|
370
|
+
return {
|
|
371
|
+
...old,
|
|
372
|
+
// Increment `total` on every page so the header count (which reads
|
|
373
|
+
// pages[0].total) stays in sync even after multiple pages are loaded.
|
|
374
|
+
pages: old.pages.map((page, idx) =>
|
|
375
|
+
idx === lastIdx
|
|
376
|
+
? {
|
|
377
|
+
...page,
|
|
378
|
+
items: [...page.items, optimistic],
|
|
379
|
+
total: page.total + 1,
|
|
380
|
+
}
|
|
381
|
+
: { ...page, total: page.total + 1 },
|
|
382
|
+
),
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
return { previous, isInfinite: true as const, listKey, optimisticId };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const previous = queryClient.getQueryData<CommentListResult>(listKey);
|
|
391
|
+
queryClient.setQueryData<CommentListResult>(listKey, (old) => {
|
|
392
|
+
if (!old) {
|
|
393
|
+
return { items: [optimistic], total: 1, limit: 20, offset: 0 };
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
...old,
|
|
397
|
+
items: [...old.items, optimistic],
|
|
398
|
+
total: old.total + 1,
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return { previous, isInfinite: false as const, listKey, optimisticId };
|
|
403
|
+
},
|
|
404
|
+
onSuccess: (data, _input, context) => {
|
|
405
|
+
if (!context) return;
|
|
406
|
+
// Replace the optimistic item with the real server response.
|
|
407
|
+
// The server may return status "pending" (autoApprove: false) or "approved"
|
|
408
|
+
// (autoApprove: true). Either way we keep the item in the cache so the
|
|
409
|
+
// author continues to see their comment — with a "Pending approval" badge
|
|
410
|
+
// when pending.
|
|
411
|
+
//
|
|
412
|
+
// For replies (non-infinite path): do NOT call invalidateQueries here.
|
|
413
|
+
// The setQueryData below already puts the authoritative server response
|
|
414
|
+
// (including the pending reply) in the cache. Invalidating would trigger
|
|
415
|
+
// a background refetch that goes to the server without auth context and
|
|
416
|
+
// returns only approved replies — overwriting the cache and making the
|
|
417
|
+
// pending reply disappear.
|
|
418
|
+
if (context.isInfinite) {
|
|
419
|
+
queryClient.setQueryData<InfiniteData<CommentListResult>>(
|
|
420
|
+
context.listKey,
|
|
421
|
+
(old) => {
|
|
422
|
+
if (!old) {
|
|
423
|
+
// Cache was cleared between onMutate and onSuccess (rare).
|
|
424
|
+
// Seed with the real server response so the thread keeps the new comment.
|
|
425
|
+
return {
|
|
426
|
+
pages: [
|
|
427
|
+
{
|
|
428
|
+
items: [data],
|
|
429
|
+
total: 1,
|
|
430
|
+
limit: _input.limit ?? params.pageSize ?? 10,
|
|
431
|
+
offset: _input.offset ?? 0,
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
pageParams: [_input.offset ?? 0],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
...old,
|
|
439
|
+
pages: old.pages.map((page) => ({
|
|
440
|
+
...page,
|
|
441
|
+
items: page.items.map((item) =>
|
|
442
|
+
item.id === context.optimisticId ? data : item,
|
|
443
|
+
),
|
|
444
|
+
})),
|
|
445
|
+
};
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
} else {
|
|
449
|
+
queryClient.setQueryData<CommentListResult>(context.listKey, (old) => {
|
|
450
|
+
if (!old) {
|
|
451
|
+
// Cache was cleared between onMutate and onSuccess (rare).
|
|
452
|
+
// Seed it with the real server response so the reply stays visible.
|
|
453
|
+
return {
|
|
454
|
+
items: [data],
|
|
455
|
+
total: 1,
|
|
456
|
+
limit: _input.limit ?? params.pageSize ?? 20,
|
|
457
|
+
offset: _input.offset ?? 0,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
...old,
|
|
462
|
+
items: old.items.map((item) =>
|
|
463
|
+
item.id === context.optimisticId ? data : item,
|
|
464
|
+
),
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
onError: (_err, _input, context) => {
|
|
470
|
+
if (!context) return;
|
|
471
|
+
queryClient.setQueryData(context.listKey, context.previous);
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Edit the body of an existing comment.
|
|
478
|
+
*/
|
|
479
|
+
export function useUpdateComment(config: CommentsClientConfig) {
|
|
480
|
+
const queryClient = useQueryClient();
|
|
481
|
+
const client = getClient(config);
|
|
482
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
483
|
+
|
|
484
|
+
return useMutation({
|
|
485
|
+
mutationFn: async (input: { id: string; body: string }) => {
|
|
486
|
+
const response = await client("@patch/comments/:id", {
|
|
487
|
+
method: "PATCH",
|
|
488
|
+
params: { id: input.id },
|
|
489
|
+
body: { body: input.body },
|
|
490
|
+
headers: config.headers,
|
|
491
|
+
});
|
|
492
|
+
const data = (response as { data?: unknown }).data;
|
|
493
|
+
if (!data) throw toError((response as { error?: unknown }).error);
|
|
494
|
+
return data as SerializedComment;
|
|
495
|
+
},
|
|
496
|
+
onSettled: () => {
|
|
497
|
+
queryClient.invalidateQueries({
|
|
498
|
+
queryKey: queries.comments.list._def,
|
|
499
|
+
});
|
|
500
|
+
// Also invalidate the infinite thread cache so edits are reflected there.
|
|
501
|
+
queryClient.invalidateQueries({ queryKey: ["commentsThread"] });
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Approve a comment (set status to "approved"). Admin use.
|
|
508
|
+
*/
|
|
509
|
+
export function useApproveComment(config: CommentsClientConfig) {
|
|
510
|
+
const queryClient = useQueryClient();
|
|
511
|
+
const client = getClient(config);
|
|
512
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
513
|
+
|
|
514
|
+
return useMutation({
|
|
515
|
+
mutationFn: async (id: string) => {
|
|
516
|
+
const response = await client("@patch/comments/:id/status", {
|
|
517
|
+
method: "PATCH",
|
|
518
|
+
params: { id },
|
|
519
|
+
body: { status: "approved" },
|
|
520
|
+
headers: config.headers,
|
|
521
|
+
});
|
|
522
|
+
const data = (response as { data?: unknown }).data;
|
|
523
|
+
if (!data) throw toError((response as { error?: unknown }).error);
|
|
524
|
+
return data as SerializedComment;
|
|
525
|
+
},
|
|
526
|
+
onSettled: () => {
|
|
527
|
+
queryClient.invalidateQueries({
|
|
528
|
+
queryKey: queries.comments.list._def,
|
|
529
|
+
});
|
|
530
|
+
queryClient.invalidateQueries({
|
|
531
|
+
queryKey: queries.commentCount.byResource._def,
|
|
532
|
+
});
|
|
533
|
+
// Also invalidate the infinite thread cache so status changes are reflected there.
|
|
534
|
+
queryClient.invalidateQueries({ queryKey: ["commentsThread"] });
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Update comment status (pending / approved / spam). Admin use.
|
|
541
|
+
*/
|
|
542
|
+
export function useUpdateCommentStatus(config: CommentsClientConfig) {
|
|
543
|
+
const queryClient = useQueryClient();
|
|
544
|
+
const client = getClient(config);
|
|
545
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
546
|
+
|
|
547
|
+
return useMutation({
|
|
548
|
+
mutationFn: async (input: {
|
|
549
|
+
id: string;
|
|
550
|
+
status: "pending" | "approved" | "spam";
|
|
551
|
+
}) => {
|
|
552
|
+
const response = await client("@patch/comments/:id/status", {
|
|
553
|
+
method: "PATCH",
|
|
554
|
+
params: { id: input.id },
|
|
555
|
+
body: { status: input.status },
|
|
556
|
+
headers: config.headers,
|
|
557
|
+
});
|
|
558
|
+
const data = (response as { data?: unknown }).data;
|
|
559
|
+
if (!data) throw toError((response as { error?: unknown }).error);
|
|
560
|
+
return data as SerializedComment;
|
|
561
|
+
},
|
|
562
|
+
onSettled: () => {
|
|
563
|
+
queryClient.invalidateQueries({
|
|
564
|
+
queryKey: queries.comments.list._def,
|
|
565
|
+
});
|
|
566
|
+
queryClient.invalidateQueries({
|
|
567
|
+
queryKey: queries.commentCount.byResource._def,
|
|
568
|
+
});
|
|
569
|
+
// Also invalidate the infinite thread cache so status changes are reflected there.
|
|
570
|
+
queryClient.invalidateQueries({ queryKey: ["commentsThread"] });
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Delete a comment. Admin use.
|
|
577
|
+
*/
|
|
578
|
+
export function useDeleteComment(config: CommentsClientConfig) {
|
|
579
|
+
const queryClient = useQueryClient();
|
|
580
|
+
const client = getClient(config);
|
|
581
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
582
|
+
|
|
583
|
+
return useMutation({
|
|
584
|
+
mutationFn: async (id: string) => {
|
|
585
|
+
const response = await client("@delete/comments/:id", {
|
|
586
|
+
method: "DELETE",
|
|
587
|
+
params: { id },
|
|
588
|
+
headers: config.headers,
|
|
589
|
+
});
|
|
590
|
+
const data = (response as { data?: unknown }).data;
|
|
591
|
+
if (!data) throw toError((response as { error?: unknown }).error);
|
|
592
|
+
return data as { success: boolean };
|
|
593
|
+
},
|
|
594
|
+
onSettled: () => {
|
|
595
|
+
queryClient.invalidateQueries({
|
|
596
|
+
queryKey: queries.comments.list._def,
|
|
597
|
+
});
|
|
598
|
+
queryClient.invalidateQueries({
|
|
599
|
+
queryKey: queries.commentCount.byResource._def,
|
|
600
|
+
});
|
|
601
|
+
// Also invalidate the infinite thread cache so deletions are reflected there.
|
|
602
|
+
queryClient.invalidateQueries({ queryKey: ["commentsThread"] });
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Toggle a like on a comment with optimistic update.
|
|
609
|
+
*
|
|
610
|
+
* Pass `infiniteKey` (from `useInfiniteComments`) for top-level thread comments
|
|
611
|
+
* so the optimistic update targets InfiniteData<CommentListResult> instead of
|
|
612
|
+
* a plain CommentListResult cache entry.
|
|
613
|
+
*/
|
|
614
|
+
export function useToggleLike(
|
|
615
|
+
config: CommentsClientConfig,
|
|
616
|
+
params: {
|
|
617
|
+
resourceId: string;
|
|
618
|
+
resourceType: string;
|
|
619
|
+
/** parentId of the comment being liked — must match the parentId used by
|
|
620
|
+
* useComments so the optimistic setQueryData hits the correct cache entry.
|
|
621
|
+
* Pass `null` for top-level comments, or the parent comment ID for replies. */
|
|
622
|
+
parentId?: string | null;
|
|
623
|
+
currentUserId?: string;
|
|
624
|
+
/** When the comment lives in an infinite thread, pass the thread's query key
|
|
625
|
+
* so the optimistic update targets the correct InfiniteData cache entry. */
|
|
626
|
+
infiniteKey?: readonly unknown[];
|
|
627
|
+
},
|
|
628
|
+
) {
|
|
629
|
+
const queryClient = useQueryClient();
|
|
630
|
+
const client = getClient(config);
|
|
631
|
+
const queries = createCommentsQueryKeys(client, config.headers);
|
|
632
|
+
|
|
633
|
+
// For top-level thread comments use the infinite key; for replies (or when no
|
|
634
|
+
// infinite key is supplied) fall back to the regular list cache entry.
|
|
635
|
+
const isInfinite = !!params.infiniteKey && (params.parentId ?? null) === null;
|
|
636
|
+
const listKey = isInfinite
|
|
637
|
+
? params.infiniteKey!
|
|
638
|
+
: queries.comments.list({
|
|
639
|
+
resourceId: params.resourceId,
|
|
640
|
+
resourceType: params.resourceType,
|
|
641
|
+
parentId: params.parentId ?? null,
|
|
642
|
+
status: "approved",
|
|
643
|
+
currentUserId: params.currentUserId,
|
|
644
|
+
}).queryKey;
|
|
645
|
+
|
|
646
|
+
function applyLikeUpdate(
|
|
647
|
+
commentId: string,
|
|
648
|
+
updater: (c: SerializedComment) => SerializedComment,
|
|
649
|
+
) {
|
|
650
|
+
if (isInfinite) {
|
|
651
|
+
queryClient.setQueryData<InfiniteData<CommentListResult>>(
|
|
652
|
+
listKey,
|
|
653
|
+
(old) => {
|
|
654
|
+
if (!old) return old;
|
|
655
|
+
return {
|
|
656
|
+
...old,
|
|
657
|
+
pages: old.pages.map((page) => ({
|
|
658
|
+
...page,
|
|
659
|
+
items: page.items.map((c) =>
|
|
660
|
+
c.id === commentId ? updater(c) : c,
|
|
661
|
+
),
|
|
662
|
+
})),
|
|
663
|
+
};
|
|
664
|
+
},
|
|
665
|
+
);
|
|
666
|
+
} else {
|
|
667
|
+
queryClient.setQueryData<CommentListResult>(listKey, (old) => {
|
|
668
|
+
if (!old) return old;
|
|
669
|
+
return {
|
|
670
|
+
...old,
|
|
671
|
+
items: old.items.map((c) => (c.id === commentId ? updater(c) : c)),
|
|
672
|
+
};
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return useMutation({
|
|
678
|
+
mutationFn: async (input: { commentId: string; authorId: string }) => {
|
|
679
|
+
const response = await client("@post/comments/:id/like", {
|
|
680
|
+
method: "POST",
|
|
681
|
+
params: { id: input.commentId },
|
|
682
|
+
body: { authorId: input.authorId },
|
|
683
|
+
headers: config.headers,
|
|
684
|
+
});
|
|
685
|
+
const data = (response as { data?: unknown }).data;
|
|
686
|
+
if (!data) throw toError((response as { error?: unknown }).error);
|
|
687
|
+
return data as { likes: number; isLiked: boolean };
|
|
688
|
+
},
|
|
689
|
+
onMutate: async (input) => {
|
|
690
|
+
await queryClient.cancelQueries({ queryKey: listKey });
|
|
691
|
+
|
|
692
|
+
// Snapshot previous state for rollback.
|
|
693
|
+
const previous = isInfinite
|
|
694
|
+
? queryClient.getQueryData<InfiniteData<CommentListResult>>(listKey)
|
|
695
|
+
: queryClient.getQueryData<CommentListResult>(listKey);
|
|
696
|
+
|
|
697
|
+
applyLikeUpdate(input.commentId, (c) => {
|
|
698
|
+
const wasLiked = c.isLikedByCurrentUser;
|
|
699
|
+
return {
|
|
700
|
+
...c,
|
|
701
|
+
isLikedByCurrentUser: !wasLiked,
|
|
702
|
+
likes: wasLiked ? Math.max(0, c.likes - 1) : c.likes + 1,
|
|
703
|
+
};
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
return { previous };
|
|
707
|
+
},
|
|
708
|
+
onError: (_err, _input, context) => {
|
|
709
|
+
if (context?.previous !== undefined) {
|
|
710
|
+
queryClient.setQueryData(listKey, context.previous);
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
onSettled: () => {
|
|
714
|
+
queryClient.invalidateQueries({ queryKey: listKey });
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
}
|