@scalemule/news-publication 0.0.28 → 0.0.30

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.
@@ -4,6 +4,227 @@
4
4
  var react = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
+ var stable = (value) => Array.isArray(value) ? value.map(stable) : value && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map((k) => [k, stable(value[k])])) : value;
8
+ var same = (a, b) => JSON.stringify(stable(a)) === JSON.stringify(stable(b));
9
+ var hasDraft = (data) => !!data && Object.keys(data).length > 0;
10
+ function useReviewDrafts(session, revision, initial, request) {
11
+ const [, render] = react.useState(0);
12
+ const entries = react.useRef({});
13
+ const active = react.useRef("");
14
+ const requester = react.useRef(request);
15
+ requester.current = request;
16
+ const timers = react.useRef({});
17
+ const flights = react.useRef({});
18
+ const key = (id, panel) => `story-response:${id}:${panel}`;
19
+ function backup(id, panel, entry) {
20
+ try {
21
+ if (entry.dirty) localStorage.setItem(key(id, panel), JSON.stringify({ ...entry, expires: Date.now() + 7 * 864e5 }));
22
+ else localStorage.removeItem(key(id, panel));
23
+ entry.local = true;
24
+ } catch {
25
+ entry.local = false;
26
+ }
27
+ }
28
+ function redraw() {
29
+ render((n) => n + 1);
30
+ }
31
+ async function flush(panel) {
32
+ clearTimeout(timers.current[panel]);
33
+ if (flights.current[panel]) {
34
+ await flights.current[panel];
35
+ if (entries.current[panel]?.dirty) return flush(panel);
36
+ return;
37
+ }
38
+ const id = active.current, revisionId = revision;
39
+ if (!revisionId) return;
40
+ const run = async () => {
41
+ let entry = entries.current[panel];
42
+ while (entry?.dirty && active.current === id) {
43
+ if (entry.status === "conflict") throw Error("Choose which response to keep before sending.");
44
+ const sent = entry.data;
45
+ entry.status = "saving";
46
+ redraw();
47
+ try {
48
+ const saved = await requester.current("", { action: "SAVE_DRAFT", revision_id: revisionId, panel, data: sent, expected_version: entry.version });
49
+ if (active.current !== id) return;
50
+ if (!Number.isSafeInteger(saved.version)) throw Error("Your response could not be saved yet. Please try again.");
51
+ entry = entries.current[panel];
52
+ entry.version = saved.version;
53
+ entry.dirty = !same(entry.data, sent);
54
+ entry.status = entry.dirty ? "saving" : "saved";
55
+ backup(id, panel, entry);
56
+ redraw();
57
+ } catch (error) {
58
+ if (active.current === id) {
59
+ entry.status = error.status === 409 ? "conflict" : "offline";
60
+ backup(id, panel, entry);
61
+ redraw();
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+ };
67
+ const promise = run();
68
+ flights.current[panel] = promise;
69
+ try {
70
+ await promise;
71
+ } finally {
72
+ if (flights.current[panel] === promise) delete flights.current[panel];
73
+ }
74
+ }
75
+ const flushRef = react.useRef(flush);
76
+ flushRef.current = flush;
77
+ react.useEffect(() => {
78
+ if (!revision) return;
79
+ const id = `${session}:${revision}`;
80
+ active.current = id;
81
+ const next = {};
82
+ for (const panel of ["questions", "changes", "comment", "edits", "approve", "decline", "media", "metadata"]) {
83
+ const server = initial?.[panel] || { panel, version: 0, data: {} };
84
+ let entry = { ...server, status: "saved" };
85
+ try {
86
+ const raw = localStorage.getItem(key(id, panel));
87
+ const local = raw ? JSON.parse(raw) : null;
88
+ if (local?.dirty && local.expires > Date.now() && !same(local.data, server.data)) {
89
+ entry = { ...local, local: true, status: local.version === server.version ? "saving" : "conflict" };
90
+ } else localStorage.removeItem(key(id, panel));
91
+ } catch {
92
+ }
93
+ next[panel] = entry;
94
+ }
95
+ entries.current = next;
96
+ redraw();
97
+ for (const [panel, entry] of Object.entries(next)) if (entry.dirty && entry.status !== "conflict") void flushRef.current(panel).catch(() => {
98
+ });
99
+ const retry = () => {
100
+ for (const [panel, entry] of Object.entries(entries.current)) if (entry.dirty && entry.status !== "conflict") void flushRef.current(panel).catch(() => {
101
+ });
102
+ };
103
+ window.addEventListener("online", retry);
104
+ const interval = setInterval(retry, 15e3);
105
+ return () => {
106
+ active.current = "";
107
+ Object.values(timers.current).forEach(clearTimeout);
108
+ flights.current = {};
109
+ window.removeEventListener("online", retry);
110
+ clearInterval(interval);
111
+ };
112
+ }, [session, revision]);
113
+ function set(panel, data) {
114
+ if (!active.current) return;
115
+ const old = entries.current[panel] || { panel, version: 0, data: {} };
116
+ const entry = { ...old, data, dirty: true, status: old.status === "conflict" ? "conflict" : "saving" };
117
+ entries.current[panel] = entry;
118
+ backup(active.current, panel, entry);
119
+ redraw();
120
+ clearTimeout(timers.current[panel]);
121
+ if (entry.status !== "conflict") timers.current[panel] = setTimeout(() => void flushRef.current(panel).catch(() => {
122
+ }), 800);
123
+ }
124
+ async function resolve(panel, keepLocal) {
125
+ const latest = await requester.current();
126
+ if (latest.revision.id !== revision) throw Error("The newsroom shared a new preview. Reload before continuing; your previous writing is saved separately.");
127
+ const server = latest.response_drafts?.[panel] || { panel, version: 0, data: {} };
128
+ const data = keepLocal ? entries.current[panel].data : server.data;
129
+ entries.current[panel] = { ...server, data, dirty: keepLocal, status: keepLocal ? "saving" : "saved" };
130
+ backup(active.current, panel, entries.current[panel]);
131
+ redraw();
132
+ if (keepLocal) await flush(panel);
133
+ }
134
+ function consumed(saved) {
135
+ clearTimeout(timers.current[saved.panel]);
136
+ entries.current[saved.panel] = { ...saved, dirty: false, status: "saved" };
137
+ backup(active.current, saved.panel, entries.current[saved.panel]);
138
+ redraw();
139
+ }
140
+ return {
141
+ entries: entries.current,
142
+ set,
143
+ flush,
144
+ resolve,
145
+ consumed,
146
+ async discard(panel) {
147
+ set(panel, {});
148
+ await flush(panel);
149
+ },
150
+ async receipt(panel) {
151
+ await flush(panel);
152
+ const entry = entries.current[panel];
153
+ return entry?.version ? { panel, version: entry.version } : void 0;
154
+ }
155
+ };
156
+ }
157
+ async function stored(key, value) {
158
+ const db = await new Promise((resolve, reject) => {
159
+ const request = indexedDB.open("story-review-pending-media", 1);
160
+ request.onupgradeneeded = () => request.result.createObjectStore("photos");
161
+ request.onsuccess = () => resolve(request.result);
162
+ request.onerror = () => reject(request.error);
163
+ });
164
+ try {
165
+ return await new Promise((resolve, reject) => {
166
+ const tx = db.transaction("photos", value === void 0 ? "readonly" : "readwrite");
167
+ const store = tx.objectStore("photos");
168
+ const request = value === void 0 ? store.get(key) : value === null ? store.delete(key) : store.put(value, key);
169
+ tx.oncomplete = () => resolve(value === void 0 ? request.result : void 0);
170
+ tx.onerror = () => reject(tx.error);
171
+ tx.onabort = () => reject(tx.error);
172
+ });
173
+ } finally {
174
+ db.close();
175
+ }
176
+ }
177
+ function usePendingPhoto(key) {
178
+ const [photo, setPhoto] = react.useState(null);
179
+ const [saved, setSaved] = react.useState(false);
180
+ const [preview, setPreview] = react.useState("");
181
+ const generation = react.useRef(0);
182
+ const chain = react.useRef(Promise.resolve());
183
+ react.useEffect(() => {
184
+ const at = ++generation.current;
185
+ setPhoto(null);
186
+ setSaved(false);
187
+ if (key) void stored(key).then((value) => {
188
+ if (generation.current !== at) return;
189
+ if (value && value.expires > Date.now()) {
190
+ setPhoto(value);
191
+ setSaved(true);
192
+ } else if (value) void stored(key, null).catch(() => {
193
+ });
194
+ }).catch(() => {
195
+ });
196
+ return () => {
197
+ generation.current++;
198
+ };
199
+ }, [key]);
200
+ react.useEffect(() => {
201
+ if (!photo?.file.type.startsWith("image/")) {
202
+ setPreview("");
203
+ return;
204
+ }
205
+ const url = URL.createObjectURL(photo.file);
206
+ setPreview(url);
207
+ return () => URL.revokeObjectURL(url);
208
+ }, [photo?.file]);
209
+ async function select(file, replacement = null) {
210
+ if (!key) return;
211
+ const at = ++generation.current;
212
+ const value = file ? { file, replacement, expires: Date.now() + 7 * 864e5 } : null;
213
+ setPhoto(value);
214
+ setSaved(false);
215
+ const operation = chain.current.catch(() => {
216
+ }).then(() => stored(key, value));
217
+ chain.current = operation;
218
+ try {
219
+ await operation;
220
+ if (generation.current === at) setSaved(!!value);
221
+ } catch {
222
+ if (generation.current === at) setSaved(false);
223
+ }
224
+ }
225
+ return { photo, saved, preview, select };
226
+ }
227
+
7
228
  // src/story-diff.ts
8
229
  function proseDiff(before, after) {
9
230
  const tokens = (s) => s.match(/\s+|[\p{L}\p{N}\p{M}]+|[^\s]/gu) ?? [];
@@ -72,11 +293,32 @@ function StoryReview({
72
293
  }) {
73
294
  const base = `/api/news/review/${encodeURIComponent(sessionId)}`;
74
295
  const [review, setReview] = react.useState(null), [error, setError] = react.useState(""), [notice, setNotice] = react.useState(""), [busy, setBusy] = react.useState(false), [panel, setPanel] = react.useState("article"), [verification, setVerification] = react.useState(false), [code, setCode] = react.useState("");
75
- const [message, setMessage] = react.useState(""), [document, setDocument] = react.useState(null), [answers, setAnswers] = react.useState({}), [file, setFile] = react.useState(null), [rights, setRights] = react.useState(false), [compare, setCompare] = react.useState(""), [compareTo, setCompareTo] = react.useState(""), [editingMedia, setEditingMedia] = react.useState(""), [commentTarget, setCommentTarget] = react.useState("STORY");
296
+ const [rights, setRights] = react.useState(false), [compare, setCompare] = react.useState(""), [compareTo, setCompareTo] = react.useState(""), [editingMedia, setEditingMedia] = react.useState(""), [commentTarget, setCommentTarget] = react.useState("STORY");
297
+ const pendingPhoto = usePendingPhoto(review ? `${sessionId}:${review.revision.id}` : void 0);
298
+ const file = pendingPhoto.photo?.file || null;
299
+ const galleryRef = react.useRef(null);
300
+ const cameraRef = react.useRef(null);
301
+ const noticeRef = react.useRef(null);
302
+ react.useEffect(() => {
303
+ if (notice) {
304
+ noticeRef.current?.scrollIntoView({ block: "center" });
305
+ noticeRef.current?.focus({ preventScroll: true });
306
+ }
307
+ }, [notice]);
76
308
  const responseRef = react.useRef(null);
77
309
  const thanksRef = react.useRef(null);
78
310
  const [moreQuestions, setMoreQuestions] = react.useState(false);
79
- const [correctionKind, setCorrectionKind] = react.useState("Correct a detail");
311
+ const [replacementPhoto, setReplacementPhoto] = react.useState(null);
312
+ const photoToReplace = pendingPhoto.photo?.replacement || replacementPhoto;
313
+ function openMedia(photo = null) {
314
+ setReplacementPhoto(file ? pendingPhoto.photo?.replacement || null : photo);
315
+ if (file) {
316
+ setPanel("media");
317
+ return;
318
+ }
319
+ setRights(false);
320
+ setPanel("media");
321
+ }
80
322
  react.useEffect(() => {
81
323
  if (panel !== "article") {
82
324
  responseRef.current?.scrollIntoView({
@@ -86,15 +328,32 @@ function StoryReview({
86
328
  responseRef.current?.focus({ preventScroll: true });
87
329
  }
88
330
  }, [panel]);
89
- const [mediaInfo, setMediaInfo] = react.useState({
90
- caption: "",
91
- who_is_pictured: "",
92
- creator: "",
93
- when_taken: "",
94
- where_taken: "",
95
- credit: "",
96
- alt: ""
97
- });
331
+ const emptyMediaInfo = { caption: "", who_is_pictured: "", creator: "", when_taken: "", where_taken: "", credit: "", alt: "" };
332
+ const drafts = useReviewDrafts(sessionId, review?.revision.id, review?.response_drafts, request);
333
+ const currentDraft = drafts.entries[panel];
334
+ const message = currentDraft?.data.message || "";
335
+ const answers = drafts.entries.questions?.data.answers || {};
336
+ const document = drafts.entries.edits?.data.document || review?.revision.document || null;
337
+ const correctionKind = drafts.entries.changes?.data.correctionKind || "Correct a detail";
338
+ const mediaPanel = panel === "metadata" ? "metadata" : "media";
339
+ const mediaInfo = { ...emptyMediaInfo, ...drafts.entries[mediaPanel]?.data.mediaInfo };
340
+ function setMessage(value) {
341
+ drafts.set(panel, value.trim() ? { ...currentDraft?.data, message: value, ...panel === "changes" ? { correctionKind } : {}, ...panel === "comment" ? { commentTarget: currentDraft?.data.commentTarget || commentTarget } : {} } : {});
342
+ }
343
+ function setAnswers(value) {
344
+ drafts.set("questions", Object.values(value).some((a) => a.trim()) ? { answers: value } : {});
345
+ }
346
+ function setDocument(value) {
347
+ drafts.set("edits", JSON.stringify(value) === JSON.stringify(review?.revision.document) ? {} : { document: value });
348
+ }
349
+ function setCorrectionKind(value) {
350
+ drafts.set("changes", { ...drafts.entries.changes?.data, correctionKind: value });
351
+ }
352
+ function setMediaInfo(value) {
353
+ drafts.set(mediaPanel, Object.values(value).some((v) => v.trim()) ? { mediaInfo: value, editingMedia: drafts.entries.metadata?.data.editingMedia || editingMedia } : {});
354
+ }
355
+ const pending = Object.entries(drafts.entries).filter(([, entry]) => hasDraft(entry.data));
356
+ const draftLabel = (name) => ({ questions: "your answers", changes: "your correction", media: "your photograph details", metadata: "your caption changes", edits: "your wording changes", comment: "your comment", decline: "your response", approve: "your approval message" })[name] || "your response";
98
357
  async function request(path = "", body) {
99
358
  const form = body instanceof FormData;
100
359
  const r = await fetch(base + path, {
@@ -106,9 +365,7 @@ function StoryReview({
106
365
  });
107
366
  const result = await r.json();
108
367
  if (!r.ok || result.success === false)
109
- throw new Error(
110
- result.error?.message || "This private review is unavailable. Please contact the newsroom."
111
- );
368
+ throw Object.assign(new Error(result.error?.message || "This private review is unavailable. Please contact the newsroom."), { status: r.status });
112
369
  return result.data ?? result;
113
370
  }
114
371
  async function load() {
@@ -119,7 +376,6 @@ function StoryReview({
119
376
  } else {
120
377
  setVerification(false);
121
378
  setReview(data);
122
- setDocument(data.revision.document);
123
379
  }
124
380
  }
125
381
  react.useEffect(() => {
@@ -139,7 +395,6 @@ function StoryReview({
139
395
  } else {
140
396
  setVerification(false);
141
397
  setReview(data);
142
- setDocument(data.revision.document);
143
398
  }
144
399
  }
145
400
  } catch (e) {
@@ -152,16 +407,27 @@ function StoryReview({
152
407
  };
153
408
  }, [sessionId]);
154
409
  async function send(action, success) {
155
- if (!review) return;
410
+ if (!review || busy) return;
411
+ if (action.action === "APPROVE" && file) {
412
+ setNotice("Your photo is not uploaded yet. Upload it or remove the selection before approving.");
413
+ setPanel("media");
414
+ return;
415
+ }
156
416
  setBusy(true);
157
417
  setError("");
158
418
  setNotice("");
159
419
  try {
160
- await request("", { revision_id: review.revision.id, ...action });
161
- await load();
420
+ const actionPanel = { ANSWER_QUESTIONS: "questions", REQUEST_CHANGES: "changes", COMMENT: "comment", PROPOSE_EDITS: "edits", EDIT_MEDIA_METADATA: "metadata", APPROVE: "approve", DECLINE: "decline" }[String(action.action)];
421
+ const receipt = actionPanel ? await drafts.receipt(actionPanel) : void 0;
422
+ const sent = await request("", { revision_id: review.revision.id, ...action, ...receipt ? { draft: receipt } : {} });
423
+ if (sent.cleared_draft) drafts.consumed(sent.cleared_draft);
424
+ try {
425
+ await load();
426
+ } catch {
427
+ success += " Refresh to see the latest preview.";
428
+ }
162
429
  setNotice(success);
163
430
  setPanel("article");
164
- setMessage("");
165
431
  } catch (e) {
166
432
  setError(
167
433
  e instanceof Error ? e.message : "Could not save your response."
@@ -182,12 +448,30 @@ function StoryReview({
182
448
  }
183
449
  }, [approved, finalEditor]);
184
450
  const approve = () => {
451
+ if (pending.length || file) {
452
+ setNotice("You have a response in progress. Send it or discard it, then return to approve the story.");
453
+ setPanel(file ? "media" : pending[0][0]);
454
+ return;
455
+ }
185
456
  if (celebration && finalEditor && !review?.questions.some((q) => q.required))
186
457
  void send({ action: "APPROVE", message: null }, approvalThanks);
187
458
  else setPanel("approve");
188
459
  };
460
+ function choosePhoto(selected2) {
461
+ if (!selected2) return;
462
+ const photo = ["image/jpeg", "image/png", "image/webp"].includes(selected2.type);
463
+ const video = ["video/mp4", "video/webm"].includes(selected2.type);
464
+ if (!photo && !video || selected2.size > (photo ? 12 : 25) * 1024 * 1024) {
465
+ setError("Please choose a JPEG, PNG or WebP photo up to 12 MB, or an MP4/WebM video up to 25 MB. For an HEIC photo, export or share it as JPEG first.");
466
+ return;
467
+ }
468
+ setError("");
469
+ setRights(false);
470
+ void pendingPhoto.select(selected2, photoToReplace);
471
+ }
189
472
  async function upload() {
190
- if (!review || !file || !rights) return;
473
+ if (!review || !file || !rights || busy) return;
474
+ const replacementPhoto2 = pendingPhoto.photo?.replacement;
191
475
  setBusy(true);
192
476
  setError("");
193
477
  try {
@@ -197,13 +481,43 @@ function StoryReview({
197
481
  "metadata",
198
482
  JSON.stringify({ ...mediaInfo, rights_confirmed: true })
199
483
  );
200
- await request("/media", form);
201
- await load();
202
- setFile(null);
484
+ const received = await request("/media", form);
485
+ let uploadNotice = "Media received privately. The newsroom will review it before publication.";
486
+ if (replacementPhoto2) {
487
+ uploadNotice = "Your replacement photo has been sent to the newsroom. The current photo will stay in this preview until we prepare an updated draft.";
488
+ if (can("COMMENT")) {
489
+ try {
490
+ await request("", {
491
+ revision_id: review.revision.id,
492
+ action: "COMMENT",
493
+ message: "Please use my newly uploaded photograph in place of this photo.",
494
+ target: {
495
+ kind: "PHOTO",
496
+ reference: replacementPhoto2.contribution_id,
497
+ replacement_contribution_id: received.id
498
+ }
499
+ });
500
+ } catch {
501
+ uploadNotice = "Your photo was uploaded privately, but we couldn't attach the replacement note. Please use Suggest a Change to tell the newsroom which photograph it should replace.";
502
+ }
503
+ } else {
504
+ uploadNotice = "Your photo was uploaded privately for newsroom review. The newsroom will select the photo for the next preview.";
505
+ }
506
+ }
507
+ await pendingPhoto.select(null);
508
+ try {
509
+ await drafts.discard("media");
510
+ } catch {
511
+ uploadNotice += " Your saved caption details still need to be cleared; you do not need to upload the photo again.";
512
+ }
513
+ try {
514
+ await load();
515
+ } catch {
516
+ uploadNotice += " Refresh to see the received photo below.";
517
+ }
203
518
  setRights(false);
204
- setNotice(
205
- "Media received privately. The newsroom will review it before publication."
206
- );
519
+ setNotice(uploadNotice);
520
+ setReplacementPhoto(null);
207
521
  setPanel("article");
208
522
  } catch (e) {
209
523
  setError(e instanceof Error ? e.message : "Upload failed.");
@@ -229,7 +543,7 @@ function StoryReview({
229
543
  ] }) }),
230
544
  /* @__PURE__ */ jsxRuntime.jsxs("main", { className: `sm-story-review${celebration ? " sm-review-celebration" : ""}`, children: [
231
545
  error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "sm-review-alert", children: error }),
232
- notice && !(approved && finalEditor && notice === approvalThanks) && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", className: "sm-review-notice", children: notice }),
546
+ notice && !(approved && finalEditor && notice === approvalThanks) && /* @__PURE__ */ jsxRuntime.jsx("p", { ref: noticeRef, tabIndex: -1, role: "status", className: "sm-review-notice", children: notice }),
233
547
  verification && /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "sm-review-form", children: [
234
548
  /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Verify your email" }),
235
549
  /* @__PURE__ */ jsxRuntime.jsx("p", { children: "The editor requires verification using the email address on your invitation. Your story remains private until verified." }),
@@ -334,7 +648,16 @@ function StoryReview({
334
648
  /* @__PURE__ */ jsxRuntime.jsx("h1", { children: review.revision.document.title }),
335
649
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sm-review-dek", children: review.revision.document.subtitle }),
336
650
  /* @__PURE__ */ jsxRuntime.jsx(EditorialNotices, { editorial: review.revision.editorial }),
337
- review.revision.document.media.filter((m) => m.position === "hero").map((m) => /* @__PURE__ */ jsxRuntime.jsx(PrivatePhoto, { media: m, base }, m.file_id)),
651
+ review.revision.document.media.filter((m) => m.position === "hero").map((m) => /* @__PURE__ */ jsxRuntime.jsx(
652
+ PrivatePhoto,
653
+ {
654
+ media: m,
655
+ base,
656
+ onChange: can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
657
+ disabled: busy
658
+ },
659
+ m.file_id
660
+ )),
338
661
  celebration && !review.revision.document.media.some(
339
662
  (m) => m.position === "hero"
340
663
  ) && can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -347,7 +670,7 @@ function StoryReview({
347
670
  /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "A photograph makes it yours." }),
348
671
  /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Family photograph may be added before publication." })
349
672
  ] }),
350
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("media"), children: "+ Add a photograph" })
673
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => openMedia(), children: "+ Add a photograph" })
351
674
  ]
352
675
  }
353
676
  ),
@@ -358,7 +681,16 @@ function StoryReview({
358
681
  dangerouslySetInnerHTML: { __html: review.revision.content_html }
359
682
  }
360
683
  ),
361
- review.revision.document.media.filter((m) => m.position !== "hero").map((m) => /* @__PURE__ */ jsxRuntime.jsx(PrivatePhoto, { media: m, base }, m.file_id))
684
+ review.revision.document.media.filter((m) => m.position !== "hero").map((m) => /* @__PURE__ */ jsxRuntime.jsx(
685
+ PrivatePhoto,
686
+ {
687
+ media: m,
688
+ base,
689
+ onChange: m.kind !== "video" && can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
690
+ disabled: busy
691
+ },
692
+ m.file_id
693
+ ))
362
694
  ] }),
363
695
  celebration && /* @__PURE__ */ jsxRuntime.jsxs("section", { id: "review-options", className: "sm-review-welcome", children: [
364
696
  /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Help us make this just right" }),
@@ -401,7 +733,7 @@ function StoryReview({
401
733
  ),
402
734
  can("REQUEST_CHANGES") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("changes"), children: celebration ? "Suggest a Change" : "Request changes" }),
403
735
  celebration && can("PROPOSE_EDITS") && !can("REQUEST_CHANGES") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("edits"), children: "Suggest a Change" }),
404
- can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("media"), children: celebration ? "Add Photos" : "Add photos" }),
736
+ can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => openMedia(), children: celebration ? "Add Photos" : "Add photos" }),
405
737
  !celebration && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
406
738
  can("PROPOSE_EDITS") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("edits"), children: "Suggest edits" }),
