@scalemule/news-publication 0.0.29 → 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.
- package/dist/story-review.cjs +662 -334
- package/dist/story-review.js +662 -334
- package/package.json +1 -1
- package/story-review.css +23 -0
package/dist/story-review.cjs
CHANGED
|
@@ -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,15 +293,29 @@ 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 [
|
|
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");
|
|
80
311
|
const [replacementPhoto, setReplacementPhoto] = react.useState(null);
|
|
312
|
+
const photoToReplace = pendingPhoto.photo?.replacement || replacementPhoto;
|
|
81
313
|
function openMedia(photo = null) {
|
|
82
|
-
setReplacementPhoto(photo);
|
|
83
|
-
|
|
314
|
+
setReplacementPhoto(file ? pendingPhoto.photo?.replacement || null : photo);
|
|
315
|
+
if (file) {
|
|
316
|
+
setPanel("media");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
84
319
|
setRights(false);
|
|
85
320
|
setPanel("media");
|
|
86
321
|
}
|
|
@@ -93,15 +328,32 @@ function StoryReview({
|
|
|
93
328
|
responseRef.current?.focus({ preventScroll: true });
|
|
94
329
|
}
|
|
95
330
|
}, [panel]);
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
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";
|
|
105
357
|
async function request(path = "", body) {
|
|
106
358
|
const form = body instanceof FormData;
|
|
107
359
|
const r = await fetch(base + path, {
|
|
@@ -113,9 +365,7 @@ function StoryReview({
|
|
|
113
365
|
});
|
|
114
366
|
const result = await r.json();
|
|
115
367
|
if (!r.ok || result.success === false)
|
|
116
|
-
throw new Error(
|
|
117
|
-
result.error?.message || "This private review is unavailable. Please contact the newsroom."
|
|
118
|
-
);
|
|
368
|
+
throw Object.assign(new Error(result.error?.message || "This private review is unavailable. Please contact the newsroom."), { status: r.status });
|
|
119
369
|
return result.data ?? result;
|
|
120
370
|
}
|
|
121
371
|
async function load() {
|
|
@@ -126,7 +376,6 @@ function StoryReview({
|
|
|
126
376
|
} else {
|
|
127
377
|
setVerification(false);
|
|
128
378
|
setReview(data);
|
|
129
|
-
setDocument(data.revision.document);
|
|
130
379
|
}
|
|
131
380
|
}
|
|
132
381
|
react.useEffect(() => {
|
|
@@ -146,7 +395,6 @@ function StoryReview({
|
|
|
146
395
|
} else {
|
|
147
396
|
setVerification(false);
|
|
148
397
|
setReview(data);
|
|
149
|
-
setDocument(data.revision.document);
|
|
150
398
|
}
|
|
151
399
|
}
|
|
152
400
|
} catch (e) {
|
|
@@ -159,16 +407,27 @@ function StoryReview({
|
|
|
159
407
|
};
|
|
160
408
|
}, [sessionId]);
|
|
161
409
|
async function send(action, success) {
|
|
162
|
-
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
|
+
}
|
|
163
416
|
setBusy(true);
|
|
164
417
|
setError("");
|
|
165
418
|
setNotice("");
|
|
166
419
|
try {
|
|
167
|
-
|
|
168
|
-
await
|
|
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
|
+
}
|
|
169
429
|
setNotice(success);
|
|
170
430
|
setPanel("article");
|
|
171
|
-
setMessage("");
|
|
172
431
|
} catch (e) {
|
|
173
432
|
setError(
|
|
174
433
|
e instanceof Error ? e.message : "Could not save your response."
|
|
@@ -189,12 +448,30 @@ function StoryReview({
|
|
|
189
448
|
}
|
|
190
449
|
}, [approved, finalEditor]);
|
|
191
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
|
+
}
|
|
192
456
|
if (celebration && finalEditor && !review?.questions.some((q) => q.required))
|
|
193
457
|
void send({ action: "APPROVE", message: null }, approvalThanks);
|
|
194
458
|
else setPanel("approve");
|
|
195
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
|
+
}
|
|
196
472
|
async function upload() {
|
|
197
|
-
if (!review || !file || !rights) return;
|
|
473
|
+
if (!review || !file || !rights || busy) return;
|
|
474
|
+
const replacementPhoto2 = pendingPhoto.photo?.replacement;
|
|
198
475
|
setBusy(true);
|
|
199
476
|
setError("");
|
|
200
477
|
try {
|
|
@@ -206,7 +483,7 @@ function StoryReview({
|
|
|
206
483
|
);
|
|
207
484
|
const received = await request("/media", form);
|
|
208
485
|
let uploadNotice = "Media received privately. The newsroom will review it before publication.";
|
|
209
|
-
if (
|
|
486
|
+
if (replacementPhoto2) {
|
|
210
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.";
|
|
211
488
|
if (can("COMMENT")) {
|
|
212
489
|
try {
|
|
@@ -216,7 +493,7 @@ function StoryReview({
|
|
|
216
493
|
message: "Please use my newly uploaded photograph in place of this photo.",
|
|
217
494
|
target: {
|
|
218
495
|
kind: "PHOTO",
|
|
219
|
-
reference:
|
|
496
|
+
reference: replacementPhoto2.contribution_id,
|
|
220
497
|
replacement_contribution_id: received.id
|
|
221
498
|
}
|
|
222
499
|
});
|
|
@@ -227,8 +504,17 @@ function StoryReview({
|
|
|
227
504
|
uploadNotice = "Your photo was uploaded privately for newsroom review. The newsroom will select the photo for the next preview.";
|
|
228
505
|
}
|
|
229
506
|
}
|
|
230
|
-
await
|
|
231
|
-
|
|
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
|
+
}
|
|
232
518
|
setRights(false);
|
|
233
519
|
setNotice(uploadNotice);
|
|
234
520
|
setReplacementPhoto(null);
|
|
@@ -257,7 +543,7 @@ function StoryReview({
|
|
|
257
543
|
] }) }),
|
|
258
544
|
/* @__PURE__ */ jsxRuntime.jsxs("main", { className: `sm-story-review${celebration ? " sm-review-celebration" : ""}`, children: [
|
|
259
545
|
error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "sm-review-alert", children: error }),
|
|
260
|
-
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 }),
|
|
261
547
|
verification && /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "sm-review-form", children: [
|
|
262
548
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Verify your email" }),
|
|
263
549
|
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "The editor requires verification using the email address on your invitation. Your story remains private until verified." }),
|
|
@@ -460,6 +746,15 @@ function StoryReview({
|
|
|
460
746
|
can("VIEW_SELECTED_HISTORY") && review.history.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("history"), children: "See previous previews" }),
|
|
461
747
|
can("DOWNLOAD_PREVIEW") && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => window.print(), children: "Print / save preview" })
|
|
462
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
|
+
] }),
|
|
463
758
|
panel !== "article" && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
464
759
|
"section",
|
|
465
760
|
{
|
|
@@ -469,356 +764,394 @@ function StoryReview({
|
|
|
469
764
|
"aria-label": "Your response",
|
|
470
765
|
children: [
|
|
471
766
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-form-heading", children: [
|
|
472
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: panel === "approve" ? "Approve this exact draft" : panel === "edits" ? "Propose changes" : panel === "media" ?
|
|
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" }),
|
|
473
768
|
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Close" })
|
|
474
769
|
] }),
|
|
475
|
-
panel
|
|
476
|
-
/* @__PURE__ */ jsxRuntime.
|
|
477
|
-
|
|
478
|
-
"
|
|
479
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
] }),
|
|
499
820
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
500
|
-
"
|
|
821
|
+
"button",
|
|
501
822
|
{
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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"
|
|
508
835
|
}
|
|
509
836
|
)
|
|
510
837
|
] }),
|
|
511
|
-
/* @__PURE__ */ jsxRuntime.
|
|
512
|
-
"
|
|
838
|
+
panel === "questions" && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
839
|
+
"form",
|
|
513
840
|
{
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
action: "
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
},
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
"Thank you for sharing. We'll take care of turning your answers into an updated draft."
|
|
537
|
-
);
|
|
538
|
-
},
|
|
539
|
-
children: [
|
|
540
|
-
/* @__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." }),
|
|
541
|
-
review.questions.filter((q, i) => moreQuestions || i < 5 || q.required).map((q) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
542
|
-
q.prompt,
|
|
543
|
-
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)),
|
|
544
863
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
545
|
-
"
|
|
864
|
+
"button",
|
|
546
865
|
{
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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"
|
|
551
877
|
}
|
|
552
878
|
)
|
|
553
|
-
]
|
|
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,
|
|
554
886
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
555
|
-
"
|
|
556
|
-
{
|
|
557
|
-
className: "sm-review-primary",
|
|
558
|
-
disabled: busy || !Object.values(answers).some((a) => a.trim()),
|
|
559
|
-
children: "Send These Answers"
|
|
560
|
-
}
|
|
561
|
-
),
|
|
562
|
-
!moreQuestions && review.questions.length > 5 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
563
|
-
"button",
|
|
887
|
+
"input",
|
|
564
888
|
{
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
children: "Answer More Questions"
|
|
889
|
+
value: mediaInfo[key],
|
|
890
|
+
onChange: (e) => setMediaInfo({ ...mediaInfo, [key]: e.target.value })
|
|
568
891
|
}
|
|
569
892
|
)
|
|
570
|
-
]
|
|
571
|
-
}
|
|
572
|
-
),
|
|
573
|
-
panel === "metadata" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
574
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Caption and credit changes are proposals for the newsroom; historical captions remain intact." }),
|
|
575
|
-
["caption", "credit", "alt"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
576
|
-
key,
|
|
893
|
+
] }, key)),
|
|
577
894
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
578
|
-
"
|
|
895
|
+
"button",
|
|
579
896
|
{
|
|
580
|
-
|
|
581
|
-
|
|
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"
|
|
582
909
|
}
|
|
583
910
|
)
|
|
584
|
-
] }
|
|
585
|
-
/* @__PURE__ */ jsxRuntime.
|
|
586
|
-
"
|
|
911
|
+
] }),
|
|
912
|
+
panel === "media" && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
913
|
+
"form",
|
|
587
914
|
{
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
"
|
|
617
|
-
{
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
)
|
|
624
|
-
] }),
|
|
625
|
-
/* @__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." }),
|
|
626
|
-
Object.entries({
|
|
627
|
-
caption: "Caption (optional)",
|
|
628
|
-
credit: "Photographer / credit (optional)"
|
|
629
|
-
}).map(([key, label]) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
630
|
-
label,
|
|
631
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
632
|
-
"input",
|
|
633
|
-
{
|
|
634
|
-
value: mediaInfo[key],
|
|
635
|
-
onChange: (e) => setMediaInfo({ ...mediaInfo, [key]: e.target.value })
|
|
636
|
-
}
|
|
637
|
-
)
|
|
638
|
-
] }, key)),
|
|
639
|
-
/* @__PURE__ */ jsxRuntime.jsxs("details", { className: "sm-review-media-details", children: [
|
|
640
|
-
/* @__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." }),
|
|
641
950
|
Object.entries({
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
when_taken: "When was it taken?",
|
|
645
|
-
where_taken: "Where was it taken?",
|
|
646
|
-
alt: "Describe the photograph for readers who cannot see it"
|
|
951
|
+
caption: "Caption (optional)",
|
|
952
|
+
credit: "Photographer / credit (optional)"
|
|
647
953
|
}).map(([key, label]) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
648
954
|
label,
|
|
649
955
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
650
956
|
"input",
|
|
651
957
|
{
|
|
652
958
|
value: mediaInfo[key],
|
|
653
|
-
onChange: (e) => setMediaInfo({
|
|
654
|
-
...mediaInfo,
|
|
655
|
-
[key]: e.target.value
|
|
656
|
-
})
|
|
959
|
+
onChange: (e) => setMediaInfo({ ...mediaInfo, [key]: e.target.value })
|
|
657
960
|
}
|
|
658
961
|
)
|
|
659
|
-
] }, key))
|
|
660
|
-
|
|
661
|
-
|
|
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?",
|
|
662
1053
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
663
|
-
"
|
|
1054
|
+
"select",
|
|
664
1055
|
{
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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))
|
|
669
1067
|
}
|
|
670
|
-
)
|
|
671
|
-
"I confirm that I have the right or permission to provide this media to ",
|
|
672
|
-
review.publication.name,
|
|
673
|
-
" for editorial publication."
|
|
1068
|
+
)
|
|
674
1069
|
] }),
|
|
675
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
),
|
|
679
|
-
["approve", "changes", "comment", "decline"].includes(panel) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
680
|
-
panel === "approve" && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
681
|
-
"You are approving version ",
|
|
682
|
-
review.revision.number,
|
|
683
|
-
". Changes to the article require a new approval.",
|
|
684
|
-
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."
|
|
685
|
-
] }),
|
|
686
|
-
panel === "decline" && /* @__PURE__ */ jsxRuntime.jsx("p", { children: ["FACT_CHECK", "CONTRIBUTOR_REVIEW"].includes(
|
|
687
|
-
review.review_policy
|
|
688
|
-
) ? "You can decline this review. The newsroom may still publish its reporting." : "Declining participation prevents publication of this voluntary announcement." }),
|
|
689
|
-
panel === "comment" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
690
|
-
"Comment on",
|
|
691
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
692
|
-
"select",
|
|
693
|
-
{
|
|
694
|
-
value: commentTarget,
|
|
695
|
-
onChange: (e) => setCommentTarget(e.target.value),
|
|
696
|
-
children: [
|
|
697
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "STORY", children: "Entire story" }),
|
|
698
|
-
review.revision.document.content_markdown.split(/\n\s*\n/).slice(0, 200).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: `PARAGRAPH:${i + 1}`, children: [
|
|
699
|
-
"Paragraph ",
|
|
700
|
-
i + 1
|
|
701
|
-
] }, i)),
|
|
702
|
-
review.revision.document.media.map((m, i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
703
|
-
"option",
|
|
704
|
-
{
|
|
705
|
-
value: `MEDIA:${m.contribution_id}`,
|
|
706
|
-
children: [
|
|
707
|
-
"Photo / caption ",
|
|
708
|
-
i + 1
|
|
709
|
-
]
|
|
710
|
-
},
|
|
711
|
-
m.contribution_id
|
|
712
|
-
)),
|
|
713
|
-
review.questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: `QUESTION:${q.id}`, children: q.prompt }, q.id))
|
|
714
|
-
]
|
|
715
|
-
}
|
|
716
|
-
)
|
|
717
|
-
] }),
|
|
718
|
-
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
|
+
] }),
|
|
719
1073
|
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
720
|
-
"
|
|
1074
|
+
panel === "changes" ? "What should change?" : "Message (optional)",
|
|
721
1075
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
722
|
-
"
|
|
1076
|
+
"textarea",
|
|
723
1077
|
{
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
"Correct a detail",
|
|
728
|
-
"Correct a name",
|
|
729
|
-
"Correct a professional / education detail",
|
|
730
|
-
"Remove something",
|
|
731
|
-
"Rewrite something",
|
|
732
|
-
"Add something",
|
|
733
|
-
"General comment"
|
|
734
|
-
].map((kind) => /* @__PURE__ */ jsxRuntime.jsx("option", { children: kind }, kind))
|
|
1078
|
+
rows: 5,
|
|
1079
|
+
value: message,
|
|
1080
|
+
onChange: (e) => setMessage(e.target.value)
|
|
735
1081
|
}
|
|
736
1082
|
)
|
|
737
1083
|
] }),
|
|
738
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "A sentence or two is enough. We'll take care of the wording." }),
|
|
739
|
-
can("PROPOSE_EDITS") && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("edits"), children: "Or edit the wording directly" })
|
|
740
|
-
] }),
|
|
741
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
742
|
-
panel === "changes" ? "What should change?" : "Message (optional)",
|
|
743
1084
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
744
|
-
"
|
|
1085
|
+
"button",
|
|
745
1086
|
{
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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"
|
|
749
1102
|
}
|
|
750
1103
|
)
|
|
751
1104
|
] }),
|
|
752
|
-
/* @__PURE__ */ jsxRuntime.
|
|
753
|
-
"
|
|
754
|
-
{
|
|
755
|
-
|
|
756
|
-
|
|
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",
|
|
757
1111
|
{
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
)
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
"select",
|
|
779
|
-
{
|
|
780
|
-
value: compare,
|
|
781
|
-
onChange: (e) => setCompare(e.target.value),
|
|
782
|
-
children: [
|
|
783
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Choose a shared revision" }),
|
|
784
|
-
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: [
|
|
785
1132
|
"Version ",
|
|
786
1133
|
r.number
|
|
787
1134
|
] }, r.id))
|
|
788
|
-
]
|
|
789
|
-
}
|
|
790
|
-
)
|
|
791
|
-
] }),
|
|
792
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
793
|
-
"Compare to",
|
|
794
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
795
|
-
"select",
|
|
796
|
-
{
|
|
797
|
-
value: compareTo || review.revision.id,
|
|
798
|
-
onChange: (e) => setCompareTo(e.target.value),
|
|
799
|
-
children: revisions.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: r.id, children: [
|
|
800
|
-
"Version ",
|
|
801
|
-
r.number
|
|
802
|
-
] }, r.id))
|
|
803
|
-
}
|
|
804
|
-
)
|
|
805
|
-
] }),
|
|
806
|
-
selected && selectedAfter && /* @__PURE__ */ jsxRuntime.jsx(ReviewDiff, { before: selected, after: selectedAfter }),
|
|
807
|
-
/* @__PURE__ */ jsxRuntime.jsxs("details", { children: [
|
|
808
|
-
/* @__PURE__ */ jsxRuntime.jsx("summary", { children: "Read selected revision" }),
|
|
809
|
-
/* @__PURE__ */ jsxRuntime.jsx("h3", { children: selected?.document.title }),
|
|
810
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { children: selected?.document.subtitle }),
|
|
811
|
-
/* @__PURE__ */ jsxRuntime.jsx(EditorialNotices, { editorial: selected?.editorial }),
|
|
812
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
813
|
-
"div",
|
|
814
|
-
{
|
|
815
|
-
className: "sm-review-prose",
|
|
816
|
-
dangerouslySetInnerHTML: {
|
|
817
|
-
__html: selected?.content_html || ""
|
|
818
1135
|
}
|
|
819
|
-
|
|
820
|
-
),
|
|
821
|
-
selected
|
|
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
|
+
] })
|
|
822
1155
|
] })
|
|
823
1156
|
] })
|
|
824
1157
|
]
|
|
@@ -860,12 +1193,7 @@ function StoryReview({
|
|
|
860
1193
|
{
|
|
861
1194
|
onClick: () => {
|
|
862
1195
|
setEditingMedia(c.id);
|
|
863
|
-
|
|
864
|
-
...mediaInfo,
|
|
865
|
-
caption: c.payload.caption || "",
|
|
866
|
-
credit: c.payload.credit || "",
|
|
867
|
-
alt: c.payload.caption || ""
|
|
868
|
-
});
|
|
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 || "" } });
|
|
869
1197
|
setPanel("metadata");
|
|
870
1198
|
},
|
|
871
1199
|
children: "Suggest caption / credit changes"
|