diff --git a/src/components/error-display/error-display.module.css b/src/components/error-display/error-display.module.css new file mode 100644 index 00000000..88e55b8b --- /dev/null +++ b/src/components/error-display/error-display.module.css @@ -0,0 +1,11 @@ +.errorMessage { + color: red; +} + +.clickableErrorMessage { + cursor: pointer; +} + +.feedbackSuccessMessage { + color: inherit; +} diff --git a/src/components/error-display/error-display.tsx b/src/components/error-display/error-display.tsx new file mode 100644 index 00000000..f1ec63c7 --- /dev/null +++ b/src/components/error-display/error-display.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { copyToClipboard } from '../../lib/utils/clipboard-utils'; +import styles from './error-display.module.css'; + +const ErrorDisplay = ({ error }: { error: any }) => { + const { t } = useTranslation(); + const [feedbackMessageKey, setFeedbackMessageKey] = useState(null); + + const originalDisplayMessage = error?.message ? `${t('error')}: ${error.message}` : null; + + const handleMessageClick = async () => { + if (!error || !error.message || feedbackMessageKey) return; + + const errorString = JSON.stringify(error, null, 2); + try { + await copyToClipboard(errorString); + setFeedbackMessageKey('copied'); + setTimeout(() => { + setFeedbackMessageKey(null); + }, 1500); + } catch (err) { + console.error('Failed to copy error: ', err); + setFeedbackMessageKey('failed'); + setTimeout(() => { + setFeedbackMessageKey(null); + }, 1500); + } + }; + + let currentDisplayMessage = ''; + const classNames = [styles.errorMessage]; + let isClickable = false; + + if (feedbackMessageKey === 'copied') { + currentDisplayMessage = t('fullErrorCopiedToClipboard', 'full error copied to the clipboard'); + classNames.pop(); + classNames.push(styles.feedbackSuccessMessage); + } else if (feedbackMessageKey === 'failed') { + currentDisplayMessage = t('copyFailed', 'copy failed'); + } else if (originalDisplayMessage) { + currentDisplayMessage = originalDisplayMessage; + isClickable = true; + classNames.push(styles.clickableErrorMessage); + } + + return ( + (error?.message || error?.stack || error?.details || error) && ( +
+ {currentDisplayMessage && ( + + {currentDisplayMessage} + + )} +
+ ) + ); +}; + +export default ErrorDisplay; diff --git a/src/components/error-display/index.ts b/src/components/error-display/index.ts new file mode 100644 index 00000000..ea4eb1f3 --- /dev/null +++ b/src/components/error-display/index.ts @@ -0,0 +1 @@ +export { default } from './error-display'; diff --git a/src/lib/utils/clipboard-utils.ts b/src/lib/utils/clipboard-utils.ts new file mode 100644 index 00000000..94a46dda --- /dev/null +++ b/src/lib/utils/clipboard-utils.ts @@ -0,0 +1,30 @@ +/** + * Universal clipboard utility that works in both Electron and web environments + */ +export const copyToClipboard = async (text: string): Promise => { + // Check if we're in Electron and use its clipboard API + if (typeof window !== 'undefined' && (window as any).electronApi?.copyToClipboard) { + try { + const result = await (window as any).electronApi.copyToClipboard(text); + if (!result.success) { + throw new Error(result.error || 'Failed to copy to clipboard'); + } + return; + } catch (error) { + console.error('Electron clipboard failed:', error); + // Fall back to web clipboard API + } + } + + // Fallback to web clipboard API + if (navigator.clipboard) { + try { + await navigator.clipboard.writeText(text); + } catch (error) { + console.error('Web clipboard failed:', error); + throw new Error('Failed to copy to clipboard. Your browser may not support this feature.'); + } + } else { + throw new Error('Your browser does not support clipboard API'); + } +}; diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx index fbdc2252..c76bf5ec 100644 --- a/src/views/board/board.tsx +++ b/src/views/board/board.tsx @@ -13,6 +13,7 @@ import useTimeFilter from '../../hooks/use-time-filter'; import useInterfaceSettingsStore from '../../stores/use-interface-settings-store'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import useSortingStore from '../../stores/use-sorting-store'; +import ErrorDisplay from '../../components/error-display/error-display'; import LoadingEllipsis from '../../components/loading-ellipsis'; import SubplebbitDescription from '../../components/subplebbit-description'; import SubplebbitRules from '../../components/subplebbit-rules'; @@ -227,12 +228,6 @@ const Board = () => { ) : ( hasMore && )} - {error && ( -
-
- {error.message} -
- )} {blocked && ( <>   [ @@ -274,6 +269,16 @@ const Board = () => { document.title = boardTitle + ' - plebchan'; }, [title, shortAddress, subplebbitAddress]); + // probably not necessary to show the error to the user if the feed loaded successfully + const [shouldShowErrorToUser, setShouldShowErrorToUser] = useState(false); + useEffect(() => { + if (error?.message && feed.length === 0) { + setShouldShowErrorToUser(true); + } else if (feed.length > 0) { + setShouldShowErrorToUser(false); + } + }, [error, feed]); + return ( <> {shouldShowSnow() &&
} @@ -289,6 +294,11 @@ const Board = () => { title={title} /> )} + {shouldShowErrorToUser && ( +
+ +
+ )} {rules && !description && rules.length > 0 && } { document.title = isInAllView ? `${t('all')} - plebchan` : postDucumentTitle; }, [title, shortAddress, subplebbitAddress, post?.title, post?.content, isInAllView, t]); + // probably not necessary to show the error to the user if the post loaded successfully + const [shouldShowErrorToUser, setShouldShowErrorToUser] = useState(false); + useEffect(() => { + if (post?.error && ((post?.replyCount > 0 && post?.replies?.length === 0) || (post?.state === 'failed' && post?.error))) { + setShouldShowErrorToUser(true); + } else if (post?.replyCount > 0 && post?.replies?.length > 0) { + setShouldShowErrorToUser(false); + } + }, [post]); + return (
{/* TODO: remove this replyCount error once api supports scrolling replies pages */} {replyCount > 60 && Error: this thread has too many replies, some of them cannot be displayed right now.} - {error && Error: {error?.message || error?.toString?.()}} + {shouldShowErrorToUser && ( +
+ +
+ )} {isInDescriptionView ? (