@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.js CHANGED
@@ -1,15 +1,9 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
1
 
8
2
  // src/provider.tsx
9
3
  import {
10
4
  createContext,
11
5
  useContext,
12
- useEffect as useEffect3,
6
+ useEffect as useEffect4,
13
7
  useRef as useRef3,
14
8
  useState as useState2,
15
9
  useCallback as useCallback4,
@@ -24,6 +18,180 @@ import {
24
18
  parseIdentityToken,
25
19
  scrubPii as scrubPii3
26
20
  } from "@mushi-mushi/core";
21
+ import {
22
+ reporterChannels as reporterChannels2
23
+ } from "@mushi-mushi/core/reporter-channels";
24
+ import { reporterShouldShowToast, reporterToastMessage, resolveReporterLocale as resolveReporterLocale3 } from "@mushi-mushi/core/reporter-ui";
25
+
26
+ // src/storage/reporter-flags.ts
27
+ async function readReporterFlag(projectId, flag) {
28
+ try {
29
+ const store = (await import("@react-native-async-storage/async-storage")).default;
30
+ return await store.getItem(`@mushi:${flag}:${projectId}`);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ async function writeReporterFlag(projectId, flag, value) {
36
+ try {
37
+ const store = (await import("@react-native-async-storage/async-storage")).default;
38
+ await store.setItem(`@mushi:${flag}:${projectId}`, value);
39
+ } catch {
40
+ }
41
+ }
42
+
43
+ // src/components/MushiUpdateToast.tsx
44
+ import { useEffect } from "react";
45
+ import { Text, TouchableOpacity, View, StyleSheet, useColorScheme } from "react-native";
46
+ import { MUSHI_SHADOW_INK } from "@mushi-mushi/core";
47
+ import { reporterCopy, resolveReporterLocale } from "@mushi-mushi/core/reporter-ui";
48
+
49
+ // src/theme.ts
50
+ import { mushiPalette, MUSHI_CONTROL_DISABLED, MUSHI_RADIUS } from "@mushi-mushi/core";
51
+ function hexToRgb(hex) {
52
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})([0-9a-f]{2})?$/i.exec(hex.trim());
53
+ if (!m) return null;
54
+ const h = m[1].length === 3 ? m[1].split("").map((c) => c + c).join("") : m[1];
55
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
56
+ }
57
+ function luminance([r, g, b]) {
58
+ const lin = (c) => {
59
+ const s5 = c / 255;
60
+ return s5 <= 0.03928 ? s5 / 12.92 : ((s5 + 0.055) / 1.055) ** 2.4;
61
+ };
62
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
63
+ }
64
+ function contrastingText(bg, fallback) {
65
+ const rgb = hexToRgb(bg);
66
+ if (!rgb) return fallback;
67
+ const l = luminance(rgb);
68
+ return 1.05 / (l + 0.05) >= (l + 0.05) / 0.05 ? "#FFFFFF" : "#000000";
69
+ }
70
+ function resolveRNTheme(dark, override = {}) {
71
+ const pal = mushiPalette(dark ? "dark" : "light");
72
+ const base = {
73
+ bg: dark ? pal.paper : pal.paperRaised,
74
+ fg: pal.ink,
75
+ muted: pal.inkMuted,
76
+ surface: dark ? pal.paperRaised : pal.paper,
77
+ border: pal.ruleStrong,
78
+ accent: pal.ink,
79
+ accentFg: dark ? pal.paper : pal.paperRaised,
80
+ success: pal.ok,
81
+ error: pal.danger,
82
+ fontFamily: void 0,
83
+ radius: MUSHI_RADIUS.card
84
+ };
85
+ const defined = Object.fromEntries(
86
+ Object.entries(override).filter(([, v]) => v !== void 0 && v !== null && v !== "")
87
+ );
88
+ const merged = { ...base, ...defined };
89
+ if (defined.accent && !defined.accentFg) merged.accentFg = contrastingText(defined.accent, base.accentFg);
90
+ return {
91
+ ...merged,
92
+ backdrop: dark ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.35)",
93
+ disabled: dark ? MUSHI_CONTROL_DISABLED.dark : MUSHI_CONTROL_DISABLED.light,
94
+ disabledFg: pal.inkFaint
95
+ };
96
+ }
97
+
98
+ // src/components/MushiUpdateToast.tsx
99
+ import { jsx, jsxs } from "react/jsx-runtime";
100
+ var AUTO_HIDE_MS = 8e3;
101
+ var MushiUpdateToast = ({ message, onView, onDismiss, theme }) => {
102
+ const colors = resolveRNTheme(useColorScheme() === "dark", theme);
103
+ const copy = reporterCopy(
104
+ resolveReporterLocale(typeof navigator !== "undefined" ? navigator.language : void 0)
105
+ ).ui;
106
+ const font = colors.fontFamily ? { fontFamily: colors.fontFamily } : null;
107
+ useEffect(() => {
108
+ const timer = setTimeout(onDismiss, AUTO_HIDE_MS);
109
+ return () => clearTimeout(timer);
110
+ }, [onDismiss]);
111
+ return /* @__PURE__ */ jsxs(
112
+ View,
113
+ {
114
+ style: [s.toast, { backgroundColor: colors.bg, borderColor: colors.border, borderRadius: colors.radius }],
115
+ accessibilityLiveRegion: "polite",
116
+ accessibilityRole: "alert",
117
+ children: [
118
+ /* @__PURE__ */ jsx(Text, { style: [s.text, { color: colors.fg }, font], numberOfLines: 2, children: message }),
119
+ /* @__PURE__ */ jsx(
120
+ TouchableOpacity,
121
+ {
122
+ onPress: onView,
123
+ accessibilityRole: "button",
124
+ style: [s.btn, { backgroundColor: colors.accent, borderRadius: colors.radius }],
125
+ children: /* @__PURE__ */ jsx(Text, { style: [{ color: colors.accentFg, fontWeight: "600" }, font], children: copy.view })
126
+ }
127
+ ),
128
+ /* @__PURE__ */ jsx(
129
+ TouchableOpacity,
130
+ {
131
+ onPress: onDismiss,
132
+ accessibilityRole: "button",
133
+ accessibilityLabel: copy.done,
134
+ hitSlop: { top: 10, bottom: 10, left: 10, right: 10 },
135
+ children: /* @__PURE__ */ jsx(Text, { style: [{ color: colors.muted, fontSize: 18 }, font], children: "\xD7" })
136
+ }
137
+ )
138
+ ]
139
+ }
140
+ );
141
+ };
142
+ var s = StyleSheet.create({
143
+ toast: {
144
+ position: "absolute",
145
+ left: 16,
146
+ right: 16,
147
+ bottom: 32,
148
+ flexDirection: "row",
149
+ alignItems: "center",
150
+ gap: 10,
151
+ paddingVertical: 10,
152
+ paddingHorizontal: 14,
153
+ borderWidth: StyleSheet.hairlineWidth,
154
+ shadowColor: MUSHI_SHADOW_INK,
155
+ shadowOpacity: 0.18,
156
+ shadowRadius: 12,
157
+ shadowOffset: { width: 0, height: 6 },
158
+ elevation: 6
159
+ },
160
+ text: { flex: 1, fontSize: 14 },
161
+ btn: { paddingHorizontal: 12, paddingVertical: 6 }
162
+ });
163
+
164
+ // src/optional-modules.ts
165
+ function unwrap(mod, key) {
166
+ if (!mod || typeof mod !== "object" && typeof mod !== "function") return null;
167
+ if (key in mod) return mod;
168
+ const def = mod.default;
169
+ return def && (typeof def === "object" || typeof def === "function") && key in def ? def : null;
170
+ }
171
+ function resolveNetInfo(injected) {
172
+ if (injected) return unwrap(injected, "addEventListener");
173
+ try {
174
+ return unwrap(require("@react-native-community/netinfo"), "addEventListener");
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+ function resolveViewShot(injected) {
180
+ if (injected) return unwrap(injected, "captureScreen");
181
+ try {
182
+ return unwrap(require("react-native-view-shot"), "captureScreen");
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+ function resolveExpoSensors(injected) {
188
+ if (injected) return unwrap(injected, "Accelerometer");
189
+ try {
190
+ return unwrap(require("expo-sensors"), "Accelerometer");
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
27
195
 
28
196
  // src/capture/console-capture.ts
29
197
  import { scrubPii } from "@mushi-mushi/core";
@@ -456,78 +624,147 @@ function sleep(ms) {
456
624
  import {
457
625
  useRef,
458
626
  useState,
459
- useEffect,
627
+ useEffect as useEffect2,
460
628
  useCallback
461
629
  } from "react";
462
630
  import {
463
631
  Modal,
464
- View,
465
- Text,
632
+ View as View2,
633
+ Text as Text2,
466
634
  TextInput,
467
635
  Image,
468
- TouchableOpacity,
636
+ TouchableOpacity as TouchableOpacity2,
469
637
  Animated,
470
638
  PanResponder,
471
- StyleSheet,
639
+ StyleSheet as StyleSheet2,
472
640
  Dimensions as Dimensions2,
473
641
  Platform as Platform2,
474
642
  KeyboardAvoidingView,
475
643
  ScrollView,
476
- useColorScheme
644
+ AccessibilityInfo,
645
+ useColorScheme as useColorScheme2
477
646
  } from "react-native";
478
647
  import {
479
- mushiPalette,
480
- MUSHI_BANNER_NEON,
481
648
  MUSHI_COPY,
482
- MUSHI_CONTROL_DISABLED,
483
649
  MUSHI_INVERSE,
484
- MUSHI_RADIUS,
485
- MUSHI_SHADOW_INK,
650
+ MUSHI_RADIUS as MUSHI_RADIUS2,
651
+ MUSHI_SHADOW_INK as MUSHI_SHADOW_INK2,
486
652
  MUSHI_SPACING,
487
653
  MUSHI_TYPE
488
654
  } from "@mushi-mushi/core";
655
+ import {
656
+ REPORTER_CATEGORIES,
657
+ isPlausibleReporterEmail,
658
+ isReporterConversation,
659
+ reporterCanSend,
660
+ reporterCategoryLabel,
661
+ reporterChipToReport,
662
+ reporterCopy as reporterCopy2,
663
+ reporterStatus,
664
+ reporterTimelineEntryText,
665
+ resolveReporterLocale as resolveReporterLocale2
666
+ } from "@mushi-mushi/core/reporter-ui";
489
667
  import { getLocale } from "@mushi-mushi/web/i18n";
490
668
 
491
- // src/reporter-status.ts
492
- function reporterStatusShort(status) {
493
- switch (status) {
494
- case "new":
495
- case "queued":
496
- case "pending":
497
- case "submitted":
498
- return "Received";
499
- case "classified":
500
- case "triaged":
501
- case "grouped":
502
- case "dispatched":
503
- return "In review";
504
- case "fixing":
505
- return "Fixing";
506
- case "fixed":
507
- case "resolved":
508
- case "completed":
509
- return "Fixed";
510
- case "dismissed":
511
- return "Closed";
512
- default:
513
- return status.replace(/_/g, " ").slice(0, 16);
669
+ // src/reporter-thread.ts
670
+ import { reporterChannels } from "@mushi-mushi/core/reporter-channels";
671
+ var THREAD_LOAD_TIMEOUT_MS = 12e3;
672
+ function settleWithin(p, ms) {
673
+ return new Promise((resolve) => {
674
+ const timer = setTimeout(() => resolve(null), ms);
675
+ p.then(
676
+ (value) => {
677
+ clearTimeout(timer);
678
+ resolve(value);
679
+ },
680
+ () => {
681
+ clearTimeout(timer);
682
+ resolve(null);
683
+ }
684
+ );
685
+ });
686
+ }
687
+ function loadReporterThread(load, reportId, timeoutMs = THREAD_LOAD_TIMEOUT_MS) {
688
+ let started;
689
+ try {
690
+ started = load(reportId);
691
+ } catch {
692
+ return Promise.resolve(null);
693
+ }
694
+ return settleWithin(started, timeoutMs);
695
+ }
696
+ async function markReporterReportRead(client, reporterToken, reportId) {
697
+ try {
698
+ const v2 = await reporterChannels(client).markReportRead(reportId, reporterToken);
699
+ if (v2.ok) return v2.data?.marked_read ?? 0;
700
+ if (v2.error?.status !== 404) return 0;
701
+ const res = await client.listNotifications(reporterToken, { limit: 100 });
702
+ if (!res.ok) return 0;
703
+ const rows = res.data?.notifications ?? [];
704
+ const ids = rows.filter((n) => !n.read_at && n.payload?.reportId === reportId).map((n) => String(n.id));
705
+ let marked = 0;
706
+ for (const id of ids) {
707
+ const r = await client.markNotificationRead(id, reporterToken);
708
+ if (r.ok) marked++;
709
+ }
710
+ return marked;
711
+ } catch {
712
+ return 0;
514
713
  }
515
714
  }
715
+ function createReporterUpdateHub(fetchUpdates) {
716
+ const listeners = /* @__PURE__ */ new Set();
717
+ async function emit() {
718
+ let updates = null;
719
+ try {
720
+ updates = await fetchUpdates();
721
+ } catch {
722
+ return null;
723
+ }
724
+ if (updates) {
725
+ for (const cb of listeners) {
726
+ try {
727
+ cb(updates);
728
+ } catch {
729
+ }
730
+ }
731
+ }
732
+ return updates;
733
+ }
734
+ return {
735
+ subscribe(cb) {
736
+ listeners.add(cb);
737
+ void emit();
738
+ return () => {
739
+ listeners.delete(cb);
740
+ };
741
+ },
742
+ emit,
743
+ listenerCount: () => listeners.size
744
+ };
745
+ }
516
746
 
517
747
  // src/components/MushiBottomSheet.tsx
518
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
748
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
519
749
  var { height: SCREEN_HEIGHT } = Dimensions2.get("window");
520
750
  var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
521
751
  var DISMISS_THRESHOLD = 80;
522
- var CATEGORY_KEYS = ["bug", "slow", "visual", "confusing", "other"];
523
752
  var CATEGORY_EMOJI = {
524
753
  bug: "\u{1F41B}",
525
754
  slow: "\u{1F422}",
526
755
  visual: "\u{1F3A8}",
527
756
  confusing: "\u{1F615}",
528
- other: "\u{1F4AC}"
757
+ idea: "\u{1F4A1}"
529
758
  };
530
- var VERIFIABLE_STATUSES = /* @__PURE__ */ new Set(["fixed", "resolved", "verified"]);
759
+ function commentsToTimeline(comments) {
760
+ return comments.map((c) => ({
761
+ kind: c.author_kind === "reporter" ? "reporter_comment" : "comment",
762
+ at: c.created_at,
763
+ text: c.body,
764
+ body: c.body,
765
+ comment_id: c.id
766
+ }));
767
+ }
531
768
  var MushiBottomSheet = ({
532
769
  visible,
533
770
  onClose,
@@ -539,12 +776,19 @@ var MushiBottomSheet = ({
539
776
  assistantLabel = MUSHI_COPY.assistantTab,
540
777
  assistantGreeting,
541
778
  assistantSuggestions = [],
542
- inboxPollIntervalMs = 0
779
+ inboxPollIntervalMs = 0,
780
+ theme,
781
+ minDescriptionLength,
782
+ emailOptIn = true
543
783
  }) => {
544
784
  const mushi = useMushiContext();
545
785
  const t = getLocale();
786
+ const locale = resolveReporterLocale2(
787
+ typeof navigator !== "undefined" ? navigator.language : void 0
788
+ );
789
+ const rc = reporterCopy2(locale);
546
790
  const greeting = assistantGreeting ?? t.assistant.defaultGreeting;
547
- const scheme = useColorScheme();
791
+ const scheme = useColorScheme2();
548
792
  const dark = scheme === "dark";
549
793
  const translateY = useRef(new Animated.Value(SHEET_HEIGHT)).current;
550
794
  const backdropOpacity = useRef(new Animated.Value(0)).current;
@@ -554,58 +798,150 @@ var MushiBottomSheet = ({
554
798
  const [submitError, setSubmitError] = useState(null);
555
799
  const [sheetTab, setSheetTab] = useState("report");
556
800
  const [inboxReports, setInboxReports] = useState([]);
801
+ const [inboxLoaded, setInboxLoaded] = useState(false);
557
802
  const [inboxLoading, setInboxLoading] = useState(false);
558
803
  const [selectedReportId, setSelectedReportId] = useState(null);
559
- const [threadComments, setThreadComments] = useState([]);
804
+ const selectedRef = useRef(null);
805
+ const [threadItems, setThreadItems] = useState([]);
806
+ const [threadStatus, setThreadStatus] = useState("idle");
560
807
  const [replyText, setReplyText] = useState("");
808
+ const [replyState, setReplyState] = useState("idle");
809
+ const [feedbackSending, setFeedbackSending] = useState(false);
810
+ const [feedbackError, setFeedbackError] = useState(false);
561
811
  const [assistantInput, setAssistantInput] = useState("");
562
812
  const [assistantThreadId, setAssistantThreadId] = useState(null);
563
813
  const [assistantSending, setAssistantSending] = useState(false);
564
814
  const [assistantError, setAssistantError] = useState(null);
565
815
  const [assistantTurns, setAssistantTurns] = useState([]);
566
816
  const [screenshotAttached, setScreenshotAttached] = useState(true);
567
- const loadInbox = useCallback(async () => {
568
- if (!mushi?.listMyReports) return;
569
- setInboxLoading(true);
570
- try {
571
- const rows = await mushi.listMyReports();
572
- setInboxReports(rows);
573
- } finally {
574
- setInboxLoading(false);
575
- }
576
- }, [mushi]);
577
- useEffect(() => {
817
+ const [emailState, setEmailState] = useState("hidden");
818
+ const [emailInput, setEmailInput] = useState("");
819
+ const [reduceMotion, setReduceMotion] = useState(false);
820
+ useEffect2(() => {
821
+ let alive = true;
822
+ AccessibilityInfo?.isReduceMotionEnabled?.().then((on) => alive && setReduceMotion(Boolean(on))).catch(() => void 0);
823
+ const sub = AccessibilityInfo?.addEventListener?.("reduceMotionChanged", (on) => setReduceMotion(Boolean(on)));
824
+ return () => {
825
+ alive = false;
826
+ sub?.remove?.();
827
+ };
828
+ }, []);
829
+ const loadInbox = useCallback(
830
+ async (opts = {}) => {
831
+ if (!mushi?.listMyReports) return;
832
+ if (!opts.silent) setInboxLoading(true);
833
+ try {
834
+ const rows = await mushi.listMyReports();
835
+ setInboxReports(rows.filter((r) => !reporterStatus(r, locale).hidden));
836
+ setInboxLoaded(true);
837
+ } catch {
838
+ } finally {
839
+ if (!opts.silent) setInboxLoading(false);
840
+ }
841
+ },
842
+ [mushi, locale]
843
+ );
844
+ useEffect2(() => {
578
845
  if (visible) {
579
846
  setScreenshotAttached(true);
580
847
  setSheetTab(preferredTab);
581
848
  if (preferredTab === "inbox") void loadInbox();
582
849
  }
583
850
  }, [visible, preferredTab, loadInbox]);
584
- useEffect(() => {
851
+ useEffect2(() => {
585
852
  if (!visible || sheetTab !== "inbox" || inboxPollIntervalMs <= 0) return;
586
853
  const timer = setInterval(() => {
587
- void loadInbox();
854
+ void loadInbox({ silent: true });
588
855
  }, inboxPollIntervalMs);
589
856
  return () => clearInterval(timer);
590
857
  }, [visible, sheetTab, inboxPollIntervalMs, loadInbox]);
591
- const openThread = useCallback(async (reportId) => {
592
- setSelectedReportId(reportId);
593
- if (!mushi?.listMyComments) return;
594
- const comments = await mushi.listMyComments(reportId);
595
- setThreadComments(comments);
596
- }, [mushi]);
858
+ const openThread = useCallback(
859
+ async (reportId, opts = {}) => {
860
+ selectedRef.current = reportId;
861
+ setSelectedReportId(reportId);
862
+ if (!opts.silent) {
863
+ setThreadItems([]);
864
+ setThreadStatus("loading");
865
+ setReplyState("idle");
866
+ setFeedbackError(false);
867
+ }
868
+ let items = null;
869
+ const detail = mushi?.loadMyReportDetail ? await settleWithin(mushi.loadMyReportDetail(reportId), THREAD_LOAD_TIMEOUT_MS) : "unsupported";
870
+ if (detail && detail !== "unsupported") {
871
+ items = detail.timeline;
872
+ } else if (detail === "unsupported") {
873
+ const load = mushi?.loadMyThread ?? mushi?.listMyComments;
874
+ const comments = load ? await loadReporterThread(load, reportId) : null;
875
+ items = comments ? commentsToTimeline(comments) : null;
876
+ }
877
+ if (selectedRef.current !== reportId) return;
878
+ if (items === null) {
879
+ setThreadStatus((prev) => opts.silent && prev === "ready" ? "ready" : "error");
880
+ return;
881
+ }
882
+ setThreadItems(items);
883
+ setThreadStatus("ready");
884
+ if (!opts.silent && mushi?.markReportRead) {
885
+ mushi.markReportRead(reportId).then((marked) => {
886
+ if (marked > 0) {
887
+ setInboxReports((rows) => rows.map((r) => r.id === reportId ? { ...r, unread_count: 0 } : r));
888
+ }
889
+ }).catch(() => void 0);
890
+ }
891
+ },
892
+ [mushi]
893
+ );
894
+ const closeThread = useCallback(() => {
895
+ selectedRef.current = null;
896
+ setSelectedReportId(null);
897
+ setThreadStatus("idle");
898
+ }, []);
597
899
  const sendReply = useCallback(async () => {
598
- if (!mushi?.replyToReport || !selectedReportId || !replyText.trim()) return;
599
- await mushi.replyToReport(selectedReportId, replyText.trim());
600
- setReplyText("");
601
- await openThread(selectedReportId);
602
- }, [mushi, selectedReportId, replyText, openThread]);
603
- const submitFeedback = useCallback(async (signal) => {
604
- if (!mushi?.submitFeedbackSignal || !selectedReportId) return;
605
- await mushi.submitFeedbackSignal(selectedReportId, signal);
606
- await loadInbox();
607
- await openThread(selectedReportId);
608
- }, [mushi, selectedReportId, loadInbox, openThread]);
900
+ const body = replyText.trim();
901
+ const reportId = selectedReportId;
902
+ if (!mushi?.replyToReport || !reportId || !body || replyState === "sending") return;
903
+ setReplyState("sending");
904
+ try {
905
+ const comment = await settleWithin(mushi.replyToReport(reportId, body), THREAD_LOAD_TIMEOUT_MS);
906
+ if (!comment) {
907
+ setReplyState("failed");
908
+ return;
909
+ }
910
+ setReplyText("");
911
+ setReplyState("idle");
912
+ if (selectedRef.current === reportId) {
913
+ setThreadItems((prev) => [
914
+ ...prev,
915
+ { kind: "reporter_comment", at: comment.created_at, text: comment.body, body: comment.body, comment_id: comment.id }
916
+ ]);
917
+ void openThread(reportId, { silent: true });
918
+ }
919
+ } catch {
920
+ setReplyState("failed");
921
+ }
922
+ }, [mushi, selectedReportId, replyText, replyState, openThread]);
923
+ const submitFeedback = useCallback(
924
+ async (signal) => {
925
+ const reportId = selectedReportId;
926
+ if (!mushi?.submitFeedbackSignal || !reportId || feedbackSending) return;
927
+ setFeedbackSending(true);
928
+ setFeedbackError(false);
929
+ try {
930
+ const outcome = await mushi.submitFeedbackSignal(reportId, signal);
931
+ if (!outcome) {
932
+ setFeedbackError(true);
933
+ return;
934
+ }
935
+ await loadInbox({ silent: true });
936
+ await openThread(reportId, { silent: true });
937
+ } catch {
938
+ setFeedbackError(true);
939
+ } finally {
940
+ setFeedbackSending(false);
941
+ }
942
+ },
943
+ [mushi, selectedReportId, feedbackSending, loadInbox, openThread]
944
+ );
609
945
  const sendAssistant = useCallback(async (message) => {
610
946
  if (!mushi?.askAssistant || !message.trim() || assistantSending) return;
611
947
  const trimmed = message.trim();
@@ -635,8 +971,15 @@ var MushiBottomSheet = ({
635
971
  setCategory(null);
636
972
  setDescription("");
637
973
  setPhase("form");
974
+ setEmailState("hidden");
975
+ setEmailInput("");
638
976
  }, []);
639
977
  const animateIn = useCallback(() => {
978
+ if (reduceMotion) {
979
+ translateY.setValue(0);
980
+ backdropOpacity.setValue(1);
981
+ return;
982
+ }
640
983
  Animated.parallel([
641
984
  Animated.spring(translateY, {
642
985
  toValue: 0,
@@ -650,18 +993,18 @@ var MushiBottomSheet = ({
650
993
  useNativeDriver: true
651
994
  })
652
995
  ]).start();
653
- }, [translateY, backdropOpacity]);
996
+ }, [translateY, backdropOpacity, reduceMotion]);
654
997
  const animateOut = useCallback(
655
998
  (cb) => {
656
999
  Animated.parallel([
657
1000
  Animated.timing(translateY, {
658
1001
  toValue: SHEET_HEIGHT,
659
- duration: 220,
1002
+ duration: reduceMotion ? 0 : 220,
660
1003
  useNativeDriver: true
661
1004
  }),
662
1005
  Animated.timing(backdropOpacity, {
663
1006
  toValue: 0,
664
- duration: 200,
1007
+ duration: reduceMotion ? 0 : 200,
665
1008
  useNativeDriver: true
666
1009
  })
667
1010
  ]).start(() => {
@@ -669,9 +1012,9 @@ var MushiBottomSheet = ({
669
1012
  cb?.();
670
1013
  });
671
1014
  },
672
- [translateY, backdropOpacity, resetForm]
1015
+ [translateY, backdropOpacity, resetForm, reduceMotion]
673
1016
  );
674
- useEffect(() => {
1017
+ useEffect2(() => {
675
1018
  if (visible) animateIn();
676
1019
  }, [visible, animateIn]);
677
1020
  const handleClose = useCallback(() => {
@@ -698,20 +1041,43 @@ var MushiBottomSheet = ({
698
1041
  }
699
1042
  })
700
1043
  ).current;
701
- const handleSubmit = async () => {
702
- if (!category || !description.trim() || !mushi) return;
703
- const trimmed = description.trim();
704
- if (trimmed.length < 20) {
705
- setSubmitError("Please write at least 20 characters so we can understand the issue.");
1044
+ const offerEmail = useCallback(async () => {
1045
+ if (!emailOptIn || !mushi?.getNotificationPrefs) return;
1046
+ try {
1047
+ const prefs = await mushi.getNotificationPrefs();
1048
+ if (prefs?.available.email && !prefs.email_verified && !prefs.email_pending) {
1049
+ setEmailInput(mushi.emailPrefill?.() ?? "");
1050
+ setEmailState("offer");
1051
+ }
1052
+ } catch {
1053
+ }
1054
+ }, [emailOptIn, mushi]);
1055
+ const submitEmail = useCallback(async () => {
1056
+ const email = emailInput.trim();
1057
+ if (!isPlausibleReporterEmail(email)) {
1058
+ setEmailState("invalid");
706
1059
  return;
707
1060
  }
1061
+ if (!mushi?.setNotificationPrefs) return;
1062
+ setEmailState("sending");
1063
+ try {
1064
+ const res = await mushi.setNotificationPrefs({ email });
1065
+ setEmailState(res.ok ? "sent" : res.error?.status === 422 ? "invalid" : "failed");
1066
+ } catch {
1067
+ setEmailState("failed");
1068
+ }
1069
+ }, [emailInput, mushi]);
1070
+ const handleSubmit = async () => {
1071
+ const hasAttachment = Boolean(screenshotDataUrl && screenshotAttached);
1072
+ if (!mushi || !reporterCanSend(description, hasAttachment, minDescriptionLength)) return;
1073
+ const trimmed = description.trim();
708
1074
  setSubmitError(null);
709
1075
  setPhase("sending");
710
1076
  try {
711
1077
  const outcome = await mushi.submitReport({
712
- category,
1078
+ ...reporterChipToReport(category),
713
1079
  description: trimmed,
714
- screenshotDataUrl: screenshotDataUrl && screenshotAttached ? screenshotDataUrl : void 0
1080
+ screenshotDataUrl: hasAttachment ? screenshotDataUrl : void 0
715
1081
  });
716
1082
  if (!outcome.ok) {
717
1083
  if (outcome.failureKind === "credentials" || outcome.failureKind === "quota") {
@@ -729,174 +1095,465 @@ var MushiBottomSheet = ({
729
1095
  return;
730
1096
  }
731
1097
  setPhase("sent");
732
- setTimeout(handleClose, 1400);
1098
+ void offerEmail();
733
1099
  } catch {
734
1100
  setSubmitError("Something went wrong. Please try again.");
735
1101
  setPhase("error");
736
1102
  }
737
1103
  };
738
1104
  const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
739
- const pal = mushiPalette(dark ? "dark" : "light");
740
- const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: MUSHI_BANNER_NEON.bg, accentInk: MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: MUSHI_CONTROL_DISABLED.dark, disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: MUSHI_BANNER_NEON.bg, accentInk: MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: MUSHI_CONTROL_DISABLED.light, disabledText: pal.inkFaint };
1105
+ const colors = resolveRNTheme(dark, theme);
1106
+ const font = colors.fontFamily ? { fontFamily: colors.fontFamily } : null;
1107
+ const radius = colors.radius;
741
1108
  const sheetTabs = ["report", "inbox", ...assistantEnabled ? ["assistant"] : []];
742
- const canSubmit = !!category && description.trim().length >= 20 && (phase === "form" || phase === "error");
743
- return /* @__PURE__ */ jsx(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ jsxs(
1109
+ const canSubmit = reporterCanSend(description, Boolean(activeScreenshot), minDescriptionLength) && (phase === "form" || phase === "error");
1110
+ const submitHint = canSubmit || phase === "sending" ? void 0 : rc.ui.addWords;
1111
+ const selectedReport = selectedReportId ? inboxReports.find((r) => r.id === selectedReportId) ?? null : null;
1112
+ const selectedView = selectedReport ? reporterStatus(selectedReport, locale) : null;
1113
+ const headerTitle = sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title;
1114
+ const renderThread = () => /* @__PURE__ */ jsxs2(Fragment, { children: [
1115
+ /* @__PURE__ */ jsx2(
1116
+ TouchableOpacity2,
1117
+ {
1118
+ onPress: closeThread,
1119
+ accessibilityRole: "button",
1120
+ accessibilityLabel: t.widget.back,
1121
+ hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
1122
+ children: /* @__PURE__ */ jsxs2(Text2, { style: [{ color: colors.accent, marginBottom: 8 }, font], children: [
1123
+ "\u2190 ",
1124
+ t.widget.back
1125
+ ] })
1126
+ }
1127
+ ),
1128
+ selectedReport && selectedView ? /* @__PURE__ */ jsxs2(View2, { style: [s2.headerCard, { backgroundColor: colors.surface, borderRadius: radius, borderColor: colors.border }], children: [
1129
+ /* @__PURE__ */ jsx2(Text2, { style: [s2.pill, { color: colors.fg, borderColor: colors.border }, font], children: selectedView.label }),
1130
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.muted, fontSize: 12, marginTop: 4 }, font], children: selectedView.detail }),
1131
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.fg, marginTop: 8 }, font], numberOfLines: 4, children: selectedReport.title ?? selectedReport.summary ?? selectedReport.description ?? "" })
1132
+ ] }) : null,
1133
+ threadStatus === "loading" ? /* @__PURE__ */ jsx2(View2, { accessibilityLabel: t.flows.reports.loading, accessibilityLiveRegion: "polite", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx2(View2, { style: [s2.skeleton, { backgroundColor: colors.surface, width: `${90 - i * 18}%` }] }, i)) }) : threadStatus === "error" ? /* @__PURE__ */ jsxs2(View2, { style: s2.errorRow, accessibilityLiveRegion: "polite", children: [
1134
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.error, flex: 1 }, font], accessibilityRole: "alert", children: rc.ui.loadError }),
1135
+ /* @__PURE__ */ jsx2(
1136
+ TouchableOpacity2,
1137
+ {
1138
+ onPress: () => selectedReportId && void openThread(selectedReportId),
1139
+ accessibilityRole: "button",
1140
+ style: [s2.retryBtn, { borderColor: colors.border, borderRadius: radius }],
1141
+ children: /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.fg, fontWeight: "600" }, font], children: rc.ui.retry })
1142
+ }
1143
+ )
1144
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
1145
+ threadItems.map(
1146
+ (item, idx) => isReporterConversation(item) ? /* @__PURE__ */ jsxs2(
1147
+ View2,
1148
+ {
1149
+ style: [
1150
+ s2.threadBubble,
1151
+ {
1152
+ backgroundColor: colors.surface,
1153
+ alignSelf: item.kind === "reporter_comment" ? "flex-end" : "flex-start"
1154
+ }
1155
+ ],
1156
+ children: [
1157
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.muted, fontSize: 11 }, font], children: item.kind === "reporter_comment" ? rc.ui.you : rc.ui.developer }),
1158
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.fg }, font], children: reporterTimelineEntryText(item, locale) })
1159
+ ]
1160
+ },
1161
+ `${item.kind}-${item.comment_id ?? idx}`
1162
+ ) : /* @__PURE__ */ jsx2(
1163
+ Text2,
1164
+ {
1165
+ style: [{ color: colors.muted, fontSize: 12, textAlign: "center", marginVertical: 6 }, font],
1166
+ children: reporterTimelineEntryText(item, locale)
1167
+ },
1168
+ `${item.kind}-${item.at}-${idx}`
1169
+ )
1170
+ ),
1171
+ !threadItems.some(isReporterConversation) ? /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.muted, marginBottom: 8 }, font], children: rc.ui.noReplies }) : null
1172
+ ] }),
1173
+ replyState === "sending" ? /* @__PURE__ */ jsxs2(View2, { style: [s2.threadBubble, { backgroundColor: colors.surface, alignSelf: "flex-end", opacity: 0.7 }], children: [
1174
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.muted, fontSize: 11 }, font], children: rc.ui.sending }),
1175
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.fg }, font], children: replyText.trim() })
1176
+ ] }) : null,
1177
+ selectedView?.canVerify ? /* @__PURE__ */ jsxs2(View2, { style: s2.verifyRow, children: [
1178
+ /* @__PURE__ */ jsx2(
1179
+ TouchableOpacity2,
1180
+ {
1181
+ style: [s2.verifyBtn, { backgroundColor: colors.accent, borderRadius: radius }],
1182
+ onPress: () => void submitFeedback("confirms"),
1183
+ disabled: feedbackSending,
1184
+ accessibilityRole: "button",
1185
+ accessibilityState: { disabled: feedbackSending, busy: feedbackSending },
1186
+ children: /* @__PURE__ */ jsx2(Text2, { style: [s2.submitText, { color: colors.accentFg }, font], children: rc.ui.yes })
1187
+ }
1188
+ ),
1189
+ /* @__PURE__ */ jsx2(
1190
+ TouchableOpacity2,
1191
+ {
1192
+ style: [s2.verifyBtn, { backgroundColor: colors.border, borderRadius: radius }],
1193
+ onPress: () => void submitFeedback("not_fixed"),
1194
+ disabled: feedbackSending,
1195
+ accessibilityRole: "button",
1196
+ accessibilityState: { disabled: feedbackSending, busy: feedbackSending },
1197
+ children: /* @__PURE__ */ jsx2(Text2, { style: [s2.submitText, { color: colors.fg }, font], children: rc.ui.notYet })
1198
+ }
1199
+ )
1200
+ ] }) : null,
1201
+ feedbackError ? /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.error, marginBottom: 8 }, font], accessibilityRole: "alert", children: rc.ui.sendFailed }) : null,
1202
+ /* @__PURE__ */ jsxs2(View2, { style: s2.composer, children: [
1203
+ /* @__PURE__ */ jsx2(
1204
+ TextInput,
1205
+ {
1206
+ style: [
1207
+ s2.input,
1208
+ s2.composerInput,
1209
+ { backgroundColor: colors.surface, color: colors.fg, borderColor: colors.border, borderRadius: radius },
1210
+ font
1211
+ ],
1212
+ placeholder: t.flows.thread.replyPlaceholder,
1213
+ placeholderTextColor: colors.muted,
1214
+ value: replyText,
1215
+ onChangeText: (text) => {
1216
+ setReplyText(text.slice(0, 2e3));
1217
+ if (replyState === "failed") setReplyState("idle");
1218
+ },
1219
+ multiline: true,
1220
+ maxLength: 2e3,
1221
+ editable: replyState !== "sending",
1222
+ accessibilityLabel: t.flows.thread.replyPlaceholder
1223
+ }
1224
+ ),
1225
+ /* @__PURE__ */ jsx2(
1226
+ TouchableOpacity2,
1227
+ {
1228
+ style: [
1229
+ s2.sendBtn,
1230
+ {
1231
+ backgroundColor: replyText.trim() && replyState !== "sending" ? colors.accent : colors.disabled,
1232
+ borderRadius: radius
1233
+ }
1234
+ ],
1235
+ onPress: () => void sendReply(),
1236
+ disabled: !replyText.trim() || replyState === "sending",
1237
+ accessibilityRole: "button",
1238
+ accessibilityLabel: replyState === "failed" ? rc.ui.retry : t.flows.thread.send,
1239
+ accessibilityState: { disabled: !replyText.trim() || replyState === "sending", busy: replyState === "sending" },
1240
+ children: /* @__PURE__ */ jsx2(
1241
+ Text2,
1242
+ {
1243
+ style: [
1244
+ s2.submitText,
1245
+ { color: replyText.trim() && replyState !== "sending" ? colors.accentFg : colors.disabledFg },
1246
+ font
1247
+ ],
1248
+ children: replyState === "failed" ? rc.ui.retry : t.flows.thread.send
1249
+ }
1250
+ )
1251
+ }
1252
+ )
1253
+ ] }),
1254
+ replyState === "failed" ? /* @__PURE__ */ jsxs2(Text2, { style: [{ color: colors.error, marginTop: 6, fontSize: 12 }, font], accessibilityRole: "alert", children: [
1255
+ rc.ui.sendFailed,
1256
+ " \xB7 ",
1257
+ rc.ui.retry
1258
+ ] }) : null
1259
+ ] });
1260
+ const renderInboxList = () => {
1261
+ if (inboxLoading && !inboxLoaded) {
1262
+ return /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.muted }, font], children: t.flows.reports.loading });
1263
+ }
1264
+ if (inboxReports.length === 0) {
1265
+ return /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.muted }, font], children: rc.ui.empty });
1266
+ }
1267
+ return inboxReports.map((r) => {
1268
+ const view = reporterStatus(r, locale);
1269
+ const unread = (r.unread_count ?? 0) > 0;
1270
+ const title = (r.title ?? r.summary ?? r.description ?? "Report").slice(0, 80);
1271
+ return /* @__PURE__ */ jsxs2(
1272
+ TouchableOpacity2,
1273
+ {
1274
+ style: [s2.inboxRow, { borderColor: colors.border }],
1275
+ onPress: () => void openThread(r.id),
1276
+ accessibilityRole: "button",
1277
+ accessibilityLabel: `${view.label}. ${title}${unread ? `. ${r.unread_count} new` : ""}`,
1278
+ children: [
1279
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.fg, fontWeight: unread ? "700" : "600" }, font], numberOfLines: 1, children: title }),
1280
+ /* @__PURE__ */ jsxs2(Text2, { style: [{ color: unread ? colors.fg : colors.muted, fontSize: 12, marginTop: 2 }, font], numberOfLines: 1, children: [
1281
+ view.label,
1282
+ view.othersNote ? ` \xB7 ${view.othersNote}` : ""
1283
+ ] }),
1284
+ unread && r.last_event_preview ? /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.fg, fontSize: 12, fontWeight: "700", marginTop: 2 }, font], numberOfLines: 1, children: r.last_event_preview }) : null
1285
+ ]
1286
+ },
1287
+ r.id
1288
+ );
1289
+ });
1290
+ };
1291
+ return /* @__PURE__ */ jsx2(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, onRequestClose: handleClose, children: /* @__PURE__ */ jsxs2(
744
1292
  KeyboardAvoidingView,
745
1293
  {
746
- style: s.flex,
1294
+ style: s2.flex,
747
1295
  behavior: Platform2.OS === "ios" ? "padding" : "height",
748
1296
  keyboardVerticalOffset: Platform2.OS === "ios" ? 0 : 20,
749
1297
  children: [
750
- /* @__PURE__ */ jsx(
1298
+ /* @__PURE__ */ jsx2(
751
1299
  Animated.View,
752
1300
  {
753
- style: [s.backdrop, { backgroundColor: colors.backdrop, opacity: backdropOpacity }],
754
- children: /* @__PURE__ */ jsx(TouchableOpacity, { style: s.flex, activeOpacity: 1, onPress: handleClose })
1301
+ style: [s2.backdrop, { backgroundColor: colors.backdrop, opacity: backdropOpacity }],
1302
+ children: /* @__PURE__ */ jsx2(
1303
+ TouchableOpacity2,
1304
+ {
1305
+ style: s2.flex,
1306
+ activeOpacity: 1,
1307
+ onPress: handleClose,
1308
+ accessibilityRole: "button",
1309
+ accessibilityLabel: t.widget.close
1310
+ }
1311
+ )
755
1312
  }
756
1313
  ),
757
- /* @__PURE__ */ jsxs(
1314
+ /* @__PURE__ */ jsxs2(
758
1315
  Animated.View,
759
1316
  {
1317
+ accessibilityViewIsModal: true,
1318
+ onAccessibilityEscape: handleClose,
760
1319
  style: [
761
- s.sheet,
1320
+ s2.sheet,
762
1321
  { backgroundColor: colors.bg, transform: [{ translateY }] }
763
1322
  ],
764
1323
  children: [
765
- /* @__PURE__ */ jsx(View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ jsx(View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
766
- /* @__PURE__ */ jsxs(View, { style: [s.brandHeader, { backgroundColor: MUSHI_BANNER_NEON.bg, borderBottomColor: MUSHI_BANNER_NEON.border }], children: [
767
- /* @__PURE__ */ jsx(Text, { style: [s.brandEyebrow, { color: MUSHI_BANNER_NEON.fg }], children: "MUSHI \xB7 BETA" }),
768
- /* @__PURE__ */ jsx(Text, { style: [s.brandTitle, { color: MUSHI_BANNER_NEON.fg }], children: sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title })
769
- ] }),
770
- /* @__PURE__ */ jsx(View, { style: [s.tabRow, { borderBottomColor: colors.border }], children: sheetTabs.map((tab) => /* @__PURE__ */ jsx(
771
- TouchableOpacity,
1324
+ /* @__PURE__ */ jsx2(
1325
+ View2,
1326
+ {
1327
+ ...panResponder.panHandlers,
1328
+ style: s2.handleArea,
1329
+ accessible: true,
1330
+ accessibilityRole: "adjustable",
1331
+ accessibilityLabel: t.widget.close,
1332
+ accessibilityActions: [{ name: "decrement", label: t.widget.close }, { name: "escape" }],
1333
+ onAccessibilityAction: (e) => {
1334
+ if (e.nativeEvent.actionName === "decrement" || e.nativeEvent.actionName === "escape") handleClose();
1335
+ },
1336
+ children: /* @__PURE__ */ jsx2(View2, { style: [s2.handle, { backgroundColor: colors.muted }] })
1337
+ }
1338
+ ),
1339
+ /* @__PURE__ */ jsx2(View2, { style: [s2.header, { borderBottomColor: colors.border }], children: /* @__PURE__ */ jsx2(Text2, { accessibilityRole: "header", style: [s2.headerTitle, { color: colors.fg }, font], children: headerTitle }) }),
1340
+ /* @__PURE__ */ jsx2(View2, { style: [s2.tabRow, { borderBottomColor: colors.border }], accessibilityRole: "tablist", children: sheetTabs.map((tab) => /* @__PURE__ */ jsx2(
1341
+ TouchableOpacity2,
772
1342
  {
773
1343
  onPress: () => {
774
1344
  setSheetTab(tab);
775
- if (tab === "inbox") void loadInbox();
776
- else setSelectedReportId(null);
1345
+ if (tab === "inbox") void loadInbox({ silent: inboxLoaded });
1346
+ else closeThread();
777
1347
  },
778
- style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
779
- children: /* @__PURE__ */ jsx(Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.text : colors.sub }], children: tab === "report" ? t.widget.trigger : tab === "inbox" ? t.flows.reports.title : assistantLabel })
1348
+ style: [s2.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
1349
+ accessibilityRole: "tab",
1350
+ accessibilityState: { selected: sheetTab === tab },
1351
+ children: /* @__PURE__ */ jsx2(Text2, { style: [s2.tabLabel, { color: sheetTab === tab ? colors.fg : colors.muted }, font], children: tab === "report" ? t.widget.trigger : tab === "inbox" ? t.flows.reports.title : assistantLabel })
780
1352
  },
781
1353
  tab
782
1354
  )) }),
783
- sheetTab === "assistant" ? /* @__PURE__ */ jsxs(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: [
784
- assistantTurns.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
785
- /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, marginBottom: 12, lineHeight: 20 }, children: greeting }),
786
- assistantSuggestions.map((chip) => /* @__PURE__ */ jsx(
787
- TouchableOpacity,
1355
+ sheetTab === "assistant" ? /* @__PURE__ */ jsxs2(ScrollView, { style: s2.body, keyboardShouldPersistTaps: "handled", children: [
1356
+ assistantTurns.length === 0 ? /* @__PURE__ */ jsxs2(Fragment, { children: [
1357
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.muted, marginBottom: 12, lineHeight: 20 }, font], children: greeting }),
1358
+ assistantSuggestions.map((chip) => /* @__PURE__ */ jsx2(
1359
+ TouchableOpacity2,
788
1360
  {
789
- style: [s.assistantChip, { borderColor: colors.border, backgroundColor: colors.card }],
1361
+ style: [s2.assistantChip, { borderColor: colors.border, backgroundColor: colors.surface }],
790
1362
  onPress: () => void sendAssistant(chip),
791
- children: /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontSize: 13 }, children: chip })
1363
+ accessibilityRole: "button",
1364
+ children: /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.fg, fontSize: 13 }, font], children: chip })
792
1365
  },
793
1366
  chip
794
1367
  ))
795
- ] }) : assistantTurns.map((turn, idx) => /* @__PURE__ */ jsxs(
796
- View,
1368
+ ] }) : assistantTurns.map((turn, idx) => /* @__PURE__ */ jsxs2(
1369
+ View2,
797
1370
  {
798
1371
  style: [
799
- s.assistantBubble,
1372
+ s2.assistantBubble,
800
1373
  {
801
1374
  alignSelf: turn.role === "user" ? "flex-end" : "flex-start",
802
- backgroundColor: turn.role === "user" ? colors.accent : colors.card
1375
+ backgroundColor: turn.role === "user" ? colors.accent : colors.surface
803
1376
  }
804
1377
  ],
805
1378
  children: [
806
- /* @__PURE__ */ jsx(Text, { style: { color: turn.role === "user" ? colors.accentInk : colors.text }, children: turn.text }),
807
- turn.options?.map((opt) => /* @__PURE__ */ jsx(TouchableOpacity, { onPress: () => void sendAssistant(opt), style: { marginTop: 8 }, children: /* @__PURE__ */ jsx(Text, { style: { color: colors.accent, fontSize: 12 }, children: opt }) }, opt))
1379
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: turn.role === "user" ? colors.accentFg : colors.fg }, font], children: turn.text }),
1380
+ turn.options?.map((opt) => /* @__PURE__ */ jsx2(
1381
+ TouchableOpacity2,
1382
+ {
1383
+ onPress: () => void sendAssistant(opt),
1384
+ style: { marginTop: 8 },
1385
+ accessibilityRole: "button",
1386
+ children: /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.accent, fontSize: 12 }, font], children: opt })
1387
+ },
1388
+ opt
1389
+ ))
808
1390
  ]
