@bbearai/react-native 0.8.2 → 0.8.4

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
@@ -12230,10 +12230,14 @@ var BugBearClient = class {
12230
12230
  this.navigationHistory = [];
12231
12231
  this.reportSubmitInFlight = false;
12232
12232
  this._queue = null;
12233
+ this._sessionStorage = new LocalStorageAdapter();
12233
12234
  this.realtimeChannels = [];
12234
12235
  this.monitor = null;
12235
12236
  this.initialized = false;
12236
12237
  this.initError = null;
12238
+ this._presenceSessionId = null;
12239
+ this._presenceInterval = null;
12240
+ this._presencePaused = false;
12237
12241
  this.config = config;
12238
12242
  if (config.apiKey) {
12239
12243
  this.pendingInit = this.resolveFromApiKey(config.apiKey);
@@ -12307,6 +12311,10 @@ var BugBearClient = class {
12307
12311
  this.initOfflineQueue();
12308
12312
  this.initMonitoring();
12309
12313
  }
12314
+ /** Cache key scoped to the active project. */
12315
+ get sessionCacheKey() {
12316
+ return `bugbear_session_${this.config.projectId ?? "unknown"}`;
12317
+ }
12310
12318
  /** Initialize offline queue if configured. Shared by both init paths. */
12311
12319
  initOfflineQueue() {
12312
12320
  if (this.config.offlineQueue?.enabled) {
@@ -12370,6 +12378,26 @@ var BugBearClient = class {
12370
12378
  await this.pendingInit;
12371
12379
  if (this.initError) throw this.initError;
12372
12380
  }
12381
+ /**
12382
+ * Fire-and-forget call to a dashboard notification endpoint.
12383
+ * Only works when apiKey is configured (needed for API auth).
12384
+ * Failures are silently ignored — notifications are best-effort.
12385
+ */
12386
+ async notifyDashboard(path, body) {
12387
+ if (!this.config.apiKey) return;
12388
+ try {
12389
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
12390
+ await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
12391
+ method: "POST",
12392
+ headers: {
12393
+ "Content-Type": "application/json",
12394
+ "Authorization": `Bearer ${this.config.apiKey}`
12395
+ },
12396
+ body: JSON.stringify(body)
12397
+ });
12398
+ } catch {
12399
+ }
12400
+ }
12373
12401
  // ── Offline Queue ─────────────────────────────────────────
12374
12402
  /**
12375
12403
  * Access the offline queue (if enabled).
@@ -12396,6 +12424,48 @@ var BugBearClient = class {
12396
12424
  }
12397
12425
  await this._queue.load();
12398
12426
  }
12427
+ // ── Session Cache ──────────────────────────────────────────
12428
+ /**
12429
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
12430
+ * Must be called before getCachedSession() for persistence across app kills.
12431
+ * Web callers don't need this — LocalStorageAdapter is the default.
12432
+ */
12433
+ setSessionStorage(adapter) {
12434
+ this._sessionStorage = adapter;
12435
+ }
12436
+ /**
12437
+ * Cache the active QA session locally for instant restore on app restart.
12438
+ * Pass null to clear the cache (e.g. after ending a session).
12439
+ */
12440
+ async cacheSession(session) {
12441
+ try {
12442
+ if (session) {
12443
+ await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
12444
+ } else {
12445
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
12446
+ }
12447
+ } catch {
12448
+ }
12449
+ }
12450
+ /**
12451
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
12452
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
12453
+ */
12454
+ async getCachedSession() {
12455
+ try {
12456
+ const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
12457
+ if (!raw) return null;
12458
+ const session = JSON.parse(raw);
12459
+ const age = Date.now() - new Date(session.startedAt).getTime();
12460
+ if (age > 24 * 60 * 60 * 1e3) {
12461
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
12462
+ return null;
12463
+ }
12464
+ return session;
12465
+ } catch {
12466
+ return null;
12467
+ }
12468
+ }
12399
12469
  registerQueueHandlers() {
12400
12470
  if (!this._queue) return;
12401
12471
  this._queue.registerHandler("report", async (payload) => {
@@ -12413,6 +12483,11 @@ var BugBearClient = class {
12413
12483
  if (error) return { success: false, error: error.message };
12414
12484
  return { success: true };
12415
12485
  });
12486
+ this._queue.registerHandler("email_capture", async (payload) => {
12487
+ const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
12488
+ if (error) return { success: false, error: error.message };
12489
+ return { success: true };
12490
+ });
12416
12491
  }
12417
12492
  // ── Realtime Subscriptions ─────────────────────────────────
12418
12493
  /** Whether realtime is enabled in config. */
@@ -12600,6 +12675,8 @@ var BugBearClient = class {
12600
12675
  if (this.config.onReportSubmitted) {
12601
12676
  this.config.onReportSubmitted(report);
12602
12677
  }
12678
+ this.notifyDashboard("report", { reportId: data.id }).catch(() => {
12679
+ });
12603
12680
  return { success: true, reportId: data.id };
12604
12681
  } catch (err) {
12605
12682
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -12612,6 +12689,44 @@ var BugBearClient = class {
12612
12689
  this.reportSubmitInFlight = false;
12613
12690
  }
12614
12691
  }
12692
+ /**
12693
+ * Capture an email for QA testing.
12694
+ * Called by the email interceptor — not typically called directly.
12695
+ */
12696
+ async captureEmail(payload) {
12697
+ try {
12698
+ await this.ready();
12699
+ if (!payload.subject || !payload.to || payload.to.length === 0) {
12700
+ return { success: false, error: "subject and to are required" };
12701
+ }
12702
+ const record = {
12703
+ project_id: this.config.projectId,
12704
+ to_addresses: payload.to,
12705
+ from_address: payload.from || null,
12706
+ subject: payload.subject,
12707
+ html_content: payload.html || null,
12708
+ text_content: payload.text || null,
12709
+ template_id: payload.templateId || null,
12710
+ metadata: payload.metadata || {},
12711
+ capture_mode: payload.captureMode,
12712
+ was_delivered: payload.wasDelivered,
12713
+ delivery_status: payload.wasDelivered ? "sent" : "pending"
12714
+ };
12715
+ const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
12716
+ if (error) {
12717
+ if (this._queue && isNetworkError(error.message)) {
12718
+ await this._queue.enqueue("email_capture", record);
12719
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
12720
+ }
12721
+ console.error("BugBear: Failed to capture email", error.message);
12722
+ return { success: false, error: error.message };
12723
+ }
12724
+ return { success: true, captureId: data.id };
12725
+ } catch (err) {
12726
+ const message = err instanceof Error ? err.message : "Unknown error";
12727
+ return { success: false, error: message };
12728
+ }
12729
+ }
12615
12730
  /**
12616
12731
  * Get assigned tests for current user
12617
12732
  * First looks up the tester by email, then fetches their assignments
@@ -13233,6 +13348,32 @@ var BugBearClient = class {
13233
13348
  return [];
13234
13349
  }
13235
13350
  }
13351
+ /**
13352
+ * Reopen a done issue that the tester believes isn't actually fixed.
13353
+ * Transitions the report from a done status back to 'confirmed'.
13354
+ */
13355
+ async reopenReport(reportId, reason) {
13356
+ try {
13357
+ const testerInfo = await this.getTesterInfo();
13358
+ if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
13359
+ const { data, error } = await this.supabase.rpc("reopen_report", {
13360
+ p_report_id: reportId,
13361
+ p_tester_id: testerInfo.id,
13362
+ p_reason: reason
13363
+ });
13364
+ if (error) {
13365
+ console.error("BugBear: Failed to reopen report", formatPgError(error));
13366
+ return { success: false, error: error.message };
13367
+ }
13368
+ if (!data?.success) {
13369
+ return { success: false, error: data?.error || "Failed to reopen report" };
13370
+ }
13371
+ return { success: true };
13372
+ } catch (err) {
13373
+ console.error("BugBear: Error reopening report", err);
13374
+ return { success: false, error: "Unexpected error" };
13375
+ }
13376
+ }
13236
13377
  /**
13237
13378
  * Basic email format validation (defense in depth)
13238
13379
  */
@@ -13742,6 +13883,7 @@ var BugBearClient = class {
13742
13883
  lastMessageAt: row.last_message_at,
13743
13884
  createdAt: row.created_at,
13744
13885
  unreadCount: Number(row.unread_count) || 0,
13886
+ reporterName: row.reporter_name || void 0,
13745
13887
  lastMessage: row.last_message_preview ? {
13746
13888
  id: "",
13747
13889
  threadId: row.thread_id,
@@ -13819,7 +13961,7 @@ var BugBearClient = class {
13819
13961
  insertData.attachments = safeAttachments;
13820
13962
  }
13821
13963
  }
13822
- const { error } = await this.supabase.from("discussion_messages").insert(insertData);
13964
+ const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
13823
13965
  if (error) {
13824
13966
  if (this._queue && isNetworkError(error.message)) {
13825
13967
  await this._queue.enqueue("message", insertData);
@@ -13828,6 +13970,10 @@ var BugBearClient = class {
13828
13970
  console.error("BugBear: Failed to send message", formatPgError(error));
13829
13971
  return false;
13830
13972
  }
13973
+ if (msgData?.id) {
13974
+ this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
13975
+ });
13976
+ }
13831
13977
  await this.markThreadAsRead(threadId);
13832
13978
  return true;
13833
13979
  } catch (err) {
@@ -13953,6 +14099,7 @@ var BugBearClient = class {
13953
14099
  if (!session) {
13954
14100
  return { success: false, error: "Session created but could not be fetched" };
13955
14101
  }
14102
+ await this.cacheSession(session);
13956
14103
  return { success: true, session };
13957
14104
  } catch (err) {
13958
14105
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -13976,6 +14123,7 @@ var BugBearClient = class {
13976
14123
  return { success: false, error: error.message };
13977
14124
  }
13978
14125
  const session = this.transformSession(data);
14126
+ await this.cacheSession(null);
13979
14127
  return { success: true, session };
13980
14128
  } catch (err) {
13981
14129
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -13991,8 +14139,13 @@ var BugBearClient = class {
13991
14139
  const testerInfo = await this.getTesterInfo();
13992
14140
  if (!testerInfo) return null;
13993
14141
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "active").order("started_at", { ascending: false }).limit(1).maybeSingle();
13994
- if (error || !data) return null;
13995
- return this.transformSession(data);
14142
+ if (error || !data) {
14143
+ await this.cacheSession(null);
14144
+ return null;
14145
+ }
14146
+ const session = this.transformSession(data);
14147
+ await this.cacheSession(session);
14148
+ return session;
13996
14149
  } catch (err) {
13997
14150
  console.error("BugBear: Error fetching active session", err);
13998
14151
  return null;
@@ -14170,6 +14323,93 @@ var BugBearClient = class {
14170
14323
  updatedAt: data.updated_at
14171
14324
  };
14172
14325
  }
14326
+ // ─── Passive Presence Tracking ──────────────────────────────
14327
+ /** Current presence session ID (null if not tracking). */
14328
+ get presenceSessionId() {
14329
+ return this._presenceSessionId;
14330
+ }
14331
+ /**
14332
+ * Start passive presence tracking for this tester.
14333
+ * Idempotent — reuses an existing active session if one exists.
14334
+ */
14335
+ async startPresence(platform) {
14336
+ try {
14337
+ await this.ensureReady();
14338
+ const testerInfo = await this.getTesterInfo();
14339
+ if (!testerInfo) return null;
14340
+ const { data, error } = await this.supabase.rpc("upsert_tester_presence", {
14341
+ p_project_id: this.config.projectId,
14342
+ p_tester_id: testerInfo.id,
14343
+ p_platform: platform
14344
+ });
14345
+ if (error) {
14346
+ console.error("BugBear: Failed to start presence", formatPgError(error));
14347
+ return null;
14348
+ }
14349
+ this._presenceSessionId = data;
14350
+ this._presencePaused = false;
14351
+ this.startPresenceHeartbeat();
14352
+ return data;
14353
+ } catch (err) {
14354
+ console.error("BugBear: Error starting presence", err);
14355
+ return null;
14356
+ }
14357
+ }
14358
+ /** Gracefully end the current presence session. */
14359
+ async endPresence() {
14360
+ this.stopPresenceHeartbeat();
14361
+ if (!this._presenceSessionId) return;
14362
+ try {
14363
+ await this.supabase.rpc("end_tester_presence", {
14364
+ p_session_id: this._presenceSessionId
14365
+ });
14366
+ } catch {
14367
+ }
14368
+ this._presenceSessionId = null;
14369
+ }
14370
+ /** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
14371
+ pausePresence() {
14372
+ this._presencePaused = true;
14373
+ this.heartbeatPresence();
14374
+ }
14375
+ /** Resume heartbeat after pause. Restarts if session was cleaned up. */
14376
+ async resumePresence() {
14377
+ if (!this._presenceSessionId) return;
14378
+ this._presencePaused = false;
14379
+ try {
14380
+ const { data } = await this.supabase.rpc("heartbeat_tester_presence", {
14381
+ p_session_id: this._presenceSessionId
14382
+ });
14383
+ if (!data) {
14384
+ this._presenceSessionId = null;
14385
+ }
14386
+ } catch {
14387
+ this._presenceSessionId = null;
14388
+ }
14389
+ }
14390
+ async heartbeatPresence() {
14391
+ if (!this._presenceSessionId || this._presencePaused) return;
14392
+ try {
14393
+ const { data, error } = await this.supabase.rpc("heartbeat_tester_presence", {
14394
+ p_session_id: this._presenceSessionId
14395
+ });
14396
+ if (error || data === false) {
14397
+ this.stopPresenceHeartbeat();
14398
+ this._presenceSessionId = null;
14399
+ }
14400
+ } catch {
14401
+ }
14402
+ }
14403
+ startPresenceHeartbeat() {
14404
+ this.stopPresenceHeartbeat();
14405
+ this._presenceInterval = setInterval(() => this.heartbeatPresence(), 6e4);
14406
+ }
14407
+ stopPresenceHeartbeat() {
14408
+ if (this._presenceInterval) {
14409
+ clearInterval(this._presenceInterval);
14410
+ this._presenceInterval = null;
14411
+ }
14412
+ }
14173
14413
  };
14174
14414
  function createBugBear(config) {
14175
14415
  return new BugBearClient(config);
@@ -14304,6 +14544,9 @@ function setActiveColors(palette) {
14304
14544
  colors = palette;
14305
14545
  shared = createSharedStyles();
14306
14546
  }
14547
+ function withAlpha(hex, alpha) {
14548
+ return hex + Math.round(alpha * 255).toString(16).padStart(2, "0");
14549
+ }
14307
14550
  function createSharedStyles() {
14308
14551
  return import_react_native.StyleSheet.create({
14309
14552
  card: {
@@ -14510,6 +14753,7 @@ var BugBearContext = (0, import_react.createContext)({
14510
14753
  issueCounts: { open: 0, done: 0, reopened: 0 },
14511
14754
  refreshIssueCounts: async () => {
14512
14755
  },
14756
+ reopenReport: async () => ({ success: false }),
14513
14757
  queuedCount: 0,
14514
14758
  dashboardUrl: void 0,
14515
14759
  onError: void 0
@@ -14651,6 +14895,14 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14651
14895
  const counts = await client.getIssueCounts();
14652
14896
  setIssueCounts(counts);
14653
14897
  }, [client]);
14898
+ const reopenReport = (0, import_react.useCallback)(async (reportId, reason) => {
14899
+ if (!client) return { success: false, error: "Client not initialized" };
14900
+ const result = await client.reopenReport(reportId, reason);
14901
+ if (result.success) {
14902
+ await refreshIssueCounts();
14903
+ }
14904
+ return result;
14905
+ }, [client, refreshIssueCounts]);
14654
14906
  const initializeBugBear = (0, import_react.useCallback)(async (bugBearClient) => {
14655
14907
  setIsLoading(true);
14656
14908
  try {
@@ -14736,6 +14988,10 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14736
14988
  setClient(newClient);
14737
14989
  (async () => {
14738
14990
  try {
14991
+ const cachedSession = await newClient.getCachedSession();
14992
+ if (cachedSession) {
14993
+ setActiveSession(cachedSession);
14994
+ }
14739
14995
  await initializeBugBear(newClient);
14740
14996
  if (newClient.monitor && config.monitoring) {
14741
14997
  const getCurrentRoute = () => contextCapture.getCurrentRoute();
@@ -14793,6 +15049,28 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14793
15049
  }
14794
15050
  return () => subscription.remove();
14795
15051
  }, [client]);
15052
+ (0, import_react.useEffect)(() => {
15053
+ if (!client || !isTester) return;
15054
+ let mounted = true;
15055
+ const platform = import_react_native2.Platform.OS === "ios" ? "ios" : "android";
15056
+ client.startPresence(platform);
15057
+ const subscription = import_react_native2.AppState.addEventListener("change", (nextState) => {
15058
+ if (nextState === "active") {
15059
+ client.resumePresence().then(() => {
15060
+ if (mounted && !client.presenceSessionId) {
15061
+ client.startPresence(platform);
15062
+ }
15063
+ });
15064
+ } else if (nextState === "background" || nextState === "inactive") {
15065
+ client.pausePresence();
15066
+ }
15067
+ });
15068
+ return () => {
15069
+ mounted = false;
15070
+ subscription.remove();
15071
+ client.endPresence();
15072
+ };
15073
+ }, [client, isTester]);
14796
15074
  (0, import_react.useEffect)(() => {
14797
15075
  if (!client || !isTester) return;
14798
15076
  if (widgetMode === "qa" && !isQAEnabled) return;
@@ -14870,6 +15148,7 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14870
15148
  // Issue tracking
14871
15149
  issueCounts,
14872
15150
  refreshIssueCounts,
15151
+ reopenReport,
14873
15152
  queuedCount,
14874
15153
  dashboardUrl: config.dashboardUrl,
14875
15154
  onError: config.onError
@@ -15601,6 +15880,17 @@ function TestDetailScreen({ testId, nav }) {
15601
15880
  setShowSteps(true);
15602
15881
  setShowDetails(false);
15603
15882
  }, [displayedAssignment?.id]);
15883
+ (0, import_react5.useEffect)(() => {
15884
+ if (!client || !displayedAssignment || displayedAssignment.status !== "pending") return;
15885
+ let cancelled = false;
15886
+ (async () => {
15887
+ await client.updateAssignmentStatus(displayedAssignment.id, "in_progress");
15888
+ if (!cancelled) await refreshAssignments();
15889
+ })();
15890
+ return () => {
15891
+ cancelled = true;
15892
+ };
15893
+ }, [client, displayedAssignment?.id, displayedAssignment?.status, refreshAssignments]);
15604
15894
  (0, import_react5.useEffect)(() => {
15605
15895
  const active = displayedAssignment?.status === "in_progress" ? displayedAssignment : null;
15606
15896
  if (!active?.startedAt) {
@@ -15651,17 +15941,6 @@ function TestDetailScreen({ testId, nav }) {
15651
15941
  setIsSubmitting(false);
15652
15942
  }
15653
15943
  }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
15654
- const handleStart = (0, import_react5.useCallback)(async () => {
15655
- if (!client || !displayedAssignment || isSubmitting) return;
15656
- import_react_native5.Keyboard.dismiss();
15657
- setIsSubmitting(true);
15658
- try {
15659
- await client.updateAssignmentStatus(displayedAssignment.id, "in_progress");
15660
- await refreshAssignments();
15661
- } finally {
15662
- setIsSubmitting(false);
15663
- }
15664
- }, [client, displayedAssignment, refreshAssignments, isSubmitting]);
15665
15944
  const handleReopen = (0, import_react5.useCallback)(async () => {
15666
15945
  if (!client || !displayedAssignment || isSubmitting) return;
15667
15946
  import_react_native5.Keyboard.dismiss();
@@ -15807,15 +16086,7 @@ function TestDetailScreen({ testId, nav }) {
15807
16086
  disabled: isSubmitting
15808
16087
  },
15809
16088
  /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.passBtnText }, "Change to Pass")
15810
- ))) : displayedAssignment.status === "pending" ? /* @__PURE__ */ import_react5.default.createElement(
15811
- import_react_native5.TouchableOpacity,
15812
- {
15813
- style: [styles5.startBtn, isSubmitting && { opacity: 0.5 }],
15814
- onPress: handleStart,
15815
- disabled: isSubmitting
15816
- },
15817
- /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.startBtnText }, isSubmitting ? "Starting..." : displayedAssignment.isVerification ? "\u{1F50D} Start Verification" : "\u25B6 Start Test")
15818
- ) : /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.actionButtons }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.failBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Reporting..." : "Failing..." : displayedAssignment.isVerification ? "\u2717 Still Broken" : "Fail")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.skipBtnText }, "Skip")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.passBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Verifying..." : "Passing..." : displayedAssignment.isVerification ? "\u2713 Fix Verified" : "Pass"))), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalOverlay }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalContent }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalSubtitle }, "Select a reason:"), [
16089
+ ))) : /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.actionButtons }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.failBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Reporting..." : "Failing..." : displayedAssignment.isVerification ? "\u2717 Still Broken" : "Fail")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.skipBtnText }, "Skip")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.passBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Verifying..." : "Passing..." : displayedAssignment.isVerification ? "\u2713 Fix Verified" : "Pass"))), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalOverlay }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalContent }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalSubtitle }, "Select a reason:"), [
15819
16090
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
15820
16091
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
15821
16092
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -15929,9 +16200,6 @@ function createStyles2() {
15929
16200
  completedLabel: { fontSize: 13, fontWeight: "600", color: colors.textSecondary },
15930
16201
  reopenBtn: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.blue },
15931
16202
  reopenBtnText: { fontSize: 14, fontWeight: "600", color: colors.blue },
15932
- // Start Test button
15933
- startBtn: { paddingVertical: 16, borderRadius: 12, alignItems: "center", backgroundColor: colors.blueSurface, borderWidth: 1, borderColor: colors.blueAccent, marginTop: 8 },
15934
- startBtnText: { fontSize: 16, fontWeight: "600", color: colors.blueText },
15935
16203
  // Action buttons
15936
16204
  actionButtons: { flexDirection: "row", gap: 10, marginTop: 8 },
15937
16205
  actionBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center" },
@@ -16800,6 +17068,8 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
16800
17068
  const [severity, setSeverity] = (0, import_react12.useState)("medium");
16801
17069
  const [category, setCategory] = (0, import_react12.useState)(null);
16802
17070
  const [description, setDescription] = (0, import_react12.useState)("");
17071
+ const [myIssues, setMyIssues] = (0, import_react12.useState)([]);
17072
+ const [similarReports, setSimilarReports] = (0, import_react12.useState)([]);
16803
17073
  const [affectedScreen, setAffectedScreen] = (0, import_react12.useState)("");
16804
17074
  const [submitting, setSubmitting] = (0, import_react12.useState)(false);
16805
17075
  const [error, setError] = (0, import_react12.useState)(null);
@@ -16815,6 +17085,41 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
16815
17085
  }, [autoCaptureUri, onAutoCaptureConsumed]);
