@bbearai/react-native 0.8.3 → 0.9.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.
Files changed (3) hide show
  1. package/dist/index.js +438 -36
  2. package/dist/index.mjs +496 -94
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -12235,6 +12235,9 @@ var BugBearClient = class {
12235
12235
  this.monitor = null;
12236
12236
  this.initialized = false;
12237
12237
  this.initError = null;
12238
+ this._presenceSessionId = null;
12239
+ this._presenceInterval = null;
12240
+ this._presencePaused = false;
12238
12241
  this.config = config;
12239
12242
  if (config.apiKey) {
12240
12243
  this.pendingInit = this.resolveFromApiKey(config.apiKey);
@@ -12932,7 +12935,7 @@ var BugBearClient = class {
12932
12935
  async updateAssignmentStatus(assignmentId, status, options) {
12933
12936
  try {
12934
12937
  await this.ensureReady();
12935
- const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
12938
+ const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at, tester_id, project_id").eq("id", assignmentId).single();
12936
12939
  if (fetchError || !currentAssignment) {
12937
12940
  console.error("BugBear: Assignment not found", {
12938
12941
  message: fetchError?.message,
@@ -12953,6 +12956,19 @@ var BugBearClient = class {
12953
12956
  const completedAt = /* @__PURE__ */ new Date();
12954
12957
  durationSeconds = Math.round((completedAt.getTime() - startedAt.getTime()) / 1e3);
12955
12958
  updateData.duration_seconds = durationSeconds;
12959
+ if (currentAssignment.tester_id && currentAssignment.project_id) {
12960
+ try {
12961
+ const { data: activeTime } = await this.supabase.rpc("compute_assignment_active_time", {
12962
+ p_tester_id: currentAssignment.tester_id,
12963
+ p_project_id: currentAssignment.project_id,
12964
+ p_started_at: currentAssignment.started_at,
12965
+ p_completed_at: updateData.completed_at
12966
+ });
12967
+ updateData.active_seconds = typeof activeTime === "number" ? activeTime : Math.min(durationSeconds, 1800);
12968
+ } catch {
12969
+ updateData.active_seconds = Math.min(durationSeconds, 1800);
12970
+ }
12971
+ }
12956
12972
  }
12957
12973
  }
12958
12974
  if (options?.notes) {
@@ -13031,6 +13047,7 @@ var BugBearClient = class {
13031
13047
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
13032
13048
  completed_at: null,
13033
13049
  duration_seconds: null,
13050
+ active_seconds: null,
13034
13051
  skip_reason: null
13035
13052
  }).eq("id", assignmentId).eq("status", current.status);
13036
13053
  if (error) {
@@ -13880,6 +13897,7 @@ var BugBearClient = class {
13880
13897
  lastMessageAt: row.last_message_at,
13881
13898
  createdAt: row.created_at,
13882
13899
  unreadCount: Number(row.unread_count) || 0,
13900
+ reporterName: row.reporter_name || void 0,
13883
13901
  lastMessage: row.last_message_preview ? {
13884
13902
  id: "",
13885
13903
  threadId: row.thread_id,
@@ -14319,6 +14337,93 @@ var BugBearClient = class {
14319
14337
  updatedAt: data.updated_at
14320
14338
  };
14321
14339
  }
14340
+ // ─── Passive Presence Tracking ──────────────────────────────
14341
+ /** Current presence session ID (null if not tracking). */
14342
+ get presenceSessionId() {
14343
+ return this._presenceSessionId;
14344
+ }
14345
+ /**
14346
+ * Start passive presence tracking for this tester.
14347
+ * Idempotent — reuses an existing active session if one exists.
14348
+ */
14349
+ async startPresence(platform) {
14350
+ try {
14351
+ await this.ensureReady();
14352
+ const testerInfo = await this.getTesterInfo();
14353
+ if (!testerInfo) return null;
14354
+ const { data, error } = await this.supabase.rpc("upsert_tester_presence", {
14355
+ p_project_id: this.config.projectId,
14356
+ p_tester_id: testerInfo.id,
14357
+ p_platform: platform
14358
+ });
14359
+ if (error) {
14360
+ console.error("BugBear: Failed to start presence", formatPgError(error));
14361
+ return null;
14362
+ }
14363
+ this._presenceSessionId = data;
14364
+ this._presencePaused = false;
14365
+ this.startPresenceHeartbeat();
14366
+ return data;
14367
+ } catch (err) {
14368
+ console.error("BugBear: Error starting presence", err);
14369
+ return null;
14370
+ }
14371
+ }
14372
+ /** Gracefully end the current presence session. */
14373
+ async endPresence() {
14374
+ this.stopPresenceHeartbeat();
14375
+ if (!this._presenceSessionId) return;
14376
+ try {
14377
+ await this.supabase.rpc("end_tester_presence", {
14378
+ p_session_id: this._presenceSessionId
14379
+ });
14380
+ } catch {
14381
+ }
14382
+ this._presenceSessionId = null;
14383
+ }
14384
+ /** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
14385
+ pausePresence() {
14386
+ this._presencePaused = true;
14387
+ this.heartbeatPresence();
14388
+ }
14389
+ /** Resume heartbeat after pause. Restarts if session was cleaned up. */
14390
+ async resumePresence() {
14391
+ if (!this._presenceSessionId) return;
14392
+ this._presencePaused = false;
14393
+ try {
14394
+ const { data } = await this.supabase.rpc("heartbeat_tester_presence", {
14395
+ p_session_id: this._presenceSessionId
14396
+ });
14397
+ if (!data) {
14398
+ this._presenceSessionId = null;
14399
+ }
14400
+ } catch {
14401
+ this._presenceSessionId = null;
14402
+ }
14403
+ }
14404
+ async heartbeatPresence() {
14405
+ if (!this._presenceSessionId || this._presencePaused) return;
14406
+ try {
14407
+ const { data, error } = await this.supabase.rpc("heartbeat_tester_presence", {
14408
+ p_session_id: this._presenceSessionId
14409
+ });
14410
+ if (error || data === false) {
14411
+ this.stopPresenceHeartbeat();
14412
+ this._presenceSessionId = null;
14413
+ }
14414
+ } catch {
14415
+ }
14416
+ }
14417
+ startPresenceHeartbeat() {
14418
+ this.stopPresenceHeartbeat();
14419
+ this._presenceInterval = setInterval(() => this.heartbeatPresence(), 6e4);
14420
+ }
14421
+ stopPresenceHeartbeat() {
14422
+ if (this._presenceInterval) {
14423
+ clearInterval(this._presenceInterval);
14424
+ this._presenceInterval = null;
14425
+ }
14426
+ }
14322
14427
  };
14323
14428
  function createBugBear(config) {
14324
14429
  return new BugBearClient(config);
@@ -14453,6 +14558,9 @@ function setActiveColors(palette) {
14453
14558
  colors = palette;
14454
14559
  shared = createSharedStyles();
14455
14560
  }
14561
+ function withAlpha(hex, alpha) {
14562
+ return hex + Math.round(alpha * 255).toString(16).padStart(2, "0");
14563
+ }
14456
14564
  function createSharedStyles() {
14457
14565
  return import_react_native.StyleSheet.create({
14458
14566
  card: {
@@ -14955,6 +15063,28 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14955
15063
  }
14956
15064
  return () => subscription.remove();
14957
15065
  }, [client]);
15066
+ (0, import_react.useEffect)(() => {
15067
+ if (!client || !isTester) return;
15068
+ let mounted = true;
15069
+ const platform = import_react_native2.Platform.OS === "ios" ? "ios" : "android";
15070
+ client.startPresence(platform);
15071
+ const subscription = import_react_native2.AppState.addEventListener("change", (nextState) => {
15072
+ if (nextState === "active") {
15073
+ client.resumePresence().then(() => {
15074
+ if (mounted && !client.presenceSessionId) {
15075
+ client.startPresence(platform);
15076
+ }
15077
+ });
15078
+ } else if (nextState === "background" || nextState === "inactive") {
15079
+ client.pausePresence();
15080
+ }
15081
+ });
15082
+ return () => {
15083
+ mounted = false;
15084
+ subscription.remove();
15085
+ client.endPresence();
15086
+ };
15087
+ }, [client, isTester]);
14958
15088
  (0, import_react.useEffect)(() => {
14959
15089
  if (!client || !isTester) return;
14960
15090
  if (widgetMode === "qa" && !isQAEnabled) return;
@@ -16952,6 +17082,8 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
16952
17082
  const [severity, setSeverity] = (0, import_react12.useState)("medium");
16953
17083
  const [category, setCategory] = (0, import_react12.useState)(null);
16954
17084
  const [description, setDescription] = (0, import_react12.useState)("");
17085
+ const [myIssues, setMyIssues] = (0, import_react12.useState)([]);
17086
+ const [similarReports, setSimilarReports] = (0, import_react12.useState)([]);
16955
17087
  const [affectedScreen, setAffectedScreen] = (0, import_react12.useState)("");
16956
17088
  const [submitting, setSubmitting] = (0, import_react12.useState)(false);
16957
17089
  const [error, setError] = (0, import_react12.useState)(null);
@@ -16967,6 +17099,41 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
16967
17099
  }, [autoCaptureUri, onAutoCaptureConsumed]);
16968
17100
  const isRetestFailure = prefill?.type === "test_fail";
16969
17101
  const isBugType = reportType === "bug" || reportType === "test_fail";
17102
+ (0, import_react12.useEffect)(() => {
17103
+ if (!client?.getIssues) return;
17104
+ let cancelled = false;
17105
+ const load = async () => {
17106
+ try {
17107
+ const [open, done] = await Promise.all([
17108
+ client.getIssues("open"),
17109
+ client.getIssues("done")
17110
+ ]);
17111
+ if (!cancelled) setMyIssues([...open, ...done]);
17112
+ } catch {
17113
+ }
17114
+ };
17115
+ load();
17116
+ return () => {
17117
+ cancelled = true;
17118
+ };
17119
+ }, [client]);
17120
+ (0, import_react12.useEffect)(() => {
17121
+ if (description.length < 10 || myIssues.length === 0) {
17122
+ setSimilarReports([]);
17123
+ return;
17124
+ }
17125
+ const words = description.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
17126
+ if (words.length === 0) {
17127
+ setSimilarReports([]);
17128
+ return;
17129
+ }
17130
+ const scored = myIssues.map((issue) => {
17131
+ const text = `${issue.title || ""} ${issue.description || ""}`.toLowerCase();
17132
+ const matches = words.filter((w) => text.includes(w)).length;
17133
+ return { issue, score: matches / words.length };
17134
+ }).filter((s2) => s2.score >= 0.3).sort((a, b) => b.score - a.score).slice(0, 3);
17135
+ setSimilarReports(scored.map((s2) => s2.issue));
17136
+ }, [description, myIssues]);
16970
17137
  (0, import_react12.useEffect)(() => {
16971
17138
  if (reportType === "feedback" || reportType === "suggestion") {
16972
17139
  setCategory("other");
@@ -17041,7 +17208,41 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
17041
17208
  numberOfLines: 4,
17042
17209
  textAlignVertical: "top"
17043
17210
  }
17044
- )), /* @__PURE__ */ import_react12.default.createElement(
17211
+ ), similarReports.length > 0 && /* @__PURE__ */ import_react12.default.createElement(import_react_native11.View, { style: {
17212
+ marginTop: 8,
17213
+ padding: 12,
17214
+ borderRadius: 8,
17215
+ backgroundColor: withAlpha(colors.orange, 0.1),
17216
+ borderWidth: 1,
17217
+ borderColor: withAlpha(colors.orange, 0.3)
17218
+ } }, /* @__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(
17219
+ import_react_native11.TouchableOpacity,
17220
+ {
17221
+ key: issue.id,
17222
+ onPress: () => nav.push({ name: "ISSUE_DETAIL", issue }),
17223
+ style: {
17224
+ flexDirection: "row",
17225
+ alignItems: "center",
17226
+ gap: 8,
17227
+ paddingVertical: 6
17228
+ }
17229
+ },
17230
+ /* @__PURE__ */ import_react12.default.createElement(import_react_native11.View, { style: {
17231
+ width: 8,
17232
+ height: 8,
17233
+ borderRadius: 4,
17234
+ backgroundColor: issue.severity === "critical" ? colors.red : issue.severity === "high" ? colors.orange : colors.yellow
17235
+ } }),
17236
+ /* @__PURE__ */ import_react12.default.createElement(
17237
+ import_react_native11.Text,
17238
+ {
17239
+ numberOfLines: 1,
17240
+ style: { fontSize: 12, color: colors.textPrimary, flex: 1 }
17241
+ },
17242
+ issue.title
17243
+ ),
17244
+ /* @__PURE__ */ import_react12.default.createElement(import_react_native11.Text, { style: { fontSize: 11, color: colors.textMuted } }, issue.status)
17245
+ )))), /* @__PURE__ */ import_react12.default.createElement(
17045
17246
  ImagePickerButtons,
17046
17247
  {
17047
17248
  images: images.images,
@@ -17097,7 +17298,41 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
17097
17298
  numberOfLines: 4,
17098
17299
  textAlignVertical: "top"
17099
17300
  }
17100
- )), 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(
17301
+ ), similarReports.length > 0 && /* @__PURE__ */ import_react12.default.createElement(import_react_native11.View, { style: {
17302
+ marginTop: 8,
17303
+ padding: 12,
17304
+ borderRadius: 8,
17305
+ backgroundColor: withAlpha(colors.orange, 0.1),
17306
+ borderWidth: 1,
17307
+ borderColor: withAlpha(colors.orange, 0.3)
17308
+ } }, /* @__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(
17309
+ import_react_native11.TouchableOpacity,
17310
+ {
17311
+ key: issue.id,
17312
+ onPress: () => nav.push({ name: "ISSUE_DETAIL", issue }),
17313
+ style: {
17314
+ flexDirection: "row",
17315
+ alignItems: "center",
17316
+ gap: 8,
17317
+ paddingVertical: 6
17318
+ }
17319
+ },
17320
+ /* @__PURE__ */ import_react12.default.createElement(import_react_native11.View, { style: {
17321
+ width: 8,
17322
+ height: 8,
17323
+ borderRadius: 4,
17324
+ backgroundColor: issue.severity === "critical" ? colors.red : issue.severity === "high" ? colors.orange : colors.yellow
17325
+ } }),
17326
+ /* @__PURE__ */ import_react12.default.createElement(
17327
+ import_react_native11.Text,
17328
+ {
17329
+ numberOfLines: 1,
17330
+ style: { fontSize: 12, color: colors.textPrimary, flex: 1 }
17331
+ },
17332
+ issue.title
17333
+ ),
17334
+ /* @__PURE__ */ import_react12.default.createElement(import_react_native11.Text, { style: { fontSize: 11, color: colors.textMuted } }, issue.status)
17335
+ )))), 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(
17101
17336
  import_react_native11.TextInput,
17102
17337
  {
17103
17338
  style: styles5.screenInput,
@@ -17178,6 +17413,19 @@ var import_react_native13 = require("react-native");
17178
17413
  function MessageListScreen({ nav }) {
17179
17414
  const { threads, unreadCount, refreshThreads, dashboardUrl, isLoading, widgetColorScheme } = useBugBear();
17180
17415
  const styles5 = (0, import_react14.useMemo)(() => createStyles7(), [widgetColorScheme]);
17416
+ const [activeFilter, setActiveFilter] = (0, import_react14.useState)("all");
17417
+ const filteredThreads = threads.filter((thread) => {
17418
+ if (activeFilter === "all") return true;
17419
+ if (activeFilter === "unread") return thread.unreadCount > 0;
17420
+ return thread.threadType === activeFilter;
17421
+ });
17422
+ const filterChips = [
17423
+ { key: "all", label: "All" },
17424
+ { key: "report", label: "Bug Reports" },
17425
+ { key: "direct", label: "Direct" },
17426
+ { key: "announcement", label: "Announcements" },
17427
+ { key: "unread", label: "Unread", count: threads.filter((t) => t.unreadCount > 0).length }
17428
+ ];
17181
17429
  if (isLoading) return /* @__PURE__ */ import_react14.default.createElement(MessageListScreenSkeleton, null);
17182
17430
  return /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, null, /* @__PURE__ */ import_react14.default.createElement(
17183
17431
  import_react_native13.TouchableOpacity,
@@ -17186,14 +17434,54 @@ function MessageListScreen({ nav }) {
17186
17434
  onPress: () => nav.push({ name: "COMPOSE_MESSAGE" })
17187
17435
  },
17188
17436
  /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.newMsgText }, "\u2709\uFE0F New Message")
17189
- ), 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(
17437
+ ), /* @__PURE__ */ import_react14.default.createElement(
17438
+ import_react_native13.ScrollView,
17439
+ {
17440
+ horizontal: true,
17441
+ showsHorizontalScrollIndicator: false,
17442
+ style: { paddingBottom: 12 },
17443
+ contentContainerStyle: { paddingHorizontal: 16, gap: 8 }
17444
+ },
17445
+ filterChips.map((chip) => /* @__PURE__ */ import_react14.default.createElement(
17446
+ import_react_native13.TouchableOpacity,
17447
+ {
17448
+ key: chip.key,
17449
+ onPress: () => setActiveFilter(chip.key),
17450
+ style: {
17451
+ paddingVertical: 6,
17452
+ paddingHorizontal: 12,
17453
+ borderRadius: 16,
17454
+ borderWidth: 1,
17455
+ borderColor: activeFilter === chip.key ? colors.blue : colors.border,
17456
+ backgroundColor: activeFilter === chip.key ? withAlpha(colors.blue, 0.15) : "transparent",
17457
+ flexDirection: "row",
17458
+ alignItems: "center",
17459
+ gap: 4
17460
+ }
17461
+ },
17462
+ /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: {
17463
+ fontSize: 12,
17464
+ fontWeight: "500",
17465
+ color: activeFilter === chip.key ? colors.blue : colors.textSecondary
17466
+ } }, chip.label),
17467
+ chip.count !== void 0 && chip.count > 0 && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: {
17468
+ backgroundColor: colors.blue,
17469
+ borderRadius: 8,
17470
+ minWidth: 16,
17471
+ height: 16,
17472
+ justifyContent: "center",
17473
+ alignItems: "center",
17474
+ paddingHorizontal: 4
17475
+ } }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: { fontSize: 10, fontWeight: "bold", color: colors.onPrimary } }, chip.count))
17476
+ ))
17477
+ ), 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(
17190
17478
  import_react_native13.TouchableOpacity,
17191
17479
  {
17192
17480
  key: thread.id,
17193
17481
  style: [styles5.threadItem, thread.unreadCount > 0 && styles5.threadItemUnread],
17194
17482
  onPress: () => nav.push({ name: "THREAD_DETAIL", thread })
17195
17483
  },
17196
- /* @__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))),
17484
+ /* @__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))),
17197
17485
  /* @__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) }] }))
17198
17486
  ))), dashboardUrl && /* @__PURE__ */ import_react14.default.createElement(
17199
17487
  import_react_native13.TouchableOpacity,
@@ -17203,7 +17491,7 @@ function MessageListScreen({ nav }) {
17203
17491
  activeOpacity: 0.7
17204
17492
  },
17205
17493
  /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles5.dashboardLinkText }, "\u{1F310}", " View on Dashboard ", "\u2192")
