react-native-inapp-inspector 1.1.1 → 1.1.2

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/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
2
- import { Alert, Animated, StyleSheet, FlatList, LayoutAnimation, Modal, Platform, Pressable, ScrollView, Text, TextInput, View, Linking, Image, InteractionManager, ActivityIndicator, StatusBar, TouchableOpacity, UIManager, LogBox, } from 'react-native';
2
+ import { Alert, Animated, StyleSheet, FlatList, LayoutAnimation, Modal, PanResponder, Platform, Pressable, ScrollView, Text, TextInput, View, Linking, Image, InteractionManager, ActivityIndicator, StatusBar, TouchableOpacity, UIManager, LogBox, } from 'react-native';
3
3
  import Svg, { Circle, Path } from 'react-native-svg';
4
4
  import LinearGradient from 'react-native-linear-gradient';
5
5
  import { useNavigationState, NavigationContext } from '@react-navigation/native';
@@ -24,7 +24,7 @@ import AnimatedEntrance from './components/AnimatedEntrance';
24
24
  // Helpers
25
25
  import { formatDateTime, getStatusColor, getNavigationInfo, deduplicateLogs, getDomainColor, formatDisplayUrl, getFetchCommand, getCurlCommand, getSize, } from './helpers';
26
26
  // Assets
27
- import { EmptyRadarIcon, FailIcon, SearchIcon, ScreenIcon, ClearIcon, SortArrowIcon, FilterIcon, InsightsIcon, GlobeIcon, DownloadIcon, CloseWhite, TrashIcon, WhiteBackNavigation, TerminalIcon, SignalIcon, AnalyticsIcon, SunIcon, MoonIcon, BrandCircleIcon, BrandSquareIcon, HtmlIcon, CssIcon, JsIcon, ClockIcon, EyeIcon, CheckIcon, SettingsIcon, RequestIcon, ResponseIcon, HeadersIcon, StatusIcon, } from './components/NetworkIcons';
27
+ import { EmptyRadarIcon, FailIcon, SearchIcon, ScreenIcon, ClearIcon, SortArrowIcon, FilterIcon, InsightsIcon, GlobeIcon, DownloadIcon, CloseWhite, TrashIcon, WhiteBackNavigation, TerminalIcon, SignalIcon, AnalyticsIcon, SunIcon, MoonIcon, BrandCircleIcon, BrandSquareIcon, HtmlIcon, CssIcon, JsIcon, ClockIcon, EyeIcon, CheckIcon, SettingsIcon, RequestIcon, ResponseIcon, HeadersIcon, StatusIcon, ChevronIcon, } from './components/NetworkIcons';
28
28
  import ErrorBoundary from './components/ErrorBoundary';
29
29
  // Stylesheet
30
30
  import { AppColors } from './styles/AppColors';