16816
17086
  const isRetestFailure = prefill?.type === "test_fail";
16817
17087
  const isBugType = reportType === "bug" || reportType === "test_fail";
17088
+ (0, import_react12.useEffect)(() => {
17089
+ if (!client?.getIssues) return;
17090
+ let cancelled = false;
17091
+ const load = async () => {
17092
+ try {
17093
+ const [open, done] = await Promise.all([
17094
+ client.getIssues("open"),
17095
+ client.getIssues("done")
17096
+ ]);
17097
+ if (!cancelled) setMyIssues([...open, ...done]);
17098
+ } catch {
17099
+ }
17100
+ };
17101
+ load();
17102
+ return () => {
17103
+ cancelled = true;
17104
+ };
17105
+ }, [client]);
17106
+ (0, import_react12.useEffect)(() => {
17107
+ if (description.length < 10 || myIssues.length === 0) {
17108
+ setSimilarReports([]);
17109
+ return;
17110
+ }
17111
+ const words = description.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
17112
+ if (words.length === 0) {
17113
+ setSimilarReports([]);
17114
+ return;
17115
+ }
17116
+ const scored = myIssues.map((issue) => {
17117
+ const text = `${issue.title || ""} ${issue.description || ""}`.toLowerCase();
17118
+ const matches = words.filter((w) => text.includes(w)).length;
17119
+ return { issue, score: matches / words.length };
17120
+ }).filter((s2) => s2.score >= 0.3).sort((a, b) => b.score - a.score).slice(0, 3);
17121
+ setSimilarReports(scored.map((s2) => s2.issue));
17122
+ }, [description, myIssues]);
16818
17123
  (0, import_react12.useEffect)(() => {
16819
17124
  if (reportType === "feedback" || reportType === "suggestion") {
16820
17125
  setCategory("other");
@@ -16889,7 +17194,41 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
16889
17194
  numberOfLines: 4,
16890
17195
  textAlignVertical: "top"
16891
17196
  }
16892
- )), /* @__PURE__ */ import_react12.default.createElement(
17197
+ ), similarReports.length > 0 && /* @__PURE__ */ import_react12.default.createElement(import_react_native11.View, { style: {
17198
+ marginTop: 8,
17199
+ padding: 12,
17200
+ borderRadius: 8,
17201
+ backgroundColor: withAlpha(colors.orange, 0.1),
17202
+ borderWidth: 1,
17203
+ borderColor: withAlpha(colors.orange, 0.3)
17204
+ } }, /* @__PURE__ */ import_react12.default.createElement(import_react_native11.Text, { style: { fontSize: 12, fontWeight: "600", color: colors.orange, marginBottom: 8 } }, "Similar reports you've already filed:"), similarReports.map((issue) => /* @__PURE__ */ import_react12.default.createElement(
17205
+ import_react_native11.TouchableOpacity,
17206
+ {
17207
+ key: issue.id,
17208
+ onPress: () => nav.push({ name: "ISSUE_DETAIL", issue }),
17209
+ style: {
17210
+ flexDirection: "row",
17211
+ alignItems: "center",
17212
+ gap: 8,
17213
+ paddingVertical: 6
17214
+ }
17215
+ },
17216
+ /* @__PURE__ */ import_react12.default.createElement(import_react_native11.View, { style: {
17217
+ width: 8,
17218
+ height: 8,
17219
+ borderRadius: 4,
17220
+ backgroundColor: issue.severity === "critical" ? colors.red : issue.severity === "high" ? colors.orange : colors.yellow
17221
+ } }),
17222
+ /* @__PURE__ */ import_react12.default.createElement(
17223
+ import_react_native11.Text,
17224
+ {
17225
+ numberOfLines: 1,
17226
+ style: { fontSize: 12, color: colors.textPrimary, flex: 1 }
17227
+ },
17228
+ issue.title
17229
+ ),
17230
+ /* @__PURE__ */ import_react12.default.createElement(import_react_native11.Text, { style: { fontSize: 11, color: colors.textMuted } }, issue.status)
17231
+ )))), /* @__PURE__ */ import_react12.default.createElement(
16893
17232
  ImagePickerButtons,
16894
17233
  {
16895
17234
  images: images.images,
@@ -16945,7 +17284,41 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
16945
17284
  numberOfLines: 4,
16946
17285
  textAlignVertical: "top"
16947
17286
  }
16948
- )), isBugType && /* @__PURE__ */ import_react12.default.createElement(import_react_native11.View, { style: styles5.section }, /* @__PURE__ */ import_react12.default.createElement(import_react_native11.Text, { style: shared.label }, "Which screen?"), /* @__PURE__ */ import_react12.default.createElement(
17287
+ ), similarReports.length > 0 && /* @__PURE__ */ import_react12.default.createElement(import_react_native11.View, { style: {
17288
+ marginTop: 8,
17289
+ padding: 12,
17290
+ borderRadius: 8,
17291
+ backgroundColor: withAlpha(colors.orange, 0.1),
17292
+ borderWidth: 1,
17293
+ borderColor: withAlpha(colors.orange, 0.3)
17294
+ } }, /* @__PURE__ */ import_react12.default.createElement(import_react_native11.Text, { style: { fontSize: 12, fontWeight: "600", color: colors.orange, marginBottom: 8 } }, "Similar reports you've already filed:"), similarReports.map((issue) => /* @__PURE__ */ import_react12.default.createElement(
17295
+ import_react_native11.TouchableOpacity,
17296
+ {
17297
+ key: issue.id,
17298
+ onPress: () => nav.push({ name: "ISSUE_DETAIL", issue }),
17299
+ style: {
17300
+ flexDirection: "row",
17301
+ alignItems: "center",
17302
+ gap: 8,
17303
+ paddingVertical: 6
17304
+ }
17305
+ },
17306
+ /* @__PURE__ */ import_react12.default.createElement(import_react_native11.View, { style: {
17307
+ width: 8,
17308
+ height: 8,
17309
+ borderRadius: 4,
17310
+ backgroundColor: issue.severity === "critical" ? colors.red : issue.severity === "high" ? colors.orange : colors.yellow
17311
+ } }),
17312
+ /* @__PURE__ */ import_react12.default.createElement(
17313
+ import_react_native11.Text,
17314
+ {
17315
+ numberOfLines: 1,
17316
+ style: { fontSize: 12, color: colors.textPrimary, flex: 1 }
17317
+ },
17318
+ issue.title
17319
+ ),
17320
+ /* @__PURE__ */ import_react12.default.createElement(import_react_native11.Text, { style: { fontSize: 11, color: colors.textMuted } }, issue.status)
17321
+ )))), isBugType && /* @__PURE__ */ import_react12.default.createElement(import_react_native11.View, { style: styles5.section }, /* @__PURE__ */ import_react12.default.createElement(import_react_native11.Text, { style: shared.label }, "Which screen?"), /* @__PURE__ */ import_react12.default.createElement(
16949
17322
  import_react_native11.TextInput,
16950
17323
  {
16951
17324
  style: styles5.screenInput,
@@ -17026,6 +17399,19 @@ var import_react_native13 = require("react-native");
17026
17399
  function MessageListScreen({ nav }) {
17027
17400
  const { threads, unreadCount, refreshThreads, dashboardUrl, isLoading, widgetColorScheme } = useBugBear();
17028
17401
  const styles5 = (0, import_react14.useMemo)(() => createStyles7(), [widgetColorScheme]);
17402
+ const [activeFilter, setActiveFilter] = (0, import_react14.useState)("all");
17403
+ const filteredThreads = threads.filter((thread) => {
17404
+ if (activeFilter === "all") return true;
17405
+ if (activeFilter === "unread") return thread.unreadCount > 0;
17406
+ return thread.threadType === activeFilter;
17407
+ });
17408
+ const filterChips = [
17409
+ { key: "all", label: "All" },
17410
+ { key: "report", label: "Bug Reports" },
17411
+ { key: "direct", label: "Direct" },
17412
+ { key: "announcement", label: "Announcements" },
17413
+ { key: "unread", label: "Unread", count: threads.filter((t) => t.unreadCount > 0).length }
17414
+ ];
17029
17415
  if (isLoading) return /* @__PURE__ */ import_react14.default.createElement(MessageListScreenSkeleton, null);
17030
17416
  return /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, null, /* @__PURE__ */ import_react14.default.createElement(
17031
17417
  import_react_native13.TouchableOpacity,
@@ -17034,14 +17420,54 @@ function MessageListScreen({ nav }) {
17034
17420
  onPress: () => nav.push({ name: "COMPOSE_MESSAGE" })
17035
17421
  },
17036
17422
  /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.newMsgText }, "\u2709\uFE0F New Message")
17037
- ), threads.length === 0 ? /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: shared.emptyState }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: shared.emptyEmoji }, "\u{1F4AC}"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: shared.emptyTitle }, "No messages yet"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: shared.emptySubtitle }, "Start a conversation or wait for messages from admins")) : /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, null, threads.map((thread) => /* @__PURE__ */ import_react14.default.createElement(
17423
+ ), /* @__PURE__ */ import_react14.default.createElement(
17424
+ import_react_native13.ScrollView,
17425
+ {
17426
+ horizontal: true,
17427
+ showsHorizontalScrollIndicator: false,
17428
+ style: { paddingBottom: 12 },
17429
+ contentContainerStyle: { paddingHorizontal: 16, gap: 8 }
17430
+ },
17431
+ filterChips.map((chip) => /* @__PURE__ */ import_react14.default.createElement(
17432
+ import_react_native13.TouchableOpacity,
17433
+ {
17434
+ key: chip.key,
17435
+ onPress: () => setActiveFilter(chip.key),
17436
+ style: {
17437
+ paddingVertical: 6,
17438
+ paddingHorizontal: 12,
17439
+ borderRadius: 16,
17440
+ borderWidth: 1,
17441
+ borderColor: activeFilter === chip.key ? colors.blue : colors.border,
17442
+ backgroundColor: activeFilter === chip.key ? withAlpha(colors.blue, 0.15) : "transparent",
17443
+ flexDirection: "row",
17444
+ alignItems: "center",
17445
+ gap: 4
17446
+ }
17447
+ },
17448
+ /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: {
17449
+ fontSize: 12,
17450
+ fontWeight: "500",
17451
+ color: activeFilter === chip.key ? colors.blue : colors.textSecondary
17452
+ } }, chip.label),
17453
+ chip.count !== void 0 && chip.count > 0 && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: {
17454
+ backgroundColor: colors.blue,
17455
+ borderRadius: 8,
17456
+ minWidth: 16,
17457
+ height: 16,
17458
+ justifyContent: "center",
17459
+ alignItems: "center",
17460
+ paddingHorizontal: 4
17461
+ } }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: { fontSize: 10, fontWeight: "bold", color: colors.onPrimary } }, chip.count))
17462
+ ))
17463
+ ), filteredThreads.length === 0 ? /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: shared.emptyState }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: shared.emptyEmoji }, "\u{1F4AC}"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: shared.emptyTitle }, "No messages yet"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: shared.emptySubtitle }, "Start a conversation or wait for messages from admins")) : /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, null, filteredThreads.map((thread) => /* @__PURE__ */ import_react14.default.createElement(
17038
17464
  import_react_native13.TouchableOpacity,
17039
17465
  {
17040
17466
  key: thread.id,
17041
17467
  style: [styles5.threadItem, thread.unreadCount > 0 && styles5.threadItemUnread],
17042
17468
  onPress: () => nav.push({ name: "THREAD_DETAIL", thread })
17043
17469
  },
17044
- /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles5.threadLeft }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.threadIcon }, getThreadTypeIcon(thread.threadType)), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles5.threadInfo }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles5.threadTitleRow }, thread.isPinned && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.pinIcon }, "\u{1F4CC}"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.threadSubject, numberOfLines: 1 }, thread.subject || "No subject")), thread.lastMessage && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.threadPreview, numberOfLines: 1 }, thread.lastMessage.senderName, ": ", thread.lastMessage.content))),
17470
+ /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles5.threadLeft }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.threadIcon }, getThreadTypeIcon(thread.threadType)), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles5.threadInfo }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles5.threadTitleRow }, thread.isPinned && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.pinIcon }, "\u{1F4CC}"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.threadSubject, numberOfLines: 1 }, thread.subject || "No subject")), thread.threadType === "report" && thread.reporterName && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: { fontSize: 11, color: colors.textMuted } }, "Reported by: ", thread.reporterName), thread.lastMessage && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.threadPreview, numberOfLines: 1 }, thread.lastMessage.senderName, ": ", thread.lastMessage.content))),
17045
17471
  /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles5.threadRight }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.threadTime }, formatRelativeTime(thread.lastMessageAt)), thread.unreadCount > 0 && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles5.unreadBadge }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.unreadText }, thread.unreadCount)), thread.priority !== "normal" && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: [styles5.priorityDot, { backgroundColor: getPriorityColor(thread.priority) }] }))