17206
- ), /* @__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"))));
17494
+ ), /* @__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"))));
17207
17495
  }
17208
17496
  function createStyles7() {
17209
17497
  return import_react_native13.StyleSheet.create({
@@ -17584,7 +17872,17 @@ function IssueListScreen({ nav, category }) {
17584
17872
  const [loading, setLoading] = (0, import_react18.useState)(true);
17585
17873
  const [counts, setCounts] = (0, import_react18.useState)(null);
17586
17874
  const [sortMode, setSortMode] = (0, import_react18.useState)("severity");
17875
+ const [searchQuery, setSearchQuery] = (0, import_react18.useState)("");
17876
+ const [debouncedQuery, setDebouncedQuery] = (0, import_react18.useState)("");
17587
17877
  const config = CATEGORY_CONFIG[activeCategory];
17878
+ (0, import_react18.useEffect)(() => {
17879
+ const timer = setTimeout(() => setDebouncedQuery(searchQuery), 300);
17880
+ return () => clearTimeout(timer);
17881
+ }, [searchQuery]);
17882
+ (0, import_react18.useEffect)(() => {
17883
+ setSearchQuery("");
17884
+ setDebouncedQuery("");
17885
+ }, [activeCategory]);
17588
17886
  (0, import_react18.useEffect)(() => {
17589
17887
  if (!client) return;
17590
17888
  client.getIssueCounts().then(setCounts).catch(() => {
@@ -17624,6 +17922,9 @@ function IssueListScreen({ nav, category }) {
17624
17922
  }
17625
17923
  return sorted;
17626
17924
  }, [issues, sortMode]);
17925
+ const searchFilteredIssues = debouncedQuery ? sortedIssues.filter(
17926
+ (issue) => (issue.title || "").toLowerCase().includes(debouncedQuery.toLowerCase()) || (issue.description || "").toLowerCase().includes(debouncedQuery.toLowerCase())
17927
+ ) : sortedIssues;
17627
17928
  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) => {
17628
17929
  const catConfig = CATEGORY_CONFIG[cat];
17629
17930
  const isActive = activeCategory === cat;
@@ -17670,7 +17971,25 @@ function IssueListScreen({ nav, category }) {
17670
17971
  styles5.sortBtnText,
17671
17972
  sortMode === s2.key && styles5.sortBtnTextActive
17672
17973
  ] }, s2.label)
17673
- ))), 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(
17974
+ ))), /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: { paddingHorizontal: 16, paddingBottom: 12 } }, /* @__PURE__ */ import_react18.default.createElement(
17975
+ import_react_native17.TextInput,
17976
+ {
17977
+ value: searchQuery,
17978
+ onChangeText: setSearchQuery,
17979
+ placeholder: "Search my reports...",
17980
+ placeholderTextColor: colors.textMuted,
17981
+ style: {
17982
+ padding: 8,
17983
+ paddingHorizontal: 12,
17984
+ borderRadius: 8,
17985
+ borderWidth: 1,
17986
+ borderColor: colors.border,
17987
+ backgroundColor: colors.card,
17988
+ color: colors.textPrimary,
17989
+ fontSize: 13
17990
+ }
17991
+ }
17992
+ )), 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(
17674
17993
  import_react_native17.TouchableOpacity,
17675
17994
  {
17676
17995
  key: issue.id,
@@ -18196,11 +18515,13 @@ function SessionStartScreen({ nav }) {
18196
18515
  const styles5 = (0, import_react20.useMemo)(() => createStyles13(), [widgetColorScheme]);
18197
18516
  const [focusArea, setFocusArea] = (0, import_react20.useState)("");
18198
18517
  const [isStarting, setIsStarting] = (0, import_react20.useState)(false);
18518
+ const [error, setError] = (0, import_react20.useState)(null);
18199
18519
  const trackNames = Array.from(new Set(
18200
18520
  assignments.filter((a) => a.testCase.track?.name).map((a) => a.testCase.track.name)
18201
18521
  )).slice(0, 6);
18202
18522
  const handleStart = async () => {
18203
18523
  if (isStarting) return;
18524
+ setError(null);
18204
18525
  import_react_native19.Keyboard.dismiss();
18205
18526
  setIsStarting(true);
18206
18527
  try {
@@ -18209,6 +18530,10 @@ function SessionStartScreen({ nav }) {
18209
18530
  });
18210
18531
  if (result.success) {
18211
18532
  nav.replace({ name: "SESSION_ACTIVE" });
18533
+ } else {
18534
+ const msg = result.error || "Failed to start session. Please try again.";
18535
+ console.warn("BugBear: Session start failed:", msg);
18536
+ setError(msg);
18212
18537
  }
18213
18538
  } finally {
18214
18539
  setIsStarting(false);
@@ -18234,7 +18559,7 @@ function SessionStartScreen({ nav }) {
18234
18559
  activeOpacity: 0.7
18235
18560
  },
18236
18561
  /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: [styles5.chipText, focusArea === name && styles5.chipTextActive] }, name)
18237
- )))), /* @__PURE__ */ import_react20.default.createElement(
18562
+ )))), 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(
18238
18563
  import_react_native19.TouchableOpacity,
18239
18564
  {
18240
18565
  style: [styles5.startBtn, isStarting && styles5.startBtnDisabled],
@@ -18313,6 +18638,20 @@ function createStyles13() {
18313
18638
  chipTextActive: {
18314
18639
  color: colors.blueLight
18315
18640
  },
18641
+ errorBox: {
18642
+ marginBottom: 12,
18643
+ paddingVertical: 10,
18644
+ paddingHorizontal: 12,
18645
+ backgroundColor: "rgba(239, 68, 68, 0.1)",
18646
+ borderWidth: 1,
18647
+ borderColor: "rgba(239, 68, 68, 0.3)",
18648
+ borderRadius: 8
18649
+ },
18650
+ errorText: {
18651
+ fontSize: 13,
18652
+ color: "#f87171",
18653
+ lineHeight: 18
18654
+ },
18316
18655
  startBtn: {
18317
18656
  paddingVertical: 14,
18318
18657
  backgroundColor: colors.blueSurface,
@@ -18790,14 +19129,31 @@ function BugBearButton({
18790
19129
  }) {
18791
19130
  const { shouldShowWidget, testerInfo, isLoading, unreadCount, assignments, widgetMode, widgetColorScheme } = useBugBear();
18792
19131
  const { currentScreen, canGoBack, push, pop, replace, reset } = useNavigation();
18793
- const [modalVisible, setModalVisible] = (0, import_react23.useState)(false);
19132
+ const [panelVisible, setPanelVisible] = (0, import_react23.useState)(false);
19133
+ const panelAnim = (0, import_react23.useRef)(new import_react_native22.Animated.Value(0)).current;
18794
19134
  const styles5 = (0, import_react23.useMemo)(() => createStyles16(), [widgetColorScheme]);
18795
19135
  const screenCaptureRef = (0, import_react23.useRef)(null);
18796
- const openModal = () => {
19136
+ const openPanel = () => {
18797
19137
  captureAppScreen().then((uri) => {
18798
19138
  screenCaptureRef.current = uri;
18799
19139
  });
18800
- setModalVisible(true);
19140
+ setPanelVisible(true);
19141
+ import_react_native22.Animated.spring(panelAnim, {
19142
+ toValue: 1,
19143
+ useNativeDriver: true,
19144
+ friction: 8,
19145
+ tension: 65
19146
+ }).start();
19147
+ };
19148
+ const closePanel = () => {
19149
+ import_react_native22.Keyboard.dismiss();
19150
+ import_react_native22.Animated.timing(panelAnim, {
19151
+ toValue: 0,
19152
+ duration: 250,
19153
+ useNativeDriver: true
19154
+ }).start(() => {
19155
+ setPanelVisible(false);
19156
+ });
18801
19157
  };
18802
19158
  const getInitialPosition = () => {
18803
19159
  const buttonSize = 56;
@@ -18848,7 +19204,7 @@ function BugBearButton({
18848
19204
  tension: 40
18849
19205
  }).start();
18850
19206
  if (!isDragging.current && Math.abs(gs.dx) < 5 && Math.abs(gs.dy) < 5) {
18851
- openModal();
19207
+ openPanel();
18852
19208
  }
18853
19209
  isDragging.current = false;
18854
19210
  }
@@ -18856,6 +19212,19 @@ function BugBearButton({
18856
19212
  ).current;
18857
19213
  const pendingTests = widgetMode === "qa" ? assignments.filter((a) => a.status === "pending" || a.status === "in_progress").length : 0;
18858
19214
  const badgeCount = pendingTests + unreadCount;
19215
+ (0, import_react23.useEffect)(() => {
19216
+ if (!panelVisible || import_react_native22.Platform.OS !== "android") return;
19217
+ const handler = import_react_native22.BackHandler.addEventListener("hardwareBackPress", () => {
19218
+ if (canGoBack) {
19219
+ import_react_native22.Keyboard.dismiss();
19220
+ pop();
19221
+ } else {
19222
+ closePanel();
19223
+ }
19224
+ return true;
19225
+ });
19226
+ return () => handler.remove();
19227
+ }, [panelVisible, canGoBack]);
18859
19228
  if (!shouldShowWidget) return null;
18860
19229
  const getHeaderTitle = () => {
18861
19230
  switch (currentScreen.name) {
@@ -18894,8 +19263,7 @@ function BugBearButton({
18894
19263
  }
18895
19264
  };
18896
19265
  const handleClose = () => {
18897
- import_react_native22.Keyboard.dismiss();
18898
- setModalVisible(false);
19266
+ closePanel();
18899
19267
  };
18900
19268
  const nav = {
18901
19269
  push: (screen) => {
@@ -18967,7 +19335,7 @@ function BugBearButton({
18967
19335
  return /* @__PURE__ */ import_react23.default.createElement(HomeScreen, { nav });
18968
19336
  }
18969
19337
  };
18970
- return /* @__PURE__ */ import_react23.default.createElement(import_react23.default.Fragment, null, /* @__PURE__ */ import_react23.default.createElement(
19338
+ return /* @__PURE__ */ import_react23.default.createElement(import_react23.default.Fragment, null, !panelVisible && /* @__PURE__ */ import_react23.default.createElement(
18971
19339
  import_react_native22.Animated.View,
18972
19340
  {
18973
19341
  style: [styles5.fabContainer, { transform: pan.getTranslateTransform() }, buttonStyle],
@@ -18977,27 +19345,37 @@ function BugBearButton({
18977
19345
  import_react_native22.TouchableOpacity,
18978
19346
  {
18979
19347
  style: styles5.fab,
18980
- onPress: openModal,
19348
+ onPress: openPanel,
18981
19349
  activeOpacity: draggable ? 1 : 0.7
18982
19350
  },
18983
19351
  /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Image, { source: { uri: BUGBEAR_LOGO_BASE64 }, style: styles5.fabIcon }),
18984
19352
  badgeCount > 0 && /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.badge }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.badgeText }, badgeCount > 9 ? "9+" : badgeCount))
18985
19353
  )
18986
- ), /* @__PURE__ */ import_react23.default.createElement(
18987
- import_react_native22.Modal,
19354
+ ), panelVisible && /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.panelWrapper, pointerEvents: "box-none" }, /* @__PURE__ */ import_react23.default.createElement(
19355
+ import_react_native22.KeyboardAvoidingView,
18988
19356
  {
18989
- visible: modalVisible,
18990
- animationType: "slide",
18991
- transparent: true,
18992
- onRequestClose: handleClose
19357
+ behavior: import_react_native22.Platform.OS === "ios" ? "padding" : "height",
19358
+ style: styles5.panelKeyboardAvoid,
19359
+ pointerEvents: "box-none"
18993
19360
  },
18994
19361
  /* @__PURE__ */ import_react23.default.createElement(
18995
- import_react_native22.KeyboardAvoidingView,
19362
+ import_react_native22.Animated.View,
18996
19363
  {
18997
- behavior: import_react_native22.Platform.OS === "ios" ? "padding" : "height",
18998
- style: styles5.modalOverlay
19364
+ style: [
19365
+ styles5.panelContainer,
19366
+ {
19367
+ transform: [{
19368
+ translateY: panelAnim.interpolate({
19369
+ inputRange: [0, 1],
19370
+ outputRange: [screenHeight, 0]
19371
+ })
19372
+ }]
19373
+ }
19374
+ ]
18999
19375
  },
19000
- /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.modalContainer }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.header }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.headerLeft }, canGoBack ? /* @__PURE__ */ import_react23.default.createElement(import_react_native22.TouchableOpacity, { onPress: () => nav.pop(), style: styles5.backButton }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.backText }, "\u2190 Back")) : /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.headerTitleRow }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ import_react23.default.createElement(import_react_native22.TouchableOpacity, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.headerActions }, currentScreen.name !== "HOME" && /* @__PURE__ */ import_react23.default.createElement(import_react_native22.TouchableOpacity, { onPress: () => nav.reset(), style: styles5.homeButton }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.homeIcon }, "\u2302")), /* @__PURE__ */ import_react23.default.createElement(import_react_native22.TouchableOpacity, { onPress: handleClose, style: styles5.closeButton }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.closeText }, "\u2715")))), /* @__PURE__ */ import_react23.default.createElement(
19376
+ /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.panelHandle }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.panelHandleBar })),
19377
+ /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.header }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.headerLeft }, canGoBack ? /* @__PURE__ */ import_react23.default.createElement(import_react_native22.TouchableOpacity, { onPress: () => nav.pop(), style: styles5.backButton }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.backText }, "\u2190 Back")) : /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.headerTitleRow }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ import_react23.default.createElement(import_react_native22.TouchableOpacity, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.headerActions }, currentScreen.name !== "HOME" && /* @__PURE__ */ import_react23.default.createElement(import_react_native22.TouchableOpacity, { onPress: () => nav.reset(), style: styles5.homeButton }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.homeIcon }, "\u2302")), /* @__PURE__ */ import_react23.default.createElement(import_react_native22.TouchableOpacity, { onPress: handleClose, style: styles5.closeButton }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.closeText }, "\u2715")))),
19378
+ /* @__PURE__ */ import_react23.default.createElement(
19001
19379
  import_react_native22.ScrollView,
19002
19380
  {
19003
19381
  style: styles5.content,
@@ -19006,9 +19384,9 @@ function BugBearButton({
19006
19384
  showsVerticalScrollIndicator: false
19007
19385
  },
19008
19386
  isLoading ? /* @__PURE__ */ import_react23.default.createElement(import_react_native22.View, { style: styles5.loadingContainer }, /* @__PURE__ */ import_react23.default.createElement(import_react_native22.ActivityIndicator, { size: "large", color: colors.blue }), /* @__PURE__ */ import_react23.default.createElement(import_react_native22.Text, { style: styles5.loadingText }, "Loading...")) : renderScreen()
19009
- ))
19387
+ )
19010
19388
  )
