1- import { memo , useCallback , useContext , useEffect , useRef , useState } from "react" ;
1+ import { memo , useCallback , useContext , useEffect , useRef } from "react" ;
22import "./NoteManager.css" ;
33import type { ISynctexBlockId } from "@fluffylabs/links-metadata" ;
4- import { Button , Textarea } from "@fluffylabs/shared-ui" ;
4+ import { cn } from "@fluffylabs/shared-ui" ;
55import { twMerge } from "tailwind-merge" ;
6- import { validateMath } from "../../utils/validateMath " ;
6+ import { useLatestCallback } from "../../hooks/useLatestCallback " ;
77import { type ILocationContext , LocationContext } from "../LocationProvider/LocationProvider" ;
88import { type INotesContext , NotesContext } from "../NotesProvider/NotesProvider" ;
9- import { LABEL_LOCAL } from "../NotesProvider/consts/labels" ;
109import type { IDecoratedNote } from "../NotesProvider/types/DecoratedNote" ;
1110import type { IStorageNote } from "../NotesProvider/types/StorageNote" ;
12- import { Selection } from "../Selection/Selection " ;
11+ import { areSelectionsEqual } from "../NotesProvider/utils/areSelectionsEqual " ;
1312import { type ISelectionContext , SelectionContext } from "../SelectionProvider/SelectionProvider" ;
13+ import { InactiveNoteSkeleton } from "./components/InactiveNoteSkeleton" ;
14+ import { NewNote } from "./components/NewNote" ;
1415import { NotesList } from "./components/NotesList" ;
1516
1617const DEFAULT_AUTHOR = "" ;
1718
1819export function NoteManager ( { className } : { className ?: string } ) {
1920 return (
2021 < div className = { twMerge ( "notes-wrapper gap-4" , className ) } >
21- < Selection />
2222 < Notes />
2323 </ div >
2424 ) ;
@@ -27,62 +27,66 @@ export function NoteManager({ className }: { className?: string }) {
2727const MemoizedNotesList = memo ( NotesList ) ;
2828
2929function Notes ( ) {
30- const [ noteContent , setNoteContent ] = useState ( "" ) ;
31- const [ noteContentError , setNoteContentError ] = useState ( "" ) ;
3230 const { locationParams, setLocationParams } = useContext ( LocationContext ) as ILocationContext ;
3331 const { notesReady, activeNotes, notes, handleAddNote, handleDeleteNote, handleUpdateNote } = useContext (
3432 NotesContext ,
3533 ) as INotesContext ;
3634 const { selectedBlocks, pageNumber, handleClearSelection } = useContext ( SelectionContext ) as ISelectionContext ;
35+ const keepShowingNewNote = useRef < { selectionEnd : ISynctexBlockId ; selectionStart : ISynctexBlockId } > ( undefined ) ;
3736
38- const handleAddNoteRef = useRef ( handleAddNote ) ;
39- handleAddNoteRef . current = handleAddNote ;
40- const handleDeleteNoteRef = useRef ( handleDeleteNote ) ;
41- handleDeleteNoteRef . current = handleDeleteNote ;
42- const handleUpdateNoteRef = useRef ( handleUpdateNote ) ;
43- handleUpdateNoteRef . current = handleUpdateNote ;
44-
45- const memoizedHandleDeleteNote = useCallback ( ( note : IDecoratedNote ) => {
46- handleDeleteNoteRef . current ( note ) ;
47- } , [ ] ) ;
48-
49- const memoizedHandleUpdateNote = useCallback ( ( note : IDecoratedNote , newNote : IStorageNote ) => {
50- handleUpdateNoteRef . current ( note , newNote ) ;
51- } , [ ] ) ;
52-
53- const handleAddNoteClick = useCallback ( ( ) => {
54- if (
55- selectedBlocks . length === 0 ||
56- pageNumber === null ||
57- ! locationParams . selectionStart ||
58- ! locationParams . selectionEnd
59- ) {
60- throw new Error ( "Attempted saving a note without selection." ) ;
61- }
62-
63- setNoteContentError ( "" ) ;
37+ const latestHandleAddNote = useLatestCallback ( handleAddNote ) ;
38+ const latestDeleteNote = useLatestCallback ( handleDeleteNote ) ;
39+ const latestUpdateNote = useLatestCallback ( handleUpdateNote ) ;
6440
65- const mathValidationError = validateMath ( noteContent ) ;
41+ const memoizedHandleDeleteNote = useCallback (
42+ ( note : IDecoratedNote ) => {
43+ latestDeleteNote . current ( note ) ;
44+ handleClearSelection ( ) ;
45+ } ,
46+ [ handleClearSelection , latestDeleteNote ] ,
47+ ) ;
6648
67- if ( mathValidationError ) {
68- setNoteContentError ( mathValidationError ) ;
69- return ;
70- }
49+ const memoizedHandleUpdateNote = useCallback (
50+ ( note : IDecoratedNote , newNote : IStorageNote ) => {
51+ latestUpdateNote . current ( note , newNote ) ;
52+ } ,
53+ [ latestUpdateNote ] ,
54+ ) ;
7155
72- const newNote : IStorageNote = {
73- noteVersion : 3 ,
74- content : noteContent ,
75- date : Date . now ( ) ,
76- author : DEFAULT_AUTHOR ,
77- selectionStart : locationParams . selectionStart ,
78- selectionEnd : locationParams . selectionEnd ,
79- version : locationParams . version ,
80- labels : [ LABEL_LOCAL ] ,
81- } ;
82-
83- handleAddNoteRef . current ( newNote ) ;
56+ const handleNewNoteCancel = useCallback ( ( ) => {
8457 handleClearSelection ( ) ;
85- } , [ noteContent , pageNumber , selectedBlocks , handleClearSelection , locationParams ] ) ;
58+ } , [ handleClearSelection ] ) ;
59+
60+ const handleAddNoteClick = useCallback (
61+ ( { noteContent, labels } : { noteContent : string ; labels : string [ ] } ) => {
62+ if (
63+ selectedBlocks . length === 0 ||
64+ pageNumber === null ||
65+ ! locationParams . selectionStart ||
66+ ! locationParams . selectionEnd
67+ ) {
68+ throw new Error ( "Attempted saving a note without selection." ) ;
69+ }
70+
71+ const newNote : IStorageNote = {
72+ noteVersion : 3 ,
73+ content : noteContent ,
74+ date : Date . now ( ) ,
75+ author : DEFAULT_AUTHOR ,
76+ selectionStart : locationParams . selectionStart ,
77+ selectionEnd : locationParams . selectionEnd ,
78+ version : locationParams . version ,
79+ labels,
80+ } ;
81+
82+ latestHandleAddNote . current ( newNote ) ;
83+ keepShowingNewNote . current = {
84+ selectionStart : locationParams . selectionStart ,
85+ selectionEnd : locationParams . selectionEnd ,
86+ } ;
87+ } ,
88+ [ pageNumber , selectedBlocks , locationParams , latestHandleAddNote ] ,
89+ ) ;
8690
8791 const locationRef = useRef ( { locationParams, setLocationParams } ) ;
8892 locationRef . current = { locationParams, setLocationParams } ;
@@ -113,38 +117,51 @@ function Notes() {
113117 [ ] ,
114118 ) ;
115119
120+ const isActiveNotes = notes . some ( ( note ) => activeNotes . includes ( note ) ) ;
121+
116122 useEffect ( ( ) => {
117- if ( selectedBlocks . length === 0 ) {
118- setNoteContent ( "" ) ;
119- setNoteContentError ( "" ) ;
123+ if ( notesReady ) {
124+ keepShowingNewNote . current = undefined ;
120125 }
121- } , [ selectedBlocks ] ) ;
126+ } , [ notesReady ] ) ;
122127
123128 return (
124- < div className = "note-manager flex flex-col gap-2.5" style = { { opacity : notesReady ? 1.0 : 0.3 } } >
125- < div className = "flex flex-col p-2 gap-2" >
126- < Textarea
127- disabled = { selectedBlocks . length === 0 }
128- className = { noteContentError ? "error" : "" }
129- autoFocus
130- value = { noteContent }
131- onChange = { ( ev ) => setNoteContent ( ev . currentTarget . value ) }
132- placeholder = "Add a note to the selected fragment. Math typesetting is supported! Use standard delimiters such as $...$, \[...\] or \begin{equation}...\end{equation}."
129+ < div className = { cn ( "note-manager flex flex-col gap-2.5" , ! notesReady && "opacity-30 pointer-events-none" ) } >
130+ { locationParams . selectionEnd &&
131+ locationParams . selectionStart &&
132+ pageNumber !== null &&
133+ selectedBlocks . length > 0 &&
134+ ! isActiveNotes &&
135+ ( notesReady || areSelectionsEqual ( locationParams , keepShowingNewNote . current ) ) && (
136+ < NewNote
137+ selectionStart = { locationParams . selectionStart }
138+ selectionEnd = { locationParams . selectionEnd }
139+ version = { locationParams . version }
140+ onCancel = { handleNewNoteCancel }
141+ onSave = { handleAddNoteClick }
142+ />
143+ ) }
144+
145+ { ! notesReady && notes . length === 0 && (
146+ < >
147+ < InactiveNoteSkeleton />
148+ < InactiveNoteSkeleton />
149+ < InactiveNoteSkeleton />
150+ < InactiveNoteSkeleton />
151+ </ >
152+ ) }
153+
154+ { notesReady && notes . length === 0 && < div className = "no-notes text-sidebar-foreground" > No notes available</ div > }
155+
156+ { notes . length > 0 && (
157+ < MemoizedNotesList
158+ activeNotes = { activeNotes }
159+ notes = { notes }
160+ onEditNote = { memoizedHandleUpdateNote }
161+ onDeleteNote = { memoizedHandleDeleteNote }
162+ onSelectNote = { memoizedHandleSelectNote }
133163 />
134-
135- { noteContentError ? < div className = "validation-message" > { noteContentError } </ div > : null }
136- < Button disabled = { noteContent . length < 1 } onClick = { handleAddNoteClick } variant = "secondary" >
137- Add
138- </ Button >
139- </ div >
140-
141- < MemoizedNotesList
142- activeNotes = { activeNotes }
143- notes = { notes }
144- onEditNote = { memoizedHandleUpdateNote }
145- onDeleteNote = { memoizedHandleDeleteNote }
146- onSelectNote = { memoizedHandleSelectNote }
147- />
164+ ) }
148165 </ div >
149166 ) ;
150167}
0 commit comments