diff --git a/src/components/NoteManager/NoteManager.tsx b/src/components/NoteManager/NoteManager.tsx
index 8a643bd2..660411a8 100644
--- a/src/components/NoteManager/NoteManager.tsx
+++ b/src/components/NoteManager/NoteManager.tsx
@@ -1,7 +1,7 @@
import { memo, useCallback, useContext, useEffect, useRef } from "react";
import "./NoteManager.css";
import type { ISynctexBlockId } from "@fluffylabs/links-metadata";
-import { cn } from "@fluffylabs/shared-ui";
+import { Alert, Button, cn } from "@fluffylabs/shared-ui";
import { twMerge } from "tailwind-merge";
import { useLatestCallback } from "../../hooks/useLatestCallback";
import { type ILocationContext, LocationContext } from "../LocationProvider/LocationProvider";
@@ -12,6 +12,7 @@ import { type ISelectionContext, SelectionContext } from "../SelectionProvider/S
import { InactiveNoteSkeleton } from "./components/InactiveNoteSkeleton";
import { NewNote } from "./components/NewNote";
import { NotesList } from "./components/NotesList";
+import { useFilteredNoteAlert } from "./hooks/useFilteredNoteAlert";
import { useNoteManagerNotes } from "./useNoteManagerNotes";
const DEFAULT_AUTHOR = "";
@@ -31,7 +32,7 @@ function Notes() {
const {
notesManagerNotes: notes,
activeNotes,
- latestHandleAddNote,
+ addNote,
sectionTitlesLoaded,
notesReady,
deleteNote,
@@ -40,6 +41,7 @@ function Notes() {
const { selectedBlocks, pageNumber, handleClearSelection } = useContext(SelectionContext) as ISelectionContext;
const keepShowingNewNote = useRef<{ selectionEnd: ISynctexBlockId; selectionStart: ISynctexBlockId }>(undefined);
const latestHandleClearSelection = useLatestCallback(handleClearSelection);
+ const { noteAlertVisibilityState, triggerFilteredNoteAlert, closeNoteAlert } = useFilteredNoteAlert();
const memoizedHandleDeleteNote = useCallback(
(note: IDecoratedNote) => {
@@ -53,9 +55,14 @@ function Notes() {
(note: IDecoratedNote, newNote: IStorageNote) => {
// NOTE(optimistic): intentional mutation for immediate UI feedback; be aware this bypasses immutability
note.original.content = newNote.content;
- updateNote(note, newNote);
+ note.original.labels = newNote.labels;
+ const { isVisible } = updateNote(note, newNote);
+ if (!isVisible) {
+ handleClearSelection();
+ triggerFilteredNoteAlert("visibleForUpdated");
+ }
},
- [updateNote],
+ [updateNote, handleClearSelection, triggerFilteredNoteAlert],
);
const handleNewNoteCancel = useCallback(() => {
@@ -84,13 +91,19 @@ function Notes() {
labels,
};
- latestHandleAddNote.current(newNote);
+ const { isVisible } = addNote(newNote);
+
+ if (!isVisible) {
+ triggerFilteredNoteAlert("visibleForCreated");
+ handleClearSelection();
+ }
+
keepShowingNewNote.current = {
selectionStart: locationParams.selectionStart,
selectionEnd: locationParams.selectionEnd,
};
},
- [pageNumber, selectedBlocks, locationParams, latestHandleAddNote],
+ [pageNumber, selectedBlocks, locationParams, addNote, handleClearSelection, triggerFilteredNoteAlert],
);
const locationRef = useRef({ locationParams, setLocationParams });
@@ -133,44 +146,65 @@ function Notes() {
}, [readyAndLoaded]);
return (
-
- {locationParams.selectionEnd &&
- locationParams.selectionStart &&
- pageNumber !== null &&
- selectedBlocks.length > 0 &&
- !isActiveNotes &&
- (readyAndLoaded || areSelectionsEqual(locationParams, keepShowingNewNote.current)) && (
-
- )}
-
- {!readyAndLoaded && notes.length === 0 && (
- <>
-
-
-
-
- >
+ <>
+ {noteAlertVisibilityState !== "hidden" && (
+
+
+ {noteAlertVisibilityState === "visibleForUpdated"
+ ? "Note hidden after update"
+ : "Note hidden by label filter"}
+
+
+
+ {noteAlertVisibilityState === "visibleForUpdated"
+ ? "Updated note doesn't match active labels."
+ : "Created note doesn't match active labels."}
+
+
+
+
)}
+
+ {locationParams.selectionEnd &&
+ locationParams.selectionStart &&
+ pageNumber !== null &&
+ selectedBlocks.length > 0 &&
+ !isActiveNotes &&
+ (readyAndLoaded || areSelectionsEqual(locationParams, keepShowingNewNote.current)) && (
+
+ )}
+
+ {!readyAndLoaded && notes.length === 0 && (
+ <>
+
+
+
+
+ >
+ )}
- {readyAndLoaded && notes.length === 0 && (
-
No notes available
- )}
+ {readyAndLoaded && notes.length === 0 && (
+
No notes available
+ )}
- {notes.length > 0 && (
-
- )}
-
+ {notes.length > 0 && (
+
+ )}
+
+ >
);
}
diff --git a/src/components/NoteManager/components/Note.tsx b/src/components/NoteManager/components/Note.tsx
index e9cfba98..20c6146a 100644
--- a/src/components/NoteManager/components/Note.tsx
+++ b/src/components/NoteManager/components/Note.tsx
@@ -22,7 +22,7 @@ type NoteProps = {
note: IDecoratedNote;
sectionTitles: { sectionTitle: string; subSectionTitle: string };
active: boolean;
- onEditNote: INotesContext["handleUpdateNote"];
+ onEditNote(noteToReplace: IDecoratedNote, newNote: IStorageNote): void;
onDeleteNote: INotesContext["handleDeleteNote"];
onSelectNote: (note: IDecoratedNote, opts: { type: "currentVersion" | "originalVersion" | "close" }) => void;
};
diff --git a/src/components/NoteManager/components/NoteContext.tsx b/src/components/NoteManager/components/NoteContext.tsx
index 76807cdc..965b1e76 100644
--- a/src/components/NoteManager/components/NoteContext.tsx
+++ b/src/components/NoteManager/components/NoteContext.tsx
@@ -1,5 +1,4 @@
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";
@@ -13,7 +12,7 @@ export type ISingleNoteContext = {
handleCancelClick: () => void;
handleNoteLabelsChange: (labels: string[]) => void;
noteDirty: IStorageNote;
- onEditNote: INotesContext["handleUpdateNote"];
+ onEditNote: (noteToReplace: IDecoratedNote, newNote: IStorageNote) => void;
isEditing: boolean;
noteOriginalVersionShort: string | undefined;
originalVersionLink: string | undefined;
diff --git a/src/components/NoteManager/hooks/useFilteredNoteAlert.ts b/src/components/NoteManager/hooks/useFilteredNoteAlert.ts
new file mode 100644
index 00000000..75fc1fa1
--- /dev/null
+++ b/src/components/NoteManager/hooks/useFilteredNoteAlert.ts
@@ -0,0 +1,37 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+
+const FILTERED_NOTE_ALERT_MS = 6_000;
+
+export const useFilteredNoteAlert = () => {
+ const [noteAlertVisibilityState, setNoteAlertVisibilityState] = useState<
+ "hidden" | "visibleForCreated" | "visibleForUpdated"
+ >("hidden");
+ const alertTimeoutRef = useRef(null);
+
+ const triggerFilteredNoteAlert = useCallback((type: "visibleForCreated" | "visibleForUpdated") => {
+ setNoteAlertVisibilityState(type);
+ if (alertTimeoutRef.current) {
+ window.clearTimeout(alertTimeoutRef.current);
+ }
+ alertTimeoutRef.current = window.setTimeout(() => {
+ setNoteAlertVisibilityState("hidden");
+ }, FILTERED_NOTE_ALERT_MS);
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ if (alertTimeoutRef.current) {
+ window.clearTimeout(alertTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ const closeNoteAlert = useCallback(() => {
+ setNoteAlertVisibilityState("hidden");
+ if (alertTimeoutRef.current) {
+ window.clearTimeout(alertTimeoutRef.current);
+ }
+ }, []);
+
+ return { noteAlertVisibilityState, triggerFilteredNoteAlert, closeNoteAlert };
+};
diff --git a/src/components/NoteManager/useNoteManagerNotes.ts b/src/components/NoteManager/useNoteManagerNotes.ts
index 71e9d685..a97d48bb 100644
--- a/src/components/NoteManager/useNoteManagerNotes.ts
+++ b/src/components/NoteManager/useNoteManagerNotes.ts
@@ -27,10 +27,19 @@ export const useNoteManagerNotes = () => {
const latestDeleteNote = useLatestCallback(handleDeleteNote);
const latestUpdateNote = useLatestCallback(handleUpdateNote);
+ const addNote = useCallback(
+ (noteToAdd) => {
+ const { isVisible } = latestHandleAddNote.current(noteToAdd);
+
+ return { isVisible };
+ },
+ [latestHandleAddNote],
+ );
+
const updateNote = useCallback(
(noteToReplace, newNote) => {
metadataCacheByKey.current.delete(noteToReplace.key);
- latestUpdateNote.current(noteToReplace, newNote);
+ return latestUpdateNote.current(noteToReplace, newNote);
},
[latestUpdateNote],
);
@@ -106,7 +115,7 @@ export const useNoteManagerNotes = () => {
sectionTitlesLoaded,
notes,
notesManagerNotes,
- latestHandleAddNote,
+ addNote,
deleteNote,
updateNote,
};
diff --git a/src/components/NotesProvider/NotesProvider.tsx b/src/components/NotesProvider/NotesProvider.tsx
index 8e8b2562..126de153 100644
--- a/src/components/NotesProvider/NotesProvider.tsx
+++ b/src/components/NotesProvider/NotesProvider.tsx
@@ -28,8 +28,8 @@ export interface INotesContext {
canRedo: boolean;
remoteSources: IRemoteSource[];
handleSetRemoteSources(r: IRemoteSource, remove?: true): void;
- handleAddNote(note: IStorageNote): void;
- handleUpdateNote(noteToReplace: IDecoratedNote, newNote: IStorageNote): void;
+ handleAddNote(note: IStorageNote): { isVisible: boolean };
+ handleUpdateNote(noteToReplace: IDecoratedNote, newNote: IStorageNote): { isVisible: boolean };
handleDeleteNote(note: IDecoratedNote): void;
handleUndo(): void;
handleRedo(): void;
@@ -105,7 +105,7 @@ export function NotesProvider({ children }: INotesProviderProps) {
const allNotesReady = useMemo(() => localNotesReady && remoteNotesReady, [localNotesReady, remoteNotesReady]);
- const { filteredNotes, labels, toggleLabel: handleToggleLabel } = useLabels(allNotes);
+ const { filteredNotes, labels, toggleLabel: handleToggleLabel, isVisibleByActiveLabelsLatest } = useLabels(allNotes);
const handleSetRemoteSources = useCallback((newSource: IRemoteSource, remove?: true) => {
setRemoteSources((prevRemoteSources) => {
@@ -148,27 +148,37 @@ export function NotesProvider({ children }: INotesProviderProps) {
handleSetRemoteSources,
handleToggleLabel,
handleAddNote: useCallback(
- (note) =>
+ (note) => {
+ const isVisible = isVisibleByActiveLabelsLatest.current(note);
+
updateLocalNotes(localNotes, {
...localNotes,
notes: [note, ...localNotes.notes],
- }),
- [localNotes, updateLocalNotes],
+ });
+
+ return { isVisible };
+ },
+ [localNotes, updateLocalNotes, isVisibleByActiveLabelsLatest],
),
handleUpdateNote: useCallback(
(noteToReplace, newNote) => {
if (noteToReplace.source === NoteSource.Remote) {
console.warn("Refusing to edit remote note.", noteToReplace);
- return;
+ return { isVisible: true };
}
+
+ const isVisible = isVisibleByActiveLabelsLatest.current(newNote);
+
const updateIdx = localNotesDecorated.indexOf(noteToReplace);
const newNotes = localNotes.notes.map((note, idx) => (updateIdx === idx ? newNote : note));
updateLocalNotes(localNotes, {
...localNotes,
notes: newNotes,
});
+
+ return { isVisible };
},
- [localNotes, localNotesDecorated, updateLocalNotes],
+ [localNotes, localNotesDecorated, updateLocalNotes, isVisibleByActiveLabelsLatest],
),
handleDeleteNote: useCallback(
(noteToDelete) => {
diff --git a/src/components/NotesProvider/hooks/useLabels.ts b/src/components/NotesProvider/hooks/useLabels.ts
index 2b0efb0e..5abd4761 100644
--- a/src/components/NotesProvider/hooks/useLabels.ts
+++ b/src/components/NotesProvider/hooks/useLabels.ts
@@ -1,5 +1,6 @@
import type { UnPrefixedLabel } from "@fluffylabs/links-metadata";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { type RefObject, useCallback, useEffect, useMemo, useState } from "react";
+import { useLatestCallback } from "../../../hooks/useLatestCallback";
import { LABEL_LOCAL, LABEL_REMOTE } from "../consts/labels";
import { type IDecoratedNote, NoteSource, isDecoratedNote } from "../types/DecoratedNote";
import type { IStorageNote } from "../types/StorageNote";
@@ -119,6 +120,7 @@ export function useLabels(allNotes: IDecoratedNote[]): {
filteredNotes: IDecoratedNote[];
labels: ILabelTreeNode[];
toggleLabel: (label: ILabelTreeNode) => void;
+ isVisibleByActiveLabelsLatest: RefObject<(note: IDecoratedNote | IStorageNote) => boolean>;
} {
const [storageLabels, setStorageLabels] = useState([]);
const [labels, setLabels] = useState(initialEmptyArray as ILabelTreeNode[]);
@@ -226,12 +228,20 @@ export function useLabels(allNotes: IDecoratedNote[]): {
});
}, [allNotes, storageActivity]);
+ const activeLabels = useMemo(() => {
+ return labels.filter((label) => label.isActive).map((label) => label.prefixedLabel);
+ }, [labels]);
+
// filter notes when labels are changing
const filteredNotes = useMemo(() => {
- const activeLabels = labels.filter((label) => label.isActive).map((label) => label.prefixedLabel);
// filter out notes
return getFilteredNotes(allNotes, activeLabels);
- }, [allNotes, labels]);
+ }, [allNotes, activeLabels]);
+
+ const isVisibleByActiveLabelsLatest = useLatestCallback((note: IStorageNote | IDecoratedNote) => {
+ const filteringResult = getFilteredNotes([note], activeLabels);
+ return filteringResult.length > 0;
+ });
- return { filteredNotes, labels, toggleLabel };
+ return { filteredNotes, labels, toggleLabel, isVisibleByActiveLabelsLatest };
}