@mushi-mushi/react-native 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -48,12 +48,184 @@ __export(index_exports, {
48
48
  module.exports = __toCommonJS(index_exports);
49
49
 
50
50
  // src/provider.tsx
51
- var import_react4 = require("react");
52
- var import_react_native5 = require("react-native");
53
- var import_core8 = require("@mushi-mushi/core");
51
+ var import_react5 = require("react");
52
+ var import_react_native6 = require("react-native");
53
+ var import_core10 = require("@mushi-mushi/core");
54
+ var import_reporter_channels2 = require("@mushi-mushi/core/reporter-channels");
55
+ var import_reporter_ui3 = require("@mushi-mushi/core/reporter-ui");
54
56
 
55
- // src/capture/console-capture.ts
57
+ // src/storage/reporter-flags.ts
58
+ async function readReporterFlag(projectId, flag) {
59
+ try {
60
+ const store = (await import("@react-native-async-storage/async-storage")).default;
61
+ return await store.getItem(`@mushi:${flag}:${projectId}`);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ async function writeReporterFlag(projectId, flag, value) {
67
+ try {
68
+ const store = (await import("@react-native-async-storage/async-storage")).default;
69
+ await store.setItem(`@mushi:${flag}:${projectId}`, value);
70
+ } catch {
71
+ }
72
+ }
73
+
74
+ // src/components/MushiUpdateToast.tsx
75
+ var import_react = require("react");
76
+ var import_react_native = require("react-native");
77
+ var import_core2 = require("@mushi-mushi/core");
78
+ var import_reporter_ui = require("@mushi-mushi/core/reporter-ui");
79
+
80
+ // src/theme.ts
56
81
  var import_core = require("@mushi-mushi/core");
82
+ function hexToRgb(hex) {
83
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})([0-9a-f]{2})?$/i.exec(hex.trim());
84
+ if (!m) return null;
85
+ const h = m[1].length === 3 ? m[1].split("").map((c) => c + c).join("") : m[1];
86
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
87
+ }
88
+ function luminance([r, g, b]) {
89
+ const lin = (c) => {
90
+ const s5 = c / 255;
91
+ return s5 <= 0.03928 ? s5 / 12.92 : ((s5 + 0.055) / 1.055) ** 2.4;
92
+ };
93
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
94
+ }
95
+ function contrastingText(bg, fallback) {
96
+ const rgb = hexToRgb(bg);
97
+ if (!rgb) return fallback;
98
+ const l = luminance(rgb);
99
+ return 1.05 / (l + 0.05) >= (l + 0.05) / 0.05 ? "#FFFFFF" : "#000000";
100
+ }
101
+ function resolveRNTheme(dark, override = {}) {
102
+ const pal = (0, import_core.mushiPalette)(dark ? "dark" : "light");
103
+ const base = {
104
+ bg: dark ? pal.paper : pal.paperRaised,
105
+ fg: pal.ink,
106
+ muted: pal.inkMuted,
107
+ surface: dark ? pal.paperRaised : pal.paper,
108
+ border: pal.ruleStrong,
109
+ accent: pal.ink,
110
+ accentFg: dark ? pal.paper : pal.paperRaised,
111
+ success: pal.ok,
112
+ error: pal.danger,
113
+ fontFamily: void 0,
114
+ radius: import_core.MUSHI_RADIUS.card
115
+ };
116
+ const defined = Object.fromEntries(
117
+ Object.entries(override).filter(([, v]) => v !== void 0 && v !== null && v !== "")
118
+ );
119
+ const merged = { ...base, ...defined };
120
+ if (defined.accent && !defined.accentFg) merged.accentFg = contrastingText(defined.accent, base.accentFg);
121
+ return {
122
+ ...merged,
123
+ backdrop: dark ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.35)",
124
+ disabled: dark ? import_core.MUSHI_CONTROL_DISABLED.dark : import_core.MUSHI_CONTROL_DISABLED.light,
125
+ disabledFg: pal.inkFaint
126
+ };
127
+ }
128
+
129
+ // src/components/MushiUpdateToast.tsx
130
+ var import_jsx_runtime = require("react/jsx-runtime");
131
+ var AUTO_HIDE_MS = 8e3;
132
+ var MushiUpdateToast = ({ message, onView, onDismiss, theme }) => {
133
+ const colors = resolveRNTheme((0, import_react_native.useColorScheme)() === "dark", theme);
134
+ const copy = (0, import_reporter_ui.reporterCopy)(
135
+ (0, import_reporter_ui.resolveReporterLocale)(typeof navigator !== "undefined" ? navigator.language : void 0)
136
+ ).ui;
137
+ const font = colors.fontFamily ? { fontFamily: colors.fontFamily } : null;
138
+ (0, import_react.useEffect)(() => {
139
+ const timer = setTimeout(onDismiss, AUTO_HIDE_MS);
140
+ return () => clearTimeout(timer);
141
+ }, [onDismiss]);
142
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
143
+ import_react_native.View,
144
+ {
145
+ style: [s.toast, { backgroundColor: colors.bg, borderColor: colors.border, borderRadius: colors.radius }],
146
+ accessibilityLiveRegion: "polite",
147
+ accessibilityRole: "alert",
148
+ children: [
149
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: [s.text, { color: colors.fg }, font], numberOfLines: 2, children: message }),
150
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
151
+ import_react_native.TouchableOpacity,
152
+ {
153
+ onPress: onView,
154
+ accessibilityRole: "button",
155
+ style: [s.btn, { backgroundColor: colors.accent, borderRadius: colors.radius }],
156
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: [{ color: colors.accentFg, fontWeight: "600" }, font], children: copy.view })
157
+ }
158
+ ),
159
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
160
+ import_react_native.TouchableOpacity,
161
+ {
162
+ onPress: onDismiss,
163
+ accessibilityRole: "button",
164
+ accessibilityLabel: copy.done,
165
+ hitSlop: { top: 10, bottom: 10, left: 10, right: 10 },
166
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: [{ color: colors.muted, fontSize: 18 }, font], children: "\xD7" })
167
+ }
168
+ )
169
+ ]
170
+ }
171
+ );
172
+ };
173
+ var s = import_react_native.StyleSheet.create({
174
+ toast: {
175
+ position: "absolute",
176
+ left: 16,
177
+ right: 16,
178
+ bottom: 32,
179
+ flexDirection: "row",
180
+ alignItems: "center",
181
+ gap: 10,
182
+ paddingVertical: 10,
183
+ paddingHorizontal: 14,
184
+ borderWidth: import_react_native.StyleSheet.hairlineWidth,
185
+ shadowColor: import_core2.MUSHI_SHADOW_INK,
186
+ shadowOpacity: 0.18,
187
+ shadowRadius: 12,
188
+ shadowOffset: { width: 0, height: 6 },
189
+ elevation: 6
190
+ },
191
+ text: { flex: 1, fontSize: 14 },
192
+ btn: { paddingHorizontal: 12, paddingVertical: 6 }
193
+ });
194
+
195
+ // src/optional-modules.ts
196
+ function unwrap(mod, key) {
197
+ if (!mod || typeof mod !== "object" && typeof mod !== "function") return null;
198
+ if (key in mod) return mod;
199
+ const def = mod.default;
200
+ return def && (typeof def === "object" || typeof def === "function") && key in def ? def : null;
201
+ }
202
+ function resolveNetInfo(injected) {
203
+ if (injected) return unwrap(injected, "addEventListener");
204
+ try {
205
+ return unwrap(require("@react-native-community/netinfo"), "addEventListener");
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+ function resolveViewShot(injected) {
211
+ if (injected) return unwrap(injected, "captureScreen");
212
+ try {
213
+ return unwrap(require("react-native-view-shot"), "captureScreen");
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+ function resolveExpoSensors(injected) {
219
+ if (injected) return unwrap(injected, "Accelerometer");
220
+ try {
221
+ return unwrap(require("expo-sensors"), "Accelerometer");
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ // src/capture/console-capture.ts
228
+ var import_core3 = require("@mushi-mushi/core");
57
229
  function setupConsoleCapture(maxEntries = 100) {
58
230
  const entries = [];
59
231
  const methods = ["log", "warn", "error"];
@@ -64,7 +236,7 @@ function setupConsoleCapture(maxEntries = 100) {
64
236
  console[method] = (...args) => {
65
237
  entries.push({
66
238
  level: method,
67
- message: (0, import_core.scrubPii)(args.map(String).join(" ").slice(0, 500)),
239
+ message: (0, import_core3.scrubPii)(args.map(String).join(" ").slice(0, 500)),
68
240
  timestamp: Date.now()
69
241
  });
70
242
  if (entries.length > maxEntries) entries.shift();
@@ -140,28 +312,28 @@ function stripQueryParams(url) {
140
312
  }
141
313
 
142
314
  // src/capture/device-info.ts
143
- var import_react_native = require("react-native");
315
+ var import_react_native2 = require("react-native");
144
316
  function getDeviceInfo() {
145
- const { width, height } = import_react_native.Dimensions.get("window");
317
+ const { width, height } = import_react_native2.Dimensions.get("window");
146
318
  const locale = (
147
319
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
148
- import_react_native.NativeModules.SettingsManager?.settings?.AppleLocale ?? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
149
- import_react_native.NativeModules.I18nManager?.localeIdentifier ?? "en"
320
+ import_react_native2.NativeModules.SettingsManager?.settings?.AppleLocale ?? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
321
+ import_react_native2.NativeModules.I18nManager?.localeIdentifier ?? "en"
150
322
  );
151
323
  let timezone = "UTC";
152
324
  try {
153
325
  timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
154
326
  } catch {
155
327
  }
156
- const appVersion = import_react_native.NativeModules.RNDeviceInfo?.appVersion ?? import_react_native.NativeModules.RNBuildConfig?.VERSION_NAME ?? String(import_react_native.Platform.Version);
328
+ const appVersion = import_react_native2.NativeModules.RNDeviceInfo?.appVersion ?? import_react_native2.NativeModules.RNBuildConfig?.VERSION_NAME ?? String(import_react_native2.Platform.Version);
157
329
  return {
158
- platform: import_react_native.Platform.OS,
159
- osVersion: import_react_native.Platform.Version,
330
+ platform: import_react_native2.Platform.OS,
331
+ osVersion: import_react_native2.Platform.Version,
160
332
  screenWidth: width,
161
333
  screenHeight: height,
162
- pixelRatio: import_react_native.PixelRatio.get(),
163
- fontScale: import_react_native.PixelRatio.getFontScale(),
164
- systemName: import_react_native.Platform.OS,
334
+ pixelRatio: import_react_native2.PixelRatio.get(),
335
+ fontScale: import_react_native2.PixelRatio.getFontScale(),
336
+ systemName: import_react_native2.Platform.OS,
165
337
  locale,
166
338
  timezone,
167
339
  appVersion
@@ -193,7 +365,7 @@ function getDeviceFingerprintHash(device) {
193
365
  }
194
366
 
195
367
  // src/storage/async-storage-queue.ts
196
- var import_core2 = require("@mushi-mushi/core");
368
+ var import_core4 = require("@mushi-mushi/core");
197
369
 
198
370
  // src/storage/secure-storage.ts
199
371
  var REPORTER_TOKEN_KEY = "@mushi:reporter_token";
@@ -393,19 +565,19 @@ var AsyncStorageQueue = class {
393
565
  scrubReportPii(report) {
394
566
  const next = { ...report };
395
567
  if (typeof next.description === "string") {
396
- next.description = (0, import_core2.scrubPii)(next.description);
568
+ next.description = (0, import_core4.scrubPii)(next.description);
397
569
  }
398
570
  if (typeof next.summary === "string") {
399
- next.summary = (0, import_core2.scrubPii)(next.summary);
571
+ next.summary = (0, import_core4.scrubPii)(next.summary);
400
572
  }
401
573
  if (Array.isArray(next.breadcrumbs)) {
402
574
  next.breadcrumbs = next.breadcrumbs.map(
403
- (b) => typeof b?.message === "string" ? { ...b, message: (0, import_core2.scrubPii)(b.message) } : b
575
+ (b) => typeof b?.message === "string" ? { ...b, message: (0, import_core4.scrubPii)(b.message) } : b
404
576
  );
405
577
  }
406
578
  if (Array.isArray(next.consoleLogs)) {
407
579
  next.consoleLogs = next.consoleLogs.map(
408
- (entry) => typeof entry?.message === "string" ? { ...entry, message: (0, import_core2.scrubPii)(entry.message) } : entry
580
+ (entry) => typeof entry?.message === "string" ? { ...entry, message: (0, import_core4.scrubPii)(entry.message) } : entry
409
581
  );
410
582
  }
411
583
  return next;
@@ -447,7 +619,7 @@ async function sendWithRetry(item, ctx, attempt = 0) {
447
619
  try {
448
620
  const res = await fetch(`${ctx.apiEndpoint}/v1/reports`, {
449
621
  method: "POST",
450
- headers: (0, import_core2.buildSdkIngestHeaders)({
622
+ headers: (0, import_core4.buildSdkIngestHeaders)({
451
623
  apiKey: ctx.apiKey,
452
624
  projectId: ctx.projectId,
453
625
  sdkPackage: ctx.sdkPackage,
@@ -456,7 +628,7 @@ async function sendWithRetry(item, ctx, attempt = 0) {
456
628
  internalKind
457
629
  }),
458
630
  body: JSON.stringify(item.report),
459
- [import_core2.MUSHI_INTERNAL_INIT_MARKER]: internalKind
631
+ [import_core4.MUSHI_INTERNAL_INIT_MARKER]: internalKind
460
632
  });
461
633
  if ((res.status === 429 || res.status >= 500) && attempt < 3) {
462
634
  await sleep(delay);
@@ -476,51 +648,111 @@ function sleep(ms) {
476
648
  }
477
649
 
478
650
  // src/components/MushiBottomSheet.tsx
479
- var import_react = require("react");
480
- var import_react_native2 = require("react-native");
481
- var import_core3 = require("@mushi-mushi/core");
651
+ var import_react2 = require("react");
652
+ var import_react_native3 = require("react-native");
653
+ var import_core5 = require("@mushi-mushi/core");
654
+ var import_reporter_ui2 = require("@mushi-mushi/core/reporter-ui");
482
655
  var import_i18n = require("@mushi-mushi/web/i18n");
483
656
 
484
- // src/reporter-status.ts
485
- function reporterStatusShort(status) {
486
- switch (status) {
487
- case "new":
488
- case "queued":
489
- case "pending":
490
- case "submitted":
491
- return "Received";
492
- case "classified":
493
- case "triaged":
494
- case "grouped":
495
- case "dispatched":
496
- return "In review";
497
- case "fixing":
498
- return "Fixing";
499
- case "fixed":
500
- case "resolved":
501
- case "completed":
502
- return "Fixed";
503
- case "dismissed":
504
- return "Closed";
505
- default:
506
- return status.replace(/_/g, " ").slice(0, 16);
657
+ // src/reporter-thread.ts
658
+ var import_reporter_channels = require("@mushi-mushi/core/reporter-channels");
659
+ var THREAD_LOAD_TIMEOUT_MS = 12e3;
660
+ function settleWithin(p, ms) {
661
+ return new Promise((resolve) => {
662
+ const timer = setTimeout(() => resolve(null), ms);
663
+ p.then(
664
+ (value) => {
665
+ clearTimeout(timer);
666
+ resolve(value);
667
+ },
668
+ () => {
669
+ clearTimeout(timer);
670
+ resolve(null);
671
+ }
672
+ );
673
+ });
674
+ }
675
+ function loadReporterThread(load, reportId, timeoutMs = THREAD_LOAD_TIMEOUT_MS) {
676
+ let started;
677
+ try {
678
+ started = load(reportId);
679
+ } catch {
680
+ return Promise.resolve(null);
681
+ }
682
+ return settleWithin(started, timeoutMs);
683
+ }
684
+ async function markReporterReportRead(client, reporterToken, reportId) {
685
+ try {
686
+ const v2 = await (0, import_reporter_channels.reporterChannels)(client).markReportRead(reportId, reporterToken);
687
+ if (v2.ok) return v2.data?.marked_read ?? 0;
688
+ if (v2.error?.status !== 404) return 0;
689
+ const res = await client.listNotifications(reporterToken, { limit: 100 });
690
+ if (!res.ok) return 0;
691
+ const rows = res.data?.notifications ?? [];
692
+ const ids = rows.filter((n) => !n.read_at && n.payload?.reportId === reportId).map((n) => String(n.id));
693
+ let marked = 0;
694
+ for (const id of ids) {
695
+ const r = await client.markNotificationRead(id, reporterToken);
696
+ if (r.ok) marked++;
697
+ }
698
+ return marked;
699
+ } catch {
700
+ return 0;
701
+ }
702
+ }
703
+ function createReporterUpdateHub(fetchUpdates) {
704
+ const listeners = /* @__PURE__ */ new Set();
705
+ async function emit() {
706
+ let updates = null;
707
+ try {
708
+ updates = await fetchUpdates();
709
+ } catch {
710
+ return null;
711
+ }
712
+ if (updates) {
713
+ for (const cb of listeners) {
714
+ try {
715
+ cb(updates);
716
+ } catch {
717
+ }
718
+ }
719
+ }
720
+ return updates;
507
721
  }
722
+ return {
723
+ subscribe(cb) {
724
+ listeners.add(cb);
725
+ void emit();
726
+ return () => {
727
+ listeners.delete(cb);
728
+ };
729
+ },
730
+ emit,
731
+ listenerCount: () => listeners.size
732
+ };
508
733
  }
509
734
 
510
735
  // src/components/MushiBottomSheet.tsx
511
- var import_jsx_runtime = require("react/jsx-runtime");
512
- var { height: SCREEN_HEIGHT } = import_react_native2.Dimensions.get("window");
736
+ var import_jsx_runtime2 = require("react/jsx-runtime");
737
+ var { height: SCREEN_HEIGHT } = import_react_native3.Dimensions.get("window");
513
738
  var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
514
739
  var DISMISS_THRESHOLD = 80;
515
- var CATEGORY_KEYS = ["bug", "slow", "visual", "confusing", "other"];
516
740
  var CATEGORY_EMOJI = {
517
741
  bug: "\u{1F41B}",
518
742
  slow: "\u{1F422}",
519
743
  visual: "\u{1F3A8}",
520
744
  confusing: "\u{1F615}",
521
- other: "\u{1F4AC}"
745
+ idea: "\u{1F4A1}"
522
746
  };
523
- var VERIFIABLE_STATUSES = /* @__PURE__ */ new Set(["fixed", "resolved", "verified"]);
747
+ function commentsToTimeline(comments) {
748
+ return comments.map((c) => ({
749
+ kind: c.author_kind === "reporter" ? "reporter_comment" : "comment",
750
+ at: c.created_at,
751
+ text: c.body,
752
+ body: c.body,
753
+ comment_id: c.id
754
+ }));
755
+ }
524
756
  var MushiBottomSheet = ({
525
757
  visible,
526
758
  onClose,
@@ -529,77 +761,176 @@ var MushiBottomSheet = ({
529
761
  onClearScreenshot,
530
762
  screenshotSensitiveHint,
531
763
  assistantEnabled = false,
532
- assistantLabel = import_core3.MUSHI_COPY.assistantTab,
764
+ assistantLabel = import_core5.MUSHI_COPY.assistantTab,
533
765
  assistantGreeting,
534
766
  assistantSuggestions = [],
535
- inboxPollIntervalMs = 0
767
+ inboxPollIntervalMs = 0,
768
+ theme,
769
+ minDescriptionLength,
770
+ emailOptIn = true
536
771
  }) => {
537
772
  const mushi = useMushiContext();
538
773
  const t = (0, import_i18n.getLocale)();
774
+ const locale = (0, import_reporter_ui2.resolveReporterLocale)(
775
+ typeof navigator !== "undefined" ? navigator.language : void 0
776
+ );
777
+ const rc = (0, import_reporter_ui2.reporterCopy)(locale);
539
778
  const greeting = assistantGreeting ?? t.assistant.defaultGreeting;
540
- const scheme = (0, import_react_native2.useColorScheme)();
779
+ const scheme = (0, import_react_native3.useColorScheme)();
541
780
  const dark = scheme === "dark";
542
- const translateY = (0, import_react.useRef)(new import_react_native2.Animated.Value(SHEET_HEIGHT)).current;
543
- const backdropOpacity = (0, import_react.useRef)(new import_react_native2.Animated.Value(0)).current;
544
- const [category, setCategory] = (0, import_react.useState)(null);
545
- const [description, setDescription] = (0, import_react.useState)("");
546
- const [phase, setPhase] = (0, import_react.useState)("form");
547
- const [submitError, setSubmitError] = (0, import_react.useState)(null);
548
- const [sheetTab, setSheetTab] = (0, import_react.useState)("report");
549
- const [inboxReports, setInboxReports] = (0, import_react.useState)([]);
550
- const [inboxLoading, setInboxLoading] = (0, import_react.useState)(false);
551
- const [selectedReportId, setSelectedReportId] = (0, import_react.useState)(null);
552
- const [threadComments, setThreadComments] = (0, import_react.useState)([]);
553
- const [replyText, setReplyText] = (0, import_react.useState)("");
554
- const [assistantInput, setAssistantInput] = (0, import_react.useState)("");
555
- const [assistantThreadId, setAssistantThreadId] = (0, import_react.useState)(null);
556
- const [assistantSending, setAssistantSending] = (0, import_react.useState)(false);
557
- const [assistantError, setAssistantError] = (0, import_react.useState)(null);
558
- const [assistantTurns, setAssistantTurns] = (0, import_react.useState)([]);
559
- const [screenshotAttached, setScreenshotAttached] = (0, import_react.useState)(true);
560
- const loadInbox = (0, import_react.useCallback)(async () => {
561
- if (!mushi?.listMyReports) return;
562
- setInboxLoading(true);
563
- try {
564
- const rows = await mushi.listMyReports();
565
- setInboxReports(rows);
566
- } finally {
567
- setInboxLoading(false);
568
- }
569
- }, [mushi]);
570
- (0, import_react.useEffect)(() => {
781
+ const translateY = (0, import_react2.useRef)(new import_react_native3.Animated.Value(SHEET_HEIGHT)).current;
782
+ const backdropOpacity = (0, import_react2.useRef)(new import_react_native3.Animated.Value(0)).current;
783
+ const [category, setCategory] = (0, import_react2.useState)(null);
784
+ const [description, setDescription] = (0, import_react2.useState)("");
785
+ const [phase, setPhase] = (0, import_react2.useState)("form");
786
+ const [submitError, setSubmitError] = (0, import_react2.useState)(null);
787
+ const [sheetTab, setSheetTab] = (0, import_react2.useState)("report");
788
+ const [inboxReports, setInboxReports] = (0, import_react2.useState)([]);
789
+ const [inboxLoaded, setInboxLoaded] = (0, import_react2.useState)(false);
790
+ const [inboxLoading, setInboxLoading] = (0, import_react2.useState)(false);
791
+ const [selectedReportId, setSelectedReportId] = (0, import_react2.useState)(null);
792
+ const selectedRef = (0, import_react2.useRef)(null);
793
+ const [threadItems, setThreadItems] = (0, import_react2.useState)([]);
794
+ const [threadStatus, setThreadStatus] = (0, import_react2.useState)("idle");
795
+ const [replyText, setReplyText] = (0, import_react2.useState)("");
796
+ const [replyState, setReplyState] = (0, import_react2.useState)("idle");
797
+ const [feedbackSending, setFeedbackSending] = (0, import_react2.useState)(false);
798
+ const [feedbackError, setFeedbackError] = (0, import_react2.useState)(false);
799
+ const [assistantInput, setAssistantInput] = (0, import_react2.useState)("");
800
+ const [assistantThreadId, setAssistantThreadId] = (0, import_react2.useState)(null);
801
+ const [assistantSending, setAssistantSending] = (0, import_react2.useState)(false);
802
+ const [assistantError, setAssistantError] = (0, import_react2.useState)(null);
803
+ const [assistantTurns, setAssistantTurns] = (0, import_react2.useState)([]);
804
+ const [screenshotAttached, setScreenshotAttached] = (0, import_react2.useState)(true);
805
+ const [emailState, setEmailState] = (0, import_react2.useState)("hidden");
806
+ const [emailInput, setEmailInput] = (0, import_react2.useState)("");
807
+ const [reduceMotion, setReduceMotion] = (0, import_react2.useState)(false);
808
+ (0, import_react2.useEffect)(() => {
809
+ let alive = true;
810
+ import_react_native3.AccessibilityInfo?.isReduceMotionEnabled?.().then((on) => alive && setReduceMotion(Boolean(on))).catch(() => void 0);
811
+ const sub = import_react_native3.AccessibilityInfo?.addEventListener?.("reduceMotionChanged", (on) => setReduceMotion(Boolean(on)));
812
+ return () => {
813
+ alive = false;
814
+ sub?.remove?.();
815
+ };
816
+ }, []);
817
+ const loadInbox = (0, import_react2.useCallback)(
818
+ async (opts = {}) => {
819
+ if (!mushi?.listMyReports) return;
820
+ if (!opts.silent) setInboxLoading(true);
821
+ try {
822
+ const rows = await mushi.listMyReports();
823
+ setInboxReports(rows.filter((r) => !(0, import_reporter_ui2.reporterStatus)(r, locale).hidden));
824
+ setInboxLoaded(true);
825
+ } catch {
826
+ } finally {
827
+ if (!opts.silent) setInboxLoading(false);
828
+ }
829
+ },
830
+ [mushi, locale]
831
+ );
832
+ (0, import_react2.useEffect)(() => {
571
833
  if (visible) {
572
834
  setScreenshotAttached(true);
573
835
  setSheetTab(preferredTab);
574
836
  if (preferredTab === "inbox") void loadInbox();
575
837
  }
576
838
  }, [visible, preferredTab, loadInbox]);
577
- (0, import_react.useEffect)(() => {
839
+ (0, import_react2.useEffect)(() => {
578
840
  if (!visible || sheetTab !== "inbox" || inboxPollIntervalMs <= 0) return;
579
841
  const timer = setInterval(() => {
580
- void loadInbox();
842
+ void loadInbox({ silent: true });
581
843
  }, inboxPollIntervalMs);
582
844
  return () => clearInterval(timer);
583
845
  }, [visible, sheetTab, inboxPollIntervalMs, loadInbox]);
584
- const openThread = (0, import_react.useCallback)(async (reportId) => {
585
- setSelectedReportId(reportId);
586
- if (!mushi?.listMyComments) return;
587
- const comments = await mushi.listMyComments(reportId);
588
- setThreadComments(comments);
589
- }, [mushi]);
590
- const sendReply = (0, import_react.useCallback)(async () => {
591
- if (!mushi?.replyToReport || !selectedReportId || !replyText.trim()) return;
592
- await mushi.replyToReport(selectedReportId, replyText.trim());
593
- setReplyText("");
594
- await openThread(selectedReportId);
595
- }, [mushi, selectedReportId, replyText, openThread]);
596
- const submitFeedback = (0, import_react.useCallback)(async (signal) => {
597
- if (!mushi?.submitFeedbackSignal || !selectedReportId) return;
598
- await mushi.submitFeedbackSignal(selectedReportId, signal);
599
- await loadInbox();
600
- await openThread(selectedReportId);
601
- }, [mushi, selectedReportId, loadInbox, openThread]);
602
- const sendAssistant = (0, import_react.useCallback)(async (message) => {
846
+ const openThread = (0, import_react2.useCallback)(
847
+ async (reportId, opts = {}) => {
848
+ selectedRef.current = reportId;
849
+ setSelectedReportId(reportId);
850
+ if (!opts.silent) {
851
+ setThreadItems([]);
852
+ setThreadStatus("loading");
853
+ setReplyState("idle");
854
+ setFeedbackError(false);
855
+ }
856
+ let items = null;
857
+ const detail = mushi?.loadMyReportDetail ? await settleWithin(mushi.loadMyReportDetail(reportId), THREAD_LOAD_TIMEOUT_MS) : "unsupported";
858
+ if (detail && detail !== "unsupported") {
859
+ items = detail.timeline;
860
+ } else if (detail === "unsupported") {
861
+ const load = mushi?.loadMyThread ?? mushi?.listMyComments;
862
+ const comments = load ? await loadReporterThread(load, reportId) : null;
863
+ items = comments ? commentsToTimeline(comments) : null;
864
+ }
865
+ if (selectedRef.current !== reportId) return;
866
+ if (items === null) {
867
+ setThreadStatus((prev) => opts.silent && prev === "ready" ? "ready" : "error");
868
+ return;
869
+ }
870
+ setThreadItems(items);
871
+ setThreadStatus("ready");
872
+ if (!opts.silent && mushi?.markReportRead) {
873
+ mushi.markReportRead(reportId).then((marked) => {
874
+ if (marked > 0) {
875
+ setInboxReports((rows) => rows.map((r) => r.id === reportId ? { ...r, unread_count: 0 } : r));
876
+ }
877
+ }).catch(() => void 0);
878
+ }
879
+ },
880
+ [mushi]
881
+ );
882
+ const closeThread = (0, import_react2.useCallback)(() => {
883
+ selectedRef.current = null;
884
+ setSelectedReportId(null);
885
+ setThreadStatus("idle");
886
+ }, []);
887
+ const sendReply = (0, import_react2.useCallback)(async () => {
888
+ const body = replyText.trim();
889
+ const reportId = selectedReportId;
890
+ if (!mushi?.replyToReport || !reportId || !body || replyState === "sending") return;
891
+ setReplyState("sending");
892
+ try {
893
+ const comment = await settleWithin(mushi.replyToReport(reportId, body), THREAD_LOAD_TIMEOUT_MS);
894
+ if (!comment) {
895
+ setReplyState("failed");
896
+ return;
897
+ }
898
+ setReplyText("");
899
+ setReplyState("idle");
900
+ if (selectedRef.current === reportId) {
901
+ setThreadItems((prev) => [
902
+ ...prev,
903
+ { kind: "reporter_comment", at: comment.created_at, text: comment.body, body: comment.body, comment_id: comment.id }
904
+ ]);
905
+ void openThread(reportId, { silent: true });
906
+ }
907
+ } catch {
908
+ setReplyState("failed");
909
+ }
910
+ }, [mushi, selectedReportId, replyText, replyState, openThread]);
911
+ const submitFeedback = (0, import_react2.useCallback)(
912
+ async (signal) => {
913
+ const reportId = selectedReportId;
914
+ if (!mushi?.submitFeedbackSignal || !reportId || feedbackSending) return;
915
+ setFeedbackSending(true);
916
+ setFeedbackError(false);
917
+ try {
918
+ const outcome = await mushi.submitFeedbackSignal(reportId, signal);
919
+ if (!outcome) {
920
+ setFeedbackError(true);
921
+ return;
922
+ }
923
+ await loadInbox({ silent: true });
924
+ await openThread(reportId, { silent: true });
925
+ } catch {
926
+ setFeedbackError(true);
927
+ } finally {
928
+ setFeedbackSending(false);
929
+ }
930
+ },
931
+ [mushi, selectedReportId, feedbackSending, loadInbox, openThread]
932
+ );
933
+ const sendAssistant = (0, import_react2.useCallback)(async (message) => {
603
934
  if (!mushi?.askAssistant || !message.trim() || assistantSending) return;
604
935
  const trimmed = message.trim();
605
936
  setAssistantInput("");
@@ -624,37 +955,44 @@ var MushiBottomSheet = ({
624
955
  setAssistantSending(false);
625
956
  }
626
957
  }, [mushi, assistantSending, assistantThreadId]);
627
- const resetForm = (0, import_react.useCallback)(() => {
958
+ const resetForm = (0, import_react2.useCallback)(() => {
628
959
  setCategory(null);
629
960
  setDescription("");
630
961
  setPhase("form");
962
+ setEmailState("hidden");
963
+ setEmailInput("");
631
964
  }, []);
632
- const animateIn = (0, import_react.useCallback)(() => {
633
- import_react_native2.Animated.parallel([
634
- import_react_native2.Animated.spring(translateY, {
965
+ const animateIn = (0, import_react2.useCallback)(() => {
966
+ if (reduceMotion) {
967
+ translateY.setValue(0);
968
+ backdropOpacity.setValue(1);
969
+ return;
970
+ }
971
+ import_react_native3.Animated.parallel([
972
+ import_react_native3.Animated.spring(translateY, {
635
973
  toValue: 0,
636
974
  useNativeDriver: true,
637
975
  damping: 20,
638
976
  stiffness: 200
639
977
  }),
640
- import_react_native2.Animated.timing(backdropOpacity, {
978
+ import_react_native3.Animated.timing(backdropOpacity, {
641
979
  toValue: 1,
642
980
  duration: 250,
643
981
  useNativeDriver: true
644
982
  })
645
983
  ]).start();
646
- }, [translateY, backdropOpacity]);
647
- const animateOut = (0, import_react.useCallback)(
984
+ }, [translateY, backdropOpacity, reduceMotion]);
985
+ const animateOut = (0, import_react2.useCallback)(
648
986
  (cb) => {
649
- import_react_native2.Animated.parallel([
650
- import_react_native2.Animated.timing(translateY, {
987
+ import_react_native3.Animated.parallel([
988
+ import_react_native3.Animated.timing(translateY, {
651
989
  toValue: SHEET_HEIGHT,
652
- duration: 220,
990
+ duration: reduceMotion ? 0 : 220,
653
991
  useNativeDriver: true
654
992
  }),
655
- import_react_native2.Animated.timing(backdropOpacity, {
993
+ import_react_native3.Animated.timing(backdropOpacity, {
656
994
  toValue: 0,
657
- duration: 200,
995
+ duration: reduceMotion ? 0 : 200,
658
996
  useNativeDriver: true
659
997
  })
660
998
  ]).start(() => {
@@ -662,16 +1000,16 @@ var MushiBottomSheet = ({
662
1000
  cb?.();
663
1001
  });
664
1002
  },
665
- [translateY, backdropOpacity, resetForm]
1003
+ [translateY, backdropOpacity, resetForm, reduceMotion]
666
1004
  );
667
- (0, import_react.useEffect)(() => {
1005
+ (0, import_react2.useEffect)(() => {
668
1006
  if (visible) animateIn();
669
1007
  }, [visible, animateIn]);
670
- const handleClose = (0, import_react.useCallback)(() => {
1008
+ const handleClose = (0, import_react2.useCallback)(() => {
671
1009
  animateOut(onClose);
672
1010
  }, [animateOut, onClose]);
673
- const panResponder = (0, import_react.useRef)(
674
- import_react_native2.PanResponder.create({
1011
+ const panResponder = (0, import_react2.useRef)(
1012
+ import_react_native3.PanResponder.create({
675
1013
  onStartShouldSetPanResponder: () => true,
676
1014
  onMoveShouldSetPanResponder: (_, g) => g.dy > 4,
677
1015
  onPanResponderMove: (_, g) => {
@@ -681,7 +1019,7 @@ var MushiBottomSheet = ({
681
1019
  if (g.dy > DISMISS_THRESHOLD) {
682
1020
  handleClose();
683
1021
  } else {
684
- import_react_native2.Animated.spring(translateY, {
1022
+ import_react_native3.Animated.spring(translateY, {
685
1023
  toValue: 0,
686
1024
  useNativeDriver: true,
687
1025
  damping: 20,
@@ -691,20 +1029,43 @@ var MushiBottomSheet = ({
691
1029
  }
692
1030
  })
693
1031
  ).current;
694
- const handleSubmit = async () => {
695
- if (!category || !description.trim() || !mushi) return;
696
- const trimmed = description.trim();
697
- if (trimmed.length < 20) {
698
- setSubmitError("Please write at least 20 characters so we can understand the issue.");
1032
+ const offerEmail = (0, import_react2.useCallback)(async () => {
1033
+ if (!emailOptIn || !mushi?.getNotificationPrefs) return;
1034
+ try {
1035
+ const prefs = await mushi.getNotificationPrefs();
1036
+ if (prefs?.available.email && !prefs.email_verified && !prefs.email_pending) {
1037
+ setEmailInput(mushi.emailPrefill?.() ?? "");
1038
+ setEmailState("offer");
1039
+ }
1040
+ } catch {
1041
+ }
1042
+ }, [emailOptIn, mushi]);
1043
+ const submitEmail = (0, import_react2.useCallback)(async () => {
1044
+ const email = emailInput.trim();
1045
+ if (!(0, import_reporter_ui2.isPlausibleReporterEmail)(email)) {
1046
+ setEmailState("invalid");
699
1047
  return;
700
1048
  }
1049
+ if (!mushi?.setNotificationPrefs) return;
1050
+ setEmailState("sending");
1051
+ try {
1052
+ const res = await mushi.setNotificationPrefs({ email });
1053
+ setEmailState(res.ok ? "sent" : res.error?.status === 422 ? "invalid" : "failed");
1054
+ } catch {
1055
+ setEmailState("failed");
1056
+ }
1057
+ }, [emailInput, mushi]);
1058
+ const handleSubmit = async () => {
1059
+ const hasAttachment = Boolean(screenshotDataUrl && screenshotAttached);
1060
+ if (!mushi || !(0, import_reporter_ui2.reporterCanSend)(description, hasAttachment, minDescriptionLength)) return;
1061
+ const trimmed = description.trim();
701
1062
  setSubmitError(null);
702
1063
  setPhase("sending");
703
1064
  try {
704
1065
  const outcome = await mushi.submitReport({
705
- category,
1066
+ ...(0, import_reporter_ui2.reporterChipToReport)(category),
706
1067
  description: trimmed,
707
- screenshotDataUrl: screenshotDataUrl && screenshotAttached ? screenshotDataUrl : void 0
1068
+ screenshotDataUrl: hasAttachment ? screenshotDataUrl : void 0
708
1069
  });
709
1070
  if (!outcome.ok) {
710
1071
  if (outcome.failureKind === "credentials" || outcome.failureKind === "quota") {
@@ -722,174 +1083,465 @@ var MushiBottomSheet = ({
722
1083
  return;
723
1084
  }
724
1085
  setPhase("sent");
725
- setTimeout(handleClose, 1400);
1086
+ void offerEmail();
726
1087
  } catch {
727
1088
  setSubmitError("Something went wrong. Please try again.");
728
1089
  setPhase("error");
729
1090
  }
730
1091
  };
731
1092
  const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
732
- const pal = (0, import_core3.mushiPalette)(dark ? "dark" : "light");
733
- const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: import_core3.MUSHI_BANNER_NEON.bg, accentInk: import_core3.MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: import_core3.MUSHI_CONTROL_DISABLED.dark, disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: import_core3.MUSHI_BANNER_NEON.bg, accentInk: import_core3.MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: import_core3.MUSHI_CONTROL_DISABLED.light, disabledText: pal.inkFaint };
1093
+ const colors = resolveRNTheme(dark, theme);
1094
+ const font = colors.fontFamily ? { fontFamily: colors.fontFamily } : null;
1095
+ const radius = colors.radius;
734
1096
  const sheetTabs = ["report", "inbox", ...assistantEnabled ? ["assistant"] : []];
735
- const canSubmit = !!category && description.trim().length >= 20 && (phase === "form" || phase === "error");
736
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
737
- import_react_native2.KeyboardAvoidingView,
1097
+ const canSubmit = (0, import_reporter_ui2.reporterCanSend)(description, Boolean(activeScreenshot), minDescriptionLength) && (phase === "form" || phase === "error");
1098
+ const submitHint = canSubmit || phase === "sending" ? void 0 : rc.ui.addWords;
1099
+ const selectedReport = selectedReportId ? inboxReports.find((r) => r.id === selectedReportId) ?? null : null;
1100
+ const selectedView = selectedReport ? (0, import_reporter_ui2.reporterStatus)(selectedReport, locale) : null;
1101
+ const headerTitle = sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title;
1102
+ const renderThread = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
1103
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1104
+ import_react_native3.TouchableOpacity,
1105
+ {
1106
+ onPress: closeThread,
1107
+ accessibilityRole: "button",
1108
+ accessibilityLabel: t.widget.back,
1109
+ hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
1110
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.Text, { style: [{ color: colors.accent, marginBottom: 8 }, font], children: [
1111
+ "\u2190 ",
1112
+ t.widget.back
1113
+ ] })
1114
+ }
1115
+ ),
1116
+ selectedReport && selectedView ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: [s2.headerCard, { backgroundColor: colors.surface, borderRadius: radius, borderColor: colors.border }], children: [
1117
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.pill, { color: colors.fg, borderColor: colors.border }, font], children: selectedView.label }),
1118
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.muted, fontSize: 12, marginTop: 4 }, font], children: selectedView.detail }),
1119
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.fg, marginTop: 8 }, font], numberOfLines: 4, children: selectedReport.title ?? selectedReport.summary ?? selectedReport.description ?? "" })
1120
+ ] }) : null,
1121
+ threadStatus === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { accessibilityLabel: t.flows.reports.loading, accessibilityLiveRegion: "polite", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { style: [s2.skeleton, { backgroundColor: colors.surface, width: `${90 - i * 18}%` }] }, i)) }) : threadStatus === "error" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: s2.errorRow, accessibilityLiveRegion: "polite", children: [
1122
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.error, flex: 1 }, font], accessibilityRole: "alert", children: rc.ui.loadError }),
1123
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1124
+ import_react_native3.TouchableOpacity,
1125
+ {
1126
+ onPress: () => selectedReportId && void openThread(selectedReportId),
1127
+ accessibilityRole: "button",
1128
+ style: [s2.retryBtn, { borderColor: colors.border, borderRadius: radius }],
1129
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.fg, fontWeight: "600" }, font], children: rc.ui.retry })
1130
+ }
1131
+ )
1132
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
1133
+ threadItems.map(
1134
+ (item, idx) => (0, import_reporter_ui2.isReporterConversation)(item) ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1135
+ import_react_native3.View,
1136
+ {
1137
+ style: [
1138
+ s2.threadBubble,
1139
+ {
1140
+ backgroundColor: colors.surface,
1141
+ alignSelf: item.kind === "reporter_comment" ? "flex-end" : "flex-start"
1142
+ }
1143
+ ],
1144
+ children: [
1145
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.muted, fontSize: 11 }, font], children: item.kind === "reporter_comment" ? rc.ui.you : rc.ui.developer }),
1146
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.fg }, font], children: (0, import_reporter_ui2.reporterTimelineEntryText)(item, locale) })
1147
+ ]
1148
+ },
1149
+ `${item.kind}-${item.comment_id ?? idx}`
1150
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1151
+ import_react_native3.Text,
1152
+ {
1153
+ style: [{ color: colors.muted, fontSize: 12, textAlign: "center", marginVertical: 6 }, font],
1154
+ children: (0, import_reporter_ui2.reporterTimelineEntryText)(item, locale)
1155
+ },
1156
+ `${item.kind}-${item.at}-${idx}`
1157
+ )
1158
+ ),
1159
+ !threadItems.some(import_reporter_ui2.isReporterConversation) ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.muted, marginBottom: 8 }, font], children: rc.ui.noReplies }) : null
1160
+ ] }),
1161
+ replyState === "sending" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: [s2.threadBubble, { backgroundColor: colors.surface, alignSelf: "flex-end", opacity: 0.7 }], children: [
1162
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.muted, fontSize: 11 }, font], children: rc.ui.sending }),
1163
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.fg }, font], children: replyText.trim() })
1164
+ ] }) : null,
1165
+ selectedView?.canVerify ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: s2.verifyRow, children: [
1166
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1167
+ import_react_native3.TouchableOpacity,
1168
+ {
1169
+ style: [s2.verifyBtn, { backgroundColor: colors.accent, borderRadius: radius }],
1170
+ onPress: () => void submitFeedback("confirms"),
1171
+ disabled: feedbackSending,
1172
+ accessibilityRole: "button",
1173
+ accessibilityState: { disabled: feedbackSending, busy: feedbackSending },
1174
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.submitText, { color: colors.accentFg }, font], children: rc.ui.yes })
1175
+ }
1176
+ ),
1177
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1178
+ import_react_native3.TouchableOpacity,
1179
+ {
1180
+ style: [s2.verifyBtn, { backgroundColor: colors.border, borderRadius: radius }],
1181
+ onPress: () => void submitFeedback("not_fixed"),
1182
+ disabled: feedbackSending,
1183
+ accessibilityRole: "button",
1184
+ accessibilityState: { disabled: feedbackSending, busy: feedbackSending },
1185
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.submitText, { color: colors.fg }, font], children: rc.ui.notYet })
1186
+ }
1187
+ )
1188
+ ] }) : null,
1189
+ feedbackError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.error, marginBottom: 8 }, font], accessibilityRole: "alert", children: rc.ui.sendFailed }) : null,
1190
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: s2.composer, children: [
1191
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1192
+ import_react_native3.TextInput,
1193
+ {
1194
+ style: [
1195
+ s2.input,
1196
+ s2.composerInput,
1197
+ { backgroundColor: colors.surface, color: colors.fg, borderColor: colors.border, borderRadius: radius },
1198
+ font
1199
+ ],
1200
+ placeholder: t.flows.thread.replyPlaceholder,
1201
+ placeholderTextColor: colors.muted,
1202
+ value: replyText,
1203
+ onChangeText: (text) => {
1204
+ setReplyText(text.slice(0, 2e3));
1205
+ if (replyState === "failed") setReplyState("idle");
1206
+ },
1207
+ multiline: true,
1208
+ maxLength: 2e3,
1209
+ editable: replyState !== "sending",
1210
+ accessibilityLabel: t.flows.thread.replyPlaceholder
1211
+ }
1212
+ ),
1213
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1214
+ import_react_native3.TouchableOpacity,
1215
+ {
1216
+ style: [
1217
+ s2.sendBtn,
1218
+ {
1219
+ backgroundColor: replyText.trim() && replyState !== "sending" ? colors.accent : colors.disabled,
1220
+ borderRadius: radius
1221
+ }
1222
+ ],
1223
+ onPress: () => void sendReply(),
1224
+ disabled: !replyText.trim() || replyState === "sending",
1225
+ accessibilityRole: "button",
1226
+ accessibilityLabel: replyState === "failed" ? rc.ui.retry : t.flows.thread.send,
1227
+ accessibilityState: { disabled: !replyText.trim() || replyState === "sending", busy: replyState === "sending" },
1228
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1229
+ import_react_native3.Text,
1230
+ {
1231
+ style: [
1232
+ s2.submitText,
1233
+ { color: replyText.trim() && replyState !== "sending" ? colors.accentFg : colors.disabledFg },
1234
+ font
1235
+ ],
1236
+ children: replyState === "failed" ? rc.ui.retry : t.flows.thread.send
1237
+ }
1238
+ )
1239
+ }
1240
+ )
1241
+ ] }),
1242
+ replyState === "failed" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.Text, { style: [{ color: colors.error, marginTop: 6, fontSize: 12 }, font], accessibilityRole: "alert", children: [
1243
+ rc.ui.sendFailed,
1244
+ " \xB7 ",
1245
+ rc.ui.retry
1246
+ ] }) : null
1247
+ ] });
1248
+ const renderInboxList = () => {
1249
+ if (inboxLoading && !inboxLoaded) {
1250
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.muted }, font], children: t.flows.reports.loading });
1251
+ }
1252
+ if (inboxReports.length === 0) {
1253
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.muted }, font], children: rc.ui.empty });
1254
+ }
1255
+ return inboxReports.map((r) => {
1256
+ const view = (0, import_reporter_ui2.reporterStatus)(r, locale);
1257
+ const unread = (r.unread_count ?? 0) > 0;
1258
+ const title = (r.title ?? r.summary ?? r.description ?? "Report").slice(0, 80);
1259
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1260
+ import_react_native3.TouchableOpacity,
1261
+ {
1262
+ style: [s2.inboxRow, { borderColor: colors.border }],
1263
+ onPress: () => void openThread(r.id),
1264
+ accessibilityRole: "button",
1265
+ accessibilityLabel: `${view.label}. ${title}${unread ? `. ${r.unread_count} new` : ""}`,
1266
+ children: [
1267
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.fg, fontWeight: unread ? "700" : "600" }, font], numberOfLines: 1, children: title }),
1268
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.Text, { style: [{ color: unread ? colors.fg : colors.muted, fontSize: 12, marginTop: 2 }, font], numberOfLines: 1, children: [
1269
+ view.label,
1270
+ view.othersNote ? ` \xB7 ${view.othersNote}` : ""
1271
+ ] }),
1272
+ unread && r.last_event_preview ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.fg, fontSize: 12, fontWeight: "700", marginTop: 2 }, font], numberOfLines: 1, children: r.last_event_preview }) : null
1273
+ ]
1274
+ },
1275
+ r.id
1276
+ );
1277
+ });
1278
+ };
1279
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, onRequestClose: handleClose, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1280
+ import_react_native3.KeyboardAvoidingView,
738
1281
  {
739
- style: s.flex,
740
- behavior: import_react_native2.Platform.OS === "ios" ? "padding" : "height",
741
- keyboardVerticalOffset: import_react_native2.Platform.OS === "ios" ? 0 : 20,
1282
+ style: s2.flex,
1283
+ behavior: import_react_native3.Platform.OS === "ios" ? "padding" : "height",
1284
+ keyboardVerticalOffset: import_react_native3.Platform.OS === "ios" ? 0 : 20,
742
1285
  children: [
743
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
744
- import_react_native2.Animated.View,
1286
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1287
+ import_react_native3.Animated.View,
745
1288
  {
746
- style: [s.backdrop, { backgroundColor: colors.backdrop, opacity: backdropOpacity }],
747
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: s.flex, activeOpacity: 1, onPress: handleClose })
1289
+ style: [s2.backdrop, { backgroundColor: colors.backdrop, opacity: backdropOpacity }],
1290
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1291
+ import_react_native3.TouchableOpacity,
1292
+ {
1293
+ style: s2.flex,
1294
+ activeOpacity: 1,
1295
+ onPress: handleClose,
1296
+ accessibilityRole: "button",
1297
+ accessibilityLabel: t.widget.close
1298
+ }
1299
+ )
748
1300
  }
