Skip to content

Commit 025ac5b

Browse files
committed
fix(mobile): resolve attachment badges, temp note sync, and offline editing issues
1 parent 0269dcc commit 025ac5b

10 files changed

Lines changed: 161 additions & 35 deletions

File tree

apps/mobile/v1/src/lib/database/index.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as SQLite from 'expo-sqlite';
1313
let database: SQLite.SQLiteDatabase | null = null;
1414

1515
const DB_NAME = 'typelets_mobile.db';
16-
const DB_VERSION = 3;
16+
const DB_VERSION = 4;
1717

1818
/**
1919
* Migrate database schema to latest version
@@ -96,10 +96,50 @@ async function migrateDatabase(db: SQLite.SQLiteDatabase): Promise<void> {
9696
}
9797
}
9898

99+
if (currentVersion < 4) {
100+
// Migration to v4: Add attachment_count column to notes table
101+
console.log('[SQLite] Running migration to v4...');
102+
103+
try {
104+
await db.execAsync(`
105+
-- Add attachment_count column to notes table
106+
ALTER TABLE notes ADD COLUMN attachment_count INTEGER DEFAULT 0;
107+
`);
108+
109+
console.log('[SQLite] Migration to v4 completed');
110+
} catch (error) {
111+
console.error('[SQLite] Migration to v4 failed:', error);
112+
throw error;
113+
}
114+
}
115+
99116
// Update schema version
100117
await db.execAsync(`PRAGMA user_version = ${DB_VERSION}`);
101118
console.log(`[SQLite] Database migrated to v${DB_VERSION}`);
102119
}
120+
121+
// Safety check: Ensure attachment_count column exists (runs every time)
122+
try {
123+
const columnCheck = await db.getFirstAsync<{ count: number }>(
124+
`SELECT COUNT(*) as count FROM pragma_table_info('notes') WHERE name='attachment_count'`
125+
);
126+
127+
if (columnCheck && columnCheck.count === 0) {
128+
console.log('[SQLite] attachment_count column missing, adding it now...');
129+
await db.execAsync(`
130+
ALTER TABLE notes ADD COLUMN attachment_count INTEGER DEFAULT 0;
131+
`);
132+
console.log('[SQLite] attachment_count column added successfully');
133+
134+
// Clear notes cache so they get re-fetched with attachment counts
135+
console.log('[SQLite] Clearing notes cache to refresh with attachment counts...');
136+
await db.execAsync(`DELETE FROM notes;`);
137+
await db.execAsync(`DELETE FROM cache_metadata WHERE resource_type = 'notes';`);
138+
console.log('[SQLite] Notes cache cleared - will be refreshed on next load');
139+
}
140+
} catch (error) {
141+
console.error('[SQLite] Failed to check/add attachment_count column:', error);
142+
}
103143
}
104144

105145
/**
@@ -145,7 +185,8 @@ export async function initializeDatabase(): Promise<SQLite.SQLiteDatabase> {
145185
encrypted_title TEXT,
146186
encrypted_content TEXT,
147187
iv TEXT,
148-
salt TEXT
188+
salt TEXT,
189+
attachment_count INTEGER DEFAULT 0
149190
);
150191
151192
-- Folders table with cache fields

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface EditorHeaderProps {
88
noteData: unknown;
99
isSaving: boolean;
1010
isOffline?: boolean;
11+
isTempNote?: boolean;
1112
attachmentsCount?: number;
1213
showAttachments?: boolean;
1314
showHeader?: boolean;
@@ -36,6 +37,7 @@ export function EditorHeader({
3637
isEditing,
3738
isSaving,
3839
isOffline = false,
40+
isTempNote = false,
3941
attachmentsCount = 0,
4042
showAttachments = false,
4143
showHeader = true,
@@ -142,9 +144,9 @@ export function EditorHeader({
142144

143145
{isEditing && (
144146
<TouchableOpacity
145-
style={[styles.actionButton, { backgroundColor: theme.colors.muted, opacity: isOffline ? 0.4 : 1 }]}
147+
style={[styles.actionButton, { backgroundColor: theme.colors.muted, opacity: (isOffline && !isTempNote) ? 0.4 : 1 }]}
146148
onPress={onDelete}
147-
disabled={isOffline}
149+
disabled={isOffline && !isTempNote}
148150
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
149151
>
150152
<Ionicons name="trash" size={20} color={theme.colors.mutedForeground} />
@@ -156,16 +158,16 @@ export function EditorHeader({
156158
styles.actionButton,
157159
{
158160
backgroundColor: theme.colors.muted,
159-
opacity: isSaving || (isOffline && isEditing) ? 0.4 : 1
161+
opacity: isSaving || (isOffline && isEditing && !isTempNote) ? 0.4 : 1
160162
}
161163
]}
162164
onPress={() => {
163-
if (!isSaving && !(isOffline && isEditing)) {
165+
if (!isSaving && !(isOffline && isEditing && !isTempNote)) {
164166
onSave();
165167
}
166168
}}
167-
disabled={isOffline && isEditing}
168-
activeOpacity={isSaving || (isOffline && isEditing) ? 1 : 0.2}
169+
disabled={isOffline && isEditing && !isTempNote}
170+
activeOpacity={isSaving || (isOffline && isEditing && !isTempNote) ? 1 : 0.2}
169171
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
170172
>
171173
<View pointerEvents="none">

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,13 @@ export default function EditNoteScreen() {
131131
const handleSave = async (options?: { skipNavigation?: boolean }) => {
132132
const titleToUse = title.trim() || 'Untitled';
133133

134-
// Prevent updates when offline (creates are allowed)
135-
if (!isOnline && ((isEditing && noteId) || createdNoteId)) {
136-
Alert.alert('Offline', 'You cannot update notes while offline. Please connect to the internet and try again.');
134+
// Prevent updates when offline (EXCEPT for temp notes created offline)
135+
const currentNoteId = (noteId as string) || createdNoteId;
136+
const isTempNote = currentNoteId?.startsWith('temp_');
137+
const isUpdatingExistingNote = (isEditing && noteId) || createdNoteId;
138+
139+
if (!isOnline && isUpdatingExistingNote && !isTempNote) {
140+
Alert.alert('Offline', 'You cannot update synced notes while offline. Please connect to the internet and try again.');
137141
return null;
138142
}
139143

@@ -239,9 +243,10 @@ export default function EditNoteScreen() {
239243
const handleDelete = async () => {
240244
if (!noteData || !noteId) return;
241245

242-
// Prevent deletes when offline
243-
if (!isOnline) {
244-
Alert.alert('Offline', 'You cannot delete notes while offline. Please connect to the internet and try again.');
246+
// Prevent deletes when offline (EXCEPT for temp notes created offline)
247+
const isTempNote = (noteId as string).startsWith('temp_');
248+
if (!isOnline && !isTempNote) {
249+
Alert.alert('Offline', 'You cannot delete synced notes while offline. Please connect to the internet and try again.');
245250
return;
246251
}
247252

@@ -310,6 +315,7 @@ export default function EditNoteScreen() {
310315
noteData={noteData}
311316
isSaving={isSaving}
312317
isOffline={!isOnline}
318+
isTempNote={((noteId as string) || createdNoteId)?.startsWith('temp_')}
313319
attachmentsCount={attachments.length}
314320
showAttachments={showAttachments}
315321
showHeader={showHeader}

apps/mobile/v1/src/screens/ViewNote/ViewHeader.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface ViewHeaderProps {
1010
attachmentsCount: number;
1111
showAttachments: boolean;
1212
isOffline?: boolean;
13+
isTempNote?: boolean;
1314
onBack: () => void;
1415
onToggleStar: () => void;
1516
onToggleHidden: () => void;
@@ -37,6 +38,7 @@ export function ViewHeader({
3738
attachmentsCount,
3839
showAttachments,
3940
isOffline = false,
41+
isTempNote = false,
4042
onBack,
4143
onToggleStar,
4244
onToggleHidden,
@@ -124,9 +126,9 @@ export function ViewHeader({
124126
</TouchableOpacity>
125127

126128
<TouchableOpacity
127-
style={[styles.editButton, { backgroundColor: theme.colors.muted, opacity: isOffline ? 0.4 : 1 }]}
129+
style={[styles.editButton, { backgroundColor: theme.colors.muted, opacity: (isOffline && !isTempNote) ? 0.4 : 1 }]}
128130
onPress={onEdit}
129-
disabled={isOffline}
131+
disabled={isOffline && !isTempNote}
130132
>
131133
<Ionicons name="create-outline" size={20} color={theme.colors.mutedForeground} />
132134
</TouchableOpacity>

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ export default function ViewNoteScreen() {
3535

3636
const { note, loading, htmlContent, handleEdit: handleEditInternal, handleToggleStar, handleToggleHidden } = useViewNote(noteId as string);
3737

38-
// Wrap handleEdit with offline check
38+
// Wrap handleEdit with offline check (allow editing temp notes created offline)
3939
const handleEdit = () => {
40-
if (!isOnline) {
41-
Alert.alert('Offline', 'You cannot edit notes while offline. Please connect to the internet and try again.');
40+
const isTempNote = (noteId as string).startsWith('temp_');
41+
if (!isOnline && !isTempNote) {
42+
Alert.alert('Offline', 'You cannot edit synced notes while offline. Please connect to the internet and try again.');
4243
return;
4344
}
4445
handleEditInternal();
@@ -124,11 +125,7 @@ export default function ViewNoteScreen() {
124125
if (scrollViewRef.current) {
125126
scrollViewRef.current.scrollTo({ y: 0, animated: false });
126127
}
127-
128-
if (noteId && lastLoadedNoteId.current === noteId && !loadingRef.current) {
129-
lastLoadedNoteId.current = null;
130-
loadAttachments();
131-
}
128+
// Don't reload attachments on refocus - they're already loaded
132129
// eslint-disable-next-line react-hooks/exhaustive-deps
133130
}, [scrollY, noteId])
134131
);
@@ -168,6 +165,7 @@ export default function ViewNoteScreen() {
168165
attachmentsCount={attachments.length}
169166
showAttachments={showAttachments}
170167
isOffline={!isOnline}
168+
isTempNote={(noteId as string).startsWith('temp_')}
171169
onBack={() => router.back()}
172170
onToggleStar={handleToggleStar}
173171
onToggleHidden={handleToggleHidden}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export async function getCachedNotes(filters?: {
218218
encryptedContent: row.encrypted_content || undefined,
219219
iv: row.iv || undefined,
220220
salt: row.salt || undefined,
221+
attachmentCount: row.attachment_count || 0,
221222
};
222223
});
223224
} catch (error) {
@@ -274,8 +275,8 @@ export async function storeCachedNotes(
274275
`INSERT OR REPLACE INTO notes (
275276
id, title, content, folder_id, user_id, starred, archived, deleted, hidden,
276277
created_at, updated_at, encrypted_title, encrypted_content, iv, salt,
277-
is_synced, is_dirty, synced_at
278-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
278+
is_synced, is_dirty, synced_at, attachment_count
279+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
279280
[
280281
note.id,
281282
title,
@@ -295,6 +296,7 @@ export async function storeCachedNotes(
295296
1, // is_synced
296297
0, // is_dirty
297298
now, // synced_at
299+
note.attachmentCount || 0, // attachment_count
298300
]
299301
);
300302
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Handles all folder-related API operations with offline-first caching
44
*/
55

