Handle changes in the modal notebook

This commit is contained in:
Jackson Harper 2023-03-24 20:18:37 +08:00
parent 356c64173a
commit 33e301cae6
6 changed files with 59 additions and 574 deletions

View file

@ -1,324 +0,0 @@
/* eslint-disable react/no-children-prop */
import {
ChangeEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { formattedShortTime } from '../../lib/dateFormatting'
import { HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
import MarkdownIt from 'markdown-it'
import MdEditor from 'react-markdown-editor-lite'
import 'react-markdown-editor-lite/lib/index.css'
import ReactMarkdown from 'react-markdown'
import throttle from 'lodash/throttle'
import { Highlight } from '../../lib/networking/fragments/highlightFragment'
const mdParser = new MarkdownIt()
type NoteSectionProps = {
placeHolder: string
mode: 'edit' | 'preview'
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
saveText: (text: string, completed: (success: boolean) => void) => void
}
export function HighlightNoteBox(props: NoteSectionProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const saveText = useCallback(
(text, updateTime) => {
props.saveText(text, (success) => {
if (success) {
setLastSaved(updateTime)
}
})
},
[props]
)
const saveRef = useRef(saveText)
useEffect(() => {
saveRef.current = saveText
}, [lastSaved, lastChanged])
const debouncedSave = useMemo<
(text: string, updateTime: Date) => void
>(() => {
const func = (text: string, updateTime: Date) => {
saveRef.current?.(text, updateTime)
}
return throttle(func, 3000)
}, [])
const handleEditorChange = useCallback(
(
data: { text: string; html: string },
event?: ChangeEvent<HTMLTextAreaElement> | undefined
) => {
if (event) {
event.preventDefault()
}
const updateTime = new Date()
setLastChanged(updateTime)
debouncedSave(data.text, updateTime)
},
[lastSaved, lastChanged]
)
return (
<>
{props.mode == 'edit' ? (
<VStack
css={{
width: '100%',
mt: '15px',
'.rc-md-editor': {
borderRadius: '5px',
},
'.rc-md-navigation': {
borderRadius: '5px',
borderBottomLeftRadius: '0px',
borderBottomRightRadius: '0px',
},
'.rc-md-editor .editor-container >.section': {
borderRight: 'unset',
},
'.rc-md-editor .editor-container .sec-md .input': {
padding: '10px',
borderRadius: '5px',
},
}}
>
<MdEditor
autoFocus={true}
defaultValue={props.text}
placeholder={props.placeHolder}
view={{ menu: true, md: true, html: false }}
canView={{
menu: props.mode == 'edit',
md: true,
html: true,
both: false,
fullScreen: false,
hideMenu: false,
}}
plugins={[
'header',
'font-bold',
'font-italic',
'font-underline',
'font-strikethrough',
'list-unordered',
'list-ordered',
'block-quote',
'link',
'auto-resize',
]}
style={{
width: '100%',
height: props.sizeMode == 'normal' ? '160px' : '320px',
}}
renderHTML={(text: string) => mdParser.render(text)}
onChange={handleEditorChange}
/>
<HStack
css={{
minHeight: '15px',
width: '100%',
fontSize: '9px',
mt: '1px',
color: '$thTextSubtle',
}}
alignment="start"
distribution="start"
>
{errorSaving && (
<SpanBox
css={{
width: '100%',
fontSize: '9px',
mt: '1px',
color: 'red',
}}
>
{errorSaving}
</SpanBox>
)}
{lastSaved !== undefined ? (
<>
{lastChanged === lastSaved
? 'Saved'
: `Last saved ${formattedShortTime(lastSaved.toISOString())}`}
</>
) : null}
</HStack>
</VStack>
) : (
<>
<SpanBox
css={{
borderRadius: '5px',
p: '10px',
width: '100%',
marginTop: '15px',
color: '$thHighContrast',
bg: '#F5F5F5',
'> *': {
m: '0px',
},
}}
onClick={() => props.setEditMode('edit')}
>
<ReactMarkdown
children={props.text ?? 'Add notes to this document...'}
/>
</SpanBox>
</>
)}
</>
)
}
type MarkdownNote = {
placeHolder: string
mode: 'edit' | 'preview'
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
highlight: Highlight
defaultText: string | undefined
handleEditorChange: (data: { text: string; html: string }) => void
}
export function MarkdownNote(props: MarkdownNote): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
return (
<>
{props.mode == 'edit' ? (
<VStack
css={{
width: '100%',
mt: '15px',
'.rc-md-editor': {
borderRadius: '5px',
},
'.rc-md-navigation': {
borderRadius: '5px',
borderBottomLeftRadius: '0px',
borderBottomRightRadius: '0px',
},
'.rc-md-editor .editor-container >.section': {
borderRight: 'unset',
},
'.rc-md-editor .editor-container .sec-md .input': {
padding: '10px',
borderRadius: '5px',
},
}}
>
<MdEditor
autoFocus={true}
defaultValue={props.defaultText}
placeholder={props.placeHolder}
view={{ menu: true, md: true, html: false }}
canView={{
menu: props.mode == 'edit',
md: true,
html: true,
both: false,
fullScreen: false,
hideMenu: false,
}}
plugins={[
'header',
'font-bold',
'font-italic',
'font-underline',
'font-strikethrough',
'list-unordered',
'list-ordered',
'block-quote',
'link',
'auto-resize',
]}
style={{
width: '100%',
height: props.sizeMode == 'normal' ? '160px' : '320px',
}}
renderHTML={(text: string) => mdParser.render(text)}
onChange={props.handleEditorChange}
/>
<HStack
css={{
minHeight: '15px',
width: '100%',
fontSize: '9px',
mt: '1px',
color: '$thTextSubtle',
}}
alignment="start"
distribution="start"
>
{errorSaving && (
<SpanBox
css={{
width: '100%',
fontSize: '9px',
mt: '1px',
color: 'red',
}}
>
{errorSaving}
</SpanBox>
)}
{lastSaved !== undefined ? (
<>
{lastChanged === lastSaved
? 'Saved'
: `Last saved ${formattedShortTime(lastSaved.toISOString())}`}
</>
) : null}
</HStack>
</VStack>
) : (
<>
<SpanBox
css={{
borderRadius: '5px',
p: '10px',
width: '100%',
marginTop: '15px',
color: '$thHighContrast',
bg: '#F5F5F5',
'> *': {
m: '0px',
},
}}
onClick={() => props.setEditMode('edit')}
>
<ReactMarkdown
children={props.highlight?.annotation ?? props.placeHolder}
/>
</SpanBox>
</>
)}
</>
)
}

View file

@ -9,8 +9,8 @@ import {
SpanBox,
HStack,
} from '../elements/LayoutPrimitives'
import { styled, theme } from '../tokens/stitches.config'
import { HighlightViewNote } from './HighlightViewNote'
import { styled } from '../tokens/stitches.config'
import { HighlightViewNote } from './HighlightNotes'
type HighlightViewProps = {
highlight: Highlight

View file

@ -1,205 +0,0 @@
/* eslint-disable react/no-children-prop */
import {
ChangeEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { formattedShortTime } from '../../lib/dateFormatting'
import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation'
import { HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
import MarkdownIt from 'markdown-it'
import MdEditor from 'react-markdown-editor-lite'
import 'react-markdown-editor-lite/lib/index.css'
import ReactMarkdown from 'react-markdown'
import throttle from 'lodash/throttle'
import { Highlight } from '../../lib/networking/fragments/highlightFragment'
const mdParser = new MarkdownIt()
type HighlightViewNoteProps = {
placeHolder: string
mode: 'edit' | 'preview'
highlight: Highlight
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
updateHighlight: (highlight: Highlight) => void
}
export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const saveText = useCallback(
(text, updateTime) => {
;(async () => {
const success = await updateHighlightMutation({
annotation: text,
highlightId: props.highlight?.id,
})
if (success) {
setLastSaved(updateTime)
props.highlight.annotation = text
props.updateHighlight(props.highlight)
} else {
setErrorSaving('Error saving highlight.')
}
})()
},
[props]
)
const saveRef = useRef(saveText)
useEffect(() => {
saveRef.current = saveText
}, [lastSaved, lastChanged, saveText])
const debouncedSave = useMemo<
(text: string, updateTime: Date) => void
>(() => {
const func = (text: string, updateTime: Date) => {
saveRef.current?.(text, updateTime)
}
return throttle(func, 3000)
}, [])
const handleEditorChange = useCallback(
(
data: { text: string; html: string },
event?: ChangeEvent<HTMLTextAreaElement> | undefined
) => {
if (event) {
event.preventDefault()
}
const updateTime = new Date()
setLastChanged(updateTime)
debouncedSave(data.text, updateTime)
},
[lastSaved, lastChanged, saveText]
)
return (
<>
{props.mode == 'edit' ? (
<VStack
css={{
width: '100%',
mt: '15px',
'.rc-md-editor': {
borderRadius: '5px',
},
'.rc-md-navigation': {
borderRadius: '5px',
borderBottomLeftRadius: '0px',
borderBottomRightRadius: '0px',
},
'.rc-md-editor .editor-container >.section': {
borderRight: 'unset',
},
'.rc-md-editor .editor-container .sec-md .input': {
padding: '10px',
borderRadius: '5px',
},
}}
>
<MdEditor
autoFocus={true}
defaultValue={props.text}
placeholder={props.placeHolder}
view={{ menu: true, md: true, html: false }}
canView={{
menu: props.mode == 'edit',
md: true,
html: true,
both: false,
fullScreen: false,
hideMenu: false,
}}
plugins={[
'header',
'font-bold',
'font-italic',
'font-underline',
'font-strikethrough',
'list-unordered',
'list-ordered',
'block-quote',
'link',
'auto-resize',
]}
style={{
width: '100%',
height: props.sizeMode == 'normal' ? '160px' : '320px',
}}
renderHTML={(text: string) => mdParser.render(text)}
onChange={handleEditorChange}
/>
<HStack
css={{
minHeight: '15px',
width: '100%',
fontSize: '9px',
mt: '1px',
color: '$thTextSubtle',
}}
alignment="start"
distribution="start"
>
{errorSaving && (
<SpanBox
css={{
width: '100%',
fontSize: '9px',
mt: '1px',
color: 'red',
}}
>
{errorSaving}
</SpanBox>
)}
{lastSaved !== undefined ? (
<>
{lastChanged === lastSaved
? 'Saved'
: `Last saved ${formattedShortTime(lastSaved.toISOString())}`}
</>
) : null}
</HStack>
</VStack>
) : (
<>
<SpanBox
css={{
borderRadius: '5px',
p: '10px',
width: '100%',
marginTop: '15px',
color: '$thHighContrast',
bg: '#F5F5F5',
'> *': {
m: '0px',
},
}}
onClick={() => props.setEditMode('edit')}
>
<ReactMarkdown
children={
props.highlight?.annotation ?? 'Add notes to this highlight...'
}
/>
</SpanBox>
</>
)}
</>
)
}

View file

@ -358,6 +358,24 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
[highlights, highlightLocations, openNoteModal]
)
const handleCloseNotebook = useCallback(
(updatedHighlights: Highlight[], deletedHighlights: Highlight[]) => {
props.setShowHighlightsModal(false)
setHighlights(updatedHighlights)
removeHighlights(
deletedHighlights.map((h) => h.id),
highlightLocations
)
updatedHighlights.forEach((h) => {
updateHighlightsCallback(h)
})
},
[highlights, highlightLocations]
)
useEffect(() => {
if (typeof window === 'undefined') {
return
@ -651,11 +669,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
<NotebookModal
highlights={highlights}
pageId={props.articleId}
onOpenChange={() => props.setShowHighlightsModal(false)}
deleteHighlightAction={(highlightId: string) => {
removeHighlightCallback(highlightId)
}}
updateHighlight={updateHighlightsCallback}
onClose={handleCloseNotebook}
/>
)
}

View file

@ -27,25 +27,13 @@ import { createHighlightMutation } from '../../../lib/networking/mutations/creat
import { v4 as uuidv4 } from 'uuid'
import { nanoid } from 'nanoid'
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
import { HighlightNoteBox } from '../../patterns/HighlightNoteBox'
import { HighlightNoteBox } from '../../patterns/HighlightNotes'
type NotebookModalProps = {
pageId: string
highlights: Highlight[]
scrollToHighlight?: (arg: string) => void
updateHighlight: (highlight: Highlight) => void
deleteHighlightAction?: (highlightId: string) => void
onOpenChange: (open: boolean) => void
}
type HighlightListReducerAction = {
type: string
highlightId?: string
createId?: string
removeId?: string
note?: string
highlight?: Highlight
highlights?: Highlight[]
onClose: (highlights: Highlight[], deletedAnnotations: Highlight[]) => void
}
export const getHighlightLocation = (patch: string): number | undefined => {
@ -61,6 +49,7 @@ type AnnotationInfo = {
noteId: string
allAnnotations: Highlight[]
deletedAnnotations: Highlight[]
}
export function NotebookModal(props: NotebookModalProps): JSX.Element {
@ -74,6 +63,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
const [notesEditMode, setNotesEditMode] = useState<'edit' | 'preview'>(
'preview'
)
const [, updateState] = useState({})
const annotationsReducer = (
@ -97,7 +87,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
loaded: true,
note: note,
noteId: note?.id ?? state.noteId,
allAnnotations: action.allHighlights ?? [],
allAnnotations: [...(action.allHighlights ?? [])],
}
}
case 'CREATE_NOTE': {
@ -138,8 +128,12 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
if (idx < 0) {
return { ...state }
}
const deleted = state.deletedAnnotations
deleted.push(state.allAnnotations[idx])
return {
...state,
deletedAnnotations: deleted,
allAnnotations: state.allAnnotations.splice(idx, 1),
}
}
@ -166,6 +160,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
note: undefined,
noteId: uuidv4(),
allAnnotations: [],
deletedAnnotations: [],
})
useEffect(() => {
@ -243,7 +238,6 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
const handleSaveNoteText = useCallback(
(text, cb: (success: boolean) => void) => {
console.log(' handleSaveNoteText: ', text, 'highlights', annotations)
if (!annotations.loaded) {
// We haven't loaded the user's annotations yet, so we can't
// find or create their highlight note.
@ -295,14 +289,12 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
[annotations, props.pageId]
)
const handleClose = useCallback(() => {
props.onClose(annotations.allAnnotations, annotations.deletedAnnotations)
}, [annotations])
return (
<ModalRoot
defaultOpen
onOpenChange={() => {
console.log('CLOSING DIALOG')
props.onOpenChange(false)
}}
>
<ModalRoot defaultOpen onOpenChange={handleClose}>
<ModalOverlay />
<ModalContent
onInteractOutside={(event) => {
@ -356,7 +348,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
title="Delete Document Note"
/>
</Dropdown>
<CloseButton close={() => props.onOpenChange(false)} />
<CloseButton close={handleClose} />
</HStack>
</HStack>
<VStack distribution="start" css={{ height: '100%', p: '20px' }}>
@ -393,7 +385,6 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
<ModalHighlightView
key={highlight.id}
highlight={highlight}
showDelete={!!props.deleteHighlightAction}
scrollToHighlight={props.scrollToHighlight}
setSetLabelsTarget={setLabelsTarget}
setShowConfirmDeleteHighlightId={
@ -406,7 +397,6 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
})
}}
updateHighlight={() => {
console.log('updating highlight: ', highlight)
dispatchAnnotations({
type: 'UPDATE_HIGHLIGHT',
updateHighlight: highlight,
@ -436,10 +426,20 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
<ConfirmationModal
message={'Are you sure you want to delete this highlight?'}
onAccept={() => {
dispatchAnnotations({
type: 'DELETE_HIGHLIGHT',
deleteHighlightId: showConfirmDeleteHighlightId,
})
;(async () => {
const success = await deleteHighlightMutation(
showConfirmDeleteHighlightId
)
if (success) {
dispatchAnnotations({
type: 'DELETE_HIGHLIGHT',
deleteHighlightId: showConfirmDeleteHighlightId,
})
showSuccessToast('Highlight deleted.')
} else {
showErrorToast('Error deleting highlight')
}
})()
setShowConfirmDeleteHighlightId(undefined)
}}
onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)}
@ -486,7 +486,6 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
type ModalHighlightViewProps = {
highlight: Highlight
showDelete: boolean
scrollToHighlight?: (arg: string) => void
deleteHighlightAction: () => void
updateHighlight: (highlight: Highlight) => void
@ -643,11 +642,6 @@ function SizeToggle(props: SizeToggleProps): JSX.Element {
},
}}
onClick={(event) => {
console.log(
' updating size mode: ',
props.mode,
props.mode == 'normal' ? 'maximized' : 'normal'
)
props.setMode(props.mode == 'normal' ? 'maximized' : 'normal')
event.preventDefault()
}}

View file

@ -1,7 +1,13 @@
import { HighlightLocation } from './highlightGenerator'
import { getHighlightElements, getHighlightNoteButton } from './highlightHelpers'
import {
getHighlightElements,
getHighlightNoteButton,
} from './highlightHelpers'
export function removeHighlights(ids: string[], locations: HighlightLocation[]): void {
export function removeHighlights(
ids: string[],
locations: HighlightLocation[]
): void {
ids.forEach((id) => {
const elements = getHighlightElements(id)
const noteButtons = getHighlightNoteButton(id)