Skip to content

Commit d7ed215

Browse files
committed
feat(mobile): add attachment support for new notes and UI improvements
- Add attachment button to new note editor header - Implement save-first workflow for new note attachments - Prompt user to save note before uploading files - Keep editor open after save when attachments section is visible - Replace checkmark icon with Lucide Check icon in editor header - Reduce bottom padding on notes list screen (100px → 40px) - Add 2px margin below divider in view note screen for better spacing
1 parent a454107 commit d7ed215

File tree

9 files changed

+103
-34
lines changed

9 files changed

+103
-34
lines changed

apps/mobile/v1/src/components/FileUpload.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,19 @@ import { useTheme } from '../theme';
1313
import { useApiService, type FileAttachment } from '../services/api';
1414

1515
interface FileUploadProps {
16-
noteId: string;
16+
noteId?: string;
1717
attachments?: FileAttachment[];
1818
onUploadComplete?: (attachments: FileAttachment[]) => void;
1919
onDeleteComplete?: () => void;
20+
onBeforeUpload?: () => Promise<string | null>;
2021
}
2122

2223
export function FileUpload({
23-
noteId,
24+
noteId: initialNoteId,
2425
attachments = [],
2526
onUploadComplete,
2627
onDeleteComplete,
28+
onBeforeUpload,
2729
}: FileUploadProps) {
2830
const theme = useTheme();
2931
const api = useApiService();
@@ -34,6 +36,22 @@ export function FileUpload({
3436
const handlePickFiles = async () => {
3537
try {
3638
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
39+
40+
// If no noteId, get it from onBeforeUpload callback
41+
let noteId = initialNoteId;
42+
if (!noteId && onBeforeUpload) {
43+
noteId = await onBeforeUpload();
44+
if (!noteId) {
45+
// Upload was cancelled or failed to create note
46+
return;
47+
}
48+
}
49+
50+
if (!noteId) {
51+
Alert.alert('Error', 'Note must be saved before adding attachments');
52+
return;
53+
}
54+
3755
const files = await api.pickFiles();
3856

3957
if (files.length === 0) {

apps/mobile/v1/src/components/MasterPasswordDialog/styles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const styles = StyleSheet.create({
5858
position: 'absolute',
5959
right: 12,
6060
top: 0,
61-
height: '100%',
61+
height: 48,
6262
justifyContent: 'center',
6363
alignItems: 'center',
6464
padding: 4,

apps/mobile/v1/src/components/ui/Input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export const Input = forwardRef<TextInput, InputProps>(({ style, ...props }, ref
2525
minHeight: 40,
2626
// iOS-specific fix for centered placeholder text
2727
...(Platform.OS === 'ios' && {
28-
paddingTop: 10,
29-
paddingBottom: 10,
28+
paddingTop: 12,
29+
paddingBottom: 12,
3030
}),
3131
},
3232
style,

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { View, StyleSheet, TouchableOpacity, ActivityIndicator, Text } from 'react-native';
33
import { Ionicons } from '@expo/vector-icons';
4+
import { Check } from 'lucide-react-native';
45

56
interface EditorHeaderProps {
67
isEditing: boolean;
@@ -48,7 +49,7 @@ export function EditorHeader({
4849
<View style={styles.titleSpacer} />
4950

5051
<View style={styles.headerActions}>
51-
{isEditing && onToggleAttachments && (
52+
{onToggleAttachments && (
5253
<TouchableOpacity
5354
style={[styles.actionButton, { backgroundColor: showAttachments ? 'rgba(59, 130, 246, 0.15)' : theme.colors.muted }]}
5455
onPress={onToggleAttachments}
@@ -89,7 +90,7 @@ export function EditorHeader({
8990
{isSaving ? (
9091
<ActivityIndicator size="small" color={theme.colors.mutedForeground} />
9192
) : (
92-
<Ionicons name="checkmark" size={20} color={theme.colors.mutedForeground} />
93+
<Check size={20} color={theme.colors.mutedForeground} strokeWidth={2.5} />
9394
)}
9495
</TouchableOpacity>
9596
</View>

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function EditorToolbar({ editor, keyboardHeight, bottomInset, theme }: Ed
2929
styles.toolbarContainer,
3030
{
3131
backgroundColor: theme.colors.background,
32+
borderTopColor: theme.colors.border,
3233
bottom: keyboardHeight,
3334
paddingBottom,
3435
},
@@ -155,8 +156,8 @@ const styles = StyleSheet.create({
155156
left: 0,
156157
right: 0,
157158
bottom: 0,
158-
borderTopWidth: 0,
159-
paddingTop: 8,
159+
borderTopWidth: Platform.OS === 'ios' ? StyleSheet.hairlineWidth : 0,
160+
paddingTop: Platform.OS === 'ios' ? 4 : 8,
160161
paddingHorizontal: 8,
161162
alignItems: 'center',
162163
justifyContent: 'center',

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

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ export default function EditNoteScreen() {
2929
const [noteData, setNoteData] = useState<Note | null>(null);
3030
const [attachments, setAttachments] = useState<FileAttachment[]>([]);
3131
const [showAttachments, setShowAttachments] = useState(false);
32+
const [createdNoteId, setCreatedNoteId] = useState<string | null>(null);
3233

3334
const keyboardHeight = useKeyboardHeight();
34-
const { editor, handleEditorLoad, loadNote } = useNoteEditor(noteId as string);
35+
const { editor, handleEditorLoad, loadNote } = useNoteEditor(noteId as string || createdNoteId || undefined);
3536

3637
// Calculate toolbar height (toolbar itself + padding + keyboard)
3738
// iOS: 60px toolbar + keyboard height + minimal padding for curved keyboard
@@ -62,21 +63,19 @@ export default function EditNoteScreen() {
6263
}, [noteId, isEditing]);
6364

6465
const refreshAttachments = async () => {
65-
if (isEditing && noteId) {
66+
const currentNoteId = (noteId as string) || createdNoteId;
67+
if (currentNoteId) {
6668
try {
67-
const noteAttachments = await api.getAttachments(noteId as string);
69+
const noteAttachments = await api.getAttachments(currentNoteId);
6870
setAttachments(noteAttachments);
6971
} catch (error) {
7072
console.error('Failed to refresh attachments:', error);
7173
}
7274
}
7375
};
7476

75-
const handleSave = async () => {
76-
if (!title.trim()) {
77-
Alert.alert('Error', 'Please enter a title for your note');
78-
return;
79-
}
77+
const handleSave = async (options?: { skipNavigation?: boolean }) => {
78+
const titleToUse = title.trim() || 'Untitled';
8079

8180
setIsSaving(true);
8281

@@ -86,36 +85,74 @@ export default function EditNoteScreen() {
8685
console.log('Content to save:', content);
8786
}
8887

89-
if (isEditing && noteId) {
90-
await api.updateNote(noteId as string, {
91-
title: title.trim(),
88+
let savedNote: Note;
89+
90+
if ((isEditing && noteId) || createdNoteId) {
91+
savedNote = await api.updateNote((noteId as string) || createdNoteId!, {
92+
title: titleToUse,
9293
content,
9394
});
9495
} else {
95-
await api.createNote({
96-
title: title.trim(),
96+
savedNote = await api.createNote({
97+
title: titleToUse,
9798
content,
9899
folderId: folderId as string | undefined,
99100
starred: false,
100101
archived: false,
101102
deleted: false,
102103
hidden: false,
103104
});
105+
setCreatedNoteId(savedNote.id);
106+
setTitle(titleToUse);
104107
}
105108

106109
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
107110

108-
setTimeout(() => {
109-
router.back();
110-
}, NAVIGATION_DELAY);
111+
if (!options?.skipNavigation) {
112+
setTimeout(() => {
113+
router.back();
114+
}, NAVIGATION_DELAY);
115+
} else {
116+
setIsSaving(false);
117+
// Refresh attachments if we just created the note and staying on the page
118+
if (!noteId && savedNote.id) {
119+
try {
120+
const noteAttachments = await api.getAttachments(savedNote.id);
121+
setAttachments(noteAttachments);
122+
} catch (error) {
123+
console.error('Failed to refresh attachments:', error);
124+
}
125+
}
126+
}
127+
128+
return savedNote;
111129
} catch (error) {
112130
if (__DEV__) console.error('Failed to save note:', error);
113-
Alert.alert('Error', `Failed to ${isEditing ? 'update' : 'create'} note`);
131+
Alert.alert('Error', `Failed to ${isEditing || createdNoteId ? 'update' : 'create'} note`);
114132
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
115133
setIsSaving(false);
134+
return null;
116135
}
117136
};
118137

138+
const handleToggleAttachments = () => {
139+
setShowAttachments(!showAttachments);
140+
};
141+
142+
const handleBeforeUpload = async (): Promise<string | null> => {
143+
// If it's a new note (no noteId and no createdNoteId), prompt to save first
144+
if (!noteId && !createdNoteId) {
145+
Alert.alert(
146+
'Save Note First',
147+
'Please save your note before adding attachments.',
148+
[{ text: 'OK' }]
149+
);
150+
return null;
151+
}
152+
153+
return (noteId as string) || createdNoteId;
154+
};
155+
119156
const handleDelete = async () => {
120157
if (!noteData || !noteId) return;
121158

@@ -179,8 +216,8 @@ export default function EditNoteScreen() {
179216
showAttachments={showAttachments}
180217
onBack={() => router.back()}
181218
onDelete={handleDelete}
182-
onSave={handleSave}
183-
onToggleAttachments={() => setShowAttachments(!showAttachments)}
219+
onSave={() => handleSave({ skipNavigation: showAttachments && !noteId && !createdNoteId })}
220+
onToggleAttachments={handleToggleAttachments}
184221
theme={theme}
185222
/>
186223

@@ -203,13 +240,14 @@ export default function EditNoteScreen() {
203240
</View>
204241
)}
205242

206-
{isEditing && noteId && showAttachments && (
243+
{showAttachments && (
207244
<View style={styles.attachmentsSection}>
208245
<FileUpload
209-
noteId={noteId as string}
246+
noteId={(noteId as string) || createdNoteId || undefined}
210247
attachments={attachments}
211248
onUploadComplete={refreshAttachments}
212249
onDeleteComplete={refreshAttachments}
250+
onBeforeUpload={handleBeforeUpload}
213251
/>
214252
</View>
215253
)}
@@ -223,7 +261,7 @@ export default function EditNoteScreen() {
223261
Platform.OS === 'android' && keyboardHeight > 0 && { marginBottom: keyboardHeight + 60 }
224262
]}>
225263
<RichText
226-
key={noteId as string || 'new-note'}
264+
key={(noteId as string) || createdNoteId || 'new-note'}
227265
editor={editor}
228266
style={{ flex: 1, backgroundColor: theme.colors.background }}
229267
onLoad={handleEditorLoad}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ export default function NotesListScreen({ navigation, route, renderHeader, scrol
601601
</View>
602602

603603
{/* Spacer to ensure content fills screen */}
604-
<View style={{ flex: 1, minHeight: 100 }} />
604+
<View style={{ flex: 1, minHeight: 40 }} />
605605
</Pressable>
606606
)}
607607
</ScrollView>

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,17 @@ export default function SettingsScreen({ onLogout }: Props) {
117117
};
118118

119119
const settingsItems = [
120+
{
121+
section: 'ACCOUNT',
122+
items: [
123+
{
124+
title: user?.fullName || user?.emailAddresses?.[0]?.emailAddress || 'User',
125+
subtitle: user?.emailAddresses?.[0]?.emailAddress || '',
126+
icon: 'person-circle-outline',
127+
onPress: undefined,
128+
},
129+
],
130+
},
120131
{
121132
section: 'SECURITY',
122133
items: [
@@ -243,7 +254,7 @@ export default function SettingsScreen({ onLogout }: Props) {
243254
});
244255

245256
return (
246-
<SafeAreaView style={[styles.container, { backgroundColor: theme.colors.background }]}>
257+
<SafeAreaView style={[styles.container, { backgroundColor: theme.colors.background }]} edges={['top', 'left', 'right']}>
247258
{/* Header with back button */}
248259
<View>
249260
<View style={styles.headerBar}>
@@ -681,7 +692,6 @@ const styles = StyleSheet.create({
681692
flex: 1,
682693
},
683694
scrollContent: {
684-
paddingBottom: 16,
685695
},
686696
sectionsContainer: {
687697
paddingHorizontal: 16,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ const styles = StyleSheet.create({
253253
divider: {
254254
height: StyleSheet.hairlineWidth,
255255
marginHorizontal: 0,
256+
marginBottom: 2,
256257
},
257258
attachmentsContainer: {
258259
borderBottomWidth: StyleSheet.hairlineWidth,

0 commit comments

Comments
 (0)