@mushi-mushi/react-native 0.21.0 → 0.23.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
@@ -15,11 +15,12 @@ import {
15
15
  useCallback as useCallback4,
16
16
  useMemo
17
17
  } from "react";
18
+ import { AppState, Platform as Platform5 } from "react-native";
18
19
  import {
19
20
  createApiClient,
20
21
  createBreadcrumbBuffer,
21
22
  DEFAULT_API_ENDPOINT,
22
- newUuid,
23
+ newUuid as newUuid2,
23
24
  parseIdentityToken,
24
25
  scrubPii as scrubPii3
25
26
  } from "@mushi-mushi/core";
@@ -475,41 +476,105 @@ import {
475
476
  useColorScheme
476
477
  } from "react-native";
477
478
  import {
478
- mushiPalette,
479
- MUSHI_BANNER_NEON,
480
479
  MUSHI_COPY,
481
- MUSHI_CONTROL_DISABLED,
482
480
  MUSHI_INVERSE,
483
- MUSHI_RADIUS,
481
+ MUSHI_RADIUS as MUSHI_RADIUS2,
484
482
  MUSHI_SHADOW_INK,
485
483
  MUSHI_SPACING,
486
484
  MUSHI_TYPE
487
485
  } from "@mushi-mushi/core";
486
+ import { reporterCopy, reporterStatus, resolveReporterLocale } from "@mushi-mushi/core/reporter-ui";
488
487
  import { getLocale } from "@mushi-mushi/web/i18n";
489
488
 
490
- // src/reporter-status.ts
491
- function reporterStatusShort(status) {
492
- switch (status) {
493
- case "new":
494
- case "queued":
495
- case "pending":
496
- case "submitted":
497
- return "Received";
498
- case "classified":
499
- case "triaged":
500
- case "grouped":
501
- case "dispatched":
502
- return "In review";
503
- case "fixing":
504
- return "Fixing";
505
- case "fixed":
506
- case "resolved":
507
- case "completed":
508
- return "Fixed";
509
- case "dismissed":
510
- return "Closed";
511
- default:
512
- return status.replace(/_/g, " ").slice(0, 16);
489
+ // src/theme.ts
490
+ import { mushiPalette, MUSHI_CONTROL_DISABLED, MUSHI_RADIUS } from "@mushi-mushi/core";
491
+ function hexToRgb(hex) {
492
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})([0-9a-f]{2})?$/i.exec(hex.trim());
493
+ if (!m) return null;
494
+ const h = m[1].length === 3 ? m[1].split("").map((c) => c + c).join("") : m[1];
495
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
496
+ }
497
+ function luminance([r, g, b]) {
498
+ const lin = (c) => {
499
+ const s4 = c / 255;
500
+ return s4 <= 0.03928 ? s4 / 12.92 : ((s4 + 0.055) / 1.055) ** 2.4;
501
+ };
502
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
503
+ }
504
+ function contrastingText(bg, fallback) {
505
+ const rgb = hexToRgb(bg);
506
+ if (!rgb) return fallback;
507
+ const l = luminance(rgb);
508
+ return 1.05 / (l + 0.05) >= (l + 0.05) / 0.05 ? "#FFFFFF" : "#000000";
509
+ }
510
+ function resolveRNTheme(dark, override = {}) {
511
+ const pal = mushiPalette(dark ? "dark" : "light");
512
+ const base = {
513
+ bg: dark ? pal.paper : pal.paperRaised,
514
+ fg: pal.ink,
515
+ muted: pal.inkMuted,
516
+ surface: dark ? pal.paperRaised : pal.paper,
517
+ border: pal.ruleStrong,
518
+ accent: pal.ink,
519
+ accentFg: dark ? pal.paper : pal.paperRaised,
520
+ success: pal.ok,
521
+ error: pal.danger,
522
+ fontFamily: void 0,
523
+ radius: MUSHI_RADIUS.card
524
+ };
525
+ const defined = Object.fromEntries(
526
+ Object.entries(override).filter(([, v]) => v !== void 0 && v !== null && v !== "")
527
+ );
528
+ const merged = { ...base, ...defined };
529
+ if (defined.accent && !defined.accentFg) merged.accentFg = contrastingText(defined.accent, base.accentFg);
530
+ return {
531
+ ...merged,
532
+ backdrop: dark ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.35)",
533
+ disabled: dark ? MUSHI_CONTROL_DISABLED.dark : MUSHI_CONTROL_DISABLED.light,
534
+ disabledFg: pal.inkFaint
535
+ };
536
+ }
537
+
538
+ // src/reporter-thread.ts
539
+ var THREAD_LOAD_TIMEOUT_MS = 12e3;
540
+ function settleWithin(p, ms) {
541
+ return new Promise((resolve) => {
542
+ const timer = setTimeout(() => resolve(null), ms);
543
+ p.then(
544
+ (value) => {
545
+ clearTimeout(timer);
546
+ resolve(value);
547
+ },
548
+ () => {
549
+ clearTimeout(timer);
550
+ resolve(null);
551
+ }
552
+ );
553
+ });
554
+ }
555
+ function loadReporterThread(load, reportId, timeoutMs = THREAD_LOAD_TIMEOUT_MS) {
556
+ let started;
557
+ try {
558
+ started = load(reportId);
559
+ } catch {
560
+ return Promise.resolve(null);
561
+ }
562
+ return settleWithin(started, timeoutMs);
563
+ }
564
+ async function markReporterReportRead(client, reporterToken, reportId) {
565
+ try {
566
+ const res = await client.listNotifications(reporterToken, { limit: 100 });
567
+ if (!res.ok) return 0;
568
+ const rows = res.data?.notifications ?? [];
569
+ const ids = rows.filter((n) => !n.read_at && n.payload?.reportId === reportId).map((n) => String(n.id));
570
+ let marked = 0;
571
+ for (const id of ids) {
572
+ const r = await client.markNotificationRead(id, reporterToken);
573
+ if (r.ok) marked++;
574
+ }
575
+ return marked;
576
+ } catch {
577
+ return 0;
513
578
  }
514
579
  }
515
580
 
@@ -518,6 +583,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
518
583
  var { height: SCREEN_HEIGHT } = Dimensions2.get("window");
519
584
  var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
520
585
  var DISMISS_THRESHOLD = 80;
586
+ var MIN_DESCRIPTION = 20;
521
587
  var CATEGORY_KEYS = ["bug", "slow", "visual", "confusing", "other"];
