diff --git a/lib/components/debug-logs-context-provider.react.js b/lib/components/debug-logs-context-provider.react.js new file mode 100644 --- /dev/null +++ b/lib/components/debug-logs-context-provider.react.js @@ -0,0 +1,45 @@ +// @flow + +import * as React from 'react'; + +import { type DebugLog, DebugLogsContext } from './debug-logs-context.js'; + +type Props = { + +children: React.Node, +}; + +function DebugLogsContextProvider(props: Props): React.Node { + const [logs, setLogs] = React.useState<$ReadOnlyArray>([]); + + const addLog = React.useCallback( + (title: string, message: string) => + setLogs(prevLogs => [ + ...prevLogs, + { + title, + message, + timestamp: Date.now(), + }, + ]), + [], + ); + + const clearLogs = React.useCallback(() => setLogs([]), []); + + const contextValue = React.useMemo( + () => ({ + logs, + addLog, + clearLogs, + }), + [addLog, clearLogs, logs], + ); + + return ( + + {props.children} + + ); +} + +export { DebugLogsContextProvider }; diff --git a/lib/components/debug-logs-context.js b/lib/components/debug-logs-context.js new file mode 100644 --- /dev/null +++ b/lib/components/debug-logs-context.js @@ -0,0 +1,31 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; + +export type DebugLog = { + +title: string, + +message: string, + +timestamp: number, +}; + +export type DebugLogsContextType = { + +logs: $ReadOnlyArray, + +addLog: (title: string, message: string) => mixed, + +clearLogs: () => mixed, +}; + +const DebugLogsContext: React.Context = + React.createContext({ + logs: [], + addLog: () => {}, + clearLogs: () => {}, + }); + +function useDebugLogs(): DebugLogsContextType { + const debugLogsContext = React.useContext(DebugLogsContext); + invariant(debugLogsContext, 'Debug logs context should be present'); + return debugLogsContext; +} + +export { DebugLogsContext, useDebugLogs }; diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js --- a/native/navigation/route-names.js +++ b/native/navigation/route-names.js @@ -186,6 +186,7 @@ export const SecondaryDeviceConnectedRouteName = 'SecondaryDeviceConnected'; export const SecondaryDeviceNotRespondingRouteName = 'SecondaryDeviceNotResponding'; +export const DebugLogsScreenRouteName = 'DebugLogsScreen'; export type RootParamList = { +LoggedOutModal: void, @@ -301,6 +302,7 @@ +KeyserverSelectionList: void, +AddKeyserver: void, +FarcasterAccountSettings: void, + +DebugLogsScreenRouteName: void, }; export type CalendarParamList = { diff --git a/native/profile/debug-logs-screen.react.js b/native/profile/debug-logs-screen.react.js new file mode 100644 --- /dev/null +++ b/native/profile/debug-logs-screen.react.js @@ -0,0 +1,81 @@ +// @flow +import Clipboard from '@react-native-clipboard/clipboard'; +import * as React from 'react'; +import { FlatList, View, Text } from 'react-native'; + +import { + useDebugLogs, + type DebugLog, +} from 'lib/components/debug-logs-context.js'; + +import type { ProfileNavigationProp } from './profile.react.js'; +import PrimaryButton from '../components/primary-button.react.js'; +import type { NavigationRoute } from '../navigation/route-names.js'; +import { useStyles } from '../themes/colors.js'; + +type Props = { + +navigation: ProfileNavigationProp<'DebugLogsScreen'>, + +route: NavigationRoute<'DebugLogsScreen'>, +}; + +// eslint-disable-next-line no-unused-vars +function DebugLogsScreen(props: Props): React.Node { + const { logs, clearLogs } = useDebugLogs(); + + const copyLogs = React.useCallback(() => { + Clipboard.setString(JSON.stringify(logs)); + }, [logs]); + + const styles = useStyles(unboundStyles); + + const renderItem = React.useCallback( + ({ item }: { +item: DebugLog, ... }) => { + const date = new Date(item.timestamp); + const timestampString = date.toISOString(); + return ( + + {timestampString} + {item.title} + {item.message} + + ); + }, + [styles.item, styles.message, styles.timestamp, styles.title], + ); + + return ( + + + + + + ); +} + +const unboundStyles = { + view: { + flex: 1, + backgroundColor: 'panelBackground', + padding: 24, + }, + item: { + backgroundColor: 'panelForeground', + borderWidth: 1, + borderTopWidth: 1, + borderColor: 'panelForegroundBorder', + marginBottom: 8, + padding: 8, + }, + timestamp: { + color: 'panelForegroundLabel', + }, + title: { + color: 'panelForegroundLabel', + fontSize: 16, + }, + message: { + color: 'panelForegroundSecondaryLabel', + }, +}; + +export default DebugLogsScreen; diff --git a/native/profile/profile-screen.react.js b/native/profile/profile-screen.react.js --- a/native/profile/profile-screen.react.js +++ b/native/profile/profile-screen.react.js @@ -57,6 +57,7 @@ KeyserverSelectionListRouteName, TunnelbrokerMenuRouteName, FarcasterAccountSettingsRouteName, + DebugLogsScreenRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; @@ -199,7 +200,8 @@ let developerTools, defaultNotifications, keyserverSelection, - tunnelbrokerMenu; + tunnelbrokerMenu, + debugLogs; const { staffCanSee } = this.props; if (staffCanSee) { developerTools = ( @@ -226,6 +228,10 @@ onPress={this.onPressTunnelbrokerMenu} /> ); + + debugLogs = ( + + ); } let backupMenu; @@ -342,6 +348,7 @@ {developerTools} {dmActions} + {debugLogs} { void this.props.onCreateDMThread(); }; + + onPressDebugLogs = () => { + this.props.navigation.navigate({ name: DebugLogsScreenRouteName }); + }; } const logOutLoadingStatusSelector = diff --git a/native/profile/profile.react.js b/native/profile/profile.react.js --- a/native/profile/profile.react.js +++ b/native/profile/profile.react.js @@ -14,6 +14,7 @@ import AppearancePreferences from './appearance-preferences.react.js'; import BackupMenu from './backup-menu.react.js'; import BuildInfo from './build-info.react.js'; +import DebugLogsScreen from './debug-logs-screen.react.js'; import DefaultNotificationsPreferences from './default-notifications-preferences.react.js'; import DeleteAccount from './delete-account.react.js'; import DevTools from './dev-tools.react.js'; @@ -52,6 +53,7 @@ type ScreenParamList, type ProfileParamList, TunnelbrokerMenuRouteName, + DebugLogsScreenRouteName, } from '../navigation/route-names.js'; import type { TabNavigationProp } from '../navigation/tab-navigator.react.js'; import { useStyles, useColors } from '../themes/colors.js'; @@ -83,6 +85,7 @@ const blockListOptions = { headerTitle: 'Block list' }; const defaultNotificationsOptions = { headerTitle: 'Default Notifications' }; const farcasterSettingsOptions = { headerTitle: 'Farcaster account' }; +const debugLogsScreenOptions = { headerTitle: 'Logs' }; export type ProfileNavigationProp< RouteName: $Keys = $Keys, @@ -222,6 +225,11 @@ component={FarcasterAccountSettings} options={farcasterSettingsOptions} /> + diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -24,6 +24,7 @@ import { PersistGate as ReduxPersistGate } from 'redux-persist/es/integration/react.js'; import { ChatMentionContextProvider } from 'lib/components/chat-mention-provider.react.js'; +import { DebugLogsContextProvider } from 'lib/components/debug-logs-context-provider.react.js'; import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js'; import { ENSCacheProvider } from 'lib/components/ens-cache-provider.react.js'; import { FarcasterChannelPrefetchHandler } from 'lib/components/farcaster-channel-prefetch-handler.react.js'; @@ -334,95 +335,97 @@ ); } return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - {gated} - - - - - - - - - - - - - - - {navigation} - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + {gated} + + + + + + + + + + + + + + + {navigation} + + + + + + + + + + + + + + + + + + + + + + + + ); }