From ebcd277d054cf3f7c1b3b24211ddb64c20961d2d Mon Sep 17 00:00:00 2001 From: 0xGingi <0xgingi@0xgingi.com> Date: Fri, 15 Nov 2024 19:14:42 +0000 Subject: [PATCH] fix sync not deleting notes on server modified UI a little add multiple select delete --- server/src/index.js | 62 ++++---- src/App.tsx | 241 ++++++++++++++++++++------------ src/components/SyncSettings.tsx | 50 ++++--- src/services/cryptoService.ts | 10 +- src/services/webStorage.ts | 86 ++++++++---- src/types/sync.ts | 1 + 6 files changed, 291 insertions(+), 159 deletions(-) diff --git a/server/src/index.js b/server/src/index.js index 8c0a77c..2e0f65f 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -165,25 +165,36 @@ async function processNotes(public_key, incoming_notes) { conflicts: [] }; - // Process incoming notes for (const note of incoming_notes) { const existing = await notesCollection.findOne({ public_key, id: note.id, }); - + if (!existing || existing.timestamp < note.timestamp) { + await notesCollection.updateOne( + { public_key, id: note.id }, + { + $set: { + id: note.id, + data: note.data, + nonce: note.nonce, + timestamp: note.timestamp, + signature: note.signature, + public_key, + deleted: note.deleted + } + }, + { upsert: true } + ); + if (note.deleted) { - // If note is marked as deleted, remove it from the database - await notesCollection.deleteOne({ public_key, id: note.id }); - } else { - // Otherwise update/insert the note - await notesCollection.updateOne( - { public_key, id: note.id }, - { $set: { ...note, public_key } }, - { upsert: true } - ); + await notesCollection.deleteOne({ + public_key, + id: note.id + }); } + results.updated.push(note.id); } else if (existing.timestamp === note.timestamp && existing.signature !== note.signature) { @@ -191,22 +202,20 @@ async function processNotes(public_key, incoming_notes) { } } - // Fetch all non-deleted notes for this user const allNotes = await notesCollection - .find({ - public_key, - deleted: { $ne: true } - }) - .toArray(); - - results.notes = allNotes.map(note => ({ - id: note.id, - data: note.data, - nonce: note.nonce, - timestamp: note.timestamp, - signature: note.signature, - deleted: note.deleted - })); + .find({ + public_key, + deleted: { $ne: true } + }) + .toArray(); + + results.notes = allNotes.map(note => ({ + id: note.id, + data: note.data, + nonce: note.nonce, + timestamp: note.timestamp, + signature: note.signature + })); return results; }); @@ -216,6 +225,7 @@ async function processNotes(public_key, incoming_notes) { await session.endSession(); } } + app.get('/api/health', async (req, res) => { try { await db.command({ ping: 1 }); diff --git a/src/App.tsx b/src/App.tsx index 7119b46..4d4c4be 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import { Burger, Anchor, Image, + Checkbox, } from '@mantine/core'; import { IconSun, @@ -30,6 +31,7 @@ import { IconUpload, IconCloud, IconBrandGithub, + IconCheckbox, } from '@tabler/icons-react'; import { useDebouncedCallback, useMediaQuery } from '@mantine/hooks'; import { MarkdownEditor } from './components/MarkdownEditor'; @@ -61,6 +63,8 @@ function App() { const [syncSettings, setSyncSettings] = useState(null); const isMobile = useMediaQuery('(max-width: 768px)'); const [mobileNavOpened, setMobileNavOpened] = useState(false); + const [selectedNotes, setSelectedNotes] = useState>(new Set()); + const [isSelectionMode, setIsSelectionMode] = useState(false); useEffect(() => { @@ -81,6 +85,40 @@ function App() { } }; + const toggleSelectionMode = () => { + setIsSelectionMode(!isSelectionMode); + if (isSelectionMode) { + setSelectedNotes(new Set()); + } + }; + + const toggleNoteSelection = (noteId: number, event: React.MouseEvent) => { + event.stopPropagation(); + const newSelection = new Set(selectedNotes); + if (newSelection.has(noteId)) { + newSelection.delete(noteId); + } else { + newSelection.add(noteId); + } + setSelectedNotes(newSelection); + }; + + const deleteSelectedNotes = async () => { + if (selectedNotes.size === 0) return; + + try { + await WebStorageService.deleteNote(Array.from(selectedNotes)); + setSelectedNotes(new Set()); + setIsSelectionMode(false); + if (selectedNote && selectedNotes.has(selectedNote.id!)) { + clearForm(); + } + await loadNotes(); + } catch (error) { + console.error('Failed to delete notes:', error); + alert('Failed to delete notes'); + } + }; const debouncedSave = useDebouncedCallback(async () => { if (title.trim() === '' && content.trim() === '') return; @@ -220,12 +258,7 @@ async function deleteNote(noteId: number) { hiddenFrom="sm" size="sm" /> - Logo + Logo )} + {isMobile ? ( - setMobileNavOpened(false)} - onNewNote={clearForm} - onSearch={setSearchQuery} - searchQuery={searchQuery} - onToggleTheme={toggleColorScheme} - colorScheme={colorScheme} - onShowSyncSettings={() => setShowSyncSettings(true)} - onExport={exportNotes} - onImport={importNotes} - selectedNote={selectedNote} - notes={filteredNotes} - onSelectNote={selectNote} - onDeleteNote={deleteNote} - /> - ) : ( + setMobileNavOpened(false)} + onNewNote={clearForm} + onSearch={setSearchQuery} + searchQuery={searchQuery} + onToggleTheme={toggleColorScheme} + colorScheme={colorScheme} + onShowSyncSettings={() => setShowSyncSettings(true)} + onExport={exportNotes} + onImport={importNotes} + selectedNote={selectedNote} + notes={filteredNotes} + onSelectNote={selectNote} + onDeleteNote={deleteNote} + /> + ) : ( - - - Logo - Trusty Notes - - - {!sidebarCollapsed && ( + + + Logo + Trusty Notes + + + + + + + + + + {!sidebarCollapsed && ( <> - - - - - - + + + + setShowSyncSettings(true)} size={30}> @@ -322,14 +361,25 @@ async function deleteNote(noteId: number) { {!sidebarCollapsed && ( <> - + + + {isSelectionMode && selectedNotes.size > 0 && ( + + )} + } @@ -342,52 +392,61 @@ async function deleteNote(noteId: number) { key={note.id} shadow="xs" p="md" - onClick={() => selectNote(note)} + onClick={() => !isSelectionMode && selectNote(note)} style={{ - cursor: 'pointer', - backgroundColor: selectedNote?.id === note.id ? + cursor: isSelectionMode ? 'default' : 'pointer', + backgroundColor: (selectedNote?.id === note.id || selectedNotes.has(note.id!)) ? 'var(--mantine-color-blue-light)' : undefined, }} > - - - {note.title || 'Untitled'} - - - {format(note.updated_at, 'MMM d, yyyy HH:mm')} - - - { - e.stopPropagation(); - deleteNote(note.id!); - }} - > - - + + {isSelectionMode && ( + toggleNoteSelection(note.id!, e as any)} + onClick={(e) => e.stopPropagation()} + /> + )} + + + {note.title || 'Untitled'} + + + {format(note.updated_at, 'MMM d, yyyy HH:mm')} + + + + {!isSelectionMode && ( + { + e.stopPropagation(); + deleteNote(note.id!); + }} + > + + + )} ))} )} - {sidebarCollapsed && ( - - - - - - - - toggleColorScheme()} size="lg"> - {colorScheme === 'dark' ? : } - - - - )} + {sidebarCollapsed && ( + *:not(:last-child)': { marginBottom: 'var(--mantine-spacing-md)' } + }} + > + + )} )} @@ -429,13 +488,13 @@ async function deleteNote(noteId: number) { paddingTop: isMobile ? '0.5rem' : '1rem' }} > - + diff --git a/src/components/SyncSettings.tsx b/src/components/SyncSettings.tsx index 890411f..805d9dc 100644 --- a/src/components/SyncSettings.tsx +++ b/src/components/SyncSettings.tsx @@ -19,6 +19,7 @@ import { WebStorageService } from '../services/webStorage'; import { SyncSettings as SyncSettingsType } from '../types/sync'; import { CryptoService } from '../services/cryptoService'; import { ApiService } from '../services/apiService'; +//import { NumberInput } from '@mantine/core'; const DEFAULT_SERVERS = [ { label: 'Official Server', value: 'https://notes-sync.toolworks.dev' }, @@ -40,6 +41,7 @@ export function SyncSettings({ onSync }: SyncSettingsProps) { const [newServerUrl, setNewServerUrl] = useState(''); const [showAddServer, setShowAddServer] = useState(false); const [isValidUrl, setIsValidUrl] = useState(false); + const [syncInterval, setSyncInterval] = useState(5); const validateUrl = (url: string) => { try { @@ -71,6 +73,7 @@ export function SyncSettings({ onSync }: SyncSettingsProps) { setSelectedServer(settings.server_url); setCustomServers(settings.custom_servers || []); setSeedPhrase(settings.seed_phrase ?? ''); + setSyncInterval(settings.sync_interval); } catch (err) { console.error('Failed to load settings:', err); } @@ -87,7 +90,7 @@ export function SyncSettings({ onSync }: SyncSettingsProps) { server_url: 'server_url' in updates ? updates.server_url! : selectedServer, custom_servers: 'custom_servers' in updates ? updates.custom_servers! : customServers, seed_phrase: 'seed_phrase' in updates ? updates.seed_phrase! : seedPhrase, - sync_interval: currentSettings.sync_interval, + sync_interval: 'sync_interval' in updates ? updates.sync_interval! : currentSettings.sync_interval, }; await WebStorageService.saveSyncSettings(newSettings); } catch (error) { @@ -172,22 +175,18 @@ export function SyncSettings({ onSync }: SyncSettingsProps) { try { console.log('Starting sync process...'); - // Initialize crypto await WebStorageService.initializeCrypto(seedPhrase); console.log('Crypto initialized'); - // Validate server health const isHealthy = await ApiService.healthCheck(selectedServer); if (!isHealthy) { throw new Error(`Server ${selectedServer} is not healthy`); } console.log('Server health check passed'); - // Perform sync await WebStorageService.syncWithServer(selectedServer); console.log('Sync completed'); - // Save settings await WebStorageService.saveSyncSettings({ seed_phrase: seedPhrase }); console.log('Settings saved'); @@ -215,13 +214,10 @@ export function SyncSettings({ onSync }: SyncSettingsProps) { const generateNewSeedPhrase = async () => { try { - // Use CryptoService's method instead of manual generation const mnemonic = CryptoService.generateNewSeedPhrase(); - // Initialize crypto service with the new seed phrase await WebStorageService.initializeCrypto(mnemonic); - // Save and display the new seed phrase setNewSeedPhrase(mnemonic); setSeedPhrase(mnemonic); await saveSettings({ seed_phrase: mnemonic }); @@ -303,14 +299,36 @@ export function SyncSettings({ onSync }: SyncSettingsProps) { - { - setAutoSync(e.currentTarget.checked); - saveSettings({ auto_sync: e.currentTarget.checked }); - }} - /> + + { + setAutoSync(e.currentTarget.checked); + saveSettings({ auto_sync: e.currentTarget.checked }); + }} + /> + + {autoSync && ( +