407
739
  can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("questions"), children: "Answer questions" }),
@@ -414,6 +746,15 @@ function StoryReview({
414
746
  can("VIEW_SELECTED_HISTORY") && review.history.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("history"), children: "See previous previews" }),
415
747
  can("DOWNLOAD_PREVIEW") && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => window.print(), children: "Print / save preview" })
416
748
  ] }),
749
+ panel === "article" && (pending.length > 0 || file) && /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: "sm-review-resume", "aria-label": "Unfinished responses", children: [
750
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "You have a response in progress" }),
751
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Send your response when you're ready, then return here to review and approve the story." }),
752
+ pending.map(([name]) => /* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: () => setPanel(name), children: [
753
+ "Continue ",
754
+ draftLabel(name)
755
+ ] }, name)),
756
+ file && !pending.some(([name]) => name === "media") && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("media"), children: "Continue your photo upload" })
757
+ ] }),
417
758
  panel !== "article" && /* @__PURE__ */ jsxRuntime.jsxs(
418
759
  "section",
419
760
  {
@@ -423,355 +764,394 @@ function StoryReview({
423
764
  "aria-label": "Your response",
424
765
  children: [
425
766
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-form-heading", children: [
426
- /* @__PURE__ */ jsxRuntime.jsx("h2", { children: panel === "approve" ? "Approve this exact draft" : panel === "edits" ? "Propose changes" : panel === "media" ? "Contribute a photograph" : panel === "questions" ? "Tell us a little more" : panel === "history" ? "Selected revision history" : panel === "decline" ? "Prefer not to publish" : panel === "changes" ? "Suggest a change" : "Message the newsroom" }),
767
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: panel === "approve" ? "Approve this exact draft" : panel === "edits" ? "Propose changes" : panel === "media" ? photoToReplace ? "Change photo" : "Contribute a photograph" : panel === "questions" ? "Tell us a little more" : panel === "history" ? "Selected revision history" : panel === "decline" ? "Prefer not to publish" : panel === "changes" ? "Suggest a change" : "Message the newsroom" }),
427
768
  /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Close" })
428
769
  ] }),
429
- panel === "edits" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
430
- /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Your suggestions will be reviewed by an editor. They do not replace the newsroom\u2019s draft." }),
431
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
432
- "Headline",
433
- /* @__PURE__ */ jsxRuntime.jsx(
434
- "input",
435
- {
436
- value: document.title,
437
- onChange: (e) => setDocument({ ...document, title: e.target.value })
438
- }
439
- )
440
- ] }),
441
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
442
- "Short introduction",
443
- /* @__PURE__ */ jsxRuntime.jsx(
444
- "input",
445
- {
446
- value: document.subtitle,
447
- onChange: (e) => setDocument({ ...document, subtitle: e.target.value })
448
- }
449
- )
450
- ] }),
451
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
452
- "Suggested article text",
770
+ panel !== "history" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-save-state", role: "status", "aria-live": "polite", children: [
771
+ currentDraft?.status === "conflict" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
772
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "A response was saved in another tab or device. Your writing here is still available. Choose which version to keep." }),
773
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => void drafts.resolve(panel, true).catch((e) => setError(e.message)), children: "Keep what I wrote here" }),
774
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => void drafts.resolve(panel, false).catch((e) => setError(e.message)), children: "Use the saved response" })
775
+ ] }) : currentDraft?.status === "offline" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
776
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: currentDraft.local ? "Saved on this device. Waiting for a connection to save online." : "Not saved yet. Keep this page open and try again." }),
777
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => void drafts.flush(panel).catch((e) => setError(e.message)), children: "Try saving again" })
778
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { children: currentDraft?.status === "saving" ? "Saving your writing\u2026" : hasDraft(currentDraft?.data) ? "Saved \u2014 not sent yet. You can safely come back later." : "Your writing saves automatically. Use the Send button below when you're ready to share it with the newsroom." }),
779
+ hasDraft(currentDraft?.data) && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "sm-review-quiet", disabled: busy, onClick: () => {
780
+ if (window.confirm("Discard this unsent response? Responses already sent will be kept.")) void drafts.discard(panel).catch((e) => setError(e.message));
781
+ }, children: "Discard this unsent response" })
782
+ ] }),
783
+ /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { disabled: busy, className: "sm-review-response-fields", children: [
784
+ panel === "edits" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
785
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Your suggestions will be reviewed by an editor. They do not replace the newsroom\u2019s draft." }),
786
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
787
+ "Headline",
788
+ /* @__PURE__ */ jsxRuntime.jsx(
789
+ "input",
790
+ {
791
+ value: document.title,
792
+ onChange: (e) => setDocument({ ...document, title: e.target.value })
793
+ }
794
+ )
795
+ ] }),
796
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
797
+ "Short introduction",
798
+ /* @__PURE__ */ jsxRuntime.jsx(
799
+ "input",
800
+ {
801
+ value: document.subtitle,
802
+ onChange: (e) => setDocument({ ...document, subtitle: e.target.value })
803
+ }
804
+ )
805
+ ] }),
806
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
807
+ "Suggested article text",
808
+ /* @__PURE__ */ jsxRuntime.jsx(
809
+ "textarea",
810
+ {
811
+ rows: 16,
812
+ value: document.content_markdown,
813
+ onChange: (e) => setDocument({
814
+ ...document,
815
+ content_markdown: e.target.value
816
+ })
817
+ }
818
+ )
819
+ ] }),
453
820
  /* @__PURE__ */ jsxRuntime.jsx(
454
- "textarea",
821
+ "button",
455
822
  {
456
- rows: 16,
457
- value: document.content_markdown,
458
- onChange: (e) => setDocument({
459
- ...document,
460
- content_markdown: e.target.value
461
- })
823
+ disabled: busy,
824
+ onClick: () => void send(
825
+ {
826
+ action: "PROPOSE_EDITS",
827
+ title: document.title,
828
+ subtitle: document.subtitle,
829
+ content_markdown: document.content_markdown,
830
+ message
831
+ },
832
+ "Your proposed changes have been sent to the newsroom."
833
+ ),
834
+ children: "Send proposed changes"
462
835
  }
463
836
  )