749
1301
  ),
750
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
751
- import_react_native2.Animated.View,
1302
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1303
+ import_react_native3.Animated.View,
752
1304
  {
1305
+ accessibilityViewIsModal: true,
1306
+ onAccessibilityEscape: handleClose,
753
1307
  style: [
754
- s.sheet,
1308
+ s2.sheet,
755
1309
  { backgroundColor: colors.bg, transform: [{ translateY }] }
756
1310
  ],
757
1311
  children: [
758
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
759
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [s.brandHeader, { backgroundColor: import_core3.MUSHI_BANNER_NEON.bg, borderBottomColor: import_core3.MUSHI_BANNER_NEON.border }], children: [
760
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.brandEyebrow, { color: import_core3.MUSHI_BANNER_NEON.fg }], children: "MUSHI \xB7 BETA" }),
761
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.brandTitle, { color: import_core3.MUSHI_BANNER_NEON.fg }], children: sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title })
762
- ] }),
763
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.tabRow, { borderBottomColor: colors.border }], children: sheetTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
764
- import_react_native2.TouchableOpacity,
1312
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1313
+ import_react_native3.View,
1314
+ {
1315
+ ...panResponder.panHandlers,
1316
+ style: s2.handleArea,
1317
+ accessible: true,
1318
+ accessibilityRole: "adjustable",
1319
+ accessibilityLabel: t.widget.close,
1320
+ accessibilityActions: [{ name: "decrement", label: t.widget.close }, { name: "escape" }],
1321
+ onAccessibilityAction: (e) => {
1322
+ if (e.nativeEvent.actionName === "decrement" || e.nativeEvent.actionName === "escape") handleClose();
1323
+ },
1324
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { style: [s2.handle, { backgroundColor: colors.muted }] })
1325
+ }
1326
+ ),
1327
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { style: [s2.header, { borderBottomColor: colors.border }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { accessibilityRole: "header", style: [s2.headerTitle, { color: colors.fg }, font], children: headerTitle }) }),
1328
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { style: [s2.tabRow, { borderBottomColor: colors.border }], accessibilityRole: "tablist", children: sheetTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1329
+ import_react_native3.TouchableOpacity,
765
1330
  {
766
1331
  onPress: () => {
767
1332
  setSheetTab(tab);
768
- if (tab === "inbox") void loadInbox();
769
- else setSelectedReportId(null);
1333
+ if (tab === "inbox") void loadInbox({ silent: inboxLoaded });
1334
+ else closeThread();
770
1335
  },
771
- style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
772
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.text : colors.sub }], children: tab === "report" ? t.widget.trigger : tab === "inbox" ? t.flows.reports.title : assistantLabel })
1336
+ style: [s2.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
1337
+ accessibilityRole: "tab",
1338
+ accessibilityState: { selected: sheetTab === tab },
1339
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.tabLabel, { color: sheetTab === tab ? colors.fg : colors.muted }, font], children: tab === "report" ? t.widget.trigger : tab === "inbox" ? t.flows.reports.title : assistantLabel })
773
1340
  },