522
588
  var CATEGORY_EMOJI = {
523
589
  bug: "\u{1F41B}",
@@ -526,7 +592,6 @@ var CATEGORY_EMOJI = {
526
592
  confusing: "\u{1F615}",
527
593
  other: "\u{1F4AC}"
528
594
  };
529
- var VERIFIABLE_STATUSES = /* @__PURE__ */ new Set(["fixed", "resolved", "verified"]);
530
595
  var MushiBottomSheet = ({
531
596
  visible,
532
597
  onClose,
@@ -538,10 +603,15 @@ var MushiBottomSheet = ({
538
603
  assistantLabel = MUSHI_COPY.assistantTab,
539
604
  assistantGreeting,
540
605
  assistantSuggestions = [],
541
- inboxPollIntervalMs = 0
606
+ inboxPollIntervalMs = 0,
607
+ theme
542
608
  }) => {
543
609
  const mushi = useMushiContext();
544
610
  const t = getLocale();
611
+ const locale = resolveReporterLocale(
612
+ typeof navigator !== "undefined" ? navigator.language : void 0
613
+ );
614
+ const rc = reporterCopy(locale);
545
615
  const greeting = assistantGreeting ?? t.assistant.defaultGreeting;
546
616
  const scheme = useColorScheme();
547
617
  const dark = scheme === "dark";
@@ -553,26 +623,37 @@ var MushiBottomSheet = ({
553
623
  const [submitError, setSubmitError] = useState(null);
554
624
  const [sheetTab, setSheetTab] = useState("report");
555
625
  const [inboxReports, setInboxReports] = useState([]);
626
+ const [inboxLoaded, setInboxLoaded] = useState(false);
556
627
  const [inboxLoading, setInboxLoading] = useState(false);
557
628
  const [selectedReportId, setSelectedReportId] = useState(null);
629
+ const selectedRef = useRef(null);
558
630
  const [threadComments, setThreadComments] = useState([]);
631
+ const [threadStatus, setThreadStatus] = useState("idle");
559
632
  const [replyText, setReplyText] = useState("");
633
+ const [replyState, setReplyState] = useState("idle");
634
+ const [feedbackSending, setFeedbackSending] = useState(false);
635
+ const [feedbackError, setFeedbackError] = useState(false);
560
636
  const [assistantInput, setAssistantInput] = useState("");
561
637
  const [assistantThreadId, setAssistantThreadId] = useState(null);
562
638
  const [assistantSending, setAssistantSending] = useState(false);
563
639
  const [assistantError, setAssistantError] = useState(null);
564
640
  const [assistantTurns, setAssistantTurns] = useState([]);
565
641
  const [screenshotAttached, setScreenshotAttached] = useState(true);
566
- const loadInbox = useCallback(async () => {
567
- if (!mushi?.listMyReports) return;
568
- setInboxLoading(true);
569
- try {
570
- const rows = await mushi.listMyReports();
571
- setInboxReports(rows);
572
- } finally {
573
- setInboxLoading(false);
574
- }
575
- }, [mushi]);
642
+ const loadInbox = useCallback(
643
+ async (opts = {}) => {
644
+ if (!mushi?.listMyReports) return;
645
+ if (!opts.silent) setInboxLoading(true);
646
+ try {
647
+ const rows = await mushi.listMyReports();
648
+ setInboxReports(rows.filter((r) => !reporterStatus(r, locale).hidden));
649
+ setInboxLoaded(true);
650
+ } catch {
651
+ } finally {
652
+ if (!opts.silent) setInboxLoading(false);
653
+ }
654
+ },
655
+ [mushi, locale]
656
+ );
576
657
  useEffect(() => {
577
658
  if (visible) {
578
659
  setScreenshotAttached(true);
@@ -583,28 +664,87 @@ var MushiBottomSheet = ({
583
664
  useEffect(() => {
584
665
  if (!visible || sheetTab !== "inbox" || inboxPollIntervalMs <= 0) return;
585
666
  const timer = setInterval(() => {
586
- void loadInbox();
667
+ void loadInbox({ silent: true });
587
668
  }, inboxPollIntervalMs);
588
669
  return () => clearInterval(timer);
589
670
  }, [visible, sheetTab, inboxPollIntervalMs, loadInbox]);
590
- const openThread = useCallback(async (reportId) => {
591
- setSelectedReportId(reportId);
592
- if (!mushi?.listMyComments) return;
593
- const comments = await mushi.listMyComments(reportId);
594
- setThreadComments(comments);
595
- }, [mushi]);
671
+ const openThread = useCallback(
672
+ async (reportId, opts = {}) => {
673
+ selectedRef.current = reportId;
674
+ setSelectedReportId(reportId);
675
+ if (!opts.silent) {
676
+ setThreadComments([]);
677
+ setThreadStatus("loading");
678
+ setReplyState("idle");
679
+ setFeedbackError(false);
680
+ }
681
+ const load = mushi?.loadMyThread ?? mushi?.listMyComments;
682
+ const comments = load ? await loadReporterThread(load, reportId) : null;
683
+ if (selectedRef.current !== reportId) return;
684
+ if (comments === null) {
685
+ setThreadStatus((prev) => opts.silent && prev === "ready" ? "ready" : "error");
686
+ return;
687
+ }
688
+ setThreadComments(comments);
689
+ setThreadStatus("ready");
690
+ if (!opts.silent && mushi?.markReportRead) {
691
+ mushi.markReportRead(reportId).then((marked) => {
692
+ if (marked > 0) {
693
+ setInboxReports((rows) => rows.map((r) => r.id === reportId ? { ...r, unread_count: 0 } : r));
694
+ }
695
+ }).catch(() => void 0);
696
+ }
697
+ },
698
+ [mushi]
699
+ );
700
+ const closeThread = useCallback(() => {
701
+ selectedRef.current = null;
702
+ setSelectedReportId(null);
703
+ setThreadStatus("idle");
704
+ }, []);
596
705
  const sendReply = useCallback(async () => {
597
- if (!mushi?.replyToReport || !selectedReportId || !replyText.trim()) return;
598
- await mushi.replyToReport(selectedReportId, replyText.trim());
599
- setReplyText("");
600
- await openThread(selectedReportId);
601
- }, [mushi, selectedReportId, replyText, openThread]);
602
- const submitFeedback = useCallback(async (signal) => {
603
- if (!mushi?.submitFeedbackSignal || !selectedReportId) return;
604
- await mushi.submitFeedbackSignal(selectedReportId, signal);
605
- await loadInbox();
606
- await openThread(selectedReportId);
607
- }, [mushi, selectedReportId, loadInbox, openThread]);
706
+ const body = replyText.trim();
707
+ const reportId = selectedReportId;
708
+ if (!mushi?.replyToReport || !reportId || !body || replyState === "sending") return;
709
+ setReplyState("sending");
710
+ try {
711
+ const comment = await settleWithin(mushi.replyToReport(reportId, body), THREAD_LOAD_TIMEOUT_MS);
712
+ if (!comment) {
713
+ setReplyState("failed");
714
+ return;
715
+ }
716
+ setReplyText("");
717
+ setReplyState("idle");
718
+ if (selectedRef.current === reportId) {
719
+ setThreadComments((prev) => [...prev, comment]);
720
+ void openThread(reportId, { silent: true });
721
+ }
722
+ } catch {
723
+ setReplyState("failed");
724
+ }
725
+ }, [mushi, selectedReportId, replyText, replyState, openThread]);
726
+ const submitFeedback = useCallback(
727
+ async (signal) => {
728
+ const reportId = selectedReportId;
729
+ if (!mushi?.submitFeedbackSignal || !reportId || feedbackSending) return;
730
+ setFeedbackSending(true);
731
+ setFeedbackError(false);
732
+ try {
733
+ const outcome = await mushi.submitFeedbackSignal(reportId, signal);
734
+ if (!outcome) {
735
+ setFeedbackError(true);
736
+ return;
737
+ }
738
+ await loadInbox({ silent: true });
739
+ await openThread(reportId, { silent: true });
740
+ } catch {
741
+ setFeedbackError(true);
742
+ } finally {
743
+ setFeedbackSending(false);
744
+ }
745
+ },
746
+ [mushi, selectedReportId, feedbackSending, loadInbox, openThread]
747
+ );
608
748
  const sendAssistant = useCallback(async (message) => {
609
749
  if (!mushi?.askAssistant || !message.trim() || assistantSending) return;
610
750
  const trimmed = message.trim();
@@ -700,7 +840,7 @@ var MushiBottomSheet = ({
700
840
  const handleSubmit = async () => {
701
841
  if (!category || !description.trim() || !mushi) return;
702
842
  const trimmed = description.trim();
703
- if (trimmed.length < 20) {
843
+ if (trimmed.length < MIN_DESCRIPTION) {
704
844
  setSubmitError("Please write at least 20 characters so we can understand the issue.");
705
845
  return;
706
846
  }
@@ -735,11 +875,181 @@ var MushiBottomSheet = ({
735
875
  }
736
876
  };
737
877
  const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
738
- const pal = mushiPalette(dark ? "dark" : "light");
739
- 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 };
878
+ const colors = resolveRNTheme(dark, theme);
879
+ const font = colors.fontFamily ? { fontFamily: colors.fontFamily } : null;
880
+ const radius = colors.radius;
740
881
  const sheetTabs = ["report", "inbox", ...assistantEnabled ? ["assistant"] : []];
741
- const canSubmit = !!category && description.trim().length >= 20 && (phase === "form" || phase === "error");
742
- return /* @__PURE__ */ jsx(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ jsxs(
882
+ const canSubmit = !!category && description.trim().length >= MIN_DESCRIPTION && (phase === "form" || phase === "error");
883
+ const submitHint = canSubmit || phase === "sending" ? void 0 : !category ? t.step1.heading : rc.ui.addWords;
884
+ const selectedReport = selectedReportId ? inboxReports.find((r) => r.id === selectedReportId) ?? null : null;
885
+ const selectedView = selectedReport ? reporterStatus(selectedReport, locale) : null;
886
+ const headerTitle = sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title;
887
+ const renderThread = () => /* @__PURE__ */ jsxs(Fragment, { children: [
888
+ /* @__PURE__ */ jsx(
889
+ TouchableOpacity,
890
+ {
891
+ onPress: closeThread,
892
+ accessibilityRole: "button",
893
+ accessibilityLabel: t.widget.back,
894
+ hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
895
+ children: /* @__PURE__ */ jsxs(Text, { style: [{ color: colors.accent, marginBottom: 8 }, font], children: [
896
+ "\u2190 ",
897
+ t.widget.back
898
+ ] })
899
+ }
900
+ ),
901
+ selectedReport && selectedView ? /* @__PURE__ */ jsxs(View, { style: [s.headerCard, { backgroundColor: colors.surface, borderRadius: radius, borderColor: colors.border }], children: [
902
+ /* @__PURE__ */ jsx(Text, { style: [s.pill, { color: colors.fg, borderColor: colors.border }, font], children: selectedView.label }),
903
+ /* @__PURE__ */ jsx(Text, { style: [{ color: colors.muted, fontSize: 12, marginTop: 4 }, font], children: selectedView.detail }),
904
+ /* @__PURE__ */ jsx(Text, { style: [{ color: colors.fg, marginTop: 8 }, font], numberOfLines: 4, children: selectedReport.title ?? selectedReport.summary ?? selectedReport.description ?? "" })
905
+ ] }) : null,
906
+ threadStatus === "loading" ? /* @__PURE__ */ jsx(View, { accessibilityLabel: t.flows.reports.loading, accessibilityLiveRegion: "polite", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx(View, { style: [s.skeleton, { backgroundColor: colors.surface, width: `${90 - i * 18}%` }] }, i)) }) : threadStatus === "error" ? /* @__PURE__ */ jsxs(View, { style: s.errorRow, accessibilityLiveRegion: "polite", children: [
907
+ /* @__PURE__ */ jsx(Text, { style: [{ color: colors.error, flex: 1 }, font], accessibilityRole: "alert", children: rc.ui.loadError }),
908
+ /* @__PURE__ */ jsx(
909
+ TouchableOpacity,
910
+ {
911
+ onPress: () => selectedReportId && void openThread(selectedReportId),
912
+ accessibilityRole: "button",
913
+ style: [s.retryBtn, { borderColor: colors.border, borderRadius: radius }],
914
+ children: /* @__PURE__ */ jsx(Text, { style: [{ color: colors.fg, fontWeight: "600" }, font], children: rc.ui.retry })
915
+ }
916
+ )
917
+ ] }) : threadComments.length === 0 ? /* @__PURE__ */ jsx(Text, { style: [{ color: colors.muted, marginBottom: 8 }, font], children: rc.ui.noReplies }) : threadComments.map((c) => /* @__PURE__ */ jsxs(
918
+ View,
919
+ {
920
+ style: [
921
+ s.threadBubble,
922
+ {
923
+ backgroundColor: colors.surface,
924
+ alignSelf: c.author_kind === "reporter" ? "flex-end" : "flex-start"
925
+ }
926
+ ],
927
+ children: [
928
+ /* @__PURE__ */ jsx(Text, { style: [{ color: colors.muted, fontSize: 11 }, font], children: c.author_kind === "reporter" ? rc.ui.you : rc.ui.developer }),
929
+ /* @__PURE__ */ jsx(Text, { style: [{ color: colors.fg }, font], children: c.body })
930
+ ]
931
+ },
932
+ c.id
933
+ )),
934
+ replyState === "sending" ? /* @__PURE__ */ jsxs(View, { style: [s.threadBubble, { backgroundColor: colors.surface, alignSelf: "flex-end", opacity: 0.7 }], children: [
935
+ /* @__PURE__ */ jsx(Text, { style: [{ color: colors.muted, fontSize: 11 }, font], children: rc.ui.sending }),
936
+ /* @__PURE__ */ jsx(Text, { style: [{ color: colors.fg }, font], children: replyText.trim() })
937
+ ] }) : null,
938
+ selectedView?.canVerify ? /* @__PURE__ */ jsxs(View, { style: s.verifyRow, children: [
939
+ /* @__PURE__ */ jsx(
940
+ TouchableOpacity,
941
+ {
942
+ style: [s.verifyBtn, { backgroundColor: colors.accent, borderRadius: radius }],
943
+ onPress: () => void submitFeedback("confirms"),
944
+ disabled: feedbackSending,
945
+ accessibilityRole: "button",
946
+ accessibilityState: { disabled: feedbackSending, busy: feedbackSending },
947
+ children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentFg }, font], children: rc.ui.yes })
948
+ }
949
+ ),
950
+ /* @__PURE__ */ jsx(
951
+ TouchableOpacity,
952
+ {
953
+ style: [s.verifyBtn, { backgroundColor: colors.border, borderRadius: radius }],
954
+ onPress: () => void submitFeedback("not_fixed"),
955
+ disabled: feedbackSending,
956
+ accessibilityRole: "button",
957
+ accessibilityState: { disabled: feedbackSending, busy: feedbackSending },
958
+ children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.fg }, font], children: rc.ui.notYet })
959
+ }
960
+ )
961
+ ] }) : null,
962
+ feedbackError ? /* @__PURE__ */ jsx(Text, { style: [{ color: colors.error, marginBottom: 8 }, font], accessibilityRole: "alert", children: rc.ui.sendFailed }) : null,
963
+ /* @__PURE__ */ jsxs(View, { style: s.composer, children: [
964
+ /* @__PURE__ */ jsx(
965
+ TextInput,
966
+ {
967
+ style: [
968
+ s.input,
969
+ s.composerInput,
970
+ { backgroundColor: colors.surface, color: colors.fg, borderColor: colors.border, borderRadius: radius },
971
+ font
972
+ ],
973
+ placeholder: t.flows.thread.replyPlaceholder,
974
+ placeholderTextColor: colors.muted,
975
+ value: replyText,
976
+ onChangeText: (text) => {
977
+ setReplyText(text.slice(0, 2e3));
978
+ if (replyState === "failed") setReplyState("idle");
979
+ },
980
+ multiline: true,
981
+ maxLength: 2e3,
982
+ editable: replyState !== "sending",
983
+ accessibilityLabel: t.flows.thread.replyPlaceholder
984
+ }
985
+ ),
986
+ /* @__PURE__ */ jsx(
987
+ TouchableOpacity,
988
+ {
989
+ style: [
990
+ s.sendBtn,
991
+ {
992
+ backgroundColor: replyText.trim() && replyState !== "sending" ? colors.accent : colors.disabled,
993
+ borderRadius: radius
994
+ }
995
+ ],
996
+ onPress: () => void sendReply(),
997
+ disabled: !replyText.trim() || replyState === "sending",
998
+ accessibilityRole: "button",
999
+ accessibilityLabel: replyState === "failed" ? rc.ui.retry : t.flows.thread.send,
1000
+ accessibilityState: { disabled: !replyText.trim() || replyState === "sending", busy: replyState === "sending" },
1001
+ children: /* @__PURE__ */ jsx(
1002
+ Text,
1003
+ {
1004
+ style: [
1005
+ s.submitText,
1006
+ { color: replyText.trim() && replyState !== "sending" ? colors.accentFg : colors.disabledFg },
1007
+ font
1008
+ ],
1009
+ children: replyState === "failed" ? rc.ui.retry : t.flows.thread.send
1010
+ }
1011
+ )
1012
+ }
1013
+ )
1014
+ ] }),
1015
+ replyState === "failed" ? /* @__PURE__ */ jsxs(Text, { style: [{ color: colors.error, marginTop: 6, fontSize: 12 }, font], accessibilityRole: "alert", children: [
1016
+ rc.ui.sendFailed,
1017
+ " \xB7 ",
1018
+ rc.ui.retry
1019
+ ] }) : null
1020
+ ] });
1021
+ const renderInboxList = () => {
1022
+ if (inboxLoading && !inboxLoaded) {
1023
+ return /* @__PURE__ */ jsx(Text, { style: [{ color: colors.muted }, font], children: t.flows.reports.loading });
1024
+ }
1025
+ if (inboxReports.length === 0) {
1026
+ return /* @__PURE__ */ jsx(Text, { style: [{ color: colors.muted }, font], children: rc.ui.empty });
1027
+ }
1028
+ return inboxReports.map((r) => {
1029
+ const view = reporterStatus(r, locale);
1030
+ const unread = (r.unread_count ?? 0) > 0;
1031
+ const title = (r.title ?? r.summary ?? r.description ?? "Report").slice(0, 80);
1032
+ return /* @__PURE__ */ jsxs(
1033
+ TouchableOpacity,
1034
+ {
1035
+ style: [s.inboxRow, { borderColor: colors.border }],
1036
+ onPress: () => void openThread(r.id),
1037
+ accessibilityRole: "button",
1038
+ accessibilityLabel: `${view.label}. ${title}${unread ? `. ${r.unread_count} new` : ""}`,
1039
+ children: [
1040
+ /* @__PURE__ */ jsx(Text, { style: [{ color: colors.fg, fontWeight: unread ? "700" : "600" }, font], numberOfLines: 1, children: title }),
1041
+ /* @__PURE__ */ jsxs(Text, { style: [{ color: unread ? colors.fg : colors.muted, fontSize: 12, marginTop: 2 }, font], numberOfLines: 1, children: [
1042
+ view.label,
1043
+ view.othersNote ? ` \xB7 ${view.othersNote}` : ""
1044
+ ] }),
1045
+ unread && r.last_event_preview ? /* @__PURE__ */ jsx(Text, { style: [{ color: colors.fg, fontSize: 12, fontWeight: "700", marginTop: 2 }, font], numberOfLines: 1, children: r.last_event_preview }) : null
1046
+ ]
1047
+ },
1048
+ r.id
1049
+ );
1050
+ });
1051
+ };
1052
+ return /* @__PURE__ */ jsx(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, onRequestClose: handleClose, children: /* @__PURE__ */ jsxs(
743
1053
  KeyboardAvoidingView,
744
1054
  {
745
1055
  style: s.flex,
@@ -750,44 +1060,69 @@ var MushiBottomSheet = ({
750
1060
  Animated.View,
751
1061
  {
752
1062
  style: [s.backdrop, { backgroundColor: colors.backdrop, opacity: backdropOpacity }],
753
- children: /* @__PURE__ */ jsx(TouchableOpacity, { style: s.flex, activeOpacity: 1, onPress: handleClose })
1063
+ children: /* @__PURE__ */ jsx(
1064
+ TouchableOpacity,
1065
+ {
1066
+ style: s.flex,
1067
+ activeOpacity: 1,
1068
+ onPress: handleClose,
1069
+ accessibilityRole: "button",
1070
+ accessibilityLabel: t.widget.close
1071
+ }
1072
+ )
754
1073
  }
755
1074
  ),
756
1075
  /* @__PURE__ */ jsxs(
757
1076
  Animated.View,
758
1077
  {
1078
+ accessibilityViewIsModal: true,
1079
+ onAccessibilityEscape: handleClose,
759
1080
  style: [
760
1081
  s.sheet,
761
1082
  { backgroundColor: colors.bg, transform: [{ translateY }] }
762
1083
  ],
763
1084
  children: [
764
- /* @__PURE__ */ jsx(View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ jsx(View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
765
- /* @__PURE__ */ jsxs(View, { style: [s.brandHeader, { backgroundColor: MUSHI_BANNER_NEON.bg, borderBottomColor: MUSHI_BANNER_NEON.border }], children: [
766
- /* @__PURE__ */ jsx(Text, { style: [s.brandEyebrow, { color: MUSHI_BANNER_NEON.fg }], children: "MUSHI \xB7 BETA" }),
767
- /* @__PURE__ */ jsx(Text, { style: [s.brandTitle, { color: MUSHI_BANNER_NEON.fg }], children: sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title })
768
- ] }),
769
- /* @__PURE__ */ jsx(View, { style: [s.tabRow, { borderBottomColor: colors.border }], children: sheetTabs.map((tab) => /* @__PURE__ */ jsx(
1085
+ /* @__PURE__ */ jsx(
1086
+ View,
1087
+ {
1088
+ ...panResponder.panHandlers,
1089
+ style: s.handleArea,
1090
+ accessible: true,
1091
+ accessibilityRole: "adjustable",
1092
+ accessibilityLabel: t.widget.close,
1093
+ accessibilityActions: [{ name: "decrement", label: t.widget.close }, { name: "escape" }],
1094
+ onAccessibilityAction: (e) => {
1095
+ if (e.nativeEvent.actionName === "decrement" || e.nativeEvent.actionName === "escape") handleClose();
1096
+ },
1097
+ children: /* @__PURE__ */ jsx(View, { style: [s.handle, { backgroundColor: colors.muted }] })
1098
+ }
1099
+ ),
1100
+ /* @__PURE__ */ jsx(View, { style: [s.header, { borderBottomColor: colors.border }], children: /* @__PURE__ */ jsx(Text, { accessibilityRole: "header", style: [s.headerTitle, { color: colors.fg }, font], children: headerTitle }) }),
1101
+ /* @__PURE__ */ jsx(View, { style: [s.tabRow, { borderBottomColor: colors.border }], accessibilityRole: "tablist", children: sheetTabs.map((tab) => /* @__PURE__ */ jsx(
770
1102
  TouchableOpacity,
771
1103
  {
772
1104
  onPress: () => {
773
1105
  setSheetTab(tab);
774
- if (tab === "inbox") void loadInbox();
775
- else setSelectedReportId(null);
1106
+ if (tab === "inbox") void loadInbox({ silent: inboxLoaded });
1107
+ else closeThread();
776
1108
  },
777
1109
  style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
778
- 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 })
1110
+ accessibilityRole: "tab",
1111
+ accessibilityState: { selected: sheetTab === tab },
1112
+ children: /* @__PURE__ */ jsx(Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.fg : colors.muted }, font], children: tab === "report" ? t.widget.trigger : tab === "inbox" ? t.flows.reports.title : assistantLabel })
779
1113
  },
780
1114
  tab
781
1115
  )) }),
782
1116
  sheetTab === "assistant" ? /* @__PURE__ */ jsxs(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: [
783
1117
  assistantTurns.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
784
- /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, marginBottom: 12, lineHeight: 20 }, children: greeting }),
1118
+ /* @__PURE__ */ jsx(Text, { style: [{ color: colors.muted, marginBottom: 12, lineHeight: 20 }, font], children: greeting }),
785
1119
  assistantSuggestions.map((chip) => /* @__PURE__ */ jsx(
786
1120
  TouchableOpacity,
787
1121
  {
788
- style: [s.assistantChip, { borderColor: colors.border, backgroundColor: colors.card }],
1122
+ style: [s.assistantChip, { borderColor: colors.border, backgroundColor: colors.surface }],
789
1123
  onPress: () => void sendAssistant(chip),
790
- children: /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontSize: 13 }, children: chip })
1124
+ accessibilityRole: "button",
1125
+ children: /* @__PURE__ */ jsx(Text, { style: [{ color: colors.fg, fontSize: 13 }, font], children: chip })
791
1126
  },
792
1127
  chip
793
1128
  ))
@@ -798,25 +1133,34 @@ var MushiBottomSheet = ({
798
1133
  s.assistantBubble,
799
1134
  {
800
1135
  alignSelf: turn.role === "user" ? "flex-end" : "flex-start",
801
- backgroundColor: turn.role === "user" ? colors.accent : colors.card
1136
+ backgroundColor: turn.role === "user" ? colors.accent : colors.surface
802
1137
  }
803
1138
  ],
804
1139
  children: [
805
- /* @__PURE__ */ jsx(Text, { style: { color: turn.role === "user" ? colors.accentInk : colors.text }, children: turn.text }),
806
- 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))
1140
+ /* @__PURE__ */ jsx(Text, { style: [{ color: turn.role === "user" ? colors.accentFg : colors.fg }, font], children: turn.text }),
1141
+ turn.options?.map((opt) => /* @__PURE__ */ jsx(
1142
+ TouchableOpacity,
1143
+ {
1144
+ onPress: () => void sendAssistant(opt),
1145
+ style: { marginTop: 8 },
1146
+ accessibilityRole: "button",
1147
+ children: /* @__PURE__ */ jsx(Text, { style: [{ color: colors.accent, fontSize: 12 }, font], children: opt })
1148
+ },
1149
+ opt
1150
+ ))
807
1151
  ]
808
1152
  },
809
1153
  `${turn.role}-${idx}`
810
1154
  )),
811
- assistantSending ? /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, marginTop: 8 }, accessibilityLiveRegion: "polite", children: t.assistant.thinking }) : null,
812
- assistantError ? /* @__PURE__ */ jsx(Text, { style: { color: pal.danger, marginTop: 8 }, accessibilityRole: "alert", children: assistantError }) : null,
1155
+ assistantSending ? /* @__PURE__ */ jsx(Text, { style: [{ color: colors.muted, marginTop: 8 }, font], accessibilityLiveRegion: "polite", children: t.assistant.thinking }) : null,
1156
+ assistantError ? /* @__PURE__ */ jsx(Text, { style: [{ color: colors.error, marginTop: 8 }, font], accessibilityRole: "alert", children: assistantError }) : null,
813
1157
  /* @__PURE__ */ jsxs(View, { style: s.assistantComposer, children: [
814
1158
  /* @__PURE__ */ jsx(
815
1159
  TextInput,
816
1160
  {
817
- style: [s.input, { flex: 1, minHeight: 44, backgroundColor: colors.card, color: colors.text, borderColor: colors.border }],
1161
+ style: [s.input, { flex: 1, minHeight: 44, backgroundColor: colors.surface, color: colors.fg, borderColor: colors.border, borderRadius: radius }, font],
818
1162
  placeholder: t.assistant.inputPlaceholder,
819
- placeholderTextColor: colors.sub,
1163
+ placeholderTextColor: colors.muted,
820
1164
  value: assistantInput,
821
1165
  onChangeText: setAssistantInput,
822
1166
  editable: !assistantSending
@@ -825,45 +1169,19 @@ var MushiBottomSheet = ({
825
1169
  /* @__PURE__ */ jsx(
826
1170
  TouchableOpacity,
827
1171
  {
828
- style: [s.submitBtn, { backgroundColor: colors.accent, paddingHorizontal: 16 }],
1172
+ style: [s.submitBtn, { backgroundColor: colors.accent, paddingHorizontal: 16, borderRadius: radius }],
829
1173
  onPress: () => void sendAssistant(assistantInput),
830
1174
  disabled: !assistantInput.trim() || assistantSending,
831
- children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: "\u2191" })
1175
+ accessibilityRole: "button",
1176
+ accessibilityLabel: t.flows.thread.send,
1177
+ accessibilityState: { disabled: !assistantInput.trim() || assistantSending, busy: assistantSending },
1178
+ children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentFg }], children: "\u2191" })
832
1179
  }
833
1180
  )
834
1181
  ] })
835
- ] }) : 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: [
836
- /* @__PURE__ */ jsx(TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ jsxs(Text, { style: { color: colors.accent, marginBottom: 8 }, children: [
837
- "\u2190 ",
838
- t.widget.back
839
- ] }) }),
840
- threadComments.map((c) => /* @__PURE__ */ jsxs(View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
841
- /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
842
- /* @__PURE__ */ jsx(Text, { style: { color: colors.text }, children: c.body })
843
- ] }, c.id)),
844
- VERIFIABLE_STATUSES.has(
845
- inboxReports.find((r) => r.id === selectedReportId)?.status ?? ""
846
- ) && /* @__PURE__ */ jsxs(View, { style: s.verifyRow, children: [
847
- /* @__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 }) }),
848
- /* @__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 }) })
849
- ] }),
850
- /* @__PURE__ */ jsx(
851
- TextInput,
852
- {
853
- style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
854
- placeholder: t.flows.thread.replyPlaceholder,
855
- placeholderTextColor: colors.sub,
856
- value: replyText,
857
- onChangeText: setReplyText
858
- }
859
- ),
860
- /* @__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 }) })
861
- ] }) : inboxReports.map((r) => /* @__PURE__ */ jsxs(TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
862
- /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
863
- /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: reporterStatusShort(r.status) })
864
- ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
1182
+ ] }) : sheetTab === "inbox" ? /* @__PURE__ */ jsx(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", contentContainerStyle: { paddingBottom: 24 }, children: selectedReportId ? renderThread() : renderInboxList() }) : phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, accessibilityLiveRegion: "polite", children: [
865
1183
  /* @__PURE__ */ jsx(Text, { style: [s.sentEmoji], children: "\u2705" }),
866
- /* @__PURE__ */ jsx(Text, { style: [s.sentText, { color: colors.text }], children: t.widget.submitted })
1184
+ /* @__PURE__ */ jsx(Text, { style: [s.sentText, { color: colors.fg }, font], children: t.widget.submitted })
867
1185
  ] }) : /* @__PURE__ */ jsxs(
868
1186
  ScrollView,
869
1187
  {
@@ -871,29 +1189,34 @@ var MushiBottomSheet = ({
871
1189
  keyboardShouldPersistTaps: "handled",
872
1190
  contentContainerStyle: { paddingBottom: 24 },
873
1191
  children: [
874
- /* @__PURE__ */ jsx(Text, { style: [s.stepLabel, { color: colors.sub }], children: t.step1.heading }),
875
- /* @__PURE__ */ jsx(View, { style: s.catRow, children: CATEGORY_KEYS.map((key) => {
1192
+ /* @__PURE__ */ jsx(Text, { style: [s.stepLabel, { color: colors.muted }, font], children: t.step1.heading }),
1193
+ /* @__PURE__ */ jsx(View, { style: s.catRow, accessibilityRole: "radiogroup", children: CATEGORY_KEYS.map((key) => {
876
1194
  const active = category === key;
877
1195
  return /* @__PURE__ */ jsxs(
878
1196
  TouchableOpacity,
879
1197
  {
880
1198
  onPress: () => setCategory(key),
881
1199
  activeOpacity: 0.7,
1200
+ accessibilityRole: "radio",
1201
+ accessibilityState: { selected: active, checked: active },
1202
+ accessibilityLabel: t.step1.categories[key],
882
1203
  style: [
883
1204
  s.catBtn,
884
1205
  {
885
- backgroundColor: active ? colors.accent : colors.card,
1206
+ backgroundColor: active ? colors.accent : colors.surface,
886
1207
  borderColor: active ? colors.accent : colors.border
887
1208
  }
888
1209
  ],
889
1210
  children: [
890
- /* @__PURE__ */ jsx(Text, { style: s.catEmoji, children: CATEGORY_EMOJI[key] }),
1211
+ /* @__PURE__ */ jsx(Text, { style: s.catEmoji, importantForAccessibility: "no", children: CATEGORY_EMOJI[key] }),
891
1212
  /* @__PURE__ */ jsx(
892
1213
  Text,
893
1214
  {
1215
+ numberOfLines: 1,
894
1216
  style: [
895
1217
  s.catLabel,
896
- { color: active ? colors.accentInk : colors.text }
1218
+ { color: active ? colors.accentFg : colors.fg },
1219
+ font
897
1220
  ],
898
1221
  children: t.step1.categories[key]
899
1222
  }
@@ -909,13 +1232,16 @@ var MushiBottomSheet = ({
909
1232
  style: [
910
1233
  s.input,
911
1234
  {
912
- backgroundColor: colors.card,
913
- color: colors.text,
914
- borderColor: colors.border
915
- }
1235
+ backgroundColor: colors.surface,
1236
+ color: colors.fg,
1237
+ borderColor: colors.border,
1238
+ borderRadius: radius
1239
+ },
1240
+ font
916
1241
  ],
917
1242
  placeholder: t.step3.descriptionPlaceholder,
918
- placeholderTextColor: colors.sub,
1243
+ placeholderTextColor: colors.muted,
1244
+ accessibilityLabel: t.step3.descriptionPlaceholder,
919
1245
  multiline: true,
920
1246
  textAlignVertical: "top",
921
1247
  value: description,
@@ -928,19 +1254,19 @@ var MushiBottomSheet = ({
928
1254
  maxLength: 4e3
929
1255
  }
930
1256
  ),
931
- submitError ? /* @__PURE__ */ jsx(Text, { style: { color: colors.accent, marginTop: 6, fontSize: 12 }, children: submitError }) : null,
1257
+ submitError ? /* @__PURE__ */ jsx(Text, { style: [{ color: colors.error, marginTop: 6, fontSize: 12 }, font], accessibilityRole: "alert", children: submitError }) : null,
932
1258
  activeScreenshot ? /* @__PURE__ */ jsxs(View, { style: s.screenshotRow, children: [
933
1259
  /* @__PURE__ */ jsx(
934
1260
  Image,
935
1261
  {
936
1262
  source: { uri: activeScreenshot },
937
- style: [s.screenshotThumb, { backgroundColor: colors.text }],
1263
+ style: [s.screenshotThumb, { backgroundColor: colors.fg }],
938
1264
  accessibilityLabel: t.step3.screenshotPreviewAlt
939
1265
  }
940
1266
  ),
941
1267
  /* @__PURE__ */ jsxs(View, { style: s.screenshotMeta, children: [
942
- /* @__PURE__ */ jsx(Text, { style: [s.screenshotLabel, { color: colors.text }], children: t.step3.screenshotAttached.replace(" \u2713", "") }),
943
- /* @__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 })
1268
+ /* @__PURE__ */ jsx(Text, { style: [s.screenshotLabel, { color: colors.fg }, font], children: t.step3.screenshotAttached.replace(" \u2713", "") }),
1269
+ /* @__PURE__ */ jsx(Text, { style: [s.screenshotSub, { color: colors.muted }, font], children: screenshotSensitiveHint && screenshotSensitiveHint.trim() ? `\u26A0 ${screenshotSensitiveHint}` : t.step3.screenshotSensitiveHint.split("\u2014")[0]?.trim() ?? t.step3.screenshotSensitiveHint })
944
1270
  ] }),
945
1271
  /* @__PURE__ */ jsx(
946
1272
  TouchableOpacity,
@@ -950,22 +1276,28 @@ var MushiBottomSheet = ({
950
1276
  onClearScreenshot?.();
951
1277
  },
952
1278
  hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
1279
+ accessibilityRole: "button",
953
1280
  accessibilityLabel: t.widget.close,
954
- children: /* @__PURE__ */ jsx(Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
1281
+ children: /* @__PURE__ */ jsx(Text, { style: [s.screenshotRemove, { color: colors.muted }], children: "\u2715" })
955
1282
  }
956
1283
  )
957
1284
  ] }) : null,
1285
+ submitHint ? /* @__PURE__ */ jsx(Text, { style: [{ color: colors.muted, fontSize: 12, marginBottom: 6 }, font], children: submitHint }) : null,
958
1286
  /* @__PURE__ */ jsx(
959
1287
  TouchableOpacity,
960
1288
  {
961
1289
  onPress: handleSubmit,
962
1290
  disabled: !canSubmit,
963
1291
  activeOpacity: 0.8,
1292
+ accessibilityRole: "button",
1293
+ accessibilityLabel: phase === "sending" ? t.widget.submitting : t.widget.submit,
1294
+ accessibilityHint: submitHint,
1295
+ accessibilityState: { disabled: !canSubmit, busy: phase === "sending" },
964
1296
  style: [
965
1297
  s.submitBtn,
966
- { backgroundColor: canSubmit ? colors.accent : colors.disabled }
1298
+ { backgroundColor: canSubmit ? colors.accent : colors.disabled, borderRadius: radius }
967
1299
  ],
968
- children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: canSubmit ? colors.accentInk : colors.disabledText }], children: phase === "sending" ? t.widget.submitting : t.widget.submit })
1300
+ children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: canSubmit ? colors.accentFg : colors.disabledFg }, font], children: phase === "sending" ? t.widget.submitting : t.widget.submit })
969
1301
  }
970
1302
  )
971
1303
  ]
@@ -995,8 +1327,8 @@ var s = StyleSheet.create({
995
1327
  left: 0,
996
1328
  right: 0,
997
1329
  minHeight: SHEET_HEIGHT,
998
- borderTopLeftRadius: MUSHI_RADIUS.sheet,
999
- borderTopRightRadius: MUSHI_RADIUS.sheet,
1330
+ borderTopLeftRadius: MUSHI_RADIUS2.sheet,
1331
+ borderTopRightRadius: MUSHI_RADIUS2.sheet,
1000
1332
  paddingBottom: Platform2.OS === "ios" ? 34 : MUSHI_SPACING.roomy,
1001
1333
  shadowColor: MUSHI_SHADOW_INK,
1002
1334
  shadowOffset: { width: 0, height: -3 },
@@ -1011,36 +1343,22 @@ var s = StyleSheet.create({
1011
1343
  handle: {
1012
1344
  width: 40,
1013
1345
  height: 5,
1014
- borderRadius: MUSHI_RADIUS.control,
1346
+ borderRadius: MUSHI_RADIUS2.control,
1015
1347
  opacity: 0.5
1016
1348
  },
1017
- brandHeader: {
1349
+ header: {
1018
1350
  paddingHorizontal: MUSHI_SPACING.lounge,
1019
- paddingTop: MUSHI_SPACING.comfy,
1020
- paddingBottom: MUSHI_SPACING.comfy,
1021
- borderBottomWidth: 1.5
1351
+ paddingBottom: MUSHI_SPACING.comfy
1022
1352
  },
1023
- brandEyebrow: {
1024
- fontSize: MUSHI_TYPE.sizeLabel,
1025
- fontWeight: "700",
1026
- letterSpacing: 1.2,
1027
- marginBottom: 2,
1028
- opacity: 0.7
1029
- },
1030
- brandTitle: {
1353
+ headerTitle: {
1031
1354
  fontSize: 18,
1032
- fontWeight: "800",
1355
+ fontWeight: "700",
1033
1356
  letterSpacing: -0.2
1034
1357
  },
1035
1358
  body: {
1036
1359
  paddingHorizontal: MUSHI_SPACING.lounge,
1037
1360
  paddingTop: MUSHI_SPACING.tight
1038
1361
  },
1039
- title: {
1040
- fontSize: 20,
1041
- fontWeight: "700",
1042
- marginBottom: MUSHI_SPACING.roomy
1043
- },
1044
1362
  stepLabel: {
1045
1363
  fontSize: MUSHI_TYPE.sizeLabel,
1046
1364
  fontWeight: "600",
@@ -1051,37 +1369,35 @@ var s = StyleSheet.create({
1051
1369
  },
1052
1370
  catRow: {
1053
1371
  flexDirection: "row",
1054
- justifyContent: "space-between",
1372
+ flexWrap: "wrap",
1373
+ gap: 8,
1055
1374
  marginBottom: MUSHI_SPACING.roomy
1056
1375
  },
1057
1376
  catBtn: {
1377
+ flexDirection: "row",
1058
1378
  alignItems: "center",
1059
- paddingVertical: MUSHI_SPACING.comfy,
1060
- paddingHorizontal: 6,
1061
- borderRadius: MUSHI_RADIUS.card,
1379
+ gap: 6,
1380
+ paddingVertical: 8,
1381
+ paddingHorizontal: 12,
1382
+ borderRadius: 999,
1062
1383
  borderWidth: 1,
1063
- flex: 1,
1064
- marginHorizontal: 3,
1065
1384
  minHeight: 44
1066
1385
  },
1067
1386
  catEmoji: {
1068
- fontSize: 22,
1069
- marginBottom: MUSHI_SPACING.tight
1387
+ fontSize: 16
1070
1388
  },
1071
1389
  catLabel: {
1072
- fontSize: MUSHI_TYPE.sizeLabel,
1390
+ fontSize: 14,
1073
1391
  fontWeight: "600"
1074
1392
  },
1075
1393
  input: {
1076
1394
  borderWidth: 1,
1077
- borderRadius: MUSHI_RADIUS.card,
1078
1395
  padding: 14,
1079
1396
  fontSize: 15,
1080
1397
  minHeight: 100,
1081
1398
  marginBottom: MUSHI_SPACING.roomy
1082
1399
  },
1083
1400
  submitBtn: {
1084
- borderRadius: MUSHI_RADIUS.card,
1085
1401
  paddingVertical: 14,
1086
1402
  alignItems: "center",
1087
1403
  minHeight: 44
@@ -1146,7 +1462,8 @@ var s = StyleSheet.create({
1146
1462
  alignItems: "center",
1147
1463
  paddingVertical: 10,
1148
1464
  borderBottomWidth: 2,
1149
- borderBottomColor: "transparent"
1465
+ borderBottomColor: "transparent",
1466
+ minHeight: 44
1150
1467
  },
1151
1468
  tabLabel: {
1152
1469
  fontSize: 14,
@@ -1154,12 +1471,47 @@ var s = StyleSheet.create({
1154
1471
  },
1155
1472
  inboxRow: {
1156
1473
  paddingVertical: 12,
1474
+ minHeight: 56,
1157
1475
  borderBottomWidth: StyleSheet.hairlineWidth
1158
1476
  },
1477
+ headerCard: {
1478
+ borderWidth: StyleSheet.hairlineWidth,
1479
+ padding: 12,
1480
+ marginBottom: 12
1481
+ },
1482
+ pill: {
1483
+ alignSelf: "flex-start",
1484
+ borderWidth: 1,
1485
+ borderRadius: 999,
1486
+ paddingHorizontal: 8,
1487
+ paddingVertical: 2,
1488
+ fontSize: 12,
1489
+ fontWeight: "600",
1490
+ overflow: "hidden"
1491
+ },
1492
+ skeleton: {
1493
+ height: 14,
1494
+ borderRadius: 6,
1495
+ marginBottom: 10
1496
+ },
1497
+ errorRow: {
1498
+ flexDirection: "row",
1499
+ alignItems: "center",
1500
+ gap: 8,
1501
+ marginBottom: 12
1502
+ },
1503
+ retryBtn: {
1504
+ borderWidth: 1,
1505
+ paddingHorizontal: 12,
1506
+ paddingVertical: 8,
1507
+ minHeight: 44,
1508
+ justifyContent: "center"
1509
+ },
1159
1510
  threadBubble: {
1160
1511
  borderRadius: 10,
1161
1512
  padding: 10,
1162
- marginBottom: 8
1513
+ marginBottom: 8,
1514
+ maxWidth: "88%"
1163
1515
  },
1164
1516
  verifyRow: {
1165
1517
  flexDirection: "row",
@@ -1168,9 +1520,26 @@ var s = StyleSheet.create({
1168
1520
  },
1169
1521
  verifyBtn: {
1170
1522
  flex: 1,
1171
- borderRadius: 10,
1172
1523
  paddingVertical: 10,
1173
- alignItems: "center"
1524
+ alignItems: "center",
1525
+ minHeight: 44
1526
+ },
1527
+ composer: {
1528
+ flexDirection: "row",
1529
+ alignItems: "flex-end",
1530
+ gap: 8
1531
+ },
1532
+ composerInput: {
1533
+ flex: 1,
1534
+ minHeight: 44,
1535
+ maxHeight: 120,
1536
+ marginBottom: 0
1537
+ },
1538
+ sendBtn: {
1539
+ paddingHorizontal: 16,
1540
+ paddingVertical: 12,
1541
+ minHeight: 44,
1542
+ justifyContent: "center"
1174
1543
  },
1175
1544
  assistantBubble: {
1176
1545
  maxWidth: "88%",
@@ -1400,7 +1769,7 @@ import {
1400
1769
  StyleSheet as StyleSheet3,
1401
1770
  Platform as Platform4
1402
1771
  } from "react-native";
1403
- 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";
1772
+ 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";
1404
1773
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1405
1774
  var MUSHI_BANNER_DEFAULT_HEIGHT = MUSHI_GEOMETRY2.bannerHeight;
1406
1775
  var MushiBanner = ({
@@ -1453,9 +1822,9 @@ var s3 = StyleSheet3.create({
1453
1822
  minHeight: MUSHI_GEOMETRY2.bannerHeight,
1454
1823
  flexDirection: "row",
1455
1824
  alignItems: "stretch",
1456
- backgroundColor: MUSHI_BANNER_NEON2.bg,
1825
+ backgroundColor: MUSHI_BANNER_NEON.bg,
1457
1826
  borderTopWidth: 1,
1458
- borderTopColor: MUSHI_BANNER_NEON2.border,
1827
+ borderTopColor: MUSHI_BANNER_NEON.border,
1459
1828
  zIndex: MUSHI_Z.banner,
1460
1829
  elevation: 8,
1461
1830
  paddingBottom: Platform4.OS === "ios" ? 0 : 0
@@ -1473,18 +1842,18 @@ var s3 = StyleSheet3.create({
1473
1842
  fontSize: MUSHI_TYPE2.sizeLabel,
1474
1843
  fontWeight: "700",
1475
1844
  letterSpacing: 1.2,
1476
- color: MUSHI_BANNER_NEON2.fg
1845
+ color: MUSHI_BANNER_NEON.fg
1477
1846
  },
1478
1847
  message: {
1479
1848
  flex: 1,
1480
1849
  fontSize: 12,
1481
1850
  fontWeight: "500",
1482
- color: MUSHI_BANNER_NEON2.fg
1851
+ color: MUSHI_BANNER_NEON.fg
1483
1852
  },
1484
1853
  cta: {
1485
1854
  fontSize: 12,
1486
1855
  fontWeight: "700",
1487
- color: MUSHI_BANNER_NEON2.fg,
1856
+ color: MUSHI_BANNER_NEON.fg,
1488
1857
  textDecorationLine: "underline"
1489
1858
  },
1490
1859
  dismiss: {
@@ -1494,18 +1863,346 @@ var s3 = StyleSheet3.create({
1494
1863
  dismissText: {
1495
1864
  fontSize: 20,
1496
1865
  lineHeight: 22,
1497
- color: MUSHI_BANNER_NEON2.fg,
1866
+ color: MUSHI_BANNER_NEON.fg,
1498
1867
  opacity: 0.7
1499
1868
  }
1500
1869
  });
1501
1870
 
1871
+ // src/analytics/event-tracker.ts
1872
+ import {
1873
+ EVENT_PROPERTY_LIMITS,
1874
+ isValidEventName,
1875
+ propertiesWithinByteLimit,
1876
+ sanitizeEventProperties
1877
+ } from "@mushi-mushi/core";
1878
+ var CONSENT_BUFFER_MAX = 50;
1879
+ var SPILL_MAX = 200;
1880
+ var SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
1881
+ var SURFACE = "mobile";
1882
+ async function defaultStorage() {
1883
+ try {
1884
+ const mod = await import("@react-native-async-storage/async-storage");
1885
+ return mod.default;
1886
+ } catch {
1887
+ return null;
1888
+ }
1889
+ }
1890
+ function createRNEventTracker(opts) {
1891
+ const cfg = opts.config ?? {};
1892
+ const enabled = cfg.enabled !== false;
1893
+ const consentMode = cfg.consent ?? "implied";
1894
+ const allowlist = cfg.propertyAllowlist ?? [];
1895
+ const spillKey = `@mushi:events_spill_${opts.projectId}`;
1896
+ const consentKey = `@mushi:analytics_consent_${opts.projectId}`;
1897
+ let buffer = [];
1898
+ let consentBuffer = [];
1899
+ let consent = consentMode === "required" ? "pending" : "granted";
1900
+ let userId = null;
1901
+ let userTraits = null;
1902
+ let pendingIdentify = false;
1903
+ let flushing = false;
1904
+ let destroyed = false;
1905
+ let timer = null;
1906
+ let storage = null;
1907
+ const consentListeners = /* @__PURE__ */ new Set();
1908
+ const loadStorage = opts.storage ?? defaultStorage;
1909
+ const resolveClient = () => typeof opts.client === "function" ? opts.client() : opts.client;
1910
+ async function readSpill() {
1911
+ if (!storage) return [];
1912
+ try {
1913
+ const raw = await storage.getItem(spillKey);
1914
+ if (!raw) return [];
1915
+ const parsed = JSON.parse(raw);
1916
+ if (!parsed || Date.now() - parsed.at > SPILL_TTL_MS) {
1917
+ await storage.removeItem(spillKey);
1918
+ return [];
1919
+ }
1920
+ return Array.isArray(parsed.events) ? parsed.events.slice(0, SPILL_MAX) : [];
1921
+ } catch {
1922
+ return [];
1923
+ }
1924
+ }
1925
+ async function writeSpill(events) {
1926
+ if (!storage) return;
1927
+ try {
1928
+ if (events.length === 0) await storage.removeItem(spillKey);
1929
+ else await storage.setItem(spillKey, JSON.stringify({ at: Date.now(), events: events.slice(-SPILL_MAX) }));
1930
+ } catch {
1931
+ }
1932
+ }
1933
+ async function flushNow() {
1934
+ if (destroyed && buffer.length === 0) return;
1935
+ await ready;
1936
+ if (flushing || consent !== "granted") return;
1937
+ if (buffer.length === 0 && !pendingIdentify) return;
1938
+ const client = resolveClient();
1939
+ if (!client) return;
1940
+ flushing = true;
1941
+ const batch = buffer.splice(0, EVENT_PROPERTY_LIMITS.maxServerBatch);
1942
+ if (pendingIdentify) {
1943
+ batch.unshift({ name: "identify", ts: (/* @__PURE__ */ new Date()).toISOString(), properties: {} });
1944
+ pendingIdentify = false;
1945
+ }
1946
+ try {
1947
+ const anonId = await Promise.resolve(opts.getAnonId());
1948
+ const payload = {
1949
+ anon_id: anonId,
1950
+ user_id: userId ?? opts.getUserId?.() ?? null,
1951
+ user_traits: userTraits ?? opts.getUserTraits?.() ?? null,
1952
+ session_id: null,
1953
+ sdk_version: opts.sdkVersion ?? null,
1954
+ surface: SURFACE,
1955
+ events: batch
1956
+ };
1957
+ const res = await client.postProductEvents(payload);
1958
+ if (!res.ok) await writeSpill([...await readSpill(), ...batch.filter((e) => e.name !== "identify")]);
1959
+ else await writeSpill([]);
1960
+ } catch {
1961
+ await writeSpill([...await readSpill(), ...batch.filter((e) => e.name !== "identify")]);
1962
+ } finally {
1963
+ flushing = false;
1964
+ }
1965
+ if (buffer.length >= EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
1966
+ }
1967
+ function enqueue(ev) {
1968
+ if (consent === "denied") return;
1969
+ if (consent === "pending") {
1970
+ if (consentBuffer.length < CONSENT_BUFFER_MAX) consentBuffer.push(ev);
1971
+ return;
1972
+ }
1973
+ buffer.push(ev);
1974
+ if (buffer.length >= EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
1975
+ }
1976
+ const ready = enabled ? (async () => {
1977
+ storage = await loadStorage();
1978
+ if (destroyed) return;
1979
+ let stored = null;
1980
+ try {
1981
+ stored = storage ? await storage.getItem(consentKey) : null;
1982
+ } catch {
1983
+ stored = null;
1984
+ }
1985
+ if (stored === "denied") {
1986
+ consent = "denied";
1987
+ buffer = [];
1988
+ consentBuffer = [];
1989
+ pendingIdentify = false;
1990
+ await writeSpill([]);
1991
+ return;
1992
+ }
1993
+ if (stored === "granted" && consent === "pending") {
1994
+ consent = "granted";
1995
+ buffer.push(...consentBuffer);
1996
+ consentBuffer = [];
1997
+ }
1998
+ const spill = await readSpill();
1999
+ if (spill.length > 0) {
2000
+ buffer.unshift(...spill);
2001
+ await writeSpill([]);
2002
+ }
2003
+ })().catch(() => void 0) : Promise.resolve();
2004
+ if (enabled) {
2005
+ const interval = Math.max(1e3, cfg.flushIntervalMs ?? 5e3);
2006
+ timer = setInterval(() => {
2007
+ void flushNow();
2008
+ }, interval);
2009
+ }
2010
+ return {
2011
+ ready,
2012
+ track(name, properties, trackOpts) {
2013
+ if (!enabled || destroyed) return false;
2014
+ if (!isValidEventName(name)) return false;
2015
+ const { properties: props } = sanitizeEventProperties(properties, { allowlist, scrub: opts.scrub });
2016
+ let merged = props;
2017
+ if (trackOpts?.reserved) {
2018
+ const { properties: res } = sanitizeEventProperties(trackOpts.reserved, { allowReserved: true, scrub: opts.scrub });
2019
+ merged = { ...props, ...res };
2020
+ }
2021
+ merged.$surface = SURFACE;
2022
+ const route = opts.getRoute?.();
2023
+ if (route && merged.$route === void 0) merged.$route = route;
2024
+ if (!propertiesWithinByteLimit(merged)) return false;
2025
+ enqueue({
2026
+ name,
2027
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2028
+ properties: merged,
2029
+ ...trackOpts?.dedupKey ? { dedup_key: trackOpts.dedupKey } : {}
2030
+ });
2031
+ return true;
2032
+ },
2033
+ setConsent(state) {
2034
+ void ready.then(() => storage?.setItem(consentKey, state)).catch(() => void 0);
2035
+ if (state === "denied") {
2036
+ consent = "denied";
2037
+ buffer = [];
2038
+ consentBuffer = [];
2039
+ pendingIdentify = false;
2040
+ void writeSpill([]);
2041
+ } else {
2042
+ const wasPending = consent === "pending";
2043
+ consent = "granted";
2044
+ if (wasPending && consentBuffer.length > 0) {
2045
+ buffer.push(...consentBuffer);
2046
+ consentBuffer = [];
2047
+ }
2048
+ if (buffer.length > 0 || pendingIdentify) void flushNow();
2049
+ }
2050
+ for (const listener of [...consentListeners]) {
2051
+ try {
2052
+ listener(state);
2053
+ } catch {
2054
+ }
2055
+ }
2056
+ },
2057
+ setIdentity(id, traits) {
2058
+ userId = id;
2059
+ userTraits = traits ?? null;
2060
+ if (id && enabled && consent !== "denied") {
2061
+ pendingIdentify = true;
2062
+ void flushNow();
2063
+ }
2064
+ },
2065
+ consentState() {
2066
+ return enabled ? consent : "denied";
2067
+ },
2068
+ onConsentChange(listener) {
2069
+ consentListeners.add(listener);
2070
+ return () => {
2071
+ consentListeners.delete(listener);
2072
+ };
2073
+ },
2074
+ async flush() {
2075
+ await ready;
2076
+ await flushNow();
2077
+ },
2078
+ async destroy() {
2079
+ if (timer != null) {
2080
+ clearInterval(timer);
2081
+ timer = null;
2082
+ }
2083
+ await flushNow().catch(() => void 0);
2084
+ destroyed = true;
2085
+ buffer = [];
2086
+ consentBuffer = [];
2087
+ pendingIdentify = false;
2088
+ }
2089
+ };
2090
+ }
2091
+
2092
+ // src/analytics/session-tracker.ts
2093
+ import { newUuid } from "@mushi-mushi/core";
2094
+ var HEARTBEAT_INTERVAL_MS = 6e4;
2095
+ var RESUME_WINDOW_MS = 30 * 6e4;
2096
+ function createRNSessionTracker(opts) {
2097
+ const clock = opts.now ?? Date.now;
2098
+ const resolveClient = () => typeof opts.client === "function" ? opts.client() : opts.client;
2099
+ let sessionId = opts.sessionId ?? `sess_${newUuid()}`;
2100
+ let pageViewCount = 0;
2101
+ let started = false;
2102
+ let allowed = false;
2103
+ let foreground = true;
2104
+ let destroyed = false;
2105
+ let backgroundedAt = null;
2106
+ let heartbeat = null;
2107
+ function route() {
2108
+ return opts.getRoute?.() ?? null;
2109
+ }
2110
+ function send(kind, extra) {
2111
+ const client = resolveClient();
2112
+ if (!client || !allowed || destroyed) return;
2113
+ const base = {
2114
+ kind,
2115
+ session_id: sessionId,
2116
+ ts: new Date(clock()).toISOString(),
2117
+ page_view_count: pageViewCount,
2118
+ user_agent: opts.userAgent ?? null,
2119
+ sdk_version: opts.sdkVersion
2120
+ };
2121
+ void Promise.resolve(opts.getReporterToken()).then((token) => client.postSessionEvent({ ...base, reporter_token_hash: token, ...extra })).catch(() => void 0);
2122
+ }
2123
+ function stopHeartbeat() {
2124
+ if (heartbeat != null) {
2125
+ clearInterval(heartbeat);
2126
+ heartbeat = null;
2127
+ }
2128
+ }
2129
+ function startHeartbeat() {
2130
+ stopHeartbeat();
2131
+ heartbeat = setInterval(() => send("session_heartbeat", { route: route() }), HEARTBEAT_INTERVAL_MS);
2132
+ }
2133
+ function startSession() {
2134
+ started = true;
2135
+ pageViewCount = 1;
2136
+ send("session_start", { route: route() });
2137
+ startHeartbeat();
2138
+ }
2139
+ function activate() {
2140
+ if (allowed || destroyed) return;
2141
+ allowed = true;
2142
+ if (!foreground) return;
2143
+ if (!started) startSession();
2144
+ else startHeartbeat();
2145
+ }
2146
+ function deactivate() {
2147
+ allowed = false;
2148
+ stopHeartbeat();
2149
+ }
2150
+ const unsubscribe = opts.consent.onConsentChange(() => {
2151
+ if (opts.consent.consentState() === "granted") activate();
2152
+ else deactivate();
2153
+ });
2154
+ void opts.consent.ready.then(() => {
2155
+ if (opts.consent.consentState() === "granted") activate();
2156
+ }).catch(() => void 0);
2157
+ return {
2158
+ onAppStateChange(state) {
2159
+ if (destroyed) return;
2160
+ if (state === "active") {
2161
+ if (foreground) return;
2162
+ foreground = true;
2163
+ const away = backgroundedAt == null ? 0 : clock() - backgroundedAt;
2164
+ backgroundedAt = null;
2165
+ if (!allowed) return;
2166
+ if (!started || away >= RESUME_WINDOW_MS) {
2167
+ sessionId = `sess_${newUuid()}`;
2168
+ startSession();
2169
+ } else {
2170
+ send("session_heartbeat", { route: route() });
2171
+ startHeartbeat();
2172
+ }
2173
+ return;
2174
+ }
2175
+ if (state !== "background" || !foreground) return;
2176
+ foreground = false;
2177
+ backgroundedAt = clock();
2178
+ stopHeartbeat();
2179
+ if (started) send("session_end", { route: route() });
2180
+ },
2181
+ pageView(next) {
2182
+ if (!allowed || !started || !foreground) return;
2183
+ pageViewCount += 1;
2184
+ send("page_view", { route: next });
2185
+ },
2186
+ destroy() {
2187
+ destroyed = true;
2188
+ deactivate();
2189
+ unsubscribe();
2190
+ }
2191
+ };
2192
+ }
2193
+
1502
2194
  // src/version.ts
1503
2195
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1504
- var MUSHI_SDK_VERSION = "0.21.0" ? "0.21.0" : "0.17.0";
2196
+ var MUSHI_SDK_VERSION = "0.23.0" ? "0.23.0" : "0.17.0";
1505
2197
 
1506
2198
  // src/provider.tsx
1507
2199
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1508
2200
  var MushiContext = createContext(null);
2201
+ function nativeUserAgent() {
2202
+ const os = Platform5?.OS ?? "native";
2203
+ const version = Platform5?.Version;
2204
+ return `${MUSHI_SDK_PACKAGE}/${MUSHI_SDK_VERSION} (${version != null ? `${os} ${String(version)}` : os})`;
2205
+ }
1509
2206
  var DEFAULT_SCREENSHOT_HINT = "Check the preview \u2014 remove it if any private info (balances, personal details) is visible.";
1510
2207
  function resolveScreenshotHint(v) {
1511
2208
  if (v === false) return null;
@@ -1548,7 +2245,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1548
2245
  const networkRef = useRef3(null);
1549
2246
  const queueRef = useRef3(null);
1550
2247
  const apiClientRef = useRef3(null);
1551
- const sessionIdRef = useRef3(`sess_${newUuid()}`);
2248
+ const sessionIdRef = useRef3(`sess_${newUuid2()}`);
1552
2249
  const breadcrumbsRef = useRef3(
1553
2250
  createBreadcrumbBuffer({ max: config.capture?.maxBreadcrumbs ?? 50 })
1554
2251
  );
@@ -1620,14 +2317,14 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1620
2317
  if (existing && existing.startsWith("mushi_")) {
1621
2318
  reporterTokenRef.current = existing;
1622
2319
  } else {
1623
- const fresh = `mushi_${newUuid()}`;
2320
+ const fresh = `mushi_${newUuid2()}`;
1624
2321
  reporterTokenRef.current = fresh;
1625
2322
  await saveReporterToken(fresh, secureStorage);
1626
2323
  }
1627
2324
  resolveReporterTokenReady();
1628
2325
  } catch {
1629
2326
  if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
1630
- reporterTokenRef.current = `mushi_${newUuid()}`;
2327
+ reporterTokenRef.current = `mushi_${newUuid2()}`;
1631
2328
  }
1632
2329
  resolveReporterTokenReady();
1633
2330
  }
@@ -1756,7 +2453,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1756
2453
  }));
1757
2454
  const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
1758
2455
  const report = {
1759
- id: newUuid(),
2456
+ id: newUuid2(),
1760
2457
  projectId: config.projectId,
1761
2458
  category: data.category,
1762
2459
  description: scrubPii3(data.description),
@@ -1848,6 +2545,63 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1848
2545
  if (rewardsFlushRef.current) clearInterval(rewardsFlushRef.current);
1849
2546
  };
1850
2547
  }, [config.rewards?.enabled, config.rewards?.trackActivity, config.rewards?.flushIntervalMs]);
2548
+ const eventTrackerRef = useRef3(null);
2549
+ const sessionTrackerRef = useRef3(null);
2550
+ useEffect3(() => {
2551
+ const getRoute = () => screenRef.current?.route ?? screenRef.current?.name ?? null;
2552
+ const tracker = createRNEventTracker({
2553
+ projectId: config.projectId,
2554
+ client: () => apiClientRef.current,
2555
+ getAnonId: async () => {
2556
+ await reporterTokenReadyRef.current;
2557
+ return reporterTokenRef.current;
2558
+ },
2559
+ getUserId: () => userRef.current?.id ?? null,
2560
+ getRoute,
2561
+ sdkVersion: MUSHI_SDK_VERSION,
2562
+ config: config.analytics,
2563
+ scrub: scrubPii3
2564
+ });
2565
+ eventTrackerRef.current = tracker;
2566
+ const sessions = config.trackSessions === false ? null : createRNSessionTracker({
2567
+ client: () => apiClientRef.current,
2568
+ getReporterToken: async () => {
2569
+ await reporterTokenReadyRef.current;
2570
+ return reporterTokenRef.current;
2571
+ },
2572
+ getRoute,
2573
+ consent: tracker,
2574
+ sessionId: sessionIdRef.current,
2575
+ sdkVersion: MUSHI_SDK_VERSION,
2576
+ userAgent: nativeUserAgent()
2577
+ });
2578
+ sessionTrackerRef.current = sessions;
2579
+ let sub;
2580
+ try {
2581
+ if (typeof AppState?.addEventListener === "function") {
2582
+ sub = AppState.addEventListener("change", (state) => {
2583
+ sessions?.onAppStateChange(state);
2584
+ if (state !== "active") tracker.flush().catch(() => {
2585
+ });
2586
+ });
2587
+ }
2588
+ } catch {
2589
+ }
2590
+ return () => {
2591
+ sub?.remove();
2592
+ sessions?.destroy();
2593
+ sessionTrackerRef.current = null;
2594
+ eventTrackerRef.current = null;
2595
+ tracker.destroy().catch(() => {
2596
+ });
2597
+ };
2598
+ }, [
2599
+ config.projectId,
2600
+ config.analytics?.enabled,
2601
+ config.analytics?.consent,
2602
+ config.analytics?.flushIntervalMs,
2603
+ config.trackSessions
2604
+ ]);
1851
2605
  const instance = useMemo(
1852
2606
  () => ({
1853
2607
  open,
@@ -1865,6 +2619,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1865
2619
  name: traits?.name,
1866
2620
  provider: traits?.provider
1867
2621
  };
2622
+ eventTrackerRef.current?.setIdentity(userId, traits ?? null);
1868
2623
  },
1869
2624
  setUser(user) {
1870
2625
  userRef.current = {
@@ -1873,6 +2628,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1873
2628
  name: user.name,
1874
2629
  provider: user.provider
1875
2630
  };
2631
+ eventTrackerRef.current?.setIdentity(user.id, { email: user.email, name: user.name, provider: user.provider });
1876
2632
  },
1877
2633
  identifyWithToken(token) {
1878
2634
  userTokenRef.current = token && typeof token === "string" ? token : null;
@@ -1884,7 +2640,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1884
2640
  ...claims.email ? { email: claims.email } : {},
1885
2641
  ...claims.name ? { name: claims.name } : {}
1886
2642
  };
2643
+ eventTrackerRef.current?.setIdentity(claims.sub, null);
1887
2644
  }
2645
+ } else {
2646
+ eventTrackerRef.current?.setIdentity(null, null);
1888
2647
  }
1889
2648
  },
1890
2649
  publishPageContext(context) {
@@ -1897,6 +2656,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1897
2656
  const prev = screenRef.current?.name;
1898
2657
  screenRef.current = screen;
1899
2658
  if (screen.name && screen.name !== prev) {
2659
+ sessionTrackerRef.current?.pageView(screen.route ?? screen.name);
1900
2660
  breadcrumbsRef.current.add({
1901
2661
  category: "navigation",
1902
2662
  level: "info",
@@ -1928,6 +2688,23 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1928
2688
  const res = await client.listReporterComments(reportId, reporterTokenRef.current);
1929
2689
  return res.ok ? res.data.comments ?? [] : [];
1930
2690
  },
2691
+ async loadMyThread(reportId) {
2692
+ const client = apiClientRef.current;
2693
+ if (!client) return null;
2694
+ try {
2695
+ await reporterTokenReadyRef.current;
2696
+ const res = await client.listReporterComments(reportId, reporterTokenRef.current);
2697
+ return res.ok ? res.data?.comments ?? [] : null;
2698
+ } catch {
2699
+ return null;
2700
+ }
2701
+ },
2702
+ async markReportRead(reportId) {
2703
+ const client = apiClientRef.current;
2704
+ if (!client) return 0;
2705
+ await reporterTokenReadyRef.current;
2706
+ return markReporterReportRead(client, reporterTokenRef.current, reportId);
2707
+ },
1931
2708
  async replyToReport(reportId, body) {
1932
2709
  const client = apiClientRef.current;
1933
2710
  if (!client) return null;
@@ -1958,6 +2735,13 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1958
2735
  if (!config.rewards?.enabled) return;
1959
2736
  activityQueueRef.current.push({ action, metadata });
1960
2737
  },
2738
+ // Product analytics (Users & Funnels)
2739
+ track(event, properties) {
2740
+ return eventTrackerRef.current?.track(event, properties) ?? false;
2741
+ },
2742
+ setConsent(state) {
2743
+ eventTrackerRef.current?.setConsent(state);
2744
+ },
1961
2745
  async getTier() {
1962
2746
  const userId = userRef.current?.id;
1963
2747
  const client = apiClientRef.current;
@@ -2028,7 +2812,8 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
2028
2812
  assistantLabel: config.assistant?.label,
2029
2813
  assistantGreeting: config.assistant?.greeting,
2030
2814
  assistantSuggestions: config.assistant?.suggestions,
2031
- inboxPollIntervalMs: config.widget?.inboxPollIntervalMs ?? 0
2815
+ inboxPollIntervalMs: config.widget?.inboxPollIntervalMs ?? 0,
2816
+ theme: config.widget?.theme
2032
2817
  }
2033
2818
  )
2034
2819
  ] });