@rocapine/community-core 0.9.0 → 0.10.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/lib/hooks.d.ts CHANGED
@@ -120,16 +120,14 @@ export declare function useVotePoll(): import("@tanstack/react-query").UseMutati
120
120
  optionId: string;
121
121
  }, PostCacheSnapshot>;
122
122
  /**
123
- * Optimistic reaction across every post cache (mirror of useVotePoll; renamed
124
- * from the mold's `usePrayForPost`). `applyReaction(post)` is single-argument
125
- * (Task 3 ruling — it does not filter by post id), so the mapper here pre-filters
126
- * to the target post itself before calling it, and separately tracks whether
127
- * that post already had `hasReacted` so a retap of an already-reacted post
128
- * (the reaction is non-retractable, `setReaction` is idempotent) does not
129
- * re-emit the analytics event.
123
+ * Optimistic reaction toggle across every post cache (mirror of useToggleLike;
124
+ * renamed from the mold's `usePrayForPost`). `on` defaults to true (react);
125
+ * `on: false` withdraws it. `alreadyReacted` keeps a retap of an
126
+ * already-reacted post from re-emitting the analytics event.
130
127
  */
131
128
  export declare function useReactToPost(): import("@tanstack/react-query").UseMutationResult<void, Error, {
132
129
  postId: string;
130
+ on?: boolean;
133
131
  }, PostCacheSnapshot & {
134
132
  alreadyReacted: boolean;
135
133
  }>;
package/lib/hooks.js CHANGED
@@ -476,27 +476,24 @@ function useVotePoll() {
476
476
  });
477
477
  }
478
478
  /**
479
- * Optimistic reaction across every post cache (mirror of useVotePoll; renamed
480
- * from the mold's `usePrayForPost`). `applyReaction(post)` is single-argument
481
- * (Task 3 ruling — it does not filter by post id), so the mapper here pre-filters
482
- * to the target post itself before calling it, and separately tracks whether
483
- * that post already had `hasReacted` so a retap of an already-reacted post
484
- * (the reaction is non-retractable, `setReaction` is idempotent) does not
485
- * re-emit the analytics event.
479
+ * Optimistic reaction toggle across every post cache (mirror of useToggleLike;
480
+ * renamed from the mold's `usePrayForPost`). `on` defaults to true (react);
481
+ * `on: false` withdraws it. `alreadyReacted` keeps a retap of an
482
+ * already-reacted post from re-emitting the analytics event.
486
483
  */
