Let a margin remark become a note, and lead to it ever after
Some remarks turn into a page of their own. "Notiz anlegen" on a margin card makes one, titles it from what was written, and goes there; the pin keeps its id so the passage leads to that note afterwards. The note is created with a link back to the document, so the connection reads both ways and the document lists it among its backlinks — a note born from a passage should not be a note with nothing on it saying which passage. An existing note can be linked instead, and a link can be let go of without touching the note. Both are offered on the phone sheet too. Two older bugs stood in the way and are fixed: `openLinkTarget` finds a note's notebook by looking it up in the notes list, and a note created in the same tick is not in the copy that callback closed over — so jumping to a fresh note went nowhere at all. Creating one now navigates by the notebook it already knows. A drawing save waits 700ms for the next stroke, and leaving the document inside that window dropped it. That is precisely what turning a remark into a note does, so the link would have been lost on the way out. The viewer flushes a pending write when it unmounts, which also covers drawing and navigating away quickly. 215 unit tests, 229 browser checks across eleven suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
011ba82a94
commit
502aecfc92
10 files changed
+541
-12
No files matched your search
@@ -135,6 +135,7 @@ function PdfPage({ doc, index, width, height, size, visible, ink, pins, selectab
|
||||
onChange={(id, text) => pins.onChange(index + 1, size, id, text)}
|
||||
onRemove={(id) => pins.onRemove(index + 1, size, id)}
|
||||
numberOf={pins.numberOf}
|
||||
link={pins.link}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -149,7 +150,7 @@ function PdfPage({ doc, index, width, height, size, visible, ink, pins, selectab
|
||||
* proportions before anything is drawn, so the scrollbar is honest from the
|
||||
* first frame instead of jumping as pages arrive.
|
||||
*/
|
||||
export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle, pinLinks }) {
|
||||
const [state, setState] = useState({ status: "loading" });
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [drawing, setDrawing] = useState(false);
|
||||
@@ -182,6 +183,12 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
setInk(incoming);
|
||||
}, [note?.drawingStrokes]);
|
||||
|
||||
/* Held in a ref so the unmount flush below can reach the current one: the
|
||||
* effect that runs it has no dependencies, and a captured `onUpdate` would be
|
||||
* the one from the first render. */
|
||||
const onUpdateRef = useRef(onUpdate);
|
||||
onUpdateRef.current = onUpdate;
|
||||
|
||||
const saveInk = useCallback(
|
||||
(next) => {
|
||||
setInk(next);
|
||||
@@ -199,6 +206,22 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
[onUpdate]
|
||||
);
|
||||
|
||||
/* A save waits 700ms for the next stroke. Leaving the document inside that
|
||||
* window — which is exactly what happens when a remark is turned into a note
|
||||
* and the app goes there — would drop it. Flushed on the way out. */
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (!saveTimer.current) return;
|
||||
clearTimeout(saveTimer.current);
|
||||
saveTimer.current = 0;
|
||||
const doc = inkRef.current;
|
||||
onUpdateRef.current?.({
|
||||
drawingStrokes: hasOverlay(doc) ? { ...doc, t: newSaveToken() } : null,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const commitPage = useCallback(
|
||||
(pageNo, strokes) => {
|
||||
const size = state.sizes?.[pageNo - 1];
|
||||
@@ -290,6 +313,35 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
[writeNotes]
|
||||
);
|
||||
|
||||
/** Points a pin at a note, or lets go of one — the note itself is untouched. */
|
||||
const linkNote = useCallback(
|
||||
(pageNumber, size, id, noteId) =>
|
||||
writeNotes(pageNumber, size, (notes) => notes.map((n) => (n.id === id ? { ...n, noteId } : n))),
|
||||
[writeNotes]
|
||||
);
|
||||
|
||||
/**
|
||||
* Makes a note out of a remark and jumps to it.
|
||||
*
|
||||
* The pin keeps the new note's id, so the passage leads there ever after; the
|
||||
* note is created with a link back, so the document lists it in its backlinks.
|
||||
*/
|
||||
const makeNoteFor = useCallback(
|
||||
async (pageNumber, size, id, text) => {
|
||||
if (!pinLinks?.onCreate) return;
|
||||
const title = (text || "").trim().split("\n")[0]?.slice(0, 60) || `Notiz zu Seite ${pageNumber}`;
|
||||
const created = await pinLinks.onCreate(title);
|
||||
if (!created) return;
|
||||
// Linked before leaving, and the pending write is flushed on unmount.
|
||||
linkNote(pageNumber, size, id, created.id);
|
||||
setOpenPin(null);
|
||||
// Not `onOpenNote`: that one looks the note up in a list this note is not
|
||||
// in yet, and would quietly go nowhere.
|
||||
pinLinks.onOpenCreated?.(created.id, created.folderId);
|
||||
},
|
||||
[pinLinks, linkNote]
|
||||
);
|
||||
|
||||
const [pageNo, setPageNo] = useState(1);
|
||||
const [visible, setVisible] = useState(() => new Set([0, 1]));
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
@@ -741,6 +793,13 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
onMove: moveNote,
|
||||
onRemove: removeNote,
|
||||
numberOf: (id) => pinNumbers.get(id) ?? "",
|
||||
link: {
|
||||
search: pinLinks?.search,
|
||||
labelOf: pinLinks?.labelOf,
|
||||
onOpenNote: pinLinks?.onOpenNote,
|
||||
onCreate: (pinId, text) => makeNoteFor(i + 1, state.sizes[i], pinId, text),
|
||||
onLink: (pinId, noteId) => linkNote(i + 1, state.sizes[i], pinId, noteId),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -758,6 +817,13 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
onChange={(text) => changeNote(openSheet.page, openSheet.size, openSheet.note.id, text)}
|
||||
onRemove={() => removeNote(openSheet.page, openSheet.size, openSheet.note.id)}
|
||||
onClose={() => setOpenPin(null)}
|
||||
link={{
|
||||
search: pinLinks?.search,
|
||||
labelOf: pinLinks?.labelOf,
|
||||
onOpenNote: pinLinks?.onOpenNote,
|
||||
onCreate: (text) => makeNoteFor(openSheet.page, openSheet.size, openSheet.note.id, text),
|
||||
onLink: (noteId) => linkNote(openSheet.page, openSheet.size, openSheet.note.id, noteId),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user