774
1341
  tab
775
1342
  )) }),
776
- sheetTab === "assistant" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: [
777
- assistantTurns.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
778
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, marginBottom: 12, lineHeight: 20 }, children: greeting }),
779
- assistantSuggestions.map((chip) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
780
- import_react_native2.TouchableOpacity,
1343
+ sheetTab === "assistant" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.ScrollView, { style: s2.body, keyboardShouldPersistTaps: "handled", children: [
1344
+ assistantTurns.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
1345
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.muted, marginBottom: 12, lineHeight: 20 }, font], children: greeting }),
1346
+ assistantSuggestions.map((chip) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1347
+ import_react_native3.TouchableOpacity,
781
1348
  {
782
- style: [s.assistantChip, { borderColor: colors.border, backgroundColor: colors.card }],
1349
+ style: [s2.assistantChip, { borderColor: colors.border, backgroundColor: colors.surface }],
783
1350
  onPress: () => void sendAssistant(chip),
784
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.text, fontSize: 13 }, children: chip })
1351
+ accessibilityRole: "button",
1352
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.fg, fontSize: 13 }, font], children: chip })
785
1353
  },
786
1354
  chip
787
1355
  ))
788
- ] }) : assistantTurns.map((turn, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
789
- import_react_native2.View,
1356
+ ] }) : assistantTurns.map((turn, idx) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1357
+ import_react_native3.View,
790
1358
  {
791
1359
  style: [
792
- s.assistantBubble,
1360
+ s2.assistantBubble,
793
1361
  {
794
1362
  alignSelf: turn.role === "user" ? "flex-end" : "flex-start",
795
- backgroundColor: turn.role === "user" ? colors.accent : colors.card
1363
+ backgroundColor: turn.role === "user" ? colors.accent : colors.surface
796
1364
  }
797
1365
  ],
798
1366
  children: [
799
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: turn.role === "user" ? colors.accentInk : colors.text }, children: turn.text }),
800
- turn.options?.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { onPress: () => void sendAssistant(opt), style: { marginTop: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.accent, fontSize: 12 }, children: opt }) }, opt))
1367
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: turn.role === "user" ? colors.accentFg : colors.fg }, font], children: turn.text }),
1368
+ turn.options?.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1369
+ import_react_native3.TouchableOpacity,
1370
+ {
1371
+ onPress: () => void sendAssistant(opt),
1372
+ style: { marginTop: 8 },
1373
+ accessibilityRole: "button",
1374
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.accent, fontSize: 12 }, font], children: opt })
1375
+ },
1376
+ opt
1377
+ ))
801
1378
  ]
