Merge pull request #1758 from omnivore-app/fix/save-highlight-position-info

Save highlight position info on web
This commit is contained in:
Jackson Harper 2023-02-02 16:29:06 +08:00 committed by GitHub
commit b2b0d8be82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 303 additions and 175 deletions

View file

@ -269,6 +269,34 @@ import Utils
return result
}
func heightBefore(pageIndex: PageIndex) -> Double {
var totalHeight = 0.0
for idx in 0 ..< pageIndex {
if let page = document.pageInfoForPage(at: idx) {
totalHeight += page.size.height
}
}
return totalHeight
}
func documentTotalHeight() -> Double {
var totalHeight = 0.0
for idx in 0 ..< document.pageCount {
if let page = document.pageInfoForPage(at: idx) {
totalHeight += page.size.height
}
}
return totalHeight
}
func highlightTop(pageView: PDFPageView, highlight: HighlightAnnotation) -> Double {
if let pageInfo = pageView.pageInfo {
return pageInfo.size.height - highlight.boundingBox.minY
}
return 0.0
}
// swiftlint:disable:next function_body_length
func highlightSelection(pageView: PDFPageView, selectedText: String, dataService: DataService) -> String {
let highlightID = UUID().uuidString.lowercased()
@ -290,13 +318,18 @@ import Utils
let overlapping = overlappingHighlights(pageView: pageView, highlight: highlight)
if let patchData = try? highlight.generateInstantJSON(), let patch = String(data: patchData, encoding: .utf8) {
let top = highlightTop(pageView: pageView, highlight: highlight)
let positionPercent = (heightBefore(pageIndex: pageView.pageIndex) + top) / documentTotalHeight()
if overlapping.isEmpty {
viewModel.createHighlight(
dataService: dataService,
shortId: shortId,
highlightID: highlightID,
quote: quote,
patch: patch
patch: patch,
positionPercent: positionPercent,
positionAnchorIndex: Int(pageView.pageIndex)
)
} else {
let overlappingRects = overlapping.map(\.rects).compactMap { $0 }.flatMap { $0 }
@ -307,6 +340,9 @@ import Utils
boundingBox: boundingBox,
pageIndex: Int(pageView.pageIndex)
) {
let top = boundingBox.minY
let positionPercent = (heightBefore(pageIndex: pageView.pageIndex) + top) / documentTotalHeight()
mergedHighlight.customData = highlight.customData
document.add(annotations: [mergedHighlight])
document.remove(annotations: overlapping + [highlight])
@ -317,6 +353,8 @@ import Utils
highlightID: highlightID,
quote: quote,
patch: patch,
positionPercent: positionPercent,
positionAnchorIndex: Int(pageView.pageIndex),
overlapHighlightIdList: highlightIds(overlapping)
)
}

View file

@ -22,14 +22,18 @@ final class PDFViewerViewModel: ObservableObject {
shortId: String,
highlightID: String,
quote: String,
patch: String
patch: String,
positionPercent: Double?,
positionAnchorIndex: Int?
) {
_ = dataService.createHighlight(
shortId: shortId,
highlightID: highlightID,
quote: quote,
patch: patch,
articleId: pdfItem.itemID
articleId: pdfItem.itemID,
positionPercent: positionPercent,
positionAnchorIndex: positionAnchorIndex
)
}
@ -40,6 +44,8 @@ final class PDFViewerViewModel: ObservableObject {
highlightID: String,
quote: String,
patch: String,
positionPercent: Double?,
positionAnchorIndex: Int?,
overlapHighlightIdList: [String]
) {
_ = dataService.mergeHighlights(
@ -48,6 +54,8 @@ final class PDFViewerViewModel: ObservableObject {
quote: quote,
patch: patch,
articleId: pdfItem.itemID,
positionPercent: positionPercent,
positionAnchorIndex: positionAnchorIndex,
overlapHighlightIdList: overlapHighlightIdList
)
}

View file

@ -83,6 +83,8 @@ struct SafariWebLink: Identifiable {
quote: messageBody["quote"] as? String ?? "",
patch: messageBody["patch"] as? String ?? "",
articleId: messageBody["articleId"] as? String ?? "",
positionPercent: messageBody["highlightPositionPercent"] as? Double,
positionAnchorIndex: messageBody["highlightPositionAnchorIndex"] as? Int,
annotation: messageBody["annotation"] as? String ?? ""
)
@ -113,7 +115,9 @@ struct SafariWebLink: Identifiable {
let quote = messageBody["quote"] as? String,
let patch = messageBody["patch"] as? String,
let articleId = messageBody["articleId"] as? String,
let overlapHighlightIdList = messageBody["overlapHighlightIdList"] as? [String]
let overlapHighlightIdList = messageBody["overlapHighlightIdList"] as? [String],
let positionPercent = messageBody["highlightPositionPercent"] as? Double,
let positionAnchorIndex = messageBody["highlightPositionAnchorIndex"] as? Int
else {
replyHandler([], "createHighlight: Error encoding response")
return
@ -125,6 +129,8 @@ struct SafariWebLink: Identifiable {
quote: quote,
patch: patch,
articleId: articleId,
positionPercent: positionPercent,
positionAnchorIndex: positionAnchorIndex,
overlapHighlightIdList: overlapHighlightIdList
)

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21512" systemVersion="21G115" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="21G115" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Highlight" representedClassName="Highlight" syncable="YES" codeGenerationType="class">
<attribute name="annotation" optional="YES" attributeType="String"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
@ -7,6 +7,8 @@
<attribute name="id" attributeType="String"/>
<attribute name="markedForDeletion" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="patch" attributeType="String"/>
<attribute name="positionAnchorIndex" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionPercent" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="prefix" optional="YES" attributeType="String"/>
<attribute name="quote" attributeType="String"/>
<attribute name="serverSyncStatus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>

View file

@ -10,6 +10,8 @@ extension DataService {
quote: String,
patch: String,
articleId: String,
positionPercent: Double?,
positionAnchorIndex: Int?,
annotation: String? = nil
) -> [String: Any]? {
let internalHighlight = InternalHighlight(
@ -23,6 +25,8 @@ extension DataService {
updatedAt: nil,
createdByMe: true,
createdBy: nil,
positionPercent: positionPercent,
positionAnchorIndex: positionAnchorIndex,
labels: []
)
@ -54,7 +58,8 @@ extension DataService {
input: InputObjects.CreateHighlightInput(
annotation: OptionalArgument(highlight.annotation),
articleId: articleId,
id: highlight.id,
highlightPositionAnchorIndex: OptionalArgument(highlight.positionAnchorIndex),
highlightPositionPercent: OptionalArgument(highlight.positionPercent), id: highlight.id,
patch: highlight.patch,
quote: highlight.quote,
shortId: highlight.shortId

View file

@ -11,6 +11,8 @@ extension DataService {
quote: String,
patch: String,
articleId: String,
positionPercent: Double?,
positionAnchorIndex: Int?,
overlapHighlightIdList: [String]
) -> [String: Any]? {
let internalHighlight = InternalHighlight(
@ -25,6 +27,8 @@ extension DataService {
updatedAt: nil,
createdByMe: true,
createdBy: nil,
positionPercent: positionPercent,
positionAnchorIndex: positionAnchorIndex,
labels: []
)

View file

@ -24,6 +24,8 @@ let highlightSelection = Selection.Highlight {
updatedAt: try $0.updatedAt().value,
createdByMe: try $0.createdByMe(),
createdBy: try $0.user(selection: userProfileSelection),
positionPercent: try $0.highlightPositionPercent(),
positionAnchorIndex: try $0.highlightPositionAnchorIndex(),
labels: try $0.labels(selection: highlightLabelSelection.list.nullable) ?? []
)
}

View file

@ -14,6 +14,8 @@ struct InternalHighlight: Encodable {
let updatedAt: Date?
let createdByMe: Bool
let createdBy: InternalUserProfile?
let positionPercent: Double?
let positionAnchorIndex: Int?
var labels: [InternalLinkedItemLabel]
func asManagedObject(context: NSManagedObjectContext) -> Highlight {
@ -35,6 +37,10 @@ struct InternalHighlight: Encodable {
highlight.createdAt = createdAt
highlight.updatedAt = updatedAt
highlight.createdByMe = createdByMe
highlight.positionPercent = positionPercent ?? -1.0
if let positionAnchorIndex = positionAnchorIndex {
highlight.positionAnchorIndex = Int64(positionAnchorIndex)
}
if let createdBy = createdBy {
highlight.createdBy = createdBy.asManagedObject(inContext: context)
@ -64,6 +70,8 @@ struct InternalHighlight: Encodable {
updatedAt: highlight.updatedAt,
createdByMe: highlight.createdByMe,
createdBy: InternalUserProfile.makeSingle(highlight.createdBy),
positionPercent: highlight.positionPercent,
positionAnchorIndex: Int(highlight.positionAnchorIndex),
labels: InternalLinkedItemLabel.make(highlight.labels)
)
}

File diff suppressed because one or more lines are too long

View file

@ -19,7 +19,7 @@ import { removeHighlights } from '../../../lib/highlights/deleteHighlight'
import { createHighlight } from '../../../lib/highlights/createHighlight'
import { HighlightNoteModal } from './HighlightNoteModal'
import { ShareHighlightModal } from './ShareHighlightModal'
import { HighlightsModal } from './HighlightsModal'
import { NotebookModal } from './NotebookModal'
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
import { showErrorToast } from '../../../lib/toastHelpers'
import { ArticleMutations } from '../../../lib/articleActions'
@ -69,17 +69,16 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
>([])
const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 })
const [focusedHighlight, setFocusedHighlight] =
useState<Highlight | undefined>(undefined)
const [focusedHighlight, setFocusedHighlight] = useState<
Highlight | undefined
>(undefined)
const [selectionData, setSelectionData] = useSelection(
highlightLocations,
false //noteModal.open,
const [selectionData, setSelectionData] = useSelection(highlightLocations)
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
undefined
)
const [labelsTarget, setLabelsTarget] =
useState<Highlight | undefined>(undefined)
const canShareNative = useCanShareNative()
// Load the highlights
@ -190,6 +189,41 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
[props.highlightBarDisabled]
)
const selectionPercentPos = (selection: Selection): number | undefined => {
if (
selection.rangeCount > 0 &&
window &&
window.document.scrollingElement
) {
const percent =
(selection.getRangeAt(0).getBoundingClientRect().y + window.scrollY) /
window.document.scrollingElement.scrollHeight
return Math.min(Math.max(0, percent * 100), 100)
}
return undefined
}
const selectionAnchorIndex = (selection: Selection): number | undefined => {
if (selection.rangeCount > 0) {
const containerElement = () => {
const node = selection.getRangeAt(0).startContainer
if (node.nodeType == Node.ELEMENT_NODE) {
return node as HTMLElement
}
return node.parentElement
}
let walk = containerElement()
while (walk) {
const idx = Number(walk.getAttribute('data-omnivore-anchor-idx'))
if (idx > 0) {
return idx
}
walk = walk.parentElement
}
}
return undefined
}
const createHighlightFromSelection = async (
selection: SelectionAttributes,
note?: string
@ -201,6 +235,8 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
existingHighlights: highlights,
highlightStartEndOffsets: highlightLocations,
annotation: note,
highlightPositionPercent: selectionPercentPos(selection.selection),
highlightPositionAnchorIndex: selectionAnchorIndex(selection.selection),
},
props.articleMutations
)
@ -256,20 +292,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
]
)
const scrollToHighlight = (id: string) => {
const foundElement = document.querySelector(
`[omnivore-highlight-id="${id}"]`
)
if (foundElement) {
foundElement.scrollIntoView({
block: 'center',
behavior: 'smooth',
})
window.location.hash = `#${id}`
props.setShowHighlightsModal(false)
}
}
// Detect mouseclick on a highlight -- call `setFocusedHighlight` when highlight detected
const handleClickHighlight = useCallback(
(event: MouseEvent) => {
@ -641,7 +663,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
if (props.showHighlightsModal) {
return (
<HighlightsModal
<NotebookModal
highlights={highlights}
onOpenChange={() => props.setShowHighlightsModal(false)}
deleteHighlightAction={(highlightId: string) => {

View file

@ -17,7 +17,7 @@ import { TrashIcon } from '../../elements/images/TrashIcon'
import { theme } from '../../tokens/stitches.config'
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { HighlightView } from '../../patterns/HighlightView'
import { useCallback, useRef, useState } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import { StyledTextArea } from '../../elements/StyledTextArea'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { DotsThree } from 'phosphor-react'
@ -27,8 +27,9 @@ import { Label } from '../../../lib/networking/fragments/labelFragment'
import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight'
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { diff_match_patch } from 'diff-match-patch'
type HighlightsModalProps = {
type NotebookModalProps = {
highlights: Highlight[]
scrollToHighlight?: (arg: string) => void
updateHighlight: (highlight: Highlight) => void
@ -36,13 +37,48 @@ type HighlightsModalProps = {
onOpenChange: (open: boolean) => void
}
export function HighlightsModal(props: HighlightsModalProps): JSX.Element {
export const getHighlightLocation = (patch: string): number | undefined => {
const dmp = new diff_match_patch()
const patches = dmp.patch_fromText(patch)
return patches[0].start1 || undefined
}
export function NotebookModal(props: NotebookModalProps): JSX.Element {
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
useState<undefined | string>(undefined)
const [labelsTarget, setLabelsTarget] =
useState<Highlight | undefined>(undefined)
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
undefined
)
const [, updateState] = useState({})
const sortedHighlights = useMemo(() => {
const sorted = (a: number, b: number) => {
if (a < b) {
return -1
}
if (a > b) {
return 1
}
return 0
}
return props.highlights.sort((a: Highlight, b: Highlight) => {
if (a.highlightPositionPercent && b.highlightPositionPercent) {
return sorted(a.highlightPositionPercent, b.highlightPositionPercent)
}
// We do this in a try/catch because it might be an invalid diff
// With PDF it will definitely be an invalid diff.
try {
const aPos = getHighlightLocation(a.patch)
const bPos = getHighlightLocation(b.patch)
if (aPos && bPos) {
return sorted(aPos, bPos)
}
} catch {}
return a.createdAt.localeCompare(b.createdAt)
})
}, [props.highlights])
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<ModalOverlay />
@ -56,7 +92,7 @@ export function HighlightsModal(props: HighlightsModalProps): JSX.Element {
<VStack distribution="start" css={{ height: '100%' }}>
<ModalTitleBar title="Notebook" onOpenChange={props.onOpenChange} />
<Box css={{ overflow: 'auto', width: '100%' }}>
{props.highlights.map((highlight) => (
{sortedHighlights.map((highlight) => (
<ModalHighlightView
key={highlight.id}
highlight={highlight}
@ -74,7 +110,7 @@ export function HighlightsModal(props: HighlightsModalProps): JSX.Element {
updateHighlight={props.updateHighlight}
/>
))}
{props.highlights.length === 0 && (
{sortedHighlights.length === 0 && (
<SpanBox css={{ textAlign: 'center', width: '100%' }}>
<StyledText css={{ mb: '40px' }}>
You have not added any highlights or notes to this document

View file

@ -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'
@ -15,8 +21,9 @@ import { ShareHighlightModal } from './ShareHighlightModal'
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
import { webBaseURL } from '../../../lib/appConfig'
import { pspdfKitKey } from '../../../lib/appConfig'
import { HighlightsModal } from './HighlightsModal'
import { NotebookModal } from './NotebookModal'
import { HighlightNoteModal } from './HighlightNoteModal'
import { showErrorToast } from '../../../lib/toastHelpers'
export type PdfArticleContainerProps = {
viewerUsername: string
@ -29,12 +36,14 @@ export default function PdfArticleContainer(
props: PdfArticleContainerProps
): JSX.Element {
const containerRef = useRef<HTMLDivElement | null>(null)
const [shareTarget, setShareTarget] =
useState<Highlight | undefined>(undefined)
const [shareTarget, setShareTarget] = useState<Highlight | undefined>(
undefined
)
const [notebookKey, setNotebookKey] = useState<string>(uuidv4())
const [noteTarget, setNoteTarget] = useState<Highlight | undefined>(undefined)
const [noteTargetPageIndex, setNoteTargetPageIndex] =
useState<number | undefined>(undefined)
const [noteTargetPageIndex, setNoteTargetPageIndex] = useState<
number | undefined
>(undefined)
const highlightsRef = useRef<Highlight[]>([])
const canShareNative = useCanShareNative()
@ -65,6 +74,18 @@ export default function PdfArticleContainer(
[nativeShare, canShareNative, props.article.title]
)
const annotationOmnivoreId = (annotation: Annotation): string | undefined => {
if (
annotation &&
annotation.customData &&
annotation.customData.omnivoreHighlight &&
(annotation.customData.omnivoreHighlight as Highlight).id
) {
return (annotation.customData.omnivoreHighlight as Highlight).id
}
return undefined
}
useEffect(() => {
let instance: Instance
const container = containerRef.current
@ -82,6 +103,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 = {
@ -103,17 +136,28 @@ 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)
}
})
const annotationId = annotationOmnivoreId(annotation)
instance
.delete(annotation)
.then(() => {
if (annotationId) {
return deleteHighlightMutation(annotationId)
}
})
.then(() => {
const highlightIdx = highlightsRef.current.findIndex(
(value) => {
return value.id == annotationId
}
)
if (highlightIdx > -1) {
highlightsRef.current.splice(highlightIdx, 1)
}
})
.catch((err) => {
showErrorToast('Error deleting highlight: ' + err)
})
},
}
const note = {
@ -183,19 +227,17 @@ 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) {
if (
!annotation ||
event.reason !== PSPDFKit.AnnotationsWillChangeReason.DELETE_END
) {
return
}
if (
annotation &&
annotation.customData &&
annotation.customData.omnivoreHighlight &&
(annotation.customData.omnivoreHighlight as Highlight).id
) {
const data = annotation.customData.omnivoreHighlight as Highlight
deleteHighlightMutation(data.id)
const annotationId = annotationOmnivoreId(annotation)
if (annotationId) {
await deleteHighlightMutation(annotationId)
}
})
@ -300,6 +342,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,
@ -308,6 +351,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)
@ -342,6 +387,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,
@ -351,6 +397,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)
@ -378,6 +426,33 @@ 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) {
continue
}
const storedId = annotationOmnivoreId(annotation)
if (storedId == 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)
}
@ -423,11 +498,18 @@ export default function PdfArticleContainer(
/>
)}
{props.showHighlightsModal && (
<HighlightsModal
<NotebookModal
key={notebookKey}
highlights={highlightsRef.current}
onOpenChange={() => props.setShowHighlightsModal(false)}
/* eslint-disable @typescript-eslint/no-empty-function */
updateHighlight={() => {}}
deleteHighlightAction={(highlightId: string) => {
const event = new CustomEvent('deleteHighlightbyId', {
detail: highlightId,
})
document.dispatchEvent(event)
}}
/>
)}
</Box>

View file

@ -18,6 +18,8 @@ type CreateHighlightInput = {
annotation?: string
existingHighlights: Highlight[]
highlightStartEndOffsets: HighlightLocation[]
highlightPositionPercent?: number
highlightPositionAnchorIndex?: number
}
type CreateHighlightOutput = {
@ -30,7 +32,6 @@ export async function createHighlight(
input: CreateHighlightInput,
articleMutations: ArticleMutations
): Promise<CreateHighlightOutput> {
if (!input.selection.selection) {
return {}
}
@ -65,7 +66,10 @@ export async function createHighlight(
annotations.push(annotation)
}
})
removeHighlights(input.selection.overlapHighlights, input.highlightStartEndOffsets)
removeHighlights(
input.selection.overlapHighlights,
input.highlightStartEndOffsets
)
}
const highlightAttributes = makeHighlightNodeAttributes(
@ -83,6 +87,8 @@ export async function createHighlight(
patch,
annotation: annotations.length > 0 ? annotations.join('\n') : undefined,
articleId: input.articleId,
highlightPositionPercent: input.highlightPositionPercent,
highlightPositionAnchorIndex: input.highlightPositionAnchorIndex,
}
let highlight: Highlight | undefined
@ -98,7 +104,9 @@ export async function createHighlight(
($0) => !input.selection.overlapHighlights.includes($0.id)
)
} else {
highlight = await articleMutations.createHighlightMutation(newHighlightAttributes)
highlight = await articleMutations.createHighlightMutation(
newHighlightAttributes
)
}
if (highlight) {

View file

@ -7,10 +7,8 @@ import {
import type { SelectionAttributes } from './highlightHelpers'
export function useSelection(
highlightLocations: HighlightLocation[],
isDisabled: boolean
highlightLocations: HighlightLocation[]
): [SelectionAttributes | null, (x: SelectionAttributes | null) => void] {
const disabled = isDisabled
const [selectionAttributes, setSelectionAttributes] =
useState<SelectionAttributes | null>(null)
@ -142,10 +140,6 @@ export function useSelection(
}, [selectionAttributes?.selection])
useEffect(() => {
if (disabled) {
return
}
document.addEventListener('mouseup', handleFinishTouch)
document.addEventListener('touchend', handleFinishTouch)
document.addEventListener('contextmenu', handleFinishTouch)
@ -157,7 +151,7 @@ export function useSelection(
document.removeEventListener('contextmenu', handleFinishTouch)
document.removeEventListener('copyTextSelection', copyTextSelection)
}
}, [highlightLocations, handleFinishTouch, disabled, copyTextSelection])
}, [highlightLocations, handleFinishTouch, copyTextSelection])
return [selectionAttributes, setSelectionAttributes]
}

View file

@ -11,8 +11,11 @@ export const highlightFragment = gql`
patch
annotation
createdByMe
createdAt
updatedAt
sharedAt
highlightPositionPercent
highlightPositionAnchorIndex
labels {
id
name
@ -31,9 +34,12 @@ export type Highlight = {
patch: string
annotation?: string
createdByMe: boolean
createdAt: string
updatedAt: string
sharedAt: string
labels?: Label[]
highlightPositionPercent?: number
highlightPositionAnchorIndex?: number
}
export type User = {

View file

@ -10,6 +10,8 @@ export type CreateHighlightInput = {
shortId: string
patch: string
articleId: string
highlightPositionPercent?: number
highlightPositionAnchorIndex?: number
}
type CreateHighlightOutput = {

View file

@ -12,6 +12,8 @@ export type MergeHighlightInput = {
suffix?: string
annotation?: string
overlapHighlightIdList: string[]
highlightPositionPercent?: number
highlightPositionAnchorIndex?: number
}
export type MergeHighlightOutput = {

View file

@ -1,97 +0,0 @@
import { ComponentStory, ComponentMeta } from '@storybook/react'
import {ShareHighlightModal} from '../components/templates/article/ShareHighlightModal';
import { Highlight } from '../lib/networking/fragments/highlightFragment';
import { updateThemeLocally } from '../lib/themeUpdater';
import { ThemeId } from '../components/tokens/stitches.config';
export default {
title: 'Components/ShareHighlightModal',
parameters: {
previewTabs: {
'storybook/docs/panel': { hidden: true }
},
viewMode: 'canvas',
},
component: ShareHighlightModal,
argTypes: {
author: {control: 'text'},
title: {control: 'text'},
},
} as ComponentMeta<typeof ShareHighlightModal>
const Template = (props: {highlight: Highlight, handleOpenChange: () => void, title: string, author: string}) => {
return (
<ShareHighlightModal
url={`https://example.com/${props.highlight.shortId}`}
title={props.title}
author={props.author}
highlight={props.highlight}
onOpenChange={() => props.handleOpenChange()}
/>
)}
const highlight: Highlight = {
id: "nnnnn",
shortId: "shortId",
quote: "children not only participate in herding work, but are also encouraged to act independently in most other areas of life. They have a say in deciding when to eat, when to sleep, and what to wear, even at temperatures of -30C (-22F).",
patch: "patchhhhhhy",
createdByMe: true,
updatedAt: '123',
sharedAt: '123',
prefix: "Among the Sami, an indigenous people spread across the northernmost regions of Norway, Sweden, Finland and Russia's Kola Peninsula,",
suffix: ' To outsiders, that independence can be surprising. Missionaries who visited the Arctic in the 18th Century and later, wrote in their diaries that it seemed like Sámi children could do whatever they liked, and that they lacked discipline altogether.',
}
const highlightWithAnnotation: Highlight = {
...highlight,
annotation: "Okay… this is wild! I love this independence. Wondering how I can reponsibly instill this type of indepence in my own kids…",
}
export const LightShareHightlightModal: ComponentStory<typeof ShareHighlightModal> = (args: any) => {
updateThemeLocally(ThemeId.Light);
highlight.annotation = undefined;
return (
<Template {...args} handleOpenChange={() => console.log('open changed')} />
)
}
export const DarkShareHightlightModal: ComponentStory<typeof ShareHighlightModal> = (args: any) => {
updateThemeLocally(ThemeId.Dark);
highlight.annotation = undefined;
return (
<Template {...args} handleOpenChange={() => console.log('open changed')} />
)
}
export const LightShareHightlightModalWithNote: ComponentStory<typeof ShareHighlightModal> = (args: any) => {
updateThemeLocally(ThemeId.Light);
return (
<Template {...args} handleOpenChange={() => console.log('open changed')} />
)
}
export const DarkShareHightlightModalWithNote: ComponentStory<typeof ShareHighlightModal> = (args: any) => {
updateThemeLocally(ThemeId.Dark);
return (
<Template {...args} handleOpenChange={() => console.log('open changed')} />
)
}
LightShareHightlightModal.args = {
highlight: highlight,
title: 'The secret of Arctic survival parenting',
author: ' by Suvi Pilvi King by bbc.com',
}
DarkShareHightlightModal.args = {
...LightShareHightlightModal.args
}
LightShareHightlightModalWithNote.args = {
...LightShareHightlightModal.args,
highlight: highlightWithAnnotation,
}
DarkShareHightlightModalWithNote.args = {
...LightShareHightlightModalWithNote.args,
}