@@ -150,6 +150,8 @@ const NetworkInspector = ({ enabled = true, }) => {
150
150
  const [showNetworkMenu, setShowNetworkMenu] = useState(false);
151
151
  const [showUiMenu, setShowUiMenu] = useState(false);
152
152
  const [sortOrder, setSortOrder] = useState('newest');
153
+ // #7 — sort order for the Logs (console) tab
154
+ const [logSortOrder, setLogSortOrder] = useState('newest');
153
155
  const [reqExpanded, setReqExpanded] = useState(undefined);
154
156
  const [resExpanded, setResExpanded] = useState(undefined);
155
157
  const [showReqDiff, setShowReqDiff] = useState(false);
@@ -329,6 +331,66 @@ const NetworkInspector = ({ enabled = true, }) => {
329
331
  const badgeAnim = useRef(new Animated.Value(1)).current;
330
332
  const activePulseAnim = useRef(new Animated.Value(0.4)).current;
331
333
  const unreadPulseAnim = useRef(new Animated.Value(1)).current;
334
+ // #11 — header "clear all" icon spin/scale animation
335
+ const clearAnim = useRef(new Animated.Value(0)).current;
336
+ // #4 — draggable floating launcher (drag anywhere on screen)
337
+ const fabPan = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current;
338
+ const fabPanRef = useRef({ x: 0, y: 0 });
339
+ useEffect(() => {
340
+ const idX = fabPan.x.addListener(v => (fabPanRef.current.x = v.value));
341
+ const idY = fabPan.y.addListener(v => (fabPanRef.current.y = v.value));
342
+ return () => {
343
+ fabPan.x.removeListener(idX);
344
+ fabPan.y.removeListener(idY);
345
+ };
346
+ }, [fabPan]);
347
+ const fabDraggedRef = useRef(false);
348
+ const fabPanResponder = useRef(PanResponder.create({
349
+ // Let taps fall through to the launcher; only hijack once the
350
+ // finger actually moves, so onPress still fires on a tap.
351
+ onStartShouldSetPanResponder: () => false,
352
+ onMoveShouldSetPanResponder: (_e, g) => Math.abs(g.dx) > 4 || Math.abs(g.dy) > 4,
353
+ onPanResponderGrant: () => {
354
+ fabDraggedRef.current = true;
355
+ fabPan.setOffset({
356
+ x: fabPanRef.current.x,
357
+ y: fabPanRef.current.y,
358
+ });
359
+ fabPan.setValue({ x: 0, y: 0 });
360
+ },
361
+ onPanResponderMove: Animated.event([null, { dx: fabPan.x, dy: fabPan.y }], {
362
+ useNativeDriver: false,
363
+ }),
364
+ onPanResponderRelease: () => {
365
+ fabPan.flattenOffset();
366
+ // small delay so the trailing tap (if any) is ignored
367
+ setTimeout(() => {
368
+ fabDraggedRef.current = false;
369
+ }, 50);
370
+ },
371
+ onPanResponderTerminate: () => {
372
+ fabPan.flattenOffset();
373
+ fabDraggedRef.current = false;
374
+ },
375
+ })).current;
376
+ // #10 — scroll-to-top affordance for the main APIs list
377
+ const apisListRef = useRef(null);
378
+ const [showScrollTop, setShowScrollTop] = useState(false);
379
+ const runClearAllWithAnimation = useCallback(() => {
380
+ Animated.sequence([
381
+ Animated.timing(clearAnim, {
382
+ toValue: 1,
383
+ duration: 320,
384
+ useNativeDriver: true,
385
+ }),
386
+ Animated.timing(clearAnim, {
387
+ toValue: 0,
388
+ duration: 0,
389
+ useNativeDriver: true,
390
+ }),
391
+ ]).start();
392
+ handleClearAll();
393
+ }, [clearAnim]);
332
394
  useEffect(() => {
333
395
  if (Platform.OS === 'android') {
334
396
  UIManager.setLayoutAnimationEnabledExperimental?.(true);
@@ -753,8 +815,12 @@ const NetworkInspector = ({ enabled = true, }) => {
753
815
  result = result.filter(log => log.message.toLowerCase().includes(s) ||
754
816
  (log.caller ?? '').toLowerCase().includes(s));
755
817
  }
818
+ // #7 — apply sort order (newest/oldest first)
819
+ result = [...result].sort((a, b) => logSortOrder === 'newest'
820
+ ? b.timestamp - a.timestamp
821
+ : a.timestamp - b.timestamp);
756
822
  return result;
757
- }, [visibleConsoleLogs, logFilters, logSearch]);
823
+ }, [visibleConsoleLogs, logFilters, logSearch, logSortOrder]);
758
824
  const filteredWebViewLogs = useMemo(() => {
759
825
  let result = webViewLogs;
760
826
  if (webViewSearch) {
@@ -880,6 +946,22 @@ const NetworkInspector = ({ enabled = true, }) => {
880
946
  ]);
881
947
  return;
882
948
  }
949
+ if (activeTab === 'redux') {
950
+ Alert.alert('Clear Redux Timeline', 'Are you sure you want to clear the dispatched action history?', [
951
+ { text: 'Cancel', style: 'cancel' },
952
+ {
953
+ text: 'Clear All',
954
+ onPress: () => {
955
+ clearActionHistory();
956
+ setReduxActionHistory([]);
957
+ setReduxLastActionMap({});
958
+ },
959
+ style: 'destructive',
960
+ },
961
+ ]);
962
+ return;
963
+ }
964
+ // Default: APIs tab. Only clears NETWORK logs — never touches the other tabs.
883
965
  if (selectedLogs.size > 0) {
884
966
  setLogs(prev => prev.filter(l => !selectedLogs.has(l.id)));
885
967
  setSelectedLogs(new Set());
@@ -887,10 +969,22 @@ const NetworkInspector = ({ enabled = true, }) => {
887
969
  else {
888
970
  Alert.alert('Clear Logs', 'Are you sure you want to clear all network logs?', [
889
971
  { text: 'Cancel', style: 'cancel' },
890
- { text: 'Clear All', onPress: handleClearAll, style: 'destructive' },
972
+ { text: 'Clear All', onPress: clearNetworkOnly, style: 'destructive' },
891
973
  ]);
892
974
  }
893
975
  }
976
+ // Clears ONLY network logs + their derived selection/filter state.
977
+ function clearNetworkOnly() {
978
+ clearNetworkLogs();
979
+ setLogs([]);
980
+ setSelectedLogs(new Set());
981
+ setSectionFilters({});
982
+ setCollapsedSections(new Set());
983
+ setStatusFilters(new Set());
984
+ setMethodFilters(new Set());
985
+ prevLogIdsRef.current = new Set();
986
+ logRouteMapRef.current = new Map();
987
+ }
894
988
  const detailTitle = useMemo(() => {
895
989
  if (!selected)
896
990
  return '';
@@ -1224,7 +1318,7 @@ const NetworkInspector = ({ enabled = true, }) => {
1224
1318
  })}
1225
1319
  </View>
1226
1320
 
1227
- {/* Preferences Section */}
1321
+ {/* UI Preferences Section */}
1228
1322
  <View style={{ marginTop: 8 }}>
1229
1323
  <Text style={{
1230
1324
  fontFamily: AppFonts.interBold,
@@ -1233,7 +1327,7 @@ const NetworkInspector = ({ enabled = true, }) => {
1233
1327
  letterSpacing: 0.6,
1234
1328
  marginBottom: 8,
1235
1329
  }}>
1236
- PREFERENCES
1330
+ UI PREFERENCES
1237
1331
  </Text>
1238
1332
  <View style={{
1239
1333
  backgroundColor: AppColors.primaryLight,
@@ -2489,50 +2583,6 @@ const NetworkInspector = ({ enabled = true, }) => {
2489
2583
  const lastActionMap = reduxLastActionMap;
2490
2584
  const actionHistory = reduxActionHistory;
2491
2585
  return (<ScrollView style={styles.detailScroll} contentContainerStyle={{ paddingBottom: 24 }}>
2492
- {/* Top Summary Card */}
2493
- <View style={{
2494
- backgroundColor: AppColors.primaryLight,
2495
- borderRadius: 12,
2496
- borderWidth: 1,
2497
- borderColor: AppColors.grayBorderSecondary,
2498
- padding: 14,
2499
- marginHorizontal: 16,
2500
- marginTop: 12,
2501
- marginBottom: 12,
2502
- flexDirection: 'row',
2503
- alignItems: 'center',
2504
- gap: 12,
2505
- }}>
2506
- <View style={{
2507
- width: 44,
2508
- height: 44,
2509
- borderRadius: 10,
2510
- backgroundColor: AppColors.purpleShade50,
2511
- alignItems: 'center',
2512
- justifyContent: 'center',
2513
- }}>
2514
- <TerminalIcon color={AppColors.purple} size={20}/>
2515
- </View>
2516
- <View style={{ flex: 1 }}>
2517
- <Text style={{
2518
- fontFamily: AppFonts.interBold,
2519
- fontSize: 13,
2520
- color: AppColors.primaryBlack,
2521
- }}>
2522
- Redux Store Snapshot
2523
- </Text>
2524
- <Text style={{
2525
- fontFamily: AppFonts.interRegular,
2526
- fontSize: 11,
2527
- color: AppColors.grayText,
2528
- marginTop: 2,
2529
- }}>
2530
- Total size: {getSize(reduxState)} • {reducerKeys.length} Reducers
2531
- </Text>
2532
- </View>
2533
- <CopyButton value={() => reduxState} label="Overall Store"/>
2534
- </View>
2535
-
2536
2586
  {/* Tab View Selection Segments */}
2537
2587
  <View style={{
2538
2588
  flexDirection: 'row',
@@ -2540,6 +2590,7 @@ const NetworkInspector = ({ enabled = true, }) => {
2540
2590
  borderRadius: 10,
2541
2591
  padding: 3,
2542
2592
  marginHorizontal: 16,
2593
+ marginTop: 12,
2543
2594
  marginBottom: 12,
2544
2595
  borderWidth: 1,
2545
2596
  borderColor: AppColors.dividerColor,
@@ -2575,6 +2626,27 @@ const NetworkInspector = ({ enabled = true, }) => {
2575
2626
  }}>
2576
2627
  Action Timeline
2577
2628
  </Text>
2629
+ <View style={{
2630
+ minWidth: 18,
2631
+ paddingHorizontal: 5,
2632
+ height: 16,
2633
+ borderRadius: 8,
2634
+ alignItems: 'center',
2635
+ justifyContent: 'center',
2636
+ backgroundColor: reduxActiveSubTab === 'timeline'
2637
+ ? 'rgba(255,255,255,0.28)'
2638
+ : AppColors.dividerColor,
2639
+ }}>
2640
+ <Text style={{
2641
+ fontFamily: AppFonts.interBold,
2642
+ fontSize: 9,
2643
+ color: reduxActiveSubTab === 'timeline'
2644
+ ? '#FFFFFF'
2645
+ : AppColors.grayText,
2646
+ }}>
2647
+ {actionHistory.length}
2648
+ </Text>
2649
+ </View>
2578
2650
  </View>
2579
2651
  </TouchableOpacity>
2580
2652
  <TouchableOpacity onPress={() => {
@@ -2604,6 +2676,27 @@ const NetworkInspector = ({ enabled = true, }) => {
2604
2676
  }}>
2605
2677
  Store Tree
2606
2678
  </Text>
2679
+ <View style={{
2680
+ minWidth: 18,
2681
+ paddingHorizontal: 5,
2682
+ height: 16,
2683
+ borderRadius: 8,
2684
+ alignItems: 'center',
2685
+ justifyContent: 'center',
2686
+ backgroundColor: reduxActiveSubTab === 'tree'
2687
+ ? 'rgba(255,255,255,0.28)'
2688
+ : AppColors.dividerColor,
2689
+ }}>
2690
+ <Text style={{
2691
+ fontFamily: AppFonts.interBold,
2692
+ fontSize: 9,
2693
+ color: reduxActiveSubTab === 'tree'
2694
+ ? '#FFFFFF'
2695
+ : AppColors.grayText,
2696
+ }}>
2697
+ {reducerKeys.length}
2698
+ </Text>
2699
+ </View>
2607
2700
  </View>
2608
2701
  </TouchableOpacity>
2609
2702
  </View>
@@ -2650,21 +2743,27 @@ const NetworkInspector = ({ enabled = true, }) => {
2650
2743
  };
2651
2744
  return (<>
2652
2745
  {hasNavigationContext && (<NavigationTracker onStateChange={setNavState}/>)}
2653
- <TouchableScale style={styles.fabWrapper} onPress={() => setVisible(true)} hitSlop={10}>
2654
- <Animated.View style={[styles.fabPulseRing, { transform: [{ scale: pulseAnim }] }]}/>
2655
- <BrandCircleIcon size={62}/>
2656
- {(logs.length > 0 || analyticsEvents.length > 0) && (<Animated.View style={[
2746
+ <Animated.View style={[styles.fabWrapper, { transform: fabPan.getTranslateTransform() }]} {...fabPanResponder.panHandlers}>
2747
+ <TouchableScale style={{ alignItems: 'center', justifyContent: 'center' }} onPress={() => {
2748
+ if (fabDraggedRef.current)
2749
+ return;
2750
+ setVisible(true);
2751
+ }} hitSlop={10}>
2752
+ <Animated.View style={[styles.fabPulseRing, { transform: [{ scale: pulseAnim }] }]}/>
2753
+ <BrandCircleIcon size={62}/>
2754
+ {(logs.length > 0 || analyticsEvents.length > 0) && (<Animated.View style={[
2657
2755
  styles.fabBadge,
2658
2756
  hasErrors ? styles.fabBadgeError : styles.fabBadgeNormal,
2659
2757
  { transform: [{ scale: badgeAnim }] },
2660
2758
  ]}>
2661
- <Text style={styles.fabBadgeText}>
2662
- {logs.length + analyticsEvents.length > 99
2759
+ <Text style={styles.fabBadgeText}>
2760
+ {logs.length + analyticsEvents.length > 99
2663
2761
  ? '99+'
2664
2762
  : logs.length + analyticsEvents.length}
2665
- </Text>
2666
- </Animated.View>)}
2667
- </TouchableScale>
2763
+ </Text>
2764
+ </Animated.View>)}
2765
+ </TouchableScale>
2766
+ </Animated.View>
2668
2767
 
2669
2768
  <Modal visible={visible} animationType="slide" transparent>
2670
2769
  {visible && (<ErrorBoundary onClose={closeModal}>
@@ -2694,10 +2793,27 @@ const NetworkInspector = ({ enabled = true, }) => {
2694
2793
  setSelectedEvent(null);
2695
2794
  });
2696
2795
  }} hitSlop={15} style={[
2697
- styles.iconBtnMinimal,
2796
+ {
2797
+ width: 38,
2798
+ height: 38,
2799
+ borderRadius: 19,
2800
+ alignItems: 'center',
2801
+ justifyContent: 'center',
2802
+ backgroundColor: 'rgba(255,255,255,0.18)',
2803
+ borderWidth: 1,
2804
+ borderColor: 'rgba(255,255,255,0.30)',
2805
+ },
2698
2806
  selected == null &&
2699
2807
  selectedEvent == null && { display: 'none' },
2700
2808
  ]}>
2809
+ {/* Soft outer glow to fake a blurred circle */}
2810
+ <View style={{
2811
+ position: 'absolute',
2812
+ width: 48,
2813
+ height: 48,
2814
+ borderRadius: 24,
2815
+ backgroundColor: 'rgba(255,255,255,0.10)',
2816
+ }}/>
2701
2817
  <WhiteBackNavigation />
2702
2818
  </TouchableScale>
2703
2819
 
@@ -2708,8 +2824,8 @@ const NetworkInspector = ({ enabled = true, }) => {
2708
2824
  flex: 1,
2709
2825
  }}>
2710
2826
  <View style={{
2711
- width: 42,
2712
- height: 42,
2827
+ width: 50,
2828
+ height: 50,
2713
2829
  borderRadius: 10,
2714
2830
  backgroundColor: 'rgba(255,255,255,0.13)',
2715
2831
  borderWidth: 1.5,
@@ -2721,13 +2837,10 @@ const NetworkInspector = ({ enabled = true, }) => {
2721
2837
  shadowRadius: 4,
2722
2838
  shadowOffset: { width: 0, height: 2 },
2723
2839
  }}>
2724
- <BrandSquareIcon size={36}/>
2840
+ <BrandSquareIcon size={45}/>
2725
2841
  </View>
2726
2842
  <View style={{ gap: 3 }}>
2727
- <Text style={[
2728
- styles.headerTitle,
2729
- { fontSize: 17, letterSpacing: 0.2 },
2730
- ]}>
2843
+ <Text style={[styles.headerTitle]}>
2731
2844
  RN InApp Inspector
2732
2845
  </Text>
2733
2846
  <View style={{
@@ -2834,21 +2947,48 @@ const NetworkInspector = ({ enabled = true, }) => {
2834
2947
  </Text>
2835
2948
  </View>
2836
2949
  <View style={styles.headerDetailSubRow}>
2837
- <View style={[
2950
+ <View style={{
2951
+ flexDirection: 'row',
2952
+ alignItems: 'center',
2953
+ gap: 5,
2954
+ paddingHorizontal: 8,
2955
+ paddingVertical: 3,
2956
+ borderRadius: 20,
2957
+ backgroundColor: `${getStatusColor(selected.status)}26`,
2958
+ borderWidth: 1,
2959
+ borderColor: `${getStatusColor(selected.status)}55`,
2960
+ }}>
2961
+ <View style={[
2838
2962
  styles.headerStatusDot,
2839
2963
  {
2840
2964
  backgroundColor: getStatusColor(selected.status),
2841
2965
  },
2842
2966
  ]}/>
2843
- <Text style={styles.headerSubTitle}>
2844
- {selected.status === 0
2967
+ <Text style={[
2968
+ styles.headerSubTitle,
2969
+ { fontFamily: AppFonts.interBold },
2970
+ ]}>
2971
+ {selected.status === 0
2845
2972
  ? 'Failed'
2846
- : selected.status ?? 'Pending'}{' '}
2847
- •{' '}
2848
- {selected.duration != null
2973
+ : selected.status ?? 'Pending'}
2974
+ </Text>
2975
+ </View>
2976
+ <View style={{
2977
+ flexDirection: 'row',
2978
+ alignItems: 'center',
2979
+ gap: 4,
2980
+ paddingHorizontal: 8,
2981
+ paddingVertical: 3,
2982
+ borderRadius: 20,
2983
+ backgroundColor: 'rgba(255,255,255,0.16)',
2984
+ }}>
2985
+ <ClockIcon color="#FFFFFF" size={11}/>
2986
+ <Text style={styles.headerSubTitle}>
2987
+ {selected.duration != null
2849
2988
  ? `${selected.duration}ms`
2850
- : '-'}
2851
- </Text>
2989
+ : ''}
2990
+ </Text>
2991
+ </View>
2852
2992
  </View>
2853
2993
  </View>) : selectedEvent != null ? (<View style={styles.headerDetailCenter}>
2854
2994
  <View style={styles.headerDetailRow}>
@@ -2891,7 +3031,50 @@ const NetworkInspector = ({ enabled = true, }) => {
2891
3031
  </View>) : null}
2892
3032
  </View>
2893
3033
 
2894
- <View style={styles.headerRight}>
3034
+ <View style={[
3035
+ styles.headerRight,
3036
+ selected == null &&
3037
+ selectedEvent == null && {
3038
+ flexShrink: 0,
3039
+ minWidth: 116,
3040
+ },
3041
+ ]}>
3042
+ {selected == null && selectedEvent == null && (<TouchableScale onPress={() => {
3043
+ Alert.alert('Clear Everything', 'This clears all tabs — APIs, Logs, Analytics, WebView and Redux timeline. Continue?', [
3044
+ { text: 'Cancel', style: 'cancel' },
3045
+ {
3046
+ text: 'Clear All',
3047
+ onPress: runClearAllWithAnimation,
3048
+ style: 'destructive',
3049
+ },
3050
+ ]);
3051
+ }} hitSlop={15} style={[
3052
+ styles.closeButtonSquare,
3053
+ {
3054
+ marginRight: 8,
3055
+ backgroundColor: 'rgba(255,255,255,0.15)',
3056
+ },
3057
+ ]}>
3058
+ <Animated.View style={{
3059
+ transform: [
3060
+ {
3061
+ rotate: clearAnim.interpolate({
3062
+ inputRange: [0, 1],
3063
+ outputRange: ['0deg', '-25deg'],
3064
+ }),
3065
+ },
3066
+ {
3067
+ scale: clearAnim.interpolate({
3068
+ inputRange: [0, 0.5, 1],
3069
+ outputRange: [1, 1.25, 1],
3070
+ }),
3071
+ },
3072
+ ],
3073
+ }}>
3074
+ <TrashIcon color="#FFFFFF" size={15}/>
3075
+ </Animated.View>
3076
+ </TouchableScale>)}
3077
+
2895
3078
  {selected == null && selectedEvent == null && (<TouchableScale onPress={() => setSettingsPage('main')} hitSlop={15} style={[
2896
3079
  styles.closeButtonSquare,
2897
3080
  {
@@ -3226,50 +3409,53 @@ const NetworkInspector = ({ enabled = true, }) => {
3226
3409
  filteredAnalyticsEvents.length === 0 && {
3227
3410
  flexGrow: 1,
3228
3411
  },
3229
- ]} keyboardShouldPersistTaps="handled"/>)) : activeTab === 'apis' && selected == null ? (<FlatList data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={true} ListHeaderComponent={<View style={{ marginTop: 8 }}>
3230
- <View style={styles.toolbarRow}>
3231
- <View style={styles.searchContainer}>
3232
- <SearchIcon color={AppColors.grayTextWeak} size={16}/>
3233
- <TextInput placeholder="Search endpoints..." placeholderTextColor={AppColors.grayTextWeak} value={search} onChangeText={setSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
3234
- {search.length > 0 && (<Pressable onPress={() => setSearch('')} hitSlop={10} style={styles.clearBtn}>
3235
- <ClearIcon color={AppColors.grayTextWeak} size={14}/>
3236
- </Pressable>)}
3237
- </View>
3412
+ ]} keyboardShouldPersistTaps="handled"/>)) : activeTab === 'apis' && selected == null ? (<View style={{ flex: 1 }}>
3413
+ <FlatList ref={apisListRef} onScroll={e => setShowScrollTop(e.nativeEvent.contentOffset.y > 320)} scrollEventThrottle={16} data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={true} ListHeaderComponent={<View style={{ marginTop: 8 }}>
3414
+ <View style={styles.toolbarRow}>
3415
+ <View style={styles.searchContainer}>
3416
+ <SearchIcon color={AppColors.grayTextWeak} size={16}/>
3417
+ <TextInput placeholder="Search endpoints..." placeholderTextColor={AppColors.grayTextWeak} value={search} onChangeText={setSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
3418
+ {search.length > 0 && (<Pressable onPress={() => setSearch('')} hitSlop={10} style={styles.clearBtn}>
3419
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
3420
+ </Pressable>)}
3421
+ </View>
3238
3422
 
3239
- <View style={styles.toolbarRight}>
3240
- <TouchableScale style={styles.toolbarBtn} onPress={handleDelete} hitSlop={10}>
3241
- <TrashIcon color={AppColors.grayTextStrong} size={18}/>
3242
- {selectedLogs.size > 0 && (<View style={styles.trashBadge}>
3243
- <Text style={styles.trashBadgeText}>
3244
- {selectedLogs.size}
3245
- </Text>
3246
- </View>)}
3247
- </TouchableScale>
3423
+ <View style={styles.toolbarRight}>
3424
+ <TouchableScale style={styles.toolbarBtn} onPress={handleDelete} hitSlop={10}>
3425
+ <TrashIcon color={AppColors.grayTextStrong} size={18}/>
3426
+ {selectedLogs.size > 0 && (<View style={styles.trashBadge}>
3427
+ <Text style={styles.trashBadgeText}>
3428
+ {selectedLogs.size}
3429
+ </Text>
3430
+ </View>)}
3431
+ </TouchableScale>
3248
3432
 
3249
- <TouchableScale style={styles.toolbarBtn} onPress={() => setSortOrder(o => o === 'newest' ? 'oldest' : 'newest')} hitSlop={10}>
3250
- <SortArrowIcon direction={sortOrder === 'newest' ? 'down' : 'up'} color={AppColors.grayTextStrong} size={18}/>
3251
- </TouchableScale>
3433
+ <TouchableScale style={styles.toolbarBtn} onPress={() => setSortOrder(o => o === 'newest' ? 'oldest' : 'newest')} hitSlop={10}>
3434
+ <SortArrowIcon direction={sortOrder === 'newest' ? 'down' : 'up'} color={AppColors.grayTextStrong} size={18}/>
3435
+ </TouchableScale>
3252
3436
 
3253
- <TouchableScale style={[
3437
+ <TouchableScale style={[
3254
3438
  styles.toolbarBtn,
3255
3439
  filtersAccordion.isOpen &&
3256
3440
  styles.toolbarBtnActive,
3257
3441
  ]} onPress={filtersAccordion.toggleOpen} hitSlop={10}>
3258
- <FilterIcon color={filtersAccordion.isOpen
3442
+ <FilterIcon color={filtersAccordion.isOpen
3259
3443
  ? AppColors.purple
3260
3444
  : AppColors.grayTextStrong} size={18}/>
3261
- </TouchableScale>
3445
+ </TouchableScale>
3446
+ </View>
3262
3447
  </View>
3263
- </View>
3264
3448
 
3265
- <Animated.View style={[
3449
+ <Animated.View style={[
3266
3450
  filtersAccordion.bodyStyle,
3267
3451
  { overflow: 'hidden' },
3268
3452
  ]}>
3269
- <View style={styles.filtersContainer}>
3270
- <Text style={styles.filtersHeading}>STATUS</Text>
3271
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
3272
- {STATUS_FILTERS.map(filter => {
3453
+ <View style={styles.filtersContainer}>
3454
+ <Text style={styles.filtersHeading}>
3455
+ STATUS
3456
+ </Text>
3457
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
3458
+ {STATUS_FILTERS.map(filter => {
3273
3459
  const isAll = filter === 'ALL';
3274
3460
  const active = isAll
3275
3461
  ? statusFilters.size === 0
@@ -3288,38 +3474,38 @@ const NetworkInspector = ({ enabled = true, }) => {
3288
3474
  });
3289
3475
  }
3290
3476
  }} hitSlop={10}>
3291
- {active ? (<View style={[
3477
+ {active ? (<View style={[
3292
3478
  styles.statusFilterChip,
3293
3479
  styles.statusFilterActive,
3294
3480
  { overflow: 'hidden' },
3295
3481
  ]}>
3296
- <LinearGradient colors={[
3482
+ <LinearGradient colors={[
3297
3483
  AppColors.purpleShade50,
3298
3484
  '#EAE5FF',
3299
3485
  ]} style={StyleSheet.absoluteFill} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}/>
3300
- <Text style={[
3486
+ <Text style={[
3301
3487
  styles.statusFilterText,
3302
3488
  { color: AppColors.purple },
3303
3489
  ]}>
3304
- {filter}
3305
- </Text>
3306
- </View>) : (<View style={styles.statusFilterChip}>
3307
- <Text style={styles.statusFilterText}>
3308
- {filter}
3309
- </Text>
3310
- </View>)}
3311
- </TouchableScale>);
3490
+ {filter}
3491
+ </Text>
3492
+ </View>) : (<View style={styles.statusFilterChip}>
3493
+ <Text style={styles.statusFilterText}>
3494
+ {filter}
3495
+ </Text>
3496
+ </View>)}
3497
+ </TouchableScale>);
3312
3498
  })}
3313
- </ScrollView>
3499
+ </ScrollView>
3314
3500
 
3315
- <Text style={[
3501
+ <Text style={[
3316
3502
  styles.filtersHeading,
3317
3503
  { marginTop: 16 },
3318
3504
  ]}>
3319
- METHOD
3320
- </Text>
3321
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
3322
- {availableMethods.map(filter => {
3505
+ METHOD
3506
+ </Text>
3507
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
3508
+ {availableMethods.map(filter => {
3323
3509
  const isAll = filter === 'ALL';
3324
3510
  const active = isAll
3325
3511
  ? methodFilters.size === 0
@@ -3338,43 +3524,55 @@ const NetworkInspector = ({ enabled = true, }) => {
3338
3524
  });
3339
3525
  }
3340
3526
  }} hitSlop={10}>
3341
- {active ? (<View style={[
3527
+ {active ? (<View style={[
3342
3528
  styles.statusFilterChip,
3343
3529
  styles.statusFilterActive,
3344
3530
  { overflow: 'hidden' },
3345
3531
  ]}>
3346
- <LinearGradient colors={[
3532
+ <LinearGradient colors={[
3347
3533
  AppColors.purpleShade50,
3348
3534
  '#EAE5FF',
3349
3535
  ]} style={StyleSheet.absoluteFill} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}/>
3350
- <Text style={[
3536
+ <Text style={[
3351
3537
  styles.statusFilterText,
3352
3538
  { color: AppColors.purple },
3353
3539
  ]}>
3354
- {filter}
3355
- </Text>
3356
- </View>) : (<View style={styles.statusFilterChip}>
3357
- <Text style={styles.statusFilterText}>
3358
- {filter}
3359
- </Text>
3360
- </View>)}
3361
- </TouchableScale>);
3540
+ {filter}
3541
+ </Text>
3542
+ </View>) : (<View style={styles.statusFilterChip}>
3543
+ <Text style={styles.statusFilterText}>
3544
+ {filter}
3545
+ </Text>
3546
+ </View>)}
3547
+ </TouchableScale>);
3362
3548
  })}
3363
- </ScrollView>
3364
- </View>
3365
- </Animated.View>
3549
+ </ScrollView>
3550
+ </View>
3551
+ </Animated.View>
3366
3552
 
3367
- {(search ||
3553
+ {(search ||
3368
3554
  statusFilters.size > 0 ||
3369
3555
  methodFilters.size > 0) && (<Text style={styles.resultCount}>
3370
- {filteredLogs.length === logs.length
3556
+ {filteredLogs.length === logs.length
3371
3557
  ? `${logs.length} requests`
3372
3558
  : `${filteredLogs.length} of ${logs.length} filtered requests`}
3373
- </Text>)}
3374
- </View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 || statusFilters.size > 0}/>} contentContainerStyle={[
3559
+ </Text>)}
3560
+ </View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 || statusFilters.size > 0}/>} contentContainerStyle={[
3375
3561
  styles.listContent,
3376
3562
  filteredLogs.length === 0 && { flexGrow: 1 },
3377
- ]} keyboardShouldPersistTaps="handled"/>) : activeTab === 'logs' ? (<View style={{ flex: 1 }}>
3563
+ ]} keyboardShouldPersistTaps="handled"/>
3564
+ {showScrollTop && (<TouchableScale onPress={() => {
3565
+ apisListRef.current?.scrollToOffset({
3566
+ offset: 0,
3567
+ animated: true,
3568
+ });
3569
+ setShowScrollTop(false);
3570
+ }} hitSlop={10} style={styles.scrollTopBtn}>
3571
+ <View style={{ transform: [{ rotate: '180deg' }] }}>
3572
+ <ChevronIcon color="#FFFFFF" size={18}/>
3573
+ </View>
3574
+ </TouchableScale>)}
3575
+ </View>) : activeTab === 'logs' ? (<View style={{ flex: 1 }}>
3378
3576
  <View style={{
3379
3577
  backgroundColor: '#FFFFFF',
3380
3578
  borderBottomWidth: 1,
@@ -3394,6 +3592,9 @@ const NetworkInspector = ({ enabled = true, }) => {
3394
3592
  </View>
3395
3593
 
3396
3594
  <View style={styles.toolbarRight}>
3595
+ <TouchableScale style={styles.toolbarBtn} onPress={() => setLogSortOrder(o => o === 'newest' ? 'oldest' : 'newest')} hitSlop={10}>
3596
+ <SortArrowIcon color={AppColors.grayTextStrong} size={18} direction={logSortOrder === 'newest' ? 'down' : 'up'}/>
3597
+ </TouchableScale>
3397
3598
  <TouchableScale style={styles.toolbarBtn} onPress={handleDelete} hitSlop={10}>
3398
3599
  <TrashIcon color={AppColors.grayTextStrong} size={18}/>
3399
3600
  </TouchableScale>
@@ -4633,7 +4834,7 @@ const NetworkInspector = ({ enabled = true, }) => {
4633
4834
  if (!resExpanded && !showResDiff)
4634
4835
  setResExpanded(true);
4635
4836
  }}/>
4636
- {showResDiff ? (<DiffViewer oldData={prevResponseData} newData={selected.response} forceOpen={resExpanded}/>) : (<JsonViewer data={selected.response} search={detailSearch} forceOpen={resExpanded}/>)}
4837
+ {showResDiff ? (<DiffViewer oldData={prevResponseData} newData={selected.response} forceOpen={resExpanded}/>) : (<JsonViewer data={selected.response} search={detailSearch} forceOpen={resExpanded} wrap/>)}
4637
4838
  </View>
4638
4839
  </>)}
4639
4840
  </ScrollView>