diff --git a/modules/navigation-buttons/index.ts b/modules/navigation-buttons/index.ts
index 722e06e0c5..83da2353eb 100644
--- a/modules/navigation-buttons/index.ts
+++ b/modules/navigation-buttons/index.ts
@@ -4,3 +4,4 @@ export {OpenSettingsButton} from './open-settings'
export {FavoriteButton} from './favorite'
export {DebugNoticeButton} from './debug'
export {SearchButton} from './search'
+export {NetworkLoggerButton} from './network-logger'
diff --git a/modules/navigation-buttons/network-logger.tsx b/modules/navigation-buttons/network-logger.tsx
new file mode 100644
index 0000000000..2655def2f0
--- /dev/null
+++ b/modules/navigation-buttons/network-logger.tsx
@@ -0,0 +1,37 @@
+import * as React from 'react'
+import {Platform, StyleSheet, Text} from 'react-native'
+import {Touchable} from '@frogpond/touchable'
+import {useNavigation, useTheme} from '@react-navigation/native'
+import {commonStyles, rightButtonStyles as styles} from './styles'
+
+export const buttonStyles = StyleSheet.create({
+ text: {
+ ...Platform.select({
+ ios: {
+ fontWeight: '600',
+ },
+ android: {
+ fontWeight: '400',
+ },
+ }),
+ },
+})
+
+export const NetworkLoggerButton: React.FC = () => {
+ const navigation = useNavigation()
+ let {colors} = useTheme()
+
+ return (
+ navigation.navigate('NetworkLogger')}
+ style={styles.button}
+ >
+
+ Log
+
+
+ )
+}
diff --git a/modules/use-debounce/index.ts b/modules/use-debounce/index.ts
index 2502d09874..e66a6d0713 100644
--- a/modules/use-debounce/index.ts
+++ b/modules/use-debounce/index.ts
@@ -1,3 +1,4 @@
+import {debounce} from 'lodash'
import {useState, useEffect} from 'react'
export function useDebounce(value: T, delay: number): T {
@@ -15,3 +16,12 @@ export function useDebounce(value: T, delay: number): T {
return debouncedValue
}
+
+export const debounceSearch = debounce(
+ (query: string, callback: () => void) => {
+ if (query.length >= 2) {
+ callback()
+ }
+ },
+ 1500,
+)
diff --git a/modules/use-debounce/package.json b/modules/use-debounce/package.json
index 75f58bea37..00129e6cd3 100644
--- a/modules/use-debounce/package.json
+++ b/modules/use-debounce/package.json
@@ -8,6 +8,9 @@
"scripts": {
"test": "jest"
},
+ "dependencies": {
+ "lodash": "4.17.21"
+ },
"peerDependencies": {
"react": "^18.0.0"
}
diff --git a/package-lock.json b/package-lock.json
index e6ed57b2ee..8c296813c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -576,6 +576,9 @@
"name": "@frogpond/use-debounce",
"version": "1.0.0",
"license": "ISC",
+ "dependencies": {
+ "lodash": "4.17.21"
+ },
"peerDependencies": {
"react": "^18.0.0"
}
diff --git a/source/navigation/routes.tsx b/source/navigation/routes.tsx
index 250d668654..e0c0f2400b 100644
--- a/source/navigation/routes.tsx
+++ b/source/navigation/routes.tsx
@@ -294,6 +294,11 @@ const SettingsStackScreens = () => {
name="APITest"
options={settings.APITestNavigationOptions}
/>
+
{
- let [searchPath, setSearchPath] = React.useState('')
- let path = useDebounce(searchPath.trim().toLowerCase(), 500)
-
- let [displayMode, setDisplayMode] = React.useState('raw')
-
- let {data, isLoading, error} = useQuery({
- queryKey: ['api-test', path],
- queryFn: ({signal, queryKey: [_group, path]}) => {
- if (!path) {
- return ''
- }
- return client.get(path, {signal, cache: 'no-store'}).text()
- },
- staleTime: 0,
- cacheTime: 0,
- })
-
- let navigation = useNavigation()
-
- const HeaderLeftButton = () => (
- setSearchPath('')}
- style={commonStyles.button}
- >
- Reset
-
- )
-
- React.useLayoutEffect(() => {
- navigation.setOptions({
- headerLeft: () => Platform.OS === 'ios' && ,
- headerSearchBarOptions: {
- autoCapitalize: 'none',
- barTintColor: c.systemFill,
- // android-only
- autoFocus: true,
- hideNavigationBar: false,
- hideWhenScrolling: false,
- onChangeText: (event: ChangeTextEvent) =>
- setSearchPath(event.nativeEvent.text),
- placeholder: 'path/to/uri',
- },
- })
- }, [navigation])
-
- const JSONView = (): JSX.Element => {
- if (data === undefined) {
- return <>>
- }
-
- const parsed = JSON.parse(data ?? '')
- const formatted = JSON.stringify(parsed, null, 2)
- const highlighted = syntaxHighlight(formatted)
-
- const HTML_CONTENT = `
- ${CSS_CODE_STYLES}
- ${highlighted}
- `
-
- return (
-
- )
- }
-
- const EmptySearch = () => {
- return (
-
-
- Search for an API Endpoint
-
- )
- }
-
- let APIResponse =
- error !== null ? (
-
- ) : !path ? (
-
- ) : isLoading ? (
-
- ) : displayMode === 'raw' ? (
-
- ) : (
-
- )
-
- const SearchResponseView = () => (
-
- setDisplayMode(val ? 'parsed' : 'raw')}
- value={displayMode === 'parsed'}
- />
-
- {APIResponse}
-
- )
-
- return path.length ? (
-
- ) : (
-
- )
-}
-
-export const NavigationOptions: NativeStackNavigationOptions = {
- title: 'API Tester',
- presentation: 'modal',
- headerRight: () => Platform.OS === 'ios' && ,
- gestureEnabled: Platform.OS === 'ios' && false,
-}
diff --git a/source/views/settings/screens/api-test/detail.tsx b/source/views/settings/screens/api-test/detail.tsx
new file mode 100644
index 0000000000..a449e3c452
--- /dev/null
+++ b/source/views/settings/screens/api-test/detail.tsx
@@ -0,0 +1,125 @@
+import React from 'react'
+import {View, StyleSheet, Platform, TextInput} from 'react-native'
+
+import {LoadingView, NoticeView} from '@frogpond/notice'
+import * as c from '@frogpond/colors'
+
+import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'
+import {NativeStackNavigationOptions} from '@react-navigation/native-stack'
+import {SettingsStackParamList} from '../../../../navigation/types'
+import {useQuery} from '@tanstack/react-query'
+import {client} from '@frogpond/api'
+import {iOSUIKit, material} from 'react-native-typography'
+import {HtmlContent} from '@frogpond/html-content'
+import {CellToggle} from '@frogpond/tableview/cells'
+import {NetworkLoggerButton} from '@frogpond/navigation-buttons'
+import {CSS_CODE_STYLES} from './util/highlight-styles'
+import {syntaxHighlight} from './util/highlight'
+import {DebugView} from '../debug'
+
+type DisplayMode = 'raw' | 'parsed'
+
+export const APITestDetailView = (): JSX.Element => {
+ let navigation = useNavigation()
+ let route = useRoute>()
+
+ const {displayName} = route.params.query
+ const cleanedName = displayName.trim().toLowerCase()
+ let [displayMode, setDisplayMode] = React.useState('raw')
+
+ let {data, isLoading, error} = useQuery({
+ queryKey: ['api-test', cleanedName],
+ queryFn: ({signal, queryKey: [_group]}) => {
+ if (!cleanedName) {
+ return ''
+ }
+ return client.get(cleanedName, {signal, cache: 'no-store'}).text()
+ },
+ staleTime: 0,
+ cacheTime: 0,
+ })
+
+ React.useLayoutEffect(() => {
+ navigation.setOptions({
+ title: cleanedName,
+ headerRight: () => ,
+ })
+ }, [cleanedName, navigation])
+
+ const JSONView = React.useCallback((): JSX.Element => {
+ if (data === undefined) {
+ return <>>
+ }
+
+ const parsed = JSON.parse(data ?? '')
+ const formatted = JSON.stringify(parsed, null, 2)
+ const highlighted = syntaxHighlight(formatted)
+
+ const HTML_CONTENT = `
+ ${CSS_CODE_STYLES}
+ ${highlighted}
+ `
+
+ return (
+
+ )
+ }, [data])
+
+ let APIResponse =
+ error !== null ? (
+
+ ) : !isLoading && !cleanedName ? (
+
+ ) : isLoading ? (
+
+ ) : displayMode === 'raw' ? (
+
+ ) : (
+
+ )
+
+ return (
+
+ setDisplayMode(val ? 'parsed' : 'raw')}
+ value={displayMode === 'parsed'}
+ />
+
+ {APIResponse}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: c.systemBackground,
+ flex: 1,
+ },
+ error: {
+ padding: 10,
+ color: c.brickRed,
+ },
+ output: {
+ marginVertical: 3,
+ paddingRight: 4,
+ ...Platform.select({
+ ios: iOSUIKit.bodyObject,
+ android: material.body1Object,
+ }),
+ },
+})
+
+export const NavigationOptions: NativeStackNavigationOptions = {}
diff --git a/source/views/settings/screens/api-test/index.ts b/source/views/settings/screens/api-test/index.ts
index 9d8c22dbf3..f93c5389ab 100644
--- a/source/views/settings/screens/api-test/index.ts
+++ b/source/views/settings/screens/api-test/index.ts
@@ -1,4 +1,8 @@
export {
APITestView,
NavigationOptions as APITestNavigationOptions,
-} from './api-test'
+} from './list'
+export {
+ APITestDetailView,
+ NavigationOptions as APITestDetailNavigationOptions,
+} from './detail'
diff --git a/source/views/settings/screens/api-test/list.tsx b/source/views/settings/screens/api-test/list.tsx
new file mode 100644
index 0000000000..2f66abe81a
--- /dev/null
+++ b/source/views/settings/screens/api-test/list.tsx
@@ -0,0 +1,122 @@
+import React from 'react'
+import {View, SectionList, SafeAreaView, StyleSheet} from 'react-native'
+
+import {ListRow, ListSectionHeader, ListSeparator, Title} from '@frogpond/lists'
+import {Column} from '@frogpond/layout'
+import {LoadingView, NoticeView} from '@frogpond/notice'
+import * as c from '@frogpond/colors'
+import {debounceSearch} from '@frogpond/use-debounce'
+import {NetworkLoggerButton} from '@frogpond/navigation-buttons'
+
+import {ServerRoute, useServerRoutes} from './query'
+import {ChangeTextEvent} from '../../../../navigation/types'
+import {useNavigation} from '@react-navigation/native'
+import {NativeStackNavigationOptions} from '@react-navigation/native-stack'
+
+export const APITestView = (): JSX.Element => {
+ let navigation = useNavigation()
+
+ let [filterPath, setFilterPath] = React.useState('')
+
+ let {
+ data: groupedRoutes = [],
+ error: routesError,
+ isLoading: isRoutesLoading,
+ isError: isRoutesError,
+ refetch: routesRefetch,
+ } = useServerRoutes()
+
+ React.useLayoutEffect(() => {
+ navigation.setOptions({
+ headerSearchBarOptions: {
+ autoCapitalize: 'none',
+ barTintColor: c.systemFill,
+ // android-only
+ autoFocus: true,
+ hideNavigationBar: false,
+ hideWhenScrolling: false,
+ onChangeText: (event: ChangeTextEvent) =>
+ setFilterPath(event.nativeEvent.text),
+ placeholder: '/path/to/uri',
+ },
+ headerRight: () => ,
+ })
+ }, [navigation])
+
+ let showSearchResult = React.useCallback(() => {
+ navigation.navigate('APITestDetail', {
+ query: {
+ displayName: filterPath,
+ path: filterPath,
+ params: [],
+ },
+ })
+ }, [filterPath, navigation])
+
+ React.useEffect(() => {
+ debounceSearch(filterPath, () => showSearchResult())
+ }, [filterPath, navigation, showSearchResult])
+
+ const renderItem = React.useCallback(
+ (item: ServerRoute) => (
+
+ navigation.navigate('APITestDetail', {query: item})}
+ style={styles.serverRouteRow}
+ >
+
+ {item.displayName}
+
+
+
+ ),
+ [navigation],
+ )
+
+ return (
+
+ {isRoutesLoading ? (
+
+ ) : isRoutesError && routesError instanceof Error ? (
+
+ ) : !groupedRoutes ? (
+
+ ) : (
+ `${item.path}-${index}`}
+ keyboardDismissMode="on-drag"
+ keyboardShouldPersistTaps="never"
+ onRefresh={routesRefetch}
+ refreshing={isRoutesLoading}
+ renderItem={({item}) => renderItem(item)}
+ renderSectionHeader={({section: {title}}) => (
+
+ )}
+ sections={groupedRoutes}
+ />
+ )}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ serverRouteContainer: {
+ flex: 1,
+ backgroundColor: c.systemBackground,
+ },
+ serverRouteRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+})
+
+export const NavigationOptions: NativeStackNavigationOptions = {
+ title: 'API Tester',
+}
diff --git a/source/views/settings/screens/api-test/routes-list.tsx b/source/views/settings/screens/api-test/routes-list.tsx
deleted file mode 100644
index e986d21868..0000000000
--- a/source/views/settings/screens/api-test/routes-list.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import React from 'react'
-import {View, SectionList, SafeAreaView, StyleSheet} from 'react-native'
-
-import {ListRow, ListSectionHeader, ListSeparator, Title} from '@frogpond/lists'
-import {Column} from '@frogpond/layout'
-import {LoadingView, NoticeView} from '@frogpond/notice'
-import * as c from '@frogpond/colors'
-
-import {useServerRoutes} from './query'
-
-interface ServerRoutesListParams {
- setSearchPath: React.Dispatch>
-}
-
-export const ServerRoutesListView = (
- props: ServerRoutesListParams,
-): JSX.Element => {
- let {
- data: groupedRoutes = [],
- error: routesError,
- isLoading: isRoutesLoading,
- isError: isRoutesError,
- refetch: routesRefetch,
- } = useServerRoutes()
-
- return (
-
- {isRoutesLoading ? (
-
- ) : isRoutesError && routesError instanceof Error ? (
-
- ) : !groupedRoutes ? (
-
- ) : (
- `${item.path}-${index}`}
- keyboardDismissMode="on-drag"
- keyboardShouldPersistTaps="never"
- onRefresh={routesRefetch}
- refreshing={isRoutesLoading}
- renderItem={({item}) => {
- return (
-
- props.setSearchPath(item.displayName)}
- style={styles.serverRouteRow}
- >
-
- {item.displayName}
-
-
-
- )
- }}
- renderSectionHeader={({section: {title}}) => (
-
- )}
- sections={groupedRoutes}
- />
- )}
-
- )
-}
-
-const styles = StyleSheet.create({
- serverRouteContainer: {
- flex: 1,
- backgroundColor: c.systemBackground,
- },
- serverRouteRow: {
- flexDirection: 'row',
- alignItems: 'center',
- },
-})