@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 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` | Selects which locale-specific behavior the _host_ wants (distinct from the UI package's own `translations.locale` prop) |
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 USER_POSTS_KEY = ["community", "userPosts"];
42
- const userPostsKey = (userId, locale) => [...USER_POSTS_KEY, userId, locale ?? "src"];
43
- const SEARCH_KEY = ["community", "search"];
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: [...SEARCH_KEY, cleaned, locale ?? "src"],
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: ["community", "myUid"],
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
- authorHandle: null,
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
- authorHandle: null,
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(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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocapine/community-core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Headless Supabase-backed community backend: identity, feed/thread/moderation services, and React Query hooks.",
5
5
  "license": "MIT",
6
6
  "repository": {