809
1391
  },
810
1392
  `${turn.role}-${idx}`
811
1393
  )),
812
- assistantSending ? /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, marginTop: 8 }, accessibilityLiveRegion: "polite", children: t.assistant.thinking }) : null,
813
- assistantError ? /* @__PURE__ */ jsx(Text, { style: { color: pal.danger, marginTop: 8 }, accessibilityRole: "alert", children: assistantError }) : null,
814
- /* @__PURE__ */ jsxs(View, { style: s.assistantComposer, children: [
815
- /* @__PURE__ */ jsx(
1394
+ assistantSending ? /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.muted, marginTop: 8 }, font], accessibilityLiveRegion: "polite", children: t.assistant.thinking }) : null,
1395
+ assistantError ? /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.error, marginTop: 8 }, font], accessibilityRole: "alert", children: assistantError }) : null,
1396
+ /* @__PURE__ */ jsxs2(View2, { style: s2.assistantComposer, children: [
1397
+ /* @__PURE__ */ jsx2(
816
1398
  TextInput,
817
1399
  {
818
- style: [s.input, { flex: 1, minHeight: 44, backgroundColor: colors.card, color: colors.text, borderColor: colors.border }],
1400
+ style: [s2.input, { flex: 1, minHeight: 44, backgroundColor: colors.surface, color: colors.fg, borderColor: colors.border, borderRadius: radius }, font],
819
1401
  placeholder: t.assistant.inputPlaceholder,
820
- placeholderTextColor: colors.sub,
1402
+ placeholderTextColor: colors.muted,
821
1403
  value: assistantInput,
822
1404
  onChangeText: setAssistantInput,
823
1405
  editable: !assistantSending
824
1406
  }
825
1407
  ),
826
- /* @__PURE__ */ jsx(
827
- TouchableOpacity,
1408
+ /* @__PURE__ */ jsx2(
1409
+ TouchableOpacity2,
828
1410
  {
829
- style: [s.submitBtn, { backgroundColor: colors.accent, paddingHorizontal: 16 }],
1411
+ style: [s2.submitBtn, { backgroundColor: colors.accent, paddingHorizontal: 16, borderRadius: radius }],
830
1412
  onPress: () => void sendAssistant(assistantInput),
831
1413
  disabled: !assistantInput.trim() || assistantSending,
832
- children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: "\u2191" })
1414
+ accessibilityRole: "button",
1415
+ accessibilityLabel: t.flows.thread.send,
1416
+ accessibilityState: { disabled: !assistantInput.trim() || assistantSending, busy: assistantSending },
1417
+ children: /* @__PURE__ */ jsx2(Text2, { style: [s2.submitText, { color: colors.accentFg }], children: "\u2191" })
833
1418
  }
834
1419
  )
835
1420
  ] })