802
1379
  },
803
1380
  `${turn.role}-${idx}`
804
1381
  )),
805
- assistantSending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, marginTop: 8 }, accessibilityLiveRegion: "polite", children: t.assistant.thinking }) : null,
806
- assistantError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: pal.danger, marginTop: 8 }, accessibilityRole: "alert", children: assistantError }) : null,
807
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.assistantComposer, children: [
808
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
809
- import_react_native2.TextInput,
1382
+ assistantSending ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.muted, marginTop: 8 }, font], accessibilityLiveRegion: "polite", children: t.assistant.thinking }) : null,
1383
+ assistantError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.error, marginTop: 8 }, font], accessibilityRole: "alert", children: assistantError }) : null,
1384
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: s2.assistantComposer, children: [
1385
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1386
+ import_react_native3.TextInput,
810
1387
  {
811
- style: [s.input, { flex: 1, minHeight: 44, backgroundColor: colors.card, color: colors.text, borderColor: colors.border }],
1388
+ style: [s2.input, { flex: 1, minHeight: 44, backgroundColor: colors.surface, color: colors.fg, borderColor: colors.border, borderRadius: radius }, font],
812
1389
  placeholder: t.assistant.inputPlaceholder,
813
- placeholderTextColor: colors.sub,
1390
+ placeholderTextColor: colors.muted,
814
1391
  value: assistantInput,
815
1392
  onChangeText: setAssistantInput,
816
1393
  editable: !assistantSending
817
1394
  }
818
1395
  ),
819
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
820
- import_react_native2.TouchableOpacity,
1396
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1397
+ import_react_native3.TouchableOpacity,
821
1398
  {
822
- style: [s.submitBtn, { backgroundColor: colors.accent, paddingHorizontal: 16 }],
1399
+ style: [s2.submitBtn, { backgroundColor: colors.accent, paddingHorizontal: 16, borderRadius: radius }],
823
1400
  onPress: () => void sendAssistant(assistantInput),
824
1401
  disabled: !assistantInput.trim() || assistantSending,
825
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentInk }], children: "\u2191" })
1402
+ accessibilityRole: "button",
1403
+ accessibilityLabel: t.flows.thread.send,
1404
+ accessibilityState: { disabled: !assistantInput.trim() || assistantSending, busy: assistantSending },
1405
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.submitText, { color: colors.accentFg }], children: "\u2191" })
826
1406
  }
827
1407
  )
828
1408
  ] })
829
- ] }) : sheetTab === "inbox" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: inboxLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub }, children: t.flows.reports.loading }) : selectedReportId ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
830
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.Text, { style: { color: colors.accent, marginBottom: 8 }, children: [
831
- "\u2190 ",
832
- t.widget.back
833
- ] }) }),
834
- threadComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
835
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
836
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.text }, children: c.body })
837
- ] }, c.id)),
838
- VERIFIABLE_STATUSES.has(
839
- inboxReports.find((r) => r.id === selectedReportId)?.status ?? ""
840
- ) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.verifyRow, children: [
841
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentInk }], children: t.flows.thread.confirmFixed }) }),
842
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.border }], onPress: () => submitFeedback("not_fixed"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.text }], children: t.flows.thread.notFixed }) })
1409
+ ] }) : sheetTab === "inbox" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.ScrollView, { style: s2.body, keyboardShouldPersistTaps: "handled", contentContainerStyle: { paddingBottom: 24 }, children: selectedReportId ? renderThread() : renderInboxList() }) : phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.ScrollView, { style: s2.body, keyboardShouldPersistTaps: "handled", contentContainerStyle: { paddingBottom: 24 }, children: [
1410
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: s2.sentWrap, accessibilityLiveRegion: "polite", children: [
1411
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.sentEmoji], importantForAccessibility: "no", children: "\u2705" }),
1412
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.sentText, { color: colors.fg }, font], children: rc.ui.receipt })
843
1413
  ] }),
844
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
845
- import_react_native2.TextInput,
846
- {
847
- style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
848
- placeholder: t.flows.thread.replyPlaceholder,
849
- placeholderTextColor: colors.sub,
850
- value: replyText,
851
- onChangeText: setReplyText
852
- }
853
- ),
854
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentInk }], children: t.flows.thread.send }) })
855
- ] }) : inboxReports.map((r) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
856
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
857
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: reporterStatusShort(r.status) })
858
- ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.sentWrap, children: [
859
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentEmoji], children: "\u2705" }),
860
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentText, { color: colors.text }], children: t.widget.submitted })
861
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
862
- import_react_native2.ScrollView,
1414
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: s2.verifyRow, children: [
1415
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1416
+ import_react_native3.TouchableOpacity,
1417
+ {
1418
+ style: [s2.verifyBtn, { backgroundColor: colors.accent, borderRadius: radius }],
1419
+ onPress: () => {
1420
+ resetForm();
1421
+ setSheetTab("inbox");
1422
+ void loadInbox();
1423
+ },
1424
+ accessibilityRole: "button",
1425
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.submitText, { color: colors.accentFg }, font], children: rc.ui.trackIt })
1426
+ }
1427
+ ),
1428
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1429
+ import_react_native3.TouchableOpacity,
1430
+ {
1431
+ style: [s2.verifyBtn, { backgroundColor: colors.border, borderRadius: radius }],
1432
+ onPress: handleClose,
1433
+ accessibilityRole: "button",
1434
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.submitText, { color: colors.fg }, font], children: rc.ui.done })
1435
+ }
1436
+ )
1437
+ ] }),
1438
+ emailState === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.fg, marginTop: 12 }, font], accessibilityLiveRegion: "polite", children: rc.ui.emailCheckInbox }) : emailState !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: { marginTop: 12 }, children: [
1439
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.fg, marginBottom: 6 }, font], children: rc.ui.emailOptIn }),
1440
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: s2.composer, children: [
1441
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1442
+ import_react_native3.TextInput,
1443
+ {
1444
+ style: [
1445
+ s2.input,
1446
+ s2.composerInput,
1447
+ { backgroundColor: colors.surface, color: colors.fg, borderColor: colors.border, borderRadius: radius },
1448
+ font
1449
+ ],
1450
+ value: emailInput,
1451
+ onChangeText: (v) => {
1452
+ setEmailInput(v.slice(0, 254));
1453
+ if (emailState === "invalid" || emailState === "failed") setEmailState("offer");
1454
+ },
1455
+ placeholder: rc.ui.emailPlaceholder,
1456
+ placeholderTextColor: colors.muted,
1457
+ keyboardType: "email-address",
1458
+ autoCapitalize: "none",
1459
+ autoComplete: "email",
1460
+ textContentType: "emailAddress",
1461
+ editable: emailState !== "sending",
1462
+ accessibilityLabel: rc.ui.emailOptIn
1463
+ }
1464
+ ),
1465
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1466
+ import_react_native3.TouchableOpacity,
1467
+ {
1468
+ style: [s2.sendBtn, { backgroundColor: emailInput.trim() ? colors.accent : colors.disabled, borderRadius: radius }],
1469
+ onPress: () => void submitEmail(),
1470
+ disabled: !emailInput.trim() || emailState === "sending",
1471
+ accessibilityRole: "button",
1472
+ accessibilityState: { disabled: !emailInput.trim() || emailState === "sending", busy: emailState === "sending" },
1473
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.submitText, { color: emailInput.trim() ? colors.accentFg : colors.disabledFg }, font], children: emailState === "sending" ? rc.ui.sending : rc.ui.emailSubmit })
1474
+ }
1475
+ )
1476
+ ] }),
1477
+ emailState === "invalid" || emailState === "failed" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.error, marginTop: 6, fontSize: 12 }, font], accessibilityRole: "alert", children: emailState === "invalid" ? rc.ui.emailInvalid : rc.ui.emailFailed }) : null
1478
+ ] }) : null
1479
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1480
+ import_react_native3.ScrollView,
863
1481
  {
864
- style: s.body,
1482
+ style: s2.body,
865
1483
  keyboardShouldPersistTaps: "handled",
866
1484
  contentContainerStyle: { paddingBottom: 24 },
867
1485
  children: [
868
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.stepLabel, { color: colors.sub }], children: t.step1.heading }),
869
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.catRow, children: CATEGORY_KEYS.map((key) => {
1486
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1487
+ import_react_native3.TextInput,
1488
+ {
1489
+ style: [
1490
+ s2.input,
1491
+ {
1492
+ backgroundColor: colors.surface,
1493
+ color: colors.fg,
1494
+ borderColor: colors.border,
1495
+ borderRadius: radius
1496
+ },
1497
+ font
1498
+ ],
1499
+ placeholder: t.step3.descriptionPlaceholder,
1500
+ placeholderTextColor: colors.muted,
1501
+ accessibilityLabel: t.step3.descriptionPlaceholder,
1502
+ multiline: true,
1503
+ textAlignVertical: "top",
1504
+ value: description,
1505
+ onChangeText: (text) => {
1506
+ setDescription(text.slice(0, 4e3));
1507
+ if (submitError) setSubmitError(null);
1508
+ if (phase === "error") setPhase("form");
1509
+ },
1510
+ editable: phase === "form" || phase === "error",
1511
+ maxLength: 4e3
1512
+ }
1513
+ ),
1514
+ submitError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.error, marginTop: 6, fontSize: 12 }, font], accessibilityRole: "alert", children: submitError }) : null,
1515
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { style: [s2.catRow, { marginTop: 12 }], accessibilityRole: "radiogroup", children: import_reporter_ui2.REPORTER_CATEGORIES.map((key) => {
870
1516
  const active = category === key;
871
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
872
- import_react_native2.TouchableOpacity,
1517
+ const label = (0, import_reporter_ui2.reporterCategoryLabel)(key, locale);
1518
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1519
+ import_react_native3.TouchableOpacity,
873
1520
  {
874
- onPress: () => setCategory(key),
1521
+ onPress: () => setCategory(active ? null : key),
875
1522
  activeOpacity: 0.7,
1523
+ accessibilityRole: "radio",
1524
+ accessibilityState: { selected: active, checked: active },
1525
+ accessibilityLabel: label,
876
1526
  style: [
877
- s.catBtn,
1527
+ s2.catBtn,
878
1528
  {
879
- backgroundColor: active ? colors.accent : colors.card,
1529
+ backgroundColor: active ? colors.accent : colors.surface,
880
1530
  borderColor: active ? colors.accent : colors.border
881
1531
  }
882
1532
  ],
883
1533
  children: [
884
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: s.catEmoji, children: CATEGORY_EMOJI[key] }),
885
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
886
- import_react_native2.Text,
1534
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: s2.catEmoji, importantForAccessibility: "no", children: CATEGORY_EMOJI[key] }),
1535
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1536
+ import_react_native3.Text,
887
1537
  {
1538
+ numberOfLines: 1,
888
1539
  style: [
889
- s.catLabel,
890
- { color: active ? colors.accentInk : colors.text }
1540
+ s2.catLabel,
1541
+ { color: active ? colors.accentFg : colors.fg },
1542
+ font
891
1543
  ],
892
- children: t.step1.categories[key]
1544
+ children: label
893
1545
  }
894
1546
  )
