jansathi-community-schema 0.19.0 → 0.22.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/dist/comment.d.ts CHANGED
@@ -2,13 +2,14 @@
2
2
  * Comment wire shapes (with one-level reply nesting).
3
3
  *
4
4
  * Comments attach to any feed item via `(contentKind, contentId)` — the
5
- * same polymorphic key used by reactions and upvotes. One level of
5
+ * same polymorphic key used by reactions and votes. One level of
6
6
  * nesting only: a reply to a comment is allowed; a reply to a reply
7
7
  * is rejected at the server.
8
8
  *
9
- * The shape mirrors the post's `viewer` pattern so the client can
10
- * render an edit button on own comments and a reaction picker on
11
- * every comment.
9
+ * The shape mirrors the post's `viewer` pattern (minus `bookmarked`
10
+ * comments aren't bookmarkable, the parent post is the right scope)
11
+ * so the client can render an edit button on own comments and a
12
+ * reaction picker on every comment.
12
13
  *
13
14
  * @module community-schema/comment
14
15
  */
@@ -58,14 +59,17 @@ export declare const communityCommentWireSchema: z.ZodObject<{
58
59
  identityVerified: z.ZodOptional<z.ZodBoolean>;
59
60
  }, z.core.$strip>;
60
61
  text: z.ZodString;
61
- upvoteCount: z.ZodNumber;
62
+ score: z.ZodNumber;
62
63
  replyCount: z.ZodNumber;
63
64
  reactionCounts: z.ZodRecord<z.ZodString, z.ZodNumber>;
64
65
  createdAt: z.ZodString;
65
66
  editedAt: z.ZodNullable<z.ZodString>;
66
67
  deletedAt: z.ZodNullable<z.ZodString>;
67
68
  viewer: z.ZodOptional<z.ZodObject<{
68
- upvoted: z.ZodBoolean;
69
+ vote: z.ZodNullable<z.ZodEnum<{
70
+ up: "up";
71
+ down: "down";
72
+ }>>;
69
73
  reaction: z.ZodNullable<z.ZodString>;
70
74
  isAuthor: z.ZodBoolean;
71
75
  }, z.core.$strip>>;
