Skip to content

Commit 89fb1d4

Browse files
committed
fix(mobile): persist task checkbox state and improve navigation UX
1 parent 2b2a6c7 commit 89fb1d4

File tree

5 files changed

+130
-45
lines changed

5 files changed

+130
-45
lines changed

apps/mobile/v1/editor/src/components/Editor.tsx

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ const createEditorHTML = (content: string, placeholder: string, isDark: boolean,
7575
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
7676
<meta name="color-scheme" content="${isDark ? 'dark' : 'light'}">
7777
<style>
78+
:root {
79+
--border-color: ${editorColors.border};
80+
--blockquote-border-color: ${editorColors.blockquoteBorder};
81+
--code-block-border-color: ${editorColors.codeBlockBorder};
82+
}
83+
7884
* {
7985
margin: 0;
8086
padding: 0;
@@ -144,7 +150,7 @@ const createEditorHTML = (content: string, placeholder: string, isDark: boolean,
144150
hr {
145151
margin: 16px 0;
146152
border: none;
147-
border-top: 1px solid ${editorColors.border};
153+
border-top: 1px solid var(--border-color);
148154
}
149155
150156
/* Lists */
@@ -163,7 +169,7 @@ const createEditorHTML = (content: string, placeholder: string, isDark: boolean,
163169
164170
/* Blockquotes */
165171
blockquote {
166-
border-left: 4px solid ${editorColors.blockquoteBorder};
172+
border-left: 4px solid var(--blockquote-border-color);
167173
padding-left: 16px;
168174
margin: 12px 0;
169175
color: ${editorColors.blockquoteText};
@@ -244,7 +250,7 @@ const createEditorHTML = (content: string, placeholder: string, isDark: boolean,
244250
245251
pre {
246252
background-color: ${editorColors.codeBlockBackground};
247-
border: 1px solid ${editorColors.codeBlockBorder};
253+
border: 1px solid var(--code-block-border-color);
248254
border-radius: 6px;
249255
padding: 12px 16px;
250256
margin: 8px 0;
@@ -385,6 +391,39 @@ const createEditorHTML = (content: string, placeholder: string, isDark: boolean,
385391
editor.addEventListener('selectionchange', checkActiveFormats);
386392
document.addEventListener('selectionchange', checkActiveFormats);
387393
394+
// Handle checkbox clicks in task lists
395+
editor.addEventListener('click', function(e) {
396+
const target = e.target;
397+
if (target && target.tagName === 'INPUT' && target.type === 'checkbox') {
398+
e.stopPropagation();
399+
// Toggle the checkbox state
400+
const newChecked = target.checked;
401+
402+
// Set both the checked attribute (for HTML persistence) and data-checked
403+
if (newChecked) {
404+
target.setAttribute('checked', 'checked');
405+
} else {
406+
target.removeAttribute('checked');
407+
}
408+
target.setAttribute('data-checked', String(newChecked));
409+
410+
// Find the parent task item and update its data-checked attribute
411+
let taskItem = target.parentElement;
412+
while (taskItem && taskItem !== editor) {
413+
if (taskItem.tagName === 'LI' && taskItem.getAttribute('data-type') === 'taskItem') {
414+
taskItem.setAttribute('data-checked', String(newChecked));
415+
break;
416+
}
417+
taskItem = taskItem.parentElement;
418+
}
419+
420+
// Immediately notify React Native of the change
421+
clearTimeout(timeout);
422+
const html = editor.innerHTML;
423+
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'content', html }));
424+
}
425+
});
426+
388427
// Handle Enter and Backspace keys
389428
editor.addEventListener('keydown', function(e) {
390429
if (e.key === 'Enter' || e.keyCode === 13) {
@@ -867,7 +906,7 @@ const createEditorHTML = (content: string, placeholder: string, isDark: boolean,
867906
});
868907
869908
// Handle content updates from React Native (external changes only)
870-
window.setContent = function(html) {
909+
(window as any).setContent = function(html: string) {
871910
if (!isInitialized) return;
872911
if (editor.innerHTML !== html) {
873912
const selection = window.getSelection();

apps/mobile/v1/src/screens/EditNote/index.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default function EditNoteScreen() {
5050

5151
const editorRef = useRef<EditorRef>(null);
5252
const keyboardHeight = useKeyboardHeight();
53+
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
5354

5455
// Map theme colors to editor colors
5556
const editorColors: EditorColors = useMemo(() => ({
@@ -107,6 +108,34 @@ export default function EditNoteScreen() {
107108
// eslint-disable-next-line react-hooks/exhaustive-deps
108109
}, [noteId, isEditing]);
109110

111+
// Auto-save when content changes (for checkbox clicks)
112+
useEffect(() => {
113+
// Only auto-save for existing notes when content contains checkboxes and has changed
114+
if ((noteId || createdNoteId) && content && content.includes('type="checkbox"')) {
115+
// Clear any existing timeout
116+
if (saveTimeoutRef.current) {
117+
clearTimeout(saveTimeoutRef.current);
118+
}
119+
120+
// Set a new timeout to save after 500ms of no changes
121+
saveTimeoutRef.current = setTimeout(() => {
122+
if (!isSaving) {
123+
handleSave({ skipNavigation: true }).catch(error => {
124+
logger.error('[NOTE] Auto-save failed', error instanceof Error ? error : undefined);
125+
});
126+
}
127+
}, 500);
128+
}
129+
130+
// Cleanup timeout on unmount
131+
return () => {
132+
if (saveTimeoutRef.current) {
133+
clearTimeout(saveTimeoutRef.current);
134+
}
135+
};
136+
// eslint-disable-next-line react-hooks/exhaustive-deps
137+
}, [content]);
138+
110139
const refreshAttachments = async () => {
111140
const currentNoteId = (noteId as string) || createdNoteId;
112141
if (currentNoteId) {

apps/mobile/v1/src/screens/FoldersScreen.tsx

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Ionicons } from '@expo/vector-icons';
22
import { BottomSheetBackdrop, BottomSheetBackdropProps,BottomSheetModal, BottomSheetTextInput, BottomSheetView } from '@gorhom/bottom-sheet';
33
import AsyncStorage from '@react-native-async-storage/async-storage';
4-
import { useIsFocused } from '@react-navigation/native';
4+
import { useFocusEffect } from '@react-navigation/native';
55
import * as Haptics from 'expo-haptics';
66
import { useRouter } from 'expo-router';
77
import React, { useCallback,useEffect, useMemo, useRef, useState } from 'react';
@@ -107,10 +107,6 @@ export default function FoldersScreen() {
107107
};
108108
}, []);
109109

110-
// Check if screen is focused
111-
const isFocused = useIsFocused();
112-
const loadTimerRef = useRef<NodeJS.Timeout | null>(null);
113-
114110
// Load folders data
115111
const loadFoldersData = useCallback(async (isRefresh = false) => {
116112
try {
@@ -180,23 +176,11 @@ export default function FoldersScreen() {
180176
}, [loadViewMode]);
181177

182178
// Reload data when screen comes into focus
183-
useEffect(() => {
184-
if (isFocused) {
185-
// Clear any pending load
186-
if (loadTimerRef.current) {
187-
clearTimeout(loadTimerRef.current);
188-
}
189-
190-
// Load immediately
179+
useFocusEffect(
180+
useCallback(() => {
191181
loadFoldersData();
192-
}
193-
194-
return () => {
195-
if (loadTimerRef.current) {
196-
clearTimeout(loadTimerRef.current);
197-
}
198-
};
199-
}, [isFocused, loadFoldersData]);
182+
}, [loadFoldersData])
183+
);
200184

201185
// Handle loading delay
202186
useEffect(() => {

apps/mobile/v1/src/screens/NotesListScreen.tsx

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2+
import { ActivityIndicator, Alert, Animated, FlatList, Keyboard, Pressable, RefreshControl, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3+
4+
import { useUser } from '@clerk/clerk-expo';
15
import { Ionicons } from '@expo/vector-icons';
2-
import { BottomSheetBackdrop, BottomSheetBackdropProps,BottomSheetModal, BottomSheetTextInput, BottomSheetView } from '@gorhom/bottom-sheet';
3-
import AsyncStorage from '@react-native-async-storage/async-storage';
6+
import {
7+
BottomSheetBackdrop,
8+
BottomSheetBackdropProps,
9+
BottomSheetModal,
10+
BottomSheetTextInput,
11+
BottomSheetView,
12+
} from '@gorhom/bottom-sheet';
413
import { useFocusEffect } from '@react-navigation/native';
5-
import { useUser } from '@clerk/clerk-expo';
14+
import AsyncStorage from '@react-native-async-storage/async-storage';
615
import * as Haptics from 'expo-haptics';
7-
import React, { useCallback,useEffect, useMemo, useRef, useState } from 'react';
8-
import { ActivityIndicator, Alert, Animated, FlatList, Keyboard, Pressable, RefreshControl, StyleSheet, Text, TouchableOpacity, useWindowDimensions,View } from 'react-native';
916

10-
import { FOLDER_CARD, FOLDER_COLORS,NOTE_CARD, SECTION } from '../constants/ui';
11-
import { type Folder, type FolderCounts,type Note, useApiService } from '../services/api';
12-
import { decryptNote, isNoteEncrypted } from '../services/api/encryption';
17+
import { FOLDER_CARD, FOLDER_COLORS, NOTE_CARD, SECTION } from '../constants/ui';
18+
import { type Folder, type FolderCounts, type Note, useApiService } from '../services/api';
19+
import { decryptNote } from '../services/api/encryption';
1320
import { useTheme } from '../theme';
1421

1522
interface RouteParams {
@@ -62,7 +69,6 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
6269
const api = useApiService();
6370
const { user } = useUser();
6471
const { folderId, viewType, searchQuery } = route?.params || {};
65-
const { height: windowHeight } = useWindowDimensions();
6672

6773
const [notes, setNotes] = useState<Note[]>([]);
6874
const [subfolders, setSubfolders] = useState<Folder[]>([]);
@@ -71,6 +77,10 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
7177
const [showLoading, setShowLoading] = useState(false);
7278
const [refreshing, setRefreshing] = useState(false);
7379

80+
// Track if notes have been loaded to prevent unnecessary refetch on navigation back
81+
const hasLoadedRef = useRef(false);
82+
const currentParamsRef = useRef<{ folderId?: string; viewType?: string; searchQuery?: string } | null>(null);
83+
7484
// Performance tracking
7585
const screenFocusTime = useRef<number>(0);
7686
const notesLoadedTime = useRef<number>(0);
@@ -169,10 +179,29 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
169179
// Load notes when screen focuses or params change
170180
useFocusEffect(
171181
React.useCallback(() => {
172-
loadNotes();
173-
loadViewMode();
174-
// Reset scroll position when screen comes into focus
175-
if (flatListRef.current) {
182+
// Check if params have changed
183+
const paramsChanged =
184+
!currentParamsRef.current ||
185+
currentParamsRef.current.folderId !== folderId ||
186+
currentParamsRef.current.viewType !== viewType ||
187+
currentParamsRef.current.searchQuery !== searchQuery;
188+
189+
// Determine if this is the first load
190+
const isFirstLoad = !hasLoadedRef.current;
191+
192+
// Always reload when screen comes into focus
193+
// Use refresh mode if not first load to avoid showing spinner
194+
if (isFirstLoad || paramsChanged) {
195+
loadNotes(); // Full load with spinner on first load or param change
196+
loadViewMode();
197+
currentParamsRef.current = { folderId, viewType, searchQuery };
198+
} else {
199+
// Silent refresh without spinner for subsequent focuses
200+
loadNotes(true); // Pass true for isRefresh to skip loading state
201+
}
202+
203+
// Reset scroll position when screen comes into focus (only if params changed)
204+
if (paramsChanged && flatListRef.current) {
176205
flatListRef.current.scrollToOffset({ offset: 0, animated: false });
177206
}
178207
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -298,6 +327,9 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
298327
setLoading(false);
299328
}
300329

330+
// Mark that notes have been loaded
331+
hasLoadedRef.current = true;
332+
301333
// Decrypt remaining notes in background
302334
if (remaining.length > 0) {
303335
const encryptedRemaining = remaining.filter(note =>
@@ -331,6 +363,8 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
331363
if (!isRefresh) {
332364
setLoading(false);
333365
}
366+
// Mark that notes have been loaded
367+
hasLoadedRef.current = true;
334368
}
335369

336370
// Load subfolders and their counts in background (non-blocking)
@@ -383,6 +417,8 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
383417
if (!isRefresh) {
384418
setLoading(false);
385419
}
420+
// Mark as loaded even on error to prevent reload loop
421+
hasLoadedRef.current = true;
386422
}
387423
};
388424

@@ -470,10 +506,7 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
470506
if (filterConfig.showStarredOnly && !note.starred) {
471507
return false;
472508
}
473-
if (filterConfig.showHiddenOnly && !note.hidden) {
474-
return false;
475-
}
476-
return true;
509+
return !filterConfig.showHiddenOnly || note.hidden;
477510
});
478511

479512
// Sort notes
@@ -883,7 +916,7 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
883916
</View>
884917
</>
885918
);
886-
}, [loading, renderHeader, viewType, subfolders, theme.colors, viewMode, filteredNotes.length, notes.length, hasActiveFilters, handleEmptyTrash, navigation, route?.params?.folderId]);
919+
}, [loading, renderHeader, viewType, subfolders, theme.colors, theme.isDark, viewMode, filteredNotes.length, notes.length, hasActiveFilters, handleEmptyTrash, navigation, route?.params?.folderId]);
887920

888921
// Render empty state
889922
const renderEmptyComponent = useCallback(() => {
@@ -1413,7 +1446,7 @@ const styles = StyleSheet.create({
14131446
fontWeight: '400',
14141447
},
14151448
noteListDivider: {
1416-
height: StyleSheet.hairlineWidth,
1449+
height: 1,
14171450
marginLeft: 0,
14181451
},
14191452
// Bottom sheet styles

apps/mobile/v1/src/services/api/notes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function createNotesApi(getToken: AuthTokenGetter, getUserId: () => strin
2424
*/
2525
const invalidateCountsCache = () => {
2626
// Clear all counts caches (general and folder-specific)
27-
apiCache.clearAll(); // This clears all caches including counts
27+
apiCache.clearAll();
2828
};
2929

3030
return {

0 commit comments

Comments
 (0)