diff --git a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx index 335a7839..84bb2dd1 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -13,10 +13,12 @@ import { Overlayer } from 'foliate-js/overlayer.js'; import { useEnv } from '@/context/EnvContext'; import { BookNote, HighlightColor, HighlightStyle } from '@/types/book'; import { uniqueId } from '@/utils/misc'; +import { useBookDataStore } from '@/store/bookDataStore'; import { useSettingsStore } from '@/store/settingsStore'; import { useReaderStore } from '@/store/readerStore'; import { useNotebookStore } from '@/store/notebookStore'; import { useFoliateEvents } from '../../hooks/useFoliateEvents'; +import { useNotesSync } from '../../hooks/useNotesSync'; import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/sel'; import { eventDispatcher } from '@/utils/event'; import Toast from '@/components/Toast'; @@ -24,7 +26,6 @@ import AnnotationPopup from './AnnotationPopup'; import WiktionaryPopup from './WiktionaryPopup'; import WikipediaPopup from './WikipediaPopup'; import DeepLPopup from './DeepLPopup'; -import { useBookDataStore } from '@/store/bookDataStore'; const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const { envConfig } = useEnv(); @@ -33,6 +34,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const { getProgress, getView, getViewsById, getViewSettings } = useReaderStore(); const { isNotebookPinned, isNotebookVisible } = useNotebookStore(); const { setNotebookVisible, setNotebookNewAnnotation } = useNotebookStore(); + + useNotesSync(bookKey); + const config = getConfig(bookKey)!; const progress = getProgress(bookKey)!; const bookData = getBookData(bookKey)!; @@ -198,6 +202,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const { booknotes = [] } = config; const annotations = booknotes.filter( (item) => + !item.deletedAt && item.type === 'annotation' && item.style && CFI.compare(item.cfi, start) >= 0 && @@ -274,7 +279,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { annotations[existingIndex] = annotation; views.forEach((view) => view?.addAnnotation(annotation)); } else { - annotations.splice(existingIndex, 1); + annotations[existingIndex]!.deletedAt = Date.now(); setShowAnnotPopup(false); } } else { diff --git a/apps/readest-app/src/app/reader/hooks/useNotesSync.ts b/apps/readest-app/src/app/reader/hooks/useNotesSync.ts new file mode 100644 index 00000000..94659302 --- /dev/null +++ b/apps/readest-app/src/app/reader/hooks/useNotesSync.ts @@ -0,0 +1,64 @@ +import { useEffect, useRef } from 'react'; +import { useAuth } from '@/context/AuthContext'; +import { useSync } from '@/hooks/useSync'; +import { useBookDataStore } from '@/store/bookDataStore'; +import { SYNC_NOTES_INTERVAL_SEC } from '@/services/constants'; + +export const useNotesSync = (bookKey: string) => { + const { user } = useAuth(); + const { syncedNotes, syncNotes, lastSyncedAtNotes } = useSync(bookKey); + const { getConfig, setConfig } = useBookDataStore(); + + const config = getConfig(bookKey); + const bookHash = bookKey.split('-')[0]!; + + useEffect(() => { + if (!config || !user) return; + syncNotes([], bookHash, 'pull'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const lastSyncTime = useRef(0); + const syncTimeoutRef = useRef | null>(null); + useEffect(() => { + if (!config || !user) return; + const bookNotes = config.booknotes ?? []; + const newNotes = bookNotes.filter( + (note) => lastSyncedAtNotes < note.updatedAt || lastSyncedAtNotes < (note.deletedAt ?? 0), + ); + newNotes.forEach((note) => { + note.bookHash = bookHash; + }); + const now = Date.now(); + const timeSinceLastSync = now - lastSyncTime.current; + if (timeSinceLastSync > SYNC_NOTES_INTERVAL_SEC * 1000) { + lastSyncTime.current = now; + syncNotes(newNotes, bookHash, 'both'); + } else { + if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current); + syncTimeoutRef.current = setTimeout( + () => { + lastSyncTime.current = Date.now(); + syncNotes(newNotes, bookHash, 'both'); + syncTimeoutRef.current = null; + }, + SYNC_NOTES_INTERVAL_SEC * 1000 - timeSinceLastSync, + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config]); + + useEffect(() => { + if (syncedNotes?.length && config) { + const newNotes = syncedNotes.filter((note) => note.bookHash === bookHash); + if (!newNotes.length) return; + const oldNotes = config.booknotes ?? []; + const mergedNotes = [ + ...oldNotes.filter((oldNote) => !newNotes.some((newNote) => newNote.id === oldNote.id)), + ...newNotes, + ]; + setConfig(bookKey, { ...config, booknotes: mergedNotes }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [syncedNotes]); +}; diff --git a/apps/readest-app/src/app/reader/hooks/useProgressSync.ts b/apps/readest-app/src/app/reader/hooks/useProgressSync.ts index 6222ea18..d87ab1cd 100644 --- a/apps/readest-app/src/app/reader/hooks/useProgressSync.ts +++ b/apps/readest-app/src/app/reader/hooks/useProgressSync.ts @@ -2,11 +2,11 @@ import { useEffect, useRef } from 'react'; import { useAuth } from '@/context/AuthContext'; import { useSync } from '@/hooks/useSync'; import { BookConfig } from '@/types/book'; -import { deserializeConfig, serializeConfig } from '@/utils/serializer'; import { useBookDataStore } from '@/store/bookDataStore'; -import { DEFAULT_BOOK_SEARCH_CONFIG, SYNC_PROGRESS_INTERVAL_SEC } from '@/services/constants'; import { useReaderStore } from '@/store/readerStore'; import { useSettingsStore } from '@/store/settingsStore'; +import { deserializeConfig, serializeConfig } from '@/utils/serializer'; +import { DEFAULT_BOOK_SEARCH_CONFIG, SYNC_PROGRESS_INTERVAL_SEC } from '@/services/constants'; export const useProgressSync = ( bookKey: string, diff --git a/apps/readest-app/src/hooks/useSync.ts b/apps/readest-app/src/hooks/useSync.ts index 0d720ffe..9cee254d 100644 --- a/apps/readest-app/src/hooks/useSync.ts +++ b/apps/readest-app/src/hooks/useSync.ts @@ -76,6 +76,8 @@ export function useSync(bookKey?: string) { try { const result = await syncClient.pullChanges(since, type, bookId); + const records = result[type]; + if (!records.length) return; const maxTime = computeMaxTimestamp(result[type]); setLastSyncedAt(maxTime); setSyncResult(result); @@ -187,6 +189,9 @@ export function useSync(bookKey?: string) { syncedBooks, syncedConfigs, syncedNotes, + lastSyncedAtBooks, + lastSyncedAtNotes, + lastSyncedAtConfigs, pullChanges, pushChanges, syncBooks, diff --git a/apps/readest-app/src/pages/api/sync.ts b/apps/readest-app/src/pages/api/sync.ts index e0441683..3a6d9f8e 100644 --- a/apps/readest-app/src/pages/api/sync.ts +++ b/apps/readest-app/src/pages/api/sync.ts @@ -151,6 +151,7 @@ export async function POST(req: NextRequest) { .insert(dbRec) .select() .single(); + console.log('Inserted record:', inserted); if (insertError) return { error: insertError.message }; authoritativeRecords.push(inserted); } else { @@ -172,7 +173,7 @@ export async function POST(req: NextRequest) { .match(matchConditions) .select() .single(); - console.log('Updating record:', updated); + console.log('Updated record:', updated); if (updateError) return { error: updateError.message }; authoritativeRecords.push(updated); } else { diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index 2d460ea3..5909271b 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -88,3 +88,4 @@ export const DOWNLOAD_READEST_URL = 'https://readest.com?utm_source=readest_web' export const READEST_WEB_BASE_URL = 'https://web.readest.com'; export const SYNC_PROGRESS_INTERVAL_SEC = 60; +export const SYNC_NOTES_INTERVAL_SEC = 60;