464
837
  ] }),
465
- /* @__PURE__ */ jsxRuntime.jsx(
466
- "button",
838
+ panel === "questions" && /* @__PURE__ */ jsxRuntime.jsxs(
839
+ "form",
467
840
  {
468
- disabled: busy,
469
- onClick: () => void send(
470
- {
471
- action: "PROPOSE_EDITS",
472
- title: document.title,
473
- subtitle: document.subtitle,
474
- content_markdown: document.content_markdown,
475
- message
476
- },
477
- "Your proposed changes have been sent to the newsroom."
478
- ),
479
- children: "Send proposed changes"
480
- }
481
- )
482
- ] }),
483
- panel === "questions" && /* @__PURE__ */ jsxRuntime.jsxs(
484
- "form",
485
- {
486
- onSubmit: (e) => {
487
- e.preventDefault();
488
- void send(
489
- { action: "ANSWER_QUESTIONS", answers },
490
- "Thank you for sharing. We'll take care of turning your answers into an updated draft."
491
- );
492
- },
493
- children: [
494
- /* @__PURE__ */ jsxRuntime.jsx("p", { children: review.questions.some((q) => q.required) ? "Only questions marked required must be answered. Share what you would like considered for publication." : "Completely optional. Even one answer can help us make the announcement more personal." }),
495
- review.questions.filter((q, i) => moreQuestions || i < 5 || q.required).map((q) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
496
- q.prompt,
497
- q.required ? " (required)" : " (optional)",
841
+ onSubmit: (e) => {
842
+ e.preventDefault();
843
+ void send(
844
+ { action: "ANSWER_QUESTIONS", answers },
845
+ "Thank you for sharing. We'll take care of turning your answers into an updated draft."
846
+ );
847
+ },
848
+ children: [
849
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: review.questions.some((q) => q.required) ? "Only questions marked required must be answered. Share what you would like considered for publication." : "Completely optional. Even one answer can help us make the announcement more personal." }),
850
+ review.questions.filter((q, i) => moreQuestions || i < 5 || q.required).map((q) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
851
+ q.prompt,
852
+ q.required ? " (required)" : " (optional)",
853
+ /* @__PURE__ */ jsxRuntime.jsx(
854
+ "textarea",
855
+ {
856
+ required: q.required,
857
+ rows: 3,
858
+ value: answers[q.id] || "",
859
+ onChange: (e) => setAnswers({ ...answers, [q.id]: e.target.value })
860
+ }
861
+ )
862
+ ] }, q.id)),
498
863
  /* @__PURE__ */ jsxRuntime.jsx(
499
- "textarea",
864
+ "button",
500
865
  {
501
- required: q.required,
502
- rows: 3,
503
- value: answers[q.id] || "",
504
- onChange: (e) => setAnswers({ ...answers, [q.id]: e.target.value })
866
+ className: "sm-review-primary",
867
+ disabled: busy || !Object.values(answers).some((a) => a.trim()),
868
+ children: "Send These Answers"
869
+ }
870
+ ),
871
+ !moreQuestions && review.questions.length > 5 && /* @__PURE__ */ jsxRuntime.jsx(
872
+ "button",
873
+ {
874
+ type: "button",
875
+ onClick: () => setMoreQuestions(true),
876
+ children: "Answer More Questions"
505
877
  }
506
878
  )
