diff --git a/lib/components/debug-logs-context-provider.react.js b/lib/components/debug-logs-context-provider.react.js new file mode 100644 index 000000000..f416920cb --- /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<DebugLog>>([]); + + 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 ( + <DebugLogsContext.Provider value={contextValue}> + {props.children} + </DebugLogsContext.Provider> + ); +} + +export { DebugLogsContextProvider }; diff --git a/lib/components/debug-logs-context.js b/lib/components/debug-logs-context.js new file mode 100644 index 000000000..4989abadb --- /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<DebugLog>, + +addLog: (title: string, message: string) => mixed, + +clearLogs: () => mixed, +}; + +const DebugLogsContext: React.Context<DebugLogsContextType> = + React.createContext<DebugLogsContextType>({ + 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 index e87834af4..5cd61338f 100644 --- a/native/navigation/route-names.js +++ b/native/navigation/route-names.js @@ -1,419 +1,421 @@ // @flow import type { RouteProp } from '@react-navigation/core'; import type { ActionResultModalParams } from './action-result-modal.react.js'; import type { InviteLinkModalParams } from './invite-link-modal.react'; import type { ConnectSecondaryDeviceParams } from '../account/qr-auth/connect-secondary-device.react.js'; import type { AvatarSelectionParams } from '../account/registration/avatar-selection.react.js'; import type { ConnectEthereumParams } from '../account/registration/connect-ethereum.react.js'; import type { ConnectFarcasterParams } from '../account/registration/connect-farcaster.react.js'; import type { EmojiAvatarSelectionParams } from '../account/registration/emoji-avatar-selection.react.js'; import type { ExistingEthereumAccountParams } from '../account/registration/existing-ethereum-account.react.js'; import type { KeyserverSelectionParams } from '../account/registration/keyserver-selection.react.js'; import type { PasswordSelectionParams } from '../account/registration/password-selection.react.js'; import type { RegistrationTermsParams } from '../account/registration/registration-terms.react.js'; import type { CreateSIWEBackupMessageParams } from '../account/registration/siwe-backup-message-creation.react.js'; import type { UsernameSelectionParams } from '../account/registration/username-selection.react.js'; import type { RestoreBackupScreenParams } from '../account/restore-backup-screen.react'; import type { TermsAndPrivacyModalParams } from '../account/terms-and-privacy-modal.react.js'; import type { RestoreSIWEBackupParams } from '../backup/restore-siwe-backup.react.js'; import type { ThreadPickerModalParams } from '../calendar/thread-picker-modal.react.js'; import type { ComposeSubchannelParams } from '../chat/compose-subchannel.react.js'; import type { FullScreenThreadMediaGalleryParams } from '../chat/fullscreen-thread-media-gallery.react.js'; import type { ImagePasteModalParams } from '../chat/image-paste-modal.react.js'; import type { MessageListParams } from '../chat/message-list-types.js'; import type { MessageReactionsModalParams } from '../chat/message-reactions-modal.react.js'; import type { MultimediaMessageTooltipModalParams } from '../chat/multimedia-message-tooltip-modal.react.js'; import type { PinnedMessagesScreenParams } from '../chat/pinned-messages-screen.react.js'; import type { RobotextMessageTooltipModalParams } from '../chat/robotext-message-tooltip-modal.react.js'; import type { AddUsersModalParams } from '../chat/settings/add-users-modal.react.js'; import type { ColorSelectorModalParams } from '../chat/settings/color-selector-modal.react.js'; import type { ComposeSubchannelModalParams } from '../chat/settings/compose-subchannel-modal.react.js'; import type { DeleteThreadParams } from '../chat/settings/delete-thread.react.js'; import type { EmojiThreadAvatarCreationParams } from '../chat/settings/emoji-thread-avatar-creation.react.js'; import type { ThreadSettingsMemberTooltipModalParams } from '../chat/settings/thread-settings-member-tooltip-modal.react.js'; import type { ThreadSettingsNotificationsParams } from '../chat/settings/thread-settings-notifications.react.js'; import type { ThreadSettingsParams } from '../chat/settings/thread-settings.react.js'; import type { SidebarListModalParams } from '../chat/sidebar-list-modal.react.js'; import type { SubchannelListModalParams } from '../chat/subchannels-list-modal.react.js'; import type { TextMessageTooltipModalParams } from '../chat/text-message-tooltip-modal.react.js'; import type { TogglePinModalParams } from '../chat/toggle-pin-modal.react.js'; import type { TagFarcasterChannelByNameParams } from '../community-settings/tag-farcaster-channel/tag-farcaster-channel-by-name.react.js'; import type { TagFarcasterChannelParams } from '../community-settings/tag-farcaster-channel/tag-farcaster-channel.react.js'; import type { CommunityJoinerModalParams } from '../components/community-joiner-modal.react.js'; import type { InviteLinksNavigatorParams } from '../invite-links/invite-links-navigator.react.js'; import type { ManagePublicLinkScreenParams } from '../invite-links/manage-public-link-screen.react.js'; import type { ViewInviteLinksScreenParams } from '../invite-links/view-invite-links-screen.react.js'; import type { ChatCameraModalParams } from '../media/chat-camera-modal.react.js'; import type { ImageModalParams } from '../media/image-modal.react.js'; import type { ThreadAvatarCameraModalParams } from '../media/thread-avatar-camera-modal.react.js'; import type { VideoPlaybackModalParams } from '../media/video-playback-modal.react.js'; import type { CustomServerModalParams } from '../profile/custom-server-modal.react.js'; import type { KeyserverSelectionBottomSheetParams } from '../profile/keyserver-selection-bottom-sheet.react.js'; import type { LinkedDevicesBottomSheetParams } from '../profile/linked-devices-bottom-sheet.react.js'; import type { UserRelationshipTooltipModalParams } from '../profile/user-relationship-tooltip-modal.react.js'; import type { ChangeRolesScreenParams } from '../roles/change-roles-screen.react.js'; import type { CommunityRolesScreenParams } from '../roles/community-roles-screen.react.js'; import type { CreateRolesScreenParams } from '../roles/create-roles-screen.react.js'; import type { MessageSearchParams } from '../search/message-search.react.js'; import type { NUXTipsOverlayParams } from '../tooltip/nux-tips-overlay.react.js'; import type { UserProfileAvatarModalParams } from '../user-profile/user-profile-avatar-modal.react.js'; import type { UserProfileBottomSheetParams } from '../user-profile/user-profile-bottom-sheet.react.js'; export const ActionResultModalRouteName = 'ActionResultModal'; export const AddUsersModalRouteName = 'AddUsersModal'; export const AppearancePreferencesRouteName = 'AppearancePreferences'; export const AppRouteName = 'App'; export const AppsRouteName = 'Apps'; export const BackgroundChatThreadListRouteName = 'BackgroundChatThreadList'; export const BackupMenuRouteName = 'BackupMenu'; export const BlockListRouteName = 'BlockList'; export const BuildInfoRouteName = 'BuildInfo'; export const CalendarRouteName = 'Calendar'; export const CalendarScreenRouteName = 'CalendarScreen'; export const ChangeRolesScreenRouteName = 'ChangeRolesScreen'; export const ChatCameraModalRouteName = 'ChatCameraModal'; export const ChatRouteName = 'Chat'; export const ChatThreadListRouteName = 'ChatThreadList'; export const ColorSelectorModalRouteName = 'ColorSelectorModal'; export const ComposeSubchannelModalRouteName = 'ComposeSubchannelModal'; export const ComposeSubchannelRouteName = 'ComposeSubchannel'; export const CommunityDrawerNavigatorRouteName = 'CommunityDrawerNavigator'; export const CustomServerModalRouteName = 'CustomServerModal'; export const DefaultNotificationsPreferencesRouteName = 'DefaultNotifications'; export const DeleteAccountRouteName = 'DeleteAccount'; export const DeleteThreadRouteName = 'DeleteThread'; export const DevToolsRouteName = 'DevTools'; export const EditPasswordRouteName = 'EditPassword'; export const EmojiThreadAvatarCreationRouteName = 'EmojiThreadAvatarCreation'; export const EmojiUserAvatarCreationRouteName = 'EmojiUserAvatarCreation'; export const FriendListRouteName = 'FriendList'; export const FullScreenThreadMediaGalleryRouteName = 'FullScreenThreadMediaGallery'; export const HomeChatThreadListRouteName = 'HomeChatThreadList'; export const ImageModalRouteName = 'ImageModal'; export const ImagePasteModalRouteName = 'ImagePasteModal'; export const InviteLinkModalRouteName = 'InviteLinkModal'; export const InviteLinkNavigatorRouteName = 'InviteLinkNavigator'; export const LinkedDevicesRouteName = 'LinkedDevices'; export const LinkedDevicesBottomSheetRouteName = 'LinkedDevicesBottomSheet'; export const LoggedOutModalRouteName = 'LoggedOutModal'; export const ManagePublicLinkRouteName = 'ManagePublicLink'; export const MessageListRouteName = 'MessageList'; export const MessageReactionsModalRouteName = 'MessageReactionsModal'; export const PinnedMessagesScreenRouteName = 'PinnedMessagesScreen'; export const MultimediaMessageTooltipModalRouteName = 'MultimediaMessageTooltipModal'; export const PrivacyPreferencesRouteName = 'PrivacyPreferences'; export const ProfileRouteName = 'Profile'; export const ProfileScreenRouteName = 'ProfileScreen'; export const UserRelationshipTooltipModalRouteName = 'UserRelationshipTooltipModal'; export const RobotextMessageTooltipModalRouteName = 'RobotextMessageTooltipModal'; export const SecondaryDeviceQRCodeScannerRouteName = 'SecondaryDeviceQRCodeScanner'; export const SidebarListModalRouteName = 'SidebarListModal'; export const SubchannelsListModalRouteName = 'SubchannelsListModal'; export const TabNavigatorRouteName = 'TabNavigator'; export const TextMessageTooltipModalRouteName = 'TextMessageTooltipModal'; export const ThreadAvatarCameraModalRouteName = 'ThreadAvatarCameraModal'; export const ThreadPickerModalRouteName = 'ThreadPickerModal'; export const ThreadSettingsMemberTooltipModalRouteName = 'ThreadSettingsMemberTooltipModal'; export const ThreadSettingsRouteName = 'ThreadSettings'; export const TunnelbrokerMenuRouteName = 'TunnelbrokerMenu'; export const UserAvatarCameraModalRouteName = 'UserAvatarCameraModal'; export const TogglePinModalRouteName = 'TogglePinModal'; export const VideoPlaybackModalRouteName = 'VideoPlaybackModal'; export const ViewInviteLinksRouteName = 'ViewInviteLinks'; export const TermsAndPrivacyRouteName = 'TermsAndPrivacyModal'; export const AuthRouteName = 'Auth'; export const KeyserverSelectionRouteName = 'KeyserverSelection'; export const CoolOrNerdModeSelectionRouteName = 'CoolOrNerdModeSelection'; export const ConnectEthereumRouteName = 'ConnectEthereum'; export const CreateSIWEBackupMessageRouteName = 'CreateSIWEBackupMessage'; export const CreateMissingSIWEBackupMessageRouteName = 'CreateMissingSIWEBackupMessage'; export const RestoreSIWEBackupRouteName = 'RestoreSIWEBackup'; export const ExistingEthereumAccountRouteName = 'ExistingEthereumAccount'; export const ConnectFarcasterRouteName = 'ConnectFarcaster'; export const UsernameSelectionRouteName = 'UsernameSelection'; export const CommunityCreationRouteName = 'CommunityCreation'; export const CommunityConfigurationRouteName = 'CommunityConfiguration'; export const MessageSearchRouteName = 'MessageSearch'; export const PasswordSelectionRouteName = 'PasswordSelection'; export const AvatarSelectionRouteName = 'AvatarSelection'; export const EmojiAvatarSelectionRouteName = 'EmojiAvatarSelection'; export const RegistrationUserAvatarCameraModalRouteName = 'RegistrationUserAvatarCameraModal'; export const RegistrationTermsRouteName = 'RegistrationTerms'; export const RolesNavigatorRouteName = 'RolesNavigator'; export const CommunityRolesScreenRouteName = 'CommunityRolesScreen'; export const CreateRolesScreenRouteName = 'CreateRolesScreen'; export const QRCodeScreenRouteName = 'QRCodeScreen'; export const RestorePromptScreenRouteName = 'RestorePromptScreen'; export const RestorePasswordAccountScreenRouteName = 'RestorePasswordAccountScreen'; export const RestoreBackupScreenRouteName = 'RestoreBackupScreen'; export const UserProfileBottomSheetNavigatorRouteName = 'UserProfileBottomSheetNavigator'; export const UserProfileBottomSheetRouteName = 'UserProfileBottomSheet'; export const UserProfileAvatarModalRouteName = 'UserProfileAvatarModal'; export const KeyserverSelectionListRouteName = 'KeyserverSelectionList'; export const AddKeyserverRouteName = 'AddKeyserver'; export const KeyserverSelectionBottomSheetRouteName = 'KeyserverSelectionBottomSheet'; export const AccountDoesNotExistRouteName = 'AccountDoesNotExist'; export const FarcasterAccountSettingsRouteName = 'FarcasterAccountSettings'; export const ConnectFarcasterBottomSheetRouteName = 'ConnectFarcasterBottomSheet'; export const DirectoryPromptBottomSheetRouteName = 'DirectoryPromptBottomSheet'; export const CommunityJoinerModalRouteName = 'CommunityJoinerModal'; export const TagFarcasterChannelNavigatorRouteName = 'TagFarcasterChannelNavigator'; export const TagFarcasterChannelRouteName = 'TagFarcasterChannel'; export const TagFarcasterChannelByNameRouteName = 'TagFarcasterChannelByName'; export const ThreadSettingsNotificationsRouteName = 'ThreadSettingsNotifications'; export const IntroTipRouteName = 'IntroTip'; export const CommunityDrawerTipRouteName = 'CommunityDrawerTip'; export const HomeTabTipRouteName = 'HomeTabTip'; export const MutedTabTipRouteName = 'MutedTabTip'; export const NUXTipOverlayBackdropRouteName = 'NUXTipOverlayBackdrop'; export const QRAuthNavigatorRouteName = 'QRAuthNavigator'; export const QRAuthNotPrimaryDeviceRouteName = 'QRAuthNotPrimaryDevice'; export const ConnectSecondaryDeviceRouteName = 'ConnectSecondaryDevice'; export const SecondaryDeviceConnectedRouteName = 'SecondaryDeviceConnected'; export const SecondaryDeviceNotRespondingRouteName = 'SecondaryDeviceNotResponding'; +export const DebugLogsScreenRouteName = 'DebugLogsScreen'; export type RootParamList = { +LoggedOutModal: void, +App: void, +ThreadPickerModal: ThreadPickerModalParams, +AddUsersModal: AddUsersModalParams, +CustomServerModal: CustomServerModalParams, +ColorSelectorModal: ColorSelectorModalParams, +ComposeSubchannelModal: ComposeSubchannelModalParams, +SidebarListModal: SidebarListModalParams, +ImagePasteModal: ImagePasteModalParams, +TermsAndPrivacyModal: TermsAndPrivacyModalParams, +SubchannelsListModal: SubchannelListModalParams, +MessageReactionsModal: MessageReactionsModalParams, +Auth: void, +CommunityCreation: void, +InviteLinkModal: InviteLinkModalParams, +InviteLinkNavigator: InviteLinksNavigatorParams, +RolesNavigator: void, +UserProfileBottomSheetNavigator: void, +TunnelbrokerMenu: void, +KeyserverSelectionBottomSheet: KeyserverSelectionBottomSheetParams, +LinkedDevicesBottomSheet: LinkedDevicesBottomSheetParams, +ConnectFarcasterBottomSheet: void, +DirectoryPromptBottomSheet: void, +CommunityJoinerModal: CommunityJoinerModalParams, +TagFarcasterChannelNavigator: void, +CreateMissingSIWEBackupMessage: void, +QRAuthNavigator: void, }; export type NUXTipRouteNames = | typeof IntroTipRouteName | typeof CommunityDrawerTipRouteName | typeof HomeTabTipRouteName | typeof MutedTabTipRouteName; export type MessageTooltipRouteNames = | typeof RobotextMessageTooltipModalRouteName | typeof MultimediaMessageTooltipModalRouteName | typeof TextMessageTooltipModalRouteName; export const PinnableMessageTooltipRouteNames = [ TextMessageTooltipModalRouteName, MultimediaMessageTooltipModalRouteName, ]; export type TooltipModalParamList = { +MultimediaMessageTooltipModal: MultimediaMessageTooltipModalParams, +TextMessageTooltipModal: TextMessageTooltipModalParams, +ThreadSettingsMemberTooltipModal: ThreadSettingsMemberTooltipModalParams, +UserRelationshipTooltipModal: UserRelationshipTooltipModalParams, +RobotextMessageTooltipModal: RobotextMessageTooltipModalParams, }; export type OverlayParamList = { +CommunityDrawerNavigator: void, +ImageModal: ImageModalParams, +ActionResultModal: ActionResultModalParams, +ChatCameraModal: ChatCameraModalParams, +UserAvatarCameraModal: void, +ThreadAvatarCameraModal: ThreadAvatarCameraModalParams, +VideoPlaybackModal: VideoPlaybackModalParams, +TogglePinModal: TogglePinModalParams, +IntroTip: NUXTipsOverlayParams, +CommunityDrawerTip: NUXTipsOverlayParams, +HomeTabTip: NUXTipsOverlayParams, +MutedTabTip: NUXTipsOverlayParams, +NUXTipOverlayBackdrop: void, ...TooltipModalParamList, }; export type TabParamList = { +Calendar: void, +Chat: void, +Profile: void, +Apps: void, }; export type ChatParamList = { +ChatThreadList: void, +MessageList: MessageListParams, +ComposeSubchannel: ComposeSubchannelParams, +ThreadSettings: ThreadSettingsParams, +EmojiThreadAvatarCreation: EmojiThreadAvatarCreationParams, +DeleteThread: DeleteThreadParams, +FullScreenThreadMediaGallery: FullScreenThreadMediaGalleryParams, +PinnedMessagesScreen: PinnedMessagesScreenParams, +MessageSearch: MessageSearchParams, +ChangeRolesScreen: ChangeRolesScreenParams, +ThreadSettingsNotifications: ThreadSettingsNotificationsParams, }; export type ChatTopTabsParamList = { +HomeChatThreadList: void, +BackgroundChatThreadList: void, }; export type ProfileParamList = { +ProfileScreen: void, +EmojiUserAvatarCreation: void, +EditPassword: void, +DeleteAccount: void, +BuildInfo: void, +DevTools: void, +AppearancePreferences: void, +PrivacyPreferences: void, +DefaultNotifications: void, +FriendList: void, +BlockList: void, +LinkedDevices: void, +BackupMenu: void, +TunnelbrokerMenu: void, +KeyserverSelectionList: void, +AddKeyserver: void, +FarcasterAccountSettings: void, + +DebugLogsScreen: void, }; export type CalendarParamList = { +CalendarScreen: void, }; export type CommunityDrawerParamList = { +TabNavigator: void }; export type AuthParamList = { +CoolOrNerdModeSelection: void, +KeyserverSelection: KeyserverSelectionParams, +ConnectEthereum: ConnectEthereumParams, +ExistingEthereumAccount: ExistingEthereumAccountParams, +ConnectFarcaster: ConnectFarcasterParams, +CreateSIWEBackupMessage: CreateSIWEBackupMessageParams, +UsernameSelection: UsernameSelectionParams, +PasswordSelection: PasswordSelectionParams, +AvatarSelection: AvatarSelectionParams, +EmojiAvatarSelection: EmojiAvatarSelectionParams, +RegistrationUserAvatarCameraModal: void, +RegistrationTerms: RegistrationTermsParams, +AccountDoesNotExist: void, +QRCodeScreen: void, +RestorePromptScreen: void, +RestorePasswordAccountScreen: void, +RestoreBackupScreen: RestoreBackupScreenParams, +RestoreSIWEBackup: RestoreSIWEBackupParams, }; export type InviteLinkParamList = { +ViewInviteLinks: ViewInviteLinksScreenParams, +ManagePublicLink: ManagePublicLinkScreenParams, }; export type CommunityCreationParamList = { +CommunityConfiguration: void, }; export type RolesParamList = { +CommunityRolesScreen: CommunityRolesScreenParams, +CreateRolesScreen: CreateRolesScreenParams, }; export type TagFarcasterChannelParamList = { +TagFarcasterChannel: TagFarcasterChannelParams, +TagFarcasterChannelByName: TagFarcasterChannelByNameParams, }; export type UserProfileBottomSheetParamList = { +UserProfileBottomSheet: UserProfileBottomSheetParams, +UserProfileAvatarModal: UserProfileAvatarModalParams, +UserRelationshipTooltipModal: UserRelationshipTooltipModalParams, }; export type QRAuthNavigatorParamList = { +SecondaryDeviceQRCodeScanner: void, +QRAuthNotPrimaryDevice: void, +ConnectSecondaryDevice: ConnectSecondaryDeviceParams, +SecondaryDeviceConnected: void, +SecondaryDeviceNotResponding: void, }; export type ScreenParamList = { ...RootParamList, ...OverlayParamList, ...TabParamList, ...ChatParamList, ...ChatTopTabsParamList, ...ProfileParamList, ...CalendarParamList, ...CommunityDrawerParamList, ...AuthParamList, ...InviteLinkParamList, ...CommunityCreationParamList, ...RolesParamList, ...UserProfileBottomSheetParamList, ...TagFarcasterChannelParamList, ...QRAuthNavigatorParamList, }; export type NavigationRoute<RouteName: string = $Keys<ScreenParamList>> = RouteProp<ScreenParamList, RouteName>; export const accountModals = [LoggedOutModalRouteName, AuthRouteName]; export const scrollBlockingModals = [ ImageModalRouteName, MultimediaMessageTooltipModalRouteName, TextMessageTooltipModalRouteName, ThreadSettingsMemberTooltipModalRouteName, UserRelationshipTooltipModalRouteName, RobotextMessageTooltipModalRouteName, VideoPlaybackModalRouteName, ]; export const chatRootModals = [ AddUsersModalRouteName, ColorSelectorModalRouteName, ComposeSubchannelModalRouteName, ]; export const threadRoutes = [ MessageListRouteName, ThreadSettingsRouteName, DeleteThreadRouteName, ComposeSubchannelRouteName, FullScreenThreadMediaGalleryRouteName, PinnedMessagesScreenRouteName, MessageSearchRouteName, EmojiThreadAvatarCreationRouteName, CommunityRolesScreenRouteName, ThreadSettingsNotificationsRouteName, ]; diff --git a/native/profile/debug-logs-screen.react.js b/native/profile/debug-logs-screen.react.js new file mode 100644 index 000000000..339dcd3e3 --- /dev/null +++ b/native/profile/debug-logs-screen.react.js @@ -0,0 +1,82 @@ +// @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 ( + <View style={styles.item}> + <Text style={styles.timestamp}>{timestampString}</Text> + <Text style={styles.title}>{item.title}</Text> + <Text style={styles.message}>{item.message}</Text> + </View> + ); + }, + [styles.item, styles.message, styles.timestamp, styles.title], + ); + + return ( + <View style={styles.view}> + <FlatList data={logs} renderItem={renderItem} /> + <PrimaryButton onPress={clearLogs} variant="danger" label="Clear Logs" /> + <PrimaryButton onPress={copyLogs} variant="enabled" label="Copy Logs" /> + </View> + ); +} + +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 index 8d92e968f..d98a9f958 100644 --- a/native/profile/profile-screen.react.js +++ b/native/profile/profile-screen.react.js @@ -1,641 +1,652 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text, Platform, ScrollView } from 'react-native'; import uuid from 'uuid'; import { logOutActionTypes, useBaseLogOut, usePrimaryDeviceLogOut, useSecondaryDeviceLogOut, } from 'lib/actions/user-actions.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { useCheckIfPrimaryDevice } from 'lib/hooks/primary-device-hooks.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import { type OutboundDMOperationSpecification, dmOperationSpecificationTypes, } from 'lib/shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from 'lib/shared/dm-ops/process-dm-ops.js'; import type { LogOutResult } from 'lib/types/account-types.js'; import type { DMCreateThreadOperation } from 'lib/types/dm-ops'; import { thickThreadTypes } from 'lib/types/thread-types-enum.js'; import { type CurrentUserInfo } from 'lib/types/user-types.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken, useIsRestoreFlowEnabled, } from 'lib/utils/services-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { deleteNativeCredentialsFor } from '../account/native-credentials.js'; import EditUserAvatar from '../avatars/edit-user-avatar.react.js'; import Action from '../components/action-row.react.js'; import Button from '../components/button.react.js'; import EditSettingButton from '../components/edit-setting-button.react.js'; import SingleLine from '../components/single-line.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { EditPasswordRouteName, DeleteAccountRouteName, BuildInfoRouteName, DevToolsRouteName, AppearancePreferencesRouteName, FriendListRouteName, BlockListRouteName, PrivacyPreferencesRouteName, DefaultNotificationsPreferencesRouteName, LinkedDevicesRouteName, BackupMenuRouteName, 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'; import Alert from '../utils/alert.js'; import { useShowVersionUnsupportedAlert } from '../utils/hooks.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; type ProfileRowProps = { +content: string, +onPress: () => void, +danger?: boolean, }; function ProfileRow(props: ProfileRowProps): React.Node { const { content, onPress, danger } = props; return ( <Action.Row onPress={onPress}> <Action.Text danger={danger} content={content} /> <Action.Icon name="ios-arrow-forward" /> </Action.Row> ); } const unboundStyles = { avatarSection: { alignItems: 'center', paddingVertical: 16, }, container: { flex: 1, }, content: { flex: 1, }, deleteAccountButton: { paddingHorizontal: 24, paddingVertical: 12, }, editPasswordButton: { paddingTop: Platform.OS === 'android' ? 3 : 2, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, label: { color: 'panelForegroundTertiaryLabel', fontSize: 16, paddingRight: 12, }, loggedInLabel: { color: 'panelForegroundTertiaryLabel', fontSize: 16, }, logOutText: { color: 'link', fontSize: 16, paddingLeft: 6, }, row: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, paddedRow: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 10, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, paddingVertical: 1, }, unpaddedSection: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, }, username: { color: 'panelForegroundLabel', flex: 1, }, value: { color: 'panelForegroundLabel', fontSize: 16, textAlign: 'right', }, }; type BaseProps = { +navigation: ProfileNavigationProp<'ProfileScreen'>, +route: NavigationRoute<'ProfileScreen'>, }; type Props = { ...BaseProps, +currentUserInfo: ?CurrentUserInfo, +isPrimaryDevice: boolean, +logOutLoading: boolean, +colors: Colors, +styles: $ReadOnly<typeof unboundStyles>, +dispatchActionPromise: DispatchActionPromise, +logOut: () => Promise<LogOutResult>, +logOutPrimaryDevice: () => Promise<LogOutResult>, +logOutSecondaryDevice: () => Promise<LogOutResult>, +staffCanSee: boolean, +stringForUser: ?string, +isAccountWithPassword: boolean, +onCreateDMThread: () => Promise<void>, +currentUserFID: ?string, +usingRestoreFlow: boolean, }; class ProfileScreen extends React.PureComponent<Props> { get loggedOutOrLoggingOut(): boolean { return ( !this.props.currentUserInfo || this.props.currentUserInfo.anonymous || this.props.logOutLoading ); } render(): React.Node { let developerTools, defaultNotifications, keyserverSelection, - tunnelbrokerMenu; + tunnelbrokerMenu, + debugLogs; const { staffCanSee } = this.props; if (staffCanSee) { developerTools = ( <ProfileRow content="Developer tools" onPress={this.onPressDevTools} /> ); defaultNotifications = ( <ProfileRow content="Default Notifications" onPress={this.onPressDefaultNotifications} /> ); keyserverSelection = ( <ProfileRow content="Keyservers" onPress={this.onPressKeyserverSelection} /> ); tunnelbrokerMenu = ( <ProfileRow content="Tunnelbroker menu" onPress={this.onPressTunnelbrokerMenu} /> ); + + debugLogs = ( + <ProfileRow content="Debug logs" onPress={this.onPressDebugLogs} /> + ); } let backupMenu; if (staffCanSee) { backupMenu = ( <ProfileRow content="Backup menu" onPress={this.onPressBackupMenu} /> ); } let passwordEditionUI; if ( accountHasPassword(this.props.currentUserInfo) && this.props.isPrimaryDevice ) { passwordEditionUI = ( <Action.Row> <Text style={this.props.styles.label}>Password</Text> <Text style={[this.props.styles.content, this.props.styles.value]} numberOfLines={1} > •••••••••••••••• </Text> <EditSettingButton onPress={this.onPressEditPassword} canChangeSettings={true} style={this.props.styles.editPasswordButton} /> </Action.Row> ); } let linkedDevices; if (this.props.usingRestoreFlow) { linkedDevices = ( <ProfileRow content="Linked devices" onPress={this.onPressDevices} /> ); } let farcasterAccountSettings; if (usingCommServicesAccessToken || __DEV__) { farcasterAccountSettings = ( <ProfileRow content="Farcaster account" onPress={this.onPressFaracsterAccount} /> ); } let dmActions; if (staffCanSee) { dmActions = ( <> <ProfileRow content="Create a new local DM thread" onPress={this.onPressCreateThread} /> </> ); } return ( <View style={this.props.styles.container}> <ScrollView contentContainerStyle={this.props.styles.scrollViewContentContainer} style={this.props.styles.scrollView} > <Text style={this.props.styles.header}>USER AVATAR</Text> <View style={[this.props.styles.section, this.props.styles.avatarSection]} > <EditUserAvatar userID={this.props.currentUserInfo?.id} fid={this.props.currentUserFID} /> </View> <Text style={this.props.styles.header}>ACCOUNT</Text> <View style={this.props.styles.section}> <Action.Row> <Text style={this.props.styles.loggedInLabel}>Logged in as </Text> <SingleLine style={[this.props.styles.label, this.props.styles.username]} > {this.props.stringForUser} </SingleLine> <Button onPress={this.onPressLogOut} disabled={this.loggedOutOrLoggingOut} > <Text style={this.props.styles.logOutText}>Log out</Text> </Button> </Action.Row> {passwordEditionUI} </View> <View style={this.props.styles.section}> <ProfileRow content="Friend list" onPress={this.onPressFriendList} /> <ProfileRow content="Block list" onPress={this.onPressBlockList} /> </View> <Text style={this.props.styles.header}>PREFERENCES</Text> <View style={this.props.styles.section}> <ProfileRow content="Appearance" onPress={this.onPressAppearance} /> <ProfileRow content="Privacy" onPress={this.onPressPrivacy} /> {defaultNotifications} {backupMenu} {tunnelbrokerMenu} </View> <View style={this.props.styles.section}> {farcasterAccountSettings} {linkedDevices} {keyserverSelection} <ProfileRow content="Build info" onPress={this.onPressBuildInfo} /> {developerTools} {dmActions} + {debugLogs} </View> <View style={this.props.styles.unpaddedSection}> <ProfileRow content="Delete account..." danger onPress={this.onPressDeleteAccount} /> </View> </ScrollView> </View> ); } onPressLogOut = () => { if (this.loggedOutOrLoggingOut) { return; } if (this.props.usingRestoreFlow) { this.onPressNewLogout(); return; } if (!this.props.isAccountWithPassword) { Alert.alert( 'Log out', 'Are you sure you want to log out?', [ { text: 'No', style: 'cancel' }, { text: 'Yes', onPress: this.logOutWithoutDeletingNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); return; } const alertTitle = Platform.OS === 'ios' ? 'Keep Login Info in Keychain' : 'Keep Login Info'; const alertDescription = 'We will automatically fill out log-in forms with your credentials ' + 'in the app.'; Alert.alert( alertTitle, alertDescription, [ { text: 'Cancel', style: 'cancel' }, { text: 'Keep', onPress: this.logOutWithoutDeletingNativeCredentialsWrapper, }, { text: 'Remove', onPress: this.logOutAndDeleteNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); }; onPressNewLogout = () => { void (async () => { if (this.loggedOutOrLoggingOut) { return; } let alertTitle, alertMessage, onPressAction; if (this.props.isPrimaryDevice) { alertTitle = 'Log out all devices?'; alertMessage = 'This device is your primary device, ' + 'so logging out will cause all of your other devices to log out too.'; onPressAction = this.logOutPrimaryDevice; } else { alertTitle = 'Log out?'; alertMessage = 'Are you sure you want to log out of this device?'; onPressAction = this.logOutSecondaryDevice; } Alert.alert( alertTitle, alertMessage, [ { text: 'No', style: 'cancel' }, { text: 'Yes', onPress: onPressAction, style: 'destructive', }, ], { cancelable: true }, ); })(); }; logOutWithoutDeletingNativeCredentialsWrapper = () => { if (this.loggedOutOrLoggingOut) { return; } this.logOut(); }; logOutAndDeleteNativeCredentialsWrapper = async () => { if (this.loggedOutOrLoggingOut) { return; } await this.deleteNativeCredentials(); this.logOut(); }; logOut() { void this.props.dispatchActionPromise( logOutActionTypes, this.props.logOut(), ); } logOutPrimaryDevice = async () => { if (this.loggedOutOrLoggingOut) { return; } void this.props.dispatchActionPromise( logOutActionTypes, this.props.logOutPrimaryDevice(), ); }; logOutSecondaryDevice = async () => { if (this.loggedOutOrLoggingOut) { return; } void this.props.dispatchActionPromise( logOutActionTypes, this.props.logOutSecondaryDevice(), ); }; async deleteNativeCredentials() { await deleteNativeCredentialsFor(); } onPressEditPassword = () => { this.props.navigation.navigate({ name: EditPasswordRouteName }); }; onPressDeleteAccount = () => { this.props.navigation.navigate({ name: DeleteAccountRouteName }); }; onPressFaracsterAccount = () => { this.props.navigation.navigate({ name: FarcasterAccountSettingsRouteName }); }; onPressDevices = () => { this.props.navigation.navigate({ name: LinkedDevicesRouteName }); }; onPressBuildInfo = () => { this.props.navigation.navigate({ name: BuildInfoRouteName }); }; onPressDevTools = () => { this.props.navigation.navigate({ name: DevToolsRouteName }); }; onPressAppearance = () => { this.props.navigation.navigate({ name: AppearancePreferencesRouteName }); }; onPressPrivacy = () => { this.props.navigation.navigate({ name: PrivacyPreferencesRouteName }); }; onPressDefaultNotifications = () => { this.props.navigation.navigate({ name: DefaultNotificationsPreferencesRouteName, }); }; onPressFriendList = () => { this.props.navigation.navigate({ name: FriendListRouteName }); }; onPressBlockList = () => { this.props.navigation.navigate({ name: BlockListRouteName }); }; onPressBackupMenu = () => { this.props.navigation.navigate({ name: BackupMenuRouteName }); }; onPressTunnelbrokerMenu = () => { this.props.navigation.navigate({ name: TunnelbrokerMenuRouteName }); }; onPressKeyserverSelection = () => { this.props.navigation.navigate({ name: KeyserverSelectionListRouteName }); }; onPressCreateThread = () => { void this.props.onCreateDMThread(); }; + + onPressDebugLogs = () => { + this.props.navigation.navigate({ name: DebugLogsScreenRouteName }); + }; } const logOutLoadingStatusSelector = createLoadingStatusSelector(logOutActionTypes); const ConnectedProfileScreen: React.ComponentType<BaseProps> = React.memo<BaseProps>(function ConnectedProfileScreen(props: BaseProps) { const currentUserInfo = useSelector(state => state.currentUserInfo); const logOutLoading = useSelector(logOutLoadingStatusSelector) === 'loading'; const colors = useColors(); const styles = useStyles(unboundStyles); const callPrimaryDeviceLogOut = usePrimaryDeviceLogOut(); const callSecondaryDeviceLogOut = useSecondaryDeviceLogOut(); const dispatchActionPromise = useDispatchActionPromise(); const staffCanSee = useStaffCanSee(); const stringForUser = useStringForUser(currentUserInfo); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const currentUserID = useCurrentUserFID(); const [isPrimaryDevice, setIsPrimaryDevice] = React.useState(false); const checkIfPrimaryDevice = useCheckIfPrimaryDevice(); const showVersionUnsupportedAlert = useShowVersionUnsupportedAlert(false); const legacyLogOutOptions = React.useMemo( () => ({ logOutType: 'legacy', handleUseNewFlowResponse: showVersionUnsupportedAlert, }), [showVersionUnsupportedAlert], ); const callLegayLogOut = useBaseLogOut(legacyLogOutOptions); const userID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const processAndSendDMOperation = useProcessAndSendDMOperation(); const onCreateDMThread = React.useCallback(async () => { invariant(userID, 'userID should be set'); const op: DMCreateThreadOperation = { type: 'create_thread', threadID: uuid.v4(), creatorID: userID, time: Date.now(), threadType: thickThreadTypes.LOCAL, memberIDs: [], roleID: uuid.v4(), newMessageID: uuid.v4(), }; const specification: OutboundDMOperationSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op, recipients: { type: 'self_devices', }, }; await processAndSendDMOperation(specification); }, [processAndSendDMOperation, userID]); React.useEffect(() => { void (async () => { const checkIfPrimaryDeviceResult = await checkIfPrimaryDevice(); setIsPrimaryDevice(checkIfPrimaryDeviceResult); })(); }, [checkIfPrimaryDevice]); const usingRestoreFlow = useIsRestoreFlowEnabled(); return ( <ProfileScreen {...props} currentUserInfo={currentUserInfo} isPrimaryDevice={isPrimaryDevice} logOutLoading={logOutLoading} colors={colors} styles={styles} logOut={callLegayLogOut} logOutPrimaryDevice={callPrimaryDeviceLogOut} logOutSecondaryDevice={callSecondaryDeviceLogOut} dispatchActionPromise={dispatchActionPromise} staffCanSee={staffCanSee} stringForUser={stringForUser} isAccountWithPassword={isAccountWithPassword} onCreateDMThread={onCreateDMThread} currentUserFID={currentUserID} usingRestoreFlow={usingRestoreFlow} /> ); }); export default ConnectedProfileScreen; diff --git a/native/profile/profile.react.js b/native/profile/profile.react.js index ace059ec5..0dce268d7 100644 --- a/native/profile/profile.react.js +++ b/native/profile/profile.react.js @@ -1,241 +1,249 @@ // @flow import type { StackNavigationProp, StackNavigationHelpers, StackHeaderProps, StackHeaderLeftButtonProps, } from '@react-navigation/core'; import { createStackNavigator } from '@react-navigation/stack'; import * as React from 'react'; import { View, useWindowDimensions } from 'react-native'; import AddKeyserver from './add-keyserver.react.js'; 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'; import EditPassword from './edit-password.react.js'; import EmojiUserAvatarCreation from './emoji-user-avatar-creation.react.js'; import FarcasterAccountSettings from './farcaster-account-settings.react.js'; import KeyserverSelectionListHeaderRightButton from './keyserver-selection-list-header-right-button.react.js'; import KeyserverSelectionList from './keyserver-selection-list.react.js'; import LinkedDevicesHeaderRightButton from './linked-devices-header-right-button.react.js'; import LinkedDevices from './linked-devices.react.js'; import PrivacyPreferences from './privacy-preferences.react.js'; import ProfileHeader from './profile-header.react.js'; import ProfileScreen from './profile-screen.react.js'; import RelationshipList from './relationship-list.react.js'; import TunnelbrokerMenu from './tunnelbroker-menu.react.js'; import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react.js'; import CommunityDrawerButton from '../navigation/community-drawer-button.react.js'; import HeaderBackButton from '../navigation/header-back-button.react.js'; import { ProfileScreenRouteName, EditPasswordRouteName, DeleteAccountRouteName, EmojiUserAvatarCreationRouteName, BuildInfoRouteName, DevToolsRouteName, AppearancePreferencesRouteName, PrivacyPreferencesRouteName, FriendListRouteName, DefaultNotificationsPreferencesRouteName, BlockListRouteName, LinkedDevicesRouteName, BackupMenuRouteName, KeyserverSelectionListRouteName, AddKeyserverRouteName, FarcasterAccountSettingsRouteName, 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'; const header = (props: StackHeaderProps) => <ProfileHeader {...props} />; const profileScreenOptions = { headerTitle: 'Profile' }; const emojiAvatarCreationOptions = { headerTitle: 'Emoji avatar selection', headerBackTitleVisible: false, }; const editPasswordOptions = { headerTitle: 'Change password' }; const deleteAccountOptions = { headerTitle: 'Delete account' }; const linkedDevicesOptions = { headerTitle: 'Linked devices', headerRight: () => <LinkedDevicesHeaderRightButton />, }; const keyserverSelectionListOptions = { headerTitle: 'Keyservers', headerRight: () => <KeyserverSelectionListHeaderRightButton />, }; const addKeyserverOptions = { headerTitle: 'Add keyserver' }; const backupMenuOptions = { headerTitle: 'Backup menu' }; const tunnelbrokerMenuOptions = { headerTitle: 'Tunnelbroker menu' }; const buildInfoOptions = { headerTitle: 'Build info' }; const devToolsOptions = { headerTitle: 'Developer tools' }; const appearanceOptions = { headerTitle: 'Appearance' }; const privacyOptions = { headerTitle: 'Privacy' }; const friendListOptions = { headerTitle: 'Friend list' }; const blockListOptions = { headerTitle: 'Block list' }; const defaultNotificationsOptions = { headerTitle: 'Default Notifications' }; const farcasterSettingsOptions = { headerTitle: 'Farcaster account' }; +const debugLogsScreenOptions = { headerTitle: 'Logs' }; export type ProfileNavigationProp< RouteName: $Keys<ProfileParamList> = $Keys<ProfileParamList>, > = StackNavigationProp<ScreenParamList, RouteName>; const Profile = createStackNavigator< ScreenParamList, ProfileParamList, StackNavigationHelpers<ScreenParamList>, >(); type Props = { +navigation: TabNavigationProp<'Profile'>, ... }; function ProfileComponent(props: Props): React.Node { const styles = useStyles(unboundStyles); const colors = useColors(); const headerLeftButton = React.useCallback( (headerProps: StackHeaderLeftButtonProps) => headerProps.canGoBack ? ( <HeaderBackButton {...headerProps} /> ) : ( <CommunityDrawerButton navigation={props.navigation} /> ), [props.navigation], ); const { width: screenWidth } = useWindowDimensions(); const screenOptions = React.useMemo( () => ({ header, headerLeft: headerLeftButton, headerStyle: { backgroundColor: colors.tabBarBackground, shadowOpacity: 0, }, gestureEnabled: true, gestureResponseDistance: screenWidth, }), [colors.tabBarBackground, headerLeftButton, screenWidth], ); return ( <View style={styles.view}> <KeyboardAvoidingView behavior="padding" style={styles.keyboardAvoidingView} > <Profile.Navigator screenOptions={screenOptions} detachInactiveScreens={false} > <Profile.Screen name={ProfileScreenRouteName} component={ProfileScreen} options={profileScreenOptions} /> <Profile.Screen name={EmojiUserAvatarCreationRouteName} component={EmojiUserAvatarCreation} options={emojiAvatarCreationOptions} /> <Profile.Screen name={EditPasswordRouteName} component={EditPassword} options={editPasswordOptions} /> <Profile.Screen name={DeleteAccountRouteName} component={DeleteAccount} options={deleteAccountOptions} /> <Profile.Screen name={LinkedDevicesRouteName} component={LinkedDevices} options={linkedDevicesOptions} /> <Profile.Screen name={KeyserverSelectionListRouteName} component={KeyserverSelectionList} options={keyserverSelectionListOptions} /> <Profile.Screen name={AddKeyserverRouteName} component={AddKeyserver} options={addKeyserverOptions} /> <Profile.Screen name={BackupMenuRouteName} component={BackupMenu} options={backupMenuOptions} /> <Profile.Screen name={TunnelbrokerMenuRouteName} component={TunnelbrokerMenu} options={tunnelbrokerMenuOptions} /> <Profile.Screen name={BuildInfoRouteName} component={BuildInfo} options={buildInfoOptions} /> <Profile.Screen name={DevToolsRouteName} component={DevTools} options={devToolsOptions} /> <Profile.Screen name={AppearancePreferencesRouteName} component={AppearancePreferences} options={appearanceOptions} /> <Profile.Screen name={PrivacyPreferencesRouteName} component={PrivacyPreferences} options={privacyOptions} /> <Profile.Screen name={DefaultNotificationsPreferencesRouteName} component={DefaultNotificationsPreferences} options={defaultNotificationsOptions} /> <Profile.Screen name={FriendListRouteName} component={RelationshipList} options={friendListOptions} /> <Profile.Screen name={BlockListRouteName} component={RelationshipList} options={blockListOptions} /> <Profile.Screen name={FarcasterAccountSettingsRouteName} component={FarcasterAccountSettings} options={farcasterSettingsOptions} /> + <Profile.Screen + name={DebugLogsScreenRouteName} + component={DebugLogsScreen} + options={debugLogsScreenOptions} + /> </Profile.Navigator> </KeyboardAvoidingView> </View> ); } const unboundStyles = { keyboardAvoidingView: { flex: 1, }, view: { flex: 1, backgroundColor: 'panelBackground', }, }; export default ProfileComponent; diff --git a/native/root.react.js b/native/root.react.js index 51644d25b..55f89715a 100644 --- a/native/root.react.js +++ b/native/root.react.js @@ -1,447 +1,450 @@ // @flow import { ActionSheetProvider } from '@expo/react-native-action-sheet'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import AsyncStorage from '@react-native-async-storage/async-storage'; import type { PossiblyStaleNavigationState, UnsafeContainerActionEvent, GenericNavigationAction, } from '@react-navigation/core'; import { useReduxDevToolsExtension } from '@react-navigation/devtools'; import { NavigationContainer } from '@react-navigation/native'; import * as SplashScreen from 'expo-splash-screen'; import invariant from 'invariant'; import * as React from 'react'; import { StyleSheet } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import { SafeAreaProvider, initialWindowMetrics, } from 'react-native-safe-area-context'; import { Provider } from 'react-redux'; 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'; import { FarcasterDataHandler } from 'lib/components/farcaster-data-handler.react.js'; import { GlobalSearchIndexProvider } from 'lib/components/global-search-index-provider.react.js'; import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import { MediaCacheProvider } from 'lib/components/media-cache-provider.react.js'; import { NeynarClientProvider } from 'lib/components/neynar-client-provider.react.js'; import PlatformDetailsSynchronizer from 'lib/components/platform-details-synchronizer.react.js'; import PrekeysHandler from 'lib/components/prekeys-handler.react.js'; import { SecondaryDeviceQRAuthContextProvider } from 'lib/components/secondary-device-qr-auth-context-provider.react.js'; import { StaffContextProvider } from 'lib/components/staff-provider.react.js'; import SyncCommunityStoreHandler from 'lib/components/sync-community-store-handler.react.js'; import { UserIdentityCacheProvider } from 'lib/components/user-identity-cache.react.js'; import { DBOpsHandler } from 'lib/handlers/db-ops-handler.react.js'; import { HoldersHandler } from 'lib/handlers/holders-handler.react.js'; import { InitialStateSharingHandler } from 'lib/handlers/initial-state-sharing-handler.react.js'; import { TunnelbrokerDeviceTokenHandler } from 'lib/handlers/tunnelbroker-device-token-handler.react.js'; import { UserInfosHandler } from 'lib/handlers/user-infos-handler.react.js'; import { IdentitySearchProvider } from 'lib/identity-search/identity-search-context.js'; import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import KeyserverConnectionsHandler from 'lib/keyserver-conn/keyserver-connections-handler.js'; import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js'; import { actionLogger } from 'lib/utils/action-logger.js'; import { RegistrationContextProvider } from './account/registration/registration-context-provider.react.js'; import NativeEditThreadAvatarProvider from './avatars/native-edit-thread-avatar-provider.react.js'; import BackupHandler from './backup/backup-handler.js'; import { BottomSheetProvider } from './bottom-sheet/bottom-sheet-provider.react.js'; import ChatContextProvider from './chat/chat-context-provider.react.js'; import MessageEditingContextProvider from './chat/message-editing-context-provider.react.js'; import AccessTokenHandler from './components/access-token-handler.react.js'; import { AutoJoinCommunityHandler } from './components/auto-join-community-handler.react.js'; import BackgroundIdentityLoginHandler from './components/background-identity-login-handler.react.js'; import ConnectFarcasterAlertHandler from './components/connect-farcaster-alert-handler.react.js'; import DMActivityHandler from './components/dm-activity-handler.react.js'; import { FeatureFlagsProvider } from './components/feature-flags-provider.react.js'; import { NUXTipsContextProvider } from './components/nux-tips-context.react.js'; import PersistedStateGate from './components/persisted-state-gate.js'; import ReportHandler from './components/report-handler.react.js'; import VersionSupportedChecker from './components/version-supported.react.js'; import ConnectedStatusBar from './connected-status-bar.react.js'; import { SQLiteDataHandler } from './data/sqlite-data-handler.js'; import ErrorBoundary from './error-boundary.react.js'; import IdentityServiceContextProvider from './identity-service/identity-service-context-provider.react.js'; import InputStateContainer from './input/input-state-container.react.js'; import LifecycleHandler from './lifecycle/lifecycle-handler.react.js'; import MarkdownContextProvider from './markdown/markdown-context-provider.react.js'; import { filesystemMediaCache } from './media/media-cache.js'; import { DeepLinksContextProvider } from './navigation/deep-links-context-provider.react.js'; import { defaultNavigationState } from './navigation/default-state.js'; import { setGlobalNavContext } from './navigation/icky-global.js'; import KeyserverReachabilityHandler from './navigation/keyserver-reachability-handler.js'; import { NavContext, type NavContextType, } from './navigation/navigation-context.js'; import NavigationHandler from './navigation/navigation-handler.react.js'; import { validNavState, isShowingNUXTips, } from './navigation/navigation-utils.js'; import OrientationHandler from './navigation/orientation-handler.react.js'; import { navStateAsyncStorageKey } from './navigation/persistance.js'; import RootNavigator from './navigation/root-navigator.react.js'; import ConnectivityUpdater from './redux/connectivity-updater.react.js'; import { DimensionsUpdater } from './redux/dimensions-updater.react.js'; import { getPersistor } from './redux/persist.js'; import { store } from './redux/redux-setup.js'; import { useSelector } from './redux/redux-utils.js'; import { RootContext } from './root-context.js'; import { MessageSearchProvider } from './search/search-provider.react.js'; import Socket from './socket.react.js'; import { useLoadCommFonts } from './themes/fonts.js'; import { DarkTheme, LightTheme } from './themes/navigation.js'; import ThemeHandler from './themes/theme-handler.react.js'; import { alchemyKey, ethersProvider } from './utils/ethers-utils.js'; import { neynarKey } from './utils/neynar-utils.js'; import { composeTunnelbrokerQRAuthMessage, handleSecondaryDeviceLogInError, parseTunnelbrokerQRAuthMessage, performBackupRestore, generateQRAuthAESKey, } from './utils/qr-code-utils.js'; // Add custom items to expo-dev-menu import './dev-menu.js'; import './types/message-types-validator.js'; const navInitAction = Object.freeze({ type: 'NAV/@@INIT' }); const navUnknownAction = Object.freeze({ type: 'NAV/@@UNKNOWN' }); SplashScreen.preventAutoHideAsync().catch(console.log); function Root() { const navStateRef = React.useRef<?PossiblyStaleNavigationState>(); const navDispatchRef = React.useRef<?( action: | GenericNavigationAction | (PossiblyStaleNavigationState => GenericNavigationAction), ) => void>(); const navStateInitializedRef = React.useRef(false); // We call this here to start the loading process // We gate the UI on the fonts loading in AppNavigator useLoadCommFonts(); const [navContext, setNavContext] = React.useState<?NavContextType>(null); const updateNavContext = React.useCallback(() => { if ( !navStateRef.current || !navDispatchRef.current || !navStateInitializedRef.current ) { return; } const updatedNavContext = { state: navStateRef.current, dispatch: navDispatchRef.current, }; setNavContext(updatedNavContext); setGlobalNavContext(updatedNavContext); }, []); const [initialState, setInitialState] = React.useState( __DEV__ ? undefined : defaultNavigationState, ); React.useEffect(() => { Orientation.lockToPortrait(); void (async () => { let loadedState = initialState; if (__DEV__) { try { const navStateString = await AsyncStorage.getItem( navStateAsyncStorageKey, ); if (navStateString) { const savedState = JSON.parse(navStateString); if (validNavState(savedState) && !isShowingNUXTips(savedState)) { loadedState = savedState; } } } catch {} } if (!loadedState) { loadedState = defaultNavigationState; } if (loadedState !== initialState) { setInitialState(loadedState); } navStateRef.current = loadedState; updateNavContext(); actionLogger.addOtherAction('navState', navInitAction, null, loadedState); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [updateNavContext]); const setNavStateInitialized = React.useCallback(() => { navStateInitializedRef.current = true; updateNavContext(); }, [updateNavContext]); const [rootContext, setRootContext] = React.useState(() => ({ setNavStateInitialized, })); const detectUnsupervisedBackgroundRef = React.useCallback( (detectUnsupervisedBackground: ?(alreadyClosed: boolean) => boolean) => { setRootContext(prevRootContext => ({ ...prevRootContext, detectUnsupervisedBackground, })); }, [], ); const frozen = useSelector(state => state.frozen); const queuedActionsRef = React.useRef<Array<GenericNavigationAction>>([]); const onNavigationStateChange = React.useCallback( (state: ?PossiblyStaleNavigationState) => { invariant(state, 'nav state should be non-null'); const prevState = navStateRef.current; navStateRef.current = state; updateNavContext(); const queuedActions = queuedActionsRef.current; queuedActionsRef.current = []; if (queuedActions.length === 0) { queuedActions.push(navUnknownAction); } for (const action of queuedActions) { actionLogger.addOtherAction('navState', action, prevState, state); } if (!__DEV__ || frozen) { return; } void (async () => { try { await AsyncStorage.setItem( navStateAsyncStorageKey, JSON.stringify(state), ); } catch (e) { console.log('AsyncStorage threw while trying to persist navState', e); } })(); }, [updateNavContext, frozen], ); const navContainerRef = React.useRef<?React.ElementRef<typeof NavigationContainer>>(); const containerRef = React.useCallback( (navContainer: ?React.ElementRef<typeof NavigationContainer>) => { navContainerRef.current = navContainer; if (navContainer && !navDispatchRef.current) { navDispatchRef.current = navContainer.dispatch; updateNavContext(); } }, [updateNavContext], ); useReduxDevToolsExtension(navContainerRef); const navContainer = navContainerRef.current; React.useEffect(() => { if (!navContainer) { return undefined; } return navContainer.addListener( '__unsafe_action__', (event: { +data: UnsafeContainerActionEvent, ... }) => { const { action, noop } = event.data; const navState = navStateRef.current; if (noop) { actionLogger.addOtherAction('navState', action, navState, navState); return; } queuedActionsRef.current.push({ ...action, type: `NAV/${action.type}`, }); }, ); }, [navContainer]); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const theme = (() => { if (activeTheme === 'light') { return LightTheme; } else if (activeTheme === 'dark') { return DarkTheme; } return undefined; })(); const gated: React.Node = ( <> <LifecycleHandler /> <KeyserverReachabilityHandler /> <DimensionsUpdater /> <ConnectivityUpdater /> <ThemeHandler /> <OrientationHandler /> <BackupHandler /> <IntegrityHandler /> <AccessTokenHandler /> <DBOpsHandler /> <UserInfosHandler /> <TunnelbrokerDeviceTokenHandler /> <HoldersHandler /> </> ); let navigation; if (initialState) { navigation = ( <NavigationContainer initialState={initialState} onStateChange={onNavigationStateChange} theme={theme} ref={containerRef} > <BottomSheetModalProvider> <ChatContextProvider> <DeepLinksContextProvider> <ChatMentionContextProvider> <GlobalSearchIndexProvider> <NUXTipsContextProvider> <RootNavigator /> </NUXTipsContextProvider> </GlobalSearchIndexProvider> </ChatMentionContextProvider> </DeepLinksContextProvider> </ChatContextProvider> <NavigationHandler /> <PersistedStateGate> <FarcasterDataHandler> <ConnectFarcasterAlertHandler /> </FarcasterDataHandler> </PersistedStateGate> </BottomSheetModalProvider> </NavigationContainer> ); } return ( - <GestureHandlerRootView style={styles.app}> - <StaffContextProvider> - <IdentityServiceContextProvider> - <UserIdentityCacheProvider> - <ENSCacheProvider - ethersProvider={ethersProvider} - alchemyKey={alchemyKey} - > - <NeynarClientProvider apiKey={neynarKey}> - <TunnelbrokerProvider> - <IdentitySearchProvider> - <SecondaryDeviceQRAuthContextProvider - parseTunnelbrokerQRAuthMessage={ - parseTunnelbrokerQRAuthMessage - } - composeTunnelbrokerQRAuthMessage={ - composeTunnelbrokerQRAuthMessage - } - generateAESKey={generateQRAuthAESKey} - performBackupRestore={performBackupRestore} - onLogInError={handleSecondaryDeviceLogInError} - > - <FeatureFlagsProvider> - <NavContext.Provider value={navContext}> - <RootContext.Provider value={rootContext}> - <InputStateContainer> - <MessageEditingContextProvider> - <SafeAreaProvider - initialMetrics={initialWindowMetrics} - > - <ActionSheetProvider> - <MediaCacheProvider - persistence={filesystemMediaCache} - > - <EditUserAvatarProvider> - <NativeEditThreadAvatarProvider> - <MarkdownContextProvider> - <MessageSearchProvider> - <BottomSheetProvider> - <RegistrationContextProvider> - <SQLiteDataHandler /> - <ConnectedStatusBar /> - <ReduxPersistGate - persistor={getPersistor()} - > - {gated} - </ReduxPersistGate> - <PersistedStateGate> - <KeyserverConnectionsHandler - socketComponent={Socket} - detectUnsupervisedBackgroundRef={ - detectUnsupervisedBackgroundRef - } - /> - <DMActivityHandler /> - <VersionSupportedChecker /> - <PlatformDetailsSynchronizer /> - <BackgroundIdentityLoginHandler /> - <PrekeysHandler /> - <ReportHandler /> - <FarcasterChannelPrefetchHandler /> - <AutoJoinCommunityHandler /> - <SyncCommunityStoreHandler /> - <InitialStateSharingHandler /> - </PersistedStateGate> - {navigation} - </RegistrationContextProvider> - </BottomSheetProvider> - </MessageSearchProvider> - </MarkdownContextProvider> - </NativeEditThreadAvatarProvider> - </EditUserAvatarProvider> - </MediaCacheProvider> - </ActionSheetProvider> - </SafeAreaProvider> - </MessageEditingContextProvider> - </InputStateContainer> - </RootContext.Provider> - </NavContext.Provider> - </FeatureFlagsProvider> - </SecondaryDeviceQRAuthContextProvider> - </IdentitySearchProvider> - </TunnelbrokerProvider> - </NeynarClientProvider> - </ENSCacheProvider> - </UserIdentityCacheProvider> - </IdentityServiceContextProvider> - </StaffContextProvider> - </GestureHandlerRootView> + <DebugLogsContextProvider> + <GestureHandlerRootView style={styles.app}> + <StaffContextProvider> + <IdentityServiceContextProvider> + <UserIdentityCacheProvider> + <ENSCacheProvider + ethersProvider={ethersProvider} + alchemyKey={alchemyKey} + > + <NeynarClientProvider apiKey={neynarKey}> + <TunnelbrokerProvider> + <IdentitySearchProvider> + <SecondaryDeviceQRAuthContextProvider + parseTunnelbrokerQRAuthMessage={ + parseTunnelbrokerQRAuthMessage + } + composeTunnelbrokerQRAuthMessage={ + composeTunnelbrokerQRAuthMessage + } + generateAESKey={generateQRAuthAESKey} + performBackupRestore={performBackupRestore} + onLogInError={handleSecondaryDeviceLogInError} + > + <FeatureFlagsProvider> + <NavContext.Provider value={navContext}> + <RootContext.Provider value={rootContext}> + <InputStateContainer> + <MessageEditingContextProvider> + <SafeAreaProvider + initialMetrics={initialWindowMetrics} + > + <ActionSheetProvider> + <MediaCacheProvider + persistence={filesystemMediaCache} + > + <EditUserAvatarProvider> + <NativeEditThreadAvatarProvider> + <MarkdownContextProvider> + <MessageSearchProvider> + <BottomSheetProvider> + <RegistrationContextProvider> + <SQLiteDataHandler /> + <ConnectedStatusBar /> + <ReduxPersistGate + persistor={getPersistor()} + > + {gated} + </ReduxPersistGate> + <PersistedStateGate> + <KeyserverConnectionsHandler + socketComponent={Socket} + detectUnsupervisedBackgroundRef={ + detectUnsupervisedBackgroundRef + } + /> + <DMActivityHandler /> + <VersionSupportedChecker /> + <PlatformDetailsSynchronizer /> + <BackgroundIdentityLoginHandler /> + <PrekeysHandler /> + <ReportHandler /> + <FarcasterChannelPrefetchHandler /> + <AutoJoinCommunityHandler /> + <SyncCommunityStoreHandler /> + <InitialStateSharingHandler /> + </PersistedStateGate> + {navigation} + </RegistrationContextProvider> + </BottomSheetProvider> + </MessageSearchProvider> + </MarkdownContextProvider> + </NativeEditThreadAvatarProvider> + </EditUserAvatarProvider> + </MediaCacheProvider> + </ActionSheetProvider> + </SafeAreaProvider> + </MessageEditingContextProvider> + </InputStateContainer> + </RootContext.Provider> + </NavContext.Provider> + </FeatureFlagsProvider> + </SecondaryDeviceQRAuthContextProvider> + </IdentitySearchProvider> + </TunnelbrokerProvider> + </NeynarClientProvider> + </ENSCacheProvider> + </UserIdentityCacheProvider> + </IdentityServiceContextProvider> + </StaffContextProvider> + </GestureHandlerRootView> + </DebugLogsContextProvider> ); } const styles = StyleSheet.create({ app: { flex: 1, }, }); function AppRoot(): React.Node { return ( <Provider store={store}> <CallKeyserverEndpointProvider> <ErrorBoundary> <Root /> </ErrorBoundary> </CallKeyserverEndpointProvider> </Provider> ); } export default AppRoot; diff --git a/web/root.js b/web/root.js index 0fbd30f65..3d2dad0f9 100644 --- a/web/root.js +++ b/web/root.js @@ -1,88 +1,91 @@ // @flow import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import localforage from 'localforage'; import * as React from 'react'; import { Provider } from 'react-redux'; import { Router, Route } from 'react-router'; import { createStore, applyMiddleware, type Store } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction.js'; import { persistReducer, persistStore } from 'redux-persist'; import thunk from 'redux-thunk'; import { WagmiProvider } from 'wagmi'; +import { DebugLogsContextProvider } from 'lib/components/debug-logs-context-provider.react.js'; import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import PrekeysHandler from 'lib/components/prekeys-handler.react.js'; import ReportHandler from 'lib/components/report-handler.react.js'; import { UserIdentityCacheProvider } from 'lib/components/user-identity-cache.react.js'; import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { getWagmiConfig } from 'lib/utils/wagmi-utils.js'; import App from './app.react.js'; import ErrorBoundary from './error-boundary.react.js'; import IdentityServiceContextProvider from './grpc/identity-service-context-provider.react.js'; import { defaultWebState } from './redux/default-state.js'; import InitialReduxStateGate from './redux/initial-state-gate.js'; import { persistConfig } from './redux/persist.js'; import { type AppState, type Action, reducer } from './redux/redux-setup.js'; import { synchronizeStoreWithOtherTabs, tabSynchronizationMiddleware, } from './redux/tab-synchronization.js'; import history from './router-history.js'; import { SQLiteDataHandler } from './shared-worker/sqlite-data-handler.js'; import { localforageConfig } from './shared-worker/utils/constants.js'; localforage.config(localforageConfig); const persistedReducer = persistReducer(persistConfig, reducer); const store: Store<AppState, Action> = createStore( persistedReducer, defaultWebState, composeWithDevTools({ maxAge: 200, })( applyMiddleware(thunk, reduxLoggerMiddleware, tabSynchronizationMiddleware), ), ); synchronizeStoreWithOtherTabs(store); const persistor = persistStore(store); const queryClient = new QueryClient(); const wagmiConfig = getWagmiConfig([ 'injected', 'rainbow', 'metamask', 'coinbase', 'walletconnect', ]); const RootProvider = (): React.Node => ( <Provider store={store}> <ErrorBoundary> - <WagmiProvider config={wagmiConfig}> - <QueryClientProvider client={queryClient}> - <CallKeyserverEndpointProvider> - <InitialReduxStateGate persistor={persistor}> - <IdentityServiceContextProvider> - <UserIdentityCacheProvider> - <Router history={history.getHistoryObject()}> - <Route path="*" component={App} /> - </Router> - <PrekeysHandler /> - <SQLiteDataHandler /> - <IntegrityHandler /> - <ReportHandler canSendReports={true} /> - </UserIdentityCacheProvider> - </IdentityServiceContextProvider> - </InitialReduxStateGate> - </CallKeyserverEndpointProvider> - </QueryClientProvider> - </WagmiProvider> + <DebugLogsContextProvider> + <WagmiProvider config={wagmiConfig}> + <QueryClientProvider client={queryClient}> + <CallKeyserverEndpointProvider> + <InitialReduxStateGate persistor={persistor}> + <IdentityServiceContextProvider> + <UserIdentityCacheProvider> + <Router history={history.getHistoryObject()}> + <Route path="*" component={App} /> + </Router> + <PrekeysHandler /> + <SQLiteDataHandler /> + <IntegrityHandler /> + <ReportHandler canSendReports={true} /> + </UserIdentityCacheProvider> + </IdentityServiceContextProvider> + </InitialReduxStateGate> + </CallKeyserverEndpointProvider> + </QueryClientProvider> + </WagmiProvider> + </DebugLogsContextProvider> </ErrorBoundary> </Provider> ); export default RootProvider; diff --git a/web/settings/account-settings.react.js b/web/settings/account-settings.react.js index 4528bd9f9..265688925 100644 --- a/web/settings/account-settings.react.js +++ b/web/settings/account-settings.react.js @@ -1,337 +1,363 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import uuid from 'uuid'; import { useBaseLogOut, logOutActionTypes, useSecondaryDeviceLogOut, } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/swmansion-icon.react.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { useCheckIfPrimaryDevice } from 'lib/hooks/primary-device-hooks.js'; import { dmOperationSpecificationTypes, type OutboundDMOperationSpecification, } from 'lib/shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from 'lib/shared/dm-ops/process-dm-ops.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { DMCreateThreadOperation } from 'lib/types/dm-ops.js'; import { thickThreadTypes } from 'lib/types/thread-types-enum.js'; import { createOlmSessionsWithOwnDevices, getContentSigningKey, } from 'lib/utils/crypto-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useIsRestoreFlowEnabled } from 'lib/utils/services-utils.js'; import css from './account-settings.css'; import AppearanceChangeModal from './appearance-change-modal.react.js'; import BackupTestRestoreModal from './backup-test-restore-modal.react.js'; +import DebugLogsModal from './debug-logs-modal.react.js'; import BlockListModal from './relationship/block-list-modal.react.js'; import FriendListModal from './relationship/friend-list-modal.react.js'; import TunnelbrokerMessagesScreen from './tunnelbroker-message-list.react.js'; import TunnelbrokerTestScreen from './tunnelbroker-test.react.js'; import EditUserAvatar from '../avatars/edit-user-avatar.react.js'; import Button from '../components/button.react.js'; import VersionUnsupportedModal from '../modals/version-unsupported-modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; function AccountSettings(): React.Node { const { pushModal, popModal } = useModalContext(); const legacyLogOutOptions = React.useMemo(() => { const showVersionUnsupportedModal = () => { pushModal(<VersionUnsupportedModal />); }; return { logOutType: 'legacy', handleUseNewFlowResponse: showVersionUnsupportedModal, }; }, [pushModal]); const sendLegacyLogoutRequest = useBaseLogOut(legacyLogOutOptions); const sendSecondaryDeviceLogoutRequest = useSecondaryDeviceLogOut(); const dispatchActionPromise = useDispatchActionPromise(); const checkIfPrimaryDevice = useCheckIfPrimaryDevice(); const usingRestoreFlow = useIsRestoreFlowEnabled(); const logOutUser = React.useCallback(async () => { // if web is primary device, we're on legacy flow const isPrimaryDevice = await checkIfPrimaryDevice(); if (usingRestoreFlow && !isPrimaryDevice) { return dispatchActionPromise( logOutActionTypes, sendSecondaryDeviceLogoutRequest(), ); } return dispatchActionPromise(logOutActionTypes, sendLegacyLogoutRequest()); }, [ checkIfPrimaryDevice, dispatchActionPromise, sendLegacyLogoutRequest, sendSecondaryDeviceLogoutRequest, usingRestoreFlow, ]); const identityContext = React.useContext(IdentityClientContext); const userID = useSelector(state => state.currentUserInfo?.id); const [deviceID, setDeviceID] = React.useState<?string>(); React.useEffect(() => { void (async () => { const contentSigningKey = await getContentSigningKey(); setDeviceID(contentSigningKey); })(); }, []); const openFriendList = React.useCallback( () => pushModal(<FriendListModal />), [pushModal], ); const openBlockList = React.useCallback( () => pushModal(<BlockListModal />), [pushModal], ); const currentUserInfo = useSelector(state => state.currentUserInfo); const stringForUser = useStringForUser(currentUserInfo); const staffCanSee = useStaffCanSee(); const { sendMessageToDevice, socketState, addListener, removeListener } = useTunnelbroker(); const openTunnelbrokerModal = React.useCallback( () => pushModal( <TunnelbrokerTestScreen sendMessageToDevice={sendMessageToDevice} onClose={popModal} />, ), [popModal, pushModal, sendMessageToDevice], ); const openTunnelbrokerMessagesModal = React.useCallback( () => pushModal( <TunnelbrokerMessagesScreen addListener={addListener} removeListener={removeListener} onClose={popModal} />, ), [addListener, popModal, pushModal, removeListener], ); const onCreateOlmSessions = React.useCallback(async () => { if (!identityContext) { return; } const authMetadata = await identityContext.getAuthMetadata(); try { await createOlmSessionsWithOwnDevices( authMetadata, identityContext.identityClient, sendMessageToDevice, ); } catch (e) { console.log(`Error creating olm sessions with own devices: ${e.message}`); } }, [identityContext, sendMessageToDevice]); const openBackupTestRestoreModal = React.useCallback( () => pushModal(<BackupTestRestoreModal onClose={popModal} />), [popModal, pushModal], ); const processAndSendDMOperation = useProcessAndSendDMOperation(); const onCreateDMThread = React.useCallback(async () => { invariant(userID, 'userID should be set'); const op: DMCreateThreadOperation = { type: 'create_thread', threadID: uuid.v4(), creatorID: userID, time: Date.now(), threadType: thickThreadTypes.LOCAL, memberIDs: [], roleID: uuid.v4(), newMessageID: uuid.v4(), }; const specification: OutboundDMOperationSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op, recipients: { type: 'self_devices', }, }; await processAndSendDMOperation(specification); }, [processAndSendDMOperation, userID]); const showAppearanceModal = React.useCallback( () => pushModal(<AppearanceChangeModal />), [pushModal], ); + const openDebugLogs = React.useCallback( + () => pushModal(<DebugLogsModal />), + [pushModal], + ); + if (!currentUserInfo || currentUserInfo.anonymous) { return null; } let preferences; if (staffCanSee) { preferences = ( <div className={css.preferencesContainer}> <h4 className={css.preferencesHeader}>Preferences</h4> <div className={css.content}> <ul> <li> <span>Appearance</span> <a className={css.editPasswordLink} onClick={showAppearanceModal}> <SWMansionIcon icon="edit-1" size={22} /> </a> </li> </ul> </div> </div> ); } let tunnelbroker; if (staffCanSee) { tunnelbroker = ( <div className={css.preferencesContainer}> <h4 className={css.preferencesHeader}>Tunnelbroker menu</h4> <div className={css.content}> <ul> <li> <span>Connected</span> <span>{socketState.connected.toString()}</span> </li> <li> <span>Send message to device</span> <Button variant="text" onClick={openTunnelbrokerModal}> <p className={css.buttonText}>Insert data</p> </Button> </li> <li> <span>Trace received messages</span> <Button variant="text" onClick={openTunnelbrokerMessagesModal}> <p className={css.buttonText}>Show list</p> </Button> </li> <li> <span>Create session with own devices</span> <Button variant="text" onClick={onCreateOlmSessions}> <p className={css.buttonText}>Create</p> </Button> </li> </ul> </div> </div> ); } let backup; if (staffCanSee) { backup = ( <div className={css.preferencesContainer}> <h4 className={css.preferencesHeader}>Backup menu</h4> <div className={css.content}> <ul> <li> <span>Test backup restore</span> <Button variant="text" onClick={openBackupTestRestoreModal}> <p className={css.buttonText}>Insert data</p> </Button> </li> </ul> </div> </div> ); } let deviceData; if (staffCanSee) { deviceData = ( <div className={css.preferencesContainer}> <h4 className={css.preferencesHeader}>Device ID</h4> <div className={css.content}> <ul> <li> <span>{deviceID}</span> </li> </ul> </div> <h4 className={css.preferencesHeader}>User ID</h4> <div className={css.content}> <ul> <li> <span>{userID}</span> </li> </ul> </div> </div> ); } let dms; if (staffCanSee) { dms = ( <div className={css.preferencesContainer}> <h4 className={css.preferencesHeader}>DMs menu</h4> <div className={css.content}> <ul> <li> <span>Create a new local DM thread</span> <Button variant="text" onClick={onCreateDMThread}> <p className={css.buttonText}>Create</p> </Button> </li> </ul> </div> </div> ); } + let debugLogs; + if (staffCanSee) { + debugLogs = ( + <div className={css.preferencesContainer}> + <h4 className={css.preferencesHeader}>Debug menu</h4> + <div className={css.content}> + <ul> + <li> + <span>See debug logs</span> + <Button variant="text" onClick={openDebugLogs}> + <p className={css.buttonText}>See List</p> + </Button> + </li> + </ul> + </div> + </div> + ); + } + return ( <div className={css.container}> <div className={css.contentContainer}> <h4 className={css.header}>My Account</h4> <EditUserAvatar userID={currentUserInfo.id} /> <div className={css.content}> <ul> <li> <p className={css.logoutContainer}> <span className={css.logoutLabel}>{'Logged in as '}</span> <span className={css.username}>{stringForUser}</span> </p> <Button variant="text" onClick={logOutUser}> <p className={css.buttonText}>Log out</p> </Button> </li> <li> <span>Friend List</span> <Button variant="text" onClick={openFriendList}> <p className={css.buttonText}>See List</p> </Button> </li> <li> <span>Block List</span> <Button variant="text" onClick={openBlockList}> <p className={css.buttonText}>See List</p> </Button> </li> </ul> </div> {preferences} {tunnelbroker} {backup} {deviceData} {dms} + {debugLogs} </div> </div> ); } export default AccountSettings; diff --git a/web/settings/debug-logs-modal.css b/web/settings/debug-logs-modal.css new file mode 100644 index 000000000..0f180f7f0 --- /dev/null +++ b/web/settings/debug-logs-modal.css @@ -0,0 +1,47 @@ +.container { + display: flex; + flex-direction: column; + height: 60vh; + gap: 8px; +} + +.logsList { + display: flex; + flex-direction: column; + flex: 1; + overflow-y: scroll; +} + +.item { + margin: 4px 0; + padding: 4px 0; + color: var(--text-background-secondary-default); + transition: 0.1s; +} + +.item:hover { + background-color: var(--modal-listItem-primary-hover); +} + +.timestamp { + font-size: var(--m-font-16); +} + +.title { + font-size: var(--l-font-18); + color: var(--text-background-primary-default); +} + +.message { + font-size: var(--m-font-16); +} + +.buttons { + display: flex; + gap: 8px; + justify-content: space-between; +} + +.button { + flex: 1; +} diff --git a/web/settings/debug-logs-modal.react.js b/web/settings/debug-logs-modal.react.js new file mode 100644 index 000000000..eb66086c9 --- /dev/null +++ b/web/settings/debug-logs-modal.react.js @@ -0,0 +1,58 @@ +// @flow + +import * as React from 'react'; + +import { useDebugLogs } from 'lib/components/debug-logs-context.js'; +import { useModalContext } from 'lib/components/modal-provider.react.js'; + +import css from './debug-logs-modal.css'; +import Button, { buttonThemes } from '../components/button.react.js'; +import Modal from '../modals/modal.react.js'; + +function DebugLogsModal(): React.Node { + const { logs, clearLogs } = useDebugLogs(); + const { popModal } = useModalContext(); + + const messageList = React.useMemo( + () => + logs.map((item, index) => { + const date = new Date(item.timestamp); + const timestampString = date.toISOString(); + return ( + <div key={index} className={css.item}> + <div className={css.timestamp}>{timestampString}</div> + <div className={css.title}>{item.title}</div> + <div className={css.message}>{item.message}</div> + </div> + ); + }), + [logs], + ); + + const copyLogs = React.useCallback(async () => { + await navigator.clipboard.writeText(JSON.stringify(logs)); + }, [logs]); + + return ( + <Modal name="Debug Logs" onClose={popModal} size="large"> + <div className={css.container}> + <div className={css.logsList}>{messageList}</div> + <div className={css.buttons}> + <Button variant="filled" className={css.button} onClick={copyLogs}> + Copy Logs + </Button> + <Button + variant="filled" + buttonColor={buttonThemes.danger} + className={css.button} + onClick={clearLogs} + > + Clear Logs + </Button> + </div> + </div> + </Modal> + ); +} + +export default DebugLogsModal;