895
1547
  ]
@@ -897,69 +1549,49 @@ var MushiBottomSheet = ({
897
1549
  key
898
1550
  );
899
1551
  }) }),
900
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
901
- import_react_native2.TextInput,
902
- {
903
- style: [
904
- s.input,
905
- {
906
- backgroundColor: colors.card,
907
- color: colors.text,
908
- borderColor: colors.border
909
- }
910
- ],
911
- placeholder: t.step3.descriptionPlaceholder,
912
- placeholderTextColor: colors.sub,
913
- multiline: true,
914
- textAlignVertical: "top",
915
- value: description,
916
- onChangeText: (text) => {
917
- setDescription(text.slice(0, 4e3));
918
- if (submitError) setSubmitError(null);
919
- if (phase === "error") setPhase("form");
920
- },
921
- editable: phase === "form" || phase === "error",
922
- maxLength: 4e3
923
- }
924
- ),
925
- submitError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.accent, marginTop: 6, fontSize: 12 }, children: submitError }) : null,
926
- activeScreenshot ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotRow, children: [
927
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
928
- import_react_native2.Image,
1552
+ activeScreenshot ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: s2.screenshotRow, children: [
1553
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1554
+ import_react_native3.Image,
929
1555
  {
930
1556
  source: { uri: activeScreenshot },
931
- style: [s.screenshotThumb, { backgroundColor: colors.text }],
1557
+ style: [s2.screenshotThumb, { backgroundColor: colors.fg }],
932
1558
  accessibilityLabel: t.step3.screenshotPreviewAlt
933
1559
  }
934
1560
  ),
935
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotMeta, children: [
936
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotLabel, { color: colors.text }], children: t.step3.screenshotAttached.replace(" \u2713", "") }),
937
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotSub, { color: colors.sub }], children: screenshotSensitiveHint && screenshotSensitiveHint.trim() ? `\u26A0 ${screenshotSensitiveHint}` : t.step3.screenshotSensitiveHint.split("\u2014")[0]?.trim() ?? t.step3.screenshotSensitiveHint })
1561
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: s2.screenshotMeta, children: [
1562
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.screenshotLabel, { color: colors.fg }, font], children: t.step3.screenshotAttached.replace(" \u2713", "") }),
1563
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.screenshotSub, { color: colors.muted }, font], children: screenshotSensitiveHint && screenshotSensitiveHint.trim() ? `\u26A0 ${screenshotSensitiveHint}` : t.step3.screenshotSensitiveHint.split("\u2014")[0]?.trim() ?? t.step3.screenshotSensitiveHint })
938
1564
  ] }),
939
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
940
- import_react_native2.TouchableOpacity,
1565
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1566
+ import_react_native3.TouchableOpacity,
941
1567
  {
942
1568
  onPress: () => {
943
1569
  setScreenshotAttached(false);
944
1570
  onClearScreenshot?.();
945
1571
  },
946
1572
  hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
1573
+ accessibilityRole: "button",
947
1574
  accessibilityLabel: t.widget.close,
948
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
1575
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.screenshotRemove, { color: colors.muted }], children: "\u2715" })
949
1576
  }
950
1577
  )
951
1578
  ] }) : null,
952
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
953
- import_react_native2.TouchableOpacity,
1579
+ submitHint ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [{ color: colors.muted, fontSize: 12, marginBottom: 6 }, font], children: submitHint }) : null,
1580
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1581
+ import_react_native3.TouchableOpacity,
954
1582
  {
955
1583
  onPress: handleSubmit,
956
1584
  disabled: !canSubmit,
957
1585
  activeOpacity: 0.8,
1586
+ accessibilityRole: "button",
1587
+ accessibilityLabel: phase === "sending" ? t.widget.submitting : t.widget.submit,
1588
+ accessibilityHint: submitHint,
1589
+ accessibilityState: { disabled: !canSubmit, busy: phase === "sending" },
958
1590
  style: [
959
- s.submitBtn,
960
- { backgroundColor: canSubmit ? colors.accent : colors.disabled }
1591
+ s2.submitBtn,
1592
+ { backgroundColor: canSubmit ? colors.accent : colors.disabled, borderRadius: radius }
961
1593
  ],
962
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: canSubmit ? colors.accentInk : colors.disabledText }], children: phase === "sending" ? t.widget.submitting : t.widget.submit })
1594
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: [s2.submitText, { color: canSubmit ? colors.accentFg : colors.disabledFg }, font], children: phase === "sending" ? t.widget.submitting : t.widget.submit })
963
1595
  }
964
1596
  )
965
1597
  ]
@@ -972,7 +1604,7 @@ var MushiBottomSheet = ({
972
1604
  }
973
1605
  ) });
974
1606
  };