507
- ] }, q.id)),
879
+ ]
880
+ }
881
+ ),
882
+ panel === "metadata" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
883
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Caption and credit changes are proposals for the newsroom; historical captions remain intact." }),
884
+ ["caption", "credit", "alt"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
885
+ key,
508
886
  /* @__PURE__ */ jsxRuntime.jsx(
509
- "button",
887
+ "input",
510
888
  {
511
- className: "sm-review-primary",
512
- disabled: busy || !Object.values(answers).some((a) => a.trim()),
513
- children: "Send These Answers"
514
- }
515
- ),
516
- !moreQuestions && review.questions.length > 5 && /* @__PURE__ */ jsxRuntime.jsx(
517
- "button",
518
- {
519
- type: "button",
520
- onClick: () => setMoreQuestions(true),
521
- children: "Answer More Questions"
889
+ value: mediaInfo[key],
890
+ onChange: (e) => setMediaInfo({ ...mediaInfo, [key]: e.target.value })
522
891
  }
523
892
  )
524
- ]
525
- }
526
- ),
527
- panel === "metadata" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
528
- /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Caption and credit changes are proposals for the newsroom; historical captions remain intact." }),
529
- ["caption", "credit", "alt"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
530
- key,
893
+ ] }, key)),
531
894
  /* @__PURE__ */ jsxRuntime.jsx(
532
- "input",
895
+ "button",
533
896
  {
534
- value: mediaInfo[key],
535
- onChange: (e) => setMediaInfo({ ...mediaInfo, [key]: e.target.value })
897
+ disabled: busy,
898
+ onClick: () => void send(
899
+ {
900
+ action: "EDIT_MEDIA_METADATA",
901
+ contribution_id: drafts.entries.metadata?.data.editingMedia || editingMedia,
902
+ caption: mediaInfo.caption,
903
+ credit: mediaInfo.credit,
904
+ alt: mediaInfo.alt
905
+ },
906
+ "Your proposed media descriptions have been sent to the newsroom."
907
+ ),
908
+ children: "Propose caption / credit changes"
536
909
  }
537
910
  )
