spaps-issue-reporting-react 0.4.0 → 0.4.2

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/index.mjs CHANGED
@@ -6,13 +6,15 @@ import {
6
6
  BugBeetle,
7
7
  CheckCircle,
8
8
  Circle,
9
+ Image,
9
10
  Microphone,
10
11
  Spinner,
11
12
  TextT,
13
+ Trash,
12
14
  X
13
15
  } from "@phosphor-icons/react";
14
16
  import { formatDistanceToNow } from "date-fns";
15
- import React2, { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
17
+ import React2, { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
16
18
 
17
19
  // src/provider.tsx
18
20
  import {
@@ -44,6 +46,13 @@ import { jsx } from "react/jsx-runtime";
44
46
  var LIST_LIMIT = 200;
45
47
  var NOTE_MIN_LENGTH = 10;
46
48
  var NOTE_MAX_LENGTH = 2e3;
49
+ var ATTACHMENT_MAX_COUNT = 5;
50
+ var ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
51
+ var ATTACHMENT_ALLOWED_MIMES = /* @__PURE__ */ new Set([
52
+ "image/png",
53
+ "image/jpeg",
54
+ "image/webp"
55
+ ]);
47
56
  var DEFAULT_INPUT_MODES = ["text"];
48
57
  var DEFAULT_VOICE_PROVIDER = "elevenlabs_scribe_realtime";
49
58
  var DEFAULT_VOICE_MODEL_ID = "scribe_v2_realtime";
@@ -116,13 +125,31 @@ var defaultIssueReportingCopy = {
116
125
  retryAction: "Retry",
117
126
  originHumanLabel: "Human",
118
127
  originMachineLabel: "Machine",
119
- machineOriginFallback: "system"
128
+ machineOriginFallback: "system",
129
+ threadTitle: "Conversation",
130
+ threadDescription: "Messages from the support team about this report, and your replies.",
131
+ threadLoading: "Loading conversation...",
132
+ threadLoadFailed: "Failed to load the conversation",
133
+ threadEmpty: "No messages on this report yet.",
134
+ threadNeedsResponseBadge: "Needs your response",
135
+ threadAuthorOperator: "Support",
136
+ threadAuthorReporter: "You",
137
+ threadKindClarificationRequest: "Question",
138
+ threadKindReporterResponse: "Your reply",
139
+ threadKindFinalResponse: "Resolution",
140
+ threadResponsePlaceholder: "Write your response...",
141
+ threadResponseLabel: "Respond to the support team",
142
+ threadResponseSubmitAction: "Send response",
143
+ threadResponseSubmittingAction: "Sending...",
144
+ threadResponseConflict: "That response was already sent, or the message changed. Refresh and try again.",
145
+ threadResponseFailed: "Failed to send your response"
120
146
  };
121
147
  var issueReportingKeys = {
122
148
  all: ["spaps-issue-reporting"],
123
149
  status: (scope) => [...issueReportingKeys.all, "status", scope],
124
150
  history: (scope) => [...issueReportingKeys.all, "history", scope],
125
- detail: (issueReportId) => [...issueReportingKeys.all, "detail", issueReportId]
151
+ detail: (issueReportId) => [...issueReportingKeys.all, "detail", issueReportId],
152
+ messages: (issueReportId) => [...issueReportingKeys.all, "messages", issueReportId]
126
153
  };
127
154
  function resolvePageUrl(getPageUrl) {
128
155
  if (getPageUrl) {
@@ -292,6 +319,27 @@ function getEntryPointClassName(state) {
292
319
  }
293
320
  return "text-slate-500";
294
321
  }
322
+ function selectReporterVisibleMessages(messages) {
323
+ return messages.filter(
324
+ (message) => message.reporter_visible && message.state === "active"
325
+ ).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
326
+ }
327
+ function isReporterMessageConflict(error) {
328
+ if (!error || typeof error !== "object") {
329
+ return false;
330
+ }
331
+ const record = error;
332
+ const code = record.code ?? record.error?.code;
333
+ if (typeof code === "string" && code === "ISSUE_REPORT_MESSAGE_CONFLICT") {
334
+ return true;
335
+ }
336
+ const status = record.status ?? record.statusCode ?? record.status_code ?? void 0;
337
+ if (status === 409) {
338
+ return true;
339
+ }
340
+ const message = error instanceof Error ? error.message : typeof record.message === "string" ? record.message : "";
341
+ return /ISSUE_REPORT_MESSAGE_CONFLICT/i.test(message) || /\b409\b/.test(message);
342
+ }
295
343
  function getIssueNoteLengthMessage(note, copy) {
296
344
  if (note.length < NOTE_MIN_LENGTH) {
297
345
  return `${NOTE_MIN_LENGTH - note.length} ${copy.noteMinimumSuffix}`;
@@ -386,18 +434,26 @@ function useIssueReportingMutations() {
386
434
  const updateMutation = useMutation({
387
435
  mutationFn: ({
388
436
  issueReportId,
389
- note
390
- }) => client.issueReporting.update(issueReportId, { note }),
437
+ note,
438
+ add_attachment_ids,
439
+ remove_attachment_ids
440
+ }) => client.issueReporting.update(issueReportId, {
441
+ note,
442
+ add_attachment_ids,
443
+ remove_attachment_ids
444
+ }),
391
445
  onSuccess: invalidateAll
392
446
  });
393
447
  const replyMutation = useMutation({
394
448
  mutationFn: ({
395
449
  issueReportId,
396
450
  note,
397
- reporterRoleHint
451
+ reporterRoleHint,
452
+ attachment_ids
398
453
  }) => client.issueReporting.reply(issueReportId, {
399
454
  note,
400
- reporter_role_hint: reporterRoleHint
455
+ reporter_role_hint: reporterRoleHint,
456
+ attachment_ids
401
457
  }),
402
458
  onSuccess: invalidateAll
403
459
  });
@@ -407,6 +463,39 @@ function useIssueReportingMutations() {
407
463
  replyMutation
408
464
  };
409
465
  }
466
+ function useIssueReportingMessages(issueReportId) {
467
+ const { client, isEligible } = useIssueReporting();
468
+ const listMessages = client.issueReporting.listMessages;
469
+ return useQuery({
470
+ queryKey: issueReportingKeys.messages(issueReportId ?? "none"),
471
+ queryFn: () => listMessages(issueReportId),
472
+ enabled: isEligible && Boolean(issueReportId) && Boolean(listMessages),
473
+ retry: false
474
+ });
475
+ }
476
+ function useIssueReportingMessageMutation(issueReportId) {
477
+ const queryClient = useQueryClient();
478
+ const { client } = useIssueReporting();
479
+ return useMutation({
480
+ mutationFn: (payload) => {
481
+ const submit = client.issueReporting.submitMessage;
482
+ if (!submit || !issueReportId) {
483
+ return Promise.reject(
484
+ new Error("This client does not support submitting messages.")
485
+ );
486
+ }
487
+ return submit(issueReportId, payload);
488
+ },
489
+ onSuccess: async () => {
490
+ if (!issueReportId) {
491
+ return;
492
+ }
493
+ await queryClient.invalidateQueries({
494
+ queryKey: issueReportingKeys.messages(issueReportId)
495
+ });
496
+ }
497
+ });
498
+ }
410
499
  function IssueReportingProvider({
411
500
  client,
412
501
  isEligible,
@@ -436,6 +525,7 @@ function IssueReportingProvider({
436
525
  const [scope, setScope] = useState(
437
526
  () => resolveInitialScope(defaultScope, allowTenantScope)
438
527
  );
528
+ const [needsResponseMap, setNeedsResponseMap] = useState({});
439
529
  const [pageConfigs, setPageConfigs] = useState([]);
440
530
  const [registeredTargets, setRegisteredTargets] = useState(
441
531
  []
@@ -460,6 +550,21 @@ function IssueReportingProvider({
460
550
  useEffect(() => {
461
551
  setScope(resolveInitialScope(defaultScope, allowTenantScope));
462
552
  }, [allowTenantScope, defaultScope]);
553
+ const setNeedsResponse = useCallback(
554
+ (issueReportId, needsResponse) => {
555
+ setNeedsResponseMap((current) => {
556
+ if (Boolean(current[issueReportId]) === needsResponse) {
557
+ return current;
558
+ }
559
+ return { ...current, [issueReportId]: needsResponse };
560
+ });
561
+ },
562
+ []
563
+ );
564
+ const needsResponseIssueIds = useMemo(
565
+ () => Object.entries(needsResponseMap).filter(([, needs]) => needs).map(([id]) => id),
566
+ [needsResponseMap]
567
+ );
463
568
  const closePopover = useCallback(() => {
464
569
  setIsPopoverOpen(false);
465
570
  }, []);
@@ -647,7 +752,9 @@ function IssueReportingProvider({
647
752
  createMode,
648
753
  inputModes: resolvedInputModes,
649
754
  defaultInputMode: resolvedDefaultInputMode,
650
- voice: resolvedVoiceConfig
755
+ voice: resolvedVoiceConfig,
756
+ needsResponseIssueIds,
757
+ setNeedsResponse
651
758
  }),
652
759
  [
653
760
  allowTenantScope,
@@ -663,6 +770,7 @@ function IssueReportingProvider({
663
770
  isReportMode,
664
771
  mergedCopy,
665
772
  modalState,
773
+ needsResponseIssueIds,
666
774
  openExistingIssueModal,
667
775
  openPageIssueModal,
668
776
  openPopover,
@@ -674,6 +782,7 @@ function IssueReportingProvider({
674
782
  resolvedVoiceConfig,
675
783
  scope,
676
784
  selectPanel,
785
+ setNeedsResponse,
677
786
  startNewIssue
678
787
  ]
679
788
  );
@@ -953,6 +1062,7 @@ function IssueReportModalBody({
953
1062
  error,
954
1063
  canUseVoice,
955
1064
  canUseText,
1065
+ canUseAttachments,
956
1066
  effectiveInputMode,
957
1067
  note,
958
1068
  normalizedNote,
@@ -966,6 +1076,11 @@ function IssueReportModalBody({
966
1076
  voiceError,
967
1077
  scribeError,
968
1078
  submitError,
1079
+ existingAttachments,
1080
+ removedExistingIds,
1081
+ pendingFiles,
1082
+ uploadProgress,
1083
+ attachmentValidationErrors,
969
1084
  onRetryHydration,
970
1085
  onClose,
971
1086
  onSelectText,
@@ -974,7 +1089,10 @@ function IssueReportModalBody({
974
1089
  onStopVoiceInput,
975
1090
  onAppendTranscript,
976
1091
  onNoteChange,
977
- onSubmit
1092
+ onSubmit,
1093
+ onAddFiles,
1094
+ onRemoveExistingAttachment,
1095
+ onRemovePendingAttachment
978
1096
  }) {
979
1097
  if (isHydrating) {
980
1098
  return /* @__PURE__ */ jsxs("div", { className: "mt-5 flex items-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-600", children: [
@@ -1044,6 +1162,20 @@ function IssueReportModalBody({
1044
1162
  onSubmit
1045
1163
  }
1046
1164
  ),
1165
+ canUseAttachments && /* @__PURE__ */ jsx2(
1166
+ AttachmentPicker,
1167
+ {
1168
+ existingAttachments,
1169
+ removedExistingIds,
1170
+ pendingFiles,
1171
+ uploadProgress,
1172
+ validationErrors: attachmentValidationErrors,
1173
+ disabled: isSubmitting,
1174
+ onAddFiles,
1175
+ onRemoveExisting: onRemoveExistingAttachment,
1176
+ onRemovePending: onRemovePendingAttachment
1177
+ }
1178
+ ),
1047
1179
  submitError ? /* @__PURE__ */ jsx2("div", { className: "mt-4 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700", children: submitError }) : null,
1048
1180
  /* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-3", children: [
1049
1181
  /* @__PURE__ */ jsx2(
@@ -1069,7 +1201,8 @@ function IssueReportModalBody({
1069
1201
  children: isSubmitting ? copy.submittingAction : copy.submitAction
1070
1202
  }
1071
1203
  )
1072
- ] })
1204
+ ] }),
1205
+ mode !== "create" && issue ? /* @__PURE__ */ jsx2(IssueReportMessageThread, { issueReportId: issue.id }) : null
1073
1206
  ] });
1074
1207
  }
1075
1208
  function useIssueReportVoiceCapture({
@@ -1167,6 +1300,493 @@ function useIssueReportVoiceCapture({
1167
1300
  appendTranscript
1168
1301
  };
1169
1302
  }
1303
+ var INITIAL_UPLOAD_PROGRESS = {
1304
+ phase: "idle",
1305
+ uploaded: 0,
1306
+ total: 0
1307
+ };
1308
+ var pendingFileCounter = 0;
1309
+ function nextPendingId() {
1310
+ pendingFileCounter += 1;
1311
+ return `pending-${pendingFileCounter}`;
1312
+ }
1313
+ function formatFileSize(bytes) {
1314
+ if (bytes < 1024) return `${bytes} B`;
1315
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1316
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1317
+ }
1318
+ function validateAttachmentFile(file) {
1319
+ if (!ATTACHMENT_ALLOWED_MIMES.has(file.type)) {
1320
+ return `"${file.name}" is not a supported image type (PNG, JPEG, or WebP).`;
1321
+ }
1322
+ if (file.size > ATTACHMENT_MAX_BYTES) {
1323
+ return `"${file.name}" exceeds the 10 MB limit.`;
1324
+ }
1325
+ return null;
1326
+ }
1327
+ function AttachmentPicker({
1328
+ existingAttachments,
1329
+ removedExistingIds,
1330
+ pendingFiles,
1331
+ uploadProgress,
1332
+ validationErrors,
1333
+ disabled,
1334
+ onAddFiles,
1335
+ onRemoveExisting,
1336
+ onRemovePending
1337
+ }) {
1338
+ const fileInputRef = useRef2(null);
1339
+ const retainedExisting = existingAttachments.filter(
1340
+ (a) => !removedExistingIds.has(a.id)
1341
+ );
1342
+ const totalCount = retainedExisting.length + pendingFiles.length;
1343
+ const canAdd = totalCount < ATTACHMENT_MAX_COUNT && !disabled;
1344
+ return /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-3", children: [
1345
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1346
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-slate-500", children: [
1347
+ /* @__PURE__ */ jsx2(Image, { className: "h-3.5 w-3.5" }),
1348
+ "Screenshots (",
1349
+ totalCount,
1350
+ "/",
1351
+ ATTACHMENT_MAX_COUNT,
1352
+ ")"
1353
+ ] }),
1354
+ /* @__PURE__ */ jsx2(
1355
+ "input",
1356
+ {
1357
+ ref: fileInputRef,
1358
+ type: "file",
1359
+ accept: "image/png,image/jpeg,image/webp",
1360
+ multiple: true,
1361
+ className: "hidden",
1362
+ onChange: (e) => {
1363
+ if (e.target.files && e.target.files.length > 0) {
1364
+ onAddFiles(e.target.files);
1365
+ }
1366
+ e.target.value = "";
1367
+ },
1368
+ disabled: !canAdd,
1369
+ "aria-label": "Select screenshot files"
1370
+ }
1371
+ ),
1372
+ /* @__PURE__ */ jsx2(
1373
+ "button",
1374
+ {
1375
+ type: "button",
1376
+ className: cn(
1377
+ "rounded-full border px-3 py-1 text-xs font-medium transition",
1378
+ canAdd ? "border-slate-300 text-slate-700 hover:bg-slate-50" : "cursor-not-allowed border-slate-200 text-slate-400"
1379
+ ),
1380
+ onClick: () => fileInputRef.current?.click(),
1381
+ disabled: !canAdd,
1382
+ "aria-label": "Add screenshots",
1383
+ children: "Add"
1384
+ }
1385
+ )
1386
+ ] }),
1387
+ validationErrors.length > 0 && /* @__PURE__ */ jsx2("div", { className: "space-y-1", children: validationErrors.map((err, i) => /* @__PURE__ */ jsx2(
1388
+ "div",
1389
+ {
1390
+ className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
1391
+ role: "alert",
1392
+ children: err
1393
+ },
1394
+ i
1395
+ )) }),
1396
+ uploadProgress.phase === "uploading" && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600", children: [
1397
+ /* @__PURE__ */ jsx2(Spinner, { className: "h-3.5 w-3.5 animate-spin" }),
1398
+ "Uploading ",
1399
+ uploadProgress.uploaded,
1400
+ " of ",
1401
+ uploadProgress.total,
1402
+ "..."
1403
+ ] }),
1404
+ uploadProgress.phase === "error" && uploadProgress.error && /* @__PURE__ */ jsx2(
1405
+ "div",
1406
+ {
1407
+ className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
1408
+ role: "alert",
1409
+ children: uploadProgress.error
1410
+ }
1411
+ ),
1412
+ (retainedExisting.length > 0 || pendingFiles.length > 0) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
1413
+ retainedExisting.map((att) => /* @__PURE__ */ jsxs(
1414
+ "div",
1415
+ {
1416
+ className: "group relative flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2",
1417
+ children: [
1418
+ /* @__PURE__ */ jsx2(Image, { className: "h-4 w-4 flex-shrink-0 text-slate-400" }),
1419
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
1420
+ /* @__PURE__ */ jsx2("div", { className: "max-w-[140px] truncate text-xs font-medium text-slate-700", children: att.original_filename }),
1421
+ /* @__PURE__ */ jsx2("div", { className: "text-xs text-slate-400", children: formatFileSize(att.byte_size) })
1422
+ ] }),
1423
+ /* @__PURE__ */ jsx2(
1424
+ "button",
1425
+ {
1426
+ type: "button",
1427
+ className: "ml-1 rounded-full p-0.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700",
1428
+ onClick: () => onRemoveExisting(att.id),
1429
+ disabled,
1430
+ "aria-label": `Remove ${att.original_filename}`,
1431
+ children: /* @__PURE__ */ jsx2(Trash, { className: "h-3.5 w-3.5" })
1432
+ }
1433
+ )
1434
+ ]
1435
+ },
1436
+ att.id
1437
+ )),
1438
+ pendingFiles.map((pf) => /* @__PURE__ */ jsxs(
1439
+ "div",
1440
+ {
1441
+ className: "group relative flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2",
1442
+ children: [
1443
+ /* @__PURE__ */ jsx2(
1444
+ "img",
1445
+ {
1446
+ src: pf.previewUrl,
1447
+ alt: pf.file.name,
1448
+ className: "h-8 w-8 flex-shrink-0 rounded object-cover"
1449
+ }
1450
+ ),
1451
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
1452
+ /* @__PURE__ */ jsx2("div", { className: "max-w-[140px] truncate text-xs font-medium text-slate-700", children: pf.file.name }),
1453
+ /* @__PURE__ */ jsx2("div", { className: "text-xs text-slate-400", children: formatFileSize(pf.file.size) })
1454
+ ] }),
1455
+ /* @__PURE__ */ jsx2(
1456
+ "button",
1457
+ {
1458
+ type: "button",
1459
+ className: "ml-1 rounded-full p-0.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700",
1460
+ onClick: () => onRemovePending(pf.clientId),
1461
+ disabled,
1462
+ "aria-label": `Remove ${pf.file.name}`,
1463
+ children: /* @__PURE__ */ jsx2(Trash, { className: "h-3.5 w-3.5" })
1464
+ }
1465
+ )
1466
+ ]
1467
+ },
1468
+ pf.clientId
1469
+ ))
1470
+ ] })
1471
+ ] });
1472
+ }
1473
+ function useAttachmentState(existingAttachments) {
1474
+ const [pendingFiles, setPendingFiles] = useState2([]);
1475
+ const [removedExistingIds, setRemovedExistingIds] = useState2(
1476
+ /* @__PURE__ */ new Set()
1477
+ );
1478
+ const [validationErrors, setValidationErrors] = useState2([]);
1479
+ const [uploadProgress, setUploadProgress] = useState2(INITIAL_UPLOAD_PROGRESS);
1480
+ const retainedExistingCount = existingAttachments.filter(
1481
+ (a) => !removedExistingIds.has(a.id)
1482
+ ).length;
1483
+ const reset = useCallback2(() => {
1484
+ for (const pf of pendingFiles) {
1485
+ URL.revokeObjectURL(pf.previewUrl);
1486
+ }
1487
+ setPendingFiles([]);
1488
+ setRemovedExistingIds(/* @__PURE__ */ new Set());
1489
+ setValidationErrors([]);
1490
+ setUploadProgress(INITIAL_UPLOAD_PROGRESS);
1491
+ }, [pendingFiles]);
1492
+ const addFiles = useCallback2(
1493
+ (fileList) => {
1494
+ const currentTotal = retainedExistingCount + pendingFiles.length;
1495
+ const errors = [];
1496
+ const accepted = [];
1497
+ let count = currentTotal;
1498
+ for (let i = 0; i < fileList.length; i++) {
1499
+ const file = fileList[i];
1500
+ if (count >= ATTACHMENT_MAX_COUNT) {
1501
+ errors.push(
1502
+ `"${file.name}" was not added; maximum ${ATTACHMENT_MAX_COUNT} screenshots reached.`
1503
+ );
1504
+ continue;
1505
+ }
1506
+ const err = validateAttachmentFile(file);
1507
+ if (err) {
1508
+ errors.push(err);
1509
+ continue;
1510
+ }
1511
+ accepted.push({
1512
+ clientId: nextPendingId(),
1513
+ file,
1514
+ previewUrl: URL.createObjectURL(file)
1515
+ });
1516
+ count += 1;
1517
+ }
1518
+ if (accepted.length > 0) {
1519
+ setPendingFiles((prev) => [...prev, ...accepted]);
1520
+ }
1521
+ setValidationErrors(errors);
1522
+ setUploadProgress(INITIAL_UPLOAD_PROGRESS);
1523
+ },
1524
+ [pendingFiles.length, retainedExistingCount]
1525
+ );
1526
+ const removeExisting = useCallback2((id) => {
1527
+ setRemovedExistingIds((prev) => /* @__PURE__ */ new Set([...prev, id]));
1528
+ }, []);
1529
+ const removePending = useCallback2((clientId) => {
1530
+ setPendingFiles((prev) => {
1531
+ const removed = prev.find((pf) => pf.clientId === clientId);
1532
+ if (removed) {
1533
+ URL.revokeObjectURL(removed.previewUrl);
1534
+ }
1535
+ return prev.filter((pf) => pf.clientId !== clientId);
1536
+ });
1537
+ setUploadProgress(INITIAL_UPLOAD_PROGRESS);
1538
+ }, []);
1539
+ const markPendingUploaded = useCallback2((clientId, attachmentId) => {
1540
+ setPendingFiles(
1541
+ (prev) => prev.map(
1542
+ (pf) => pf.clientId === clientId ? { ...pf, uploadedAttachmentId: attachmentId } : pf
1543
+ )
1544
+ );
1545
+ }, []);
1546
+ return {
1547
+ pendingFiles,
1548
+ removedExistingIds,
1549
+ validationErrors,
1550
+ uploadProgress,
1551
+ setUploadProgress,
1552
+ addFiles,
1553
+ removeExisting,
1554
+ removePending,
1555
+ markPendingUploaded,
1556
+ reset
1557
+ };
1558
+ }
1559
+ var REPORTER_MESSAGE_MIN_LENGTH = 1;
1560
+ var REPORTER_MESSAGE_MAX_LENGTH = 2e3;
1561
+ function generateIdempotencyKey() {
1562
+ const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
1563
+ if (cryptoObj && typeof cryptoObj.randomUUID === "function") {
1564
+ return `reporter-msg-${cryptoObj.randomUUID()}`;
1565
+ }
1566
+ return `reporter-msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1567
+ }
1568
+ function getMessageKindLabel(kind, copy) {
1569
+ switch (kind) {
1570
+ case "clarification_request":
1571
+ return copy.threadKindClarificationRequest;
1572
+ case "reporter_response":
1573
+ return copy.threadKindReporterResponse;
1574
+ case "final_response":
1575
+ return copy.threadKindFinalResponse;
1576
+ default:
1577
+ return kind;
1578
+ }
1579
+ }
1580
+ function getMessageKindClassName(kind) {
1581
+ switch (kind) {
1582
+ case "clarification_request":
1583
+ return "bg-amber-100 text-amber-700";
1584
+ case "final_response":
1585
+ return "bg-emerald-100 text-emerald-700";
1586
+ default:
1587
+ return "bg-slate-100 text-slate-600";
1588
+ }
1589
+ }
1590
+ function MessageBubble({
1591
+ message,
1592
+ copy
1593
+ }) {
1594
+ const isReporter = message.actor.author_type === "reporter";
1595
+ const isFinal = message.kind === "final_response";
1596
+ const authorLabel = isReporter ? copy.threadAuthorReporter : copy.threadAuthorOperator;
1597
+ return /* @__PURE__ */ jsxs(
1598
+ "li",
1599
+ {
1600
+ className: cn(
1601
+ "flex flex-col gap-1",
1602
+ isReporter ? "items-end" : "items-start"
1603
+ ),
1604
+ children: [
1605
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1606
+ /* @__PURE__ */ jsx2(
1607
+ "span",
1608
+ {
1609
+ className: cn(
1610
+ "rounded-full px-2 py-0.5 font-medium",
1611
+ BADGE_TEXT,
1612
+ getMessageKindClassName(message.kind)
1613
+ ),
1614
+ children: getMessageKindLabel(message.kind, copy)
1615
+ }
1616
+ ),
1617
+ /* @__PURE__ */ jsx2("span", { className: cn("font-medium text-slate-600", LABEL_TEXT), children: authorLabel }),
1618
+ /* @__PURE__ */ jsx2(
1619
+ "time",
1620
+ {
1621
+ className: "text-xs text-slate-400",
1622
+ dateTime: message.created_at,
1623
+ children: formatRelativeTime(message.created_at)
1624
+ }
1625
+ )
1626
+ ] }),
1627
+ /* @__PURE__ */ jsx2(
1628
+ "div",
1629
+ {
1630
+ className: cn(
1631
+ "max-w-[85%] whitespace-pre-wrap rounded-2xl border px-3 py-2 text-sm",
1632
+ isReporter ? "border-slate-900 bg-slate-900 text-white" : isFinal ? "border-emerald-200 bg-emerald-50 text-emerald-950" : "border-slate-200 bg-white text-slate-800"
1633
+ ),
1634
+ children: message.body
1635
+ }
1636
+ )
1637
+ ]
1638
+ }
1639
+ );
1640
+ }
1641
+ function ReporterResponseComposer({
1642
+ issueReportId,
1643
+ copy
1644
+ }) {
1645
+ const mutation = useIssueReportingMessageMutation(issueReportId);
1646
+ const idempotencyKeyRef = useRef2(generateIdempotencyKey());
1647
+ const [body, setBody] = useState2("");
1648
+ const [submitError, setSubmitError] = useState2(null);
1649
+ const normalized = body.trim();
1650
+ const isValid = normalized.length >= REPORTER_MESSAGE_MIN_LENGTH && normalized.length <= REPORTER_MESSAGE_MAX_LENGTH;
1651
+ const isSubmitting = mutation.isPending;
1652
+ const handleSubmit = async () => {
1653
+ if (!isValid || isSubmitting) {
1654
+ return;
1655
+ }
1656
+ setSubmitError(null);
1657
+ try {
1658
+ await mutation.mutateAsync({
1659
+ body: normalized,
1660
+ idempotency_key: idempotencyKeyRef.current
1661
+ });
1662
+ setBody("");
1663
+ idempotencyKeyRef.current = generateIdempotencyKey();
1664
+ } catch (error) {
1665
+ if (isReporterMessageConflict(error)) {
1666
+ setSubmitError(copy.threadResponseConflict);
1667
+ idempotencyKeyRef.current = generateIdempotencyKey();
1668
+ return;
1669
+ }
1670
+ setSubmitError(resolveErrorMessage(error, copy.threadResponseFailed));
1671
+ }
1672
+ };
1673
+ return /* @__PURE__ */ jsxs("div", { className: "mt-3 space-y-2", children: [
1674
+ /* @__PURE__ */ jsx2(
1675
+ "label",
1676
+ {
1677
+ htmlFor: "issue-report-thread-response",
1678
+ className: cn(
1679
+ "block font-medium uppercase tracking-wide text-slate-500",
1680
+ LABEL_TEXT
1681
+ ),
1682
+ children: copy.threadResponseLabel
1683
+ }
1684
+ ),
1685
+ /* @__PURE__ */ jsx2(
1686
+ "textarea",
1687
+ {
1688
+ id: "issue-report-thread-response",
1689
+ value: body,
1690
+ onChange: (event) => setBody(event.target.value),
1691
+ onKeyDown: (event) => {
1692
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
1693
+ event.preventDefault();
1694
+ void handleSubmit();
1695
+ }
1696
+ },
1697
+ placeholder: copy.threadResponsePlaceholder,
1698
+ className: "h-24 w-full resize-none rounded-2xl border border-slate-300 px-3 py-2 text-sm text-slate-800 outline-none transition focus:border-slate-500 focus:ring-2 focus:ring-slate-200",
1699
+ disabled: isSubmitting
1700
+ }
1701
+ ),
1702
+ submitError ? /* @__PURE__ */ jsx2(
1703
+ "div",
1704
+ {
1705
+ className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
1706
+ role: "alert",
1707
+ children: submitError
1708
+ }
1709
+ ) : null,
1710
+ /* @__PURE__ */ jsx2("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx2(
1711
+ "button",
1712
+ {
1713
+ type: "button",
1714
+ className: cn(
1715
+ "rounded-full px-4 py-2 text-sm font-semibold text-white transition",
1716
+ isValid && !isSubmitting ? "bg-slate-900 hover:bg-slate-800" : "cursor-not-allowed bg-slate-300"
1717
+ ),
1718
+ onClick: () => void handleSubmit(),
1719
+ disabled: !isValid || isSubmitting,
1720
+ children: isSubmitting ? copy.threadResponseSubmittingAction : copy.threadResponseSubmitAction
1721
+ }
1722
+ ) })
1723
+ ] });
1724
+ }
1725
+ function IssueReportMessageThread({
1726
+ issueReportId
1727
+ }) {
1728
+ const { copy, client, setNeedsResponse } = useIssueReporting();
1729
+ const supportsMessages = Boolean(client.issueReporting.listMessages);
1730
+ const supportsSubmit = Boolean(client.issueReporting.submitMessage);
1731
+ const query = useIssueReportingMessages(issueReportId);
1732
+ const needsResponse = query.data?.needs_response ?? false;
1733
+ useEffect2(() => {
1734
+ setNeedsResponse(issueReportId, needsResponse);
1735
+ return () => {
1736
+ setNeedsResponse(issueReportId, false);
1737
+ };
1738
+ }, [issueReportId, needsResponse, setNeedsResponse]);
1739
+ if (!supportsMessages) {
1740
+ return null;
1741
+ }
1742
+ const visibleMessages = selectReporterVisibleMessages(query.data?.items ?? []);
1743
+ return /* @__PURE__ */ jsxs(
1744
+ "section",
1745
+ {
1746
+ "aria-label": copy.threadTitle,
1747
+ className: "mt-6 border-t border-slate-100 pt-5",
1748
+ children: [
1749
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
1750
+ /* @__PURE__ */ jsx2("h3", { className: "text-sm font-semibold text-slate-900", children: copy.threadTitle }),
1751
+ needsResponse ? /* @__PURE__ */ jsx2(
1752
+ "span",
1753
+ {
1754
+ className: cn(
1755
+ "rounded-full px-2 py-0.5 font-medium",
1756
+ BADGE_TEXT,
1757
+ "bg-amber-100 text-amber-700"
1758
+ ),
1759
+ children: copy.threadNeedsResponseBadge
1760
+ }
1761
+ ) : null
1762
+ ] }),
1763
+ /* @__PURE__ */ jsx2("p", { className: "mt-1 text-xs text-slate-500", children: copy.threadDescription }),
1764
+ query.isPending ? /* @__PURE__ */ jsxs("div", { className: "mt-4 flex items-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-600", children: [
1765
+ /* @__PURE__ */ jsx2(Spinner, { className: "h-4 w-4 animate-spin" }),
1766
+ /* @__PURE__ */ jsx2("span", { children: copy.threadLoading })
1767
+ ] }) : query.error ? /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-4 text-sm text-rose-700", children: [
1768
+ /* @__PURE__ */ jsx2("div", { children: resolveErrorMessage(query.error, copy.threadLoadFailed) }),
1769
+ /* @__PURE__ */ jsx2(
1770
+ "button",
1771
+ {
1772
+ type: "button",
1773
+ className: "rounded-full border border-rose-300 px-3 py-1 font-medium transition hover:bg-rose-100",
1774
+ onClick: () => void query.refetch(),
1775
+ children: copy.retryAction
1776
+ }
1777
+ )
1778
+ ] }) : visibleMessages.length === 0 ? /* @__PURE__ */ jsx2("div", { className: "mt-4 rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-6 text-center text-sm text-slate-500", children: copy.threadEmpty }) : /* @__PURE__ */ jsx2("ul", { className: "mt-4 space-y-3", children: visibleMessages.map((message) => /* @__PURE__ */ jsx2(MessageBubble, { message, copy }, message.id)) }),
1779
+ supportsSubmit ? /* @__PURE__ */ jsx2(
1780
+ ReporterResponseComposer,
1781
+ {
1782
+ issueReportId,
1783
+ copy
1784
+ }
1785
+ ) : null
1786
+ ]
1787
+ }
1788
+ );
1789
+ }
1170
1790
  function IssueReportModeBanner() {
1171
1791
  const { copy } = useIssueReporting();
1172
1792
  const reportMode = useReportMode();
@@ -1467,7 +2087,13 @@ function IssueReportModal() {
1467
2087
  const { isOpen, mode, issue, target, isHydrating, error } = modalState;
1468
2088
  const canUseVoice = mode === "create" && inputModes.includes("voice");
1469
2089
  const canUseText = mode !== "create" || inputModes.includes("text");
1470
- const isSubmitting = createMutation.isPending || updateMutation.isPending || replyMutation.isPending;
2090
+ const canUseAttachments = Boolean(client.issueReporting.uploadAttachment);
2091
+ const existingAttachments = useMemo2(
2092
+ () => mode === "edit" && issue ? issue.attachments ?? [] : [],
2093
+ [issue, mode]
2094
+ );
2095
+ const attachmentState = useAttachmentState(existingAttachments);
2096
+ const isSubmitting = createMutation.isPending || updateMutation.isPending || replyMutation.isPending || attachmentState.uploadProgress.phase === "uploading";
1471
2097
  const {
1472
2098
  inputMode,
1473
2099
  setInputMode,
@@ -1491,6 +2117,7 @@ function IssueReportModal() {
1491
2117
  useEffect2(() => {
1492
2118
  if (!isOpen) {
1493
2119
  resetVoiceCapture();
2120
+ attachmentState.reset();
1494
2121
  setNote("");
1495
2122
  setSubmitError(null);
1496
2123
  return;
@@ -1501,6 +2128,7 @@ function IssueReportModal() {
1501
2128
  setNote("");
1502
2129
  }
1503
2130
  resetVoiceCapture();
2131
+ attachmentState.reset();
1504
2132
  setSubmitError(null);
1505
2133
  }, [isOpen, issue, mode, resetVoiceCapture]);
1506
2134
  const effectiveInputMode = mode !== "create" ? "text" : canUseVoice && inputMode === "voice" ? "voice" : "text";
@@ -1509,6 +2137,7 @@ function IssueReportModal() {
1509
2137
  const title = mode === "create" ? `${copy.createTitlePrefix}: ${target?.component_label ?? ""}` : mode === "edit" ? `${copy.editTitlePrefix}: ${target?.component_label ?? ""}` : `${copy.replyTitlePrefix}: ${target?.component_label ?? ""}`;
1510
2138
  const handleCloseModal = () => {
1511
2139
  resetVoiceCapture();
2140
+ attachmentState.reset();
1512
2141
  closeModal();
1513
2142
  };
1514
2143
  const handleStartVoiceInput = async () => {
@@ -1520,6 +2149,38 @@ function IssueReportModal() {
1520
2149
  const handleAppendTranscript = () => {
1521
2150
  appendTranscript(setNote);
1522
2151
  };
2152
+ const uploadPendingFiles = async () => {
2153
+ const upload = client.issueReporting.uploadAttachment;
2154
+ if (!upload || attachmentState.pendingFiles.length === 0) {
2155
+ return [];
2156
+ }
2157
+ const files = attachmentState.pendingFiles;
2158
+ attachmentState.setUploadProgress({
2159
+ phase: "uploading",
2160
+ uploaded: 0,
2161
+ total: files.length
2162
+ });
2163
+ const ids = [];
2164
+ for (let i = 0; i < files.length; i++) {
2165
+ const pf = files[i];
2166
+ const attachmentId = pf.uploadedAttachmentId ?? (await upload(pf.file, { filename: pf.file.name })).id;
2167
+ if (!pf.uploadedAttachmentId) {
2168
+ attachmentState.markPendingUploaded(pf.clientId, attachmentId);
2169
+ }
2170
+ ids.push(attachmentId);
2171
+ attachmentState.setUploadProgress({
2172
+ phase: "uploading",
2173
+ uploaded: i + 1,
2174
+ total: files.length
2175
+ });
2176
+ }
2177
+ attachmentState.setUploadProgress({
2178
+ phase: "done",
2179
+ uploaded: files.length,
2180
+ total: files.length
2181
+ });
2182
+ return ids;
2183
+ };
1523
2184
  const handleSubmit = async () => {
1524
2185
  if (!target || !isValid || isSubmitting) {
1525
2186
  return;
@@ -1527,6 +2188,10 @@ function IssueReportModal() {
1527
2188
  setSubmitError(null);
1528
2189
  try {
1529
2190
  const noteForSubmit = normalizedNote;
2191
+ let newAttachmentIds = [];
2192
+ if (canUseAttachments && attachmentState.pendingFiles.length > 0) {
2193
+ newAttachmentIds = await uploadPendingFiles();
2194
+ }
1530
2195
  const effectiveVoiceMetadata = effectiveInputMode === "voice" ? voiceSubmitMetadata ?? {
1531
2196
  provider: voice.provider,
1532
2197
  token: "",
@@ -1551,24 +2216,37 @@ function IssueReportModal() {
1551
2216
  }
1552
2217
  } : target,
1553
2218
  note: noteForSubmit,
1554
- reporter_role_hint: reporterRoleHint
2219
+ reporter_role_hint: reporterRoleHint,
2220
+ attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0
1555
2221
  });
1556
2222
  } else if (mode === "edit" && issue) {
2223
+ const removedIds = [...attachmentState.removedExistingIds];
1557
2224
  await updateMutation.mutateAsync({
1558
2225
  issueReportId: issue.id,
1559
- note: noteForSubmit
2226
+ note: noteForSubmit,
2227
+ add_attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0,
2228
+ remove_attachment_ids: removedIds.length > 0 ? removedIds : void 0
1560
2229
  });
1561
2230
  } else if (mode === "reply" && issue) {
1562
2231
  await replyMutation.mutateAsync({
1563
2232
  issueReportId: issue.id,
1564
2233
  note: noteForSubmit,
1565
- reporterRoleHint
2234
+ reporterRoleHint,
2235
+ attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0
1566
2236
  });
1567
2237
  }
1568
2238
  resetVoiceCapture();
2239
+ attachmentState.reset();
1569
2240
  closeModal();
1570
2241
  } catch (submissionError) {
1571
- setSubmitError(resolveErrorMessage(submissionError, "Failed to submit issue report"));
2242
+ const message = resolveErrorMessage(
2243
+ submissionError,
2244
+ "Failed to submit issue report"
2245
+ );
2246
+ attachmentState.setUploadProgress(
2247
+ (current) => current.phase === "uploading" ? { ...current, phase: "error", error: message } : current
2248
+ );
2249
+ setSubmitError(message);
1572
2250
  }
1573
2251
  };
1574
2252
  return /* @__PURE__ */ jsx2(Dialog.Root, { open: isOpen, onOpenChange: (open) => !open && handleCloseModal(), children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
@@ -1577,7 +2255,7 @@ function IssueReportModal() {
1577
2255
  Dialog.Content,
1578
2256
  {
1579
2257
  className: cn(
1580
- "fixed left-1/2 top-1/2 max-w-xl -translate-x-1/2 -translate-y-1/2 border border-slate-200 bg-white p-6 focus:outline-none",
2258
+ "fixed left-1/2 top-1/2 max-h-[90vh] max-w-xl -translate-x-1/2 -translate-y-1/2 overflow-y-auto border border-slate-200 bg-white p-6 focus:outline-none",
1581
2259
  Z_MODAL_CONTENT,
1582
2260
  MODAL_WIDTH,
1583
2261
  MODAL_RADIUS,
@@ -1603,6 +2281,7 @@ function IssueReportModal() {
1603
2281
  error,
1604
2282
  canUseVoice,
1605
2283
  canUseText,
2284
+ canUseAttachments,
1606
2285
  effectiveInputMode,
1607
2286
  note,
1608
2287
  normalizedNote,
@@ -1616,6 +2295,11 @@ function IssueReportModal() {
1616
2295
  voiceError,
1617
2296
  scribeError,
1618
2297
  submitError,
2298
+ existingAttachments,
2299
+ removedExistingIds: attachmentState.removedExistingIds,
2300
+ pendingFiles: attachmentState.pendingFiles,
2301
+ uploadProgress: attachmentState.uploadProgress,
2302
+ attachmentValidationErrors: attachmentState.validationErrors,
1619
2303
  onRetryHydration: () => void retryModalHydration(),
1620
2304
  onClose: handleCloseModal,
1621
2305
  onSelectText: () => setInputMode("text"),
@@ -1624,7 +2308,10 @@ function IssueReportModal() {
1624
2308
  onStopVoiceInput: handleStopVoiceInput,
1625
2309
  onAppendTranscript: handleAppendTranscript,
1626
2310
  onNoteChange: setNote,
1627
- onSubmit: () => void handleSubmit()
2311
+ onSubmit: () => void handleSubmit(),
2312
+ onAddFiles: attachmentState.addFiles,
2313
+ onRemoveExistingAttachment: attachmentState.removeExisting,
2314
+ onRemovePendingAttachment: attachmentState.removePending
1628
2315
  }
1629
2316
  ),
1630
2317
  /* @__PURE__ */ jsx2(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx2(
@@ -1651,33 +2338,44 @@ function FloatingIssueReportButton({
1651
2338
  isReportMode,
1652
2339
  isPopoverOpen,
1653
2340
  openPopover,
1654
- closePopover
2341
+ closePopover,
2342
+ needsResponseIssueIds
1655
2343
  } = useIssueReporting();
1656
2344
  const status = useIssueReportingStatus();
1657
2345
  const entryPointState = getEntryPointState(status.data);
2346
+ const needsResponse = needsResponseIssueIds.length > 0;
1658
2347
  if (!isEligible) {
1659
2348
  return null;
1660
2349
  }
1661
2350
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1662
2351
  /* @__PURE__ */ jsx2(IssueReportModeBanner, {}),
1663
- !isReportMode ? /* @__PURE__ */ jsx2("div", { className: cn("fixed bottom-12 right-4", Z_FLOATING_BUTTON, positionClassName), children: /* @__PURE__ */ jsx2(IssueReportPopover, { children: /* @__PURE__ */ jsx2(
2352
+ !isReportMode ? /* @__PURE__ */ jsx2("div", { className: cn("fixed bottom-12 right-4", Z_FLOATING_BUTTON, positionClassName), children: /* @__PURE__ */ jsx2(IssueReportPopover, { children: /* @__PURE__ */ jsxs(
1664
2353
  "button",
1665
2354
  {
1666
2355
  type: "button",
1667
- "aria-label": copy.entryAriaLabel,
2356
+ "aria-label": needsResponse ? `${copy.entryAriaLabel} (${copy.threadNeedsResponseBadge})` : copy.entryAriaLabel,
1668
2357
  onClick: () => isPopoverOpen ? closePopover() : openPopover(),
1669
2358
  className: cn(
1670
- "flex h-12 w-12 items-center justify-center rounded-full border border-slate-200 bg-white shadow-lg transition hover:-translate-y-0.5 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-slate-300",
2359
+ "relative flex h-12 w-12 items-center justify-center rounded-full border border-slate-200 bg-white shadow-lg transition hover:-translate-y-0.5 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-slate-300",
1671
2360
  status.isPending && "animate-pulse",
1672
2361
  className
1673
2362
  ),
1674
- children: /* @__PURE__ */ jsx2(
1675
- BugBeetle,
1676
- {
1677
- className: cn("h-6 w-6", getEntryPointClassName(entryPointState)),
1678
- weight: "fill"
1679
- }
1680
- )
2363
+ children: [
2364
+ /* @__PURE__ */ jsx2(
2365
+ BugBeetle,
2366
+ {
2367
+ className: cn("h-6 w-6", getEntryPointClassName(entryPointState)),
2368
+ weight: "fill"
2369
+ }
2370
+ ),
2371
+ needsResponse ? /* @__PURE__ */ jsx2(
2372
+ "span",
2373
+ {
2374
+ "data-testid": "issue-report-needs-response-badge",
2375
+ className: "absolute -right-0.5 -top-0.5 h-3 w-3 rounded-full border-2 border-white bg-amber-500"
2376
+ }
2377
+ ) : null
2378
+ ]
1681
2379
  }
1682
2380
  ) }) }) : null,
1683
2381
  /* @__PURE__ */ jsx2(IssueReportModal, {})
@@ -1729,6 +2427,7 @@ function ReportableSection({
1729
2427
  }
1730
2428
  export {
1731
2429
  FloatingIssueReportButton,
2430
+ IssueReportMessageThread,
1732
2431
  IssueReportingPageConfig,
1733
2432
  IssueReportingProvider,
1734
2433
  ReportModeContext,
@@ -1742,9 +2441,13 @@ export {
1742
2441
  getIssueStatusClassName,
1743
2442
  isClosedIssueStatus,
1744
2443
  isOpenIssueStatus,
2444
+ isReporterMessageConflict,
1745
2445
  issueReportingKeys,
2446
+ selectReporterVisibleMessages,
1746
2447
  useIssueReporting,
1747
2448
  useIssueReportingHistory,
2449
+ useIssueReportingMessageMutation,
2450
+ useIssueReportingMessages,
1748
2451
  useIssueReportingMutations,
1749
2452
  useIssueReportingStatus,
1750
2453
  useReportMode