mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1758 from omnivore-app/fix/save-highlight-position-info
Save highlight position info on web
This commit is contained in:
commit
b2b0d8be82
18 changed files with 303 additions and 175 deletions
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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) ?? []
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ export type CreateHighlightInput = {
|
|||
shortId: string
|
||||
patch: string
|
||||
articleId: string
|
||||
highlightPositionPercent?: number
|
||||
highlightPositionAnchorIndex?: number
|
||||
}
|
||||
|
||||
type CreateHighlightOutput = {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export type MergeHighlightInput = {
|
|||
suffix?: string
|
||||
annotation?: string
|
||||
overlapHighlightIdList: string[]
|
||||
highlightPositionPercent?: number
|
||||
highlightPositionAnchorIndex?: number
|
||||
}
|
||||
|
||||
export type MergeHighlightOutput = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
Loading…
Reference in a new issue