19011
- ));
19389
+ )));
19012
19390
  }
19013
19391
  function createStyles16() {
19014
19392
  return import_react_native22.StyleSheet.create({
@@ -19052,18 +19430,42 @@ function createStyles16() {
19052
19430
  fontSize: 11,
19053
19431
  fontWeight: "700"
19054
19432
  },
19055
- // Modal
19056
- modalOverlay: {
19433
+ // Panel (replaces Modal — no backdrop, app stays interactive)
19434
+ panelWrapper: {
19435
+ position: "absolute",
19436
+ top: 0,
19437
+ left: 0,
19438
+ right: 0,
19439
+ bottom: 0,
19440
+ zIndex: 9998,
19441
+ justifyContent: "flex-end"
19442
+ },
19443
+ panelKeyboardAvoid: {
19057
19444
  flex: 1,
19058
- justifyContent: "flex-end",
19059
- backgroundColor: "rgba(0,0,0,0.5)"
19445
+ justifyContent: "flex-end"
19060
19446
  },
19061
- modalContainer: {
19447
+ panelContainer: {
19062
19448
  backgroundColor: colors.bg,
19063
19449
  borderTopLeftRadius: 20,
19064
19450
  borderTopRightRadius: 20,
19065
- maxHeight: "85%",
19066
- minHeight: "50%"
19451
+ maxHeight: "70%",
19452
+ minHeight: "40%",
19453
+ shadowColor: colors.onBright,
19454
+ shadowOffset: { width: 0, height: -4 },
19455
+ shadowOpacity: 0.25,
19456
+ shadowRadius: 12,
19457
+ elevation: 16
19458
+ },
19459
+ panelHandle: {
19460
+ alignItems: "center",
19461
+ paddingTop: 8,
19462
+ paddingBottom: 4
19463
+ },
19464
+ panelHandleBar: {
19465
+ width: 36,
19466
+ height: 4,
19467
+ borderRadius: 2,
19468
+ backgroundColor: colors.border
19067
19469
  },
19068
19470
  // Header
19069
19471
  header: {
@@ -19146,7 +19548,7 @@ function createStyles16() {
19146
19548
  },
19147
19549
  contentContainer: {
19148
19550
  padding: 16,
19149
- paddingBottom: 32
19551
+ paddingBottom: import_react_native22.Platform.OS === "ios" ? 50 : 28
19150
19552
  },
19151
19553
  // Loading
19152
19554
  loadingContainer: {