diff --git a/shared/app/global-errors.tsx b/shared/app/global-errors.tsx new file mode 100644 index 000000000000..23e6106654b6 --- /dev/null +++ b/shared/app/global-errors.tsx @@ -0,0 +1,332 @@ +import * as C from '@/constants' +import * as Kb from '@/common-adapters' +import * as React from 'react' +import logger from '@/logger' +import {ignoreDisconnectOverlay} from '@/local-debug' +import {useConfigState} from '@/stores/config' +import type {RPCError} from '@/util/errors' +import {settingsFeedbackTab} from '@/constants/settings' +import {useDaemonState} from '@/stores/daemon' + +type Size = 'Closed' | 'Small' | 'Big' + +const summaryForError = (err?: Error | RPCError) => err?.message ?? '' +const detailsForError = (err?: Error | RPCError) => err?.stack ?? '' + +const maxHeightForSize = (size: Size) => { + return { + Big: 900, + Closed: 0, + Small: 35, + }[size] +} + +const useData = () => { + const loggedIn = useConfigState(s => s.loggedIn) + const daemonError = useDaemonState(s => s.error) + const error = useConfigState(s => s.globalError) + const setGlobalError = useConfigState(s => s.dispatch.setGlobalError) + const clearModals = C.useRouterState(s => s.dispatch.clearModals) + const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) + const onFeedback = React.useCallback(() => { + setGlobalError() + if (loggedIn) { + clearModals() + navigateAppend(settingsFeedbackTab) + } else { + navigateAppend('feedback') + } + }, [navigateAppend, clearModals, loggedIn, setGlobalError]) + const onDismiss = React.useCallback(() => { + setGlobalError() + }, [setGlobalError]) + + const [cachedSummary, setSummary] = React.useState(summaryForError(error)) + const [cachedDetails, setDetails] = React.useState(detailsForError(error)) + const [size, setSize] = React.useState('Closed') + const countdownTimerRef = React.useRef>(undefined) + + const clearCountdown = React.useCallback(() => { + countdownTimerRef.current && clearTimeout(countdownTimerRef.current) + countdownTimerRef.current = undefined + }, [countdownTimerRef]) + + const onExpandClick = React.useCallback(() => { + setSize('Big') + if (!C.isMobile) { + clearCountdown() + } + }, [clearCountdown]) + + const resetError = React.useCallback( + (newError: boolean) => { + setSize(newError ? 'Small' : 'Closed') + if (!C.isMobile) { + clearCountdown() + if (newError) { + countdownTimerRef.current = setTimeout(() => { + onDismiss() + }, 10000) + } + } + }, + [clearCountdown, onDismiss] + ) + + C.useOnUnMountOnce(() => { + clearCountdown() + }) + + C.useOnMountOnce(() => { + resetError(!!error) + }) + + React.useEffect(() => { + const id = setTimeout( + () => { + setDetails(detailsForError(error)) + if (!C.isMobile) { + setSummary(summaryForError(error)) + } + }, + error ? 0 : 7000 + ) // if it's set, do it immediately, if it's cleared set it in a bit + resetError(!!error) + return () => { + clearTimeout(id) + } + }, [error, resetError]) + + return { + cachedDetails, + cachedSummary, + daemonError, + error, + onDismiss, + onExpandClick, + onFeedback, + size, + } +} + +const GlobalError = () => { + const d = useData() + const {daemonError, error, onDismiss, onFeedback} = d + const {cachedDetails, cachedSummary, size, onExpandClick} = d + + if (size === 'Closed') { + return null + } + + if (!daemonError && !error) { + return null + } + + if (daemonError) { + if (C.isMobile) { + return null + } + if (ignoreDisconnectOverlay) { + logger.warn('Ignoring disconnect overlay') + return null + } + + const message = daemonError.message || 'Keybase is currently unreachable. Trying to reconnect you…' + return ( + + + + {message} + + + + + + + ) + } + + if (C.isMobile) { + return ( + + + + + + {size !== 'Big' && ( + + )} + {' '} + An error occurred. + + + + + + + + {size === 'Big' && ( + + + {error?.message} + {'\n\n'} + {cachedDetails} + + + )} + + ) + } + + const summary = cachedSummary + const details = cachedDetails + + let stylesContainer: Kb.Styles.StylesCrossPlatform + switch (size) { + case 'Big': + stylesContainer = styles.containerBig + break + case 'Small': + stylesContainer = styles.containerSmall + break + } + + return ( + + + + {summary} + + + {summary && ( + + )} + + + + {details} + + + + ) +} + +const styles = Kb.Styles.styleSheetCreate(() => { + const containerBase = { + left: 0, + overflow: 'hidden' as const, + position: 'absolute' as const, + right: 0, + top: 40, + zIndex: 1000, + ...Kb.Styles.transition('max-height'), + } + + return { + containerBig: Kb.Styles.platformStyles({ + isElectron: {...containerBase, maxHeight: maxHeightForSize('Big')}, + }), + containerOverlay: { + ...Kb.Styles.globalStyles.fillAbsolute, + zIndex: 1000, + }, + containerSmall: Kb.Styles.platformStyles({ + isElectron: {...containerBase, maxHeight: maxHeightForSize('Small')}, + }), + details: { + backgroundColor: Kb.Styles.globalColors.black, + color: Kb.Styles.globalColors.white_75, + ...Kb.Styles.padding(8, Kb.Styles.globalMargins.xlarge), + }, + innerContainer: { + ...Kb.Styles.globalStyles.flexBoxCenter, + backgroundColor: Kb.Styles.globalColors.black, + flex: 1, + gap: Kb.Styles.globalMargins.small, + minHeight: maxHeightForSize('Small'), + ...Kb.Styles.padding(Kb.Styles.globalMargins.xtiny, Kb.Styles.globalMargins.small), + }, + message: { + color: Kb.Styles.globalColors.white, + }, + mobileContainer: { + backgroundColor: Kb.Styles.globalColors.black, + position: 'absolute', + top: 0, + }, + mobileDetails: { + color: Kb.Styles.globalColors.white_75, + fontSize: 14, + lineHeight: 19, + ...Kb.Styles.padding(Kb.Styles.globalMargins.tiny, Kb.Styles.globalMargins.xtiny, Kb.Styles.globalMargins.xtiny), + }, + mobileErrorText: { + color: Kb.Styles.globalColors.white, + flex: 1, + }, + mobileErrorTextContainer: { + paddingBottom: Kb.Styles.globalMargins.xtiny, + position: 'relative', + }, + mobileSafeAreaView: { + backgroundColor: Kb.Styles.globalColors.transparent, + flexGrow: 0, + }, + mobileSummaryRow: { + alignItems: 'center', + flexShrink: 0, + justifyContent: 'center', + ...Kb.Styles.padding(Kb.Styles.globalMargins.tiny, Kb.Styles.globalMargins.xsmall), + }, + overlayFill: { + ...Kb.Styles.globalStyles.flexBoxCenter, + backgroundColor: Kb.Styles.globalColors.white, + flex: 1, + }, + overlayRow: { + ...Kb.Styles.globalStyles.flexBoxCenter, + backgroundColor: Kb.Styles.globalColors.blue, + padding: 8, + }, + summary: { + color: Kb.Styles.globalColors.white, + flex: 1, + }, + } as const +}) + +export default GlobalError diff --git a/shared/app/global-errors/hook.tsx b/shared/app/global-errors/hook.tsx deleted file mode 100644 index df6a580e4787..000000000000 --- a/shared/app/global-errors/hook.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import * as C from '@/constants' -import * as React from 'react' -import {useConfigState} from '@/stores/config' -import type {RPCError} from '@/util/errors' -import {settingsFeedbackTab} from '@/constants/settings' -import {useDaemonState} from '@/stores/daemon' - -export type Size = 'Closed' | 'Small' | 'Big' - -const summaryForError = (err?: Error | RPCError) => err?.message ?? '' -const detailsForError = (err?: Error | RPCError) => err?.stack ?? '' - -const useData = () => { - const loggedIn = useConfigState(s => s.loggedIn) - const daemonError = useDaemonState(s => s.error) - const error = useConfigState(s => s.globalError) - const setGlobalError = useConfigState(s => s.dispatch.setGlobalError) - const clearModals = C.useRouterState(s => s.dispatch.clearModals) - const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) - const onFeedback = React.useCallback(() => { - setGlobalError() - if (loggedIn) { - clearModals() - navigateAppend(settingsFeedbackTab) - } else { - navigateAppend('feedback') - } - }, [navigateAppend, clearModals, loggedIn, setGlobalError]) - const copyToClipboard = useConfigState(s => s.dispatch.defer.copyToClipboard) - const onDismiss = React.useCallback(() => { - setGlobalError() - }, [setGlobalError]) - - const [cachedSummary, setSummary] = React.useState(summaryForError(error)) - const [cachedDetails, setDetails] = React.useState(detailsForError(error)) - const [size, setSize] = React.useState('Closed') - const countdownTimerRef = React.useRef>(undefined) - - const clearCountdown = React.useCallback(() => { - countdownTimerRef.current && clearTimeout(countdownTimerRef.current) - countdownTimerRef.current = undefined - }, [countdownTimerRef]) - - const onExpandClick = React.useCallback(() => { - setSize('Big') - if (!C.isMobile) { - clearCountdown() - } - }, [clearCountdown]) - - const resetError = React.useCallback( - (newError: boolean) => { - setSize(newError ? 'Small' : 'Closed') - if (!C.isMobile) { - clearCountdown() - if (newError) { - countdownTimerRef.current = setTimeout(() => { - onDismiss() - }, 10000) - } - } - }, - [clearCountdown, onDismiss] - ) - - C.useOnUnMountOnce(() => { - clearCountdown() - }) - - C.useOnMountOnce(() => { - resetError(!!error) - }) - - React.useEffect(() => { - const id = setTimeout( - () => { - setDetails(detailsForError(error)) - if (!C.isMobile) { - setSummary(summaryForError(error)) - } - }, - error ? 0 : 7000 - ) // if it's set, do it immediately, if it's cleared set it in a bit - resetError(!!error) - return () => { - clearTimeout(id) - } - }, [error, resetError]) - - return { - cachedDetails, - cachedSummary, - copyToClipboard, - daemonError, - error, - onDismiss, - onExpandClick, - onFeedback, - size, - } -} - -export default useData diff --git a/shared/app/global-errors/index.d.ts b/shared/app/global-errors/index.d.ts deleted file mode 100644 index 695dd42f1e55..000000000000 --- a/shared/app/global-errors/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type * as React from 'react' -declare const GlobalError: () => React.ReactNode -export default GlobalError diff --git a/shared/app/global-errors/index.desktop.tsx b/shared/app/global-errors/index.desktop.tsx deleted file mode 100644 index 815396255845..000000000000 --- a/shared/app/global-errors/index.desktop.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import logger from '@/logger' -import * as Kb from '@/common-adapters' -import {ignoreDisconnectOverlay} from '@/local-debug.desktop' -import useData, {type Size} from './hook' - -const maxHeightForSize = (size: Size) => { - return { - Big: 900, - Closed: 0, - Small: 35, - }[size] -} - -const GlobalError = () => { - const d = useData() - const {daemonError, error, onDismiss, onFeedback} = d - const {cachedDetails, cachedSummary, size, onExpandClick} = d - - if (size === 'Closed') { - return null - } - - if (!daemonError && !error) { - return null - } - - if (daemonError) { - if (ignoreDisconnectOverlay) { - logger.warn('Ignoring disconnect overlay') - return null - } - - const message = daemonError.message || 'Keybase is currently unreachable. Trying to reconnect you…' - return ( - - - - {message} - - - - - - - ) - } else { - const summary = cachedSummary - const details = cachedDetails - - let stylesContainer: Kb.Styles.StylesCrossPlatform - switch (size) { - case 'Big': - stylesContainer = styles.containerBig - break - case 'Small': - stylesContainer = styles.containerSmall - break - } - - return ( - - - - {summary} - - - {summary && ( - - )} - - - - {details} - - - - ) - } -} - -const styles = Kb.Styles.styleSheetCreate(() => { - const containerBase = { - ...Kb.Styles.globalStyles.flexBoxColumn, - left: 0, - overflow: 'hidden', - position: 'absolute', - right: 0, - top: 40, - zIndex: 1000, - ...Kb.Styles.transition('max-height'), - } as const - - return { - closeIcon: { - position: 'absolute', - right: Kb.Styles.globalMargins.xsmall, - top: 10, - }, - containerBig: Kb.Styles.platformStyles({ - isElectron: {...containerBase, maxHeight: maxHeightForSize('Big')}, - }), - containerClosed: Kb.Styles.platformStyles({ - isElectron: {...containerBase, maxHeight: maxHeightForSize('Closed')}, - }), - containerOverlay: { - ...Kb.Styles.globalStyles.flexBoxColumn, - bottom: 0, - left: 0, - position: 'absolute', - right: 0, - top: 0, - zIndex: 1000, - }, - containerSmall: Kb.Styles.platformStyles({ - isElectron: {...containerBase, maxHeight: maxHeightForSize('Small')}, - }), - details: { - backgroundColor: Kb.Styles.globalColors.black, - color: Kb.Styles.globalColors.white_75, - padding: 8, - paddingLeft: Kb.Styles.globalMargins.xlarge, - paddingRight: Kb.Styles.globalMargins.xlarge, - }, - feedbackButton: { - marginRight: Kb.Styles.globalMargins.large, - }, - innerContainer: { - ...Kb.Styles.globalStyles.flexBoxRow, - alignItems: 'center', - backgroundColor: Kb.Styles.globalColors.black, - flex: 1, - justifyContent: 'center', - minHeight: maxHeightForSize('Small'), - padding: Kb.Styles.globalMargins.xtiny, - position: 'relative', - }, - message: { - color: Kb.Styles.globalColors.white, - }, - overlayFill: { - ...Kb.Styles.globalStyles.flexBoxColumn, - alignItems: 'center', - backgroundColor: Kb.Styles.globalColors.white, - flex: 1, - justifyContent: 'center', - }, - overlayRow: { - ...Kb.Styles.globalStyles.flexBoxRow, - alignItems: 'center', - backgroundColor: Kb.Styles.globalColors.blue, - justifyContent: 'center', - padding: 8, - }, - summary: { - color: Kb.Styles.globalColors.white, - flex: 1, - }, - summaryRow: { - ...Kb.Styles.globalStyles.flexBoxRow, - alignItems: 'center', - flex: 1, - justifyContent: 'center', - padding: Kb.Styles.globalMargins.xtiny, - position: 'relative', - }, - summaryRowError: { - backgroundColor: Kb.Styles.globalColors.black, - minHeight: maxHeightForSize('Small'), - }, - } as const -}) - -export default GlobalError diff --git a/shared/app/global-errors/index.native.tsx b/shared/app/global-errors/index.native.tsx deleted file mode 100644 index 7555c1ef588c..000000000000 --- a/shared/app/global-errors/index.native.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as Kb from '@/common-adapters' -import NativeScrollView from '@/common-adapters/scroll-view.native' -import useData from './hook' - -const GlobalError = () => { - const d = useData() - const {daemonError, error, onDismiss, onFeedback} = d - const {cachedDetails, size, onExpandClick} = d - - if (size === 'Closed') { - return null - } - - if (!daemonError && !error) { - return null - } - - return ( - - - - - - {size !== 'Big' && ( - - )} - {' '} - An error occurred. - - - - - - - - {size === 'Big' && ( - - - {error?.message} - {'\n\n'} - {cachedDetails} - - - )} - - ) -} - -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - container: { - backgroundColor: Kb.Styles.globalColors.black, - position: 'absolute', - top: 0, - }, - details: { - color: Kb.Styles.globalColors.white_75, - fontSize: 14, - lineHeight: 19, - padding: Kb.Styles.globalMargins.xtiny, - paddingTop: Kb.Styles.globalMargins.tiny, - }, - errorText: { - color: Kb.Styles.globalColors.white, - flex: 1, - }, - errorTextContainer: { - paddingBottom: Kb.Styles.globalMargins.xtiny, - position: 'relative', - }, - itemText: { - color: Kb.Styles.globalColors.white, - fontSize: 8, - lineHeight: 8, - }, - safeAreaView: { - backgroundColor: Kb.Styles.globalColors.transparent, - flexGrow: 0, - }, - summaryRow: { - ...Kb.Styles.globalStyles.flexBoxRow, - alignItems: 'center', - flexShrink: 0, - justifyContent: 'center', - paddingBottom: Kb.Styles.globalMargins.tiny, - paddingLeft: Kb.Styles.globalMargins.xsmall, - paddingRight: Kb.Styles.globalMargins.xsmall, - paddingTop: Kb.Styles.globalMargins.tiny, - }, - }) as const -) - -export default GlobalError