Skip to content

Commit b63aae9

Browse files
committed
fix: folder counts not updating and performance issues
- Implement auto-refresh in FoldersScreen using useIsFocused() hook to reload counts when navigating back - Replace expensive getNotes() calls with lightweight getCounts() API endpoint - Add per-folder count support using FolderCounts interface
1 parent 626dd12 commit b63aae9

File tree

6 files changed

+246
-73
lines changed

6 files changed

+246
-73
lines changed

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

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
2-
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, ActivityIndicator, RefreshControl, Alert, Keyboard, Animated } from 'react-native';
2+
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, ActivityIndicator, RefreshControl, Alert, Keyboard, Animated, AppState } from 'react-native';
33
import AsyncStorage from '@react-native-async-storage/async-storage';
44
import * as Haptics from 'expo-haptics';
55
import { SafeAreaView } from 'react-native-safe-area-context';
66
import { useRouter } from 'expo-router';
7+
import { useIsFocused } from '@react-navigation/native';
78
import { useTheme } from '../theme';
89
import { Ionicons } from '@expo/vector-icons';
9-
import { useApiService, type Folder } from '../services/api';
10+
import { useApiService, type Folder, type FolderCounts } from '../services/api';
1011
import { FOLDER_CARD, ACTION_BUTTON, FOLDER_COLORS } from '../constants/ui';
1112
import { BottomSheetModal, BottomSheetView, BottomSheetBackdrop, BottomSheetTextInput, BottomSheetBackdropProps } from '@gorhom/bottom-sheet';
1213

@@ -99,10 +100,33 @@ export default function FoldersScreen() {
99100
};
100101
}, []);
101102

103+
// Check if screen is focused
104+
const isFocused = useIsFocused();
105+
const loadTimerRef = useRef<NodeJS.Timeout | null>(null);
106+
107+
// Reload data when screen comes into focus
108+
useEffect(() => {
109+
if (isFocused) {
110+
// Clear any pending load
111+
if (loadTimerRef.current) {
112+
clearTimeout(loadTimerRef.current);
113+
}
114+
115+
// Load immediately
116+
loadFoldersData();
117+
}
118+
119+
return () => {
120+
if (loadTimerRef.current) {
121+
clearTimeout(loadTimerRef.current);
122+
}
123+
};
124+
}, [isFocused, loadFoldersData]);
125+
126+
// Load view mode only on mount
102127
useEffect(() => {
103-
loadFoldersData();
104128
loadViewMode();
105-
}, []); // Only run on mount
129+
}, []);
106130

