import React, { useEffect, useMemo, useRef, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import type { DocumentData } from '@prisma/client'; import { Loader } from 'lucide-react'; import { type PDFDocumentProxy, PasswordResponses } from 'pdfjs-dist'; import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf'; import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; import 'react-pdf/dist/esm/Page/TextLayer.css'; import { match } from 'ts-pattern'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { cn } from '../lib/utils'; import { PasswordDialog } from './document-password-dialog'; import { useToast } from './use-toast'; export type LoadedPDFDocument = PDFDocumentProxy; /** * This imports the worker from the `pdfjs-dist` package. */ pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.js', import.meta.url, ).toString(); export type OnPDFViewerPageClick = (_event: { pageNumber: number; numPages: number; originalEvent: React.MouseEvent; pageHeight: number; pageWidth: number; pageX: number; pageY: number; }) => void | Promise; const PDFLoader = () => ( <>

Loading document...

); export type PDFViewerProps = { className?: string; documentData: DocumentData; password?: string | null; onPasswordSubmit?: (password: string) => void | Promise; onDocumentLoad?: (_doc: LoadedPDFDocument) => void; onPageClick?: OnPDFViewerPageClick; [key: string]: unknown; } & Omit, 'onPageClick'>; export const PDFViewer = ({ className, documentData, password: defaultPassword, onPasswordSubmit, onDocumentLoad, onPageClick, ...props }: PDFViewerProps) => { const { _ } = useLingui(); const { toast } = useToast(); const $el = useRef(null); const passwordCallbackRef = useRef<((password: string | null) => void) | null>(null); const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false); const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false); const [isPasswordError, setIsPasswordError] = useState(false); const [documentBytes, setDocumentBytes] = useState(null); const [width, setWidth] = useState(0); const [numPages, setNumPages] = useState(0); const [pdfError, setPdfError] = useState(false); const memoizedData = useMemo( () => ({ type: documentData.type, data: documentData.data }), [documentData.data, documentData.type], ); const isLoading = isDocumentBytesLoading || !documentBytes; const onDocumentLoaded = (doc: LoadedPDFDocument) => { setNumPages(doc.numPages); onDocumentLoad?.(doc); }; const onDocumentPageClick = ( event: React.MouseEvent, pageNumber: number, ) => { const $el = event.target instanceof HTMLElement ? event.target : null; if (!$el) { return; } const $page = $el.closest(PDF_VIEWER_PAGE_SELECTOR); if (!$page) { return; } const { height, width, top, left } = $page.getBoundingClientRect(); const pageX = event.clientX - left; const pageY = event.clientY - top; if (onPageClick) { void onPageClick({ pageNumber, numPages, originalEvent: event, pageHeight: height, pageWidth: width, pageX, pageY, }); } }; useEffect(() => { if ($el.current) { const $current = $el.current; const { width } = $current.getBoundingClientRect(); setWidth(width); const onResize = () => { const { width } = $current.getBoundingClientRect(); setWidth(width); }; window.addEventListener('resize', onResize); return () => { window.removeEventListener('resize', onResize); }; } }, []); useEffect(() => { const fetchDocumentBytes = async () => { try { setIsDocumentBytesLoading(true); const bytes = await getFile(memoizedData); setDocumentBytes(bytes); setIsDocumentBytesLoading(false); } catch (err) { console.error(err); toast({ title: _(msg`Error`), description: _(msg`An error occurred while loading the document.`), variant: 'destructive', }); } }; void fetchDocumentBytes(); }, [memoizedData, toast]); return (
{isLoading ? (
) : ( <> { // If the document already has a password, we don't need to ask for it again. if (defaultPassword && reason !== PasswordResponses.INCORRECT_PASSWORD) { callback(defaultPassword); return; } setIsPasswordModalOpen(true); passwordCallbackRef.current = callback; match(reason) .with(PasswordResponses.NEED_PASSWORD, () => setIsPasswordError(false)) .with(PasswordResponses.INCORRECT_PASSWORD, () => setIsPasswordError(true)); }} onLoadSuccess={(d) => onDocumentLoaded(d)} // Uploading a invalid document causes an error which doesn't appear to be handled by the `error` prop. // Therefore we add some additional custom error handling. onSourceError={() => { setPdfError(true); }} externalLinkTarget="_blank" loading={
{pdfError ? (

Something went wrong while loading the document.

Please try again or contact our support.

) : ( )}
} error={

Something went wrong while loading the document.

Please try again or contact our support.

} > {Array(numPages) .fill(null) .map((_, i) => (
''} onClick={(e) => onDocumentPageClick(e, i + 1)} />

Page {i + 1} of {numPages}

))}
{ passwordCallbackRef.current?.(password); setIsPasswordModalOpen(false); void onPasswordSubmit?.(password); }} isError={isPasswordError} /> )}
); }; export default PDFViewer;