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