Skip to content

Commit 9f81668

Browse files
authored
feat: add alerts when note is updated or created and won't be visible (#358)
feat: add alerts when note is updated or created and won't be visible
1 parent 5846bd7 commit 9f81668

File tree

7 files changed

+158
-59
lines changed

7 files changed

+158
-59
lines changed

src/components/NoteManager/NoteManager.tsx

Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { memo, useCallback, useContext, useEffect, useRef } from "react";
22
import "./NoteManager.css";
33
import type { ISynctexBlockId } from "@fluffylabs/links-metadata";
4-
import { cn } from "@fluffylabs/shared-ui";
4+
import { Alert, Button, cn } from "@fluffylabs/shared-ui";
55
import { twMerge } from "tailwind-merge";
66
import { useLatestCallback } from "../../hooks/useLatestCallback";
77
import { type ILocationContext, LocationContext } from "../LocationProvider/LocationProvider";
@@ -12,6 +12,7 @@ import { type ISelectionContext, SelectionContext } from "../SelectionProvider/S
1212
import { InactiveNoteSkeleton } from "./components/InactiveNoteSkeleton";
1313
import { NewNote } from "./components/NewNote";
1414
import { NotesList } from "./components/NotesList";
15+
import { useFilteredNoteAlert } from "./hooks/useFilteredNoteAlert";
1516
import { useNoteManagerNotes } from "./useNoteManagerNotes";
1617