538
- ] }, key)),
539
- /* @__PURE__ */ jsxRuntime.jsx(
540
- "button",
911
+ ] }),
912
+ panel === "media" && /* @__PURE__ */ jsxRuntime.jsxs(
913
+ "form",
541
914
  {
542
- disabled: busy,
543
- onClick: () => void send(
544
- {
545
- action: "EDIT_MEDIA_METADATA",
546
- contribution_id: editingMedia,
547
- caption: mediaInfo.caption,
548
- credit: mediaInfo.credit,
549
- alt: mediaInfo.alt
550
- },
551
- "Your proposed media descriptions have been sent to the newsroom."
552
- ),
553
- children: "Propose caption / credit changes"
554
- }
555
- )
556
- ] }),
557
- panel === "media" && /* @__PURE__ */ jsxRuntime.jsxs(
558
- "form",
559
- {
560
- onSubmit: (e) => {
561
- e.preventDefault();
562
- void upload();
563
- },
564
- children: [
565
- /* @__PURE__ */ jsxRuntime.jsx("p", { children: "JPEG, PNG or WebP photos up to 12 MB; MP4 or WebM videos up to 25 MB. Media remain private until selected and approved by the newsroom." }),
566
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
567
- "Photograph or video",
568
- /* @__PURE__ */ jsxRuntime.jsx(
569
- "input",
570
- {
571
- type: "file",
572
- accept: "image/jpeg,image/png,image/webp,video/mp4,video/webm",
573
- required: true,
574
- onChange: (e) => setFile(e.target.files?.[0] || null)
575
- }
576
- )
577
- ] }),
578
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sm-review-help", children: "Choose a photograph and confirm permission below. All descriptions are optional; we can help with the caption." }),
579
- Object.entries({
580
- caption: "Caption (optional)",
581
- credit: "Photographer / credit (optional)"
582
- }).map(([key, label]) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
583
- label,
584
- /* @__PURE__ */ jsxRuntime.jsx(
585
- "input",
586
- {
587
- value: mediaInfo[key],
588
- onChange: (e) => setMediaInfo({ ...mediaInfo, [key]: e.target.value })
589
- }
590
- )
591
- ] }, key)),
592
- /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "sm-review-media-details", children: [
593
- /* @__PURE__ */ jsxRuntime.jsx("summary", { children: "Add more photograph details (optional)" }),
915
+ onSubmit: (e) => {
916
+ e.preventDefault();
917
+ void upload();
918
+ },
919
+ children: [
920
+ photoToReplace && /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Choose the photograph you'd like us to use instead. We'll review it and prepare an updated preview; the current photo stays in place until then." }),
921
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "JPEG, PNG or WebP photos up to 12 MB; MP4 or WebM videos up to 25 MB. Media remain private until selected and approved by the newsroom." }),
922
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-photo-picker", children: [
923
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "1. Choose a photo" }) }),
924
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => galleryRef.current?.click(), children: "Choose from my photos" }),
925
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => cameraRef.current?.click(), children: "Take a photo" }),
926
+ /* @__PURE__ */ jsxRuntime.jsx("input", { ref: cameraRef, "aria-label": "Take a photo", type: "file", accept: "image/jpeg,image/png,image/webp", capture: "environment", hidden: true, onChange: (e) => choosePhoto(e.target.files?.[0] || null) }),
927
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "sm-review-native-file", children: [
928
+ "Photograph or video",
929
+ /* @__PURE__ */ jsxRuntime.jsx(
930
+ "input",
931
+ {
932
+ ref: galleryRef,
933
+ type: "file",
934
+ accept: "image/jpeg,image/png,image/webp,video/mp4,video/webm",
935
+ onChange: (e) => choosePhoto(e.target.files?.[0] || null)
936
+ }
937
+ )
938
+ ] })
939
+ ] }),
940
+ file && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-selected-photo", children: [
941
+ pendingPhoto.preview && /* @__PURE__ */ jsxRuntime.jsx("img", { src: pendingPhoto.preview, alt: "Your selected photograph" }),
942
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: file.name }) }),
943
+ /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: pendingPhoto.saved ? "Photo kept on this device \u2014 not uploaded yet." : "Photo selected \u2014 not uploaded yet. Keep this page open." }),
944
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => {
945
+ void pendingPhoto.select(null);
946
+ setRights(false);
947
+ }, children: "Remove selected photo" })
948
+ ] }),
949
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sm-review-help", children: "Choose a photograph and confirm permission below. All descriptions are optional; we can help with the caption." }),
594
950
  Object.entries({
595
- who_is_pictured: "Who is pictured?",
596
- creator: "Photographer / creator",
597
- when_taken: "When was it taken?",
598
- where_taken: "Where was it taken?",
599
- alt: "Describe the photograph for readers who cannot see it"
951
+ caption: "Caption (optional)",
952
+ credit: "Photographer / credit (optional)"
600
953
  }).map(([key, label]) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
601
954
  label,
602
955
  /* @__PURE__ */ jsxRuntime.jsx(
603
956
  "input",
604
957
  {
605
958
  value: mediaInfo[key],
606
- onChange: (e) => setMediaInfo({
607
- ...mediaInfo,
608
- [key]: e.target.value
609
- })
959
+ onChange: (e) => setMediaInfo({ ...mediaInfo, [key]: e.target.value })
610
960
  }
611
961
  )
612
- ] }, key))
613
- ] }),
614
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "sm-review-check", children: [
962
+ ] }, key)),
963
+ /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "sm-review-media-details", children: [
964
+ /* @__PURE__ */ jsxRuntime.jsx("summary", { children: "Add more photograph details (optional)" }),
965
+ Object.entries({
966
+ who_is_pictured: "Who is pictured?",
967
+ creator: "Photographer / creator",
968
+ when_taken: "When was it taken?",
969
+ where_taken: "Where was it taken?",
970
+ alt: "Describe the photograph for readers who cannot see it"
971
+ }).map(([key, label]) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
972
+ label,
973
+ /* @__PURE__ */ jsxRuntime.jsx(
974
+ "input",
975
+ {
976
+ value: mediaInfo[key],
977
+ onChange: (e) => setMediaInfo({
978
+ ...mediaInfo,
979
+ [key]: e.target.value
980
+ })
981
+ }
982
+ )
983
+ ] }, key))
984
+ ] }),
985
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "2. Confirm permission" }) }),
986
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "sm-review-check", children: [
987
+ /* @__PURE__ */ jsxRuntime.jsx(
988
+ "input",
989
+ {
990
+ type: "checkbox",
991
+ required: true,
992
+ checked: rights,
993
+ onChange: (e) => setRights(e.target.checked)
994
+ }
995
+ ),
996
+ "I confirm that I have the right or permission to provide this media to ",
997
+ review.publication.name,
998
+ " for editorial publication."
999
+ ] }),
1000
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `sm-review-send-bar${rights && file ? " is-ready" : ""}`, children: [
1001
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: busy ? "Uploading your photo. Please keep this page open\u2026" : !file ? "Choose a photo above to continue." : !rights ? "Tick the permission box above, then upload your photo." : "Ready. Tap Upload privately to send this photo to the newsroom." }),
1002
+ busy && /* @__PURE__ */ jsxRuntime.jsx("progress", { "aria-label": "Uploading photo" }),
1003
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "sm-review-primary", disabled: busy || !rights || !file, children: busy ? "Uploading\u2026" : "Upload privately" })
1004
+ ] })
1005
+ ]
1006
+ }
1007
+ ),
1008
+ ["approve", "changes", "comment", "decline"].includes(panel) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1009
+ panel === "approve" && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
1010
+ "You are approving version ",
1011
+ review.revision.number,
1012
+ ". Changes to the article require a new approval.",
1013
+ review.review_policy === "SUBJECT_APPROVAL_AUTO_PUBLISH" ? " The editor has configured this version to publish or schedule after the required approvals are received." : " The newsroom will complete its editorial workflow before publication."
1014
+ ] }),
1015
+ panel === "decline" && /* @__PURE__ */ jsxRuntime.jsx("p", { children: ["FACT_CHECK", "CONTRIBUTOR_REVIEW"].includes(
1016
+ review.review_policy
1017
+ ) ? "You can decline this review. The newsroom may still publish its reporting." : "Declining participation prevents publication of this voluntary announcement." }),
1018
+ panel === "comment" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1019
+ "Comment on",
1020
+ /* @__PURE__ */ jsxRuntime.jsxs(
1021
+ "select",
1022
+ {
1023
+ value: currentDraft?.data.commentTarget || commentTarget,
1024
+ onChange: (e) => {
1025
+ setCommentTarget(e.target.value);
1026
+ if (message) drafts.set("comment", { ...currentDraft?.data, commentTarget: e.target.value });
1027
+ },
1028
+ children: [
1029
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "STORY", children: "Entire story" }),
1030
+ review.revision.document.content_markdown.split(/\n\s*\n/).slice(0, 200).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: `PARAGRAPH:${i + 1}`, children: [
1031
+ "Paragraph ",
1032
+ i + 1
1033
+ ] }, i)),
1034
+ review.revision.document.media.map((m, i) => /* @__PURE__ */ jsxRuntime.jsxs(
1035
+ "option",
1036
+ {
1037
+ value: `MEDIA:${m.contribution_id}`,
1038
+ children: [
1039
+ "Photo / caption ",
1040
+ i + 1
1041
+ ]
1042
+ },
1043
+ m.contribution_id
1044
+ )),
1045
+ review.questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: `QUESTION:${q.id}`, children: q.prompt }, q.id))
1046
+ ]
1047
+ }
1048
+ )
1049
+ ] }),
1050
+ panel === "changes" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1051
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1052
+ "What would you like to change?",
615
1053
  /* @__PURE__ */ jsxRuntime.jsx(
616
- "input",
1054
+ "select",
617
1055
  {
618
- type: "checkbox",
619
- required: true,
620
- checked: rights,
621
- onChange: (e) => setRights(e.target.checked)
1056
+ value: correctionKind,
1057
+ onChange: (e) => setCorrectionKind(e.target.value),
1058
+ children: [
1059
+ "Correct a detail",
1060
+ "Correct a name",
1061
+ "Correct a professional / education detail",
1062
+ "Remove something",
1063
+ "Rewrite something",
1064
+ "Add something",
1065
+ "General comment"
1066
+ ].map((kind) => /* @__PURE__ */ jsxRuntime.jsx("option", { children: kind }, kind))
622
1067
  }
623
- ),
624
- "I confirm that I have the right or permission to provide this media to ",
625
- review.publication.name,
626
- " for editorial publication."
1068
+ )
627
1069
  ] }),