6+
import { isOnline } from '../network/networkManager';
67
import { apiCache, CACHE_KEYS, CACHE_TTL } from './cache';
78
import { AuthTokenGetter, createHttpClient, NotModifiedError } from './client';
89
import {
@@ -106,9 +107,13 @@ export function createFoldersApi(getToken: AuthTokenGetter) {
106107
return cachedFolders;
107108
}
108109

109-
// Step 4: No cache available - wait for API (first load)
110-
if (__DEV__) {
111-
console.log('[API] No folders cache available - fetching from server');
110+
// Step 4: No cache available - check if online before fetching
111+
const online = await isOnline();
112+
if (!online) {
113+
if (__DEV__) {
114+
console.log('[API] Device offline and no folders cache available - returning empty array');
115+
}
116+
return []; // Don't try API when offline - prevents error
112117
}
113118

114119
try {

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ export function createNotesApi(getToken: AuthTokenGetter, getUserId: () => strin
114114
// Step 1: Try to get from local database cache
115115
const cachedNotes = await getCachedNotes(cacheFilters);
116116

117+
if (__DEV__) {
118+
console.log(`[API] Cache check: found ${cachedNotes.length} notes with filters:`, cacheFilters);
119+
}
120+
117121
// Step 2: Get cache metadata for conditional request
118122
const cacheMetadata = await getCacheMetadata(resourceType);
119123

@@ -193,7 +197,17 @@ export function createNotesApi(getToken: AuthTokenGetter, getUserId: () => strin
193197
return cachedNotes;
194198
}
195199

196-
// Step 4: No cache available - wait for API (first load)
200+
// Step 4: No cache available - check if online before fetching
201+
const online = await isOnline();
202+
if (!online) {
203+
if (__DEV__) {
204+
console.log('[API] Device offline and no cache available - returning empty array');
205+
}
206+
// Device is offline and no cache available - return empty array
207+
// This is a first-time user or cache was cleared
208+
return [];
209+
}
210+
197211
if (__DEV__) {
198212
console.log('[API] No cache available - fetching from server');
199213
}
@@ -537,13 +551,14 @@ export function createNotesApi(getToken: AuthTokenGetter, getUserId: () => strin
537551
updateFields.push('hidden = ?');
538552
updateValues.push(updates.hidden ? 1 : 0);
539553
}
540-
if (updates.iv !== undefined) {
554+
// Update iv and salt from encrypted payload (not updates)
555+
if (updatePayload.iv !== undefined) {
541556
updateFields.push('iv = ?');
542-
updateValues.push(updates.iv);
557+
updateValues.push(updatePayload.iv);
543558
}
544-
if (updates.salt !== undefined) {
559+
if (updatePayload.salt !== undefined) {
545560
updateFields.push('salt = ?');
546-
updateValues.push(updates.salt);
561+
updateValues.push(updatePayload.salt);
547562
}
548563

549564
// Always update updatedAt timestamp

apps/mobile/v1/src/services/fileService.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,23 @@ class FileService {
355355
* Get attachments for a note
356356
*/
357357
async getAttachments(noteId: string): Promise<FileAttachment[]> {
358+
// Temp notes don't exist on server yet - return empty array
359+
if (noteId.startsWith('temp_')) {
360+
return [];
361+
}
362+
363+
// Check if token provider is set and token is available
364+
if (!this.getToken) {
365+
console.warn('[FileService] Token provider not set yet - skipping attachment fetch');
366+
return [];
367+
}
368+
369+
const token = await this.getToken();
370+
if (!token) {
371+
console.warn('[FileService] No auth token available yet - skipping attachment fetch');
372+
return [];
373+
}
374+
358375
const response = await fetch(`${API_BASE_URL}/notes/${noteId}/files`, {
359376
headers: await this.getAuthHeaders(),
360377
});

0 commit comments

Comments
 (0)