import { nanoid } from 'nanoid' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import 'react-markdown-editor-lite/lib/index.css' import { v4 as uuidv4 } from 'uuid' import { formattedShortTime } from '../../../lib/dateFormatting' import { sortHighlights } from '../../../lib/highlights/sortHighlights' import type { Highlight } from '../../../lib/networking/fragments/highlightFragment' import { useCreateHighlight, useDeleteHighlight, useUpdateHighlight, } from '../../../lib/networking/highlights/useItemHighlights' import { ReadableItem, useGetLibraryItemContent, } from '../../../lib/networking/library_items/useLibraryItems' import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' import { isDarkTheme } from '../../../lib/themeUpdater' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { TrashIcon } from '../../elements/icons/TrashIcon' import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { ArticleNotes } from '../../patterns/ArticleNotes' import { ConfirmationModal } from '../../patterns/ConfirmationModal' import { theme } from '../../tokens/stitches.config' import { HighlightViewItem } from './HighlightViewItem' import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter' type NotebookContentProps = { viewer: UserBasicData item: ReadableItem viewInReader: (highlightId: string) => void onAnnotationsChanged?: (highlights: Highlight[]) => void showConfirmDeleteNote?: boolean setShowConfirmDeleteNote?: (show: boolean) => void } type NoteState = { isCreating: boolean note: Highlight | undefined createStarted: Date | undefined } export function NotebookContent(props: NotebookContentProps): JSX.Element { const isDark = isDarkTheme() const createHighlight = useCreateHighlight() const deleteHighlight = useDeleteHighlight() const updateHighlight = useUpdateHighlight() const { data: article } = useGetLibraryItemContent( props.viewer.profile.username as string, props.item.slug as string ) const [noteText, setNoteText] = useState('') const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = useState(undefined) const [labelsTarget, setLabelsTarget] = useState( undefined ) const noteState = useRef({ isCreating: false, note: undefined, createStarted: undefined, }) const newNoteId = useMemo(() => { return uuidv4() }, []) const updateNote = useCallback( (note: Highlight, text: string, startTime: Date) => { ;(async () => { const result = await updateHighlightMutation({ libraryItemId: props.item.id, highlightId: note.id, annotation: text, }) if (result) { setLastSaved(startTime) } else { setErrorSaving('Error saving') } })() }, [props] ) const deleteNote = useCallback( (noteId: string) => { ;(async () => { const result = await deleteHighlight.mutateAsync({ itemId: props.item.id, slug: props.item.slug, highlightId: noteId, }) if (result) { noteState.current.note = undefined setNoteText('') } else { setErrorSaving('Error deleting note') } })() }, [props, deleteHighlight] ) const createNote = useCallback( (text: string) => { noteState.current.isCreating = true noteState.current.createStarted = new Date() ;(async () => { try { const success = await createHighlight.mutateAsync({ itemId: props.item.id, slug: props.item.slug, input: { id: newNoteId, shortId: nanoid(8), type: 'NOTE', articleId: props.item.id, annotation: text, }, }) if (success) { noteState.current.note = success noteState.current.isCreating = false } else { setErrorSaving('Error creating note') } } catch (error) { console.error('error creating note: ', error) noteState.current.isCreating = false setErrorSaving('Error creating note') } })() }, [props, newNoteId] ) const highlights = useMemo(() => { const result = article?.highlights const note = result?.find((h) => h.type === 'NOTE') if (note) { noteState.current.note = note noteState.current.isCreating = false setNoteText(note.annotation || '') } else { setNoteText('') } return result }, [article]) useEffect(() => { if (highlights && props.onAnnotationsChanged) { props.onAnnotationsChanged(highlights) } }, [props, highlights]) const sortedHighlights = useMemo(() => { return sortHighlights(highlights ?? []) }, [highlights]) const handleSaveNoteText = useCallback( (text: string) => { const changeTime = new Date() setLastChanged(changeTime) if (noteState.current.note) { if (noteState.current.note.type === 'NOTE' && text === '') { deleteNote(noteState.current.note.id) return } updateNote(noteState.current.note, text, changeTime) return } if (text === '') { return } if (noteState.current.isCreating) { if (noteState.current.createStarted) { const timeSinceStart = new Date().getTime() - noteState.current.createStarted.getTime() if (timeSinceStart > 4000) { createNote(text) return } } return } createNote(text) }, [createNote, updateNote, deleteNote] ) const deleteDocumentNote = useCallback(() => { ;(async () => { highlights ?.filter((h) => h.type === 'NOTE') .forEach(async (h) => { const result = await deleteHighlight.mutateAsync({ itemId: props.item.id, slug: props.item.slug, highlightId: h.id, }) if (!result) { showErrorToast('Error deleting note') } }) noteState.current.note = undefined })() setNoteText('') }, [props, noteState, highlights]) const [errorSaving, setErrorSaving] = useState(undefined) const [lastChanged, setLastChanged] = useState(undefined) const [lastSaved, setLastSaved] = useState(undefined) return ( <> {errorSaving && ( {errorSaving} )} {lastSaved !== undefined ? ( <> {lastChanged === lastSaved ? 'Saved' : `Last saved ${formattedShortTime(lastSaved.toISOString())}`} ) : null} {sortedHighlights.map((highlight) => ( { // nothing should be needed here anymore with new caching console.log('update highlight') }} /> ))} {sortedHighlights.length === 0 && ( You have not added any highlights to this document. )} {showConfirmDeleteHighlightId && ( { ;(async () => { const highlightId = showConfirmDeleteHighlightId const success = await deleteHighlight.mutateAsync({ itemId: props.item.id, slug: props.item.slug, highlightId: showConfirmDeleteHighlightId, }) if (success) { showSuccessToast('Highlight deleted.', { position: 'bottom-right', }) const event = new CustomEvent('deleteHighlightbyId', { detail: highlightId, }) document.dispatchEvent(event) } else { showErrorToast('Error deleting highlight', { position: 'bottom-right', }) } })() setShowConfirmDeleteHighlightId(undefined) }} onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)} icon={ } /> )} {labelsTarget && ( { // Don't actually need to do something here console.log('update highlight: ', highlight) }} onOpenChange={() => { setLabelsTarget(undefined) }} /> )} {props.showConfirmDeleteNote && ( { deleteDocumentNote() if (props.setShowConfirmDeleteNote) { props.setShowConfirmDeleteNote(false) } }} onOpenChange={() => { if (props.setShowConfirmDeleteNote) { props.setShowConfirmDeleteNote(false) } }} /> )} ) }