628
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy || !rights || !file, children: "Upload privately" })
629
- ]
630
- }
631
- ),
632
- ["approve", "changes", "comment", "decline"].includes(panel) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
633
- panel === "approve" && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
634
- "You are approving version ",
635
- review.revision.number,
636
- ". Changes to the article require a new approval.",
637
- review.review_policy === "SUBJECT_APPROVAL_AUTO_PUBLISH" ? " The editor has configured this version to publish or schedule after the required approvals are received." : " The newsroom will complete its editorial workflow before publication."
638
- ] }),
639
- panel === "decline" && /* @__PURE__ */ jsxRuntime.jsx("p", { children: ["FACT_CHECK", "CONTRIBUTOR_REVIEW"].includes(
640
- review.review_policy
641
- ) ? "You can decline this review. The newsroom may still publish its reporting." : "Declining participation prevents publication of this voluntary announcement." }),
642
- panel === "comment" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
643
- "Comment on",
644
- /* @__PURE__ */ jsxRuntime.jsxs(
645
- "select",
646
- {
647
- value: commentTarget,
648
- onChange: (e) => setCommentTarget(e.target.value),
649
- children: [
650
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "STORY", children: "Entire story" }),
651
- review.revision.document.content_markdown.split(/\n\s*\n/).slice(0, 200).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: `PARAGRAPH:${i + 1}`, children: [
652
- "Paragraph ",
653
- i + 1
654
- ] }, i)),
655
- review.revision.document.media.map((m, i) => /* @__PURE__ */ jsxRuntime.jsxs(
656
- "option",
657
- {
658
- value: `MEDIA:${m.contribution_id}`,
659
- children: [
660
- "Photo / caption ",
661
- i + 1
662
- ]
663
- },
664
- m.contribution_id
665
- )),
666
- review.questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: `QUESTION:${q.id}`, children: q.prompt }, q.id))
667
- ]
668
- }
669
- )
670
- ] }),
671
- panel === "changes" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1070
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "A sentence or two is enough. We'll take care of the wording." }),
1071
+ can("PROPOSE_EDITS") && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("edits"), children: "Or edit the wording directly" })
1072
+ ] }),
672
1073
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
673
- "What would you like to change?",
1074
+ panel === "changes" ? "What should change?" : "Message (optional)",
674
1075
  /* @__PURE__ */ jsxRuntime.jsx(
675
- "select",
1076
+ "textarea",
676
1077
  {
677
- value: correctionKind,
678
- onChange: (e) => setCorrectionKind(e.target.value),
679
- children: [
680
- "Correct a detail",
681
- "Correct a name",
682
- "Correct a professional / education detail",
683
- "Remove something",
684
- "Rewrite something",
685
- "Add something",
686
- "General comment"
687
- ].map((kind) => /* @__PURE__ */ jsxRuntime.jsx("option", { children: kind }, kind))
1078
+ rows: 5,
1079
+ value: message,
1080
+ onChange: (e) => setMessage(e.target.value)
688
1081
  }
689
1082
  )
