Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions src/components/NoteManager/NoteManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { cn } from "@fluffylabs/shared-ui";
import { twMerge } from "tailwind-merge";
import { useLatestCallback } from "../../hooks/useLatestCallback";
import { type ILocationContext, LocationContext } from "../LocationProvider/LocationProvider";
import { type INotesContext, NotesContext } from "../NotesProvider/NotesProvider";
import type { IDecoratedNote } from "../NotesProvider/types/DecoratedNote";
import type { IStorageNote } from "../NotesProvider/types/StorageNote";
import { areSelectionsEqual } from "../NotesProvider/utils/areSelectionsEqual";
import { type ISelectionContext, SelectionContext } from "../SelectionProvider/SelectionProvider";
import { InactiveNoteSkeleton } from "./components/InactiveNoteSkeleton";
import { NewNote } from "./components/NewNote";
import { NotesList } from "./components/NotesList";
import { useNoteManagerNotes } from "./useNoteManagerNotes";

const DEFAULT_AUTHOR = "";

Expand All @@ -28,26 +28,30 @@ const MemoizedNotesList = memo(NotesList);

function Notes() {
const { locationParams, setLocationParams } = useContext(LocationContext) as ILocationContext;
const { notesReady, activeNotes, notes, handleAddNote, handleDeleteNote, handleUpdateNote } = useContext(
NotesContext,
) as INotesContext;
const {
notesManagerNotes: notes,
activeNotes,
latestDeleteNote,
latestHandleAddNote,
latestUpdateNote,
sectionTitlesLoaded,
notesReady,
} = useNoteManagerNotes();
const { selectedBlocks, pageNumber, handleClearSelection } = useContext(SelectionContext) as ISelectionContext;
const keepShowingNewNote = useRef<{ selectionEnd: ISynctexBlockId; selectionStart: ISynctexBlockId }>(undefined);

const latestHandleAddNote = useLatestCallback(handleAddNote);
const latestDeleteNote = useLatestCallback(handleDeleteNote);
const latestUpdateNote = useLatestCallback(handleUpdateNote);
const latestHandleClearSelection = useLatestCallback(handleClearSelection);

const memoizedHandleDeleteNote = useCallback(
(note: IDecoratedNote) => {
latestDeleteNote.current(note);
handleClearSelection();
latestHandleClearSelection.current();
},
[handleClearSelection, latestDeleteNote],
[latestHandleClearSelection, latestDeleteNote],
);

const memoizedHandleUpdateNote = useCallback(
(note: IDecoratedNote, newNote: IStorageNote) => {
note.original.content = newNote.content;
latestUpdateNote.current(note, newNote);
},
[latestUpdateNote],
Expand Down Expand Up @@ -117,22 +121,24 @@ function Notes() {
[],
);

const isActiveNotes = notes.some((note) => activeNotes.has(note));
const isActiveNotes = notes.some((note) => activeNotes.has(note.noteObject));

const readyAndLoaded = notesReady && sectionTitlesLoaded;

useEffect(() => {
if (notesReady) {
if (readyAndLoaded) {
keepShowingNewNote.current = undefined;
}
}, [notesReady]);
}, [readyAndLoaded]);

return (
<div className={cn("note-manager flex flex-col gap-2.5", !notesReady && "opacity-30 pointer-events-none")}>
<div className={cn("note-manager flex flex-col gap-2.5", !readyAndLoaded && "opacity-30 pointer-events-none")}>
{locationParams.selectionEnd &&
locationParams.selectionStart &&
pageNumber !== null &&
selectedBlocks.length > 0 &&
!isActiveNotes &&
(notesReady || areSelectionsEqual(locationParams, keepShowingNewNote.current)) && (
(readyAndLoaded || areSelectionsEqual(locationParams, keepShowingNewNote.current)) && (
<NewNote
selectionStart={locationParams.selectionStart}
selectionEnd={locationParams.selectionEnd}
Expand All @@ -142,7 +148,7 @@ function Notes() {
/>
)}

{!notesReady && notes.length === 0 && (
{!readyAndLoaded && notes.length === 0 && (
<>
<InactiveNoteSkeleton />
<InactiveNoteSkeleton />
Expand All @@ -151,7 +157,9 @@ function Notes() {
</>
)}

{notesReady && notes.length === 0 && <div className="no-notes text-sidebar-foreground">No notes available</div>}
{readyAndLoaded && notes.length === 0 && (
<div className="no-notes text-sidebar-foreground">No notes available</div>
)}

{notes.length > 0 && (
<MemoizedNotesList
Expand Down
28 changes: 10 additions & 18 deletions src/components/NoteManager/components/NewNote.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ISynctexBlockId } from "@fluffylabs/links-metadata";
import { Button } from "@fluffylabs/shared-ui";
import { type ChangeEvent, type ChangeEventHandler, useCallback, useMemo, useRef, useState } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { useLatestCallback } from "../../../hooks/useLatestCallback";
import { validateMath } from "../../../utils/validateMath";
import { type IDecoratedNote, NoteSource } from "../../NotesProvider/types/DecoratedNote";
Expand All @@ -18,7 +18,6 @@ type NewNoteProps = {
};

export const NewNote = ({ version, onCancel, onSave, selectionStart, selectionEnd }: NewNoteProps) => {
const [noteContent, setNoteContent] = useState("");
const [noteContentError, setNoteContentError] = useState<string | null>(null);
const [labels, setLabels] = useState<string[]>(["local"]);

Expand All @@ -30,6 +29,8 @@ export const NewNote = ({ version, onCancel, onSave, selectionStart, selectionEn
}, [latestOnCancel]);

const handleSaveClick = useCallback(() => {
const noteContent = textAreaRef.current?.value ?? "";

const mathValidationError = validateMath(noteContent);
if (mathValidationError) {
setNoteContentError(mathValidationError);
Expand All @@ -41,25 +42,19 @@ export const NewNote = ({ version, onCancel, onSave, selectionStart, selectionEn
}

latestOnSave.current({ noteContent, labels });
}, [noteContent, latestOnSave, labels]);
}, [latestOnSave, labels]);

const currentVersionLink = "";
const originalVersionLink = "";

const handleNoteContentChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setNoteContent(event.target.value);
setNoteContentError(null);
}, []);

const noteDirty = useDumbDirtyNoteObj({ noteContent, labels, version });
const noteDirty = useDumbDirtyNoteObj({ labels, version });
const note = useDumbNoteObj({ version, selectionStart, selectionEnd });

const noteLayoutContext = useNewNoteLayoutContext({
note,
noteDirty,
currentVersionLink,
handleCancelClick,
handleNoteContentChange,
handleNoteLabelsChange: setLabels,
handleSaveClick,
originalVersionLink,
Expand Down Expand Up @@ -140,35 +135,34 @@ const useDumbNoteObj = ({
);

const useDumbDirtyNoteObj = ({
noteContent,
labels,
version,
}: {
noteContent: string;
labels: string[];
version: string;
}) =>
useMemo(
() =>
({
author: "",
content: noteContent,
content: "",
labels,
date: 0,
noteVersion: 3,
selectionEnd: createDumbISyntexBlockId(),
selectionStart: createDumbISyntexBlockId(),
version,
}) satisfies INoteV3,
[noteContent, version, labels],
[version, labels],
);

const sectionTitles = { sectionTitle: "", subSectionTitle: "" };

const useNewNoteLayoutContext = ({
handleSaveClick,
handleCancelClick,
note,
noteDirty,
handleNoteContentChange,
handleNoteLabelsChange,
currentVersionLink,
originalVersionLink,
Expand All @@ -177,7 +171,6 @@ const useNewNoteLayoutContext = ({
handleCancelClick: () => void;
note: IDecoratedNote;
noteDirty: INoteV3;
handleNoteContentChange: ChangeEventHandler<HTMLTextAreaElement>;
handleNoteLabelsChange: (labels: string[]) => void;
currentVersionLink: string;
originalVersionLink: string;
Expand All @@ -196,16 +189,15 @@ const useNewNoteLayoutContext = ({
isEditing: true,
note,
noteDirty,
handleNoteContentChange,
handleNoteLabelsChange,
handleSelectNote: () => {},
noteOriginalVersionShort: "",
currentVersionLink,
originalVersionLink,
sectionTitles,
}) satisfies ISingleNoteContext,
[
noteDirty,
handleNoteContentChange,
handleNoteLabelsChange,
note,
handleCancelClick,
Expand Down
37 changes: 26 additions & 11 deletions src/components/NoteManager/components/Note.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button } from "@fluffylabs/shared-ui";
import { type ChangeEvent, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { validateMath } from "../../../utils/validateMath";
import { useVersionContext } from "../../LocationProvider/VersionProvider";
import { useGetLocationParamsToHash } from "../../LocationProvider/hooks/useGetLocationParamsToHash";
Expand All @@ -20,14 +20,16 @@ export type NotesItem = {
type NoteProps = {
ref?: RefObject<HTMLDivElement | null>;
note: IDecoratedNote;
sectionTitles: { sectionTitle: string; subSectionTitle: string };
active: boolean;
onEditNote: INotesContext["handleUpdateNote"];
onDeleteNote: INotesContext["handleDeleteNote"];
onSelectNote: (note: IDecoratedNote, opts: { type: "currentVersion" | "originalVersion" | "close" }) => void;
};

export function Note({ ref, note, active = false, onEditNote, onDeleteNote, onSelectNote }: NoteProps) {
export function Note({ ref, note, active = false, sectionTitles, onEditNote, onDeleteNote, onSelectNote }: NoteProps) {
const [isEditing, setIsEditing] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);

const [noteDirty, setNoteDirty] = useState<IStorageNote>({
...note.original,
Expand All @@ -41,14 +43,22 @@ export function Note({ ref, note, active = false, onEditNote, onDeleteNote, onSe
const isEditable = note.source !== NoteSource.Remote;

const handleSaveClick = useCallback(() => {
const mathValidationError = validateMath(noteDirty.content);
const content = textAreaRef.current?.value ?? "";
const mathValidationError = validateMath(content);
setNoteContentError("");

if (mathValidationError) {
setNoteContentError(mathValidationError);
return;
}

if (!content.trim()) {
setNoteContentError("Note content cannot be empty");
return;
}

noteDirty.content = content;

onEditNote(note, noteDirty);
setIsEditing(false);
}, [note, noteDirty, onEditNote]);
Expand All @@ -66,10 +76,6 @@ export function Note({ ref, note, active = false, onEditNote, onDeleteNote, onSe
setNoteDirty((prevNoteDirty) => ({ ...prevNoteDirty, labels }));
}, []);

const handleNoteContentChange = useCallback((ev: ChangeEvent<HTMLTextAreaElement>) => {
setNoteDirty((prev) => ({ ...prev, content: ev.currentTarget.value }));
}, []);

const handleDeleteClick = useCallback(() => {
onDeleteNote(note);
}, [note, onDeleteNote]);
Expand Down Expand Up @@ -159,12 +165,12 @@ export function Note({ ref, note, active = false, onEditNote, onDeleteNote, onSe
onEditNote,
isEditing,
noteDirty,
handleNoteContentChange,
handleNoteLabelsChange,
handleSelectNote: memoizedOnSelectNote,
noteOriginalVersionShort,
currentVersionLink,
originalVersionLink: originalLink,
sectionTitles,
}) satisfies ISingleNoteContext,
[
active,
Expand All @@ -176,18 +182,19 @@ export function Note({ ref, note, active = false, onEditNote, onDeleteNote, onSe
onEditNote,
isEditing,
noteDirty,
handleNoteContentChange,
handleNoteLabelsChange,
memoizedOnSelectNote,
noteOriginalVersionShort,
currentVersionLink,
originalLink,
sectionTitles,
],
);

const [isHovered, setIsHovered] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);

const mousePositionRef = useRef({ x: 0, y: 0 });

const internalNoteRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -224,6 +231,14 @@ export function Note({ ref, note, active = false, onEditNote, onDeleteNote, onSe
}
};

useEffect(() => {
if (!active) {
setIsHovered(false);
setIsFocused(false);
setIsDropdownOpen(false);
}
}, [active]);

return (
<NoteLayout.Root value={noteLayoutContext}>
<NoteContainer
Expand Down Expand Up @@ -251,7 +266,7 @@ export function Note({ ref, note, active = false, onEditNote, onDeleteNote, onSe
<div className="flex flex-col gap-2">
{!active && (
<>
<NoteLink note={note} active={false} />
<NoteLink showTooltip={isHovered} />
<NoteLayout.Text />
</>
)}
Expand All @@ -266,7 +281,7 @@ export function Note({ ref, note, active = false, onEditNote, onDeleteNote, onSe
<>
<>
<NoteLayout.SelectedText />
<NoteLayout.TextArea className={noteContentError ? "error" : ""} />
<NoteLayout.TextArea className={noteContentError ? "error" : ""} ref={textAreaRef} />
{noteContentError ? <div className="validation-message">{noteContentError}</div> : null}
<NoteLayout.Labels />
<div className="actions gap-2">
Expand Down
4 changes: 2 additions & 2 deletions src/components/NoteManager/components/NoteContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ChangeEvent, createContext, useContext } from "react";
import { createContext, useContext } from "react";
import type { INotesContext } from "../../NotesProvider/NotesProvider";
import type { IDecoratedNote } from "../../NotesProvider/types/DecoratedNote";
import type { IStorageNote } from "../../NotesProvider/types/StorageNote";
Expand All @@ -11,14 +11,14 @@ export type ISingleNoteContext = {
handleEditClick: () => void;
handleSaveClick: () => void;
handleCancelClick: () => void;
handleNoteContentChange: (ev: ChangeEvent<HTMLTextAreaElement>) => void;
handleNoteLabelsChange: (labels: string[]) => void;
noteDirty: IStorageNote;
onEditNote: INotesContext["handleUpdateNote"];
isEditing: boolean;
noteOriginalVersionShort: string | undefined;
originalVersionLink: string | undefined;
currentVersionLink: string | undefined;
sectionTitles: { sectionTitle: string; subSectionTitle: string };
};

export const noteContext = createContext<ISingleNoteContext | null>(null);
Expand Down
Loading