975
- var s = import_react_native2.StyleSheet.create({
1607
+ var s2 = import_react_native3.StyleSheet.create({
976
1608
  flex: { flex: 1 },
977
1609
  backdrop: {
978
1610
  // Inlined StyleSheet.absoluteFillObject — RN 0.86 dropped it from the
@@ -989,10 +1621,10 @@ var s = import_react_native2.StyleSheet.create({
989
1621
  left: 0,
990
1622
  right: 0,
991
1623
  minHeight: SHEET_HEIGHT,
992
- borderTopLeftRadius: import_core3.MUSHI_RADIUS.sheet,
993
- borderTopRightRadius: import_core3.MUSHI_RADIUS.sheet,
994
- paddingBottom: import_react_native2.Platform.OS === "ios" ? 34 : import_core3.MUSHI_SPACING.roomy,
995
- shadowColor: import_core3.MUSHI_SHADOW_INK,
1624
+ borderTopLeftRadius: import_core5.MUSHI_RADIUS.sheet,
1625
+ borderTopRightRadius: import_core5.MUSHI_RADIUS.sheet,
1626
+ paddingBottom: import_react_native3.Platform.OS === "ios" ? 34 : import_core5.MUSHI_SPACING.roomy,
1627
+ shadowColor: import_core5.MUSHI_SHADOW_INK,
996
1628
  shadowOffset: { width: 0, height: -3 },
997
1629
  shadowOpacity: 0.12,
998
1630
  shadowRadius: 8,
@@ -1000,88 +1632,72 @@ var s = import_react_native2.StyleSheet.create({
1000
1632
  },
1001
1633
  handleArea: {
1002
1634
  alignItems: "center",
1003
- paddingVertical: import_core3.MUSHI_SPACING.comfy
1635
+ paddingVertical: import_core5.MUSHI_SPACING.comfy
1004
1636
  },
1005
1637
  handle: {
1006
1638
  width: 40,
1007
1639
  height: 5,
1008
- borderRadius: import_core3.MUSHI_RADIUS.control,
1640
+ borderRadius: import_core5.MUSHI_RADIUS.control,
1009
1641
  opacity: 0.5
1010
1642
  },
1011
- brandHeader: {
1012
- paddingHorizontal: import_core3.MUSHI_SPACING.lounge,
1013
- paddingTop: import_core3.MUSHI_SPACING.comfy,
1014
- paddingBottom: import_core3.MUSHI_SPACING.comfy,
1015
- borderBottomWidth: 1.5
1643
+ header: {
1644
+ paddingHorizontal: import_core5.MUSHI_SPACING.lounge,
1645
+ paddingBottom: import_core5.MUSHI_SPACING.comfy
1016
1646
  },
1017
- brandEyebrow: {
1018
- fontSize: import_core3.MUSHI_TYPE.sizeLabel,
1019
- fontWeight: "700",
1020
- letterSpacing: 1.2,
1021
- marginBottom: 2,
1022
- opacity: 0.7
1023
- },
1024
- brandTitle: {
1647
+ headerTitle: {
1025
1648
  fontSize: 18,
1026
- fontWeight: "800",
1649
+ fontWeight: "700",
1027
1650
  letterSpacing: -0.2
1028
1651
  },
1029
1652
  body: {
1030
- paddingHorizontal: import_core3.MUSHI_SPACING.lounge,
1031
- paddingTop: import_core3.MUSHI_SPACING.tight
1032
- },
1033
- title: {
1034
- fontSize: 20,
1035
- fontWeight: "700",
1036
- marginBottom: import_core3.MUSHI_SPACING.roomy
1653
+ paddingHorizontal: import_core5.MUSHI_SPACING.lounge,
1654
+ paddingTop: import_core5.MUSHI_SPACING.tight
1037
1655
  },
1038
1656
  stepLabel: {
1039
- fontSize: import_core3.MUSHI_TYPE.sizeLabel,
1657
+ fontSize: import_core5.MUSHI_TYPE.sizeLabel,
1040
1658
  fontWeight: "600",
1041
1659
  letterSpacing: 0.2,
1042
- marginTop: import_core3.MUSHI_SPACING.snug,
1043
- marginBottom: import_core3.MUSHI_SPACING.comfy,
1660
+ marginTop: import_core5.MUSHI_SPACING.snug,
1661
+ marginBottom: import_core5.MUSHI_SPACING.comfy,
1044
1662
  textTransform: "uppercase"
1045
1663
  },
1046
1664
  catRow: {
1047
1665
  flexDirection: "row",
1048
- justifyContent: "space-between",
1049
- marginBottom: import_core3.MUSHI_SPACING.roomy
1666
+ flexWrap: "wrap",
1667
+ gap: 8,
1668
+ marginBottom: import_core5.MUSHI_SPACING.roomy
1050
1669
  },
1051
1670
  catBtn: {
1671
+ flexDirection: "row",
1052
1672
  alignItems: "center",
1053
- paddingVertical: import_core3.MUSHI_SPACING.comfy,
1054
- paddingHorizontal: 6,
1055
- borderRadius: import_core3.MUSHI_RADIUS.card,
1673
+ gap: 6,
1674
+ paddingVertical: 8,
1675
+ paddingHorizontal: 12,
1676
+ borderRadius: 999,
1056
1677
  borderWidth: 1,
1057
- flex: 1,
1058
- marginHorizontal: 3,
1059
1678
  minHeight: 44
1060
1679
  },
1061
1680
  catEmoji: {
1062
- fontSize: 22,
1063
- marginBottom: import_core3.MUSHI_SPACING.tight
1681
+ fontSize: 16
1064
1682
  },
1065
1683
  catLabel: {
1066
- fontSize: import_core3.MUSHI_TYPE.sizeLabel,
1684
+ fontSize: 14,
1067
1685
  fontWeight: "600"
1068
1686
  },
1069
1687
  input: {
1070
1688
  borderWidth: 1,
1071
- borderRadius: import_core3.MUSHI_RADIUS.card,
1072
1689
  padding: 14,
1073
1690
  fontSize: 15,
1074
1691
  minHeight: 100,
1075
- marginBottom: import_core3.MUSHI_SPACING.roomy
1692
+ marginBottom: import_core5.MUSHI_SPACING.roomy
1076
1693
  },
1077
1694
  submitBtn: {
1078
- borderRadius: import_core3.MUSHI_RADIUS.card,
1079
1695
  paddingVertical: 14,
1080
1696
  alignItems: "center",
1081
1697
  minHeight: 44
1082
1698
  },
1083
1699
  submitText: {
1084
- color: import_core3.MUSHI_INVERSE,
1700
+ color: import_core5.MUSHI_INVERSE,
1085
1701
  fontSize: 16,
1086
1702
  fontWeight: "700"
1087
1703
  },
@@ -1132,7 +1748,7 @@ var s = import_react_native2.StyleSheet.create({
1132
1748
  },
1133
1749
  tabRow: {
1134
1750
  flexDirection: "row",
1135
- borderBottomWidth: import_react_native2.StyleSheet.hairlineWidth,
1751
+ borderBottomWidth: import_react_native3.StyleSheet.hairlineWidth,
1136
1752
  marginBottom: 8
1137
1753
  },
1138
1754
  tabBtn: {
@@ -1140,7 +1756,8 @@ var s = import_react_native2.StyleSheet.create({
1140
1756
  alignItems: "center",
1141
1757
  paddingVertical: 10,
1142
1758
  borderBottomWidth: 2,
1143
- borderBottomColor: "transparent"
1759
+ borderBottomColor: "transparent",
1760
+ minHeight: 44
1144
1761
  },
1145
1762
  tabLabel: {
1146
1763
  fontSize: 14,
@@ -1148,12 +1765,47 @@ var s = import_react_native2.StyleSheet.create({
1148
1765
  },
1149
1766
  inboxRow: {
1150
1767
  paddingVertical: 12,
1151
- borderBottomWidth: import_react_native2.StyleSheet.hairlineWidth
1768
+ minHeight: 56,
1769
+ borderBottomWidth: import_react_native3.StyleSheet.hairlineWidth
1770
+ },
1771
+ headerCard: {
1772
+ borderWidth: import_react_native3.StyleSheet.hairlineWidth,
1773
+ padding: 12,
1774
+ marginBottom: 12
1775
+ },
1776
+ pill: {
1777
+ alignSelf: "flex-start",
1778
+ borderWidth: 1,
1779
+ borderRadius: 999,
1780
+ paddingHorizontal: 8,
1781
+ paddingVertical: 2,
1782
+ fontSize: 12,
1783
+ fontWeight: "600",
1784
+ overflow: "hidden"
1785
+ },
1786
+ skeleton: {
1787
+ height: 14,
1788
+ borderRadius: 6,
1789
+ marginBottom: 10
1790
+ },
1791
+ errorRow: {
1792
+ flexDirection: "row",
1793
+ alignItems: "center",
1794
+ gap: 8,
1795
+ marginBottom: 12
1796
+ },
1797
+ retryBtn: {
1798
+ borderWidth: 1,
1799
+ paddingHorizontal: 12,
1800
+ paddingVertical: 8,
1801
+ minHeight: 44,
1802
+ justifyContent: "center"
1152
1803
  },
1153
1804
  threadBubble: {
1154
1805
  borderRadius: 10,
1155
1806
  padding: 10,
1156
- marginBottom: 8
1807
+ marginBottom: 8,
1808
+ maxWidth: "88%"
1157
1809
  },
1158
1810
  verifyRow: {
1159
1811
  flexDirection: "row",
@@ -1162,9 +1814,26 @@ var s = import_react_native2.StyleSheet.create({
1162
1814
  },
1163
1815
  verifyBtn: {
1164
1816
  flex: 1,
1165
- borderRadius: 10,
1166
1817
  paddingVertical: 10,
1167
- alignItems: "center"
1818
+ alignItems: "center",
1819
+ minHeight: 44
1820
+ },
1821
+ composer: {
1822
+ flexDirection: "row",
1823
+ alignItems: "flex-end",
1824
+ gap: 8
1825
+ },
1826
+ composerInput: {
1827
+ flex: 1,
1828
+ minHeight: 44,
1829
+ maxHeight: 120,
1830
+ marginBottom: 0
1831
+ },
1832
+ sendBtn: {
1833
+ paddingHorizontal: 16,
1834
+ paddingVertical: 12,
1835
+ minHeight: 44,
1836
+ justifyContent: "center"
1168
1837
  },
1169
1838
  assistantBubble: {
1170
1839
  maxWidth: "88%",
@@ -1190,14 +1859,14 @@ var s = import_react_native2.StyleSheet.create({
1190
1859
  });
1191
1860
 
1192
1861
  // src/components/MushiFloatingButton.tsx
1193
- var import_react2 = require("react");
1194
- var import_react_native3 = require("react-native");
1195
- var import_core4 = require("@mushi-mushi/core");
1196
- var import_jsx_runtime2 = require("react/jsx-runtime");
1197
- var FAB_SIZE = import_core4.MUSHI_GEOMETRY.fabSize;
1862
+ var import_react3 = require("react");
1863
+ var import_react_native4 = require("react-native");
1864
+ var import_core6 = require("@mushi-mushi/core");
1865
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1866
+ var FAB_SIZE = import_core6.MUSHI_GEOMETRY.fabSize;
1198
1867
  var EDGE_PADDING = 12;
1199
1868
  function getScreenBounds() {
1200
- const { width, height } = import_react_native3.Dimensions.get("window");
1869
+ const { width, height } = import_react_native4.Dimensions.get("window");
1201
1870
  return { width, height };
1202
1871
  }
1203
1872
  function clamp(value, min, max) {
@@ -1228,25 +1897,25 @@ var MushiFloatingButton = ({
1228
1897
  config,
1229
1898
  accent
1230
1899
  }) => {
1231
- const scheme = (0, import_react_native3.useColorScheme)();
1900
+ const scheme = (0, import_react_native4.useColorScheme)();
1232
1901
  const dark = scheme === "dark";
1233
1902
  const draggable = config?.draggable ?? false;
1234
1903
  const snapToEdge = config?.snapToEdge ?? true;
1235
1904
  const persist = config?.persist ?? false;
1236
- const baseBottom = inset?.bottom ?? (import_react_native3.Platform.OS === "ios" ? 50 : 28);
1905
+ const baseBottom = inset?.bottom ?? (import_react_native4.Platform.OS === "ios" ? 50 : 28);
1237
1906
  const baseX = position === "bottom-left" ? inset?.left ?? EDGE_PADDING : (() => {
1238
1907
  const { width } = getScreenBounds();
1239
1908
  return width - FAB_SIZE - (inset?.right ?? EDGE_PADDING);
1240
1909
  })();
1241
- const scale = (0, import_react2.useRef)(new import_react_native3.Animated.Value(1)).current;
1242
- const posX = (0, import_react2.useRef)(new import_react_native3.Animated.Value(baseX)).current;
1243
- const posY = (0, import_react2.useRef)(new import_react_native3.Animated.Value(0)).current;
1244
- const currentX = (0, import_react2.useRef)(baseX);
1245
- const currentY = (0, import_react2.useRef)(0);
1246
- const isDragging = (0, import_react2.useRef)(false);
1247
- const pal = (0, import_core4.mushiPalette)(dark ? "dark" : "light");
1910
+ const scale = (0, import_react3.useRef)(new import_react_native4.Animated.Value(1)).current;
1911
+ const posX = (0, import_react3.useRef)(new import_react_native4.Animated.Value(baseX)).current;
1912
+ const posY = (0, import_react3.useRef)(new import_react_native4.Animated.Value(0)).current;
1913
+ const currentX = (0, import_react3.useRef)(baseX);
1914
+ const currentY = (0, import_react3.useRef)(0);
1915
+ const isDragging = (0, import_react3.useRef)(false);
1916
+ const pal = (0, import_core6.mushiPalette)(dark ? "dark" : "light");
1248
1917
  const accentColor = accent ?? pal.accent;
1249
- (0, import_react2.useEffect)(() => {
1918
+ (0, import_react3.useEffect)(() => {
1250
1919
  if (!persist) return;
1251
1920
  void loadStoredPos().then((pos) => {
1252
1921
  if (!pos) return;
@@ -1256,7 +1925,7 @@ var MushiFloatingButton = ({
1256
1925
  posY.setValue(pos.y);
1257
1926
  });
1258
1927
  }, [persist, posX, posY]);
1259
- const snapAndPersist = (0, import_react2.useCallback)(
1928
+ const snapAndPersist = (0, import_react3.useCallback)(
1260
1929
  (x, y) => {
1261
1930
  const { width, height } = getScreenBounds();
1262
1931
  let snappedX = x;
@@ -1268,13 +1937,13 @@ var MushiFloatingButton = ({
1268
1937
  const clampedY = clamp(y, -(height - FAB_SIZE - baseBottom - 60), 0);
1269
1938
  currentX.current = clampedX;
1270
1939
  currentY.current = clampedY;
1271
- import_react_native3.Animated.spring(posX, {
1940
+ import_react_native4.Animated.spring(posX, {
1272
1941
  toValue: clampedX,
1273
1942
  useNativeDriver: false,
1274
1943
  damping: 18,
1275
1944
  stiffness: 220
1276
1945
  }).start();
1277
- import_react_native3.Animated.spring(posY, {
1946
+ import_react_native4.Animated.spring(posY, {
1278
1947
  toValue: clampedY,
1279
1948
  useNativeDriver: false,
1280
1949
  damping: 18,
@@ -1284,8 +1953,8 @@ var MushiFloatingButton = ({
1284
1953
  },
1285
1954
  [snapToEdge, persist, posX, posY, baseBottom]
1286
1955
  );
1287
- const panResponder = (0, import_react2.useRef)(
1288
- draggable ? import_react_native3.PanResponder.create({
1956
+ const panResponder = (0, import_react3.useRef)(
1957
+ draggable ? import_react_native4.PanResponder.create({
1289
1958
  onStartShouldSetPanResponder: () => true,
1290
1959
  onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 6 || Math.abs(g.dy) > 6,
1291
1960
  onPanResponderGrant: () => {
@@ -1318,19 +1987,19 @@ var MushiFloatingButton = ({
1318
1987
  }
1319
1988
  isDragging.current = false;
1320
1989
  }
1321
- }) : import_react_native3.PanResponder.create({})
1990
+ }) : import_react_native4.PanResponder.create({})
1322
1991
  ).current;
1323
- const handlePressIn = (0, import_react2.useCallback)(() => {
1992
+ const handlePressIn = (0, import_react3.useCallback)(() => {
1324
1993
  if (isDragging.current) return;
1325
- import_react_native3.Animated.spring(scale, {
1994
+ import_react_native4.Animated.spring(scale, {
1326
1995
  toValue: 0.85,
1327
1996
  useNativeDriver: true,
1328
1997
  damping: 15,
1329
1998
  stiffness: 300
1330
1999
  }).start();
1331
2000
  }, [scale]);
1332
- const handlePressOut = (0, import_react2.useCallback)(() => {
1333
- import_react_native3.Animated.spring(scale, {
2001
+ const handlePressOut = (0, import_react3.useCallback)(() => {
2002
+ import_react_native4.Animated.spring(scale, {
1334
2003
  toValue: 1,
1335
2004
  useNativeDriver: true,
1336
2005
  damping: 10,
@@ -1338,15 +2007,15 @@ var MushiFloatingButton = ({
1338
2007
  }).start();
1339
2008
  if (!isDragging.current) onPress();
1340
2009
  }, [scale, onPress]);
1341
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1342
- import_react_native3.Animated.View,
2010
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2011
+ import_react_native4.Animated.View,
1343
2012
  {
1344
2013
  ...draggable ? panResponder.panHandlers : {},
1345
2014
  style: [
1346
- s2.fab,
2015
+ s3.fab,
1347
2016
  {
1348
2017
  left: posX,
1349
- bottom: import_react_native3.Animated.add(new import_react_native3.Animated.Value(baseBottom), import_react_native3.Animated.multiply(posY, -1)),
2018
+ bottom: import_react_native4.Animated.add(new import_react_native4.Animated.Value(baseBottom), import_react_native4.Animated.multiply(posY, -1)),
1350
2019
  backgroundColor: accentColor,
1351
2020
  shadowColor: dark ? accentColor : pal.ink,
1352
2021
  transform: [{ scale }]
@@ -1355,11 +2024,11 @@ var MushiFloatingButton = ({
1355
2024
  onStartShouldSetResponder: () => !draggable,
1356
2025
  onResponderGrant: handlePressIn,
1357
2026
  onResponderRelease: handlePressOut,
1358
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children: import_core4.MUSHI_COPY.triggerText })
2027
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.Animated.Text, { style: s3.emoji, children: import_core6.MUSHI_COPY.triggerText })
1359
2028
  }
1360
2029
  );
1361
2030
  };
1362
- var s2 = import_react_native3.StyleSheet.create({
2031
+ var s3 = import_react_native4.StyleSheet.create({
1363
2032
  fab: {
1364
2033
  position: "absolute",
1365
2034
  width: FAB_SIZE,
@@ -1379,93 +2048,93 @@ var s2 = import_react_native3.StyleSheet.create({
1379
2048
  });
1380
2049
 
1381
2050
  // src/components/MushiBanner.tsx
1382
- var import_react3 = require("react");
1383
- var import_react_native4 = require("react-native");
1384
- var import_core5 = require("@mushi-mushi/core");
1385
- var import_jsx_runtime3 = require("react/jsx-runtime");
1386
- var MUSHI_BANNER_DEFAULT_HEIGHT = import_core5.MUSHI_GEOMETRY.bannerHeight;
2051
+ var import_react4 = require("react");
2052
+ var import_react_native5 = require("react-native");
2053
+ var import_core7 = require("@mushi-mushi/core");
2054
+ var import_jsx_runtime4 = require("react/jsx-runtime");
2055
+ var MUSHI_BANNER_DEFAULT_HEIGHT = import_core7.MUSHI_GEOMETRY.bannerHeight;
1387
2056
  var MushiBanner = ({
1388
2057
  onPress,
1389
2058
  onDismiss,
1390
- message = import_core5.MUSHI_COPY.bannerMessage,
1391
- label = import_core5.MUSHI_COPY.bannerLabel,
2059
+ message = import_core7.MUSHI_COPY.bannerMessage,
2060
+ label = import_core7.MUSHI_COPY.bannerLabel,
1392
2061
  onLayoutHeight
1393
2062
  }) => {
1394
- const handleLayout = (0, import_react3.useCallback)(
2063
+ const handleLayout = (0, import_react4.useCallback)(
1395
2064
  (e) => {
1396
2065
  onLayoutHeight?.(e.nativeEvent.layout.height);
1397
2066
  },
1398
2067
  [onLayoutHeight]
1399
2068
  );
1400
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1401
- import_react_native4.View,
2069
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2070
+ import_react_native5.View,
1402
2071
  {
1403
- style: s3.wrap,
2072
+ style: s4.wrap,
1404
2073
  onLayout: handleLayout,
1405
2074
  accessibilityRole: "header",
1406
2075
  accessibilityLabel: "Mushi feedback banner",
1407
2076
  children: [
1408
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native4.TouchableOpacity, { style: s3.main, onPress, activeOpacity: 0.85, children: [
1409
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.Text, { style: s3.eyebrow, children: label.toUpperCase() }),
1410
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.Text, { style: s3.message, numberOfLines: 2, children: message }),
1411
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.Text, { style: s3.cta, children: import_core5.MUSHI_COPY.bugCta })
2077
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native5.TouchableOpacity, { style: s4.main, onPress, activeOpacity: 0.85, children: [
2078
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native5.Text, { style: s4.eyebrow, children: label.toUpperCase() }),
2079
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native5.Text, { style: s4.message, numberOfLines: 2, children: message }),
2080
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native5.Text, { style: s4.cta, children: import_core7.MUSHI_COPY.bugCta })
1412
2081
  ] }),
1413
- onDismiss ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1414
- import_react_native4.TouchableOpacity,
2082
+ onDismiss ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2083
+ import_react_native5.TouchableOpacity,
1415
2084
  {
1416
- style: s3.dismiss,
2085
+ style: s4.dismiss,
1417
2086
  onPress: onDismiss,
1418
2087
  accessibilityRole: "button",
1419
2088
  accessibilityLabel: "Dismiss banner",
1420
2089
  hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
1421
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.Text, { style: s3.dismissText, children: "\xD7" })
2090
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native5.Text, { style: s4.dismissText, children: "\xD7" })
1422
2091
  }
1423
2092
  ) : null
1424
2093
  ]
1425
2094
  }
1426
2095
  );
1427
2096
  };
1428
- var s3 = import_react_native4.StyleSheet.create({
2097
+ var s4 = import_react_native5.StyleSheet.create({
1429
2098
  wrap: {
1430
2099
  position: "absolute",
1431
2100
  left: 0,
1432
2101
  right: 0,
1433
2102
  bottom: 0,
1434
- minHeight: import_core5.MUSHI_GEOMETRY.bannerHeight,
2103
+ minHeight: import_core7.MUSHI_GEOMETRY.bannerHeight,
1435
2104
  flexDirection: "row",
1436
2105
  alignItems: "stretch",
1437
- backgroundColor: import_core5.MUSHI_BANNER_NEON.bg,
2106
+ backgroundColor: import_core7.MUSHI_BANNER_NEON.bg,
1438
2107
  borderTopWidth: 1,
1439
- borderTopColor: import_core5.MUSHI_BANNER_NEON.border,
1440
- zIndex: import_core5.MUSHI_Z.banner,
2108
+ borderTopColor: import_core7.MUSHI_BANNER_NEON.border,
2109
+ zIndex: import_core7.MUSHI_Z.banner,
1441
2110
  elevation: 8,
1442
- paddingBottom: import_react_native4.Platform.OS === "ios" ? 0 : 0
2111
+ paddingBottom: import_react_native5.Platform.OS === "ios" ? 0 : 0
1443
2112
  },
1444
2113
  main: {
1445
2114
  flex: 1,
1446
2115
  flexDirection: "row",
1447
2116
  alignItems: "center",
1448
- gap: import_core5.MUSHI_SPACING.snug,
1449
- paddingHorizontal: import_core5.MUSHI_SPACING.comfy,
1450
- paddingVertical: import_core5.MUSHI_SPACING.snug,
2117
+ gap: import_core7.MUSHI_SPACING.snug,
2118
+ paddingHorizontal: import_core7.MUSHI_SPACING.comfy,
2119
+ paddingVertical: import_core7.MUSHI_SPACING.snug,
1451
2120
  minHeight: 44
1452
2121
  },
1453
2122
  eyebrow: {
1454
- fontSize: import_core5.MUSHI_TYPE.sizeLabel,
2123
+ fontSize: import_core7.MUSHI_TYPE.sizeLabel,
1455
2124
  fontWeight: "700",
1456
2125
  letterSpacing: 1.2,
1457
- color: import_core5.MUSHI_BANNER_NEON.fg
2126
+ color: import_core7.MUSHI_BANNER_NEON.fg
1458
2127
  },
1459
2128
  message: {
1460
2129
  flex: 1,
1461
2130
  fontSize: 12,
1462
2131
  fontWeight: "500",
1463
- color: import_core5.MUSHI_BANNER_NEON.fg
2132
+ color: import_core7.MUSHI_BANNER_NEON.fg
1464
2133
  },
1465
2134
  cta: {
1466
2135
  fontSize: 12,
1467
2136
  fontWeight: "700",
1468
- color: import_core5.MUSHI_BANNER_NEON.fg,
2137
+ color: import_core7.MUSHI_BANNER_NEON.fg,
1469
2138
  textDecorationLine: "underline"
1470
2139
  },
1471
2140
  dismiss: {
@@ -1475,13 +2144,13 @@ var s3 = import_react_native4.StyleSheet.create({
1475
2144
  dismissText: {
1476
2145
  fontSize: 20,
1477
2146
  lineHeight: 22,
1478
- color: import_core5.MUSHI_BANNER_NEON.fg,
2147
+ color: import_core7.MUSHI_BANNER_NEON.fg,
1479
2148
  opacity: 0.7
1480
2149
  }
1481
2150
  });
1482
2151
 
1483
2152
  // src/analytics/event-tracker.ts
1484
- var import_core6 = require("@mushi-mushi/core");
2153
+ var import_core8 = require("@mushi-mushi/core");
1485
2154
  var CONSENT_BUFFER_MAX = 50;
1486
2155
  var SPILL_MAX = 200;
1487
2156
  var SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -1545,7 +2214,7 @@ function createRNEventTracker(opts) {
1545
2214
  const client = resolveClient();
1546
2215
  if (!client) return;
1547
2216
  flushing = true;
1548
- const batch = buffer.splice(0, import_core6.EVENT_PROPERTY_LIMITS.maxServerBatch);
2217
+ const batch = buffer.splice(0, import_core8.EVENT_PROPERTY_LIMITS.maxServerBatch);
1549
2218
  if (pendingIdentify) {
1550
2219
  batch.unshift({ name: "identify", ts: (/* @__PURE__ */ new Date()).toISOString(), properties: {} });
1551
2220
  pendingIdentify = false;
@@ -1569,7 +2238,7 @@ function createRNEventTracker(opts) {
1569
2238
  } finally {
1570
2239
  flushing = false;
1571
2240
  }
1572
- if (buffer.length >= import_core6.EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
2241
+ if (buffer.length >= import_core8.EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
1573
2242
  }
1574
2243
  function enqueue(ev) {
1575
2244
  if (consent === "denied") return;
@@ -1578,7 +2247,7 @@ function createRNEventTracker(opts) {
1578
2247
  return;
1579
2248
  }
1580
2249
  buffer.push(ev);
1581
- if (buffer.length >= import_core6.EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
2250
+ if (buffer.length >= import_core8.EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
1582
2251
  }
1583
2252
  const ready = enabled ? (async () => {
1584
2253
  storage = await loadStorage();
@@ -1618,17 +2287,17 @@ function createRNEventTracker(opts) {
1618
2287
  ready,
1619
2288
  track(name, properties, trackOpts) {
1620
2289
  if (!enabled || destroyed) return false;
1621
- if (!(0, import_core6.isValidEventName)(name)) return false;
1622
- const { properties: props } = (0, import_core6.sanitizeEventProperties)(properties, { allowlist, scrub: opts.scrub });
2290
+ if (!(0, import_core8.isValidEventName)(name)) return false;
2291
+ const { properties: props } = (0, import_core8.sanitizeEventProperties)(properties, { allowlist, scrub: opts.scrub });
1623
2292
  let merged = props;
1624
2293
  if (trackOpts?.reserved) {
1625
- const { properties: res } = (0, import_core6.sanitizeEventProperties)(trackOpts.reserved, { allowReserved: true, scrub: opts.scrub });
2294
+ const { properties: res } = (0, import_core8.sanitizeEventProperties)(trackOpts.reserved, { allowReserved: true, scrub: opts.scrub });
1626
2295
  merged = { ...props, ...res };
1627
2296
  }
1628
2297
  merged.$surface = SURFACE;
1629
2298
  const route = opts.getRoute?.();
1630
2299
  if (route && merged.$route === void 0) merged.$route = route;
1631
- if (!(0, import_core6.propertiesWithinByteLimit)(merged)) return false;
2300
+ if (!(0, import_core8.propertiesWithinByteLimit)(merged)) return false;
1632
2301
  enqueue({
1633
2302
  name,
1634
2303
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1697,13 +2366,13 @@ function createRNEventTracker(opts) {
1697
2366
  }
1698
2367
 
1699
2368
  // src/analytics/session-tracker.ts
1700
- var import_core7 = require("@mushi-mushi/core");
2369
+ var import_core9 = require("@mushi-mushi/core");
1701
2370
  var HEARTBEAT_INTERVAL_MS = 6e4;
1702
2371
  var RESUME_WINDOW_MS = 30 * 6e4;
1703
2372
  function createRNSessionTracker(opts) {
1704
2373
  const clock = opts.now ?? Date.now;
1705
2374
  const resolveClient = () => typeof opts.client === "function" ? opts.client() : opts.client;
1706
- let sessionId = opts.sessionId ?? `sess_${(0, import_core7.newUuid)()}`;
2375
+ let sessionId = opts.sessionId ?? `sess_${(0, import_core9.newUuid)()}`;
1707
2376
  let pageViewCount = 0;
1708
2377
  let started = false;
1709
2378
  let allowed = false;
@@ -1771,7 +2440,7 @@ function createRNSessionTracker(opts) {
1771
2440
  backgroundedAt = null;
1772
2441
  if (!allowed) return;
1773
2442
  if (!started || away >= RESUME_WINDOW_MS) {
1774
- sessionId = `sess_${(0, import_core7.newUuid)()}`;
2443
+ sessionId = `sess_${(0, import_core9.newUuid)()}`;
1775
2444
  startSession();
1776
2445
  } else {
1777
2446
  send("session_heartbeat", { route: route() });
@@ -1800,14 +2469,14 @@ function createRNSessionTracker(opts) {
1800
2469
 
1801
2470
  // src/version.ts
1802
2471
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1803
- var MUSHI_SDK_VERSION = "0.22.0" ? "0.22.0" : "0.17.0";
2472
+ var MUSHI_SDK_VERSION = "0.24.0" ? "0.24.0" : "0.17.0";
1804
2473
 
1805
2474
  // src/provider.tsx
1806
- var import_jsx_runtime4 = require("react/jsx-runtime");
1807
- var MushiContext = (0, import_react4.createContext)(null);
2475
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2476
+ var MushiContext = (0, import_react5.createContext)(null);
1808
2477
  function nativeUserAgent() {
1809
- const os = import_react_native5.Platform?.OS ?? "native";
1810
- const version = import_react_native5.Platform?.Version;
2478
+ const os = import_react_native6.Platform?.OS ?? "native";
2479
+ const version = import_react_native6.Platform?.Version;
1811
2480
  return `${MUSHI_SDK_PACKAGE}/${MUSHI_SDK_VERSION} (${version != null ? `${os} ${String(version)}` : os})`;
1812
2481
  }
1813
2482
  var DEFAULT_SCREENSHOT_HINT = "Check the preview \u2014 remove it if any private info (balances, personal details) is visible.";
@@ -1848,28 +2517,28 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1848
2517
  );
1849
2518
  }
1850
2519
  }
1851
- const consoleRef = (0, import_react4.useRef)(null);
1852
- const networkRef = (0, import_react4.useRef)(null);
1853
- const queueRef = (0, import_react4.useRef)(null);
1854
- const apiClientRef = (0, import_react4.useRef)(null);
1855
- const sessionIdRef = (0, import_react4.useRef)(`sess_${(0, import_core8.newUuid)()}`);
1856
- const breadcrumbsRef = (0, import_react4.useRef)(
1857
- (0, import_core8.createBreadcrumbBuffer)({ max: config.capture?.maxBreadcrumbs ?? 50 })
2520
+ const consoleRef = (0, import_react5.useRef)(null);
2521
+ const networkRef = (0, import_react5.useRef)(null);
2522
+ const queueRef = (0, import_react5.useRef)(null);
2523
+ const apiClientRef = (0, import_react5.useRef)(null);
2524
+ const sessionIdRef = (0, import_react5.useRef)(`sess_${(0, import_core10.newUuid)()}`);
2525
+ const breadcrumbsRef = (0, import_react5.useRef)(
2526
+ (0, import_core10.createBreadcrumbBuffer)({ max: config.capture?.maxBreadcrumbs ?? 50 })
1858
2527
  );
1859
- const [sheetVisible, setSheetVisible] = (0, import_react4.useState)(false);
1860
- const [sheetPreferredTab, setSheetPreferredTab] = (0, import_react4.useState)("report");
1861
- const [bannerDismissed, setBannerDismissed] = (0, import_react4.useState)(false);
1862
- const [sheetScreenshot, setSheetScreenshot] = (0, import_react4.useState)(null);
1863
- const reporterTokenRef = (0, import_react4.useRef)(`rn-${config.projectId}-anon`);
2528
+ const [sheetVisible, setSheetVisible] = (0, import_react5.useState)(false);
2529
+ const [sheetPreferredTab, setSheetPreferredTab] = (0, import_react5.useState)("report");
2530
+ const [bannerDismissed, setBannerDismissed] = (0, import_react5.useState)(false);
2531
+ const [sheetScreenshot, setSheetScreenshot] = (0, import_react5.useState)(null);
2532
+ const reporterTokenRef = (0, import_react5.useRef)(`rn-${config.projectId}-anon`);
1864
2533
  let resolveReporterTokenReady = () => {
1865
2534
  };
1866
- const reporterTokenReadyRef = (0, import_react4.useRef)(
2535
+ const reporterTokenReadyRef = (0, import_react5.useRef)(
1867
2536
  new Promise((resolve) => {
1868
2537
  resolveReporterTokenReady = resolve;
1869
2538
  })
1870
2539
  );
1871
- const userTokenRef = (0, import_react4.useRef)(null);
1872
- const pageContextRef = (0, import_react4.useRef)(null);
2540
+ const userTokenRef = (0, import_react5.useRef)(null);
2541
+ const pageContextRef = (0, import_react5.useRef)(null);
1873
2542
  const rawEndpoint = config.apiEndpoint ?? config.endpoint;
1874
2543
  if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
1875
2544
  throw new Error(
@@ -1879,15 +2548,15 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1879
2548
  if (config.endpoint && !config.apiEndpoint && __DEV__) {
1880
2549
  console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
1881
2550
  }
1882
- const apiEndpoint = rawEndpoint ?? import_core8.DEFAULT_API_ENDPOINT;
1883
- (0, import_react4.useEffect)(() => {
2551
+ const apiEndpoint = rawEndpoint ?? import_core10.DEFAULT_API_ENDPOINT;
2552
+ (0, import_react5.useEffect)(() => {
1884
2553
  if (config.capture?.console !== false) {
1885
2554
  consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
1886
2555
  }
1887
2556
  if (config.capture?.network !== false) {
1888
2557
  networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
1889
2558
  }
1890
- apiClientRef.current = (0, import_core8.createApiClient)({
2559
+ apiClientRef.current = (0, import_core10.createApiClient)({
1891
2560
  projectId: config.projectId,
1892
2561
  apiKey: config.apiKey,
1893
2562
  apiEndpoint,
@@ -1924,14 +2593,14 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1924
2593
  if (existing && existing.startsWith("mushi_")) {
1925
2594
  reporterTokenRef.current = existing;
1926
2595
  } else {
1927
- const fresh = `mushi_${(0, import_core8.newUuid)()}`;
2596
+ const fresh = `mushi_${(0, import_core10.newUuid)()}`;
1928
2597
  reporterTokenRef.current = fresh;
1929
2598
  await saveReporterToken(fresh, secureStorage);
1930
2599
  }
1931
2600
  resolveReporterTokenReady();
1932
2601
  } catch {
1933
2602
  if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
1934
- reporterTokenRef.current = `mushi_${(0, import_core8.newUuid)()}`;
2603
+ reporterTokenRef.current = `mushi_${(0, import_core10.newUuid)()}`;
1935
2604
  }
1936
2605
  resolveReporterTokenReady();
1937
2606
  }
@@ -1942,11 +2611,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1942
2611
  networkRef.current?.restore();
1943
2612
  };
1944
2613
  }, []);
1945
- (0, import_react4.useEffect)(() => {
2614
+ (0, import_react5.useEffect)(() => {
1946
2615
  let unsubscribe;
1947
2616
  try {
1948
- const NetInfo = require("@react-native-community/netinfo").default;
1949
- unsubscribe = NetInfo.addEventListener((state) => {
2617
+ unsubscribe = resolveNetInfo(config.netInfo)?.addEventListener((state) => {
1950
2618
  if (state.isConnected && state.isInternetReachable) {
1951
2619
  queueRef.current?.flush().catch(() => {
1952
2620
  });
@@ -1955,8 +2623,8 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1955
2623
  } catch {
1956
2624
  }
1957
2625
  return () => unsubscribe?.();
1958
- }, []);
1959
- const open = (0, import_react4.useCallback)(() => {
2626
+ }, [config.netInfo]);
2627
+ const open = (0, import_react5.useCallback)(() => {
1960
2628
  setSheetPreferredTab("report");
1961
2629
  breadcrumbsRef.current.add({
1962
2630
  category: "lifecycle",
@@ -1969,11 +2637,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1969
2637
  setSheetVisible(true);
1970
2638
  return;
1971
2639
  }
1972
- let vshot = null;
1973
- try {
1974
- vshot = require("react-native-view-shot");
1975
- } catch {
1976
- }
2640
+ const vshot = resolveViewShot(config.viewShot);
1977
2641
  if (!vshot) {
1978
2642
  setSheetScreenshot(null);
1979
2643
  setSheetVisible(true);
@@ -1986,16 +2650,16 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1986
2650
  }).finally(() => {
1987
2651
  setSheetVisible(true);
1988
2652
  });
1989
- }, [config.capture?.screenshot]);
1990
- const openAssistant = (0, import_react4.useCallback)(() => {
2653
+ }, [config.capture?.screenshot, config.viewShot]);
2654
+ const openAssistant = (0, import_react5.useCallback)(() => {
1991
2655
  if (!config.assistant?.enabled) return;
1992
2656
  setSheetPreferredTab("assistant");
1993
2657
  setSheetScreenshot(null);
1994
2658
  setSheetVisible(true);
1995
2659
  }, [config.assistant?.enabled]);
1996
- const close = (0, import_react4.useCallback)(() => setSheetVisible(false), []);
1997
- const attachTo = (0, import_react4.useCallback)(() => ({ onPress: open }), [open]);
1998
- (0, import_react4.useEffect)(() => {
2660
+ const close = (0, import_react5.useCallback)(() => setSheetVisible(false), []);
2661
+ const attachTo = (0, import_react5.useCallback)(() => ({ onPress: open }), [open]);
2662
+ (0, import_react5.useEffect)(() => {
1999
2663
  const trigger2 = config.widget?.trigger ?? "button";
2000
2664
  if (trigger2 !== "shake" && trigger2 !== "both") return;
2001
2665
  let disposed = false;
@@ -2003,8 +2667,8 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2003
2667
  let lastShake = 0;
2004
2668
  const threshold = config.widget?.shakeThreshold ?? 2.7;
2005
2669
  try {
2006
- const mod = require("expo-sensors");
2007
- if (!disposed) {
2670
+ const mod = resolveExpoSensors(config.expoSensors);
2671
+ if (mod && !disposed) {
2008
2672
  mod.Accelerometer.setUpdateInterval(120);
2009
2673
  const sub = mod.Accelerometer.addListener((evt) => {
2010
2674
  const g = Math.sqrt(evt.x * evt.x + evt.y * evt.y + evt.z * evt.z);
@@ -2022,8 +2686,8 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2022
2686
  disposed = true;
2023
2687
  cleanup?.();
2024
2688
  };
2025
- }, [config.widget?.trigger, config.widget?.shakeThreshold, open]);
2026
- const submitReport = (0, import_react4.useCallback)(
2689
+ }, [config.widget?.trigger, config.widget?.shakeThreshold, config.expoSensors, open]);
2690
+ const submitReport = (0, import_react5.useCallback)(
2027
2691
  async (data) => {
2028
2692
  await reporterTokenReadyRef.current;
2029
2693
  const deviceInfo = getDeviceInfo();
@@ -2051,7 +2715,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2051
2715
  const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
2052
2716
  const breadcrumbs = breadcrumbsRef.current.getAll().map((c) => ({
2053
2717
  ...c,
2054
- message: (0, import_core8.scrubPii)(c.message)
2718
+ message: (0, import_core10.scrubPii)(c.message)
2055
2719
  }));
2056
2720
  const timeline = breadcrumbs.map((c) => ({
2057
2721
  ts: c.timestamp,
@@ -2060,10 +2724,11 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2060
2724
  }));
2061
2725
  const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
2062
2726
  const report = {
2063
- id: (0, import_core8.newUuid)(),
2727
+ id: (0, import_core10.newUuid)(),
2064
2728
  projectId: config.projectId,
2065
2729
  category: data.category,
2066
- description: (0, import_core8.scrubPii)(data.description),
2730
+ ...data.userCategory ? { userCategory: data.userCategory } : {},
2731
+ description: (0, import_core10.scrubPii)(data.description),
2067
2732
  environment: {
2068
2733
  userAgent: deviceInfo.systemName ?? "ReactNative",
2069
2734
  platform: deviceInfo.platform ?? "mobile",
@@ -2076,7 +2741,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2076
2741
  },
2077
2742
  consoleLogs: (consoleRef.current?.getEntries() ?? []).map((entry) => ({
2078
2743
  ...entry,
2079
- message: (0, import_core8.scrubPii)(entry.message)
2744
+ message: (0, import_core10.scrubPii)(entry.message)
2080
2745
  })),
2081
2746
  networkLogs: networkRef.current?.getEntries().map((entry) => ({
2082
2747
  ...entry,
@@ -2106,7 +2771,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2106
2771
  const code = result.error?.code ?? "";
2107
2772
  const isCredentialError = code === "HTTP_401" || code === "HTTP_403" || code.includes("UNAUTHORIZED") || code.includes("FORBIDDEN") || code.includes("INVALID_API_KEY");
2108
2773
  if (isCredentialError) {
2109
- const where = apiEndpoint.replace(/\/$/, "") === import_core8.DEFAULT_API_ENDPOINT ? "Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects" : "Get the correct values from your Mushi console's Projects page.";
2774
+ const where = apiEndpoint.replace(/\/$/, "") === import_core10.DEFAULT_API_ENDPOINT ? "Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects" : "Get the correct values from your Mushi console's Projects page.";
2110
2775
  console.error(
2111
2776
  '[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). ' + where
2112
2777
  );
@@ -2122,17 +2787,18 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2122
2787
  };
2123
2788
  } else {
2124
2789
  config.callbacks?.onReportSubmitted?.(report.id);
2790
+ void writeReporterFlag(config.projectId, "has_reports", "1");
2125
2791
  return { ok: true };
2126
2792
  }
2127
2793
  },
2128
2794
  [config.projectId, config.callbacks, apiEndpoint]
2129
2795
  );
2130
- const userRef = (0, import_react4.useRef)(null);
2131
- const metadataRef = (0, import_react4.useRef)({});
2132
- const screenRef = (0, import_react4.useRef)(null);
2133
- const activityQueueRef = (0, import_react4.useRef)([]);
2134
- const rewardsFlushRef = (0, import_react4.useRef)(null);
2135
- (0, import_react4.useEffect)(() => {
2796
+ const userRef = (0, import_react5.useRef)(null);
2797
+ const metadataRef = (0, import_react5.useRef)({});
2798
+ const screenRef = (0, import_react5.useRef)(null);
2799
+ const activityQueueRef = (0, import_react5.useRef)([]);
2800
+ const rewardsFlushRef = (0, import_react5.useRef)(null);
2801
+ (0, import_react5.useEffect)(() => {
2136
2802
  const rewards = config.rewards;
2137
2803
  if (!rewards?.enabled || !rewards.trackActivity) return;
2138
2804
  const flushMs = Math.max(3e4, rewards.flushIntervalMs ?? 3e5);
@@ -2152,9 +2818,9 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2152
2818
  if (rewardsFlushRef.current) clearInterval(rewardsFlushRef.current);
2153
2819
  };
2154
2820
  }, [config.rewards?.enabled, config.rewards?.trackActivity, config.rewards?.flushIntervalMs]);
2155
- const eventTrackerRef = (0, import_react4.useRef)(null);
2156
- const sessionTrackerRef = (0, import_react4.useRef)(null);
2157
- (0, import_react4.useEffect)(() => {
2821
+ const eventTrackerRef = (0, import_react5.useRef)(null);
2822
+ const sessionTrackerRef = (0, import_react5.useRef)(null);
2823
+ (0, import_react5.useEffect)(() => {
2158
2824
  const getRoute = () => screenRef.current?.route ?? screenRef.current?.name ?? null;
2159
2825
  const tracker = createRNEventTracker({
2160
2826
  projectId: config.projectId,
@@ -2167,7 +2833,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2167
2833
  getRoute,
2168
2834
  sdkVersion: MUSHI_SDK_VERSION,
2169
2835
  config: config.analytics,
2170
- scrub: import_core8.scrubPii
2836
+ scrub: import_core10.scrubPii
2171
2837
  });
2172
2838
  eventTrackerRef.current = tracker;
2173
2839
  const sessions = config.trackSessions === false ? null : createRNSessionTracker({
@@ -2185,8 +2851,8 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2185
2851
  sessionTrackerRef.current = sessions;
2186
2852
  let sub;
2187
2853
  try {
2188
- if (typeof import_react_native5.AppState?.addEventListener === "function") {
2189
- sub = import_react_native5.AppState.addEventListener("change", (state) => {
2854
+ if (typeof import_react_native6.AppState?.addEventListener === "function") {
2855
+ sub = import_react_native6.AppState.addEventListener("change", (state) => {
2190
2856
  sessions?.onAppStateChange(state);
2191
2857
  if (state !== "active") tracker.flush().catch(() => {
2192
2858
  });
@@ -2209,7 +2875,71 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2209
2875
  config.analytics?.flushIntervalMs,
2210
2876
  config.trackSessions
2211
2877
  ]);
2212
- const instance = (0, import_react4.useMemo)(
2878
+ const reporterUpdatesRef = (0, import_react5.useRef)(
2879
+ createReporterUpdateHub(async () => {
2880
+ const client = apiClientRef.current;
2881
+ if (!client) return null;
2882
+ await reporterTokenReadyRef.current;
2883
+ const res = await (0, import_reporter_channels2.reporterChannels)(client).getUpdates(reporterTokenRef.current);
2884
+ return res.ok ? res.data ?? null : null;
2885
+ })
2886
+ );
2887
+ (0, import_react5.useEffect)(() => {
2888
+ let sub;
2889
+ try {
2890
+ if (typeof import_react_native6.AppState?.addEventListener === "function") {
2891
+ sub = import_react_native6.AppState.addEventListener("change", (state) => {
2892
+ if (state === "active" && reporterUpdatesRef.current.listenerCount() > 0) void reporterUpdatesRef.current.emit();
2893
+ });
2894
+ }
2895
+ } catch {
2896
+ }
2897
+ return () => sub?.remove();
2898
+ }, []);
2899
+ const [toast, setToast] = (0, import_react5.useState)(null);
2900
+ const toastShownRef = (0, import_react5.useRef)(false);
2901
+ const toastEnabled = config.notifications?.toast !== false;
2902
+ const maybeShowToast = (0, import_react5.useCallback)(async () => {
2903
+ if (!toastEnabled || toastShownRef.current) return;
2904
+ if (await readReporterFlag(config.projectId, "has_reports") !== "1") return;
2905
+ const updates = await reporterUpdatesRef.current.emit();
2906
+ if (!updates) return;
2907
+ const now = Date.now();
2908
+ const show = (0, import_reporter_ui3.reporterShouldShowToast)({
2909
+ enabled: toastEnabled,
2910
+ unreadTotal: updates.unread_total,
2911
+ shownThisSession: toastShownRef.current,
2912
+ lastShownAt: Number(await readReporterFlag(config.projectId, "toast_shown_at")) || null,
2913
+ now
2914
+ });
2915
+ if (!show) return;
2916
+ toastShownRef.current = true;
2917
+ void writeReporterFlag(config.projectId, "toast_shown_at", String(now));
2918
+ const locale = (0, import_reporter_ui3.resolveReporterLocale)(
2919
+ typeof navigator !== "undefined" ? navigator.language : void 0
2920
+ );
2921
+ setToast({ message: (0, import_reporter_ui3.reporterToastMessage)(updates, locale), reportId: updates.latest[0]?.report_id ?? null });
2922
+ }, [toastEnabled, config.projectId]);
2923
+ (0, import_react5.useEffect)(() => {
2924
+ void reporterTokenReadyRef.current.then(() => maybeShowToast());
2925
+ let sub;
2926
+ try {
2927
+ if (typeof import_react_native6.AppState?.addEventListener === "function") {
2928
+ sub = import_react_native6.AppState.addEventListener("change", (state) => {
2929
+ if (state === "active") void maybeShowToast();
2930
+ });
2931
+ }
2932
+ } catch {
2933
+ }
2934
+ return () => sub?.remove();
2935
+ }, [maybeShowToast]);
2936
+ const openMyReports = (0, import_react5.useCallback)(() => {
2937
+ setToast(null);
2938
+ setSheetScreenshot(null);
2939
+ setSheetPreferredTab("inbox");
2940
+ setSheetVisible(true);
2941
+ }, []);
2942
+ const instance = (0, import_react5.useMemo)(
2213
2943
  () => ({
2214
2944
  open,
2215
2945
  close,
@@ -2240,7 +2970,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2240
2970
  identifyWithToken(token) {
2241
2971
  userTokenRef.current = token && typeof token === "string" ? token : null;
2242
2972
  if (userTokenRef.current) {
2243
- const claims = (0, import_core8.parseIdentityToken)(userTokenRef.current);
2973
+ const claims = (0, import_core10.parseIdentityToken)(userTokenRef.current);
2244
2974
  if (claims?.sub) {
2245
2975
  userRef.current = {
2246
2976
  id: claims.sub,
@@ -2295,6 +3025,54 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2295
3025
  const res = await client.listReporterComments(reportId, reporterTokenRef.current);
2296
3026
  return res.ok ? res.data.comments ?? [] : [];
2297
3027
  },
3028
+ async loadMyThread(reportId) {
3029
+ const client = apiClientRef.current;
3030
+ if (!client) return null;
3031
+ try {
3032
+ await reporterTokenReadyRef.current;
3033
+ const res = await client.listReporterComments(reportId, reporterTokenRef.current);
3034
+ return res.ok ? res.data?.comments ?? [] : null;
3035
+ } catch {
3036
+ return null;
3037
+ }
3038
+ },
3039
+ async markReportRead(reportId) {
3040
+ const client = apiClientRef.current;
3041
+ if (!client) return 0;
3042
+ await reporterTokenReadyRef.current;
3043
+ return markReporterReportRead(client, reporterTokenRef.current, reportId);
3044
+ },
3045
+ getReporterUpdates: () => reporterUpdatesRef.current.emit(),
3046
+ async loadMyReportDetail(reportId) {
3047
+ const client = apiClientRef.current;
3048
+ if (!client) return null;
3049
+ try {
3050
+ await reporterTokenReadyRef.current;
3051
+ const res = await (0, import_reporter_channels2.reporterChannels)(client).getReport(reportId, reporterTokenRef.current);
3052
+ if (res.ok) return res.data ?? null;
3053
+ return res.error?.status === 404 && res.error.code !== "NOT_FOUND" ? "unsupported" : null;
3054
+ } catch {
3055
+ return null;
3056
+ }
3057
+ },
3058
+ emailPrefill() {
3059
+ return config.notifications?.emailFromIdentity ? userRef.current?.email ?? null : null;
3060
+ },
3061
+ openMyReports,
3062
+ onReporterUpdate: (cb) => reporterUpdatesRef.current.subscribe(cb),
3063
+ async getNotificationPrefs() {
3064
+ const client = apiClientRef.current;
3065
+ if (!client) return null;
3066
+ await reporterTokenReadyRef.current;
3067
+ const res = await (0, import_reporter_channels2.reporterChannels)(client).getPrefs(reporterTokenRef.current);
3068
+ return res.ok ? res.data ?? null : null;
3069
+ },
3070
+ async setNotificationPrefs(update) {
3071
+ const client = apiClientRef.current;
3072
+ if (!client) return { ok: false, error: { code: "NOT_INITIALIZED", message: "Mushi is not ready yet" } };
3073
+ await reporterTokenReadyRef.current;
3074
+ return (0, import_reporter_channels2.reporterChannels)(client).setPrefs(reporterTokenRef.current, update);
3075
+ },
2298
3076
  async replyToReport(reportId, body) {
2299
3077
  const client = apiClientRef.current;
2300
3078
  if (!client) return null;
@@ -2365,14 +3143,14 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2365
3143
  return res.ok ? res.data : null;
2366
3144
  }
2367
3145
  }),
2368
- [open, close, attachTo, submitReport, openAssistant, config.rewards?.enabled, config.assistant?.enabled]
3146
+ [open, close, attachTo, submitReport, openAssistant, openMyReports, config.rewards?.enabled, config.assistant?.enabled, config.notifications?.emailFromIdentity]
2369
3147
  );
2370
3148
  const trigger = config.widget?.trigger ?? "button";
2371
3149
  const showBanner = trigger === "banner" && !bannerDismissed;
2372
3150
  const showFab = !showBanner && (trigger === "button" || trigger === "both" || trigger === "auto" || trigger === "edge-tab");
2373
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(MushiContext.Provider, { value: instance, children: [
3151
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(MushiContext.Provider, { value: instance, children: [
2374
3152
  children,
2375
- showBanner ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3153
+ showBanner ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2376
3154
  MushiBanner,
2377
3155
  {
2378
3156
  onPress: open,
@@ -2381,7 +3159,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2381
3159
  label: config.widget?.banner?.label
2382
3160
  }
2383
3161
  ) : null,
2384
- showFab && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3162
+ showFab && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2385
3163
  MushiFloatingButton,
2386
3164
  {
2387
3165
  onPress: open,
@@ -2389,7 +3167,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2389
3167
  inset: config.widget?.inset
2390
3168
  }
2391
3169
  ),
2392
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3170
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2393
3171
  MushiBottomSheet,
2394
3172
  {
2395
3173
  visible: sheetVisible,
@@ -2402,13 +3180,25 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2402
3180
  assistantLabel: config.assistant?.label,
2403
3181
  assistantGreeting: config.assistant?.greeting,
2404
3182
  assistantSuggestions: config.assistant?.suggestions,
2405
- inboxPollIntervalMs: config.widget?.inboxPollIntervalMs ?? 0
3183
+ inboxPollIntervalMs: config.widget?.inboxPollIntervalMs ?? 0,
3184
+ theme: config.widget?.theme,
3185
+ minDescriptionLength: config.widget?.minDescriptionLength,
3186
+ emailOptIn: config.notifications?.email !== false
3187
+ }
3188
+ ),
3189
+ toast ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3190
+ MushiUpdateToast,
3191
+ {
3192
+ message: toast.message,
3193
+ onView: openMyReports,
3194
+ onDismiss: () => setToast(null),
3195
+ theme: config.widget?.theme
2406
3196
  }
2407
- )
3197
+ ) : null
2408
3198
  ] });
2409
3199
  }
2410
3200
  function useMushiContext() {
2411
- return (0, import_react4.useContext)(MushiContext);
3201
+ return (0, import_react5.useContext)(MushiContext);
2412
3202
  }
2413
3203
 
2414
3204
  // src/hooks/useMushi.ts
@@ -2417,10 +3207,10 @@ function useMushi() {
2417
3207
  }
2418
3208
 
2419
3209
  // src/hooks/useMushiReport.ts
2420
- var import_react5 = require("react");
3210
+ var import_react6 = require("react");
2421
3211
  function useMushiReport() {
2422
3212
  const mushi = useMushiContext();
2423
- const submitReport = (0, import_react5.useCallback)(
3213
+ const submitReport = (0, import_react6.useCallback)(
2424
3214
  async (data) => {
2425
3215
  if (!mushi) throw new Error("MushiProvider not found");
2426
3216
  await mushi.submitReport(data);
@@ -2431,22 +3221,22 @@ function useMushiReport() {
2431
3221
  }
2432
3222
 
2433
3223
  // src/hooks/useMushiWidget.ts
2434
- var import_react6 = require("react");
3224
+ var import_react7 = require("react");
2435
3225
  function useMushiWidget() {
2436
3226
  const mushi = useMushiContext();
2437
3227
  return {
2438
- open: (0, import_react6.useCallback)(() => mushi?.open(), [mushi]),
2439
- close: (0, import_react6.useCallback)(() => mushi?.close(), [mushi])
3228
+ open: (0, import_react7.useCallback)(() => mushi?.open(), [mushi]),
3229
+ close: (0, import_react7.useCallback)(() => mushi?.close(), [mushi])
2440
3230
  };
2441
3231
  }
2442
3232
 
2443
3233
  // src/components/MushiTrigger.tsx
2444
- var import_react7 = require("react");
3234
+ var import_react8 = require("react");
2445
3235
  function MushiTrigger({ children }) {
2446
3236
  const mushi = useMushiContext();
2447
- if (!(0, import_react7.isValidElement)(children)) return children;
3237
+ if (!(0, import_react8.isValidElement)(children)) return children;
2448
3238
  const originalOnPress = children.props.onPress;
2449
- return (0, import_react7.cloneElement)(children, {
3239
+ return (0, import_react8.cloneElement)(children, {
2450
3240
  onPress: () => {
2451
3241
  originalOnPress?.();
2452
3242
  mushi?.open();
@@ -2455,10 +3245,10 @@ function MushiTrigger({ children }) {
2455
3245
  }
2456
3246
 
2457
3247
  // src/capture/navigation-capture.ts
2458
- var import_react8 = require("react");
3248
+ var import_react9 = require("react");
2459
3249
  function useNavigationCapture(navigationRef) {
2460
- const historyRef = (0, import_react8.useRef)([]);
2461
- (0, import_react8.useEffect)(() => {
3250
+ const historyRef = (0, import_react9.useRef)([]);
3251
+ (0, import_react9.useEffect)(() => {
2462
3252
  if (!navigationRef?.addListener || !navigationRef?.getCurrentRoute) return;
2463
3253
  const unsubscribe = navigationRef.addListener("state", () => {
2464
3254
  const route = navigationRef.getCurrentRoute?.();