17046
17472
  ))), dashboardUrl && /* @__PURE__ */ import_react14.default.createElement(
17047
17473
  import_react_native13.TouchableOpacity,
@@ -17051,7 +17477,7 @@ function MessageListScreen({ nav }) {
17051
17477
  activeOpacity: 0.7
17052
17478
  },
17053
17479
  /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.dashboardLinkText }, "\u{1F310}", " View on Dashboard ", "\u2192")
17054
- ), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles5.footer }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.footerText }, threads.length, " thread", threads.length !== 1 ? "s" : "", " \xB7 ", unreadCount, " unread"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.TouchableOpacity, { onPress: refreshThreads }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.refreshText }, "\u21BB Refresh"))));
17480
+ ), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles5.footer }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.footerText }, filteredThreads.length, " thread", filteredThreads.length !== 1 ? "s" : "", " \xB7 ", unreadCount, " unread"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.TouchableOpacity, { onPress: refreshThreads }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.refreshText }, "\u21BB Refresh"))));
17055
17481
  }
17056
17482
  function createStyles7() {
17057
17483
  return import_react_native13.StyleSheet.create({
@@ -17432,7 +17858,17 @@ function IssueListScreen({ nav, category }) {
17432
17858
  const [loading, setLoading] = (0, import_react18.useState)(true);
17433
17859
  const [counts, setCounts] = (0, import_react18.useState)(null);
17434
17860
  const [sortMode, setSortMode] = (0, import_react18.useState)("severity");
17861
+ const [searchQuery, setSearchQuery] = (0, import_react18.useState)("");
17862
+ const [debouncedQuery, setDebouncedQuery] = (0, import_react18.useState)("");
17435
17863
  const config = CATEGORY_CONFIG[activeCategory];
17864
+ (0, import_react18.useEffect)(() => {
17865
+ const timer = setTimeout(() => setDebouncedQuery(searchQuery), 300);
17866
+ return () => clearTimeout(timer);
17867
+ }, [searchQuery]);
17868
+ (0, import_react18.useEffect)(() => {
17869
+ setSearchQuery("");
17870
+ setDebouncedQuery("");
17871
+ }, [activeCategory]);
17436
17872
  (0, import_react18.useEffect)(() => {
17437
17873
  if (!client) return;
17438
17874
  client.getIssueCounts().then(setCounts).catch(() => {
@@ -17472,6 +17908,9 @@ function IssueListScreen({ nav, category }) {
17472
17908
  }
17473
17909
  return sorted;
17474
17910
  }, [issues, sortMode]);
17911
+ const searchFilteredIssues = debouncedQuery ? sortedIssues.filter(
17912
+ (issue) => (issue.title || "").toLowerCase().includes(debouncedQuery.toLowerCase()) || (issue.description || "").toLowerCase().includes(debouncedQuery.toLowerCase())
17913
+ ) : sortedIssues;
17475
17914
  return /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, null, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles5.tabBar }, CATEGORIES.map((cat) => {
17476
17915
  const catConfig = CATEGORY_CONFIG[cat];
17477
17916
  const isActive = activeCategory === cat;
@@ -17518,7 +17957,25 @@ function IssueListScreen({ nav, category }) {
17518
17957
  styles5.sortBtnText,
17519
17958
  sortMode === s2.key && styles5.sortBtnTextActive
17520
17959
  ] }, s2.label)
17521
- ))), loading ? /* @__PURE__ */ import_react18.default.createElement(IssueListScreenSkeleton, null) : sortedIssues.length === 0 ? /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles5.emptyContainer }, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles5.emptyIcon }, config.emptyIcon), /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles5.emptyText }, config.emptyText)) : sortedIssues.map((issue) => /* @__PURE__ */ import_react18.default.createElement(
17960
+ ))), /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: { paddingHorizontal: 16, paddingBottom: 12 } }, /* @__PURE__ */ import_react18.default.createElement(
17961
+ import_react_native17.TextInput,
17962
+ {
17963
+ value: searchQuery,
17964
+ onChangeText: setSearchQuery,
17965
+ placeholder: "Search my reports...",
17966
+ placeholderTextColor: colors.textMuted,
17967
+ style: {
17968
+ padding: 8,
17969
+ paddingHorizontal: 12,
17970
+ borderRadius: 8,
17971
+ borderWidth: 1,
17972
+ borderColor: colors.border,
17973
+ backgroundColor: colors.card,
17974
+ color: colors.textPrimary,
17975
+ fontSize: 13
17976
+ }
17977
+ }
17978
+ )), loading ? /* @__PURE__ */ import_react18.default.createElement(IssueListScreenSkeleton, null) : searchFilteredIssues.length === 0 ? /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles5.emptyContainer }, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles5.emptyIcon }, debouncedQuery ? "\u{1F50D}" : config.emptyIcon), /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles5.emptyText }, debouncedQuery ? "No matching issues" : config.emptyText)) : searchFilteredIssues.map((issue) => /* @__PURE__ */ import_react18.default.createElement(
17522
17979
  import_react_native17.TouchableOpacity,
17523
17980
  {
17524
17981
  key: issue.id,
@@ -17685,9 +18142,15 @@ function createStyles11() {
17685
18142
  // src/widget/screens/IssueDetailScreen.tsx
17686
18143
  var import_react19 = __toESM(require("react"));
17687
18144
  var import_react_native18 = require("react-native");
18145
+ var DONE_STATUSES = ["verified", "resolved", "closed", "reviewed"];
17688
18146
  function IssueDetailScreen({ nav, issue }) {
17689
- const { dashboardUrl, widgetColorScheme } = useBugBear();
18147
+ const { dashboardUrl, widgetColorScheme, reopenReport } = useBugBear();
17690
18148
  const styles5 = (0, import_react19.useMemo)(() => createStyles12(), [widgetColorScheme]);
18149
+ const [showReopenForm, setShowReopenForm] = (0, import_react19.useState)(false);
18150
+ const [reopenReason, setReopenReason] = (0, import_react19.useState)("");
18151
+ const [isSubmitting, setIsSubmitting] = (0, import_react19.useState)(false);
18152
+ const [reopenError, setReopenError] = (0, import_react19.useState)(null);
18153
+ const [wasReopened, setWasReopened] = (0, import_react19.useState)(false);
17691
18154
  const STATUS_LABELS = (0, import_react19.useMemo)(() => ({
17692
18155
  new: { label: "New", bg: colors.blueDark, color: colors.blueLight },
17693
18156
  triaging: { label: "Triaging", bg: colors.blueDark, color: colors.blueLight },
@@ -17710,7 +18173,68 @@ function IssueDetailScreen({ nav, issue }) {
17710
18173
  }), [widgetColorScheme]);
17711
18174
  const statusConfig = STATUS_LABELS[issue.status] || { label: issue.status, bg: colors.card, color: colors.textSecondary };
17712
18175
  const severityConfig = issue.severity ? SEVERITY_CONFIG[issue.severity] : null;
17713
- return /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, null, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.badgeRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: statusConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: statusConfig.color }] }, statusConfig.label)), severityConfig && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.title }, issue.title), issue.route && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.route }, issue.route), issue.description && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.descriptionCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.descriptionText }, issue.description)), issue.verifiedByName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedIcon }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugTitle }, "Original Bug")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugBody }, "Retest of: ", issue.originalBugTitle)), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotSection }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ import_react19.default.createElement(import_react_native18.TouchableOpacity, { key: i, onPress: () => import_react_native18.Linking.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Image, { source: { uri: url }, style: styles5.screenshotThumb }))))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.metaSection }, issue.reporterName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))), dashboardUrl && /* @__PURE__ */ import_react19.default.createElement(
18176
+ const isDone = DONE_STATUSES.includes(issue.status);
18177
+ const handleReopen = (0, import_react19.useCallback)(async () => {
18178
+ if (!reopenReason.trim()) return;
18179
+ setIsSubmitting(true);
18180
+ setReopenError(null);
18181
+ const result = await reopenReport(issue.id, reopenReason.trim());
18182
+ setIsSubmitting(false);
18183
+ if (result.success) {
18184
+ setWasReopened(true);
18185
+ setShowReopenForm(false);
18186
+ } else {
18187
+ setReopenError(result.error || "Failed to reopen");
18188
+ }
18189
+ }, [reopenReason, reopenReport, issue.id]);
18190
+ return /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, null, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.badgeRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: wasReopened ? colors.yellowDark : statusConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: wasReopened ? colors.yellowLight : statusConfig.color }] }, wasReopened ? "Reopened" : statusConfig.label)), severityConfig && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.title }, issue.title), issue.route && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.route }, issue.route), issue.description && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.descriptionCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.descriptionText }, issue.description)), wasReopened && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenedRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenedIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenedText }, "Issue reopened \u2014 your team has been notified"))), issue.verifiedByName && !wasReopened && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedIcon }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugTitleText }, "Original Bug")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugBody }, "Retest of: ", issue.originalBugTitle)), isDone && !wasReopened && !showReopenForm && /* @__PURE__ */ import_react19.default.createElement(
18191
+ import_react_native18.TouchableOpacity,
18192
+ {
18193
+ style: styles5.reopenButton,
18194
+ onPress: () => setShowReopenForm(true),
18195
+ activeOpacity: 0.7
18196
+ },
18197
+ /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenButtonText }, "\u{1F504}", " Not Fixed \u2014 Reopen Issue")
18198
+ ), showReopenForm && !wasReopened && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenForm }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenFormTitle }, "Why isn't this fixed?"), /* @__PURE__ */ import_react19.default.createElement(
18199
+ import_react_native18.TextInput,
18200
+ {
18201
+ value: reopenReason,
18202
+ onChangeText: setReopenReason,
18203
+ placeholder: "Describe what you're still seeing...",
18204
+ placeholderTextColor: colors.textMuted,
18205
+ style: styles5.reopenInput,
18206
+ multiline: true,
18207
+ autoFocus: true
18208
+ }
18209
+ ), reopenError && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenErrorText }, reopenError), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenActions }, /* @__PURE__ */ import_react19.default.createElement(
18210
+ import_react_native18.TouchableOpacity,
18211
+ {
18212
+ style: [
18213
+ styles5.reopenSubmitButton,
18214
+ (!reopenReason.trim() || isSubmitting) && styles5.reopenSubmitDisabled
18215
+ ],
18216
+ onPress: handleReopen,
18217
+ disabled: isSubmitting || !reopenReason.trim(),
18218
+ activeOpacity: 0.7
18219
+ },
18220
+ isSubmitting ? /* @__PURE__ */ import_react19.default.createElement(import_react_native18.ActivityIndicator, { size: "small", color: "#fff" }) : /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [
18221
+ styles5.reopenSubmitText,
18222
+ !reopenReason.trim() && styles5.reopenSubmitTextDisabled
18223
+ ] }, "Reopen Issue")
18224
+ ), /* @__PURE__ */ import_react19.default.createElement(
18225
+ import_react_native18.TouchableOpacity,
18226
+ {
18227
+ style: styles5.reopenCancelButton,
18228
+ onPress: () => {
18229
+ setShowReopenForm(false);
18230
+ setReopenReason("");
18231
+ setReopenError(null);
18232
+ },
18233
+ disabled: isSubmitting,
18234
+ activeOpacity: 0.7
18235
+ },
18236
+ /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenCancelText }, "Cancel")
18237
+ ))), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotSection }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ import_react19.default.createElement(import_react_native18.TouchableOpacity, { key: i, onPress: () => import_react_native18.Linking.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Image, { source: { uri: url }, style: styles5.screenshotThumb }))))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.metaSection }, issue.reporterName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))), dashboardUrl && /* @__PURE__ */ import_react19.default.createElement(
17714
18238
  import_react_native18.TouchableOpacity,
17715
18239
  {
17716
18240
  style: styles5.dashboardLink,
@@ -17762,6 +18286,28 @@ function createStyles12() {
17762
18286
  color: colors.textSecondary,
17763
18287
  lineHeight: 19
17764
18288
  },
18289
+ reopenedCard: {
18290
+ backgroundColor: colors.yellowDark,
18291
+ borderWidth: 1,
18292
+ borderColor: colors.yellowBorder,
18293
+ borderRadius: 8,
18294
+ padding: 12,
18295
+ marginBottom: 12
18296
+ },
18297
+ reopenedRow: {
18298
+ flexDirection: "row",
18299
+ alignItems: "center",
18300
+ gap: 8
18301
+ },
18302
+ reopenedIcon: {
18303
+ fontSize: 14
18304
+ },
18305
+ reopenedText: {
18306
+ fontSize: 13,
18307
+ fontWeight: "600",
18308
+ color: colors.yellowLight,
18309
+ flex: 1
18310
+ },
17765
18311
  verifiedCard: {
17766
18312
  backgroundColor: colors.greenDark,
17767
18313
  borderWidth: 1,
@@ -17805,7 +18351,7 @@ function createStyles12() {
17805
18351
  originalBugIcon: {
17806
18352
  fontSize: 16
17807
18353
  },
17808
- originalBugTitle: {
18354
+ originalBugTitleText: {
17809
18355
  fontSize: 13,
17810
18356
  fontWeight: "600",
17811
18357
  color: colors.yellowLight
@@ -17814,6 +18360,89 @@ function createStyles12() {
17814
18360
  fontSize: 12,
17815
18361
  color: colors.yellowSubtle
17816
18362
  },
18363
+ reopenButton: {
18364
+ borderWidth: 1,
18365
+ borderColor: colors.orange,
18366
+ borderRadius: 8,
18367
+ paddingVertical: 10,
18368
+ paddingHorizontal: 16,
18369
+ alignItems: "center",
18370
+ marginBottom: 12
18371
+ },
18372
+ reopenButtonText: {
18373
+ fontSize: 13,
18374
+ fontWeight: "600",
18375
+ color: colors.orange
18376
+ },
18377
+ reopenForm: {
18378
+ backgroundColor: colors.card,
18379
+ borderWidth: 1,
18380
+ borderColor: colors.orange,
18381
+ borderRadius: 8,
18382
+ padding: 12,
18383
+ marginBottom: 12
18384
+ },
18385
+ reopenFormTitle: {
18386
+ fontSize: 13,
18387
+ fontWeight: "600",
18388
+ color: colors.orange,
18389
+ marginBottom: 8
18390
+ },
18391
+ reopenInput: {
18392
+ backgroundColor: colors.bg,
18393
+ borderWidth: 1,
18394
+ borderColor: colors.border,
18395
+ borderRadius: 6,
18396
+ padding: 8,
18397
+ color: colors.textPrimary,
18398
+ fontSize: 13,
18399
+ lineHeight: 18,
18400
+ minHeight: 60,
18401
+ textAlignVertical: "top"
18402
+ },
18403
+ reopenErrorText: {
18404
+ fontSize: 12,
18405
+ color: colors.red,
18406
+ marginTop: 6
18407
+ },
18408
+ reopenActions: {
18409
+ flexDirection: "row",
18410
+ gap: 8,
18411
+ marginTop: 8
18412
+ },
18413
+ reopenSubmitButton: {
18414
+ flex: 1,
18415
+ backgroundColor: colors.orange,
18416
+ borderRadius: 6,
18417
+ paddingVertical: 8,
18418
+ paddingHorizontal: 12,
18419
+ alignItems: "center",
18420
+ justifyContent: "center"
18421
+ },
18422
+ reopenSubmitDisabled: {
18423
+ backgroundColor: colors.card,
18424
+ opacity: 0.7
18425
+ },
18426
+ reopenSubmitText: {
18427
+ fontSize: 13,
18428
+ fontWeight: "600",
18429
+ color: "#fff"
18430
+ },
18431
+ reopenSubmitTextDisabled: {
18432
+ color: colors.textMuted
18433
+ },
18434
+ reopenCancelButton: {
18435
+ borderWidth: 1,
18436
+ borderColor: colors.border,
18437
+ borderRadius: 6,
18438
+ paddingVertical: 8,
18439
+ paddingHorizontal: 12,
18440
+ alignItems: "center"
18441
+ },
18442
+ reopenCancelText: {
18443
+ fontSize: 13,
18444
+ color: colors.textSecondary
18445
+ },
17817
18446
  screenshotSection: {
17818
18447
  marginBottom: 12
17819
18448
  },
@@ -17872,11 +18501,13 @@ function SessionStartScreen({ nav }) {
17872
18501
  const styles5 = (0, import_react20.useMemo)(() => createStyles13(), [widgetColorScheme]);
17873
18502
  const [focusArea, setFocusArea] = (0, import_react20.useState)("");
17874
18503
  const [isStarting, setIsStarting] = (0, import_react20.useState)(false);
18504
+ const [error, setError] = (0, import_react20.useState)(null);
17875
18505
  const trackNames = Array.from(new Set(
17876
18506
  assignments.filter((a) => a.testCase.track?.name).map((a) => a.testCase.track.name)
17877
18507
  )).slice(0, 6);
17878
18508
  const handleStart = async () => {
17879
18509
  if (isStarting) return;
18510
+ setError(null);
17880
18511
  import_react_native19.Keyboard.dismiss();
17881
18512
  setIsStarting(true);
17882
18513
  try {
@@ -17885,6 +18516,10 @@ function SessionStartScreen({ nav }) {
17885
18516
  });
17886
18517
  if (result.success) {
17887
18518
  nav.replace({ name: "SESSION_ACTIVE" });
18519
+ } else {
18520
+ const msg = result.error || "Failed to start session. Please try again.";
18521
+ console.warn("BugBear: Session start failed:", msg);
18522
+ setError(msg);
17888
18523
  }
17889
18524
  } finally {
17890
18525
  setIsStarting(false);
@@ -17910,7 +18545,7 @@ function SessionStartScreen({ nav }) {
17910
18545
  activeOpacity: 0.7
17911
18546
  },
17912
18547
  /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: [styles5.chipText, focusArea === name && styles5.chipTextActive] }, name)
17913
- )))), /* @__PURE__ */ import_react20.default.createElement(
18548
+ )))), error && /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles5.errorBox }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles5.errorText }, error)), /* @__PURE__ */ import_react20.default.createElement(
17914
18549
  import_react_native19.TouchableOpacity,
17915
18550
  {
17916
18551
  style: [styles5.startBtn, isStarting && styles5.startBtnDisabled],
@@ -17989,6 +18624,20 @@ function createStyles13() {
17989
18624
  chipTextActive: {
17990
18625
  color: colors.blueLight
17991
18626
  },
18627
+ errorBox: {
18628
+ marginBottom: 12,
18629
+ paddingVertical: 10,
18630
+ paddingHorizontal: 12,
18631
+ backgroundColor: "rgba(239, 68, 68, 0.1)",
18632
+ borderWidth: 1,
18633
+ borderColor: "rgba(239, 68, 68, 0.3)",
18634
+ borderRadius: 8
18635
+ },
18636
+ errorText: {
18637
+ fontSize: 13,
18638
+ color: "#f87171",
18639
+ lineHeight: 18
18640
+ },
17992
18641
  startBtn: {
17993
18642
  paddingVertical: 14,
17994
18643
  backgroundColor: colors.blueSurface,