@openrails/contentkit-ui 0.68.0 → 0.69.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.
Files changed (91) hide show
  1. package/README.md +55 -10
  2. package/dist/client/client.d.ts +4 -2
  3. package/dist/client/client.js +1 -0
  4. package/dist/client/client.js.map +1 -1
  5. package/dist/client/content/bans.js +7 -7
  6. package/dist/client/content/bans.js.map +1 -1
  7. package/dist/client/content/comments.js +16 -14
  8. package/dist/client/content/comments.js.map +1 -1
  9. package/dist/client/content/favorites.js +5 -5
  10. package/dist/client/content/favorites.js.map +1 -1
  11. package/dist/client/content/moderation.js +5 -5
  12. package/dist/client/content/moderation.js.map +1 -1
  13. package/dist/client/content/polls.js +34 -31
  14. package/dist/client/content/polls.js.map +1 -1
  15. package/dist/client/content/posts.js +21 -20
  16. package/dist/client/content/posts.js.map +1 -1
  17. package/dist/client/content/reactions.js +4 -4
  18. package/dist/client/content/reactions.js.map +1 -1
  19. package/dist/client/generated/error-codes.d.ts +1 -1
  20. package/dist/client/generated/error-codes.js +4 -0
  21. package/dist/client/generated/error-codes.js.map +1 -1
  22. package/dist/client/generated/routes.js.map +1 -1
  23. package/dist/client/http.d.ts +6 -2
  24. package/dist/client/http.js +11 -3
  25. package/dist/client/http.js.map +1 -1
  26. package/dist/client/media/client.js +24 -24
  27. package/dist/client/media/client.js.map +1 -1
  28. package/dist/client/media/rules.d.ts +2 -0
  29. package/dist/client/media/rules.js +6 -2
  30. package/dist/client/media/rules.js.map +1 -1
  31. package/dist/client/taxonomy.js +2 -2
  32. package/dist/client/taxonomy.js.map +1 -1
  33. package/dist/components/media-folder-editor.d.ts +17 -3
  34. package/dist/components/media-folder-editor.js +146 -99
  35. package/dist/components/media-folder-editor.js.map +1 -1
  36. package/dist/components/slot-upload.js +102 -100
  37. package/dist/components/slot-upload.js.map +1 -1
  38. package/dist/locales/de.js +1 -0
  39. package/dist/locales/de.js.map +1 -1
  40. package/dist/locales/en.d.ts +1 -0
  41. package/dist/locales/en.js +1 -0
  42. package/dist/locales/en.js.map +1 -1
  43. package/dist/locales/es.js +1 -0
  44. package/dist/locales/es.js.map +1 -1
  45. package/dist/locales/ja.js +1 -0
  46. package/dist/locales/ja.js.map +1 -1
  47. package/dist/locales/ko.js +1 -0
  48. package/dist/locales/ko.js.map +1 -1
  49. package/dist/locales/zh.js +1 -0
  50. package/dist/locales/zh.js.map +1 -1
  51. package/dist/react/canonical.d.ts +2 -2
  52. package/dist/react/canonical.js +2 -12
  53. package/dist/react/canonical.js.map +1 -1
  54. package/dist/react/comments.js +35 -27
  55. package/dist/react/comments.js.map +1 -1
  56. package/dist/react/context.d.ts +7 -0
  57. package/dist/react/context.js +20 -15
  58. package/dist/react/context.js.map +1 -1
  59. package/dist/react/engagement.js +2 -2
  60. package/dist/react/engagement.js.map +1 -1
  61. package/dist/react/folder.d.ts +29 -9
  62. package/dist/react/folder.js +170 -113
  63. package/dist/react/folder.js.map +1 -1
  64. package/dist/react/polls.js +3 -3
  65. package/dist/react/polls.js.map +1 -1
  66. package/dist/react/posts.js +2 -2
  67. package/dist/react/posts.js.map +1 -1
  68. package/dist/react/provider.d.ts +7 -3
  69. package/dist/react/provider.js +34 -20
  70. package/dist/react/provider.js.map +1 -1
  71. package/dist/react/public.js +11 -11
  72. package/dist/react/public.js.map +1 -1
  73. package/dist/react/read.js +93 -92
  74. package/dist/react/read.js.map +1 -1
  75. package/dist/react/resources.d.ts +6 -3
  76. package/dist/react/resources.js +40 -40
  77. package/dist/react/resources.js.map +1 -1
  78. package/dist/react/slot.d.ts +4 -2
  79. package/dist/react/slot.js +96 -92
  80. package/dist/react/slot.js.map +1 -1
  81. package/dist/react/store.d.ts +1 -1
  82. package/dist/react/store.js +21 -20
  83. package/dist/react/store.js.map +1 -1
  84. package/dist/react/upload.d.ts +3 -2
  85. package/dist/react/upload.js +20 -15
  86. package/dist/react/upload.js.map +1 -1
  87. package/dist/react/use-resource.d.ts +1 -0
  88. package/dist/react/use-resource.js +38 -30
  89. package/dist/react/use-resource.js.map +1 -1
  90. package/dist/react.js +15 -15
  91. package/package.json +7 -5