107131
const loadViewMode = async () => {
108132
try {
@@ -132,29 +156,39 @@ export default function FoldersScreen() {
132156
setLoading(true);
133157
}
134158

135-
// Fetch folders and counts in parallel (optimized - no note data fetched)
159+
// Use the counts endpoint instead of fetching all notes
136160
const [foldersData, noteCounts] = await Promise.all([
137161
api.getFolders(),
138-
api.getNoteCounts()
162+
api.getCounts() // Get counts from the API endpoint
139163
]);
140164

141-
if (__DEV__) {
142-
console.log(`${isRefresh ? 'Refresh' : 'Initial'} load - Note counts:`, noteCounts);
143-
}
144-
145165
// Show only ROOT folders (no parentId) on main screen
146-
// Note: For now, we don't show per-folder counts on home screen
147-
// Could be added later with a separate API endpoint
148-
const rootFolders = foldersData
149-
.filter(folder => !folder.parentId)
150-
.map(folder => ({ ...folder, noteCount: 0 }));
151-
152-
setAllFolders(rootFolders);
153-
setCounts(noteCounts);
154-
155-
if (__DEV__) {
156-
console.log('Updated note counts:', noteCounts);
157-
}
166+
const rootFolders = foldersData.filter(folder => !folder.parentId);
167+
168+
// When called without folder_id, API returns:
169+
// { all: 78, starred: 7, archived: 0, trash: 1, folders: { folderId: { all, starred, ... } } }
170+
const foldersObject = noteCounts.folders as Record<string, FolderCounts> | undefined;
171+
172+
// Add folder note counts from the API response
173+
const rootFoldersWithCounts = rootFolders.map(folder => {
174+
const folderCount = foldersObject?.[folder.id];
175+
return {
176+
...folder,
177+
noteCount: folderCount?.all || 0
178+
};
179+
});
180+
181+
setAllFolders(rootFoldersWithCounts);
182+
183+
// Use the root-level counts from the API
184+
const newCounts = {
185+
all: noteCounts.all || 0,
186+
starred: noteCounts.starred || 0,
187+
archived: noteCounts.archived || 0,
188+
trash: noteCounts.trash || 0,
189+
};
190+
191+
setCounts(newCounts);
158192
} catch (error) {
159193
if (__DEV__) console.error('Failed to load folders data:', error);
160194
setAllFolders([]);
@@ -189,7 +223,7 @@ export default function FoldersScreen() {
189223
const createdFolder = await api.createFolder(newFolderName.trim(), selectedColor);
190224

191225
// Add the new folder to the list
192-
setAllFolders(prev => [...prev, { ...createdFolder, noteCount: 0 }]);
226+
setAllFolders(prev => [...prev, createdFolder]);
193227

194228
// Reset modal state
195229
setNewFolderName('');

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

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
44
import * as Haptics from 'expo-haptics';
55
import { useFocusEffect } from '@react-navigation/native';
66
import { useTheme } from '../theme';
7-
import { useApiService, type Note, type Folder } from '../services/api';
7+
import { useApiService, type Note, type Folder, type FolderCounts } from '../services/api';
88
import { Ionicons } from '@expo/vector-icons';
99
import { NOTE_CARD, FOLDER_CARD, SECTION, FOLDER_COLORS } from '../constants/ui';
1010
import { BottomSheetModal, BottomSheetView, BottomSheetBackdrop, BottomSheetTextInput, BottomSheetBackdropProps } from '@gorhom/bottom-sheet';
@@ -151,28 +151,28 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
151151
if (!isRefresh) {
152152
setLoading(true);
153153
}
154-
if (__DEV__) {
155-
console.log('🎯 Loading notes for:', { folderId, viewType });
156-
}
157154

158-
const [allNotesData, foldersData] = await Promise.all([
159-
api.getNotes(), // Get all notes, filter client-side
160-
api.getFolders() // Load all folders to find subfolders
161-
]);
155+
// Build query params for getNotes to fetch only what we need
156+
const queryParams: Record<string, boolean | undefined> = {};
162157

163-
if (__DEV__) {
164-
console.log('📝 All notes received:', allNotesData.length);
158+
if (viewType === 'starred') {
159+
queryParams.starred = true;
160+
} else if (viewType === 'archived') {
161+
queryParams.archived = true;
162+
} else if (viewType === 'trash') {
163+
queryParams.deleted = true;
165164
}
166165

167-
// Client-side filtering
166+
// For folder view, we need to get all notes to filter by folderId
167+
// (API doesn't support server-side folder filtering yet)
168+
const allNotesData = await api.getNotes();
169+
170+
// Client-side filtering for folder
168171
let filteredNotes = allNotesData;
169172

170173
// First filter by folder if specified
171174
if (folderId) {
172175
filteredNotes = filteredNotes.filter(note => note.folderId === folderId);
173-
if (__DEV__) {
174-
console.log('📁 After folder filter:', filteredNotes.length);
175-
}
176176
}
177177

178178
// Then filter by view type
@@ -202,45 +202,36 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
202202

203203
setNotes(filteredNotes);
204204

205-
// Find subfolders
206-
let currentFolderSubfolders: Folder[];
207-
205+
// Load subfolders and their counts
208206
if (folderId) {
207+
// Get folders and counts in parallel
208+
const [foldersData, noteCounts] = await Promise.all([
209+
api.getFolders(),
210+
api.getCounts(folderId) // Get counts for subfolders of this folder
211+
]);
212+
209213
// If viewing a specific folder, show its subfolders
210-
currentFolderSubfolders = foldersData.filter(folder => folder.parentId === folderId);
214+
const currentFolderSubfolders = foldersData.filter(folder => folder.parentId === folderId);
215+
216+
// Add note counts from the API response
217+
// API returns folder counts directly as { folderId: { all, starred, ... } }
218+
const subfoldersWithCounts = currentFolderSubfolders.map(folder => {
219+
const folderCount = noteCounts[folder.id] as FolderCounts | undefined;
220+
return {
221+
...folder,
222+
noteCount: folderCount?.all || 0
223+
};
224+
});
225+
226+
setSubfolders(subfoldersWithCounts);
227+
setAllFolders(foldersData);
211228
} else {
212229
// Don't show folders in special views (all, starred, archived, trash)
213-
currentFolderSubfolders = [];
230+
setSubfolders([]);
231+
// Still need folders for displaying folder info in note list
232+
const foldersData = await api.getFolders();
233+
setAllFolders(foldersData);
214234
}
215-
216-
// Recursive function to get all nested folder IDs
217-
const getAllNestedFolderIds = (parentFolderId: string): string[] => {
218-
const ids = [parentFolderId];
219-
const childFolders = foldersData.filter(f => f.parentId === parentFolderId);
220-
childFolders.forEach(childFolder => {
221-
ids.push(...getAllNestedFolderIds(childFolder.id));
222-
});
223-
return ids;
224-
};
225-
226-
// Add note counts to subfolders (including nested subfolder notes)
227-
const subfoldersWithCounts = currentFolderSubfolders.map(folder => {
228-
const allNestedFolderIds = getAllNestedFolderIds(folder.id);
229-
const folderNotes = allNotesData.filter(note =>
230-
allNestedFolderIds.includes(note.folderId || '') &&
231-
!note.deleted &&
232-
!note.archived
233-
);
234-
235-
return {
236-
...folder,
237-
noteCount: folderNotes.length
238-
};
239-
});
240-
setSubfolders(subfoldersWithCounts);
241-
242-
// Store all folders for looking up note folder info
243-
setAllFolders(foldersData);
244235
} catch (error) {
245236
if (__DEV__) console.error('Failed to load notes:', error);
246237
Alert.alert('Error', 'Failed to load notes. Please try again.');
@@ -263,7 +254,7 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
263254
setIsCreatingFolder(true);
264255
const createdFolder = await api.createFolder(newFolderName.trim(), selectedColor, folderId);
265256

266-
// Add the new folder to the subfolders list
257+
// Add the new folder to the subfolders list (count will be 0 for new folders)
267258
setSubfolders(prev => [...prev, { ...createdFolder, noteCount: 0 }]);
268259

269260
// Reset modal state

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

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export default function SettingsScreen({ onLogout }: Props) {
4242
const themeModeSheetRef = useRef<BottomSheetModal>(null);
4343
const themeColorSheetRef = useRef<BottomSheetModal>(null);
4444
const securitySheetRef = useRef<BottomSheetModal>(null);
45+
const deleteAccountSheetRef = useRef<BottomSheetModal>(null);
4546
const viewModeSheetRef = useRef<BottomSheetModal>(null);
4647
const usageSheetRef = useRef<BottomSheetModal>(null);
4748

@@ -55,6 +56,7 @@ export default function SettingsScreen({ onLogout }: Props) {
5556
const themeModeSnapPoints = useMemo(() => ['45%'], []);
5657
const themeColorSnapPoints = useMemo(() => ['80%'], []);
5758
const securitySnapPoints = useMemo(() => ['70%'], []);
59+
const deleteAccountSnapPoints = useMemo(() => ['60%'], []);
5860
const viewModeSnapPoints = useMemo(() => ['40%'], []);
5961
const usageSnapPoints = useMemo(() => ['50%'], []);
6062

@@ -155,6 +157,13 @@ export default function SettingsScreen({ onLogout }: Props) {
155157
icon: 'key-outline',
156158
onPress: handleResetMasterPassword,
157159
},
160+
{
161+
title: 'Delete Account',
162+
subtitle: 'Permanently delete your account',
163+
icon: 'trash-outline',
164+
isDestructive: true,
165+
onPress: () => deleteAccountSheetRef.current?.present(),
166+
},
158167
{
159168
title: 'Logout',
160169
subtitle: 'Sign out of your account',
@@ -329,7 +338,7 @@ export default function SettingsScreen({ onLogout }: Props) {
329338
<Text style={[
330339
styles.settingItemTitle,
331340
{
332-
color: theme.colors.foreground
341+
color: item.isDestructive ? '#ef4444' : theme.colors.foreground
333342
}
334343
]}>
335344
{item.title}
@@ -674,6 +683,100 @@ export default function SettingsScreen({ onLogout }: Props) {
674683
</BottomSheetView>
675684
</BottomSheetModal>
676685

686+
{/* Delete Account Bottom Sheet */}
687+
<BottomSheetModal
688+
ref={deleteAccountSheetRef}
689+
snapPoints={deleteAccountSnapPoints}
690+
backdropComponent={renderBackdrop}
691+
backgroundStyle={{ backgroundColor: theme.colors.card }}
692+
handleIndicatorStyle={{ backgroundColor: theme.colors.border }}
693+
topInset={45}
694+
enableDynamicSizing={false}
695+
>
696+
<View style={{ paddingBottom: 32 }}>
697+
<View style={styles.bottomSheetHeader}>
698+
<Text style={[styles.bottomSheetTitle, { color: theme.colors.foreground }]}>
699+
Delete Account
700+
</Text>
701+
<TouchableOpacity
702+
style={[styles.iconButton, { backgroundColor: theme.colors.muted }]}
703+
onPress={() => deleteAccountSheetRef.current?.dismiss()}
704+
>
705+
<Ionicons name="close" size={20} color={theme.colors.mutedForeground} />
706+
</TouchableOpacity>
707+
</View>
708+
709+
<View style={[styles.divider, { backgroundColor: theme.colors.border }]} />
710+
711+
<BottomSheetScrollView style={{ paddingHorizontal: 20, paddingTop: 16 }}>
712+
<View style={{ gap: 20, paddingBottom: 20 }}>
713+
<View>
714+
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
715+
<Ionicons name="warning" size={24} color="#ef4444" style={{ marginRight: 8 }} />
716+
<Text style={[styles.securityFeatureTitle, { color: '#ef4444' }]}>
717+
This action is permanent
718+
</Text>
719+
</View>
720+
<Text style={[styles.securityFeatureDescription, { color: theme.colors.mutedForeground }]}>
721+
Deleting your account will permanently remove all your notes, folders, and data. This action cannot be undone.
722+
</Text>
723+
</View>
724+
725+
<View>
726+
<Text style={[styles.securityFeatureTitle, { color: theme.colors.foreground, marginBottom: 12 }]}>
727+
How to delete your account:
728+
</Text>
729+
<View style={{ gap: 12 }}>
730+
<View style={{ flexDirection: 'row' }}>
731+
<Text style={[styles.securityFeatureDescription, { color: theme.colors.mutedForeground, marginRight: 8 }]}>1.</Text>
732+
<Text style={[styles.securityFeatureDescription, { color: theme.colors.mutedForeground, flex: 1 }]}>
733+
Open the web app by tapping the button below
734+
</Text>
735+
</View>
736+
<View style={{ flexDirection: 'row' }}>
737+
<Text style={[styles.securityFeatureDescription, { color: theme.colors.mutedForeground, marginRight: 8 }]}>2.</Text>
738+
<Text style={[styles.securityFeatureDescription, { color: theme.colors.mutedForeground, flex: 1 }]}>
739+
Sign in with your account credentials
740+
</Text>
741+
</View>
742+
<View style={{ flexDirection: 'row' }}>
743+
<Text style={[styles.securityFeatureDescription, { color: theme.colors.mutedForeground, marginRight: 8 }]}>3.</Text>
744+
<Text style={[styles.securityFeatureDescription, { color: theme.colors.mutedForeground, flex: 1 }]}>
745+
Click on your avatar and select &ldquo;Manage Account&rdquo;
746+
</Text>
747+
</View>
748+
<View style={{ flexDirection: 'row' }}>
749+
<Text style={[styles.securityFeatureDescription, { color: theme.colors.mutedForeground, marginRight: 8 }]}>4.</Text>
750+
<Text style={[styles.securityFeatureDescription, { color: theme.colors.mutedForeground, flex: 1 }]}>
751+
Go to the Security section and select &ldquo;Delete Account&rdquo;
752+
</Text>
753+
</View>
754+
</View>
755+
</View>
756+
757+
<TouchableOpacity
758+
style={{
759+
backgroundColor: '#ef4444',
760+
paddingVertical: 14,
761+
paddingHorizontal: 20,
762+
borderRadius: 8,
763+
alignItems: 'center',
764+
marginTop: 8,
765+
}}
766+
onPress={() => {
767+
deleteAccountSheetRef.current?.dismiss();
768+
Linking.openURL('https://app.typelets.com');
769+
}}
770+
>
771+
<Text style={{ color: '#ffffff', fontSize: 16, fontWeight: '600' }}>
772+
Open Web App to Delete Account
773+
</Text>
774+
</TouchableOpacity>
775+
</View>
776+
</BottomSheetScrollView>
777+
</View>
778+
</BottomSheetModal>
779+
677780
{/* Usage Bottom Sheet */}
678781
<UsageBottomSheet sheetRef={usageSheetRef as React.RefObject<BottomSheetModal>} snapPoints={usageSnapPoints} />
679782

0 commit comments

Comments
 (0)