690
1083
  ] }),
691
- /* @__PURE__ */ jsxRuntime.jsx("p", { children: "A sentence or two is enough. We'll take care of the wording." }),
692
- can("PROPOSE_EDITS") && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("edits"), children: "Or edit the wording directly" })
693
- ] }),
694
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
695
- panel === "changes" ? "What should change?" : "Message (optional)",
696
1084
  /* @__PURE__ */ jsxRuntime.jsx(
697
- "textarea",
1085
+ "button",
698
1086
  {
699
- rows: 5,
700
- value: message,
701
- onChange: (e) => setMessage(e.target.value)
1087
+ disabled: busy || panel === "changes" && !message.trim() || panel === "comment" && !message.trim(),
1088
+ onClick: () => void send(
1089
+ {
1090
+ action: panel === "approve" ? "APPROVE" : panel === "changes" ? "REQUEST_CHANGES" : panel === "decline" ? "DECLINE" : "COMMENT",
1091
+ message: panel === "changes" ? `${correctionKind}: ${message}` : message || null,
1092
+ ...panel === "comment" ? {
1093
+ target: {
1094
+ kind: (currentDraft?.data.commentTarget || commentTarget).split(":")[0],
1095
+ reference: (currentDraft?.data.commentTarget || commentTarget).split(":")[1] || null
1096
+ }
1097
+ } : {}
1098
+ },
1099
+ panel === "approve" ? celebration && finalEditor ? approvalThanks : "Your approval has been recorded for this version." : panel === "decline" ? "Thank you for letting us know. The newsroom has received your decision." : "Your message has been sent to the newsroom."
1100
+ ),
1101
+ children: panel === "approve" ? `Approve version ${review.revision.number}` : panel === "changes" ? "Send change request" : panel === "decline" ? "Prefer not to publish" : "Send comment"
702
1102
  }
703
1103
  )
704
1104
  ] }),