487
484
  function useReactToPost() {
488
485
  const cfg = (0, provider_1.useCommunityConfig)();
489
486
  const queryClient = (0, react_query_1.useQueryClient)();
490
487
  return (0, react_query_1.useMutation)({
491
- mutationFn: ({ postId }) => (0, service_1.setReaction)(cfg, postId),
492
- onMutate: async ({ postId }) => {
488
+ mutationFn: ({ postId, on = true }) => (0, service_1.setReaction)(cfg, postId, on),
489
+ onMutate: async ({ postId, on = true }) => {
493
490
  let alreadyReacted = false;
494
491
  const snapshot = await applyOptimisticToAllPostCaches(queryClient, (post) => {
495
492
  if (post.id !== postId)
496
493
  return post;
497
494
  if (post.hasReacted)
498
495
  alreadyReacted = true;
499
- return (0, models_1.applyReaction)(post);
496
+ return (0, models_1.applyReaction)(post, on);
500
497
  });
501
498
  return { ...snapshot, alreadyReacted };
502
499
  },
@@ -504,8 +501,8 @@ function useReactToPost() {
504
501
  if (context)
505
502
  rollbackPostCaches(queryClient, context);
506
503
  },
507
- onSuccess: (_data, { postId }, context) => {
508
- if (!context?.alreadyReacted)
504
+ onSuccess: (_data, { postId, on = true }, context) => {
505
+ if (on && !context?.alreadyReacted)
509
506
  (0, events_1.emitEvent)(cfg, events_1.COMMUNITY_EVENTS.reactionAdded, { postId });
510
507
  },
511
508
  });
package/lib/models.d.ts CHANGED
@@ -203,7 +203,7 @@ export declare function applyPollVote(post: FeedPost, postId: string, optionId:
203
203
  * caller is expected to have already located the target post (e.g. via
204
204
  * `posts.map((p) => (p.id === postId ? applyReaction(p) : p))`).
205
205
  */
206
- export declare function applyReaction(post: FeedPost): FeedPost;
206
+ export declare function applyReaction(post: FeedPost, on?: boolean): FeedPost;
207
207
  export declare function mapCommentRow(row: CommentRow, myUid: string, fallback: string): ThreadComment;
208
208
  /** Floating pill label. "1 new post" / "N new posts". */
209
209
  export declare function newPostsLabel(count: number): string;
package/lib/models.js CHANGED
@@ -150,10 +150,14 @@ function applyPollVote(post, postId, optionId) {
150
150
  * caller is expected to have already located the target post (e.g. via
151
151
  * `posts.map((p) => (p.id === postId ? applyReaction(p) : p))`).
152
152
  */
153
- function applyReaction(post) {
154
- if (post.hasReacted)
153
+ function applyReaction(post, on = true) {
154
+ if (post.hasReacted === on)
155
155
  return post;
156
- return { ...post, hasReacted: true, reactionCount: post.reactionCount + 1 };
156
+ return {
157
+ ...post,
158
+ hasReacted: on,
159
+ reactionCount: Math.max(0, post.reactionCount + (on ? 1 : -1)),
160
+ };
157
161
  }
158
162
  function mapCommentRow(row, myUid, fallback) {
159
163
  return {
package/lib/service.d.ts CHANGED
@@ -102,10 +102,10 @@ export declare function moderateOne(cfg: ResolvedCommunityConfig, input: {
102
102
  }): Promise<ModerationVerdict>;
103
103
  export declare function setLike(cfg: ResolvedCommunityConfig, postId: string, on: boolean): Promise<void>;
104
104
  /**
105
- * `setPrayed` generalized to `setReaction`: insert my private, non-retractable
106
- * reaction row (rows are never deleted — see spec §6). Gated on `cfg.modules.reaction`.
105
+ * `setPrayed` generalized to `setReaction`: insert (`on`) or delete my private
106
+ * reaction row, like `setLike`. Gated on `cfg.modules.reaction`.
107
107
  */
108
- export declare function setReaction(cfg: ResolvedCommunityConfig, postId: string): Promise<void>;
108
+ export declare function setReaction(cfg: ResolvedCommunityConfig, postId: string, on?: boolean): Promise<void>;
109
109
  export type ReportReason = "spam" | "harassment" | "hate" | "inappropriate" | "other";
110
110
  export interface ReportInput {
111
111
  reportedUserId: string;
package/lib/service.js CHANGED
@@ -419,18 +419,28 @@ async function setLike(cfg, postId, on) {
419
419
  }
420
420
  }
421
421
  /**
422
- * `setPrayed` generalized to `setReaction`: insert my private, non-retractable
423
- * reaction row (rows are never deleted — see spec §6). Gated on `cfg.modules.reaction`.
422
+ * `setPrayed` generalized to `setReaction`: insert (`on`) or delete my private
423
+ * reaction row, like `setLike`. Gated on `cfg.modules.reaction`.
424
424
  */
425
- async function setReaction(cfg, postId) {
425
+ async function setReaction(cfg, postId, on = true) {
426
426
  if (!cfg.modules.reaction)
427
427
  throw new Error("reaction module is not enabled");
428
428
  const client = cfg.requireClient();
429
429
  const uid = await requireUid(cfg);
430
- const { error } = await client.from("post_reactions").insert({ post_id: postId, user_id: uid });
431
- // 23505 = already reacted to this post: the desired state holds.
432
- if (error && error.code !== "23505")
433
- throw error;
430
+ if (on) {
431
+ const { error } = await client.from("post_reactions").insert({ post_id: postId, user_id: uid });
432
+ // 23505 = already reacted to this post: the desired state holds.
433
+ if (error && error.code !== "23505")
434
+ throw error;
435
+ }
436
+ else {
437
+ const { error } = await client
438
+ .from("post_reactions")
439
+ .delete()
440
+ .match({ post_id: postId, user_id: uid });
441
+ if (error)
442
+ throw error;
443
+ }
434
444
  }
435
445
  async function reportContent(cfg, input) {
436
446
  const client = cfg.requireClient();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocapine/community-core",
3
- "version": "0.9.0",
3
+ "version": "0.10.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": {