package/README.md CHANGED
@@ -41,6 +41,8 @@ const urls = createContentURLs({ routes: { video: "watch", gallery: "g" }, langu
41
41
  <ContentKitProvider
42
42
  client={contentkit}
43
43
  viewer={user?.id ?? null} // the signed-in user as ContentKit sees it
44
+ language={lang} // same value as the client's language callback
45
+ accessRevision={accessRevision} // increment when permissions/entitlements change
44
46
  onSignIn={() => openSignIn()} // asks signed-out visitors to sign in where the server refuses them
45
47
  urls={urls}
46
48
  navigate={(to, o) => navigate(to, o)}
@@ -53,6 +55,22 @@ const urls = createContentURLs({ routes: { video: "watch", gallery: "g" }, langu
53
55
  </ContentKitProvider>;
54
56
  ```
55
57
 
58
+ Keep `viewer`, `language` and `accessRevision` current on the provider. A change
59
+ starts fresh media and social reads without remounting the children. Old requests
60
+ are abandoned when their last reader leaves; their results cannot fill the new
61
+ scope. `accessRevision` is optional when the host has no changing access state.
62
+ It is a cache signal, not an authorization decision. Without a provider, replace
63
+ the client when its session or request language changes. Host-supplied `read` and
64
+ `initial` data must also belong to the current viewer and language.
65
+
66
+ Writes retain the cache scope they started in. After a scope change, a late
67
+ notification refetches affected reads instead of copying its response into them;
68
+ `onChange` still runs. Use a separate client for each independent session or
69
+ request-language context, not one client across differently scoped providers.
70
+ The framework-free client exposes `setReadScope(key)` and passes that key as
71
+ the second `subscribe` / `onChange` argument, or `null` when the scope changed during the
72
+ operation. This does not change the operation's result or its authentication.
73
+
56
74
  - **Mounts.** Without `mounts`, modules live under `baseUrl`: content at the
57
75
  root, `/media` (reads, HLS), `/media/upload`, `/codes`, `/taxonomy`.
58
76
  - **`client.media`**: `upload`, `put` (upload, commit, wait), `commit`, `read`,
@@ -77,7 +95,7 @@ const urls = createContentURLs({ routes: { video: "watch", gallery: "g" }, langu
77
95
  unpublished post's images to its editors (below).
78
96
  - **`client.config()`** says what the content module allows: which
79
97
  interactions signed-out visitors may make (`content.Options.Anonymous`,
80
- none by default) and the longest comment (`CommentMaxLength`, 400 by
98
+ none by default) and the longest comment (`CommentMaxLength`, 2,200 by
81
99
  default; longer is `comment_too_long` with `details.max`).
82
100
  - **`client.subscribe(listener)`** receives every successful mutation
83
101
  (`media.committed`, `media.processed`, `comment.created`, `reaction.changed`,
@@ -165,7 +183,7 @@ loaded. `refresh()` joins a read in flight (a player's grant refresh);
165
183
 
166
184
  ```tsx
167
185
  const folder = useMediaFolder(ref, { paths: ["images/{name}", "videos/{name}"], commit: "manual" });
168
- folder.add(files); // screened, queued; returns the refusals
186
+ folder.add(files); // screened and queued; refusals in folder.refused
169
187
  await folder.commit(); // the uploaded files, in queue order
170
188
  await folder.move(path, 0); await folder.rename(path, "cover");
171
189
  await folder.edit(path, edit); await folder.replace(path, file); await folder.remove([path]);
@@ -176,7 +194,13 @@ An item's folder for its editors: the editor read (all windows), an upload
176
194
  queue, and the kind's upload rules from the editor read, which screen type,
177
195
  size and file caps before anything uploads and name files around names
178
196
  taken. `groups` lists each path's uploads; `commit: "auto"` commits each
179
- upload once those before it have (a draft). Failures go to the provider's
197
+ upload once those before it have (a draft). Files added before the editor
198
+ read arrives wait in `waiting` and are screened when its rules do (or
199
+ refused if the read fails or states no rules). `ref` may be null until the item exists,
200
+ so a composer can create its draft on the first drop. `busy` is true while
201
+ work is in flight (files waiting, uploads queued or running, a commit or
202
+ update); `fileCount` counts the uploads and the files on their way in. A
203
+ committed file leaves the queue once the read lists it. Failures go to the provider's
180
204
  `onError` with an operation (`upload`, `folder.commit`, `folder.update`,
181
205
  `folder.process` for uploads the worker fails after the editor opened) and
182
206
  the file's name.
@@ -216,7 +240,8 @@ Once a content page's link arrives, replaces the address with its canonical
216
240
  path (code spelling, merged code, current slug) through the provider's
217
241
  `navigate`, else `history.replaceState`, and keeps `<link rel=canonical>`,
218
242
  `og:url`, `og:title`, `og:image` and hreflang alternates (each language's
219
- own slug, plus `x-default`) in the head while mounted. Needs the provider's
243
+ own slug, plus `x-default`) in the head while mounted. Unmounting removes
244
+ them, server-rendered ones included, so the next page sets its own. Needs the provider's
220
245
  `urls`; `/urls` also exports `hreflang()` and `canonicalURL()` (drops
221
246
  tracking parameters) for server rendering.
222
247
 
@@ -238,7 +263,10 @@ save and re-crop flow inside a host layout); `SlotImage`; `ImageCropDialog`;
238
263
  `MediaGallery`, `VideoPlayer` and `VideoMiniPlayer` (HLS with ABR, inline
239
264
  muted previews, a grant refresh before `expires`, and a reason, Retry and
240
265
  support code for every failure; see below). A `PublicPreset` (`{ preset, aspect, renditions }`)
241
- is what a slot or poster shows; a read's `public` lists `PublicImage`s.
266
+ is what a slot or poster shows; a read's `public` lists `PublicImage`s. A
267
+ slot crops at its image's aspect, else its public preset's
268
+ (`GET /media/presets`), so an item without an image yet still crops a 3:1
269
+ cover at 3:1.
242
270
 
243
271
  ### MediaFolderEditor
244
272
 
@@ -246,6 +274,11 @@ is what a slot or poster shows; a read's `public` lists `PublicImage`s.
246
274
  <MediaFolderEditor item={post} paths={["images/{name}", "videos/{name}"]} commit="auto" ref={handle}
247
275
  toolbar={<ZipImport />} rowActions={(f) => <SetCover file={f} />} footer={note} />
248
276
  handle.current.discard();
277
+
278
+ // Or the host owns the folder and renders with its state:
279
+ const folder = useMediaFolder(draft, { paths, commit: "auto" }); // draft: null until created
280
+ <MediaFolderEditor folder={folder} />
281
+ <Button disabled={folder.busy || (!title && !text && !folder.fileCount)}>Publish</Button>
249
282
  ```
250
283
 
251
284
  `useMediaFolder` with its UI: a drop zone stating the rules, a queue with
@@ -411,13 +444,25 @@ code has a message in every bundle (`useMessages().error(e)`).
411
444
  ## Development
412
445
 
413
446
  ```sh
414
- pnpm check # typecheck, lint, unit + jsdom tests, build
415
- CONTENTKIT_TEST_URL=… CONTENTKIT_TEST_S3_ENDPOINT=… CONTENTKIT_TEST_S3_ACCESS_KEY=… CONTENTKIT_TEST_S3_SECRET_KEY=… \
416
- pnpm test:integration # the real handlers (contentkit.Runtime.Handler) over PostgreSQL and MinIO
417
- pnpm build && pnpm screenshots # demo/ in Chromium, light/dark × desktop/mobile
418
- # (social.html needs the integration variables)
447
+ pnpm check # typecheck, lint, unit tests (pure math and client-free components), build
448
+ pnpm build
449
+ pnpm test:e2e # the integration project and the browser specs against a real ContentKit
450
+ pnpm test:integration # or either alone
451
+ pnpm test:browser # (demo/ in Chromium, light/dark × desktop/mobile screenshots)
452
+ pnpm demo # the stack with seeded demo pages, until Ctrl-C
419
453
  ```
420
454
 
455
+ The suites need Docker, Go and ffmpeg. `e2e/server` composes the real
456
+ `contentkit.Runtime` (content, taxonomy, codes, media upload and read) with
457
+ AuthKit for identity over PGroonga PostgreSQL 18, MinIO, and media-worker and
458
+ media-gateway built from this checkout (`e2e/compose.yaml`). Each run brings
459
+ the stack up under its own project name and takes it down; the server exits
460
+ with its runner. Pages sign in with auth-ui and pass `auth.authFetch`. Tests
461
+ set up through `/__test`: accounts (`staff`, `moderator`, `editor` roles),
462
+ items the host's resolver knows, stored-object checks, and faults (no CORS,
463
+ 429 and expired tokens at the gateway, dropped or held bucket requests, API
464
+ error replies).
465
+
421
466
  The wire types, route table and error codes in `src/client/generated/` are
422
467
  generated from ContentKit's route catalog (`pnpm contract`; CI runs
423
468
  `pnpm contract:check`). UI primitives come from
@@ -45,7 +45,9 @@ export interface ContentKitClient {
45
45
  config(signal?: AbortSignal): Promise<Config>;
46
46
  /** A module's base URL as configured (no trailing slash). */
47
47
  url(module: ContentKitModule, ref?: RefBody): string;
48
- /** Every successful mutation made through this client; returns the unsubscribe. */
49
- subscribe(listener: (change: ContentKitChange) => void): () => void;
48
+ /** Cache context only, not authentication. The React provider keeps this in sync. */
49
+ setReadScope(scope: string): void;
50
+ /** Every successful mutation; a null scope means its data predates a context change. */
51
+ subscribe(listener: (change: ContentKitChange, scope: string | null) => void): () => void;
50
52
  }
51
53
  export declare function createContentKitClient(options: ContentKitClientOptions): ContentKitClient;
@@ -26,6 +26,7 @@ function f(f) {
26
26
  codes: new n(g),
27
27
  config: (t) => e(g, "GET", "/config", { signal: t }),
28
28
  url: (e, t) => g.mount(e, t),
29
+ setReadScope: (e) => g.setReadScope(e),
29
30
  subscribe: (e) => g.subscribe(e)
30
31
  };
31
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import { CodesClient } from \"./codes.js\";\nimport { BansClient } from \"./content/bans.js\";\nimport { CommentsClient } from \"./content/comments.js\";\nimport { FavoritesClient } from \"./content/favorites.js\";\nimport { ModerationClient } from \"./content/moderation.js\";\nimport { PollsClient } from \"./content/polls.js\";\nimport { PostsClient } from \"./content/posts.js\";\nimport { ReactionsClient } from \"./content/reactions.js\";\nimport type { Config, RefBody } from \"./generated/wire.js\";\nimport { Http, type ContentKitChange, type ContentKitModule, type HttpOptions } from \"./http.js\";\nimport { MediaClient, type ContentFolders, type MediaOptions } from \"./media/client.js\";\nimport { call } from \"./route.js\";\nimport { TaxonomyClient } from \"./taxonomy.js\";\n\nexport interface ContentKitClientOptions extends HttpOptions {\n /** Upload tuning (transport, part concurrency, retries). */\n media?: MediaOptions;\n /** The media kinds of post and poll folders, when the host renamed them (content.Media). */\n folders?: ContentFolders;\n}\n\n/**\n * One ContentKit deployment as the browser sees it. Each module is a property\n * over one shared transport (mounts, auth, language, errors, change events).\n */\nexport interface ContentKitClient {\n /** Uploads, multipart, commits, reads, frames, HLS bases, hls.js `xhrSetup` and inline images. */\n readonly media: MediaClient;\n /** Published posts, staff CRUD, reactions, cover and body images. */\n readonly posts: PostsClient;\n /** Threads, replies, edits, tombstones, reactions, standing, the latest feed, moderation. */\n readonly comments: CommentsClient;\n /** Likes and dislikes of an item. */\n readonly reactions: ReactionsClient;\n /** The signed-in caller's favorites. */\n readonly favorites: FavoritesClient;\n /** Polls: votes, free-text answers and the staff editor. */\n readonly polls: PollsClient;\n /** Comment bans, owner and global. */\n readonly bans: BansClient;\n /** The review queue of held comments and posts. */\n readonly moderation: ModerationClient;\n /** The taxonomy admin API. */\n readonly taxonomy: TaxonomyClient;\n /** Content-code lookup. */\n readonly codes: CodesClient;\n\n /** What the content module allows: which interactions signed-out visitors may make. */\n config(signal?: AbortSignal): Promise<Config>;\n /** A module's base URL as configured (no trailing slash). */\n url(module: ContentKitModule, ref?: RefBody): string;\n /** Every successful mutation made through this client; returns the unsubscribe. */\n subscribe(listener: (change: ContentKitChange) => void): () => void;\n}\n\nexport function createContentKitClient(options: ContentKitClientOptions): ContentKitClient {\n const { media: mediaOptions, folders, ...http } = options;\n const h = new Http(http);\n const media = new MediaClient(h, mediaOptions, folders);\n return {\n media,\n posts: new PostsClient(h, media),\n comments: new CommentsClient(h),\n reactions: new ReactionsClient(h),\n favorites: new FavoritesClient(h),\n polls: new PollsClient(h, media),\n bans: new BansClient(h),\n moderation: new ModerationClient(h),\n taxonomy: new TaxonomyClient(h),\n codes: new CodesClient(h),\n config: (signal) => call(h, \"GET\", \"/config\", { signal }),\n url: (module, ref) => h.mount(module, ref),\n subscribe: (listener) => h.subscribe(listener),\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAuDA,SAAgB,EAAuB,GAAoD;CACzF,IAAM,EAAE,OAAO,GAAc,YAAS,GAAG,MAAS,GAC5C,IAAI,IAAI,EAAK,CAAI,GACjB,IAAQ,IAAI,EAAY,GAAG,GAAc,CAAO;CACtD,OAAO;EACL;EACA,OAAO,IAAI,EAAY,GAAG,CAAK;EAC/B,UAAU,IAAI,EAAe,CAAC;EAC9B,WAAW,IAAI,EAAgB,CAAC;EAChC,WAAW,IAAI,EAAgB,CAAC;EAChC,OAAO,IAAI,EAAY,GAAG,CAAK;EAC/B,MAAM,IAAI,EAAW,CAAC;EACtB,YAAY,IAAI,EAAiB,CAAC;EAClC,UAAU,IAAI,EAAe,CAAC;EAC9B,OAAO,IAAI,EAAY,CAAC;EACxB,SAAS,MAAW,EAAK,GAAG,OAAO,WAAW,EAAE,UAAO,CAAC;EACxD,MAAM,GAAQ,MAAQ,EAAE,MAAM,GAAQ,CAAG;EACzC,YAAY,MAAa,EAAE,UAAU,CAAQ;CAC/C;AACF"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import { CodesClient } from \"./codes.js\";\nimport { BansClient } from \"./content/bans.js\";\nimport { CommentsClient } from \"./content/comments.js\";\nimport { FavoritesClient } from \"./content/favorites.js\";\nimport { ModerationClient } from \"./content/moderation.js\";\nimport { PollsClient } from \"./content/polls.js\";\nimport { PostsClient } from \"./content/posts.js\";\nimport { ReactionsClient } from \"./content/reactions.js\";\nimport type { Config, RefBody } from \"./generated/wire.js\";\nimport { Http, type ContentKitChange, type ContentKitModule, type HttpOptions } from \"./http.js\";\nimport { MediaClient, type ContentFolders, type MediaOptions } from \"./media/client.js\";\nimport { call } from \"./route.js\";\nimport { TaxonomyClient } from \"./taxonomy.js\";\n\nexport interface ContentKitClientOptions extends HttpOptions {\n /** Upload tuning (transport, part concurrency, retries). */\n media?: MediaOptions;\n /** The media kinds of post and poll folders, when the host renamed them (content.Media). */\n folders?: ContentFolders;\n}\n\n/**\n * One ContentKit deployment as the browser sees it. Each module is a property\n * over one shared transport (mounts, auth, language, errors, change events).\n */\nexport interface ContentKitClient {\n /** Uploads, multipart, commits, reads, frames, HLS bases, hls.js `xhrSetup` and inline images. */\n readonly media: MediaClient;\n /** Published posts, staff CRUD, reactions, cover and body images. */\n readonly posts: PostsClient;\n /** Threads, replies, edits, tombstones, reactions, standing, the latest feed, moderation. */\n readonly comments: CommentsClient;\n /** Likes and dislikes of an item. */\n readonly reactions: ReactionsClient;\n /** The signed-in caller's favorites. */\n readonly favorites: FavoritesClient;\n /** Polls: votes, free-text answers and the staff editor. */\n readonly polls: PollsClient;\n /** Comment bans, owner and global. */\n readonly bans: BansClient;\n /** The review queue of held comments and posts. */\n readonly moderation: ModerationClient;\n /** The taxonomy admin API. */\n readonly taxonomy: TaxonomyClient;\n /** Content-code lookup. */\n readonly codes: CodesClient;\n\n /** What the content module allows: which interactions signed-out visitors may make. */\n config(signal?: AbortSignal): Promise<Config>;\n /** A module's base URL as configured (no trailing slash). */\n url(module: ContentKitModule, ref?: RefBody): string;\n /** Cache context only, not authentication. The React provider keeps this in sync. */\n setReadScope(scope: string): void;\n /** Every successful mutation; a null scope means its data predates a context change. */\n subscribe(listener: (change: ContentKitChange, scope: string | null) => void): () => void;\n}\n\nexport function createContentKitClient(options: ContentKitClientOptions): ContentKitClient {\n const { media: mediaOptions, folders, ...http } = options;\n const h = new Http(http);\n const media = new MediaClient(h, mediaOptions, folders);\n return {\n media,\n posts: new PostsClient(h, media),\n comments: new CommentsClient(h),\n reactions: new ReactionsClient(h),\n favorites: new FavoritesClient(h),\n polls: new PollsClient(h, media),\n bans: new BansClient(h),\n moderation: new ModerationClient(h),\n taxonomy: new TaxonomyClient(h),\n codes: new CodesClient(h),\n config: (signal) => call(h, \"GET\", \"/config\", { signal }),\n url: (module, ref) => h.mount(module, ref),\n setReadScope: (scope) => h.setReadScope(scope),\n subscribe: (listener) => h.subscribe(listener),\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAyDA,SAAgB,EAAuB,GAAoD;CACzF,IAAM,EAAE,OAAO,GAAc,YAAS,GAAG,MAAS,GAC5C,IAAI,IAAI,EAAK,CAAI,GACjB,IAAQ,IAAI,EAAY,GAAG,GAAc,CAAO;CACtD,OAAO;EACL;EACA,OAAO,IAAI,EAAY,GAAG,CAAK;EAC/B,UAAU,IAAI,EAAe,CAAC;EAC9B,WAAW,IAAI,EAAgB,CAAC;EAChC,WAAW,IAAI,EAAgB,CAAC;EAChC,OAAO,IAAI,EAAY,GAAG,CAAK;EAC/B,MAAM,IAAI,EAAW,CAAC;EACtB,YAAY,IAAI,EAAiB,CAAC;EAClC,UAAU,IAAI,EAAe,CAAC;EAC9B,OAAO,IAAI,EAAY,CAAC;EACxB,SAAS,MAAW,EAAK,GAAG,OAAO,WAAW,EAAE,UAAO,CAAC;EACxD,MAAM,GAAQ,MAAQ,EAAE,MAAM,GAAQ,CAAG;EACzC,eAAe,MAAU,EAAE,aAAa,CAAK;EAC7C,YAAY,MAAa,EAAE,UAAU,CAAQ;CAC/C;AACF"}
@@ -15,19 +15,19 @@ var t = class {
15
15
  });
16
16
  }
17
17
  async ban(t, n, r = {}) {
18
- let i = {
18
+ let i = this.http.captureChanges(), a = {
19
19
  params: { user: n },
20
20
  body: r
21
- }, a = await (t === "global" ? e(this.http, "PUT", "/global-comment-bans/{user}", i) : e(this.http, "PUT", "/comment-bans/{user}", i));
22
- return this.http.emit({
21
+ }, o = await (t === "global" ? e(this.http, "PUT", "/global-comment-bans/{user}", a) : e(this.http, "PUT", "/comment-bans/{user}", a));
22
+ return i({
23
23
  type: "ban.saved",
24
24
  scope: t,
25
- ban: a
26
- }), a;
25
+ ban: o
26
+ }), o;
27
27
  }
28
28
  async lift(t, n) {
29
- let r = { params: { user: n } };
30
- await (t === "global" ? e(this.http, "DELETE", "/global-comment-bans/{user}", r) : e(this.http, "DELETE", "/comment-bans/{user}", r)), this.http.emit({
29
+ let r = this.http.captureChanges(), i = { params: { user: n } };
30
+ await (t === "global" ? e(this.http, "DELETE", "/global-comment-bans/{user}", i) : e(this.http, "DELETE", "/comment-bans/{user}", i)), r({
31
31
  type: "ban.lifted",
32
32
  scope: t,
33
33
  user: n
@@ -1 +1 @@
1
- {"version":3,"file":"bans.js","names":[],"sources":["../../../src/client/content/bans.ts"],"sourcesContent":["import type { BanInput, CommentBan } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call, type PageQuery } from \"../route.js\";\nimport type { BanScope } from \"./types.js\";\n\n/** Comment bans: \"owner\" on the caller's own content, \"global\" across the site (CommentBan). */\nexport class BansClient {\n constructor(private readonly http: Http) {}\n\n /** The scope's bans, newest first, expired ones included. */\n list(scope: BanScope, q: PageQuery = {}, signal?: AbortSignal): Promise<CommentBan[]> {\n return scope === \"global\"\n ? call(this.http, \"GET\", \"/global-comment-bans\", { query: q, signal })\n : call(this.http, \"GET\", \"/comment-bans\", { query: q, signal });\n }\n /** Bans user from commenting in the scope, replacing any ban there; no until lasts until lifted. */\n async ban(scope: BanScope, user: string, input: BanInput = {}): Promise<CommentBan> {\n const o = { params: { user }, body: input };\n const ban: CommentBan = await (scope === \"global\"\n ? call(this.http, \"PUT\", \"/global-comment-bans/{user}\", o)\n : call(this.http, \"PUT\", \"/comment-bans/{user}\", o));\n this.http.emit({ type: \"ban.saved\", scope, ban });\n return ban;\n }\n /** Lifts user's ban in the scope; idempotent. */\n async lift(scope: BanScope, user: string): Promise<void> {\n const o = { params: { user } };\n await (scope === \"global\" ? call(this.http, \"DELETE\", \"/global-comment-bans/{user}\", o) : call(this.http, \"DELETE\", \"/comment-bans/{user}\", o));\n this.http.emit({ type: \"ban.lifted\", scope, user });\n }\n}\n"],"mappings":";;AAMA,IAAa,IAAb,MAAwB;CACO;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,KAAK,GAAiB,IAAe,CAAC,GAAG,GAA6C;EACpF,OAAO,MAAU,WACb,EAAK,KAAK,MAAM,OAAO,wBAAwB;GAAE,OAAO;GAAG;EAAO,CAAC,IACnE,EAAK,KAAK,MAAM,OAAO,iBAAiB;GAAE,OAAO;GAAG;EAAO,CAAC;CAClE;CAEA,MAAM,IAAI,GAAiB,GAAc,IAAkB,CAAC,GAAwB;EAClF,IAAM,IAAI;GAAE,QAAQ,EAAE,QAAK;GAAG,MAAM;EAAM,GACpC,IAAkB,OAAO,MAAU,WACrC,EAAK,KAAK,MAAM,OAAO,+BAA+B,CAAC,IACvD,EAAK,KAAK,MAAM,OAAO,wBAAwB,CAAC;EAEpD,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAa;GAAO;EAAI,CAAC,GACzC;CACT;CAEA,MAAM,KAAK,GAAiB,GAA6B;EACvD,IAAM,IAAI,EAAE,QAAQ,EAAE,QAAK,EAAE;EAE7B,AADA,OAAO,MAAU,WAAW,EAAK,KAAK,MAAM,UAAU,+BAA+B,CAAC,IAAI,EAAK,KAAK,MAAM,UAAU,wBAAwB,CAAC,IAC7I,KAAK,KAAK,KAAK;GAAE,MAAM;GAAc;GAAO;EAAK,CAAC;CACpD;AACF"}
1
+ {"version":3,"file":"bans.js","names":[],"sources":["../../../src/client/content/bans.ts"],"sourcesContent":["import type { BanInput, CommentBan } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call, type PageQuery } from \"../route.js\";\nimport type { BanScope } from \"./types.js\";\n\n/** Comment bans: \"owner\" on the caller's own content, \"global\" across the site (CommentBan). */\nexport class BansClient {\n constructor(private readonly http: Http) {}\n\n /** The scope's bans, newest first, expired ones included. */\n list(scope: BanScope, q: PageQuery = {}, signal?: AbortSignal): Promise<CommentBan[]> {\n return scope === \"global\"\n ? call(this.http, \"GET\", \"/global-comment-bans\", { query: q, signal })\n : call(this.http, \"GET\", \"/comment-bans\", { query: q, signal });\n }\n /** Bans user from commenting in the scope, replacing any ban there; no until lasts until lifted. */\n async ban(scope: BanScope, user: string, input: BanInput = {}): Promise<CommentBan> {\n const emit = this.http.captureChanges();\n const o = { params: { user }, body: input };\n const ban: CommentBan = await (scope === \"global\"\n ? call(this.http, \"PUT\", \"/global-comment-bans/{user}\", o)\n : call(this.http, \"PUT\", \"/comment-bans/{user}\", o));\n emit({ type: \"ban.saved\", scope, ban });\n return ban;\n }\n /** Lifts user's ban in the scope; idempotent. */\n async lift(scope: BanScope, user: string): Promise<void> {\n const emit = this.http.captureChanges();\n const o = { params: { user } };\n await (scope === \"global\" ? call(this.http, \"DELETE\", \"/global-comment-bans/{user}\", o) : call(this.http, \"DELETE\", \"/comment-bans/{user}\", o));\n emit({ type: \"ban.lifted\", scope, user });\n }\n}\n"],"mappings":";;AAMA,IAAa,IAAb,MAAwB;CACO;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,KAAK,GAAiB,IAAe,CAAC,GAAG,GAA6C;EACpF,OAAO,MAAU,WACb,EAAK,KAAK,MAAM,OAAO,wBAAwB;GAAE,OAAO;GAAG;EAAO,CAAC,IACnE,EAAK,KAAK,MAAM,OAAO,iBAAiB;GAAE,OAAO;GAAG;EAAO,CAAC;CAClE;CAEA,MAAM,IAAI,GAAiB,GAAc,IAAkB,CAAC,GAAwB;EAClF,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAI;GAAE,QAAQ,EAAE,QAAK;GAAG,MAAM;EAAM,GACpC,IAAkB,OAAO,MAAU,WACrC,EAAK,KAAK,MAAM,OAAO,+BAA+B,CAAC,IACvD,EAAK,KAAK,MAAM,OAAO,wBAAwB,CAAC;EAEpD,OADA,EAAK;GAAE,MAAM;GAAa;GAAO;EAAI,CAAC,GAC/B;CACT;CAEA,MAAM,KAAK,GAAiB,GAA6B;EACvD,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAI,EAAE,QAAQ,EAAE,QAAK,EAAE;EAE7B,AADA,OAAO,MAAU,WAAW,EAAK,KAAK,MAAM,UAAU,+BAA+B,CAAC,IAAI,EAAK,KAAK,MAAM,UAAU,wBAAwB,CAAC,IAC7I,EAAK;GAAE,MAAM;GAAc;GAAO;EAAK,CAAC;CAC1C;AACF"}
@@ -24,39 +24,40 @@ var n = (e) => ({
24
24
  });
25
25
  }
26
26
  async create(t, r) {
27
- let i = await e(this.http, "POST", "/{kind}/{id}/comments", {
27
+ let i = this.http.captureChanges(), a = await e(this.http, "POST", "/{kind}/{id}/comments", {
28
28
  params: n(t),
29
29
  body: r
30
30
  });
31
- return this.http.emit({
31
+ return i({
32
32
  type: "comment.created",
33
33
  ref: n(t),
34
- comment: i
35
- }), i;
34
+ comment: a
35
+ }), a;
36
36
  }
37
37
  async edit(t, n) {
38
- let r = await e(this.http, "PATCH", "/comments/{cid}", {
38
+ let r = this.http.captureChanges(), i = await e(this.http, "PATCH", "/comments/{cid}", {
39
39
  params: { cid: t },
40
40
  body: { body: n }
41
41
  });
42
- return this.http.emit({
42
+ return r({
43
43
  type: "comment.updated",
44
- comment: r
45
- }), r;
44
+ comment: i
45
+ }), i;
46
46
  }
47
47
  async delete(t) {
48
- await e(this.http, "DELETE", "/comments/{cid}", { params: { cid: t } }), this.http.emit({
48
+ let n = this.http.captureChanges();
49
+ await e(this.http, "DELETE", "/comments/{cid}", { params: { cid: t } }), n({
49
50
  type: "comment.deleted",
50
51
  id: t
51
52
  });
52
53
  }
53
54
  async react(n, r) {
54
- let i = await e(this.http, "POST", `/comments/{cid}/${t(r)}`, { params: { cid: n } });
55
- return this.http.emit({
55
+ let i = this.http.captureChanges(), a = await e(this.http, "POST", `/comments/{cid}/${t(r)}`, { params: { cid: n } });
56
+ return i({
56
57
  type: "comment.reacted",
57
58
  id: n,
58
- counts: i
59
- }), i;
59
+ counts: a
60
+ }), a;
60
61
  }
61
62
  standing(t, r) {
62
63
  return e(this.http, "GET", "/{kind}/{id}/can-comment", {
@@ -81,7 +82,8 @@ var n = (e) => ({
81
82
  });
82
83
  }
83
84
  async restore(t) {
84
- await e(this.http, "POST", "/comments/{cid}/restore", { params: { cid: t } }), this.http.emit({
85
+ let n = this.http.captureChanges();
86
+ await e(this.http, "POST", "/comments/{cid}/restore", { params: { cid: t } }), n({
85
87
  type: "comment.restored",
86
88
  id: t
87
89
  });
@@ -1 +1 @@
1
- {"version":3,"file":"comments.js","names":[],"sources":["../../../src/client/content/comments.ts"],"sourcesContent":["import type { AdminComment, Comment, CommentInput, CommentStanding, FeedItem, ReactionCounts, RefBody } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call, type PageQuery } from \"../route.js\";\nimport { reactionVerb, type Reaction, type Sort } from \"./types.js\";\n\nexport interface CommentQuery extends PageQuery {\n sort?: Sort;\n}\n\nexport interface AdminCommentQuery extends PageQuery {\n /** Only comments on this content kind. */\n contentKind?: string;\n}\n\n// Content ids are the host's opaque text (not only media's UUIDv7s).\nconst target = (ref: RefBody) => ({ kind: ref.kind, id: ref.id });\n\n/** Comments on an item: threads, replies, edits, tombstones, reactions, standing, the latest feed and moderation. */\nexport class CommentsClient {\n constructor(private readonly http: Http) {}\n\n /** The item's top-level comments with reply counts; the caller also gets its own held and rejected ones. */\n list(ref: RefBody, q: CommentQuery = {}, signal?: AbortSignal): Promise<Comment[]> {\n return call(this.http, \"GET\", \"/{kind}/{id}/comments\", { params: target(ref), query: q, signal });\n }\n /** A top-level comment's replies, oldest first. */\n replies(id: string, q: PageQuery = {}, signal?: AbortSignal): Promise<Comment[]> {\n return call(this.http, \"GET\", \"/comments/{cid}/replies\", { params: { cid: id }, query: q, signal });\n }\n /** Comments on the item, or replies to a top-level comment (reply_to_id); anonymous callers give anon_name. */\n async create(ref: RefBody, input: CommentInput): Promise<Comment> {\n const comment: Comment = await call(this.http, \"POST\", \"/{kind}/{id}/comments\", { params: target(ref), body: input });\n this.http.emit({ type: \"comment.created\", ref: target(ref), comment });\n return comment;\n }\n /** Replaces the body: its author, or a moderator. */\n async edit(id: string, body: string): Promise<Comment> {\n const comment: Comment = await call(this.http, \"PATCH\", \"/comments/{cid}\", { params: { cid: id }, body: { body } });\n this.http.emit({ type: \"comment.updated\", comment });\n return comment;\n }\n /** Leaves a tombstone: its author, or a moderator. */\n async delete(id: string): Promise<void> {\n await call(this.http, \"DELETE\", \"/comments/{cid}\", { params: { cid: id } });\n this.http.emit({ type: \"comment.deleted\", id });\n }\n /** Sets the caller's reaction to a comment. */\n async react(id: string, value: Reaction): Promise<ReactionCounts> {\n const counts: ReactionCounts = await call(this.http, \"POST\", `/comments/{cid}/${reactionVerb(value)}` as const, { params: { cid: id } });\n this.http.emit({ type: \"comment.reacted\", id, counts });\n return counts;\n }\n /** Whether the caller may comment on the item, the ban that stops it, and what it may do to others' comments. */\n standing(ref: RefBody, signal?: AbortSignal): Promise<CommentStanding> {\n return call(this.http, \"GET\", \"/{kind}/{id}/can-comment\", { params: target(ref), signal });\n }\n /** The newest published comments across the site, with their items; a page may under-fill. */\n latest(q: PageQuery = {}, signal?: AbortSignal): Promise<FeedItem[]> {\n return call(this.http, \"GET\", \"/comments/latest\", { query: q, signal });\n }\n /** Every comment, newest first, deleted, held and rejected ones with their bodies (CommentModerate). */\n adminList(q: AdminCommentQuery = {}, signal?: AbortSignal): Promise<AdminComment[]> {\n const { contentKind, ...page } = q;\n return call(this.http, \"GET\", \"/comments/admin\", { query: { ...page, content_kind: contentKind }, signal });\n }\n /** Restores a deleted comment (CommentModerate). */\n async restore(id: string): Promise<void> {\n await call(this.http, \"POST\", \"/comments/{cid}/restore\", { params: { cid: id } });\n this.http.emit({ type: \"comment.restored\", id });\n }\n}\n"],"mappings":";;;AAeA,IAAM,KAAU,OAAkB;CAAE,MAAM,EAAI;CAAM,IAAI,EAAI;AAAG,IAGlD,IAAb,MAA4B;CACG;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,KAAK,GAAc,IAAkB,CAAC,GAAG,GAA0C;EACjF,OAAO,EAAK,KAAK,MAAM,OAAO,yBAAyB;GAAE,QAAQ,EAAO,CAAG;GAAG,OAAO;GAAG;EAAO,CAAC;CAClG;CAEA,QAAQ,GAAY,IAAe,CAAC,GAAG,GAA0C;EAC/E,OAAO,EAAK,KAAK,MAAM,OAAO,2BAA2B;GAAE,QAAQ,EAAE,KAAK,EAAG;GAAG,OAAO;GAAG;EAAO,CAAC;CACpG;CAEA,MAAM,OAAO,GAAc,GAAuC;EAChE,IAAM,IAAmB,MAAM,EAAK,KAAK,MAAM,QAAQ,yBAAyB;GAAE,QAAQ,EAAO,CAAG;GAAG,MAAM;EAAM,CAAC;EAEpH,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAmB,KAAK,EAAO,CAAG;GAAG;EAAQ,CAAC,GAC9D;CACT;CAEA,MAAM,KAAK,GAAY,GAAgC;EACrD,IAAM,IAAmB,MAAM,EAAK,KAAK,MAAM,SAAS,mBAAmB;GAAE,QAAQ,EAAE,KAAK,EAAG;GAAG,MAAM,EAAE,QAAK;EAAE,CAAC;EAElH,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAmB;EAAQ,CAAC,GAC5C;CACT;CAEA,MAAM,OAAO,GAA2B;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,UAAU,mBAAmB,EAAE,QAAQ,EAAE,KAAK,EAAG,EAAE,CAAC,GAC1E,KAAK,KAAK,KAAK;GAAE,MAAM;GAAmB;EAAG,CAAC;CAChD;CAEA,MAAM,MAAM,GAAY,GAA0C;EAChE,IAAM,IAAyB,MAAM,EAAK,KAAK,MAAM,QAAQ,mBAAmB,EAAa,CAAK,KAAc,EAAE,QAAQ,EAAE,KAAK,EAAG,EAAE,CAAC;EAEvI,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAmB;GAAI;EAAO,CAAC,GAC/C;CACT;CAEA,SAAS,GAAc,GAAgD;EACrE,OAAO,EAAK,KAAK,MAAM,OAAO,4BAA4B;GAAE,QAAQ,EAAO,CAAG;GAAG;EAAO,CAAC;CAC3F;CAEA,OAAO,IAAe,CAAC,GAAG,GAA2C;EACnE,OAAO,EAAK,KAAK,MAAM,OAAO,oBAAoB;GAAE,OAAO;GAAG;EAAO,CAAC;CACxE;CAEA,UAAU,IAAuB,CAAC,GAAG,GAA+C;EAClF,IAAM,EAAE,gBAAa,GAAG,MAAS;EACjC,OAAO,EAAK,KAAK,MAAM,OAAO,mBAAmB;GAAE,OAAO;IAAE,GAAG;IAAM,cAAc;GAAY;GAAG;EAAO,CAAC;CAC5G;CAEA,MAAM,QAAQ,GAA2B;EAEvC,AADA,MAAM,EAAK,KAAK,MAAM,QAAQ,2BAA2B,EAAE,QAAQ,EAAE,KAAK,EAAG,EAAE,CAAC,GAChF,KAAK,KAAK,KAAK;GAAE,MAAM;GAAoB;EAAG,CAAC;CACjD;AACF"}
1
+ {"version":3,"file":"comments.js","names":[],"sources":["../../../src/client/content/comments.ts"],"sourcesContent":["import type { AdminComment, Comment, CommentInput, CommentStanding, FeedItem, ReactionCounts, RefBody } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call, type PageQuery } from \"../route.js\";\nimport { reactionVerb, type Reaction, type Sort } from \"./types.js\";\n\nexport interface CommentQuery extends PageQuery {\n sort?: Sort;\n}\n\nexport interface AdminCommentQuery extends PageQuery {\n /** Only comments on this content kind. */\n contentKind?: string;\n}\n\n// Content ids are the host's opaque text (not only media's UUIDv7s).\nconst target = (ref: RefBody) => ({ kind: ref.kind, id: ref.id });\n\n/** Comments on an item: threads, replies, edits, tombstones, reactions, standing, the latest feed and moderation. */\nexport class CommentsClient {\n constructor(private readonly http: Http) {}\n\n /** The item's top-level comments with reply counts; the caller also gets its own held and rejected ones. */\n list(ref: RefBody, q: CommentQuery = {}, signal?: AbortSignal): Promise<Comment[]> {\n return call(this.http, \"GET\", \"/{kind}/{id}/comments\", { params: target(ref), query: q, signal });\n }\n /** A top-level comment's replies, oldest first. */\n replies(id: string, q: PageQuery = {}, signal?: AbortSignal): Promise<Comment[]> {\n return call(this.http, \"GET\", \"/comments/{cid}/replies\", { params: { cid: id }, query: q, signal });\n }\n /** Comments on the item, or replies to a top-level comment (reply_to_id); anonymous callers give anon_name. */\n async create(ref: RefBody, input: CommentInput): Promise<Comment> {\n const emit = this.http.captureChanges();\n const comment: Comment = await call(this.http, \"POST\", \"/{kind}/{id}/comments\", { params: target(ref), body: input });\n emit({ type: \"comment.created\", ref: target(ref), comment });\n return comment;\n }\n /** Replaces the body: its author, or a moderator. */\n async edit(id: string, body: string): Promise<Comment> {\n const emit = this.http.captureChanges();\n const comment: Comment = await call(this.http, \"PATCH\", \"/comments/{cid}\", { params: { cid: id }, body: { body } });\n emit({ type: \"comment.updated\", comment });\n return comment;\n }\n /** Leaves a tombstone: its author, or a moderator. */\n async delete(id: string): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/comments/{cid}\", { params: { cid: id } });\n emit({ type: \"comment.deleted\", id });\n }\n /** Sets the caller's reaction to a comment. */\n async react(id: string, value: Reaction): Promise<ReactionCounts> {\n const emit = this.http.captureChanges();\n const counts: ReactionCounts = await call(this.http, \"POST\", `/comments/{cid}/${reactionVerb(value)}` as const, { params: { cid: id } });\n emit({ type: \"comment.reacted\", id, counts });\n return counts;\n }\n /** Whether the caller may comment on the item, the ban that stops it, and what it may do to others' comments. */\n standing(ref: RefBody, signal?: AbortSignal): Promise<CommentStanding> {\n return call(this.http, \"GET\", \"/{kind}/{id}/can-comment\", { params: target(ref), signal });\n }\n /** The newest published comments across the site, with their items; a page may under-fill. */\n latest(q: PageQuery = {}, signal?: AbortSignal): Promise<FeedItem[]> {\n return call(this.http, \"GET\", \"/comments/latest\", { query: q, signal });\n }\n /** Every comment, newest first, deleted, held and rejected ones with their bodies (CommentModerate). */\n adminList(q: AdminCommentQuery = {}, signal?: AbortSignal): Promise<AdminComment[]> {\n const { contentKind, ...page } = q;\n return call(this.http, \"GET\", \"/comments/admin\", { query: { ...page, content_kind: contentKind }, signal });\n }\n /** Restores a deleted comment (CommentModerate). */\n async restore(id: string): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"POST\", \"/comments/{cid}/restore\", { params: { cid: id } });\n emit({ type: \"comment.restored\", id });\n }\n}\n"],"mappings":";;;AAeA,IAAM,KAAU,OAAkB;CAAE,MAAM,EAAI;CAAM,IAAI,EAAI;AAAG,IAGlD,IAAb,MAA4B;CACG;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,KAAK,GAAc,IAAkB,CAAC,GAAG,GAA0C;EACjF,OAAO,EAAK,KAAK,MAAM,OAAO,yBAAyB;GAAE,QAAQ,EAAO,CAAG;GAAG,OAAO;GAAG;EAAO,CAAC;CAClG;CAEA,QAAQ,GAAY,IAAe,CAAC,GAAG,GAA0C;EAC/E,OAAO,EAAK,KAAK,MAAM,OAAO,2BAA2B;GAAE,QAAQ,EAAE,KAAK,EAAG;GAAG,OAAO;GAAG;EAAO,CAAC;CACpG;CAEA,MAAM,OAAO,GAAc,GAAuC;EAChE,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAmB,MAAM,EAAK,KAAK,MAAM,QAAQ,yBAAyB;GAAE,QAAQ,EAAO,CAAG;GAAG,MAAM;EAAM,CAAC;EAEpH,OADA,EAAK;GAAE,MAAM;GAAmB,KAAK,EAAO,CAAG;GAAG;EAAQ,CAAC,GACpD;CACT;CAEA,MAAM,KAAK,GAAY,GAAgC;EACrD,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAmB,MAAM,EAAK,KAAK,MAAM,SAAS,mBAAmB;GAAE,QAAQ,EAAE,KAAK,EAAG;GAAG,MAAM,EAAE,QAAK;EAAE,CAAC;EAElH,OADA,EAAK;GAAE,MAAM;GAAmB;EAAQ,CAAC,GAClC;CACT;CAEA,MAAM,OAAO,GAA2B;EACtC,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,UAAU,mBAAmB,EAAE,QAAQ,EAAE,KAAK,EAAG,EAAE,CAAC,GAC1E,EAAK;GAAE,MAAM;GAAmB;EAAG,CAAC;CACtC;CAEA,MAAM,MAAM,GAAY,GAA0C;EAChE,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAyB,MAAM,EAAK,KAAK,MAAM,QAAQ,mBAAmB,EAAa,CAAK,KAAc,EAAE,QAAQ,EAAE,KAAK,EAAG,EAAE,CAAC;EAEvI,OADA,EAAK;GAAE,MAAM;GAAmB;GAAI;EAAO,CAAC,GACrC;CACT;CAEA,SAAS,GAAc,GAAgD;EACrE,OAAO,EAAK,KAAK,MAAM,OAAO,4BAA4B;GAAE,QAAQ,EAAO,CAAG;GAAG;EAAO,CAAC;CAC3F;CAEA,OAAO,IAAe,CAAC,GAAG,GAA2C;EACnE,OAAO,EAAK,KAAK,MAAM,OAAO,oBAAoB;GAAE,OAAO;GAAG;EAAO,CAAC;CACxE;CAEA,UAAU,IAAuB,CAAC,GAAG,GAA+C;EAClF,IAAM,EAAE,gBAAa,GAAG,MAAS;EACjC,OAAO,EAAK,KAAK,MAAM,OAAO,mBAAmB;GAAE,OAAO;IAAE,GAAG;IAAM,cAAc;GAAY;GAAG;EAAO,CAAC;CAC5G;CAEA,MAAM,QAAQ,GAA2B;EACvC,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,QAAQ,2BAA2B,EAAE,QAAQ,EAAE,KAAK,EAAG,EAAE,CAAC,GAChF,EAAK;GAAE,MAAM;GAAoB;EAAG,CAAC;CACvC;AACF"}
@@ -15,19 +15,19 @@ var t = class {
15
15
  });
16
16
  }
17
17
  async set(t, n) {
18
- let r = await e(this.http, n ? "POST" : "DELETE", "/{kind}/{id}/favorite", { params: {
18
+ let r = this.http.captureChanges(), i = await e(this.http, n ? "POST" : "DELETE", "/{kind}/{id}/favorite", { params: {
19
19
  kind: t.kind,
20
20
  id: t.id
21
21
  } });
22
- return this.http.emit({
22
+ return r({
23
23
  type: "favorite.changed",
24
24
  ref: {
25
25
  kind: t.kind,
26
26
  id: t.id
27
27
  },
28
- favorited: r.favorited,
29
- count: r.count
30
- }), r;
28
+ favorited: i.favorited,
29
+ count: i.count
30
+ }), i;
31
31
  }
32
32
  list(t = {}, n) {
33
33
  return e(this.http, "GET", "/favorites", {
@@ -1 +1 @@
1
- {"version":3,"file":"favorites.js","names":[],"sources":["../../../src/client/content/favorites.ts"],"sourcesContent":["import type { FavoriteItem, FavoriteState, RefBody } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call, type PageQuery } from \"../route.js\";\n\n/** Favorites: the item's count for everyone, the signed-in caller's own. */\nexport class FavoritesClient {\n constructor(private readonly http: Http) {}\n\n /** The item's favorite count and whether the caller favorited it (never, signed out). */\n get(ref: RefBody, signal?: AbortSignal): Promise<FavoriteState> {\n return call(this.http, \"GET\", \"/{kind}/{id}/favorite\", { params: { kind: ref.kind, id: ref.id }, signal });\n }\n /** Favorites or unfavorites the item (signed in); repeating is a no-op. Resolves with the new state and count. */\n async set(ref: RefBody, favorited: boolean): Promise<FavoriteState> {\n const state: FavoriteState = await call(this.http, favorited ? \"POST\" : \"DELETE\", \"/{kind}/{id}/favorite\", { params: { kind: ref.kind, id: ref.id } });\n this.http.emit({ type: \"favorite.changed\", ref: { kind: ref.kind, id: ref.id }, favorited: state.favorited, count: state.count });\n return state;\n }\n /** The caller's favorites, newest first (references only; the host hydrates them). */\n list(q: PageQuery = {}, signal?: AbortSignal): Promise<FavoriteItem[]> {\n return call(this.http, \"GET\", \"/favorites\", { query: q, signal });\n }\n}\n"],"mappings":";;AAKA,IAAa,IAAb,MAA6B;CACE;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,IAAI,GAAc,GAA8C;EAC9D,OAAO,EAAK,KAAK,MAAM,OAAO,yBAAyB;GAAE,QAAQ;IAAE,MAAM,EAAI;IAAM,IAAI,EAAI;GAAG;GAAG;EAAO,CAAC;CAC3G;CAEA,MAAM,IAAI,GAAc,GAA4C;EAClE,IAAM,IAAuB,MAAM,EAAK,KAAK,MAAM,IAAY,SAAS,UAAU,yBAAyB,EAAE,QAAQ;GAAE,MAAM,EAAI;GAAM,IAAI,EAAI;EAAG,EAAE,CAAC;EAErJ,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAoB,KAAK;IAAE,MAAM,EAAI;IAAM,IAAI,EAAI;GAAG;GAAG,WAAW,EAAM;GAAW,OAAO,EAAM;EAAM,CAAC,GACzH;CACT;CAEA,KAAK,IAAe,CAAC,GAAG,GAA+C;EACrE,OAAO,EAAK,KAAK,MAAM,OAAO,cAAc;GAAE,OAAO;GAAG;EAAO,CAAC;CAClE;AACF"}
1
+ {"version":3,"file":"favorites.js","names":[],"sources":["../../../src/client/content/favorites.ts"],"sourcesContent":["import type { FavoriteItem, FavoriteState, RefBody } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call, type PageQuery } from \"../route.js\";\n\n/** Favorites: the item's count for everyone, the signed-in caller's own. */\nexport class FavoritesClient {\n constructor(private readonly http: Http) {}\n\n /** The item's favorite count and whether the caller favorited it (never, signed out). */\n get(ref: RefBody, signal?: AbortSignal): Promise<FavoriteState> {\n return call(this.http, \"GET\", \"/{kind}/{id}/favorite\", { params: { kind: ref.kind, id: ref.id }, signal });\n }\n /** Favorites or unfavorites the item (signed in); repeating is a no-op. Resolves with the new state and count. */\n async set(ref: RefBody, favorited: boolean): Promise<FavoriteState> {\n const emit = this.http.captureChanges();\n const state: FavoriteState = await call(this.http, favorited ? \"POST\" : \"DELETE\", \"/{kind}/{id}/favorite\", { params: { kind: ref.kind, id: ref.id } });\n emit({ type: \"favorite.changed\", ref: { kind: ref.kind, id: ref.id }, favorited: state.favorited, count: state.count });\n return state;\n }\n /** The caller's favorites, newest first (references only; the host hydrates them). */\n list(q: PageQuery = {}, signal?: AbortSignal): Promise<FavoriteItem[]> {\n return call(this.http, \"GET\", \"/favorites\", { query: q, signal });\n }\n}\n"],"mappings":";;AAKA,IAAa,IAAb,MAA6B;CACE;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,IAAI,GAAc,GAA8C;EAC9D,OAAO,EAAK,KAAK,MAAM,OAAO,yBAAyB;GAAE,QAAQ;IAAE,MAAM,EAAI;IAAM,IAAI,EAAI;GAAG;GAAG;EAAO,CAAC;CAC3G;CAEA,MAAM,IAAI,GAAc,GAA4C;EAClE,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAuB,MAAM,EAAK,KAAK,MAAM,IAAY,SAAS,UAAU,yBAAyB,EAAE,QAAQ;GAAE,MAAM,EAAI;GAAM,IAAI,EAAI;EAAG,EAAE,CAAC;EAErJ,OADA,EAAK;GAAE,MAAM;GAAoB,KAAK;IAAE,MAAM,EAAI;IAAM,IAAI,EAAI;GAAG;GAAG,WAAW,EAAM;GAAW,OAAO,EAAM;EAAM,CAAC,GAC/G;CACT;CAEA,KAAK,IAAe,CAAC,GAAG,GAA+C;EACrE,OAAO,EAAK,KAAK,MAAM,OAAO,cAAc;GAAE,OAAO;GAAG;EAAO,CAAC;CAClE;AACF"}
@@ -12,9 +12,9 @@ var t = class {
12
12
  });
13
13
  }
14
14
  async resolve(t, n, r) {
15
- let i = t.kind, a = await e(this.http, "POST", "/moderation/{kind}/{id}/resolve", {
15
+ let i = this.http.captureChanges(), a = t.kind, o = await e(this.http, "POST", "/moderation/{kind}/{id}/resolve", {
16
16
  params: {
17
- kind: i,
17
+ kind: a,
18
18
  id: t.id
19
19
  },
20
20
  body: {
@@ -23,12 +23,12 @@ var t = class {
23
23
  ...r ? { reason: r } : {}
24
24
  }
25
25
  });
26
- return this.http.emit({
26
+ return i({
27
27
  type: "moderation.resolved",
28
- kind: i,
28
+ kind: a,
29
29
  id: t.id,
30
30
  decision: n
31
- }), a;
31
+ }), o;
32
32
  }
33
33
  };
34
34
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"moderation.js","names":[],"sources":["../../../src/client/content/moderation.ts"],"sourcesContent":["import type { HeldItem, HeldPage, ReviewOutcome } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call } from \"../route.js\";\nimport type { Decision, HeldKind } from \"./types.js\";\n\nexport interface HeldQuery {\n kind: HeldKind;\n /** The previous page's next. */\n cursor?: string;\n limit?: number;\n}\n\n/** The review queue of held comments and posts (ModerationReview). */\nexport class ModerationClient {\n constructor(private readonly http: Http) {}\n\n /** Held items awaiting review, oldest first. */\n held(q: HeldQuery, signal?: AbortSignal): Promise<HeldPage> {\n return call(this.http, \"GET\", \"/moderation/held\", { query: { ...q }, signal });\n }\n /** Approves (publishes) or rejects a held item at the revision the queue listed; conflict when it was edited since. */\n async resolve(item: Pick<HeldItem, \"kind\" | \"id\" | \"revision\">, decision: Decision, reason?: string): Promise<ReviewOutcome> {\n const kind = item.kind as HeldKind;\n const out: ReviewOutcome = await call(this.http, \"POST\", \"/moderation/{kind}/{id}/resolve\", {\n params: { kind, id: item.id },\n body: { decision, revision: item.revision, ...(reason ? { reason } : {}) },\n });\n this.http.emit({ type: \"moderation.resolved\", kind, id: item.id, decision });\n return out;\n }\n}\n"],"mappings":";;AAaA,IAAa,IAAb,MAA8B;CACC;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,KAAK,GAAc,GAAyC;EAC1D,OAAO,EAAK,KAAK,MAAM,OAAO,oBAAoB;GAAE,OAAO,EAAE,GAAG,EAAE;GAAG;EAAO,CAAC;CAC/E;CAEA,MAAM,QAAQ,GAAkD,GAAoB,GAAyC;EAC3H,IAAM,IAAO,EAAK,MACZ,IAAqB,MAAM,EAAK,KAAK,MAAM,QAAQ,mCAAmC;GAC1F,QAAQ;IAAE;IAAM,IAAI,EAAK;GAAG;GAC5B,MAAM;IAAE;IAAU,UAAU,EAAK;IAAU,GAAI,IAAS,EAAE,UAAO,IAAI,CAAC;GAAG;EAC3E,CAAC;EAED,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAuB;GAAM,IAAI,EAAK;GAAI;EAAS,CAAC,GACpE;CACT;AACF"}
1
+ {"version":3,"file":"moderation.js","names":[],"sources":["../../../src/client/content/moderation.ts"],"sourcesContent":["import type { HeldItem, HeldPage, ReviewOutcome } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call } from \"../route.js\";\nimport type { Decision, HeldKind } from \"./types.js\";\n\nexport interface HeldQuery {\n kind: HeldKind;\n /** The previous page's next. */\n cursor?: string;\n limit?: number;\n}\n\n/** The review queue of held comments and posts (ModerationReview). */\nexport class ModerationClient {\n constructor(private readonly http: Http) {}\n\n /** Held items awaiting review, oldest first. */\n held(q: HeldQuery, signal?: AbortSignal): Promise<HeldPage> {\n return call(this.http, \"GET\", \"/moderation/held\", { query: { ...q }, signal });\n }\n /** Approves (publishes) or rejects a held item at the revision the queue listed; conflict when it was edited since. */\n async resolve(item: Pick<HeldItem, \"kind\" | \"id\" | \"revision\">, decision: Decision, reason?: string): Promise<ReviewOutcome> {\n const emit = this.http.captureChanges();\n const kind = item.kind as HeldKind;\n const out: ReviewOutcome = await call(this.http, \"POST\", \"/moderation/{kind}/{id}/resolve\", {\n params: { kind, id: item.id },\n body: { decision, revision: item.revision, ...(reason ? { reason } : {}) },\n });\n emit({ type: \"moderation.resolved\", kind, id: item.id, decision });\n return out;\n }\n}\n"],"mappings":";;AAaA,IAAa,IAAb,MAA8B;CACC;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,KAAK,GAAc,GAAyC;EAC1D,OAAO,EAAK,KAAK,MAAM,OAAO,oBAAoB;GAAE,OAAO,EAAE,GAAG,EAAE;GAAG;EAAO,CAAC;CAC/E;CAEA,MAAM,QAAQ,GAAkD,GAAoB,GAAyC;EAC3H,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAO,EAAK,MACZ,IAAqB,MAAM,EAAK,KAAK,MAAM,QAAQ,mCAAmC;GAC1F,QAAQ;IAAE;IAAM,IAAI,EAAK;GAAG;GAC5B,MAAM;IAAE;IAAU,UAAU,EAAK;IAAU,GAAI,IAAS,EAAE,UAAO,IAAI,CAAC;GAAG;EAC3E,CAAC;EAED,OADA,EAAK;GAAE,MAAM;GAAuB;GAAM,IAAI,EAAK;GAAI;EAAS,CAAC,GAC1D;CACT;AACF"}
@@ -25,94 +25,97 @@ var t = class {
25
25
  });
26
26
  }
27
27
  async create(t) {
28
- let n = await e(this.http, "POST", "/polls", { body: t });
29
- return this.http.emit({
28
+ let n = this.http.captureChanges(), r = await e(this.http, "POST", "/polls", { body: t });
29
+ return n({
30
30
  type: "poll.created",
31
- poll: n
32
- }), n;
31
+ poll: r
32
+ }), r;
33
33
  }
34
34
  async update(t, n) {
35
- let r = await e(this.http, "PATCH", "/polls/{id}", {
35
+ let r = this.http.captureChanges(), i = await e(this.http, "PATCH", "/polls/{id}", {
36
36
  params: { id: t },
37
37
  body: n
38
38
  });
39
- return this.http.emit({
39
+ return r({
40
40
  type: "poll.updated",
41
- poll: r
42
- }), r;
41
+ poll: i
42
+ }), i;
43
43
  }
44
44
  async delete(t) {
45
- await e(this.http, "DELETE", "/polls/{id}", { params: { id: t } }), this.http.emit({
45
+ let n = this.http.captureChanges();
46
+ await e(this.http, "DELETE", "/polls/{id}", { params: { id: t } }), n({
46
47
  type: "poll.deleted",
47
48
  id: t
48
49
  });
49
50
  }
50
51
  async vote(t, n) {
51
- let r = await e(this.http, "POST", "/polls/{id}/vote", {
52
+ let r = this.http.captureChanges(), i = await e(this.http, "POST", "/polls/{id}/vote", {
52
53
  params: { id: t },
53
54
  body: { option_id: n }
54
55
  });
55
- return this.http.emit({
56
+ return r({
56
57
  type: "poll.updated",
57
- poll: r
58
- }), r;
58
+ poll: i
59
+ }), i;
59
60
  }
60
61
  async answer(t, n) {
61
- let r = await e(this.http, "POST", "/polls/{id}/answer", {
62
+ let r = this.http.captureChanges(), i = await e(this.http, "POST", "/polls/{id}/answer", {
62
63
  params: { id: t },
63
64
  body: { text: n }
64
65
  });
65
- return this.http.emit({
66
+ return r({
66
67
  type: "poll.updated",
67
- poll: r
68
- }), r;
68
+ poll: i
69
+ }), i;
69
70
  }
70
71
  async setImage(t, n) {
71
- let r = await e(this.http, "PUT", "/polls/{id}/image", {
72
+ let r = this.http.captureChanges(), i = await e(this.http, "PUT", "/polls/{id}/image", {
72
73
  params: { id: t },
73
74
  body: { image: n ?? "" }
74
75
  });
75
- return this.http.emit({
76
+ return r({
76
77
  type: "poll.changed",
77
78
  id: t
78
- }), r.image_url;
79
+ }), i.image_url;
79
80
  }
80
81
  async uploadImage(e, t, n) {
81
82
  return this.setImage(e, t ? (await this.media.uploadNamed(this.media.pollRef(e), t, n)).name : null);
82
83
  }
83
84
  async addOption(t, n) {
84
- let r = await e(this.http, "POST", "/polls/{id}/options", {
85
+ let r = this.http.captureChanges(), i = await e(this.http, "POST", "/polls/{id}/options", {
85
86
  params: { id: t },
86
87
  body: n
87
88
  });
88
- return this.http.emit({
89
+ return r({
89
90
  type: "poll.changed",
90
91
  id: t
91
- }), r;
92
+ }), i;
92
93
  }
93
94
  async updateOption(t, n, r) {
94
- let i = await e(this.http, "PATCH", "/polls/{id}/options/{oid}", {
95
+ let i = this.http.captureChanges(), a = await e(this.http, "PATCH", "/polls/{id}/options/{oid}", {
95
96
  params: {
96
97
  id: t,
97
98
  oid: n
98
99
  },
99
100
  body: r
100
101
  });
101
- return this.http.emit({
102
+ return i({
102
103
  type: "poll.changed",
103
104
  id: t
104
- }), i;
105
+ }), a;
105
106
  }
106
107
  async deleteOption(t, n) {
108
+ let r = this.http.captureChanges();
107
109
  await e(this.http, "DELETE", "/polls/{id}/options/{oid}", { params: {
108
110
  id: t,
109
111
  oid: n
110
- } }), this.http.emit({
112
+ } }), r({
111
113
  type: "poll.changed",
112
114
  id: t
113
115
  });
114
116
  }
115
117
  async reorderOptions(t, n) {
118
+ let r = this.http.captureChanges();
116
119
  for (let [r, i] of n.entries()) i.position !== r && await e(this.http, "PATCH", "/polls/{id}/options/{oid}", {
117
120
  params: {
118
121
  id: t,
@@ -120,23 +123,23 @@ var t = class {
120
123
  },
121
124
  body: { position: r }
122
125
  });
123
- this.http.emit({
126
+ r({
124
127
  type: "poll.changed",
125
128
  id: t
126
129
  });
127
130
  }
128
131
  async setOptionImage(t, n, r) {
129
- let i = await e(this.http, "PUT", "/polls/{id}/options/{oid}/image", {
132
+ let i = this.http.captureChanges(), a = await e(this.http, "PUT", "/polls/{id}/options/{oid}/image", {
130
133
  params: {
131
134
  id: t,
132
135
  oid: n
133
136
  },
134
137
  body: { image: r ?? "" }
135
138
  });
136
- return this.http.emit({
139
+ return i({
137
140
  type: "poll.changed",
138
141
  id: t
139
- }), i.image_url;
142
+ }), a.image_url;
140
143
  }
141
144
  async uploadOptionImage(e, t, n, r) {
142
145
  return this.setOptionImage(e, t, n ? (await this.media.uploadNamed(this.media.pollRef(e), n, r)).name : null);
@@ -1 +1 @@
1
- {"version":3,"file":"polls.js","names":[],"sources":["../../../src/client/content/polls.ts"],"sourcesContent":["import type { Poll, PollImage, PollInput, PollOption, PollOptionPatch, PollUpdate } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport type { MediaClient, NamedOptions } from \"../media/client.js\";\nimport { call, type PageQuery } from \"../route.js\";\n\nexport interface PollQuery extends PageQuery {\n language?: string;\n /** Polls live in this month, YYYY-MM. */\n month?: string;\n /** Polls live on this day, YYYY-MM-DD. */\n date?: string;\n}\n\n/** Site-wide polls: multiple choice or free text; staff write them (PollWrite). */\nexport class PollsClient {\n constructor(\n private readonly http: Http,\n private readonly media: MediaClient,\n ) {}\n\n /** Live polls, newest first. */\n list(q: PollQuery = {}, signal?: AbortSignal): Promise<Poll[]> {\n return call(this.http, \"GET\", \"/polls\", { query: q, signal });\n }\n /** Every poll, scheduled and inactive ones included (PollWrite). */\n adminList(q: PollQuery = {}, signal?: AbortSignal): Promise<Poll[]> {\n return call(this.http, \"GET\", \"/polls/admin\", { query: q, signal });\n }\n /** A poll with the caller's vote or answer. */\n get(id: string, signal?: AbortSignal): Promise<Poll> {\n return call(this.http, \"GET\", \"/polls/{id}\", { params: { id }, signal });\n }\n /** Creates a poll with its options (images are set once it exists). */\n async create(input: PollInput): Promise<Poll> {\n const poll: Poll = await call(this.http, \"POST\", \"/polls\", { body: input });\n this.http.emit({ type: \"poll.created\", poll });\n return poll;\n }\n /** Updates the given fields. */\n async update(id: string, patch: PollUpdate): Promise<Poll> {\n const poll: Poll = await call(this.http, \"PATCH\", \"/polls/{id}\", { params: { id }, body: patch });\n this.http.emit({ type: \"poll.updated\", poll });\n return poll;\n }\n /** Deletes the poll and its images. */\n async delete(id: string): Promise<void> {\n await call(this.http, \"DELETE\", \"/polls/{id}\", { params: { id } });\n this.http.emit({ type: \"poll.deleted\", id });\n }\n /** Votes for an option of an open multiple-choice poll; a vote is final, and voting again changes nothing. */\n async vote(id: string, option: string): Promise<Poll> {\n const poll: Poll = await call(this.http, \"POST\", \"/polls/{id}/vote\", { params: { id }, body: { option_id: option } });\n this.http.emit({ type: \"poll.updated\", poll });\n return poll;\n }\n /** Stores or replaces the signed-in caller's answer to an open free-text poll. */\n async answer(id: string, text: string): Promise<Poll> {\n const poll: Poll = await call(this.http, \"POST\", \"/polls/{id}/answer\", { params: { id }, body: { text } });\n this.http.emit({ type: \"poll.updated\", poll });\n return poll;\n }\n /** Sets the question image to an inline image name of the poll's folder, or clears it; resolves with its URL. */\n async setImage(id: string, image: string | null): Promise<string | null> {\n const r: PollImage = await call(this.http, \"PUT\", \"/polls/{id}/image\", { params: { id }, body: { image: image ?? \"\" } });\n this.http.emit({ type: \"poll.changed\", id });\n return r.image_url;\n }\n /** Uploads file to the poll's folder as the question image (null clears it). */\n async uploadImage(id: string, file: Blob | null, o?: NamedOptions): Promise<string | null> {\n return this.setImage(id, file ? (await this.media.uploadNamed(this.media.pollRef(id), file, o)).name : null);\n }\n /** Adds an option, at the end unless position is given. */\n async addOption(id: string, option: PollOptionPatch): Promise<PollOption> {\n const o: PollOption = await call(this.http, \"POST\", \"/polls/{id}/options\", { params: { id }, body: option });\n this.http.emit({ type: \"poll.changed\", id });\n return o;\n }\n async updateOption(id: string, option: string, patch: PollOptionPatch): Promise<PollOption> {\n const o: PollOption = await call(this.http, \"PATCH\", \"/polls/{id}/options/{oid}\", { params: { id, oid: option }, body: patch });\n this.http.emit({ type: \"poll.changed\", id });\n return o;\n }\n /** Removes an option and its votes; a poll keeps at least two. */\n async deleteOption(id: string, option: string): Promise<void> {\n await call(this.http, \"DELETE\", \"/polls/{id}/options/{oid}\", { params: { id, oid: option } });\n this.http.emit({ type: \"poll.changed\", id });\n }\n /** Gives the options the positions of their order, patching those that moved. */\n async reorderOptions(id: string, options: readonly Pick<PollOption, \"id\" | \"position\">[]): Promise<void> {\n for (const [position, o] of options.entries()) {\n if (o.position !== position) await call(this.http, \"PATCH\", \"/polls/{id}/options/{oid}\", { params: { id, oid: o.id }, body: { position } });\n }\n this.http.emit({ type: \"poll.changed\", id });\n }\n async setOptionImage(id: string, option: string, image: string | null): Promise<string | null> {\n const r: PollImage = await call(this.http, \"PUT\", \"/polls/{id}/options/{oid}/image\", { params: { id, oid: option }, body: { image: image ?? \"\" } });\n this.http.emit({ type: \"poll.changed\", id });\n return r.image_url;\n }\n /** Uploads file to the poll's folder as the option's image (null clears it). */\n async uploadOptionImage(id: string, option: string, file: Blob | null, o?: NamedOptions): Promise<string | null> {\n return this.setOptionImage(id, option, file ? (await this.media.uploadNamed(this.media.pollRef(id), file, o)).name : null);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"polls.js","names":[],"sources":["../../../src/client/content/polls.ts"],"sourcesContent":["import type { Poll, PollImage, PollInput, PollOption, PollOptionPatch, PollUpdate } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport type { MediaClient, NamedOptions } from \"../media/client.js\";\nimport { call, type PageQuery } from \"../route.js\";\n\nexport interface PollQuery extends PageQuery {\n language?: string;\n /** Polls live in this month, YYYY-MM. */\n month?: string;\n /** Polls live on this day, YYYY-MM-DD. */\n date?: string;\n}\n\n/** Site-wide polls: multiple choice or free text; staff write them (PollWrite). */\nexport class PollsClient {\n constructor(\n private readonly http: Http,\n private readonly media: MediaClient,\n ) {}\n\n /** Live polls, newest first. */\n list(q: PollQuery = {}, signal?: AbortSignal): Promise<Poll[]> {\n return call(this.http, \"GET\", \"/polls\", { query: q, signal });\n }\n /** Every poll, scheduled and inactive ones included (PollWrite). */\n adminList(q: PollQuery = {}, signal?: AbortSignal): Promise<Poll[]> {\n return call(this.http, \"GET\", \"/polls/admin\", { query: q, signal });\n }\n /** A poll with the caller's vote or answer. */\n get(id: string, signal?: AbortSignal): Promise<Poll> {\n return call(this.http, \"GET\", \"/polls/{id}\", { params: { id }, signal });\n }\n /** Creates a poll with its options (images are set once it exists). */\n async create(input: PollInput): Promise<Poll> {\n const emit = this.http.captureChanges();\n const poll: Poll = await call(this.http, \"POST\", \"/polls\", { body: input });\n emit({ type: \"poll.created\", poll });\n return poll;\n }\n /** Updates the given fields. */\n async update(id: string, patch: PollUpdate): Promise<Poll> {\n const emit = this.http.captureChanges();\n const poll: Poll = await call(this.http, \"PATCH\", \"/polls/{id}\", { params: { id }, body: patch });\n emit({ type: \"poll.updated\", poll });\n return poll;\n }\n /** Deletes the poll and its images. */\n async delete(id: string): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/polls/{id}\", { params: { id } });\n emit({ type: \"poll.deleted\", id });\n }\n /** Votes for an option of an open multiple-choice poll; a vote is final, and voting again changes nothing. */\n async vote(id: string, option: string): Promise<Poll> {\n const emit = this.http.captureChanges();\n const poll: Poll = await call(this.http, \"POST\", \"/polls/{id}/vote\", { params: { id }, body: { option_id: option } });\n emit({ type: \"poll.updated\", poll });\n return poll;\n }\n /** Stores or replaces the signed-in caller's answer to an open free-text poll. */\n async answer(id: string, text: string): Promise<Poll> {\n const emit = this.http.captureChanges();\n const poll: Poll = await call(this.http, \"POST\", \"/polls/{id}/answer\", { params: { id }, body: { text } });\n emit({ type: \"poll.updated\", poll });\n return poll;\n }\n /** Sets the question image to an inline image name of the poll's folder, or clears it; resolves with its URL. */\n async setImage(id: string, image: string | null): Promise<string | null> {\n const emit = this.http.captureChanges();\n const r: PollImage = await call(this.http, \"PUT\", \"/polls/{id}/image\", { params: { id }, body: { image: image ?? \"\" } });\n emit({ type: \"poll.changed\", id });\n return r.image_url;\n }\n /** Uploads file to the poll's folder as the question image (null clears it). */\n async uploadImage(id: string, file: Blob | null, o?: NamedOptions): Promise<string | null> {\n return this.setImage(id, file ? (await this.media.uploadNamed(this.media.pollRef(id), file, o)).name : null);\n }\n /** Adds an option, at the end unless position is given. */\n async addOption(id: string, option: PollOptionPatch): Promise<PollOption> {\n const emit = this.http.captureChanges();\n const o: PollOption = await call(this.http, \"POST\", \"/polls/{id}/options\", { params: { id }, body: option });\n emit({ type: \"poll.changed\", id });\n return o;\n }\n async updateOption(id: string, option: string, patch: PollOptionPatch): Promise<PollOption> {\n const emit = this.http.captureChanges();\n const o: PollOption = await call(this.http, \"PATCH\", \"/polls/{id}/options/{oid}\", { params: { id, oid: option }, body: patch });\n emit({ type: \"poll.changed\", id });\n return o;\n }\n /** Removes an option and its votes; a poll keeps at least two. */\n async deleteOption(id: string, option: string): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/polls/{id}/options/{oid}\", { params: { id, oid: option } });\n emit({ type: \"poll.changed\", id });\n }\n /** Gives the options the positions of their order, patching those that moved. */\n async reorderOptions(id: string, options: readonly Pick<PollOption, \"id\" | \"position\">[]): Promise<void> {\n const emit = this.http.captureChanges();\n for (const [position, o] of options.entries()) {\n if (o.position !== position) await call(this.http, \"PATCH\", \"/polls/{id}/options/{oid}\", { params: { id, oid: o.id }, body: { position } });\n }\n emit({ type: \"poll.changed\", id });\n }\n async setOptionImage(id: string, option: string, image: string | null): Promise<string | null> {\n const emit = this.http.captureChanges();\n const r: PollImage = await call(this.http, \"PUT\", \"/polls/{id}/options/{oid}/image\", { params: { id, oid: option }, body: { image: image ?? \"\" } });\n emit({ type: \"poll.changed\", id });\n return r.image_url;\n }\n /** Uploads file to the poll's folder as the option's image (null clears it). */\n async uploadOptionImage(id: string, option: string, file: Blob | null, o?: NamedOptions): Promise<string | null> {\n return this.setOptionImage(id, option, file ? (await this.media.uploadNamed(this.media.pollRef(id), file, o)).name : null);\n }\n}\n"],"mappings":"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"}