705
- /* @__PURE__ */ jsxRuntime.jsx(
706
- "button",
707
- {
708
- disabled: busy || panel === "changes" && !message.trim() || panel === "comment" && !message.trim(),
709
- onClick: () => void send(
1105
+ panel === "history" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1106
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Only revisions selected by the editor are visible here." }),
1107
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1108
+ "Compare from",
1109
+ /* @__PURE__ */ jsxRuntime.jsxs(
1110
+ "select",
710
1111
  {
711
- action: panel === "approve" ? "APPROVE" : panel === "changes" ? "REQUEST_CHANGES" : panel === "decline" ? "DECLINE" : "COMMENT",
712
- message: panel === "changes" ? `${correctionKind}: ${message}` : message || null,
713
- ...panel === "comment" ? {
714
- target: {
715
- kind: commentTarget.split(":")[0],
716
- reference: commentTarget.split(":")[1] || null
717
- }
718
- } : {}
719
- },
720
- panel === "approve" ? celebration && finalEditor ? approvalThanks : "Your approval has been recorded for this version." : panel === "decline" ? "Thank you for letting us know. The newsroom has received your decision." : "Your message has been sent to the newsroom."
721
- ),
722
- children: panel === "approve" ? `Approve version ${review.revision.number}` : panel === "changes" ? "Send change request" : panel === "decline" ? "Prefer not to publish" : "Send comment"
723
- }
724
- )
725
- ] }),
726
- panel === "history" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
727
- /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Only revisions selected by the editor are visible here." }),
728
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
729
- "Compare from",
730
- /* @__PURE__ */ jsxRuntime.jsxs(
731
- "select",
732
- {
733
- value: compare,
734
- onChange: (e) => setCompare(e.target.value),
735
- children: [
736
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Choose a shared revision" }),
737
- revisions.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: r.id, children: [
1112
+ value: compare,
1113
+ onChange: (e) => setCompare(e.target.value),
1114
+ children: [
1115
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Choose a shared revision" }),
1116
+ revisions.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: r.id, children: [
1117
+ "Version ",
1118
+ r.number
1119
+ ] }, r.id))
1120
+ ]
1121
+ }
1122
+ )
1123
+ ] }),
1124
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1125
+ "Compare to",
1126
+ /* @__PURE__ */ jsxRuntime.jsx(
1127
+ "select",
1128
+ {
1129
+ value: compareTo || review.revision.id,
1130
+ onChange: (e) => setCompareTo(e.target.value),
1131
+ children: revisions.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: r.id, children: [
738
1132
  "Version ",
739
1133
  r.number
740
1134
  ] }, r.id))
741
- ]
742
- }
743
- )
744
- ] }),
745
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
746
- "Compare to",
747
- /* @__PURE__ */ jsxRuntime.jsx(
748
- "select",
749
- {
750
- value: compareTo || review.revision.id,
751
- onChange: (e) => setCompareTo(e.target.value),
752
- children: revisions.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: r.id, children: [
753
- "Version ",
754
- r.number
755
- ] }, r.id))
756
- }
757
- )
758
- ] }),
759
- selected && selectedAfter && /* @__PURE__ */ jsxRuntime.jsx(ReviewDiff, { before: selected, after: selectedAfter }),
760
- /* @__PURE__ */ jsxRuntime.jsxs("details", { children: [
761
- /* @__PURE__ */ jsxRuntime.jsx("summary", { children: "Read selected revision" }),
762
- /* @__PURE__ */ jsxRuntime.jsx("h3", { children: selected?.document.title }),
763
- /* @__PURE__ */ jsxRuntime.jsx("p", { children: selected?.document.subtitle }),
764
- /* @__PURE__ */ jsxRuntime.jsx(EditorialNotices, { editorial: selected?.editorial }),
765
- /* @__PURE__ */ jsxRuntime.jsx(
766
- "div",
767
- {
768
- className: "sm-review-prose",
769
- dangerouslySetInnerHTML: {
770
- __html: selected?.content_html || ""
771
1135
  }
772
- }
773
- ),
774
- selected?.document.media.map((m) => /* @__PURE__ */ jsxRuntime.jsx(PrivatePhoto, { media: m, base }, m.file_id))
1136
+ )
1137
+ ] }),
1138
+ selected && selectedAfter && /* @__PURE__ */ jsxRuntime.jsx(ReviewDiff, { before: selected, after: selectedAfter }),
1139
+ /* @__PURE__ */ jsxRuntime.jsxs("details", { children: [
1140
+ /* @__PURE__ */ jsxRuntime.jsx("summary", { children: "Read selected revision" }),
1141
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: selected?.document.title }),
1142
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: selected?.document.subtitle }),
1143
+ /* @__PURE__ */ jsxRuntime.jsx(EditorialNotices, { editorial: selected?.editorial }),
1144
+ /* @__PURE__ */ jsxRuntime.jsx(
1145
+ "div",
1146
+ {
1147
+ className: "sm-review-prose",
1148
+ dangerouslySetInnerHTML: {
1149
+ __html: selected?.content_html || ""
1150
+ }
1151
+ }
1152
+ ),
1153
+ selected?.document.media.map((m) => /* @__PURE__ */ jsxRuntime.jsx(PrivatePhoto, { media: m, base }, m.file_id))
1154
+ ] })
775
1155
  ] })
776
1156
  ] })
777
1157
  ]
@@ -813,12 +1193,7 @@ function StoryReview({
813
1193
  {
814
1194
  onClick: () => {
815
1195
  setEditingMedia(c.id);
816
- setMediaInfo({
817
- ...mediaInfo,
818
- caption: c.payload.caption || "",
819
- credit: c.payload.credit || "",
820
- alt: c.payload.caption || ""
821
- });
1196
+ if (!hasDraft(drafts.entries.metadata?.data)) drafts.set("metadata", { editingMedia: c.id, mediaInfo: { ...emptyMediaInfo, caption: c.payload.caption || "", credit: c.payload.credit || "", alt: c.payload.alt || "" } });
822
1197
  setPanel("metadata");
823
1198
  },
824
1199
  children: "Suggest caption / credit changes"
@@ -847,7 +1222,7 @@ function StoryReview({
847
1222
  publicationFooter
848
1223
  ] });
849
1224
  }
850
- function PrivatePhoto({ base, media }) {
1225
+ function PrivatePhoto({ base, media, onChange, disabled }) {
851
1226
  const [url, setUrl] = react.useState("");
852
1227
  const [video, setVideo] = react.useState(false);
853
1228
  react.useEffect(() => {
@@ -881,9 +1256,12 @@ function PrivatePhoto({ base, media }) {
881
1256
  "aria-label": media.alt
882
1257
  }
883
1258
  ) : /* @__PURE__ */ jsxRuntime.jsx("img", { src: url, alt: media.alt, referrerPolicy: "no-referrer" }) : /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Media unavailable." }),
884
- /* @__PURE__ */ jsxRuntime.jsxs("figcaption", { children: [
885
- media.caption,
886
- media.credit && ` Photo: ${media.credit}`
1259
+ /* @__PURE__ */ jsxRuntime.jsxs("figcaption", { className: "sm-review-photo-details", children: [
1260
+ (media.caption || media.credit) && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1261
+ media.caption,
1262
+ media.credit && ` Photo: ${media.credit}`
1263
+ ] }),
1264
+ onChange && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "sm-review-change-photo", type: "button", disabled, onClick: onChange, children: "Change photo" })
887
1265
  ] })
888
1266
  ] });
889
1267
  }