@buoy-gg/storage 7.0.32 → 7.0.34

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.
@@ -108,24 +108,12 @@ async function capture() {
108
108
  }
109
109
  async function restore(data, options) {
110
110
  const snapshot = data;
111
- const excludedSet = new Set(options?.excludeKeys ?? []);
112
- const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
111
+ const scope = (0, _sharedUi.createRestoreScope)(options);
113
112
  /** True when this item may be touched (scope + exclusions agree). */
114
- const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
115
- let excludedHits = 0;
116
- const result = {
117
- ok: true,
118
- applied: 0,
119
- skipped: [],
120
- warnings: []
121
- };
113
+ const touch = scope.touch;
114
+ const result = (0, _sharedUi.emptyRestoreResult)();
122
115
  if (!snapshot) {
123
- return {
124
- ok: false,
125
- applied: 0,
126
- skipped: [],
127
- warnings: ["empty storage snapshot"]
128
- };
116
+ return (0, _sharedUi.failedRestore)("empty storage snapshot");
129
117
  }
130
118
 
131
119
  // ── AsyncStorage: delete app keys missing from the snapshot, write the rest ──
@@ -134,7 +122,7 @@ async function restore(data, options) {
134
122
  const snapKeys = new Set(snapPairs.map(([k]) => k));
135
123
  const liveKeys = (await _asyncStorage.default.getAllKeys()).filter(k => !(0, _sharedUi.isDevToolsStorageKey)(k));
136
124
  const toRemove = liveKeys.filter(k => !snapKeys.has(k) && touch(`async:${k}`));
137
- excludedHits += (snapshot.async ?? []).length - snapPairs.length;
125
+ scope.countExcluded((snapshot.async ?? []).length - snapPairs.length);
138
126
  if (toRemove.length > 0) await (0, _asyncStorageCompat.removeMany)(toRemove);
139
127
  if (snapPairs.length > 0) await (0, _asyncStorageCompat.writeMany)(snapPairs);
140
128
  result.applied += snapPairs.length + toRemove.length;
@@ -167,10 +155,7 @@ async function restore(data, options) {
167
155
  const snapKeys = new Set(entries.map(e => e.key));
168
156
  const liveKeys = instance.getAllKeys().filter(key => !(0, _sharedUi.isDevToolsStorageKey)(key));
169
157
  for (const key of liveKeys) {
170
- if (!touch(`mmkv:${id}/${key}`)) {
171
- excludedHits += 1;
172
- continue;
173
- }
158
+ if (!scope.allows(`mmkv:${id}/${key}`)) continue;
174
159
  if (!snapKeys.has(key)) {
175
160
  deleteMMKVKey(instance, key);
176
161
  result.applied += 1;
@@ -201,10 +186,7 @@ async function restore(data, options) {
201
186
  key,
202
187
  options
203
188
  } of _SecureStoreRegistry.secureStoreRegistry.getAll()) {
204
- if (!touch(`secure:${key}`)) {
205
- excludedHits += 1;
206
- continue;
207
- }
189
+ if (!scope.allows(`secure:${key}`)) continue;
208
190
  if (options.requireAuthentication) {
209
191
  if (key in secureSnap) {
210
192
  result.skipped.push({
@@ -237,20 +219,14 @@ async function restore(data, options) {
237
219
  });
238
220
  }
239
221
  }
240
- if (excludedHits > 0) {
241
- result.warnings.push(`${excludedHits} item(s) excluded by you`);
242
- }
222
+ const excludedWarning = scope.excludedWarning("item(s)");
223
+ if (excludedWarning) result.warnings.push(excludedWarning);
243
224
  return result;
244
225
  }
245
226
 
246
227
  /** Wipe to fresh-install state (Buoy keys preserved). Used by baseline restore. */
247
228
  async function clear() {
248
- const result = {
249
- ok: true,
250
- applied: 0,
251
- skipped: [],
252
- warnings: []
253
- };
229
+ const result = (0, _sharedUi.emptyRestoreResult)();
254
230
  try {
255
231
  await (0, _clearAllStorage.clearAllAppStorage)();
256
232
  result.applied += 1;
@@ -301,40 +277,24 @@ async function clear() {
301
277
  return result;
302
278
  }
303
279
  const ITEM_RENDER_CHAR_LIMIT = 256 * 1024;
280
+
281
+ /**
282
+ * Storage values are strings (or MMKV primitives): identity equality and a
283
+ * character-length size gate are the right tests, not a deep walk.
284
+ */
304
285
  function classifyValue(key, label, oldValue, newValue, inOld, inNew) {
305
- if (!inNew) return {
286
+ return (0, _sharedUi.classifyPreviewItem)({
306
287
  key,
307
288
  label,
308
- verdict: "removed",
309
- oldValue
310
- };
311
- if (!inOld) return {
312
- key,
313
- label,
314
- verdict: "added",
315
- newValue
316
- };
317
- if (Object.is(oldValue, newValue)) return {
318
- key,
319
- label,
320
- verdict: "unchanged"
321
- };
322
- const tooLarge = typeof oldValue === "string" && oldValue.length > ITEM_RENDER_CHAR_LIMIT || typeof newValue === "string" && newValue.length > ITEM_RENDER_CHAR_LIMIT;
323
- if (tooLarge) {
324
- return {
325
- key,
326
- label,
327
- verdict: "too-large",
328
- reason: "value too large to render a diff — restore still applies it"
329
- };
330
- }
331
- return {
332
- key,
333
- label,
334
- verdict: "changed",
335
289
  oldValue,
336
- newValue
337
- };
290
+ newValue,
291
+ inOld,
292
+ inNew
293
+ }, {
294
+ equal: Object.is,
295
+ tooLarge: value => typeof value === "string" && value.length > ITEM_RENDER_CHAR_LIMIT,
296
+ tooLargeReason: "value too large to render a diff — restore still applies it"
297
+ });
338
298
  }
339
299
 
340
300
  /** Per-key restore preview across all three backends. */
@@ -356,46 +316,31 @@ async function previewItems(target, base) {
356
316
  // ── AsyncStorage ──
357
317
  const oldAsync = new Map(baseData.async ?? []);
358
318
  const newAsync = new Map(targetData.async ?? []);
359
- for (const key of new Set([...oldAsync.keys(), ...newAsync.keys()])) {
319
+ for (const key of (0, _sharedUi.unionKeys)(oldAsync, newAsync)) {
360
320
  if ((0, _sharedUi.isDevToolsStorageKey)(key)) continue;
361
321
  items.push(classifyValue(`async:${key}`, key, oldAsync.get(key), newAsync.get(key), oldAsync.has(key), newAsync.has(key)));
362
322
  }
363
323
 
364
324
  // ── MMKV ──
365
- const instanceIds = new Set([...Object.keys(baseData.mmkv ?? {}), ...Object.keys(targetData.mmkv ?? {})]);
366
- for (const id of instanceIds) {
325
+ for (const id of (0, _sharedUi.unionKeys)(baseData.mmkv, targetData.mmkv)) {
367
326
  const registered = liveMode ? _MMKVInstanceRegistry.mmkvInstanceRegistry.get(id) : null;
368
327
  const instanceUnavailable = liveMode && !registered;
369
328
  const readOnly = liveMode && registered?.instance.isReadOnly === true;
370
329
  const oldEntries = new Map((baseData.mmkv?.[id] ?? []).map(e => [e.key, e]));
371
330
  const newEntries = new Map((targetData.mmkv?.[id] ?? []).map(e => [e.key, e]));
372
- for (const key of new Set([...oldEntries.keys(), ...newEntries.keys()])) {
331
+ for (const key of (0, _sharedUi.unionKeys)(oldEntries, newEntries)) {
373
332
  if ((0, _sharedUi.isDevToolsStorageKey)(key)) continue;
374
333
  const itemKey = `mmkv:${id}/${key}`;
375
334
  const label = `${id} · ${key}`;
376
335
  const oldE = oldEntries.get(key);
377
336
  const newE = newEntries.get(key);
378
337
  let item = classifyValue(itemKey, label, oldE?.value, newE?.value, oldEntries.has(key), newEntries.has(key));
379
- if (item.verdict !== "unchanged") {
380
- if (instanceUnavailable) {
381
- item = {
382
- ...item,
383
- verdict: "wont-apply",
384
- reason: "MMKV instance not registered"
385
- };
386
- } else if (readOnly) {
387
- item = {
388
- ...item,
389
- verdict: "wont-apply",
390
- reason: "MMKV instance registered read-only"
391
- };
392
- } else if (newE?.valueType === "buffer" || oldE?.valueType === "buffer") {
393
- item = {
394
- ...item,
395
- verdict: "wont-apply",
396
- reason: "ArrayBuffer values cannot be restored"
397
- };
398
- }
338
+ if (instanceUnavailable) {
339
+ item = (0, _sharedUi.wontApply)(item, "MMKV instance not registered");
340
+ } else if (readOnly) {
341
+ item = (0, _sharedUi.wontApply)(item, "MMKV instance registered read-only");
342
+ } else if (newE?.valueType === "buffer" || oldE?.valueType === "buffer") {
343
+ item = (0, _sharedUi.wontApply)(item, "ArrayBuffer values cannot be restored");
399
344
  }
400
345
  items.push(item);
401
346
  }
@@ -405,22 +350,14 @@ async function previewItems(target, base) {
405
350
  const oldSecure = baseData.secure ?? {};
406
351
  const newSecure = targetData.secure ?? {};
407
352
  const registeredSecure = liveMode ? new Map(_SecureStoreRegistry.secureStoreRegistry.getAll().map(e => [e.key, e])) : null;
408
- for (const key of new Set([...Object.keys(oldSecure), ...Object.keys(newSecure)])) {
353
+ for (const key of (0, _sharedUi.unionKeys)(oldSecure, newSecure)) {
409
354
  let item = classifyValue(`secure:${key}`, key, oldSecure[key], newSecure[key], key in oldSecure, key in newSecure);
410
- if (liveMode && item.verdict !== "unchanged") {
355
+ if (liveMode) {
411
356
  const entry = registeredSecure?.get(key);
412
357
  if (!entry) {
413
- item = {
414
- ...item,
415
- verdict: "wont-apply",
416
- reason: "key no longer registered with registerSecureStoreKeys"
417
- };
358
+ item = (0, _sharedUi.wontApply)(item, "key no longer registered with registerSecureStoreKeys");
418
359
  } else if (entry.options.requireAuthentication) {
419
- item = {
420
- ...item,
421
- verdict: "wont-apply",
422
- reason: "biometric-protected keys are never written"
423
- };
360
+ item = (0, _sharedUi.wontApply)(item, "biometric-protected keys are never written");
424
361
  }
425
362
  }
426
363
  items.push(item);
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import { StorageIcon } from "@buoy-gg/floating-tools-core";
21
+ import { createToolFactory, defineToolPreset } from "@buoy-gg/shared-ui";
21
22
  import { StorageModalWithTabs } from "./storage/components/StorageModalWithTabs";
22
23
  import { jsx as _jsx } from "react/jsx-runtime";
23
24
  /**
@@ -28,7 +29,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
28
29
  * - Live storage event monitoring
29
30
  * - Required key validation
30
31
  */
31
- export const storageToolPreset = {
32
+ export const storageToolPreset = defineToolPreset({
32
33
  id: "storage",
33
34
  name: "STORAGE",
34
35
  description: "AsyncStorage browser",
@@ -43,7 +44,7 @@ export const storageToolPreset = {
43
44
  requiredStorageKeys: [],
44
45
  enableSharedModalDimensions: false
45
46
  }
46
- };
47
+ });
47
48
 
48
49
  /**
49
50
  * Create a custom AsyncStorage browser tool configuration.
@@ -69,21 +70,7 @@ export const storageToolPreset = {
69
70
  * });
70
71
  * ```
71
72
  */
72
- export function createStorageTool(options) {
73
- return {
74
- id: options?.id || "storage",
75
- name: options?.name || "STORAGE",
76
- description: options?.description || "AsyncStorage browser",
77
- slot: "both",
78
- icon: ({
79
- size
80
- }) => /*#__PURE__*/_jsx(StorageIcon, {
81
- size: size
82
- }),
83
- component: StorageModalWithTabs,
84
- props: {
85
- requiredStorageKeys: options?.requiredStorageKeys || [],
86
- enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
87
- }
88
- };
89
- }
73
+ export const createStorageTool = createToolFactory(storageToolPreset, options => ({
74
+ requiredStorageKeys: options.requiredStorageKeys || [],
75
+ enableSharedModalDimensions: options.enableSharedModalDimensions ?? false
76
+ }));
@@ -3,7 +3,7 @@
3
3
  import { useMemo, useCallback, useState, useEffect, useRef } from "react";
4
4
  import { StyleSheet, Text, View, TouchableOpacity, ScrollView, Alert } from "react-native";
5
5
  import { absoluteFill, useLimit, TIER_LIMITS } from "@buoy-gg/shared-ui";
6
- import { Database, Zap, ProBadge, ProUpgradeModal } from "@buoy-gg/shared-ui";
6
+ import { Database, Zap, ProBadge, ProUpgradeModal, ToolEmptyState, resolveListEmptyState } from "@buoy-gg/shared-ui";
7
7
  import { isDevToolsStorageKey } from "@buoy-gg/shared-ui";
8
8
  import { StorageKeySection, storageRowId } from "./StorageKeySection";
9
9
  import { StorageKeyDetailScreen } from "./StorageKeyDetailScreen";
@@ -859,19 +859,60 @@ export function GameUIStorageBrowser({
859
859
  children: searchQuery ? "Upgrade to Pro to search your full storage" : "Upgrade to Pro to unlock full storage access"
860
860
  })]
861
861
  }) : null;
862
- const emptyStateNode = /*#__PURE__*/_jsxs(View, {
863
- style: styles.emptyState,
864
- children: [/*#__PURE__*/_jsx(Database, {
865
- size: 32,
866
- color: night.color.textTertiary
867
- }), /*#__PURE__*/_jsx(Text, {
868
- style: styles.emptyTitle,
869
- children: searchQuery ? "No results found" : activeFilter === "all" ? "No valid keys found" : activeFilter === "missing" ? "No missing keys" : "No issues found"
870
- }), /*#__PURE__*/_jsx(Text, {
871
- style: styles.emptySubtitle,
872
- children: searchQuery ? `No keys matching "${searchQuery}"` : activeFilter === "all" ? "All keys have issues or are missing" : activeFilter === "missing" ? "All required keys are present" : "All storage keys are correctly configured"
873
- })]
874
- });
862
+ const emptyStateNode = (() => {
863
+ // Short-circuits on the first real key, so this costs nothing.
864
+ const hasAnyKeys = allStorageKeys.some(k => !isDevToolsStorageKey(k.key));
865
+
866
+ // Three causes here are unlike any other tool's: an empty MISSING or
867
+ // ISSUES list is a PASS, not a failure, and an empty VALID list means
868
+ // everything is broken. Those skip the shared resolver and say so.
869
+ let content;
870
+ if (searchQuery || !hasAnyKeys) {
871
+ content = resolveListEmptyState({
872
+ subject: "keys",
873
+ verb: "registered",
874
+ search: searchQuery,
875
+ total: hasAnyKeys ? 1 : 0,
876
+ waiting: {
877
+ // Previously this fell into the "all" branch and claimed every key
878
+ // had an issue — of the zero keys that exist.
879
+ otto: "empty",
880
+ title: "No storage keys",
881
+ message: "Nothing is registered for this tool to read."
882
+ }
883
+ });
884
+ if (searchQuery && hasAnyKeys) {
885
+ // No count: the pre-search total would mean re-running the whole key
886
+ // pipeline a second time for a number the sentence does not need.
887
+ content = {
888
+ ...content,
889
+ message: "No keys match. Clear the search to see them."
890
+ };
891
+ }
892
+ } else if (activeFilter === "missing") {
893
+ content = {
894
+ otto: "thumbsup",
895
+ title: "No missing keys",
896
+ message: "All required keys are present"
897
+ };
898
+ } else if (activeFilter === "issues") {
899
+ content = {
900
+ otto: "cool",
901
+ title: "No issues found",
902
+ message: "All storage keys are correctly configured"
903
+ };
904
+ } else {
905
+ content = {
906
+ otto: "broken",
907
+ title: "No valid keys found",
908
+ message: "All keys have issues or are missing"
909
+ };
910
+ }
911
+ return /*#__PURE__*/_jsx(ToolEmptyState, {
912
+ ...content,
913
+ testID: "buoy-storage-keys-empty"
914
+ });
915
+ })();
875
916
  const upgradeModalNode = /*#__PURE__*/_jsx(ProUpgradeModal, {
876
917
  visible: showUpgradeModal,
877
918
  onClose: () => setShowUpgradeModal(false),
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  import { useState, useCallback, useEffect, useRef, useMemo } from "react";
4
- import { Text, View, TouchableOpacity, StyleSheet, FlatList, Alert } from "react-native";
4
+ import { View, TouchableOpacity, StyleSheet, FlatList, Alert } from "react-native";
5
5
  import AsyncStorage from "@react-native-async-storage/async-storage";
6
- import { JsModal, ModalHeader, TabSelector, parseValue, devToolsStorageKeys, macOSColors, night, Database, Filter, RefreshCw, Search, SearchBar, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, TIER_LIMITS, windowDragProps } from "@buoy-gg/shared-ui";
6
+ import { JsModal, ModalHeader, TabSelector, parseValue, devToolsStorageKeys, macOSColors, night, ToolEmptyState, Filter, RefreshCw, Search, SearchBar, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, TIER_LIMITS, windowDragProps, usePersistedState } from "@buoy-gg/shared-ui";
7
7
  import { StorageBrowserMode } from "./StorageBrowserMode";
8
8
  import { StorageTypeFilterBar, countByStorageType } from "./StorageTypeFilterBar";
9
9
  import { clearAllAppStorage, clearAllStorageIncludingDevTools } from "../utils/clearAllStorage";
@@ -24,6 +24,41 @@ import { useIsPro } from "@buoy-gg/license";
24
24
  */
25
25
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
26
26
  export const FREE_TIER_EVENT_LIMIT = TIER_LIMITS["storage.events"].free;
27
+ /**
28
+ * Persistence recipes. These keys were always written straight to
29
+ * AsyncStorage (not the FileSystem-first persistentStorage), so they keep
30
+ * that backend — a user's saved tab, filters and pins survive the upgrade.
31
+ */
32
+ const ASYNC_STORAGE_BACKEND = {
33
+ getItem: key => AsyncStorage.getItem(key),
34
+ setItem: (key, value) => AsyncStorage.setItem(key, value)
35
+ };
36
+ const TAB_PERSISTENCE = {
37
+ storage: ASYNC_STORAGE_BACKEND,
38
+ // Stored as the bare tab name, not JSON.
39
+ parse: raw => raw === "browser" || raw === "events" ? raw : undefined,
40
+ serialize: tab => tab
41
+ };
42
+ const BOOLEAN_PERSISTENCE = {
43
+ storage: ASYNC_STORAGE_BACKEND,
44
+ // Stored as "true" / "false".
45
+ parse: raw => raw === "true",
46
+ serialize: value => value.toString()
47
+ };
48
+ const STRING_SET_PERSISTENCE = {
49
+ storage: ASYNC_STORAGE_BACKEND,
50
+ parse: raw => new Set(JSON.parse(raw)),
51
+ serialize: set => JSON.stringify(Array.from(set))
52
+ };
53
+ const IGNORED_PATTERNS_PERSISTENCE = {
54
+ ...STRING_SET_PERSISTENCE,
55
+ parse: raw => {
56
+ const filters = JSON.parse(raw);
57
+ // Always ensure @react_buoy is included to hide dev tool keys
58
+ if (!filters.includes("@react_buoy")) filters.push("@react_buoy");
59
+ return new Set(filters);
60
+ }
61
+ };
27
62
  export function StorageModalWithTabs({
28
63
  visible,
29
64
  onClose,
@@ -38,7 +73,7 @@ export function StorageModalWithTabs({
38
73
  }) {
39
74
  const isPro = useIsPro();
40
75
  const eventLimit = useLimit("storage.events");
41
- const [activeTab, setActiveTab] = useState("browser");
76
+ const [activeTab, setActiveTab] = usePersistedState(devToolsStorageKeys.storage.activeTab(), "browser", TAB_PERSISTENCE);
42
77
  useEffect(() => {
43
78
  onTabChange?.(activeTab);
44
79
  }, [activeTab, onTabChange]);
@@ -54,7 +89,7 @@ export function StorageModalWithTabs({
54
89
  const [isSearchActive, setIsSearchActive] = useState(false);
55
90
 
56
91
  // Local state to control subscription (true = subscribed, false = unsubscribed)
57
- const [isListeningEnabled, setIsListeningEnabled] = useState(true);
92
+ const [isListeningEnabled, setIsListeningEnabled] = usePersistedState(devToolsStorageKeys.storage.isMonitoring(), true, BOOLEAN_PERSISTENCE);
58
93
 
59
94
  // Event Listener state - using centralized store via hook
60
95
  // Pass enabled option to control subscription - this allows events tool to
@@ -71,8 +106,9 @@ export function StorageModalWithTabs({
71
106
  const [selectedHistoryEventIndex, setSelectedHistoryEventIndex] = useState(null);
72
107
  const [selectedEventIndex, setSelectedEventIndex] = useState(0);
73
108
  const [showFilters, setShowFilters] = useState(false);
74
- const [ignoredPatterns, setIgnoredPatterns] = useState(new Set(["@react_buoy", "@RNAsyncStorage", "redux-persist", "persist:"]) // Auto-hide dev tool keys by default
75
- );
109
+ const [ignoredPatterns, setIgnoredPatterns] = usePersistedState(devToolsStorageKeys.storage.eventFilters(),
110
+ // Auto-hide dev tool keys by default
111
+ new Set(["@react_buoy", "@RNAsyncStorage", "redux-persist", "persist:"]), IGNORED_PATTERNS_PERSISTENCE);
76
112
  /**
77
113
  * Which backend both tabs are looking at.
78
114
  *
@@ -88,12 +124,8 @@ export function StorageModalWithTabs({
88
124
  const [storageTypeFilter, setStorageTypeFilter] = useState("all");
89
125
  const [enabledStorageTypes, setEnabledStorageTypes] = useState(new Set(['async', 'mmkv', 'secure']) // All enabled by default
90
126
  );
91
- const [pinnedKeys, setPinnedKeys] = useState(new Set());
127
+ const [pinnedKeys, setPinnedKeys] = usePersistedState(devToolsStorageKeys.storage.pinnedKeys(), new Set(), STRING_SET_PERSISTENCE);
92
128
  const lastEventRef = useRef(null);
93
- const hasLoadedFilters = useRef(false);
94
- const hasLoadedPins = useRef(false);
95
- const hasLoadedTabState = useRef(false);
96
- const hasLoadedMonitoringState = useRef(false);
97
129
 
98
130
  // isListening = enabled and capturing (has subscribers)
99
131
  const isListening = isListeningEnabled && isCapturing;
@@ -108,140 +140,6 @@ export function StorageModalWithTabs({
108
140
 
109
141
  // Timer removed - using useTickEveryMinute hook instead
110
142
 
111
- // Load persisted tab state on mount
112
- useEffect(() => {
113
- if (!visible || hasLoadedTabState.current) return;
114
- const loadTabState = async () => {
115
- try {
116
- const storedTab = await AsyncStorage.getItem(devToolsStorageKeys.storage.activeTab());
117
- if (storedTab && (storedTab === "browser" || storedTab === "events")) {
118
- setActiveTab(storedTab);
119
- }
120
- hasLoadedTabState.current = true;
121
- } catch (error) {
122
- // Failed to load tab state
123
- }
124
- };
125
- loadTabState();
126
- }, [visible]);
127
-
128
- // Load persisted monitoring state on mount
129
- useEffect(() => {
130
- if (!visible || hasLoadedMonitoringState.current) return;
131
- const loadMonitoringState = async () => {
132
- try {
133
- const storedMonitoring = await AsyncStorage.getItem(devToolsStorageKeys.storage.isMonitoring());
134
- if (storedMonitoring !== null) {
135
- const shouldMonitor = storedMonitoring === "true";
136
- setIsListeningEnabled(shouldMonitor);
137
- }
138
- hasLoadedMonitoringState.current = true;
139
- } catch (error) {
140
- // Failed to load monitoring state
141
- }
142
- };
143
- loadMonitoringState();
144
- }, [visible]);
145
-
146
- // Note: Conversations will appear when storage events are triggered
147
- // Click on any conversation to see the unified view with toggle cards
148
-
149
- // Save tab state when it changes
150
- useEffect(() => {
151
- if (!hasLoadedTabState.current) return; // Don't save on initial load
152
-
153
- const saveTabState = async () => {
154
- try {
155
- await AsyncStorage.setItem(devToolsStorageKeys.storage.activeTab(), activeTab);
156
- } catch (error) {
157
- // Failed to save tab state
158
- }
159
- };
160
- saveTabState();
161
- }, [activeTab]);
162
-
163
- // Save monitoring state when it changes
164
- useEffect(() => {
165
- if (!hasLoadedMonitoringState.current) return; // Don't save on initial load
166
-
167
- const saveMonitoringState = async () => {
168
- try {
169
- await AsyncStorage.setItem(devToolsStorageKeys.storage.isMonitoring(), isListening.toString());
170
- } catch (error) {
171
- // Failed to save monitoring state
172
- }
173
- };
174
- saveMonitoringState();
175
- }, [isListening]);
176
-
177
- // Load persisted filters on mount
178
- useEffect(() => {
179
- if (!visible || hasLoadedFilters.current) return;
180
- const loadFilters = async () => {
181
- try {
182
- const storedFilters = await AsyncStorage.getItem(devToolsStorageKeys.storage.eventFilters());
183
- if (storedFilters) {
184
- const filters = JSON.parse(storedFilters);
185
- // Always ensure @react_buoy is included to hide dev tool keys
186
- if (!filters.includes("@react_buoy")) {
187
- filters.push("@react_buoy");
188
- }
189
- setIgnoredPatterns(new Set(filters));
190
- }
191
- hasLoadedFilters.current = true;
192
- } catch (error) {
193
- // Failed to load filters
194
- }
195
- };
196
- loadFilters();
197
- }, [visible]);
198
-
199
- // Save filters when they change
200
- useEffect(() => {
201
- if (!hasLoadedFilters.current) return; // Don't save on initial load
202
-
203
- const saveFilters = async () => {
204
- try {
205
- const filters = Array.from(ignoredPatterns);
206
- await AsyncStorage.setItem(devToolsStorageKeys.storage.eventFilters(), JSON.stringify(filters));
207
- } catch (error) {
208
- // Failed to save filters
209
- }
210
- };
211
- saveFilters();
212
- }, [ignoredPatterns]);
213
-
214
- // Load persisted pinned keys on mount
215
- useEffect(() => {
216
- if (!visible || hasLoadedPins.current) return;
217
- const loadPins = async () => {
218
- try {
219
- const stored = await AsyncStorage.getItem(devToolsStorageKeys.storage.pinnedKeys());
220
- if (stored) {
221
- setPinnedKeys(new Set(JSON.parse(stored)));
222
- }
223
- hasLoadedPins.current = true;
224
- } catch (error) {
225
- // Failed to load pinned keys
226
- }
227
- };
228
- loadPins();
229
- }, [visible]);
230
-
231
- // Save pinned keys when they change
232
- useEffect(() => {
233
- if (!hasLoadedPins.current) return; // Don't save on initial load
234
-
235
- const savePins = async () => {
236
- try {
237
- await AsyncStorage.setItem(devToolsStorageKeys.storage.pinnedKeys(), JSON.stringify(Array.from(pinnedKeys)));
238
- } catch (error) {
239
- // Failed to save pinned keys
240
- }
241
- };
242
- savePins();
243
- }, [pinnedKeys]);
244
-
245
143
  // Event listener setup - now handled by useStorageEvents hook
246
144
  // The hook subscribes to the centralized storageEventStore
247
145
 
@@ -671,18 +569,26 @@ export function StorageModalWithTabs({
671
569
  const filtered = storageTypeFilter !== "all";
672
570
  return /*#__PURE__*/_jsxs(View, {
673
571
  style: styles.flexFill,
674
- children: [eventTypeFilterBar, /*#__PURE__*/_jsxs(View, {
675
- style: styles.emptyState,
676
- children: [/*#__PURE__*/_jsx(Database, {
677
- size: 48,
678
- color: night.color.textTertiary
679
- }), /*#__PURE__*/_jsx(Text, {
680
- style: styles.emptyTitle,
681
- children: !isListening ? "Event listener is paused" : filtered ? `No ${storageTypeFilter} events` : "No storage events yet"
682
- }), /*#__PURE__*/_jsx(Text, {
683
- style: styles.emptySubtitle,
684
- children: !isListening ? "Press play to start monitoring" : filtered ? "Other backends have events — pick All Types to see them" : "Storage operations will appear here"
685
- })]
572
+ children: [eventTypeFilterBar, /*#__PURE__*/_jsx(ToolEmptyState, {
573
+ testID: "buoy-storage-events-empty"
574
+ // Three literal causes, no search on this tab — so it renders
575
+ // through the shared shell without the shared resolver. The
576
+ // backend pills need their own sentence naming the pill that
577
+ // undoes them, which the generic "Filtered Out" would drop.
578
+ ,
579
+ ...(!isListening ? {
580
+ otto: "paused",
581
+ title: "Event listener is paused",
582
+ message: "Press play to start monitoring"
583
+ } : filtered ? {
584
+ otto: "filtering",
585
+ title: `No ${storageTypeFilter} events`,
586
+ message: "Other backends have events — pick All Types to see them"
587
+ } : {
588
+ otto: "listening",
589
+ title: "No storage events yet",
590
+ message: "Storage operations will appear here"
591
+ })
686
592
  })]
687
593
  });
688
594
  }