@@ -1 +1 @@
1
- {"version":3,"file":"comment.d.ts","sourceRoot":"","sources":["../src/comment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkCrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAI9E,eAAO,MAAM,uBAAuB;;;iBAMlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAIxE,eAAO,MAAM,uBAAuB;;iBAElC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
1
+ {"version":3,"file":"comment.d.ts","sourceRoot":"","sources":["../src/comment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAI9E,eAAO,MAAM,uBAAuB;;;iBAMlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAIxE,eAAO,MAAM,uBAAuB;;iBAElC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
package/dist/comment.js CHANGED
@@ -2,18 +2,20 @@
2
2
  * Comment wire shapes (with one-level reply nesting).
3
3
  *
4
4
  * Comments attach to any feed item via `(contentKind, contentId)` — the
5
- * same polymorphic key used by reactions and upvotes. One level of
5
+ * same polymorphic key used by reactions and votes. One level of
6
6
  * nesting only: a reply to a comment is allowed; a reply to a reply
7
7
  * is rejected at the server.
8
8
  *
9
- * The shape mirrors the post's `viewer` pattern so the client can
10
- * render an edit button on own comments and a reaction picker on
11
- * every comment.
9
+ * The shape mirrors the post's `viewer` pattern (minus `bookmarked`
10
+ * comments aren't bookmarkable, the parent post is the right scope)
11
+ * so the client can render an edit button on own comments and a
12
+ * reaction picker on every comment.
12
13
  *
13
14
  * @module community-schema/comment
14
15
  */
15
16
  import { z } from 'zod';
16
17
  import { COMMENT_MAX_BODY_CHARS } from './constants.js';
18
+ import { voteValueSchema } from './engagement.js';
17
19
  import { contentKindSchema } from './enums.js';
18
20
  import { postAuthorSnapshotSchema } from './post.js';
19
21
  // ─── Wire shape ─────────────────────────────────────────────────────────────
@@ -30,7 +32,8 @@ export const communityCommentWireSchema = z.object({
30
32
  // Body
31
33
  text: z.string().max(COMMENT_MAX_BODY_CHARS),
32
34
  // Denormalised engagement
33
- upvoteCount: z.number().int().nonnegative(),
35
+ /** Net score = upvotes - downvotes. Signed. */
36
+ score: z.number().int(),
34
37
  /** Number of replies. Only meaningful on top-level comments; always
35
38
  * 0 on replies (the server enforces no-nesting). */
36
39
  replyCount: z.number().int().nonnegative(),
@@ -42,7 +45,7 @@ export const communityCommentWireSchema = z.object({
42
45
  // Per-viewer state
43
46
  viewer: z
44
47
  .object({
45
- upvoted: z.boolean(),
48
+ vote: voteValueSchema.nullable(),
46
49
  reaction: z.string().nullable(),
47
50
  isAuthor: z.boolean(),
48
51
  })
@@ -1 +1 @@
1
- {"version":3,"file":"comment.js","sourceRoot":"","sources":["../src/comment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAErD,+EAA+E;AAE/E,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,6CAA6C;IAC7C,WAAW,EAAE,iBAAiB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB;;mEAE+D;IAC/D,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACtC,MAAM,EAAE,wBAAwB;IAEhC,OAAO;IACP,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,sBAAsB,CAAC;IAE5C,0BAA0B;IAC1B,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC3C;yDACqD;IACrD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC1C,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IAEpE,YAAY;IACZ,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC1C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAE3C,mBAAmB;IACnB,MAAM,EAAE,CAAC;SACN,MAAM,CAAC;QACN,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;QACpB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC/B,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;KACtB,CAAC;SACD,QAAQ,EAAE;CACd,CAAC,CAAC;AAGH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC;IACnD;;mCAE+B;IAC/B,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAC;AAGH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC;CACpD,CAAC,CAAC","sourcesContent":["/**\n * Comment wire shapes (with one-level reply nesting).\n *\n * Comments attach to any feed item via `(contentKind, contentId)` — the\n * same polymorphic key used by reactions and upvotes. One level of\n * nesting only: a reply to a comment is allowed; a reply to a reply\n * is rejected at the server.\n *\n * The shape mirrors the post's `viewer` pattern so the client can\n * render an edit button on own comments and a reaction picker on\n * every comment.\n *\n * @module community-schema/comment\n */\n\nimport { z } from 'zod';\nimport { COMMENT_MAX_BODY_CHARS } from './constants.js';\nimport { contentKindSchema } from './enums.js';\nimport { postAuthorSnapshotSchema } from './post.js';\n\n// ─── Wire shape ─────────────────────────────────────────────────────────────\n\nexport const communityCommentWireSchema = z.object({\n _id: z.string(),\n /** The feed item this comment belongs to. */\n contentKind: contentKindSchema,\n contentId: z.string(),\n /** The parent comment id, or null if this is a top-level comment.\n * A non-null value MAY only point at a top-level comment — the\n * server enforces this so the tree never grows to depth 3. */\n parentCommentId: z.string().nullable(),\n author: postAuthorSnapshotSchema,\n\n // Body\n text: z.string().max(COMMENT_MAX_BODY_CHARS),\n\n // Denormalised engagement\n upvoteCount: z.number().int().nonnegative(),\n /** Number of replies. Only meaningful on top-level comments; always\n * 0 on replies (the server enforces no-nesting). */\n replyCount: z.number().int().nonnegative(),\n reactionCounts: z.record(z.string(), z.number().int().nonnegative()),\n\n // Lifecycle\n createdAt: z.string().datetime(),\n editedAt: z.string().datetime().nullable(),\n deletedAt: z.string().datetime().nullable(),\n\n // Per-viewer state\n viewer: z\n .object({\n upvoted: z.boolean(),\n reaction: z.string().nullable(),\n isAuthor: z.boolean(),\n })\n .optional(),\n});\nexport type CommunityCommentWire = z.infer<typeof communityCommentWireSchema>;\n\n// ─── Create body ────────────────────────────────────────────────────────────\n\nexport const createCommentBodySchema = z.object({\n text: z.string().min(1).max(COMMENT_MAX_BODY_CHARS),\n /** Null = top-level comment on the content; a string id = reply to\n * a specific top-level comment. Server rejects ids pointing at a\n * reply (depth-3 attempt). */\n parentCommentId: z.string().nullable(),\n});\nexport type CreateCommentBody = z.infer<typeof createCommentBodySchema>;\n\n// ─── Update body ────────────────────────────────────────────────────────────\n\nexport const updateCommentBodySchema = z.object({\n text: z.string().min(1).max(COMMENT_MAX_BODY_CHARS),\n});\nexport type UpdateCommentBody = z.infer<typeof updateCommentBodySchema>;\n"]}
1
+ {"version":3,"file":"comment.js","sourceRoot":"","sources":["../src/comment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAErD,+EAA+E;AAE/E,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,6CAA6C;IAC7C,WAAW,EAAE,iBAAiB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB;;mEAE+D;IAC/D,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACtC,MAAM,EAAE,wBAAwB;IAEhC,OAAO;IACP,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,sBAAsB,CAAC;IAE5C,0BAA0B;IAC1B,+CAA+C;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACvB;yDACqD;IACrD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC1C,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IAEpE,YAAY;IACZ,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC1C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAE3C,mBAAmB;IACnB,MAAM,EAAE,CAAC;SACN,MAAM,CAAC;QACN,IAAI,EAAE,eAAe,CAAC,QAAQ,EAAE;QAChC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC/B,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;KACtB,CAAC;SACD,QAAQ,EAAE;CACd,CAAC,CAAC;AAGH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC;IACnD;;mCAE+B;IAC/B,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAC;AAGH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC;CACpD,CAAC,CAAC","sourcesContent":["/**\n * Comment wire shapes (with one-level reply nesting).\n *\n * Comments attach to any feed item via `(contentKind, contentId)` — the\n * same polymorphic key used by reactions and votes. One level of\n * nesting only: a reply to a comment is allowed; a reply to a reply\n * is rejected at the server.\n *\n * The shape mirrors the post's `viewer` pattern (minus `bookmarked` —\n * comments aren't bookmarkable, the parent post is the right scope)\n * so the client can render an edit button on own comments and a\n * reaction picker on every comment.\n *\n * @module community-schema/comment\n */\n\nimport { z } from 'zod';\nimport { COMMENT_MAX_BODY_CHARS } from './constants.js';\nimport { voteValueSchema } from './engagement.js';\nimport { contentKindSchema } from './enums.js';\nimport { postAuthorSnapshotSchema } from './post.js';\n\n// ─── Wire shape ─────────────────────────────────────────────────────────────\n\nexport const communityCommentWireSchema = z.object({\n _id: z.string(),\n /** The feed item this comment belongs to. */\n contentKind: contentKindSchema,\n contentId: z.string(),\n /** The parent comment id, or null if this is a top-level comment.\n * A non-null value MAY only point at a top-level comment — the\n * server enforces this so the tree never grows to depth 3. */\n parentCommentId: z.string().nullable(),\n author: postAuthorSnapshotSchema,\n\n // Body\n text: z.string().max(COMMENT_MAX_BODY_CHARS),\n\n // Denormalised engagement\n /** Net score = upvotes - downvotes. Signed. */\n score: z.number().int(),\n /** Number of replies. Only meaningful on top-level comments; always\n * 0 on replies (the server enforces no-nesting). */\n replyCount: z.number().int().nonnegative(),\n reactionCounts: z.record(z.string(), z.number().int().nonnegative()),\n\n // Lifecycle\n createdAt: z.string().datetime(),\n editedAt: z.string().datetime().nullable(),\n deletedAt: z.string().datetime().nullable(),\n\n // Per-viewer state\n viewer: z\n .object({\n vote: voteValueSchema.nullable(),\n reaction: z.string().nullable(),\n isAuthor: z.boolean(),\n })\n .optional(),\n});\nexport type CommunityCommentWire = z.infer<typeof communityCommentWireSchema>;\n\n// ─── Create body ────────────────────────────────────────────────────────────\n\nexport const createCommentBodySchema = z.object({\n text: z.string().min(1).max(COMMENT_MAX_BODY_CHARS),\n /** Null = top-level comment on the content; a string id = reply to\n * a specific top-level comment. Server rejects ids pointing at a\n * reply (depth-3 attempt). */\n parentCommentId: z.string().nullable(),\n});\nexport type CreateCommentBody = z.infer<typeof createCommentBodySchema>;\n\n// ─── Update body ────────────────────────────────────────────────────────────\n\nexport const updateCommentBodySchema = z.object({\n text: z.string().min(1).max(COMMENT_MAX_BODY_CHARS),\n});\nexport type UpdateCommentBody = z.infer<typeof updateCommentBodySchema>;\n"]}
@@ -1,23 +1,63 @@
1
1
  /**
2
- * Engagement wire shapes — upvotes and reactions.
2
+ * Engagement wire shapes — vote, reaction, and bookmark.
3
3
  *
4
- * Both are keyed by `(contentKind, contentId)` so the same engagement
5
- * bar applies to every kind of feed item (post, lost-and-found,
6
- * voice-box) and to comments.
4
+ * Three independent axes per the product decision:
7
5
  *
8
- * Per the product decision, upvote and reaction are TWO INDEPENDENT
9
- * axes a viewer can upvote AND react. Upvote drives feed sort;
10
- * reaction is the expressive layer.
6
+ * - **Vote** drives ranking. A viewer can be in one of three vote
7
+ * states: `'up'` (+1 contribution to score), `'down'` (-1), or
8
+ * `null` (no vote). The card surfaces these as paired up/down
9
+ * arrows on either side of a single signed `score` — the score
10
+ * can go negative when downvotes exceed upvotes.
11
+ *
12
+ * - **Reaction** is the expressive layer. At most one active
13
+ * reaction per (viewer, target); reactions don't influence the
14
+ * score.
15
+ *
16
+ * - **Bookmark** is private to the viewer. Adds the target to the
17
+ * viewer's saved-items list; doesn't affect counts or ranking
18
+ * visible to others.
19
+ *
20
+ * Every axis is keyed by `(contentKind, contentId)` so the same
21
+ * EngagementBar applies to every kind of feed item AND to comments.
11
22
  *
12
23
  * @module community-schema/engagement
13
24
  */
14
25
  import { z } from 'zod';
15
26
  /**
16
- * What the client sends to `POST /api/v1/community/:contentKind/:id/upvote`.
17
- * The endpoint is a toggle — calling it twice in a row removes the
18
- * upvote. Idempotent: the server returns the post-toggle state.
27
+ * The vote state of a viewer relative to a target.
28
+ *
29
+ * - `'up'` the viewer upvoted (+1 to score)
30
+ * - `'down'` the viewer downvoted (-1 to score)
31
+ * - `null` no vote cast
32
+ */
33
+ export declare const VOTE_VALUES: readonly ["up", "down"];
34
+ export declare const voteValueSchema: z.ZodEnum<{
35
+ up: "up";
36
+ down: "down";
37
+ }>;
38
+ export type VoteValue = z.infer<typeof voteValueSchema>;
39
+ /**
40
+ * Body for `POST /api/v1/community/:contentKind/:id/vote`.
41
+ *
42
+ * Setting `vote: 'up' | 'down'` records that as the viewer's vote
43
+ * (replacing any prior vote in the opposite direction). Setting
44
+ * `vote: null` clears the viewer's vote entirely. The endpoint is
45
+ * idempotent — sending the same vote twice is a no-op; the response
46
+ * always reflects the final state.
47
+ */
48
+ export declare const setVoteBodySchema: z.ZodObject<{
49
+ vote: z.ZodNullable<z.ZodEnum<{
50
+ up: "up";
51
+ down: "down";
52
+ }>>;
53
+ }, z.core.$strip>;
54
+ export type SetVoteBody = z.infer<typeof setVoteBodySchema>;
55
+ /**
56
+ * Response from the vote endpoint. Returns the viewer's new vote
57
+ * AND the target's post-write score so the client can render
58
+ * straight off the response without a follow-up read.
19
59
  */
20
- export declare const toggleUpvoteResponseSchema: z.ZodObject<{
60
+ export declare const setVoteResponseSchema: z.ZodObject<{
21
61
  contentKind: z.ZodEnum<{
22
62
  post: "post";
23
63
  lost_found: "lost_found";
@@ -26,16 +66,19 @@ export declare const toggleUpvoteResponseSchema: z.ZodObject<{
26
66
  poll: "poll";
27
67
  }>;
28
68
  contentId: z.ZodString;
29
- upvoted: z.ZodBoolean;
30
- upvoteCount: z.ZodNumber;
69
+ vote: z.ZodNullable<z.ZodEnum<{
70
+ up: "up";
71
+ down: "down";
72
+ }>>;
73
+ score: z.ZodNumber;
31
74
  }, z.core.$strip>;
32
- export type ToggleUpvoteResponse = z.infer<typeof toggleUpvoteResponseSchema>;
75
+ export type SetVoteResponse = z.infer<typeof setVoteResponseSchema>;
33
76
  /**
34
- * What the client sends to `POST /api/v1/community/:contentKind/:id/reactions`.
77
+ * Body for `POST /api/v1/community/:contentKind/:id/reactions`.
35
78
  *
36
- * Setting `reaction` to a value replaces the user's current reaction
37
- * (a user may have at most ONE active reaction per target).
38
- * Setting `reaction` to null clears it.
79
+ * Setting `reaction` to a value replaces the viewer's current
80
+ * reaction (at most ONE active reaction per target). Setting
81
+ * `reaction` to null clears it.
39
82
  */
40
83
  export declare const setReactionBodySchema: z.ZodObject<{
41
84
  reaction: z.ZodNullable<z.ZodEnum<{
@@ -70,4 +113,24 @@ export declare const setReactionResponseSchema: z.ZodObject<{
70
113
  reactionCounts: z.ZodRecord<z.ZodString, z.ZodNumber>;
71
114
  }, z.core.$strip>;
72
115
  export type SetReactionResponse = z.infer<typeof setReactionResponseSchema>;
116
+ /**
117
+ * Response from `POST /api/v1/community/:contentKind/:id/bookmark`.
118
+ *
119
+ * The endpoint is a toggle — it flips the viewer's bookmark state on
120
+ * the target. Returns the new state so the card can render directly
121
+ * off the response. Bookmarks are PRIVATE to the viewer; they don't
122
+ * affect any aggregate count surfaced to other viewers.
123
+ */
124
+ export declare const toggleBookmarkResponseSchema: z.ZodObject<{
125
+ contentKind: z.ZodEnum<{
126
+ post: "post";
127
+ lost_found: "lost_found";
128
+ voice_box: "voice_box";
129
+ donate_item: "donate_item";
130
+ poll: "poll";
131
+ }>;
132
+ contentId: z.ZodString;
133
+ bookmarked: z.ZodBoolean;
134
+ }, z.core.$strip>;
135
+ export type ToggleBookmarkResponse = z.infer<typeof toggleBookmarkResponseSchema>;
73
136
  //# sourceMappingURL=engagement.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engagement.d.ts","sourceRoot":"","sources":["../src/engagement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;GAIG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;iBAQrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAI9E;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;iBAEhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;iBAOpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
1
+ {"version":3,"file":"engagement.d.ts","sourceRoot":"","sources":["../src/engagement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,yBAA0B,CAAC;AACnD,eAAO,MAAM,eAAe;;;EAAsB,CAAC;AACnD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB;;;;;iBAE5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;iBAUhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAIpE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;iBAEhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;iBAOpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;iBAKvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"}
@@ -1,40 +1,74 @@
1
1
  /**
2
- * Engagement wire shapes — upvotes and reactions.
2
+ * Engagement wire shapes — vote, reaction, and bookmark.
3
3
  *
4
- * Both are keyed by `(contentKind, contentId)` so the same engagement
5
- * bar applies to every kind of feed item (post, lost-and-found,
6
- * voice-box) and to comments.
4
+ * Three independent axes per the product decision:
7
5
  *
8
- * Per the product decision, upvote and reaction are TWO INDEPENDENT
9
- * axes a viewer can upvote AND react. Upvote drives feed sort;
10
- * reaction is the expressive layer.
6
+ * - **Vote** drives ranking. A viewer can be in one of three vote
7
+ * states: `'up'` (+1 contribution to score), `'down'` (-1), or
8
+ * `null` (no vote). The card surfaces these as paired up/down
9
+ * arrows on either side of a single signed `score` — the score
10
+ * can go negative when downvotes exceed upvotes.
11
+ *
12
+ * - **Reaction** is the expressive layer. At most one active
13
+ * reaction per (viewer, target); reactions don't influence the
14
+ * score.
15
+ *
16
+ * - **Bookmark** is private to the viewer. Adds the target to the
17
+ * viewer's saved-items list; doesn't affect counts or ranking
18
+ * visible to others.
19
+ *
20
+ * Every axis is keyed by `(contentKind, contentId)` so the same
21
+ * EngagementBar applies to every kind of feed item AND to comments.
11
22
  *
12
23
  * @module community-schema/engagement
13
24
  */
14
25
  import { z } from 'zod';
15
26
  import { contentKindSchema, reactionTypeSchema } from './enums.js';
16
- // ─── Upvote ─────────────────────────────────────────────────────────────────
27
+ // ─── Vote ───────────────────────────────────────────────────────────────────
28
+ /**
29
+ * The vote state of a viewer relative to a target.
30
+ *
31
+ * - `'up'` the viewer upvoted (+1 to score)
32
+ * - `'down'` the viewer downvoted (-1 to score)
33
+ * - `null` no vote cast
34
+ */
35
+ export const VOTE_VALUES = ['up', 'down'];
36
+ export const voteValueSchema = z.enum(VOTE_VALUES);
37
+ /**
38
+ * Body for `POST /api/v1/community/:contentKind/:id/vote`.
39
+ *
40
+ * Setting `vote: 'up' | 'down'` records that as the viewer's vote
41
+ * (replacing any prior vote in the opposite direction). Setting
42
+ * `vote: null` clears the viewer's vote entirely. The endpoint is
43
+ * idempotent — sending the same vote twice is a no-op; the response
44
+ * always reflects the final state.
45
+ */
46
+ export const setVoteBodySchema = z.object({
47
+ vote: voteValueSchema.nullable(),
48
+ });
17
49
  /**
18
- * What the client sends to `POST /api/v1/community/:contentKind/:id/upvote`.
19
- * The endpoint is a toggle calling it twice in a row removes the
20
- * upvote. Idempotent: the server returns the post-toggle state.
50
+ * Response from the vote endpoint. Returns the viewer's new vote
51
+ * AND the target's post-write score so the client can render
52
+ * straight off the response without a follow-up read.
21
53
  */
22
- export const toggleUpvoteResponseSchema = z.object({
54
+ export const setVoteResponseSchema = z.object({
23
55
  contentKind: contentKindSchema,
24
56
  contentId: z.string(),
25
- /** New upvote state for the calling user. */
26
- upvoted: z.boolean(),
27
- /** Post-toggle aggregate count, returned so the client can render
28
- * without a follow-up read. */
29
- upvoteCount: z.number().int().nonnegative(),
57
+ /** The viewer's vote AFTER this call. */
58
+ vote: voteValueSchema.nullable(),
59
+ /**
60
+ * Net score = upvotes - downvotes. CAN BE NEGATIVE when downvotes
61
+ * exceed upvotes. The card's middle number reads this directly.
62
+ */
63
+ score: z.number().int(),
30
64
  });
31
65
  // ─── Reaction ───────────────────────────────────────────────────────────────
32
66
  /**
33
- * What the client sends to `POST /api/v1/community/:contentKind/:id/reactions`.
67
+ * Body for `POST /api/v1/community/:contentKind/:id/reactions`.
34
68
  *
35
- * Setting `reaction` to a value replaces the user's current reaction
36
- * (a user may have at most ONE active reaction per target).
37
- * Setting `reaction` to null clears it.
69
+ * Setting `reaction` to a value replaces the viewer's current
70
+ * reaction (at most ONE active reaction per target). Setting
71
+ * `reaction` to null clears it.
38
72
  */
39
73
  export const setReactionBodySchema = z.object({
40
74
  reaction: reactionTypeSchema.nullable(),
@@ -42,9 +76,24 @@ export const setReactionBodySchema = z.object({
42
76
  export const setReactionResponseSchema = z.object({
43
77
  contentKind: contentKindSchema,
44
78
  contentId: z.string(),
45
- /** The viewer's new reaction, or null when cleared. */
79
+ /** Viewer's new reaction, or null when cleared. */
46
80
  reaction: reactionTypeSchema.nullable(),
47
81
  /** Post-set aggregate counts, keyed by reaction type. */
48
82
  reactionCounts: z.record(z.string(), z.number().int().nonnegative()),
49
83
  });
84
+ // ─── Bookmark ───────────────────────────────────────────────────────────────
85
+ /**
86
+ * Response from `POST /api/v1/community/:contentKind/:id/bookmark`.
87
+ *
88
+ * The endpoint is a toggle — it flips the viewer's bookmark state on
89
+ * the target. Returns the new state so the card can render directly
90
+ * off the response. Bookmarks are PRIVATE to the viewer; they don't
91
+ * affect any aggregate count surfaced to other viewers.
92
+ */
93
+ export const toggleBookmarkResponseSchema = z.object({
94
+ contentKind: contentKindSchema,
95
+ contentId: z.string(),
96
+ /** Viewer's bookmark state AFTER this call. */
97
+ bookmarked: z.boolean(),
98
+ });
50
99
  //# sourceMappingURL=engagement.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"engagement.js","sourceRoot":"","sources":["../src/engagement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnE,+EAA+E;AAE/E;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,WAAW,EAAE,iBAAiB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,6CAA6C;IAC7C,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB;oCACgC;IAChC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;CAC5C,CAAC,CAAC;AAGH,+EAA+E;AAE/E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,QAAQ,EAAE,kBAAkB,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,WAAW,EAAE,iBAAiB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,uDAAuD;IACvD,QAAQ,EAAE,kBAAkB,CAAC,QAAQ,EAAE;IACvC,yDAAyD;IACzD,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;CACrE,CAAC,CAAC","sourcesContent":["/**\n * Engagement wire shapes — upvotes and reactions.\n *\n * Both are keyed by `(contentKind, contentId)` so the same engagement\n * bar applies to every kind of feed item (post, lost-and-found,\n * voice-box) and to comments.\n *\n * Per the product decision, upvote and reaction are TWO INDEPENDENT\n * axes a viewer can upvote AND react. Upvote drives feed sort;\n * reaction is the expressive layer.\n *\n * @module community-schema/engagement\n */\n\nimport { z } from 'zod';\nimport { contentKindSchema, reactionTypeSchema } from './enums.js';\n\n// ─── Upvote ─────────────────────────────────────────────────────────────────\n\n/**\n * What the client sends to `POST /api/v1/community/:contentKind/:id/upvote`.\n * The endpoint is a togglecalling it twice in a row removes the\n * upvote. Idempotent: the server returns the post-toggle state.\n */\nexport const toggleUpvoteResponseSchema = z.object({\n contentKind: contentKindSchema,\n contentId: z.string(),\n /** New upvote state for the calling user. */\n upvoted: z.boolean(),\n /** Post-toggle aggregate count, returned so the client can render\n * without a follow-up read. */\n upvoteCount: z.number().int().nonnegative(),\n});\nexport type ToggleUpvoteResponse = z.infer<typeof toggleUpvoteResponseSchema>;\n\n// ─── Reaction ───────────────────────────────────────────────────────────────\n\n/**\n * What the client sends to `POST /api/v1/community/:contentKind/:id/reactions`.\n *\n * Setting `reaction` to a value replaces the user's current reaction\n * (a user may have at most ONE active reaction per target).\n * Setting `reaction` to null clears it.\n */\nexport const setReactionBodySchema = z.object({\n reaction: reactionTypeSchema.nullable(),\n});\nexport type SetReactionBody = z.infer<typeof setReactionBodySchema>;\n\nexport const setReactionResponseSchema = z.object({\n contentKind: contentKindSchema,\n contentId: z.string(),\n /** The viewer's new reaction, or null when cleared. */\n reaction: reactionTypeSchema.nullable(),\n /** Post-set aggregate counts, keyed by reaction type. */\n reactionCounts: z.record(z.string(), z.number().int().nonnegative()),\n});\nexport type SetReactionResponse = z.infer<typeof setReactionResponseSchema>;\n"]}
1
+ {"version":3,"file":"engagement.js","sourceRoot":"","sources":["../src/engagement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnE,+EAA+E;AAE/E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,CAAU,CAAC;AACnD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;AAGnD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,IAAI,EAAE,eAAe,CAAC,QAAQ,EAAE;CACjC,CAAC,CAAC;AAGH;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,WAAW,EAAE,iBAAiB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,yCAAyC;IACzC,IAAI,EAAE,eAAe,CAAC,QAAQ,EAAE;IAChC;;;OAGG;IACH,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;CACxB,CAAC,CAAC;AAGH,+EAA+E;AAE/E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,QAAQ,EAAE,kBAAkB,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,WAAW,EAAE,iBAAiB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,mDAAmD;IACnD,QAAQ,EAAE,kBAAkB,CAAC,QAAQ,EAAE;IACvC,yDAAyD;IACzD,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;CACrE,CAAC,CAAC;AAGH,+EAA+E;AAE/E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC,MAAM,CAAC;IACnD,WAAW,EAAE,iBAAiB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,+CAA+C;IAC/C,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE;CACxB,CAAC,CAAC","sourcesContent":["/**\n * Engagement wire shapes — vote, reaction, and bookmark.\n *\n * Three independent axes per the product decision:\n *\n * - **Vote** drives ranking. A viewer can be in one of three vote\n * states: `'up'` (+1 contribution to score), `'down'` (-1), or\n * `null` (no vote). The card surfaces these as paired up/down\n * arrows on either side of a single signed `score` the score\n * can go negative when downvotes exceed upvotes.\n *\n * - **Reaction** is the expressive layer. At most one active\n * reaction per (viewer, target); reactions don't influence the\n * score.\n *\n * - **Bookmark** is private to the viewer. Adds the target to the\n * viewer's saved-items list; doesn't affect counts or ranking\n * visible to others.\n *\n * Every axis is keyed by `(contentKind, contentId)` so the same\n * EngagementBar applies to every kind of feed item AND to comments.\n *\n * @module community-schema/engagement\n */\n\nimport { z } from 'zod';\nimport { contentKindSchema, reactionTypeSchema } from './enums.js';\n\n// ─── Vote ───────────────────────────────────────────────────────────────────\n\n/**\n * The vote state of a viewer relative to a target.\n *\n * - `'up'` the viewer upvoted (+1 to score)\n * - `'down'` the viewer downvoted (-1 to score)\n * - `null` no vote cast\n */\nexport const VOTE_VALUES = ['up', 'down'] as const;\nexport const voteValueSchema = z.enum(VOTE_VALUES);\nexport type VoteValue = z.infer<typeof voteValueSchema>;\n\n/**\n * Body for `POST /api/v1/community/:contentKind/:id/vote`.\n *\n * Setting `vote: 'up' | 'down'` records that as the viewer's vote\n * (replacing any prior vote in the opposite direction). Setting\n * `vote: null` clears the viewer's vote entirely. The endpoint is\n * idempotentsending the same vote twice is a no-op; the response\n * always reflects the final state.\n */\nexport const setVoteBodySchema = z.object({\n vote: voteValueSchema.nullable(),\n});\nexport type SetVoteBody = z.infer<typeof setVoteBodySchema>;\n\n/**\n * Response from the vote endpoint. Returns the viewer's new vote\n * AND the target's post-write score so the client can render\n * straight off the response without a follow-up read.\n */\nexport const setVoteResponseSchema = z.object({\n contentKind: contentKindSchema,\n contentId: z.string(),\n /** The viewer's vote AFTER this call. */\n vote: voteValueSchema.nullable(),\n /**\n * Net score = upvotes - downvotes. CAN BE NEGATIVE when downvotes\n * exceed upvotes. The card's middle number reads this directly.\n */\n score: z.number().int(),\n});\nexport type SetVoteResponse = z.infer<typeof setVoteResponseSchema>;\n\n// ─── Reaction ───────────────────────────────────────────────────────────────\n\n/**\n * Body for `POST /api/v1/community/:contentKind/:id/reactions`.\n *\n * Setting `reaction` to a value replaces the viewer's current\n * reaction (at most ONE active reaction per target). Setting\n * `reaction` to null clears it.\n */\nexport const setReactionBodySchema = z.object({\n reaction: reactionTypeSchema.nullable(),\n});\nexport type SetReactionBody = z.infer<typeof setReactionBodySchema>;\n\nexport const setReactionResponseSchema = z.object({\n contentKind: contentKindSchema,\n contentId: z.string(),\n /** Viewer's new reaction, or null when cleared. */\n reaction: reactionTypeSchema.nullable(),\n /** Post-set aggregate counts, keyed by reaction type. */\n reactionCounts: z.record(z.string(), z.number().int().nonnegative()),\n});\nexport type SetReactionResponse = z.infer<typeof setReactionResponseSchema>;\n\n// ─── Bookmark ───────────────────────────────────────────────────────────────\n\n/**\n * Response from `POST /api/v1/community/:contentKind/:id/bookmark`.\n *\n * The endpoint is a toggle — it flips the viewer's bookmark state on\n * the target. Returns the new state so the card can render directly\n * off the response. Bookmarks are PRIVATE to the viewer; they don't\n * affect any aggregate count surfaced to other viewers.\n */\nexport const toggleBookmarkResponseSchema = z.object({\n contentKind: contentKindSchema,\n contentId: z.string(),\n /** Viewer's bookmark state AFTER this call. */\n bookmarked: z.boolean(),\n});\nexport type ToggleBookmarkResponse = z.infer<typeof toggleBookmarkResponseSchema>;\n"]}