836
- ] }) : sheetTab === "inbox" ? /* @__PURE__ */ jsx(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: inboxLoading ? /* @__PURE__ */ jsx(Text, { style: { color: colors.sub }, children: t.flows.reports.loading }) : selectedReportId ? /* @__PURE__ */ jsxs(Fragment, { children: [
837
- /* @__PURE__ */ jsx(TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ jsxs(Text, { style: { color: colors.accent, marginBottom: 8 }, children: [
838
- "\u2190 ",
839
- t.widget.back
840
- ] }) }),
841
- threadComments.map((c) => /* @__PURE__ */ jsxs(View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
842
- /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
843
- /* @__PURE__ */ jsx(Text, { style: { color: colors.text }, children: c.body })
844
- ] }, c.id)),
845
- VERIFIABLE_STATUSES.has(
846
- inboxReports.find((r) => r.id === selectedReportId)?.status ?? ""
847
- ) && /* @__PURE__ */ jsxs(View, { style: s.verifyRow, children: [
848
- /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: t.flows.thread.confirmFixed }) }),
849
- /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.border }], onPress: () => submitFeedback("not_fixed"), children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.text }], children: t.flows.thread.notFixed }) })
1421
+ ] }) : sheetTab === "inbox" ? /* @__PURE__ */ jsx2(ScrollView, { style: s2.body, keyboardShouldPersistTaps: "handled", contentContainerStyle: { paddingBottom: 24 }, children: selectedReportId ? renderThread() : renderInboxList() }) : phase === "sent" ? /* @__PURE__ */ jsxs2(ScrollView, { style: s2.body, keyboardShouldPersistTaps: "handled", contentContainerStyle: { paddingBottom: 24 }, children: [
1422
+ /* @__PURE__ */ jsxs2(View2, { style: s2.sentWrap, accessibilityLiveRegion: "polite", children: [
1423
+ /* @__PURE__ */ jsx2(Text2, { style: [s2.sentEmoji], importantForAccessibility: "no", children: "\u2705" }),
1424
+ /* @__PURE__ */ jsx2(Text2, { style: [s2.sentText, { color: colors.fg }, font], children: rc.ui.receipt })
850
1425
  ] }),
851
- /* @__PURE__ */ jsx(
852
- TextInput,
853
- {
854
- style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
855
- placeholder: t.flows.thread.replyPlaceholder,
856
- placeholderTextColor: colors.sub,
857
- value: replyText,
858
- onChangeText: setReplyText
859
- }
860
- ),
861
- /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: t.flows.thread.send }) })
862
- ] }) : inboxReports.map((r) => /* @__PURE__ */ jsxs(TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
863
- /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
864
- /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: reporterStatusShort(r.status) })
865
- ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
866
- /* @__PURE__ */ jsx(Text, { style: [s.sentEmoji], children: "\u2705" }),
867
- /* @__PURE__ */ jsx(Text, { style: [s.sentText, { color: colors.text }], children: t.widget.submitted })
868
- ] }) : /* @__PURE__ */ jsxs(
1426
+ /* @__PURE__ */ jsxs2(View2, { style: s2.verifyRow, children: [
1427
+ /* @__PURE__ */ jsx2(
1428
+ TouchableOpacity2,
1429
+ {
1430
+ style: [s2.verifyBtn, { backgroundColor: colors.accent, borderRadius: radius }],
1431
+ onPress: () => {
1432
+ resetForm();
1433
+ setSheetTab("inbox");
1434
+ void loadInbox();
1435
+ },
1436
+ accessibilityRole: "button",
1437
+ children: /* @__PURE__ */ jsx2(Text2, { style: [s2.submitText, { color: colors.accentFg }, font], children: rc.ui.trackIt })
1438
+ }
1439
+ ),
1440
+ /* @__PURE__ */ jsx2(
1441
+ TouchableOpacity2,
1442
+ {
1443
+ style: [s2.verifyBtn, { backgroundColor: colors.border, borderRadius: radius }],
1444
+ onPress: handleClose,
1445
+ accessibilityRole: "button",
1446
+ children: /* @__PURE__ */ jsx2(Text2, { style: [s2.submitText, { color: colors.fg }, font], children: rc.ui.done })
1447
+ }
1448
+ )
1449
+ ] }),
1450
+ emailState === "sent" ? /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.fg, marginTop: 12 }, font], accessibilityLiveRegion: "polite", children: rc.ui.emailCheckInbox }) : emailState !== "hidden" ? /* @__PURE__ */ jsxs2(View2, { style: { marginTop: 12 }, children: [
1451
+ /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.fg, marginBottom: 6 }, font], children: rc.ui.emailOptIn }),
1452
+ /* @__PURE__ */ jsxs2(View2, { style: s2.composer, children: [
1453
+ /* @__PURE__ */ jsx2(
1454
+ TextInput,
1455
+ {
1456
+ style: [
1457
+ s2.input,
1458
+ s2.composerInput,
1459
+ { backgroundColor: colors.surface, color: colors.fg, borderColor: colors.border, borderRadius: radius },
1460
+ font
1461
+ ],
1462
+ value: emailInput,
1463
+ onChangeText: (v) => {
1464
+ setEmailInput(v.slice(0, 254));
1465
+ if (emailState === "invalid" || emailState === "failed") setEmailState("offer");
1466
+ },
1467
+ placeholder: rc.ui.emailPlaceholder,
1468
+ placeholderTextColor: colors.muted,
1469
+ keyboardType: "email-address",
1470
+ autoCapitalize: "none",
1471
+ autoComplete: "email",
1472
+ textContentType: "emailAddress",
1473
+ editable: emailState !== "sending",
1474
+ accessibilityLabel: rc.ui.emailOptIn
1475
+ }
1476
+ ),
1477
+ /* @__PURE__ */ jsx2(
1478
+ TouchableOpacity2,
1479
+ {
1480
+ style: [s2.sendBtn, { backgroundColor: emailInput.trim() ? colors.accent : colors.disabled, borderRadius: radius }],
1481
+ onPress: () => void submitEmail(),
1482
+ disabled: !emailInput.trim() || emailState === "sending",
1483
+ accessibilityRole: "button",
1484
+ accessibilityState: { disabled: !emailInput.trim() || emailState === "sending", busy: emailState === "sending" },
1485
+ children: /* @__PURE__ */ jsx2(Text2, { style: [s2.submitText, { color: emailInput.trim() ? colors.accentFg : colors.disabledFg }, font], children: emailState === "sending" ? rc.ui.sending : rc.ui.emailSubmit })
1486
+ }
1487
+ )
1488
+ ] }),
1489
+ emailState === "invalid" || emailState === "failed" ? /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.error, marginTop: 6, fontSize: 12 }, font], accessibilityRole: "alert", children: emailState === "invalid" ? rc.ui.emailInvalid : rc.ui.emailFailed }) : null
1490
+ ] }) : null
1491
+ ] }) : /* @__PURE__ */ jsxs2(
869
1492
  ScrollView,
870
1493
  {
871
- style: s.body,
1494
+ style: s2.body,
872
1495
  keyboardShouldPersistTaps: "handled",
873
1496
  contentContainerStyle: { paddingBottom: 24 },
874
1497
  children: [
875
- /* @__PURE__ */ jsx(Text, { style: [s.stepLabel, { color: colors.sub }], children: t.step1.heading }),
876
- /* @__PURE__ */ jsx(View, { style: s.catRow, children: CATEGORY_KEYS.map((key) => {
1498
+ /* @__PURE__ */ jsx2(
1499
+ TextInput,
1500
+ {
1501
+ style: [
1502
+ s2.input,
1503
+ {
1504
+ backgroundColor: colors.surface,
1505
+ color: colors.fg,
1506
+ borderColor: colors.border,
1507
+ borderRadius: radius
1508
+ },
1509
+ font
1510
+ ],
1511
+ placeholder: t.step3.descriptionPlaceholder,
1512
+ placeholderTextColor: colors.muted,
1513
+ accessibilityLabel: t.step3.descriptionPlaceholder,
1514
+ multiline: true,
1515
+ textAlignVertical: "top",
1516
+ value: description,
1517
+ onChangeText: (text) => {
1518
+ setDescription(text.slice(0, 4e3));
1519
+ if (submitError) setSubmitError(null);
1520
+ if (phase === "error") setPhase("form");
1521
+ },
1522
+ editable: phase === "form" || phase === "error",
1523
+ maxLength: 4e3
1524
+ }
1525
+ ),
1526
+ submitError ? /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.error, marginTop: 6, fontSize: 12 }, font], accessibilityRole: "alert", children: submitError }) : null,
1527
+ /* @__PURE__ */ jsx2(View2, { style: [s2.catRow, { marginTop: 12 }], accessibilityRole: "radiogroup", children: REPORTER_CATEGORIES.map((key) => {
877
1528
  const active = category === key;
878
- return /* @__PURE__ */ jsxs(
879
- TouchableOpacity,
1529
+ const label = reporterCategoryLabel(key, locale);
1530
+ return /* @__PURE__ */ jsxs2(
1531
+ TouchableOpacity2,
880
1532
  {
881
- onPress: () => setCategory(key),
1533
+ onPress: () => setCategory(active ? null : key),
882
1534
  activeOpacity: 0.7,
1535
+ accessibilityRole: "radio",
1536
+ accessibilityState: { selected: active, checked: active },
1537
+ accessibilityLabel: label,
883
1538
  style: [
884
- s.catBtn,
1539
+ s2.catBtn,
885
1540
  {
886
- backgroundColor: active ? colors.accent : colors.card,
1541
+ backgroundColor: active ? colors.accent : colors.surface,
887
1542
  borderColor: active ? colors.accent : colors.border
888
1543
  }
889
1544
  ],
890
1545
  children: [
891
- /* @__PURE__ */ jsx(Text, { style: s.catEmoji, children: CATEGORY_EMOJI[key] }),
892
- /* @__PURE__ */ jsx(
893
- Text,
1546
+ /* @__PURE__ */ jsx2(Text2, { style: s2.catEmoji, importantForAccessibility: "no", children: CATEGORY_EMOJI[key] }),
1547
+ /* @__PURE__ */ jsx2(
1548
+ Text2,
894
1549
  {
1550
+ numberOfLines: 1,
895
1551
  style: [
896
- s.catLabel,
897
- { color: active ? colors.accentInk : colors.text }
1552
+ s2.catLabel,
1553
+ { color: active ? colors.accentFg : colors.fg },
1554
+ font
898
1555
  ],
899
- children: t.step1.categories[key]
1556
+ children: label
900
1557
  }
901
1558
  )
902
1559
  ]
@@ -904,69 +1561,49 @@ var MushiBottomSheet = ({
904
1561
  key
905
1562
  );
906
1563
  }) }),
907
- /* @__PURE__ */ jsx(
908
- TextInput,
909
- {
910
- style: [
911
- s.input,
912
- {
913
- backgroundColor: colors.card,
914
- color: colors.text,
915
- borderColor: colors.border
916
- }
917
- ],
918
- placeholder: t.step3.descriptionPlaceholder,
919
- placeholderTextColor: colors.sub,
920
- multiline: true,
921
- textAlignVertical: "top",
922
- value: description,
923
- onChangeText: (text) => {
924
- setDescription(text.slice(0, 4e3));
925
- if (submitError) setSubmitError(null);
926
- if (phase === "error") setPhase("form");
927
- },
928
- editable: phase === "form" || phase === "error",
929
- maxLength: 4e3
930
- }
931
- ),
932
- submitError ? /* @__PURE__ */ jsx(Text, { style: { color: colors.accent, marginTop: 6, fontSize: 12 }, children: submitError }) : null,
933
- activeScreenshot ? /* @__PURE__ */ jsxs(View, { style: s.screenshotRow, children: [
934
- /* @__PURE__ */ jsx(
1564
+ activeScreenshot ? /* @__PURE__ */ jsxs2(View2, { style: s2.screenshotRow, children: [
1565
+ /* @__PURE__ */ jsx2(
935
1566
  Image,
936
1567
  {
937
1568
  source: { uri: activeScreenshot },
938
- style: [s.screenshotThumb, { backgroundColor: colors.text }],
1569
+ style: [s2.screenshotThumb, { backgroundColor: colors.fg }],
939
1570
  accessibilityLabel: t.step3.screenshotPreviewAlt
940
1571
  }
941
1572
  ),
942
- /* @__PURE__ */ jsxs(View, { style: s.screenshotMeta, children: [
943
- /* @__PURE__ */ jsx(Text, { style: [s.screenshotLabel, { color: colors.text }], children: t.step3.screenshotAttached.replace(" \u2713", "") }),
944
- /* @__PURE__ */ jsx(Text, { style: [s.screenshotSub, { color: colors.sub }], children: screenshotSensitiveHint && screenshotSensitiveHint.trim() ? `\u26A0 ${screenshotSensitiveHint}` : t.step3.screenshotSensitiveHint.split("\u2014")[0]?.trim() ?? t.step3.screenshotSensitiveHint })
1573
+ /* @__PURE__ */ jsxs2(View2, { style: s2.screenshotMeta, children: [
1574
+ /* @__PURE__ */ jsx2(Text2, { style: [s2.screenshotLabel, { color: colors.fg }, font], children: t.step3.screenshotAttached.replace(" \u2713", "") }),
1575
+ /* @__PURE__ */ jsx2(Text2, { style: [s2.screenshotSub, { color: colors.muted }, font], children: screenshotSensitiveHint && screenshotSensitiveHint.trim() ? `\u26A0 ${screenshotSensitiveHint}` : t.step3.screenshotSensitiveHint.split("\u2014")[0]?.trim() ?? t.step3.screenshotSensitiveHint })
945
1576
  ] }),
946
- /* @__PURE__ */ jsx(
947
- TouchableOpacity,
1577
+ /* @__PURE__ */ jsx2(
1578
+ TouchableOpacity2,
948
1579
  {
949
1580
  onPress: () => {
950
1581
  setScreenshotAttached(false);
951
1582
  onClearScreenshot?.();
952
1583
  },
953
1584
  hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
1585
+ accessibilityRole: "button",
954
1586
  accessibilityLabel: t.widget.close,
955
- children: /* @__PURE__ */ jsx(Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
1587
+ children: /* @__PURE__ */ jsx2(Text2, { style: [s2.screenshotRemove, { color: colors.muted }], children: "\u2715" })
956
1588
  }
957
1589
  )
958
1590
  ] }) : null,
959
- /* @__PURE__ */ jsx(
960
- TouchableOpacity,
1591
+ submitHint ? /* @__PURE__ */ jsx2(Text2, { style: [{ color: colors.muted, fontSize: 12, marginBottom: 6 }, font], children: submitHint }) : null,
1592
+ /* @__PURE__ */ jsx2(
1593
+ TouchableOpacity2,
961
1594
  {
962
1595
  onPress: handleSubmit,
963
1596
  disabled: !canSubmit,
964
1597
  activeOpacity: 0.8,
1598
+ accessibilityRole: "button",
1599
+ accessibilityLabel: phase === "sending" ? t.widget.submitting : t.widget.submit,
1600
+ accessibilityHint: submitHint,
1601
+ accessibilityState: { disabled: !canSubmit, busy: phase === "sending" },
965
1602
  style: [
966
- s.submitBtn,
967
- { backgroundColor: canSubmit ? colors.accent : colors.disabled }
1603
+ s2.submitBtn,
1604
+ { backgroundColor: canSubmit ? colors.accent : colors.disabled, borderRadius: radius }
968
1605
  ],
969
- children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: canSubmit ? colors.accentInk : colors.disabledText }], children: phase === "sending" ? t.widget.submitting : t.widget.submit })
1606
+ children: /* @__PURE__ */ jsx2(Text2, { style: [s2.submitText, { color: canSubmit ? colors.accentFg : colors.disabledFg }, font], children: phase === "sending" ? t.widget.submitting : t.widget.submit })
970
1607
  }
971
1608
  )
972
1609
  ]
@@ -979,7 +1616,7 @@ var MushiBottomSheet = ({
979
1616
  }
980
1617
  ) });
981
1618
  };
982
- var s = StyleSheet.create({
1619
+ var s2 = StyleSheet2.create({
983
1620
  flex: { flex: 1 },
984
1621
  backdrop: {
985
1622
  // Inlined StyleSheet.absoluteFillObject — RN 0.86 dropped it from the
@@ -996,10 +1633,10 @@ var s = StyleSheet.create({
996
1633
  left: 0,
997
1634
  right: 0,
998
1635
  minHeight: SHEET_HEIGHT,
999
- borderTopLeftRadius: MUSHI_RADIUS.sheet,
1000
- borderTopRightRadius: MUSHI_RADIUS.sheet,
1636
+ borderTopLeftRadius: MUSHI_RADIUS2.sheet,
1637
+ borderTopRightRadius: MUSHI_RADIUS2.sheet,
1001
1638
  paddingBottom: Platform2.OS === "ios" ? 34 : MUSHI_SPACING.roomy,
1002
- shadowColor: MUSHI_SHADOW_INK,
1639
+ shadowColor: MUSHI_SHADOW_INK2,
1003
1640
  shadowOffset: { width: 0, height: -3 },
1004
1641
  shadowOpacity: 0.12,
1005
1642
  shadowRadius: 8,
@@ -1012,36 +1649,22 @@ var s = StyleSheet.create({
1012
1649
  handle: {
1013
1650
  width: 40,
1014
1651
  height: 5,
1015
- borderRadius: MUSHI_RADIUS.control,
1652
+ borderRadius: MUSHI_RADIUS2.control,
1016
1653
  opacity: 0.5
1017
1654
  },
1018
- brandHeader: {
1655
+ header: {
1019
1656
  paddingHorizontal: MUSHI_SPACING.lounge,
1020
- paddingTop: MUSHI_SPACING.comfy,
1021
- paddingBottom: MUSHI_SPACING.comfy,
1022
- borderBottomWidth: 1.5
1657
+ paddingBottom: MUSHI_SPACING.comfy
1023
1658
  },
1024
- brandEyebrow: {
1025
- fontSize: MUSHI_TYPE.sizeLabel,
1026
- fontWeight: "700",
1027
- letterSpacing: 1.2,
1028
- marginBottom: 2,
1029
- opacity: 0.7
1030
- },
1031
- brandTitle: {
1659
+ headerTitle: {
1032
1660
  fontSize: 18,
1033
- fontWeight: "800",
1661
+ fontWeight: "700",
1034
1662
  letterSpacing: -0.2
1035
1663
  },
1036
1664
  body: {
1037
1665
  paddingHorizontal: MUSHI_SPACING.lounge,
1038
1666
  paddingTop: MUSHI_SPACING.tight
1039
1667
  },
1040
- title: {
1041
- fontSize: 20,
1042
- fontWeight: "700",
1043
- marginBottom: MUSHI_SPACING.roomy
1044
- },
1045
1668
  stepLabel: {
1046
1669
  fontSize: MUSHI_TYPE.sizeLabel,
1047
1670
  fontWeight: "600",
@@ -1052,37 +1675,35 @@ var s = StyleSheet.create({
1052
1675
  },
1053
1676
  catRow: {
1054
1677
  flexDirection: "row",
1055
- justifyContent: "space-between",
1678
+ flexWrap: "wrap",
1679
+ gap: 8,
1056
1680
  marginBottom: MUSHI_SPACING.roomy
1057
1681
  },
1058
1682
  catBtn: {
1683
+ flexDirection: "row",
1059
1684
  alignItems: "center",
1060
- paddingVertical: MUSHI_SPACING.comfy,
1061
- paddingHorizontal: 6,
1062
- borderRadius: MUSHI_RADIUS.card,
1685
+ gap: 6,
1686
+ paddingVertical: 8,
1687
+ paddingHorizontal: 12,
1688
+ borderRadius: 999,
1063
1689
  borderWidth: 1,
1064
- flex: 1,
1065
- marginHorizontal: 3,
1066
1690
  minHeight: 44
1067
1691
  },
1068
1692
  catEmoji: {
1069
- fontSize: 22,
1070
- marginBottom: MUSHI_SPACING.tight
1693
+ fontSize: 16
1071
1694
  },
1072
1695
  catLabel: {
1073
- fontSize: MUSHI_TYPE.sizeLabel,
1696
+ fontSize: 14,
1074
1697
  fontWeight: "600"
1075
1698
  },
1076
1699
  input: {
1077
1700
  borderWidth: 1,
1078
- borderRadius: MUSHI_RADIUS.card,
1079
1701
  padding: 14,
1080
1702
  fontSize: 15,
1081
1703
  minHeight: 100,
1082
1704
  marginBottom: MUSHI_SPACING.roomy
1083
1705
  },
1084
1706
  submitBtn: {
1085
- borderRadius: MUSHI_RADIUS.card,
1086
1707
  paddingVertical: 14,
1087
1708
  alignItems: "center",
1088
1709
  minHeight: 44
@@ -1139,7 +1760,7 @@ var s = StyleSheet.create({
1139
1760
  },
1140
1761
  tabRow: {
1141
1762
  flexDirection: "row",
1142
- borderBottomWidth: StyleSheet.hairlineWidth,
1763
+ borderBottomWidth: StyleSheet2.hairlineWidth,
1143
1764
  marginBottom: 8
1144
1765
  },
1145
1766
  tabBtn: {
@@ -1147,7 +1768,8 @@ var s = StyleSheet.create({
1147
1768
  alignItems: "center",
1148
1769
  paddingVertical: 10,
1149
1770
  borderBottomWidth: 2,
1150
- borderBottomColor: "transparent"
1771
+ borderBottomColor: "transparent",
1772
+ minHeight: 44
1151
1773
  },
1152
1774
  tabLabel: {
1153
1775
  fontSize: 14,
@@ -1155,12 +1777,47 @@ var s = StyleSheet.create({
1155
1777
  },
1156
1778
  inboxRow: {
1157
1779
  paddingVertical: 12,
1158
- borderBottomWidth: StyleSheet.hairlineWidth
1780
+ minHeight: 56,
1781
+ borderBottomWidth: StyleSheet2.hairlineWidth
1782
+ },
1783
+ headerCard: {
1784
+ borderWidth: StyleSheet2.hairlineWidth,
1785
+ padding: 12,
1786
+ marginBottom: 12
1787
+ },
1788
+ pill: {
1789
+ alignSelf: "flex-start",
1790
+ borderWidth: 1,
1791
+ borderRadius: 999,
1792
+ paddingHorizontal: 8,
1793
+ paddingVertical: 2,
1794
+ fontSize: 12,
1795
+ fontWeight: "600",
1796
+ overflow: "hidden"
1797
+ },
1798
+ skeleton: {
1799
+ height: 14,
1800
+ borderRadius: 6,
1801
+ marginBottom: 10
1802
+ },
1803
+ errorRow: {
1804
+ flexDirection: "row",
1805
+ alignItems: "center",
1806
+ gap: 8,
1807
+ marginBottom: 12
1808
+ },
1809
+ retryBtn: {
1810
+ borderWidth: 1,
1811
+ paddingHorizontal: 12,
1812
+ paddingVertical: 8,
1813
+ minHeight: 44,
1814
+ justifyContent: "center"
1159
1815
  },
1160
1816
  threadBubble: {
1161
1817
  borderRadius: 10,
1162
1818
  padding: 10,
1163
- marginBottom: 8
1819
+ marginBottom: 8,
1820
+ maxWidth: "88%"
1164
1821
  },
1165
1822
  verifyRow: {
1166
1823
  flexDirection: "row",
@@ -1169,9 +1826,26 @@ var s = StyleSheet.create({
1169
1826
  },
1170
1827
  verifyBtn: {
1171
1828
  flex: 1,
1172
- borderRadius: 10,
1173
1829
  paddingVertical: 10,
1174
- alignItems: "center"
1830
+ alignItems: "center",
1831
+ minHeight: 44
1832
+ },
1833
+ composer: {
1834
+ flexDirection: "row",
1835
+ alignItems: "flex-end",
1836
+ gap: 8
1837
+ },
1838
+ composerInput: {
1839
+ flex: 1,
1840
+ minHeight: 44,
1841
+ maxHeight: 120,
1842
+ marginBottom: 0
1843
+ },
1844
+ sendBtn: {
1845
+ paddingHorizontal: 16,
1846
+ paddingVertical: 12,
1847
+ minHeight: 44,
1848
+ justifyContent: "center"
1175
1849
  },
1176
1850
  assistantBubble: {
1177
1851
  maxWidth: "88%",
@@ -1197,17 +1871,17 @@ var s = StyleSheet.create({
1197
1871
  });
1198
1872
 
1199
1873
  // src/components/MushiFloatingButton.tsx
1200
- import { useRef as useRef2, useCallback as useCallback2, useEffect as useEffect2 } from "react";
1874
+ import { useRef as useRef2, useCallback as useCallback2, useEffect as useEffect3 } from "react";
1201
1875
  import {
1202
1876
  Animated as Animated2,
1203
1877
  PanResponder as PanResponder2,
1204
- StyleSheet as StyleSheet2,
1878
+ StyleSheet as StyleSheet3,
1205
1879
  Platform as Platform3,
1206
1880
  Dimensions as Dimensions3,
1207
- useColorScheme as useColorScheme2
1881
+ useColorScheme as useColorScheme3
1208
1882
  } from "react-native";
1209
1883
  import { mushiPalette as mushiPalette2, MUSHI_COPY as MUSHI_COPY2, MUSHI_GEOMETRY } from "@mushi-mushi/core";
1210
- import { jsx as jsx2 } from "react/jsx-runtime";
1884
+ import { jsx as jsx3 } from "react/jsx-runtime";
1211
1885
  var FAB_SIZE = MUSHI_GEOMETRY.fabSize;
1212
1886
  var EDGE_PADDING = 12;
1213
1887
  function getScreenBounds() {
@@ -1242,7 +1916,7 @@ var MushiFloatingButton = ({
1242
1916
  config,
1243
1917
  accent
1244
1918
  }) => {
1245
- const scheme = useColorScheme2();
1919
+ const scheme = useColorScheme3();
1246
1920
  const dark = scheme === "dark";
1247
1921
  const draggable = config?.draggable ?? false;
1248
1922
  const snapToEdge = config?.snapToEdge ?? true;
@@ -1260,7 +1934,7 @@ var MushiFloatingButton = ({
1260
1934
  const isDragging = useRef2(false);
1261
1935
  const pal = mushiPalette2(dark ? "dark" : "light");
1262
1936
  const accentColor = accent ?? pal.accent;
1263
- useEffect2(() => {
1937
+ useEffect3(() => {
1264
1938
  if (!persist) return;
1265
1939
  void loadStoredPos().then((pos) => {
1266
1940
  if (!pos) return;
@@ -1352,12 +2026,12 @@ var MushiFloatingButton = ({
1352
2026
  }).start();
1353
2027
  if (!isDragging.current) onPress();
1354
2028
  }, [scale, onPress]);
1355
- return /* @__PURE__ */ jsx2(
2029
+ return /* @__PURE__ */ jsx3(
1356
2030
  Animated2.View,
1357
2031
  {
1358
2032
  ...draggable ? panResponder.panHandlers : {},
1359
2033
  style: [
1360
- s2.fab,
2034
+ s3.fab,
1361
2035
  {
1362
2036
  left: posX,
1363
2037
  bottom: Animated2.add(new Animated2.Value(baseBottom), Animated2.multiply(posY, -1)),
@@ -1369,11 +2043,11 @@ var MushiFloatingButton = ({
1369
2043
  onStartShouldSetResponder: () => !draggable,
1370
2044
  onResponderGrant: handlePressIn,
1371
2045
  onResponderRelease: handlePressOut,
1372
- children: /* @__PURE__ */ jsx2(Animated2.Text, { style: s2.emoji, children: MUSHI_COPY2.triggerText })
2046
+ children: /* @__PURE__ */ jsx3(Animated2.Text, { style: s3.emoji, children: MUSHI_COPY2.triggerText })
1373
2047
  }
1374
2048
  );
1375
2049
  };
1376
- var s2 = StyleSheet2.create({
2050
+ var s3 = StyleSheet3.create({
1377
2051
  fab: {
1378
2052
  position: "absolute",
1379
2053
  width: FAB_SIZE,
@@ -1395,14 +2069,14 @@ var s2 = StyleSheet2.create({
1395
2069
  // src/components/MushiBanner.tsx
1396
2070
  import { useCallback as useCallback3 } from "react";
1397
2071
  import {
1398
- View as View2,
1399
- Text as Text2,
1400
- TouchableOpacity as TouchableOpacity2,
1401
- StyleSheet as StyleSheet3,
2072
+ View as View3,
2073
+ Text as Text3,
2074
+ TouchableOpacity as TouchableOpacity3,
2075
+ StyleSheet as StyleSheet4,
1402
2076
  Platform as Platform4
1403
2077
  } from "react-native";
1404
- import { MUSHI_BANNER_NEON as MUSHI_BANNER_NEON2, MUSHI_COPY as MUSHI_COPY3, MUSHI_GEOMETRY as MUSHI_GEOMETRY2, MUSHI_SPACING as MUSHI_SPACING2, MUSHI_TYPE as MUSHI_TYPE2, MUSHI_Z } from "@mushi-mushi/core";
1405
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2078
+ import { MUSHI_BANNER_NEON, MUSHI_COPY as MUSHI_COPY3, MUSHI_GEOMETRY as MUSHI_GEOMETRY2, MUSHI_SPACING as MUSHI_SPACING2, MUSHI_TYPE as MUSHI_TYPE2, MUSHI_Z } from "@mushi-mushi/core";
2079
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1406
2080
  var MUSHI_BANNER_DEFAULT_HEIGHT = MUSHI_GEOMETRY2.bannerHeight;
1407
2081
  var MushiBanner = ({
1408
2082
  onPress,
@@ -1417,35 +2091,35 @@ var MushiBanner = ({
1417
2091
  },
1418
2092
  [onLayoutHeight]
1419
2093
  );
1420
- return /* @__PURE__ */ jsxs2(
1421
- View2,
2094
+ return /* @__PURE__ */ jsxs3(
2095
+ View3,
1422
2096
  {
1423
- style: s3.wrap,
2097
+ style: s4.wrap,
1424
2098
  onLayout: handleLayout,
1425
2099
  accessibilityRole: "header",
1426
2100
  accessibilityLabel: "Mushi feedback banner",
1427
2101
  children: [
1428
- /* @__PURE__ */ jsxs2(TouchableOpacity2, { style: s3.main, onPress, activeOpacity: 0.85, children: [
1429
- /* @__PURE__ */ jsx3(Text2, { style: s3.eyebrow, children: label.toUpperCase() }),
1430
- /* @__PURE__ */ jsx3(Text2, { style: s3.message, numberOfLines: 2, children: message }),
1431
- /* @__PURE__ */ jsx3(Text2, { style: s3.cta, children: MUSHI_COPY3.bugCta })
2102
+ /* @__PURE__ */ jsxs3(TouchableOpacity3, { style: s4.main, onPress, activeOpacity: 0.85, children: [
2103
+ /* @__PURE__ */ jsx4(Text3, { style: s4.eyebrow, children: label.toUpperCase() }),
2104
+ /* @__PURE__ */ jsx4(Text3, { style: s4.message, numberOfLines: 2, children: message }),
2105
+ /* @__PURE__ */ jsx4(Text3, { style: s4.cta, children: MUSHI_COPY3.bugCta })
1432
2106
  ] }),
1433
- onDismiss ? /* @__PURE__ */ jsx3(
1434
- TouchableOpacity2,
2107
+ onDismiss ? /* @__PURE__ */ jsx4(
2108
+ TouchableOpacity3,
1435
2109
  {
1436
- style: s3.dismiss,
2110
+ style: s4.dismiss,
1437
2111
  onPress: onDismiss,
1438
2112
  accessibilityRole: "button",
1439
2113
  accessibilityLabel: "Dismiss banner",
1440
2114
  hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
1441
- children: /* @__PURE__ */ jsx3(Text2, { style: s3.dismissText, children: "\xD7" })
2115
+ children: /* @__PURE__ */ jsx4(Text3, { style: s4.dismissText, children: "\xD7" })
1442
2116
  }
1443
2117
  ) : null
1444
2118
  ]
1445
2119
  }
1446
2120
  );
1447
2121
  };
1448
- var s3 = StyleSheet3.create({
2122
+ var s4 = StyleSheet4.create({
1449
2123
  wrap: {
1450
2124
  position: "absolute",
1451
2125
  left: 0,
@@ -1454,9 +2128,9 @@ var s3 = StyleSheet3.create({
1454
2128
  minHeight: MUSHI_GEOMETRY2.bannerHeight,
1455
2129
  flexDirection: "row",
1456
2130
  alignItems: "stretch",
1457
- backgroundColor: MUSHI_BANNER_NEON2.bg,
2131
+ backgroundColor: MUSHI_BANNER_NEON.bg,
1458
2132
  borderTopWidth: 1,
1459
- borderTopColor: MUSHI_BANNER_NEON2.border,
2133
+ borderTopColor: MUSHI_BANNER_NEON.border,
1460
2134
  zIndex: MUSHI_Z.banner,
1461
2135
  elevation: 8,
1462
2136
  paddingBottom: Platform4.OS === "ios" ? 0 : 0
@@ -1474,18 +2148,18 @@ var s3 = StyleSheet3.create({
1474
2148
  fontSize: MUSHI_TYPE2.sizeLabel,
1475
2149
  fontWeight: "700",
1476
2150
  letterSpacing: 1.2,
1477
- color: MUSHI_BANNER_NEON2.fg
2151
+ color: MUSHI_BANNER_NEON.fg
1478
2152
  },
1479
2153
  message: {
1480
2154
  flex: 1,
1481
2155
  fontSize: 12,
1482
2156
  fontWeight: "500",
1483
- color: MUSHI_BANNER_NEON2.fg
2157
+ color: MUSHI_BANNER_NEON.fg
1484
2158
  },
1485
2159
  cta: {
1486
2160
  fontSize: 12,
1487
2161
  fontWeight: "700",
1488
- color: MUSHI_BANNER_NEON2.fg,
2162
+ color: MUSHI_BANNER_NEON.fg,
1489
2163
  textDecorationLine: "underline"
1490
2164
  },
1491
2165
  dismiss: {
@@ -1495,7 +2169,7 @@ var s3 = StyleSheet3.create({
1495
2169
  dismissText: {
1496
2170
  fontSize: 20,
1497
2171
  lineHeight: 22,
1498
- color: MUSHI_BANNER_NEON2.fg,
2172
+ color: MUSHI_BANNER_NEON.fg,
1499
2173
  opacity: 0.7
1500
2174
  }
1501
2175
  });
@@ -1825,10 +2499,10 @@ function createRNSessionTracker(opts) {
1825
2499
 
1826
2500
  // src/version.ts
1827
2501
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1828
- var MUSHI_SDK_VERSION = "0.22.0" ? "0.22.0" : "0.17.0";
2502
+ var MUSHI_SDK_VERSION = "0.24.0" ? "0.24.0" : "0.17.0";
1829
2503
 
1830
2504
  // src/provider.tsx
1831
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2505
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1832
2506
  var MushiContext = createContext(null);
1833
2507
  function nativeUserAgent() {
1834
2508
  const os = Platform5?.OS ?? "native";
@@ -1905,7 +2579,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1905
2579
  console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
1906
2580
  }
1907
2581
  const apiEndpoint = rawEndpoint ?? DEFAULT_API_ENDPOINT;
1908
- useEffect3(() => {
2582
+ useEffect4(() => {
1909
2583
  if (config.capture?.console !== false) {
1910
2584
  consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
1911
2585
  }
@@ -1967,11 +2641,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1967
2641
  networkRef.current?.restore();
1968
2642
  };
1969
2643
  }, []);
1970
- useEffect3(() => {
2644
+ useEffect4(() => {
1971
2645
  let unsubscribe;
1972
2646
  try {
1973
- const NetInfo = __require("@react-native-community/netinfo").default;
1974
- unsubscribe = NetInfo.addEventListener((state) => {
2647
+ unsubscribe = resolveNetInfo(config.netInfo)?.addEventListener((state) => {
1975
2648
  if (state.isConnected && state.isInternetReachable) {
1976
2649
  queueRef.current?.flush().catch(() => {
1977
2650
  });
@@ -1980,7 +2653,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1980
2653
  } catch {
1981
2654
  }
1982
2655
  return () => unsubscribe?.();
1983
- }, []);
2656
+ }, [config.netInfo]);
1984
2657
  const open = useCallback4(() => {
1985
2658
  setSheetPreferredTab("report");
1986
2659
  breadcrumbsRef.current.add({
@@ -1994,11 +2667,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1994
2667
  setSheetVisible(true);
1995
2668
  return;
1996
2669
  }
1997
- let vshot = null;
1998
- try {
1999
- vshot = __require("react-native-view-shot");
2000
- } catch {
2001
- }
2670
+ const vshot = resolveViewShot(config.viewShot);
2002
2671
  if (!vshot) {
2003
2672
  setSheetScreenshot(null);
2004
2673
  setSheetVisible(true);
@@ -2011,7 +2680,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2011
2680
  }).finally(() => {
2012
2681
  setSheetVisible(true);
2013
2682
  });
2014
- }, [config.capture?.screenshot]);
2683
+ }, [config.capture?.screenshot, config.viewShot]);
2015
2684
  const openAssistant = useCallback4(() => {
2016
2685
  if (!config.assistant?.enabled) return;
2017
2686
  setSheetPreferredTab("assistant");
@@ -2020,7 +2689,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2020
2689
  }, [config.assistant?.enabled]);
2021
2690
  const close = useCallback4(() => setSheetVisible(false), []);
2022
2691
  const attachTo = useCallback4(() => ({ onPress: open }), [open]);
2023
- useEffect3(() => {
2692
+ useEffect4(() => {
2024
2693
  const trigger2 = config.widget?.trigger ?? "button";
2025
2694
  if (trigger2 !== "shake" && trigger2 !== "both") return;
2026
2695
  let disposed = false;
@@ -2028,8 +2697,8 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2028
2697
  let lastShake = 0;
2029
2698
  const threshold = config.widget?.shakeThreshold ?? 2.7;
2030
2699
  try {
2031
- const mod = __require("expo-sensors");
2032
- if (!disposed) {
2700
+ const mod = resolveExpoSensors(config.expoSensors);
2701
+ if (mod && !disposed) {
2033
2702
  mod.Accelerometer.setUpdateInterval(120);
2034
2703
  const sub = mod.Accelerometer.addListener((evt) => {
2035
2704
  const g = Math.sqrt(evt.x * evt.x + evt.y * evt.y + evt.z * evt.z);
@@ -2047,7 +2716,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2047
2716
  disposed = true;
2048
2717
  cleanup?.();
2049
2718
  };
2050
- }, [config.widget?.trigger, config.widget?.shakeThreshold, open]);
2719
+ }, [config.widget?.trigger, config.widget?.shakeThreshold, config.expoSensors, open]);
2051
2720
  const submitReport = useCallback4(
2052
2721
  async (data) => {
2053
2722
  await reporterTokenReadyRef.current;
@@ -2088,6 +2757,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2088
2757
  id: newUuid2(),
2089
2758
  projectId: config.projectId,
2090
2759
  category: data.category,
2760
+ ...data.userCategory ? { userCategory: data.userCategory } : {},
2091
2761
  description: scrubPii3(data.description),
2092
2762
  environment: {
2093
2763
  userAgent: deviceInfo.systemName ?? "ReactNative",
@@ -2147,6 +2817,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2147
2817
  };
2148
2818
  } else {
2149
2819
  config.callbacks?.onReportSubmitted?.(report.id);
2820
+ void writeReporterFlag(config.projectId, "has_reports", "1");
2150
2821
  return { ok: true };
2151
2822
  }
2152
2823
  },
@@ -2157,7 +2828,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2157
2828
  const screenRef = useRef3(null);
2158
2829
  const activityQueueRef = useRef3([]);
2159
2830
  const rewardsFlushRef = useRef3(null);
2160
- useEffect3(() => {
2831
+ useEffect4(() => {
2161
2832
  const rewards = config.rewards;
2162
2833
  if (!rewards?.enabled || !rewards.trackActivity) return;
2163
2834
  const flushMs = Math.max(3e4, rewards.flushIntervalMs ?? 3e5);
@@ -2179,7 +2850,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2179
2850
  }, [config.rewards?.enabled, config.rewards?.trackActivity, config.rewards?.flushIntervalMs]);
2180
2851
  const eventTrackerRef = useRef3(null);
2181
2852
  const sessionTrackerRef = useRef3(null);
2182
- useEffect3(() => {
2853
+ useEffect4(() => {
2183
2854
  const getRoute = () => screenRef.current?.route ?? screenRef.current?.name ?? null;
2184
2855
  const tracker = createRNEventTracker({
2185
2856
  projectId: config.projectId,
@@ -2234,6 +2905,70 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2234
2905
  config.analytics?.flushIntervalMs,
2235
2906
  config.trackSessions
2236
2907
  ]);
2908
+ const reporterUpdatesRef = useRef3(
2909
+ createReporterUpdateHub(async () => {
2910
+ const client = apiClientRef.current;
2911
+ if (!client) return null;
2912
+ await reporterTokenReadyRef.current;
2913
+ const res = await reporterChannels2(client).getUpdates(reporterTokenRef.current);
2914
+ return res.ok ? res.data ?? null : null;
2915
+ })
2916
+ );
2917
+ useEffect4(() => {
2918
+ let sub;
2919
+ try {
2920
+ if (typeof AppState?.addEventListener === "function") {
2921
+ sub = AppState.addEventListener("change", (state) => {
2922
+ if (state === "active" && reporterUpdatesRef.current.listenerCount() > 0) void reporterUpdatesRef.current.emit();
2923
+ });
2924
+ }
2925
+ } catch {
2926
+ }
2927
+ return () => sub?.remove();
2928
+ }, []);
2929
+ const [toast, setToast] = useState2(null);
2930
+ const toastShownRef = useRef3(false);
2931
+ const toastEnabled = config.notifications?.toast !== false;
2932
+ const maybeShowToast = useCallback4(async () => {
2933
+ if (!toastEnabled || toastShownRef.current) return;
2934
+ if (await readReporterFlag(config.projectId, "has_reports") !== "1") return;
2935
+ const updates = await reporterUpdatesRef.current.emit();
2936
+ if (!updates) return;
2937
+ const now = Date.now();
2938
+ const show = reporterShouldShowToast({
2939
+ enabled: toastEnabled,
2940
+ unreadTotal: updates.unread_total,
2941
+ shownThisSession: toastShownRef.current,
2942
+ lastShownAt: Number(await readReporterFlag(config.projectId, "toast_shown_at")) || null,
2943
+ now
2944
+ });
2945
+ if (!show) return;
2946
+ toastShownRef.current = true;
2947
+ void writeReporterFlag(config.projectId, "toast_shown_at", String(now));
2948
+ const locale = resolveReporterLocale3(
2949
+ typeof navigator !== "undefined" ? navigator.language : void 0
2950
+ );
2951
+ setToast({ message: reporterToastMessage(updates, locale), reportId: updates.latest[0]?.report_id ?? null });
2952
+ }, [toastEnabled, config.projectId]);
2953
+ useEffect4(() => {
2954
+ void reporterTokenReadyRef.current.then(() => maybeShowToast());
2955
+ let sub;
2956
+ try {
2957
+ if (typeof AppState?.addEventListener === "function") {
2958
+ sub = AppState.addEventListener("change", (state) => {
2959
+ if (state === "active") void maybeShowToast();
2960
+ });
2961
+ }
2962
+ } catch {
2963
+ }
2964
+ return () => sub?.remove();
2965
+ }, [maybeShowToast]);
2966
+ const openMyReports = useCallback4(() => {
2967
+ setToast(null);
2968
+ setSheetScreenshot(null);
2969
+ setSheetPreferredTab("inbox");
2970
+ setSheetVisible(true);
2971
+ }, []);
2237
2972
  const instance = useMemo(
2238
2973
  () => ({
2239
2974
  open,
@@ -2320,6 +3055,54 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2320
3055
  const res = await client.listReporterComments(reportId, reporterTokenRef.current);
2321
3056
  return res.ok ? res.data.comments ?? [] : [];
2322
3057
  },
3058
+ async loadMyThread(reportId) {
3059
+ const client = apiClientRef.current;
3060
+ if (!client) return null;
3061
+ try {
3062
+ await reporterTokenReadyRef.current;
3063
+ const res = await client.listReporterComments(reportId, reporterTokenRef.current);
3064
+ return res.ok ? res.data?.comments ?? [] : null;
3065
+ } catch {
3066
+ return null;
3067
+ }
3068
+ },
3069
+ async markReportRead(reportId) {
3070
+ const client = apiClientRef.current;
3071
+ if (!client) return 0;
3072
+ await reporterTokenReadyRef.current;
3073
+ return markReporterReportRead(client, reporterTokenRef.current, reportId);
3074
+ },
3075
+ getReporterUpdates: () => reporterUpdatesRef.current.emit(),
3076
+ async loadMyReportDetail(reportId) {
3077
+ const client = apiClientRef.current;
3078
+ if (!client) return null;
3079
+ try {
3080
+ await reporterTokenReadyRef.current;
3081
+ const res = await reporterChannels2(client).getReport(reportId, reporterTokenRef.current);
3082
+ if (res.ok) return res.data ?? null;
3083
+ return res.error?.status === 404 && res.error.code !== "NOT_FOUND" ? "unsupported" : null;
3084
+ } catch {
3085
+ return null;
3086
+ }
3087
+ },
3088
+ emailPrefill() {
3089
+ return config.notifications?.emailFromIdentity ? userRef.current?.email ?? null : null;
3090
+ },
3091
+ openMyReports,
3092
+ onReporterUpdate: (cb) => reporterUpdatesRef.current.subscribe(cb),
3093
+ async getNotificationPrefs() {
3094
+ const client = apiClientRef.current;
3095
+ if (!client) return null;
3096
+ await reporterTokenReadyRef.current;
3097
+ const res = await reporterChannels2(client).getPrefs(reporterTokenRef.current);
3098
+ return res.ok ? res.data ?? null : null;
3099
+ },
3100
+ async setNotificationPrefs(update) {
3101
+ const client = apiClientRef.current;
3102
+ if (!client) return { ok: false, error: { code: "NOT_INITIALIZED", message: "Mushi is not ready yet" } };
3103
+ await reporterTokenReadyRef.current;
3104
+ return reporterChannels2(client).setPrefs(reporterTokenRef.current, update);
3105
+ },
2323
3106
  async replyToReport(reportId, body) {
2324
3107
  const client = apiClientRef.current;
2325
3108
  if (!client) return null;
@@ -2390,14 +3173,14 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2390
3173
  return res.ok ? res.data : null;
2391
3174
  }
2392
3175
  }),
2393
- [open, close, attachTo, submitReport, openAssistant, config.rewards?.enabled, config.assistant?.enabled]
3176
+ [open, close, attachTo, submitReport, openAssistant, openMyReports, config.rewards?.enabled, config.assistant?.enabled, config.notifications?.emailFromIdentity]
2394
3177
  );
2395
3178
  const trigger = config.widget?.trigger ?? "button";
2396
3179
  const showBanner = trigger === "banner" && !bannerDismissed;
2397
3180
  const showFab = !showBanner && (trigger === "button" || trigger === "both" || trigger === "auto" || trigger === "edge-tab");
2398
- return /* @__PURE__ */ jsxs3(MushiContext.Provider, { value: instance, children: [
3181
+ return /* @__PURE__ */ jsxs4(MushiContext.Provider, { value: instance, children: [
2399
3182
  children,
2400
- showBanner ? /* @__PURE__ */ jsx4(
3183
+ showBanner ? /* @__PURE__ */ jsx5(
2401
3184
  MushiBanner,
2402
3185
  {
2403
3186
  onPress: open,
@@ -2406,7 +3189,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2406
3189
  label: config.widget?.banner?.label
2407
3190
  }
2408
3191
  ) : null,
2409
- showFab && /* @__PURE__ */ jsx4(
3192
+ showFab && /* @__PURE__ */ jsx5(
2410
3193
  MushiFloatingButton,
2411
3194
  {
2412
3195
  onPress: open,
@@ -2414,7 +3197,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2414
3197
  inset: config.widget?.inset
2415
3198
  }
2416
3199
  ),
2417
- /* @__PURE__ */ jsx4(
3200
+ /* @__PURE__ */ jsx5(
2418
3201
  MushiBottomSheet,
2419
3202
  {
2420
3203
  visible: sheetVisible,
@@ -2427,9 +3210,21 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2427
3210
  assistantLabel: config.assistant?.label,
2428
3211
  assistantGreeting: config.assistant?.greeting,
2429
3212
  assistantSuggestions: config.assistant?.suggestions,
2430
- inboxPollIntervalMs: config.widget?.inboxPollIntervalMs ?? 0
3213
+ inboxPollIntervalMs: config.widget?.inboxPollIntervalMs ?? 0,
3214
+ theme: config.widget?.theme,
3215
+ minDescriptionLength: config.widget?.minDescriptionLength,
3216
+ emailOptIn: config.notifications?.email !== false
3217
+ }
3218
+ ),
3219
+ toast ? /* @__PURE__ */ jsx5(
3220
+ MushiUpdateToast,
3221
+ {
3222
+ message: toast.message,
3223
+ onView: openMyReports,
3224
+ onDismiss: () => setToast(null),
3225
+ theme: config.widget?.theme
2431
3226
  }
2432
- )
3227
+ ) : null
2433
3228
  ] });
2434
3229
  }
2435
3230
  function useMushiContext() {
@@ -2480,10 +3275,10 @@ function MushiTrigger({ children }) {
2480
3275
  }
2481
3276
 
2482
3277
  // src/capture/navigation-capture.ts
2483
- import { useEffect as useEffect4, useRef as useRef4 } from "react";
3278
+ import { useEffect as useEffect5, useRef as useRef4 } from "react";
2484
3279
  function useNavigationCapture(navigationRef) {
2485
3280
  const historyRef = useRef4([]);
2486
- useEffect4(() => {
3281
+ useEffect5(() => {
2487
3282
  if (!navigationRef?.addListener || !navigationRef?.getCurrentRoute) return;
2488
3283
  const unsubscribe = navigationRef.addListener("state", () => {
2489
3284
  const route = navigationRef.getCurrentRoute?.();