diff --git a/packages/web/components/patterns/HighlightView.tsx b/packages/web/components/patterns/HighlightView.tsx index bd8650781..31b73acef 100644 --- a/packages/web/components/patterns/HighlightView.tsx +++ b/packages/web/components/patterns/HighlightView.tsx @@ -29,6 +29,7 @@ export function HighlightView(props: HighlightViewProps): JSX.Element { return ( + {props.highlight.highlightPositionPercent}% { if (props.scrollToHighlight) { diff --git a/packages/web/components/templates/article/PdfArticleContainer.tsx b/packages/web/components/templates/article/PdfArticleContainer.tsx index e2bffea2a..c82617253 100644 --- a/packages/web/components/templates/article/PdfArticleContainer.tsx +++ b/packages/web/components/templates/article/PdfArticleContainer.tsx @@ -2,7 +2,13 @@ import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticle import { Box } from '../../elements/LayoutPrimitives' import { v4 as uuidv4 } from 'uuid' import { nanoid } from 'nanoid' -import { useState, useEffect, useCallback, useRef } from 'react' +import { + useState, + useEffect, + useCallback, + useRef, + ReactComponentElement, +} from 'react' import { isDarkTheme } from '../../../lib/themeUpdater' import PSPDFKit from 'pspdfkit' import { Instance, HighlightAnnotation, List, Annotation, Rect } from 'pspdfkit' @@ -17,6 +23,7 @@ import { webBaseURL } from '../../../lib/appConfig' import { pspdfKitKey } from '../../../lib/appConfig' import { NotebookModal } from './NotebookModal' import { HighlightNoteModal } from './HighlightNoteModal' +import { showErrorToast } from '../../../lib/toastHelpers' export type PdfArticleContainerProps = { viewerUsername: string @@ -32,11 +39,11 @@ export default function PdfArticleContainer( const [shareTarget, setShareTarget] = useState( undefined ) + const [notebookKey, setNotebookKey] = useState(uuidv4()) const [noteTarget, setNoteTarget] = useState(undefined) const [noteTargetPageIndex, setNoteTargetPageIndex] = useState< number | undefined >(undefined) - const highlightsRef = useRef([]) const canShareNative = useCanShareNative() @@ -84,6 +91,18 @@ export default function PdfArticleContainer( (i) => ALLOWED_TOOLBAR_ITEM_TYPES.indexOf(i.type) !== -1 ) + const positionPercentForAnnotation = (annotation: Annotation) => { + let totalSize = 0 + let sizeBefore = 0 + for (let idx = 0; idx < annotation.pageIndex; idx++) { + sizeBefore += instance.pageInfoForIndex(idx)?.height ?? 0 + } + for (let idx = 0; idx < instance.totalPageCount; idx++) { + totalSize += instance.pageInfoForIndex(idx)?.height ?? 0 + } + return (sizeBefore + annotation.boundingBox.top) / totalSize + } + const annotationTooltipCallback = (annotation: Annotation) => { const highlightAnnotation = annotation as HighlightAnnotation const copy = { @@ -105,17 +124,22 @@ export default function PdfArticleContainer( id: 'tooltip-remove-annotation', className: 'TooltipItem-Remove', onPress: () => { - instance.delete(annotation).then(() => { - if ( - annotation.customData && - annotation.customData.omnivoreHighlight && - (annotation.customData.omnivoreHighlight as Highlight).id - ) { - const data = annotation.customData - .omnivoreHighlight as Highlight - deleteHighlightMutation(data.id) - } - }) + instance + .delete(annotation) + .then(() => { + if ( + annotation.customData && + annotation.customData.omnivoreHighlight && + (annotation.customData.omnivoreHighlight as Highlight).id + ) { + const data = annotation.customData + .omnivoreHighlight as Highlight + return deleteHighlightMutation(data.id) + } + }) + .catch((err) => { + showErrorToast('Error deleting highlight: ' + err) + }) }, } const note = { @@ -185,7 +209,7 @@ export default function PdfArticleContainer( }), }) - instance.addEventListener('annotations.willChange', (event) => { + instance.addEventListener('annotations.willChange', async (event) => { const annotation = event.annotations.get(0) if (event.reason !== PSPDFKit.AnnotationsWillChangeReason.DELETE_END) { return @@ -197,7 +221,7 @@ export default function PdfArticleContainer( (annotation.customData.omnivoreHighlight as Highlight).id ) { const data = annotation.customData.omnivoreHighlight as Highlight - deleteHighlightMutation(data.id) + await deleteHighlightMutation(data.id) } }) @@ -302,6 +326,7 @@ export default function PdfArticleContainer( PSPDFKit.Annotations.toSerializableObject(annotation) if (overlapping.size === 0) { + const positionPercent = positionPercentForAnnotation(annotation) const result = await createHighlightMutation({ id: id, shortId: shortId, @@ -310,6 +335,8 @@ export default function PdfArticleContainer( prefix: surroundingText.prefix, suffix: surroundingText.suffix, patch: JSON.stringify(serialized), + highlightPositionPercent: positionPercent * 100, + highlightPositionAnchorIndex: annotation.pageIndex, }) if (result) { highlightsRef.current.push(result) @@ -344,6 +371,7 @@ export default function PdfArticleContainer( const mergedIds = overlapping.map( (ha) => (ha.customData?.omnivoreHighlight as Highlight).id ) + const positionPercent = positionPercentForAnnotation(annotation) const result = await mergeHighlightMutation({ quote, id, @@ -353,6 +381,8 @@ export default function PdfArticleContainer( suffix: surroundingText.suffix, articleId: props.article.id, overlapHighlightIdList: mergedIds.toArray(), + highlightPositionPercent: positionPercent * 100, + highlightPositionAnchorIndex: annotation.pageIndex, }) if (result) { highlightsRef.current.push(result) @@ -380,6 +410,35 @@ export default function PdfArticleContainer( ) })() + document.addEventListener('deleteHighlightbyId', async (event) => { + const annotationId = (event as CustomEvent).detail as string + for (let pageIdx = 0; pageIdx < instance.totalPageCount; pageIdx++) { + const annotations = await instance.getAnnotations(pageIdx) + for (let annIdx = 0; annIdx < annotations.size; annIdx++) { + const annotation = annotations.get(annIdx) + if ( + annotation && + annotation.customData && + annotation.customData.omnivoreHighlight && + (annotation.customData.omnivoreHighlight as Highlight).id == + annotationId + ) { + await instance.delete(annotation) + await deleteHighlightMutation(annotationId) + + const highlightIdx = highlightsRef.current.findIndex((value) => { + return value.id == annotationId + }) + if (highlightIdx > -1) { + highlightsRef.current.splice(highlightIdx, 1) + } + // This is needed to force the notebook to reload the highlights + setNotebookKey(uuidv4()) + } + } + } + }) + return () => { PSPDFKit && container && PSPDFKit.unload(container) } @@ -426,10 +485,17 @@ export default function PdfArticleContainer( )} {props.showHighlightsModal && ( props.setShowHighlightsModal(false)} /* eslint-disable @typescript-eslint/no-empty-function */ updateHighlight={() => {}} + deleteHighlightAction={(highlightId: string) => { + const event = new CustomEvent('deleteHighlightbyId', { + detail: highlightId, + }) + document.dispatchEvent(event) + }} /> )} diff --git a/packages/web/lib/networking/mutations/createHighlightMutation.ts b/packages/web/lib/networking/mutations/createHighlightMutation.ts index c0f9290e3..acfc18206 100644 --- a/packages/web/lib/networking/mutations/createHighlightMutation.ts +++ b/packages/web/lib/networking/mutations/createHighlightMutation.ts @@ -10,6 +10,8 @@ export type CreateHighlightInput = { shortId: string patch: string articleId: string + highlightPositionPercent?: number + highlightPositionAnchorIndex?: number } type CreateHighlightOutput = { diff --git a/packages/web/lib/networking/mutations/mergeHighlightMutation.ts b/packages/web/lib/networking/mutations/mergeHighlightMutation.ts index 89d1f0151..fe63a6fc1 100644 --- a/packages/web/lib/networking/mutations/mergeHighlightMutation.ts +++ b/packages/web/lib/networking/mutations/mergeHighlightMutation.ts @@ -12,6 +12,8 @@ export type MergeHighlightInput = { suffix?: string annotation?: string overlapHighlightIdList: string[] + highlightPositionPercent?: number + highlightPositionAnchorIndex?: number } export type MergeHighlightOutput = {