@rocapine/community-core 0.3.0 → 0.5.0
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 +8 -8
- package/lib/hooks.d.ts +16 -1
- package/lib/hooks.js +55 -42
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -1
- package/lib/keys.d.ts +7 -0
- package/lib/keys.js +18 -0
- package/lib/service.d.ts +12 -0
- package/lib/service.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,14 +73,14 @@ backend configured — the pattern `examples/expo-app` uses when
|
|
|
73
73
|
|
|
74
74
|
All optional, all default to a no-op:
|
|
75
75
|
|
|
76
|
-
| Field | Signature | Purpose
|
|
77
|
-
| -------------------- | -------------------------------------------------------- |
|
|
78
|
-
| `getDisplayName` | `() => string \| null` | Feeds the display name shown for the current user before their community username is set
|
|
79
|
-
| `getAnalyticsIds` | `() => { amplitudeId?: string; revenuecatId?: string }` | Cross-referencing ids attached where the host wants them
|
|
80
|
-
| `onEvent` | `(name: string, props: Record<string, unknown>) => void` | **The only analytics seam.** Every product event goes through this; the SDK never depends on any analytics provider. Never throws through to the UI even if the adapter itself throws
|
|
81
|
-
| `rulesAcceptance` | `{ get(): Promise<boolean>; set(): Promise<void> }` | Persists whether the user has accepted the one-time community guidelines sheet
|
|
82
|
-
| `onContentPublished` | `() => void` | Fired after a post/comment is successfully published (e.g. to arm a review-gate)
|
|
83
|
-
| `getLocale` | `() => string` |
|
|
76
|
+
| Field | Signature | Purpose |
|
|
77
|
+
| -------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `getDisplayName` | `() => string \| null` | Feeds the display name shown for the current user before their community username is set |
|
|
79
|
+
| `getAnalyticsIds` | `() => { amplitudeId?: string; revenuecatId?: string }` | Cross-referencing ids attached where the host wants them |
|
|
80
|
+
| `onEvent` | `(name: string, props: Record<string, unknown>) => void` | **The only analytics seam.** Every product event goes through this; the SDK never depends on any analytics provider. Never throws through to the UI even if the adapter itself throws |
|
|
81
|
+
| `rulesAcceptance` | `{ get(): Promise<boolean>; set(): Promise<void> }` | Persists whether the user has accepted the one-time community guidelines sheet |
|
|
82
|
+
| `onContentPublished` | `() => void` | Fired after a post/comment is successfully published (e.g. to arm a review-gate) |
|
|
83
|
+
| `getLocale` | `() => string` | The app's language. Resolved against `modules.translation.locales` to choose the language content is read in (`readerLocale`), and mirrored to `profiles.locale` by `syncProfileFromHost` so pushes arrive in each recipient's language. Distinct from the UI package's `translations.locale` prop (interface copy) |
|
|
84
84
|
|
|
85
85
|
### Feed row extension point (`CommunityConfig.feed`)
|
|
86
86
|
|
package/lib/hooks.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
import { type InfiniteData } from "@tanstack/react-query";
|
|
1
|
+
import { type InfiniteData, type QueryClient } from "@tanstack/react-query";
|
|
2
2
|
import { type CommunityProfile, type FeedPost, type ThreadComment } from "./models";
|
|
3
3
|
import { type ModerationVerdict, type ReportInput, type UpdateProfileInput, type UpdateProfileResult } from "./service";
|
|
4
|
+
/** True while a post/comment is still the optimistic placeholder inserted by
|
|
5
|
+
* `useCreatePost`/`useCreateComment` (its temp id is swapped for the real one
|
|
6
|
+
* once moderation answers). Server actions keyed on the id (reactions, likes,
|
|
7
|
+
* votes) must wait until this is false. */
|
|
8
|
+
export declare function isOptimistic(item: {
|
|
9
|
+
id: string;
|
|
10
|
+
}): boolean;
|
|
11
|
+
/** Our own author fields for an optimistic row, read from the query cache
|
|
12
|
+
* (`useMyUid` + `useProfile`), so the placeholder links to our profile and
|
|
13
|
+
* shows our avatar instead of an empty author until the next refetch. */
|
|
14
|
+
export declare function ownAuthorFields(queryClient: QueryClient): {
|
|
15
|
+
authorId: string;
|
|
16
|
+
authorHandle: string | null;
|
|
17
|
+
authorAvatarUrl: string | null;
|
|
18
|
+
};
|
|
4
19
|
type PostCacheEntry = [readonly unknown[], InfiniteData<FeedPost[]> | undefined];
|
|
5
20
|
/**
|
|
6
21
|
* Snapshot of every post-holding cache, for optimistic-update rollback on
|
package/lib/hooks.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// `applyReaction`) and unseen-count hook (parameterized on `lastSeenAtIso` instead of
|
|
11
11
|
// reading that host's own store). No React Native / analytics-SDK / store imports here.
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.isOptimistic = isOptimistic;
|
|
14
|
+
exports.ownAuthorFields = ownAuthorFields;
|
|
13
15
|
exports.useCommunityFeed = useCommunityFeed;
|
|
14
16
|
exports.useSearchPosts = useSearchPosts;
|
|
15
17
|
exports.useNewPostsCount = useNewPostsCount;
|
|
@@ -30,17 +32,32 @@ exports.useDeleteContent = useDeleteContent;
|
|
|
30
32
|
const react_query_1 = require("@tanstack/react-query");
|
|
31
33
|
const events_1 = require("./events");
|
|
32
34
|
const identity_1 = require("./identity");
|
|
35
|
+
const keys_1 = require("./keys");
|
|
33
36
|
const locale_1 = require("./locale");
|
|
34
37
|
const models_1 = require("./models");
|
|
35
38
|
const provider_1 = require("./provider");
|
|
36
39
|
const service_1 = require("./service");
|
|
37
|
-
const FEED_KEY = ["community", "feed"];
|
|
38
|
-
const feedKey = (topic, locale) => [...FEED_KEY, topic ?? "all", locale ?? "src"];
|
|
39
|
-
const threadKey = (postId, locale) => ["community", "thread", postId, locale ?? "src"];
|
|
40
40
|
const profileKey = (userId) => ["community", "profile", userId];
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
const MY_UID_KEY = ["community", "myUid"];
|
|
42
|
+
/** True while a post/comment is still the optimistic placeholder inserted by
|
|
43
|
+
* `useCreatePost`/`useCreateComment` (its temp id is swapped for the real one
|
|
44
|
+
* once moderation answers). Server actions keyed on the id (reactions, likes,
|
|
45
|
+
* votes) must wait until this is false. */
|
|
46
|
+
function isOptimistic(item) {
|
|
47
|
+
return item.id.startsWith("optimistic-");
|
|
48
|
+
}
|
|
49
|
+
/** Our own author fields for an optimistic row, read from the query cache
|
|
50
|
+
* (`useMyUid` + `useProfile`), so the placeholder links to our profile and
|
|
51
|
+
* shows our avatar instead of an empty author until the next refetch. */
|
|
52
|
+
function ownAuthorFields(queryClient) {
|
|
53
|
+
const uid = queryClient.getQueryData(MY_UID_KEY) ?? "";
|
|
54
|
+
const profile = uid ? queryClient.getQueryData(profileKey(uid)) : undefined;
|
|
55
|
+
return {
|
|
56
|
+
authorId: uid,
|
|
57
|
+
authorHandle: profile?.handle ?? null,
|
|
58
|
+
authorAvatarUrl: profile?.avatarUrl ?? null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
44
61
|
/**
|
|
45
62
|
* Adjust a post's commentCount across every cached feed page (all topic
|
|
46
63
|
* filters). The feed is never refetched after commenting (kept smooth), so the
|
|
@@ -49,7 +66,7 @@ const SEARCH_KEY = ["community", "search"];
|
|
|
49
66
|
* change is not reflected in the user-posts/search caches, same as upstream.
|
|
50
67
|
*/
|
|
51
68
|
function bumpFeedCommentCount(queryClient, postId, delta) {
|
|
52
|
-
queryClient.setQueriesData({ queryKey: FEED_KEY }, (data) => data
|
|
69
|
+
queryClient.setQueriesData({ queryKey: keys_1.FEED_KEY }, (data) => data
|
|
53
70
|
? {
|
|
54
71
|
...data,
|
|
55
72
|
pages: data.pages.map((page) => page.map((post) => post.id === postId
|
|
@@ -68,19 +85,19 @@ function bumpFeedCommentCount(queryClient, postId, delta) {
|
|
|
68
85
|
*/
|
|
69
86
|
async function applyOptimisticToAllPostCaches(queryClient, mapPost) {
|
|
70
87
|
await Promise.all([
|
|
71
|
-
queryClient.cancelQueries({ queryKey: FEED_KEY }),
|
|
72
|
-
queryClient.cancelQueries({ queryKey: USER_POSTS_KEY }),
|
|
73
|
-
queryClient.cancelQueries({ queryKey: SEARCH_KEY }),
|
|
88
|
+
queryClient.cancelQueries({ queryKey: keys_1.FEED_KEY }),
|
|
89
|
+
queryClient.cancelQueries({ queryKey: keys_1.USER_POSTS_KEY }),
|
|
90
|
+
queryClient.cancelQueries({ queryKey: keys_1.SEARCH_KEY }),
|
|
74
91
|
]);
|
|
75
92
|
const entries = [
|
|
76
|
-
...queryClient.getQueriesData({ queryKey: FEED_KEY }),
|
|
77
|
-
...queryClient.getQueriesData({ queryKey: USER_POSTS_KEY }),
|
|
78
|
-
...queryClient.getQueriesData({ queryKey: SEARCH_KEY }),
|
|
93
|
+
...queryClient.getQueriesData({ queryKey: keys_1.FEED_KEY }),
|
|
94
|
+
...queryClient.getQueriesData({ queryKey: keys_1.USER_POSTS_KEY }),
|
|
95
|
+
...queryClient.getQueriesData({ queryKey: keys_1.SEARCH_KEY }),
|
|
79
96
|
];
|
|
80
97
|
const applyToPages = (data) => data ? { ...data, pages: data.pages.map((page) => page.map(mapPost)) } : data;
|
|
81
|
-
queryClient.setQueriesData({ queryKey: FEED_KEY }, applyToPages);
|
|
82
|
-
queryClient.setQueriesData({ queryKey: USER_POSTS_KEY }, applyToPages);
|
|
83
|
-
queryClient.setQueriesData({ queryKey: SEARCH_KEY }, applyToPages);
|
|
98
|
+
queryClient.setQueriesData({ queryKey: keys_1.FEED_KEY }, applyToPages);
|
|
99
|
+
queryClient.setQueriesData({ queryKey: keys_1.USER_POSTS_KEY }, applyToPages);
|
|
100
|
+
queryClient.setQueriesData({ queryKey: keys_1.SEARCH_KEY }, applyToPages);
|
|
84
101
|
return { entries };
|
|
85
102
|
}
|
|
86
103
|
function rollbackPostCaches(queryClient, snapshot) {
|
|
@@ -97,7 +114,7 @@ function useCommunityFeed(topic, enabled = true) {
|
|
|
97
114
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
98
115
|
const locale = (0, locale_1.readerLocale)(cfg);
|
|
99
116
|
return (0, react_query_1.useInfiniteQuery)({
|
|
100
|
-
queryKey: feedKey(topic, locale),
|
|
117
|
+
queryKey: (0, keys_1.feedKey)(topic, locale),
|
|
101
118
|
queryFn: ({ pageParam }) => (0, service_1.fetchFeedPage)(cfg, { topic, cursor: pageParam }),
|
|
102
119
|
initialPageParam: 0,
|
|
103
120
|
getNextPageParam: (lastPage, pages) => lastPage.length === models_1.FEED_PAGE_SIZE ? pages.length : undefined,
|
|
@@ -115,7 +132,7 @@ function useSearchPosts(term) {
|
|
|
115
132
|
const cleaned = term.trim();
|
|
116
133
|
const locale = (0, locale_1.readerLocale)(cfg);
|
|
117
134
|
return (0, react_query_1.useInfiniteQuery)({
|
|
118
|
-
queryKey:
|
|
135
|
+
queryKey: (0, keys_1.searchKey)(cleaned, locale),
|
|
119
136
|
queryFn: ({ pageParam }) => (0, service_1.searchPosts)(cfg, cleaned, { cursor: pageParam }),
|
|
120
137
|
initialPageParam: 0,
|
|
121
138
|
getNextPageParam: (lastPage, pages) => lastPage.length === models_1.FEED_PAGE_SIZE ? pages.length : undefined,
|
|
@@ -168,7 +185,7 @@ function useThread(postId) {
|
|
|
168
185
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
169
186
|
const locale = (0, locale_1.readerLocale)(cfg);
|
|
170
187
|
return (0, react_query_1.useQuery)({
|
|
171
|
-
queryKey: threadKey(postId ?? "none", locale),
|
|
188
|
+
queryKey: (0, keys_1.threadKey)(postId ?? "none", locale),
|
|
172
189
|
queryFn: () => (0, service_1.fetchThread)(cfg, postId),
|
|
173
190
|
enabled: cfg.supabase !== null && !!postId,
|
|
174
191
|
staleTime: 1000 * 15,
|
|
@@ -192,7 +209,7 @@ function useUserPosts(userId) {
|
|
|
192
209
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
193
210
|
const locale = (0, locale_1.readerLocale)(cfg);
|
|
194
211
|
return (0, react_query_1.useInfiniteQuery)({
|
|
195
|
-
queryKey: userPostsKey(userId ?? "none", locale),
|
|
212
|
+
queryKey: (0, keys_1.userPostsKey)(userId ?? "none", locale),
|
|
196
213
|
queryFn: ({ pageParam }) => (0, service_1.fetchUserPosts)(cfg, userId, { cursor: pageParam }),
|
|
197
214
|
initialPageParam: 0,
|
|
198
215
|
getNextPageParam: (lastPage, pages) => lastPage.length === models_1.FEED_PAGE_SIZE ? pages.length : undefined,
|
|
@@ -205,7 +222,7 @@ function useUserPosts(userId) {
|
|
|
205
222
|
function useMyUid() {
|
|
206
223
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
207
224
|
const q = (0, react_query_1.useQuery)({
|
|
208
|
-
queryKey:
|
|
225
|
+
queryKey: MY_UID_KEY,
|
|
209
226
|
queryFn: () => (0, identity_1.ensureIdentity)(cfg),
|
|
210
227
|
enabled: cfg.supabase !== null,
|
|
211
228
|
staleTime: Infinity,
|
|
@@ -243,15 +260,13 @@ function useCreatePost() {
|
|
|
243
260
|
const locale = (0, locale_1.readerLocale)(cfg);
|
|
244
261
|
return (0, react_query_1.useMutation)({
|
|
245
262
|
onMutate: async ({ topic, text, authorName, pollOptions }) => {
|
|
246
|
-
await queryClient.cancelQueries({ queryKey: FEED_KEY });
|
|
263
|
+
await queryClient.cancelQueries({ queryKey: keys_1.FEED_KEY });
|
|
247
264
|
const tempId = `optimistic-${Date.now()}`;
|
|
248
265
|
const optimistic = {
|
|
249
266
|
id: tempId,
|
|
250
|
-
authorId: "",
|
|
251
267
|
authorName,
|
|
252
268
|
authorOfficial: false,
|
|
253
|
-
|
|
254
|
-
authorAvatarUrl: null,
|
|
269
|
+
...ownAuthorFields(queryClient),
|
|
255
270
|
topic,
|
|
256
271
|
text,
|
|
257
272
|
status: "visible",
|
|
@@ -281,8 +296,8 @@ function useCreatePost() {
|
|
|
281
296
|
translation: null,
|
|
282
297
|
};
|
|
283
298
|
const prepend = (key) => queryClient.setQueryData(key, (d) => d ? { ...d, pages: [[optimistic, ...(d.pages[0] ?? [])], ...d.pages.slice(1)] } : d);
|
|
284
|
-
prepend(feedKey(undefined, locale));
|
|
285
|
-
prepend(feedKey(topic, locale));
|
|
299
|
+
prepend((0, keys_1.feedKey)(undefined, locale));
|
|
300
|
+
prepend((0, keys_1.feedKey)(topic, locale));
|
|
286
301
|
return { tempId };
|
|
287
302
|
},
|
|
288
303
|
mutationFn: async ({ topic, text, pollOptions }) => {
|
|
@@ -292,7 +307,7 @@ function useCreatePost() {
|
|
|
292
307
|
},
|
|
293
308
|
onSuccess: ({ id, verdict }, { topic, text, pollOptions }, { tempId }) => {
|
|
294
309
|
const rejected = verdict.status === "rejected";
|
|
295
|
-
queryClient.setQueriesData({ queryKey: FEED_KEY }, (d) => d
|
|
310
|
+
queryClient.setQueriesData({ queryKey: keys_1.FEED_KEY }, (d) => d
|
|
296
311
|
? {
|
|
297
312
|
...d,
|
|
298
313
|
pages: d.pages.map((p) => rejected
|
|
@@ -311,11 +326,11 @@ function useCreatePost() {
|
|
|
311
326
|
// The in-place swap above keeps the optimistic poll's temp option ids;
|
|
312
327
|
// voting needs the real uuids, so a poll post refetches in background.
|
|
313
328
|
if (hasPoll)
|
|
314
|
-
queryClient.invalidateQueries({ queryKey: FEED_KEY });
|
|
329
|
+
queryClient.invalidateQueries({ queryKey: keys_1.FEED_KEY });
|
|
315
330
|
}
|
|
316
331
|
},
|
|
317
332
|
onError: (_e, _v, context) => {
|
|
318
|
-
queryClient.setQueriesData({ queryKey: FEED_KEY }, (d) => d
|
|
333
|
+
queryClient.setQueriesData({ queryKey: keys_1.FEED_KEY }, (d) => d
|
|
319
334
|
? { ...d, pages: d.pages.map((p) => p.filter((post) => post.id !== context?.tempId)) }
|
|
320
335
|
: d);
|
|
321
336
|
},
|
|
@@ -328,23 +343,21 @@ function useCreateComment() {
|
|
|
328
343
|
const locale = (0, locale_1.readerLocale)(cfg);
|
|
329
344
|
return (0, react_query_1.useMutation)({
|
|
330
345
|
onMutate: async ({ postId, text, authorName }) => {
|
|
331
|
-
await queryClient.cancelQueries({ queryKey: threadKey(postId, locale) });
|
|
332
|
-
await queryClient.cancelQueries({ queryKey: FEED_KEY });
|
|
346
|
+
await queryClient.cancelQueries({ queryKey: (0, keys_1.threadKey)(postId, locale) });
|
|
347
|
+
await queryClient.cancelQueries({ queryKey: keys_1.FEED_KEY });
|
|
333
348
|
const tempId = `optimistic-${Date.now()}`;
|
|
334
349
|
const optimistic = {
|
|
335
350
|
id: tempId,
|
|
336
351
|
postId,
|
|
337
|
-
authorId: "",
|
|
338
352
|
authorName,
|
|
339
353
|
authorOfficial: false,
|
|
340
|
-
|
|
341
|
-
authorAvatarUrl: null,
|
|
354
|
+
...ownAuthorFields(queryClient),
|
|
342
355
|
text,
|
|
343
356
|
isOwn: true,
|
|
344
357
|
createdAt: new Date().toISOString(),
|
|
345
358
|
translation: null,
|
|
346
359
|
};
|
|
347
|
-
queryClient.setQueryData(threadKey(postId, locale), (d) => [
|
|
360
|
+
queryClient.setQueryData((0, keys_1.threadKey)(postId, locale), (d) => [
|
|
348
361
|
...(d ?? []),
|
|
349
362
|
optimistic,
|
|
350
363
|
]);
|
|
@@ -358,7 +371,7 @@ function useCreateComment() {
|
|
|
358
371
|
},
|
|
359
372
|
onSuccess: ({ id, verdict }, { postId, text }, { tempId }) => {
|
|
360
373
|
const rejected = verdict.status === "rejected";
|
|
361
|
-
queryClient.setQueryData(threadKey(postId, locale), (d) => (d ?? [])
|
|
374
|
+
queryClient.setQueryData((0, keys_1.threadKey)(postId, locale), (d) => (d ?? [])
|
|
362
375
|
.filter((c) => !(rejected && c.id === tempId))
|
|
363
376
|
.map((c) => (c.id === tempId ? { ...c, id } : c)));
|
|
364
377
|
// A rejected comment never becomes visible, so undo the optimistic bump.
|
|
@@ -373,7 +386,7 @@ function useCreateComment() {
|
|
|
373
386
|
}
|
|
374
387
|
},
|
|
375
388
|
onError: (_e, { postId }, context) => {
|
|
376
|
-
queryClient.setQueryData(threadKey(postId, locale), (d) => (d ?? []).filter((c) => c.id !== context?.tempId));
|
|
389
|
+
queryClient.setQueryData((0, keys_1.threadKey)(postId, locale), (d) => (d ?? []).filter((c) => c.id !== context?.tempId));
|
|
377
390
|
bumpFeedCommentCount(queryClient, postId, -1);
|
|
378
391
|
},
|
|
379
392
|
});
|
|
@@ -492,10 +505,10 @@ function useDeleteContent() {
|
|
|
492
505
|
mutationFn: ({ kind, id }) => kind === "post" ? (0, service_1.deleteOwnPost)(cfg, id) : (0, service_1.deleteOwnComment)(cfg, id),
|
|
493
506
|
onSuccess: (_data, { kind, postId }) => {
|
|
494
507
|
(0, events_1.emitEvent)(cfg, events_1.COMMUNITY_EVENTS.contentDeleted, { contentType: kind });
|
|
495
|
-
queryClient.invalidateQueries({ queryKey: threadKey(postId, locale) });
|
|
496
|
-
queryClient.invalidateQueries({ queryKey: FEED_KEY });
|
|
497
|
-
queryClient.invalidateQueries({ queryKey: USER_POSTS_KEY });
|
|
498
|
-
queryClient.invalidateQueries({ queryKey: SEARCH_KEY });
|
|
508
|
+
queryClient.invalidateQueries({ queryKey: (0, keys_1.threadKey)(postId, locale) });
|
|
509
|
+
queryClient.invalidateQueries({ queryKey: keys_1.FEED_KEY });
|
|
510
|
+
queryClient.invalidateQueries({ queryKey: keys_1.USER_POSTS_KEY });
|
|
511
|
+
queryClient.invalidateQueries({ queryKey: keys_1.SEARCH_KEY });
|
|
499
512
|
},
|
|
500
513
|
});
|
|
501
514
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type { CommunityEventName } from "./events";
|
|
|
11
11
|
export { ensureIdentity, resetIdentity, syncProfileFromHost } from "./identity";
|
|
12
12
|
export { buildFeedSelect, fetchFeedPage, countNewPosts, fetchProfile, fetchUserPosts, searchPosts, fetchThread, createPost, votePoll, createComment, moderateOne, setLike, setReaction, fetchReactionSummaries, reportContent, blockUser, deleteOwnPost, deleteOwnComment, updateProfile, uploadAvatar, } from "./service";
|
|
13
13
|
export type { ModerationVerdict, CreatePostInput, ReportReason, ReportInput, UpdateProfileInput, UpdateProfileResult, } from "./service";
|
|
14
|
-
export { useCommunityFeed, useSearchPosts, useNewPostsCount, useCommunityUnseenCount, useThread, useProfile, useUserPosts, useMyUid, useUpdateProfile, useCreatePost, useCreateComment, useToggleLike, useVotePoll, useReactToPost, useReport, useBlockUser, useDeleteContent, } from "./hooks";
|
|
14
|
+
export { useCommunityFeed, useSearchPosts, useNewPostsCount, useCommunityUnseenCount, useThread, useProfile, useUserPosts, useMyUid, useUpdateProfile, useCreatePost, useCreateComment, useToggleLike, useVotePoll, useReactToPost, useReport, useBlockUser, useDeleteContent, isOptimistic, } from "./hooks";
|
|
15
15
|
export { fetchInbox, localizeExcerpts, markInboxSeen, unreadCount } from "./inbox-service";
|
|
16
16
|
export type { InboxItem, InboxState } from "./inbox-service";
|
|
17
17
|
export { useNotificationInbox, useUnreadNotificationCount, useMarkInboxSeen } from "./inbox-hooks";
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.deleteOwnComment = exports.deleteOwnPost = exports.blockUser = exports.reportContent = exports.fetchReactionSummaries = exports.setReaction = exports.setLike = exports.moderateOne = exports.createComment = exports.votePoll = exports.createPost = exports.fetchThread = exports.searchPosts = exports.fetchUserPosts = exports.fetchProfile = exports.countNewPosts = exports.fetchFeedPage = exports.buildFeedSelect = exports.syncProfileFromHost = exports.resetIdentity = exports.ensureIdentity = exports.emitEvent = exports.COMMUNITY_EVENTS = exports.timeAgoParts = exports.timeAgo = exports.newestCreatedAt = exports.newPostsLabel = exports.mapCommentRow = exports.applyReaction = exports.applyPollVote = exports.mapPostRow = exports.pollPercent = exports.buildPoll = exports.mapProfileRow = exports.normalizeTopic = exports.displayName = exports.EMPTY_REACTION_DATA = exports.EMPTY_POLL_DATA = exports.POLL_OPTION_MAX_LENGTH = exports.POLL_MAX_OPTIONS = exports.POLL_MIN_OPTIONS = exports.COMMENT_MAX_LENGTH = exports.POST_MAX_LENGTH = exports.FEED_PAGE_SIZE = exports.useCommunityConfig = exports.CommunityProvider = exports.languageOf = exports.readerLocale = exports.resolveConfig = exports.CommunityDisabledError = void 0;
|
|
4
|
-
exports.REQUIRED_SCHEMA_VERSION = exports.useMarkInboxSeen = exports.useUnreadNotificationCount = exports.useNotificationInbox = exports.unreadCount = exports.markInboxSeen = exports.localizeExcerpts = exports.fetchInbox = exports.useDeleteContent = exports.useBlockUser = exports.useReport = exports.useReactToPost = exports.useVotePoll = exports.useToggleLike = exports.useCreateComment = exports.useCreatePost = exports.useUpdateProfile = exports.useMyUid = exports.useUserPosts = exports.useProfile = exports.useThread = exports.useCommunityUnseenCount = exports.useNewPostsCount = exports.useSearchPosts = exports.useCommunityFeed = exports.uploadAvatar = exports.updateProfile = void 0;
|
|
4
|
+
exports.REQUIRED_SCHEMA_VERSION = exports.useMarkInboxSeen = exports.useUnreadNotificationCount = exports.useNotificationInbox = exports.unreadCount = exports.markInboxSeen = exports.localizeExcerpts = exports.fetchInbox = exports.isOptimistic = exports.useDeleteContent = exports.useBlockUser = exports.useReport = exports.useReactToPost = exports.useVotePoll = exports.useToggleLike = exports.useCreateComment = exports.useCreatePost = exports.useUpdateProfile = exports.useMyUid = exports.useUserPosts = exports.useProfile = exports.useThread = exports.useCommunityUnseenCount = exports.useNewPostsCount = exports.useSearchPosts = exports.useCommunityFeed = exports.uploadAvatar = exports.updateProfile = void 0;
|
|
5
5
|
var config_1 = require("./config");
|
|
6
6
|
Object.defineProperty(exports, "CommunityDisabledError", { enumerable: true, get: function () { return config_1.CommunityDisabledError; } });
|
|
7
7
|
Object.defineProperty(exports, "resolveConfig", { enumerable: true, get: function () { return config_1.resolveConfig; } });
|
|
@@ -80,6 +80,7 @@ Object.defineProperty(exports, "useReactToPost", { enumerable: true, get: functi
|
|
|
80
80
|
Object.defineProperty(exports, "useReport", { enumerable: true, get: function () { return hooks_1.useReport; } });
|
|
81
81
|
Object.defineProperty(exports, "useBlockUser", { enumerable: true, get: function () { return hooks_1.useBlockUser; } });
|
|
82
82
|
Object.defineProperty(exports, "useDeleteContent", { enumerable: true, get: function () { return hooks_1.useDeleteContent; } });
|
|
83
|
+
Object.defineProperty(exports, "isOptimistic", { enumerable: true, get: function () { return hooks_1.isOptimistic; } });
|
|
83
84
|
var inbox_service_1 = require("./inbox-service");
|
|
84
85
|
Object.defineProperty(exports, "fetchInbox", { enumerable: true, get: function () { return inbox_service_1.fetchInbox; } });
|
|
85
86
|
Object.defineProperty(exports, "localizeExcerpts", { enumerable: true, get: function () { return inbox_service_1.localizeExcerpts; } });
|
package/lib/keys.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const FEED_KEY: readonly ["community", "feed"];
|
|
2
|
+
export declare const feedKey: (topic?: string, locale?: string | null) => readonly ["community", "feed", string, string];
|
|
3
|
+
export declare const threadKey: (postId: string, locale?: string | null) => readonly ["community", "thread", string, string];
|
|
4
|
+
export declare const USER_POSTS_KEY: readonly ["community", "userPosts"];
|
|
5
|
+
export declare const userPostsKey: (userId: string, locale?: string | null) => readonly ["community", "userPosts", string, string];
|
|
6
|
+
export declare const SEARCH_KEY: readonly ["community", "search"];
|
|
7
|
+
export declare const searchKey: (cleaned: string, locale?: string | null) => readonly ["community", "search", string, string];
|
package/lib/keys.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// React Query keys (internal, not exported from the package). Every full key
|
|
3
|
+
// ends with the reader locale ("src" = originals) so switching locale never
|
|
4
|
+
// serves another locale's cached translations; the *_KEY prefixes stay
|
|
5
|
+
// prefixes of the full keys so setQueriesData/invalidateQueries match them all.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.searchKey = exports.SEARCH_KEY = exports.userPostsKey = exports.USER_POSTS_KEY = exports.threadKey = exports.feedKey = exports.FEED_KEY = void 0;
|
|
8
|
+
exports.FEED_KEY = ["community", "feed"];
|
|
9
|
+
const feedKey = (topic, locale) => [...exports.FEED_KEY, topic ?? "all", locale ?? "src"];
|
|
10
|
+
exports.feedKey = feedKey;
|
|
11
|
+
const threadKey = (postId, locale) => ["community", "thread", postId, locale ?? "src"];
|
|
12
|
+
exports.threadKey = threadKey;
|
|
13
|
+
exports.USER_POSTS_KEY = ["community", "userPosts"];
|
|
14
|
+
const userPostsKey = (userId, locale) => [...exports.USER_POSTS_KEY, userId, locale ?? "src"];
|
|
15
|
+
exports.userPostsKey = userPostsKey;
|
|
16
|
+
exports.SEARCH_KEY = ["community", "search"];
|
|
17
|
+
const searchKey = (cleaned, locale) => [...exports.SEARCH_KEY, cleaned, locale ?? "src"];
|
|
18
|
+
exports.searchKey = searchKey;
|
package/lib/service.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ import { type CommunityProfile, type FeedPost, type ReactionData, type ThreadCom
|
|
|
6
6
|
* names are dropped with a `console.warn` rather than failing the query.
|
|
7
7
|
*/
|
|
8
8
|
export declare function buildFeedSelect(extraPostColumns?: readonly string[], polls?: boolean, translations?: boolean): string;
|
|
9
|
+
/** Embedded-resource filters: keep only the reader locale's translation rows,
|
|
10
|
+
* so `rows?.[0]` in the mappers is always that locale, never some other
|
|
11
|
+
* locale's leftover row. PostgREST accepts dotted paths for nested embeds,
|
|
12
|
+
* the same mechanism as the existing `.eq("comments.status", "visible")`. A
|
|
13
|
+
* no-op (module off / no resolved locale) leaves the query, and therefore
|
|
14
|
+
* every byte of it, unchanged from before translations existed. */
|
|
15
|
+
export declare function withTranslationFilters<Q extends {
|
|
16
|
+
eq(column: string, value: string): Q;
|
|
17
|
+
}>(query: Q, cfg: ResolvedCommunityConfig): Q;
|
|
9
18
|
/**
|
|
10
19
|
* Batched reaction state for one page: count + last reactor's display name + my-reaction
|
|
11
20
|
* flag, all from the single security-definer `post_reaction_summary` RPC (the backend
|
|
@@ -49,6 +58,9 @@ export declare function fetchUserPosts(cfg: ResolvedCommunityConfig, userId: str
|
|
|
49
58
|
export declare function searchPosts(cfg: ResolvedCommunityConfig, searchTerm: string, opts?: {
|
|
50
59
|
cursor?: number;
|
|
51
60
|
}): Promise<FeedPost[]>;
|
|
61
|
+
/** The thread (comments) select: + the translation embed iff the reader has a
|
|
62
|
+
* resolved translation locale. */
|
|
63
|
+
export declare function threadSelect(cfg: ResolvedCommunityConfig): string;
|
|
52
64
|
/**
|
|
53
65
|
* Note: `cfg.feed.extraPostColumns`/`transformPost` do NOT apply here — this
|
|
54
66
|
* queries `comments`, not `posts` (the thread's parent post is passed in by
|
package/lib/service.js
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
// generalized from that host's prayer-style feature's summary RPC into `post_reaction_summary`).
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.buildFeedSelect = buildFeedSelect;
|
|
12
|
+
exports.withTranslationFilters = withTranslationFilters;
|
|
12
13
|
exports.fetchReactionSummaries = fetchReactionSummaries;
|
|
13
14
|
exports.fetchFeedPage = fetchFeedPage;
|
|
14
15
|
exports.countNewPosts = countNewPosts;
|
|
15
16
|
exports.fetchProfile = fetchProfile;
|
|
16
17
|
exports.fetchUserPosts = fetchUserPosts;
|
|
17
18
|
exports.searchPosts = searchPosts;
|
|
19
|
+
exports.threadSelect = threadSelect;
|
|
18
20
|
exports.fetchThread = fetchThread;
|
|
19
21
|
exports.createPost = createPost;
|
|
20
22
|
exports.votePoll = votePoll;
|
|
@@ -299,6 +301,11 @@ async function searchPosts(cfg, searchTerm, opts = {}) {
|
|
|
299
301
|
throw error;
|
|
300
302
|
return toFeedPosts(cfg, client, (data ?? []), uid);
|
|
301
303
|
}
|
|
304
|
+
/** The thread (comments) select: + the translation embed iff the reader has a
|
|
305
|
+
* resolved translation locale. */
|
|
306
|
+
function threadSelect(cfg) {
|
|
307
|
+
return (0, locale_1.readerLocale)(cfg) ? `${COMMENTS_SELECT}, ${COMMENT_TRANSLATIONS_SELECT}` : COMMENTS_SELECT;
|
|
308
|
+
}
|
|
302
309
|
/**
|
|
303
310
|
* Note: `cfg.feed.extraPostColumns`/`transformPost` do NOT apply here — this
|
|
304
311
|
* queries `comments`, not `posts` (the thread's parent post is passed in by
|
|
@@ -309,10 +316,9 @@ async function fetchThread(cfg, postId) {
|
|
|
309
316
|
const client = cfg.requireClient();
|
|
310
317
|
const uid = await requireUid(cfg);
|
|
311
318
|
const locale = (0, locale_1.readerLocale)(cfg);
|
|
312
|
-
const select = locale ? `${COMMENTS_SELECT}, ${COMMENT_TRANSLATIONS_SELECT}` : COMMENTS_SELECT;
|
|
313
319
|
let query = client
|
|
314
320
|
.from("comments")
|
|
315
|
-
.select(
|
|
321
|
+
.select(threadSelect(cfg))
|
|
316
322
|
.eq("post_id", postId)
|
|
317
323
|
.in("status", ["visible", "pending"])
|
|
318
324
|
.order("created_at", { ascending: true });
|
package/package.json
CHANGED