1718
const DEFAULT_AUTHOR = "";
@@ -31,7 +32,7 @@ function Notes() {
3132
const {
3233
notesManagerNotes: notes,
3334
activeNotes,
34-
latestHandleAddNote,
35+
addNote,
3536
sectionTitlesLoaded,
3637
notesReady,
3738
deleteNote,
@@ -40,6 +41,7 @@ function Notes() {
4041
const { selectedBlocks, pageNumber, handleClearSelection } = useContext(SelectionContext) as ISelectionContext;
4142
const keepShowingNewNote = useRef<{ selectionEnd: ISynctexBlockId; selectionStart: ISynctexBlockId }>(undefined);
4243
const latestHandleClearSelection = useLatestCallback(handleClearSelection);
44+
const { noteAlertVisibilityState, triggerFilteredNoteAlert, closeNoteAlert } = useFilteredNoteAlert();
4345

4446
const memoizedHandleDeleteNote = useCallback(
4547
(note: IDecoratedNote) => {
@@ -53,9 +55,14 @@ function Notes() {
5355
(note: IDecoratedNote, newNote: IStorageNote) => {
5456
// NOTE(optimistic): intentional mutation for immediate UI feedback; be aware this bypasses immutability
5557
note.original.content = newNote.content;
56-
updateNote(note, newNote);
58+
note.original.labels = newNote.labels;
59+
const { isVisible } = updateNote(note, newNote);
60+
if (!isVisible) {
61+
handleClearSelection();
62+
triggerFilteredNoteAlert("visibleForUpdated");
63+
}
5764
},
58-
[updateNote],
65+
[updateNote, handleClearSelection, triggerFilteredNoteAlert],
5966
);
6067

6168
const handleNewNoteCancel = useCallback(() => {
@@ -84,13 +91,19 @@ function Notes() {
8491
labels,
8592
};
8693

87-
latestHandleAddNote.current(newNote);
94+
const { isVisible } = addNote(newNote);
95+
96+
if (!isVisible) {
97+
triggerFilteredNoteAlert("visibleForCreated");
98+
handleClearSelection();
99+
}
100+
88101
keepShowingNewNote.current = {
89102
selectionStart: locationParams.selectionStart,
90103
selectionEnd: locationParams.selectionEnd,
91104
};
92105
},
93-
[pageNumber, selectedBlocks, locationParams, latestHandleAddNote],
106+
[pageNumber, selectedBlocks, locationParams, addNote, handleClearSelection, triggerFilteredNoteAlert],
94107
);
95108

96109
const locationRef = useRef({ locationParams, setLocationParams });
@@ -133,44 +146,65 @@ function Notes() {
133146
}, [readyAndLoaded]);
134147

135148
return (
136-
<div className={cn("note-manager flex flex-col gap-2.5", !readyAndLoaded && "opacity-30 pointer-events-none")}>
137-
{locationParams.selectionEnd &&
138-
locationParams.selectionStart &&
139-
pageNumber !== null &&
140-
selectedBlocks.length > 0 &&
141-
!isActiveNotes &&
142-
(readyAndLoaded || areSelectionsEqual(locationParams, keepShowingNewNote.current)) && (
143-
<NewNote
144-
selectionStart={locationParams.selectionStart}
145-
selectionEnd={locationParams.selectionEnd}
146-
version={locationParams.version}
147-
onCancel={handleNewNoteCancel}
148-
onSave={handleAddNoteClick}
149-
/>
150-
)}
151-
152-
{!readyAndLoaded && notes.length === 0 && (
153-
<>
154-
<InactiveNoteSkeleton />
155-
<InactiveNoteSkeleton />
156-
<InactiveNoteSkeleton />
157-
<InactiveNoteSkeleton />
158-
</>
149+
<>
150+
{noteAlertVisibilityState !== "hidden" && (
151+
<Alert intent="warning">
152+
<Alert.Title>
153+
{noteAlertVisibilityState === "visibleForUpdated"
154+
? "Note hidden after update"
155+
: "Note hidden by label filter"}
156+
</Alert.Title>
157+
<div className="flex gap-4">
158+
<Alert.Text>
159+
{noteAlertVisibilityState === "visibleForUpdated"
160+
? "Updated note doesn't match active labels."
161+
: "Created note doesn't match active labels."}
162+
</Alert.Text>
163+
<Button variant="secondary" intent="warning" size="sm" className="self-end" onClick={closeNoteAlert}>
164+
Close
165+
</Button>
166+
</div>
167+
</Alert>
159168
)}
169+
<div className={cn("note-manager flex flex-col gap-2.5", !readyAndLoaded && "opacity-30 pointer-events-none")}>
170+
{locationParams.selectionEnd &&
171+
locationParams.selectionStart &&
172+
pageNumber !== null &&
173+
selectedBlocks.length > 0 &&
174+
!isActiveNotes &&
175+
(readyAndLoaded || areSelectionsEqual(locationParams, keepShowingNewNote.current)) && (
176+
<NewNote
177+
selectionStart={locationParams.selectionStart}
178+
selectionEnd={locationParams.selectionEnd}
179+
version={locationParams.version}
180+
onCancel={handleNewNoteCancel}
181+
onSave={handleAddNoteClick}
182+
/>
183+
)}
184+
185+
{!readyAndLoaded && notes.length === 0 && (
186+
<>
187+
<InactiveNoteSkeleton />
188+
<InactiveNoteSkeleton />
189+
<InactiveNoteSkeleton />
190+
<InactiveNoteSkeleton />
191+
</>
192+
)}
160193

161-
{readyAndLoaded && notes.length === 0 && (
162-
<div className="no-notes text-sidebar-foreground">No notes available</div>
163-
)}
194+
{readyAndLoaded && notes.length === 0 && (
195+
<div className="no-notes text-sidebar-foreground">No notes available</div>
196+
)}
164197

165-
{notes.length > 0 && (
166-
<MemoizedNotesList
167-
activeNotes={activeNotes}
168-
notes={notes}
169-
onEditNote={memoizedHandleUpdateNote}
170-
onDeleteNote={memoizedHandleDeleteNote}
171-
onSelectNote={memoizedHandleSelectNote}
172-
/>
173-
)}
174-
</div>
198+
{notes.length > 0 && (
199+
<MemoizedNotesList
200+
activeNotes={activeNotes}
201+
notes={notes}
202+
onEditNote={memoizedHandleUpdateNote}
203+
onDeleteNote={memoizedHandleDeleteNote}
204+
onSelectNote={memoizedHandleSelectNote}
205+
/>
206+
)}
207+
</div>
208+
</>
175209
);
176210
}

src/components/NoteManager/components/Note.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type NoteProps = {
2222
note: IDecoratedNote;
2323
sectionTitles: { sectionTitle: string; subSectionTitle: string };
2424
active: boolean;
25-
onEditNote: INotesContext["handleUpdateNote"];
25+
onEditNote(noteToReplace: IDecoratedNote, newNote: IStorageNote): void;
2626
onDeleteNote: INotesContext["handleDeleteNote"];
2727
onSelectNote: (note: IDecoratedNote, opts: { type: "currentVersion" | "originalVersion" | "close" }) => void;
2828
};

src/components/NoteManager/components/NoteContext.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { createContext, useContext } from "react";
2-
import type { INotesContext } from "../../NotesProvider/NotesProvider";
32
import type { IDecoratedNote } from "../../NotesProvider/types/DecoratedNote";
43
import type { IStorageNote } from "../../NotesProvider/types/StorageNote";
54

@@ -13,7 +12,7 @@ export type ISingleNoteContext = {
1312
handleCancelClick: () => void;
1413
handleNoteLabelsChange: (labels: string[]) => void;
1514
noteDirty: IStorageNote;
16-
onEditNote: INotesContext["handleUpdateNote"];
15+
onEditNote: (noteToReplace: IDecoratedNote, newNote: IStorageNote) => void;
1716
isEditing: boolean;
1817
noteOriginalVersionShort: string | undefined;
1918
originalVersionLink: string | undefined;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useCallback, useEffect, useRef, useState } from "react";
2+
3+
const FILTERED_NOTE_ALERT_MS = 6_000;
4+
5+
export const useFilteredNoteAlert = () => {
6+
const [noteAlertVisibilityState, setNoteAlertVisibilityState] = useState<
7+
"hidden" | "visibleForCreated" | "visibleForUpdated"
8+
>("hidden");
9+
const alertTimeoutRef = useRef<number | null>(null);
10+
11+
const triggerFilteredNoteAlert = useCallback((type: "visibleForCreated" | "visibleForUpdated") => {
12+
setNoteAlertVisibilityState(type);
13+
if (alertTimeoutRef.current) {
14+
window.clearTimeout(alertTimeoutRef.current);
15+
}
16+
alertTimeoutRef.current = window.setTimeout(() => {
17+
setNoteAlertVisibilityState("hidden");
18+
}, FILTERED_NOTE_ALERT_MS);
19+
}, []);
20+
21+
useEffect(() => {
22+
return () => {
23+
if (alertTimeoutRef.current) {
24+
window.clearTimeout(alertTimeoutRef.current);
25+
}
26+
};
27+
}, []);
28+
29+
const closeNoteAlert = useCallback(() => {
30+
setNoteAlertVisibilityState("hidden");
31+
if (alertTimeoutRef.current) {
32+
window.clearTimeout(alertTimeoutRef.current);
33+
}
34+
}, []);
35+
36+
return { noteAlertVisibilityState, triggerFilteredNoteAlert, closeNoteAlert };
37+
};

src/components/NoteManager/useNoteManagerNotes.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,19 @@ export const useNoteManagerNotes = () => {
2727
const latestDeleteNote = useLatestCallback(handleDeleteNote);
2828
const latestUpdateNote = useLatestCallback(handleUpdateNote);
2929

30+
const addNote = useCallback<INotesContext["handleAddNote"]>(
31+
(noteToAdd) => {
32+
const { isVisible } = latestHandleAddNote.current(noteToAdd);
33+
34+
return { isVisible };
35+
},
36+
[latestHandleAddNote],
37+
);
38+
3039
const updateNote = useCallback<INotesContext["handleUpdateNote"]>(
3140
(noteToReplace, newNote) => {
3241
metadataCacheByKey.current.delete(noteToReplace.key);
33-
latestUpdateNote.current(noteToReplace, newNote);
42+
return latestUpdateNote.current(noteToReplace, newNote);
3443
},
3544
[latestUpdateNote],
3645
);
@@ -106,7 +115,7 @@ export const useNoteManagerNotes = () => {
106115
sectionTitlesLoaded,
107116
notes,
108117
notesManagerNotes,
109-
latestHandleAddNote,
118+
addNote,
110119
deleteNote,
111120
updateNote,
112121
};

src/components/NotesProvider/NotesProvider.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export interface INotesContext {
2828
canRedo: boolean;
2929
remoteSources: IRemoteSource[];
3030
handleSetRemoteSources(r: IRemoteSource, remove?: true): void;
31-
handleAddNote(note: IStorageNote): void;
32-
handleUpdateNote(noteToReplace: IDecoratedNote, newNote: IStorageNote): void;
31+
handleAddNote(note: IStorageNote): { isVisible: boolean };
32+
handleUpdateNote(noteToReplace: IDecoratedNote, newNote: IStorageNote): { isVisible: boolean };
3333
handleDeleteNote(note: IDecoratedNote): void;
3434
handleUndo(): void;
3535
handleRedo(): void;
@@ -105,7 +105,7 @@ export function NotesProvider({ children }: INotesProviderProps) {
105105

106106
const allNotesReady = useMemo(() => localNotesReady && remoteNotesReady, [localNotesReady, remoteNotesReady]);
107107

108-
const { filteredNotes, labels, toggleLabel: handleToggleLabel } = useLabels(allNotes);
108+
const { filteredNotes, labels, toggleLabel: handleToggleLabel, isVisibleByActiveLabelsLatest } = useLabels(allNotes);
109109

110110
const handleSetRemoteSources = useCallback((newSource: IRemoteSource, remove?: true) => {
111111
setRemoteSources((prevRemoteSources) => {
@@ -148,27 +148,37 @@ export function NotesProvider({ children }: INotesProviderProps) {
148148
handleSetRemoteSources,
149149
handleToggleLabel,
150150
handleAddNote: useCallback(
151-
(note) =>
151+
(note) => {
152+
const isVisible = isVisibleByActiveLabelsLatest.current(note);
153+
152154
updateLocalNotes(localNotes, {
153155
...localNotes,
154156
notes: [note, ...localNotes.notes],
155-
}),
156-
[localNotes, updateLocalNotes],
157+
});
158+
159+
return { isVisible };
160+
},
161+
[localNotes, updateLocalNotes, isVisibleByActiveLabelsLatest],
157162
),
158163
handleUpdateNote: useCallback(
159164
(noteToReplace, newNote) => {
160165
if (noteToReplace.source === NoteSource.Remote) {
161166
console.warn("Refusing to edit remote note.", noteToReplace);
162-
return;
167+
return { isVisible: true };
163168
}
169+
170+
const isVisible = isVisibleByActiveLabelsLatest.current(newNote);
171+
164172
const updateIdx = localNotesDecorated.indexOf(noteToReplace);
165173
const newNotes = localNotes.notes.map((note, idx) => (updateIdx === idx ? newNote : note));
166174
updateLocalNotes(localNotes, {
167175
...localNotes,
168176
notes: newNotes,
169177
});
178+
179+
return { isVisible };
170180
},
171-
[localNotes, localNotesDecorated, updateLocalNotes],
181+
[localNotes, localNotesDecorated, updateLocalNotes, isVisibleByActiveLabelsLatest],
172182
),
173183
handleDeleteNote: useCallback(
174184
(noteToDelete) => {

src/components/NotesProvider/hooks/useLabels.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { UnPrefixedLabel } from "@fluffylabs/links-metadata";
2-
import { useCallback, useEffect, useMemo, useState } from "react";
2+
import { type RefObject, useCallback, useEffect, useMemo, useState } from "react";
3+
import { useLatestCallback } from "../../../hooks/useLatestCallback";
34
import { LABEL_LOCAL, LABEL_REMOTE } from "../consts/labels";
45
import { type IDecoratedNote, NoteSource, isDecoratedNote } from "../types/DecoratedNote";
56
import type { IStorageNote } from "../types/StorageNote";
@@ -119,6 +120,7 @@ export function useLabels(allNotes: IDecoratedNote[]): {
119120
filteredNotes: IDecoratedNote[];
120121
labels: ILabelTreeNode[];
121122
toggleLabel: (label: ILabelTreeNode) => void;
123+
isVisibleByActiveLabelsLatest: RefObject<(note: IDecoratedNote | IStorageNote) => boolean>;
122124
} {
123125
const [storageLabels, setStorageLabels] = useState<IStorageLabel[]>([]);
124126
const [labels, setLabels] = useState<ILabelTreeNode[]>(initialEmptyArray as ILabelTreeNode[]);
@@ -226,12 +228,20 @@ export function useLabels(allNotes: IDecoratedNote[]): {
226228
});
227229
}, [allNotes, storageActivity]);
228230

231+
const activeLabels = useMemo(() => {
232+
return labels.filter((label) => label.isActive).map((label) => label.prefixedLabel);
233+
}, [labels]);
234+
229235
// filter notes when labels are changing
230236
const filteredNotes = useMemo(() => {
231-
const activeLabels = labels.filter((label) => label.isActive).map((label) => label.prefixedLabel);
232237
// filter out notes
233238
return getFilteredNotes(allNotes, activeLabels);
234-
}, [allNotes, labels]);
239+
}, [allNotes, activeLabels]);
240+
241+
const isVisibleByActiveLabelsLatest = useLatestCallback((note: IStorageNote | IDecoratedNote) => {
242+
const filteringResult = getFilteredNotes([note], activeLabels);
243+
return filteringResult.length > 0;
244+
});
235245

236-
return { filteredNotes, labels, toggleLabel };
246+
return { filteredNotes, labels, toggleLabel, isVisibleByActiveLabelsLatest };
237247
}

0 commit comments

Comments
 (0)