import { Box, HStack, VStack, SpanBox } from '../../elements/LayoutPrimitives' import { StyledText } from '../../elements/StyledText' import { theme } from '../../tokens/stitches.config' import type { Highlight } from '../../../lib/networking/fragments/highlightFragment' import { useCallback, useEffect, useMemo, useReducer, useRef, useState, } from 'react' import { BookOpen, CaretDown, CaretRight, DotsThree, Pencil, PencilLine, X, } from 'phosphor-react' import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { diff_match_patch } from 'diff-match-patch' import 'react-markdown-editor-lite/lib/index.css' import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation' import { v4 as uuidv4 } from 'uuid' import { nanoid } from 'nanoid' import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation' import { HighlightViewItem } from './HighlightViewItem' import { ConfirmationModal } from '../../patterns/ConfirmationModal' import { TrashIcon } from '../../elements/images/TrashIcon' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter' import { Button } from '../../elements/Button' import { ArticleNotes } from '../../patterns/ArticleNotes' import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery' type NotebookContentProps = { viewer: UserBasicData item: ReadableItem highlights: Highlight[] viewInReader: (highlightId: string) => void onAnnotationsChanged?: (highlights: Highlight[]) => void showConfirmDeleteNote?: boolean setShowConfirmDeleteNote?: (show: boolean) => void } export const getHighlightLocation = (patch: string): number | undefined => { const dmp = new diff_match_patch() const patches = dmp.patch_fromText(patch) return patches[0].start1 || undefined } type NoteState = { isCreating: boolean note: Highlight | undefined createStarted: Date | undefined } export function NotebookContent(props: NotebookContentProps): JSX.Element { const { articleData, mutate } = useGetArticleQuery({ slug: props.item.slug, username: props.viewer.profile.username, includeFriendsHighlights: false, }) const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = useState(undefined) const [labelsTarget, setLabelsTarget] = useState( undefined ) const [notesEditMode, setNotesEditMode] = useState<'edit' | 'preview'>( 'preview' ) const noteState = useRef({ isCreating: false, note: undefined, createStarted: undefined, }) const newNoteId = useMemo(() => { return uuidv4() }, []) const updateNote = useCallback((note: Highlight, text: string) => { ;(async () => { const result = await updateHighlightMutation({ highlightId: note.id, annotation: text, }) })() }, []) const createNote = useCallback((text: string) => { console.log('creating note: ', newNoteId, noteState.current.isCreating) noteState.current.isCreating = true noteState.current.createStarted = new Date() ;(async () => { try { const success = await createHighlightMutation({ id: newNoteId, shortId: nanoid(8), type: 'NOTE', articleId: props.item.id, annotation: text, }) if (success) { noteState.current.note = success noteState.current.isCreating = false } } catch (error) { console.error('error creating note: ', error) noteState.current.isCreating = false } })() }, []) const highlights = useMemo(() => { const result = articleData?.article.article.highlights const note = result?.find((h) => h.type === 'NOTE') if (note) { noteState.current.note = note noteState.current.isCreating = false } return result }, [articleData]) useEffect(() => { if (highlights && props.onAnnotationsChanged) { props.onAnnotationsChanged(highlights) } }, [highlights]) const sortedHighlights = useMemo(() => { const sorted = (a: number, b: number) => { if (a < b) { return -1 } if (a > b) { return 1 } return 0 } return (highlights ?? []) .filter((h) => h.type === 'HIGHLIGHT') .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) }) }, [highlights]) const handleSaveNoteText = useCallback( (text, cb: (success: boolean) => void) => { console.log('handleSaveNoteText', noteState.current) if (noteState.current.note) { updateNote(noteState.current.note, text) return } if (noteState.current.isCreating) { console.log('note is being created, deferring') if (noteState.current.createStarted) { const timeSinceStart = new Date().getTime() - noteState.current.createStarted.getTime() console.log(' -- timeSinceStart: ', timeSinceStart) if (timeSinceStart > 4000) { createNote(text) return } } return } createNote(text) }, [noteState, createNote, updateNote] ) const [articleNotesCollapsed, setArticleNotesCollapsed] = useState(false) const [highlightsCollapsed, setHighlightsCollapsed] = useState(false) return ( {!articleNotesCollapsed && ( )} {!highlightsCollapsed && ( <> {sortedHighlights.map((highlight) => ( { // dispatchAnnotations({ // type: 'UPDATE_HIGHLIGHT', // updateHighlight: highlight, // }) }} /> ))} {sortedHighlights.length === 0 && ( You have not added any highlights to this document. )} )} {/* */} {showConfirmDeleteHighlightId && ( { ;(async () => { const success = await deleteHighlightMutation( showConfirmDeleteHighlightId ) mutate() if (success) { showSuccessToast('Highlight deleted.') } else { showErrorToast('Error deleting highlight') } })() setShowConfirmDeleteHighlightId(undefined) }} onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)} icon={ } /> )} {labelsTarget && ( setLabelsTarget(undefined)} /> )} {props.showConfirmDeleteNote && ( { // deleteDocumentNote() if (props.setShowConfirmDeleteNote) { props.setShowConfirmDeleteNote(false) } }} onOpenChange={() => { if (props.setShowConfirmDeleteNote) { props.setShowConfirmDeleteNote(false) } }} /> )} ) } type SectionTitleProps = { title: string collapsed: boolean setCollapsed: (set: boolean) => void } function SectionTitle(props: SectionTitleProps): JSX.Element { return ( <> ) }