diff --git a/apps/mobile/v1/eas.json b/apps/mobile/v1/eas.json index 327f212..24a1f50 100644 --- a/apps/mobile/v1/eas.json +++ b/apps/mobile/v1/eas.json @@ -7,12 +7,14 @@ "development": { "developmentClient": true, "distribution": "internal", + "node": "22.12.0", "env": { "RCT_NEW_ARCH_ENABLED": "0" } }, "preview": { "distribution": "internal", + "node": "22.12.0", "env": { "RCT_NEW_ARCH_ENABLED": "1", "SENTRY_ORG": "bata-labs", @@ -21,6 +23,7 @@ }, "production": { "autoIncrement": true, + "node": "22.12.0", "env": { "RCT_NEW_ARCH_ENABLED": "1", "SENTRY_ORG": "bata-labs", diff --git a/apps/mobile/v1/package.json b/apps/mobile/v1/package.json index 7ae458e..18cf788 100644 --- a/apps/mobile/v1/package.json +++ b/apps/mobile/v1/package.json @@ -1,7 +1,7 @@ { "name": "v1", "main": "index.js", - "version": "1.45.2", + "version": "1.45.4", "scripts": { "start": "expo start", "reset-project": "node ./scripts/reset-project.js", diff --git a/apps/mobile/v1/src/components/CreateFileSheet.tsx b/apps/mobile/v1/src/components/CreateFileSheet.tsx new file mode 100644 index 0000000..3bc8f69 --- /dev/null +++ b/apps/mobile/v1/src/components/CreateFileSheet.tsx @@ -0,0 +1,258 @@ +import { Ionicons } from '@expo/vector-icons'; +import { + BottomSheetBackdrop, + BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetScrollView, +} from '@gorhom/bottom-sheet'; +import { GlassView } from 'expo-glass-effect'; +import React, { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; +import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { MdiIcon, mdiCodeTags, mdiFileDocumentOutline, mdiFileTableBoxOutline, mdiTextBoxOutline, mdiVectorSquare } from './MdiIcon'; +import { useTheme } from '../theme'; + +export interface CreateFileSheetRef { + present: () => void; + dismiss: () => void; +} + +interface CreateFileSheetProps { + onCreateNote: () => void; +} + +interface FileTypeOption { + id: 'note' | 'sheet' | 'diagram' | 'code' | 'document'; + title: string; + subtitle: string; + iconPath: string; + iconColor: string; + available: boolean; +} + +const FILE_TYPES: FileTypeOption[] = [ + { + id: 'note', + title: 'Note', + subtitle: 'Write with rich text formatting', + iconPath: mdiTextBoxOutline, + iconColor: '#f43f5e', + available: true, + }, + { + id: 'sheet', + title: 'Spreadsheet', + subtitle: 'Create tables and calculations', + iconPath: mdiFileTableBoxOutline, + iconColor: '#22c55e', + available: false, + }, + { + id: 'diagram', + title: 'Diagram', + subtitle: 'Draw flowcharts and diagrams', + iconPath: mdiVectorSquare, + iconColor: '#a855f7', + available: false, + }, + { + id: 'code', + title: 'Code', + subtitle: 'Write and save code snippets', + iconPath: mdiCodeTags, + iconColor: '#f59e0b', + available: false, + }, + { + id: 'document', + title: 'Document', + subtitle: 'Long-form writing with pages', + iconPath: mdiFileDocumentOutline, + iconColor: '#3b82f6', + available: false, + }, +]; + +export const CreateFileSheet = forwardRef( + ({ onCreateNote }, ref) => { + const theme = useTheme(); + const insets = useSafeAreaInsets(); + const sheetRef = useRef(null); + + useImperativeHandle(ref, () => ({ + present: () => sheetRef.current?.present(), + dismiss: () => sheetRef.current?.dismiss(), + })); + + const renderBackdrop = useCallback( + (props: BottomSheetBackdropProps) => ( + + ), + [] + ); + + const handleSelect = (option: FileTypeOption) => { + if (!option.available) { + Alert.alert('Coming Soon', `${option.title} creation is coming in a future update. Stay tuned!`); + return; + } + + sheetRef.current?.dismiss(); + + if (option.id === 'note') { + onCreateNote(); + } + }; + + return ( + + + + + Create New + + + sheetRef.current?.dismiss()} + > + + + + + + + + + {FILE_TYPES.map((option) => ( + handleSelect(option)} + activeOpacity={0.7} + > + + + + + + {option.title} + + + {option.subtitle} + + + {!option.available && ( + + + Soon + + + )} + + ))} + + + + ); + } +); + +CreateFileSheet.displayName = 'CreateFileSheet'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingBottom: 12, + }, + title: { + fontSize: 20, + fontWeight: '600', + }, + glassButton: { + borderRadius: 17, + overflow: 'hidden', + }, + closeButton: { + width: 34, + height: 34, + alignItems: 'center', + justifyContent: 'center', + }, + divider: { + height: 0.5, + }, + optionsContainer: { + paddingHorizontal: 20, + paddingTop: 16, + gap: 12, + }, + optionItem: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + borderRadius: 12, + borderWidth: 1, + }, + optionIcon: { + width: 48, + height: 48, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + marginRight: 16, + }, + optionText: { + flex: 1, + }, + optionTitle: { + fontSize: 16, + fontWeight: '600', + marginBottom: 2, + }, + optionSubtitle: { + fontSize: 14, + }, + comingSoonBadge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 6, + }, + comingSoonText: { + fontSize: 12, + fontWeight: '500', + }, +}); diff --git a/apps/mobile/v1/src/components/MdiIcon.tsx b/apps/mobile/v1/src/components/MdiIcon.tsx new file mode 100644 index 0000000..98a3ed3 --- /dev/null +++ b/apps/mobile/v1/src/components/MdiIcon.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; + +// MDI icon paths (from @mdi/js) - hardcoded to avoid Node version issues +export const mdiTextBoxOutline = 'M5,3C3.89,3 3,3.89 3,5V19C3,20.11 3.89,21 5,21H19C20.11,21 21,20.11 21,19V5C21,3.89 20.11,3 19,3H5M5,5H19V19H5V5M7,7V9H17V7H7M7,11V13H17V11H7M7,15V17H14V15H7Z'; +export const mdiVectorSquare = 'M2,2H8V4H16V2H22V8H20V16H22V22H16V20H8V22H2V16H4V8H2V2M16,8V6H8V8H6V16H8V18H16V16H18V8H16M4,4V6H6V4H4M18,4V6H20V4H18M4,18V20H6V18H4M18,18V20H20V18H18Z'; +export const mdiFileTableBoxOutline = 'M19 3H5C3.89 3 3 3.89 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.89 20.1 3 19 3M19 19H5V5H19V19M7 7H11V11H7V7M7 13H11V17H7V13M13 7H17V11H13V7M13 13H17V17H13V13Z'; +export const mdiCodeTags = 'M14.6,16.6L19.2,12L14.6,7.4L16,6L22,12L16,18L14.6,16.6M9.4,16.6L4.8,12L9.4,7.4L8,6L2,12L8,18L9.4,16.6Z'; +export const mdiFileDocumentOutline = 'M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z'; + +interface MdiIconProps { + path: string; + size?: number; + color?: string; +} + +/** + * Renders MDI icons using react-native-svg + * MDI icons use a 24x24 viewBox + */ +export function MdiIcon({ path, size = 24, color = '#000' }: MdiIconProps) { + return ( + + + + ); +} + +export default MdiIcon; diff --git a/apps/mobile/v1/src/components/SheetsViewer.tsx b/apps/mobile/v1/src/components/SheetsViewer.tsx new file mode 100644 index 0000000..b72d741 --- /dev/null +++ b/apps/mobile/v1/src/components/SheetsViewer.tsx @@ -0,0 +1,599 @@ +import React, { useMemo, useState } from 'react'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; +import { WebView } from 'react-native-webview'; + +interface SheetsViewerProps { + content: string; + theme: { + colors: { + foreground: string; + mutedForeground: string; + border: string; + muted: string; + }; + isDark: boolean; + }; +} + +/** + * SheetsViewer - Read-only spreadsheet viewer for mobile + * Uses Univer library via WebView to render spreadsheets exactly as they appear on web + */ +export function SheetsViewer({ content, theme }: SheetsViewerProps) { + const [loading, setLoading] = useState(true); + + // Escape content for safe inclusion in HTML + const escapedContent = useMemo(() => { + return content + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r'); + }, [content]); + + const fullHtml = useMemo(() => ` + + + + + + + + + +
+ + + + + + + + + + + + `, [escapedContent, theme.isDark, theme.colors.mutedForeground]); + + return ( + + {loading && ( + + + + )} + { + setTimeout(() => setLoading(false), 1000); + }} + onMessage={(event) => { + try { + const data = JSON.parse(event.nativeEvent.data); + if (data.type === 'loaded') { + setLoading(false); + } else if (data.type === 'error') { + setLoading(false); + console.error('SheetsViewer error:', data.message); + } else if (data.type === 'debug') { + console.log('SheetsViewer debug:', JSON.stringify(data, null, 2)); + } + } catch { + // Ignore parse errors + } + }} + /> + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + webview: { + flex: 1, + backgroundColor: 'transparent', + }, + loadingOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + alignItems: 'center', + justifyContent: 'center', + zIndex: 1, + paddingBottom: 60, // Account for header + }, +}); + +export default SheetsViewer; diff --git a/apps/mobile/v1/src/components/settings/UsageBottomSheet.tsx b/apps/mobile/v1/src/components/settings/UsageBottomSheet.tsx index d353682..5ea9917 100644 --- a/apps/mobile/v1/src/components/settings/UsageBottomSheet.tsx +++ b/apps/mobile/v1/src/components/settings/UsageBottomSheet.tsx @@ -138,7 +138,7 @@ export function UsageBottomSheet({ sheetRef, snapPoints }: UsageBottomSheetProps {usage.storage.usagePercent >= 95 && ( - ⚠️ Storage nearly full. Delete some notes to free up space. + ⚠️ Storage nearly full. Delete some files to free up space. )} @@ -151,10 +151,10 @@ export function UsageBottomSheet({ sheetRef, snapPoints }: UsageBottomSheetProps )} - {/* Notes Count */} + {/* Files Count */} - Notes + Files {usage.notes.count.toLocaleString()} / {usage.notes.limit.toLocaleString()} @@ -179,14 +179,14 @@ export function UsageBottomSheet({ sheetRef, snapPoints }: UsageBottomSheetProps {usage.notes.usagePercent >= 95 && ( - ⚠️ Note limit nearly reached. Delete some notes to create new ones. + ⚠️ Typelet limit nearly reached. Delete some to create new ones. )} {usage.notes.usagePercent >= 80 && usage.notes.usagePercent < 95 && ( - ⚠️ Approaching note limit + ⚠️ Approaching typelet limit )} diff --git a/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/NoteListItem.tsx b/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/NoteListItem.tsx index 4014b91..d62b208 100644 --- a/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/NoteListItem.tsx +++ b/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/NoteListItem.tsx @@ -1,6 +1,8 @@ import { Ionicons } from '@expo/vector-icons'; import { GlassView } from 'expo-glass-effect'; -import { Code2, Globe, Network } from 'lucide-react-native'; +import { Globe, SquareCode } from 'lucide-react-native'; + +import { MdiIcon, mdiFileTableBoxOutline, mdiTextBoxOutline, mdiVectorSquare } from '@/src/components/MdiIcon'; import React, { useMemo, useRef } from 'react'; import { ActivityIndicator, Animated, Pressable, StyleSheet, Text, View } from 'react-native'; import Swipeable from 'react-native-gesture-handler/ReanimatedSwipeable'; @@ -70,6 +72,7 @@ const NoteListItemComponent: React.FC = ({ const noteType = useMemo(() => detectNoteType(note), [note]); const isDiagram = noteType === 'diagram'; const isCode = noteType === 'code'; + const isSheet = noteType === 'sheets'; // Check if note is still encrypted (loading skeleton) const noteIsEncrypted = note.title === '[ENCRYPTED]' || note.content === '[ENCRYPTED]'; @@ -237,62 +240,72 @@ const NoteListItemComponent: React.FC = ({ style={[styles.noteListItem, { backgroundColor, paddingHorizontal: 16 }]} android_ripple={{ color: mutedColor }} > - - - - {String(note.title || 'Untitled')} - - - {isDiagram && ( - - )} - {isCode && ( - - )} - {note.isPublished && ( - + + {/* Note type icon on the left */} + + {isDiagram ? ( + + ) : isCode ? ( + + ) : isSheet ? ( + + ) : ( + )} - {((note.attachments?.length ?? 0) > 0 || (note.attachmentCount ?? 0) > 0) && ( - - - - - - {note.attachments?.length || note.attachmentCount || 0} + + + {/* Content on the right */} + + + + {String(note.title || 'Untitled')} + + + {note.isPublished && ( + + )} + {((note.attachments?.length ?? 0) > 0 || (note.attachmentCount ?? 0) > 0) && ( + + + + + + {note.attachments?.length || note.attachmentCount || 0} + + + )} + {note.starred && ( + + )} + + {enhancedData?.formattedDate || ''} - )} - {note.starred && ( - - )} - - {enhancedData?.formattedDate || ''} + + + {enhancedData?.preview || ''} + {!folderId && folderPath && ( + + + + {folderPath} + + + )} - - {enhancedData?.preview || ''} - - {!folderId && folderPath && ( - - - - {folderPath} - - - )} - - {!isLastItem && } + {!isLastItem && } ); }; @@ -306,8 +319,16 @@ const styles = StyleSheet.create({ }, noteListItem: { }, - noteListContent: { + noteListRow: { + flexDirection: 'row', paddingVertical: 12, + }, + noteTypeIconContainer: { + marginRight: 12, + paddingTop: 2, + }, + noteListContent: { + flex: 1, paddingHorizontal: 0, }, noteListHeader: { @@ -364,7 +385,7 @@ const styles = StyleSheet.create({ noteListDivider: { // StyleSheet.hairlineWidth is intentionally used for height (ultra-thin divider) height: StyleSheet.hairlineWidth, - marginHorizontal: 16, + marginRight: 16, marginVertical: 0, }, deleteAction: { diff --git a/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/NotesHeader.tsx b/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/NotesHeader.tsx index 060a131..04f384c 100644 --- a/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/NotesHeader.tsx +++ b/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/NotesHeader.tsx @@ -81,7 +81,7 @@ export const NotesHeader: React.FC = ({ > - Create Note + Start Writing diff --git a/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/index.tsx b/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/index.tsx index 7d8d163..6023e3c 100644 --- a/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/index.tsx +++ b/apps/mobile/v1/src/screens/FolderNotes/components/NotesList/index.tsx @@ -17,6 +17,8 @@ import { type Folder, type Note, useApiService } from '@/src/services/api'; import { useTheme } from '@/src/theme'; import { stripHtmlTags } from '@/src/utils/noteUtils'; +import { CreateFileSheet, CreateFileSheetRef } from '@/src/components/CreateFileSheet'; + import { CreateFolderSheet } from './CreateFolderSheet'; import { EmptyState } from './EmptyState'; import { FilterConfig, FilterSortSheet, SortConfig } from './FilterSortSheet'; @@ -114,6 +116,7 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa const createFolderSheetRef = useRef(null); const filterSortSheetRef = useRef(null); const noteActionsSheetRef = useRef(null); + const createFileSheetRef = useRef(null); const flatListRef = useRef>(null); const createNoteButtonRef = useRef(null); @@ -678,13 +681,13 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa viewType={viewType} subfoldersCount={subfolders.length} onFilterPress={() => filterSortSheetRef.current?.present()} - onCreateNotePress={() => navigation?.navigate('CreateNote', { folderId: route?.params?.folderId })} + onCreateNotePress={() => createFileSheetRef.current?.present()} onEmptyTrashPress={handleEmptyTrash} createNoteButtonRef={createNoteButtonRef} /> ); - }, [loading, notes.length, renderHeader, viewType, subfolders, viewMode, filteredNotes.length, hasActiveFilters, handleEmptyTrash, navigation, route?.params?.folderId, filterSortSheetRef]); + }, [loading, notes.length, renderHeader, viewType, subfolders, viewMode, filteredNotes.length, hasActiveFilters, handleEmptyTrash, filterSortSheetRef]); // Render empty state const renderEmptyComponent = useCallback(() => { @@ -774,7 +777,7 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa right: 20, } ]} - onPress={() => navigation?.navigate('CreateNote', { folderId: route?.params?.folderId })} + onPress={() => createFileSheetRef.current?.present()} > @@ -783,6 +786,12 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa )} + + {/* Create File Type Sheet */} + navigation?.navigate('CreateNote', { folderId: route?.params?.folderId })} + /> ); } diff --git a/apps/mobile/v1/src/screens/FolderNotes/index.tsx b/apps/mobile/v1/src/screens/FolderNotes/index.tsx index 61fce60..8e35325 100644 --- a/apps/mobile/v1/src/screens/FolderNotes/index.tsx +++ b/apps/mobile/v1/src/screens/FolderNotes/index.tsx @@ -19,12 +19,12 @@ import NotesList from './components/NotesList'; function getViewTitle(viewType: string): string { switch (viewType) { - case 'all': return 'All Notes'; + case 'all': return 'All Files'; case 'starred': return 'Starred'; case 'public': return 'Public'; case 'archived': return 'Archived'; case 'trash': return 'Trash'; - default: return 'Notes'; + default: return 'Files'; } } @@ -98,7 +98,7 @@ export default function FolderNotesScreen({ folderId, folderName, viewType }: Fo useEffect(() => { const buildBreadcrumbs = async () => { if (!folderId && !viewType) { - setBreadcrumbs(['Notes']); + setBreadcrumbs(['Files']); // Still fetch folders for navigation menu try { const folders = await api.getFolders(); @@ -147,7 +147,7 @@ export default function FolderNotesScreen({ folderId, folderName, viewType }: Fo setAllFolders(folders); } catch (error) { console.error('Failed to build breadcrumbs:', error); - setBreadcrumbs([folderName || 'Notes']); + setBreadcrumbs([folderName || 'Files']); setBreadcrumbFolders([]); setAllFolders([]); } @@ -159,7 +159,7 @@ export default function FolderNotesScreen({ folderId, folderName, viewType }: Fo // Format breadcrumbs for display const formatBreadcrumbs = (crumbs: string[]): string => { - if (crumbs.length === 0) return 'Notes'; + if (crumbs.length === 0) return 'Files'; return crumbs.join(' / '); }; @@ -410,7 +410,7 @@ export default function FolderNotesScreen({ folderId, folderName, viewType }: Fo {isSearchVisible && ( ('grid'); - // Bottom sheet ref + // Bottom sheet refs const createFolderSheetRef = useRef(null); + const createFileSheetRef = useRef(null); // Scroll tracking const scrollY = useRef(new Animated.Value(0)).current; @@ -384,13 +386,13 @@ export default function HomeScreen() { {/* Quick Actions */} - router.push('/edit-note')}> + createFileSheetRef.current?.present()}> - Start writing + Start Writing @@ -739,6 +741,12 @@ export default function HomeScreen() { + + {/* Create File Type Sheet */} + router.push('/edit-note')} + /> ); } diff --git a/apps/mobile/v1/src/screens/Settings/index.tsx b/apps/mobile/v1/src/screens/Settings/index.tsx index 13526d8..e86dc97 100644 --- a/apps/mobile/v1/src/screens/Settings/index.tsx +++ b/apps/mobile/v1/src/screens/Settings/index.tsx @@ -161,7 +161,7 @@ export default function SettingsScreen({ onLogout }: Props) { const handleClearCache = async () => { Alert.alert( 'Clear Cache', - 'This will clear all cached data. Your notes and folders will be synced from the server on next load.', + 'This will clear all cached data. Your files and folders will be synced from the server on next load.', [ { text: 'Cancel', style: 'cancel' }, { @@ -201,7 +201,7 @@ export default function SettingsScreen({ onLogout }: Props) { const handleRefreshCache = async () => { Alert.alert( 'Refresh Cache', - 'This will clear your cache and reload all notes from the server. This may take a few moments.', + 'This will clear your cache and reload all files from the server. This may take a few moments.', [ { text: 'Cancel', style: 'cancel' }, { @@ -333,13 +333,13 @@ export default function SettingsScreen({ onLogout }: Props) { items: [ { title: 'Usage & Limits', - subtitle: 'View storage and note limits', + subtitle: 'View storage and typelet limits', icon: 'pie-chart-outline', onPress: () => usageSheetRef.current?.present(), }, { title: 'Cache Status', - subtitle: `${cacheStats.noteCount} notes, ${cacheStats.folderCount} folders • ${cacheStats.cacheSizeMB} MB • ${cacheDecrypted ? 'Decrypted' : 'Encrypted'}`, + subtitle: `${cacheStats.noteCount} files, ${cacheStats.folderCount} folders • ${cacheStats.cacheSizeMB} MB • ${cacheDecrypted ? 'Decrypted' : 'Encrypted'}`, icon: 'server-outline', onPress: undefined, }, @@ -351,7 +351,7 @@ export default function SettingsScreen({ onLogout }: Props) { }, { title: 'Refresh Cache', - subtitle: 'Re-download all notes from server', + subtitle: 'Re-download all files from server', icon: 'refresh-outline', onPress: handleRefreshCache, }, @@ -635,13 +635,13 @@ export default function SettingsScreen({ onLogout }: Props) { { value: true, title: 'Cache Decrypted', - subtitle: 'Store decrypted notes locally for instant loading. Requires master password on app start.', + subtitle: 'Store decrypted files locally for instant loading. Requires master password on app start.', icon: 'flash' }, { value: false, title: 'Cache Encrypted', - subtitle: 'Only store encrypted notes. Slightly slower but maximum security.', + subtitle: 'Only store encrypted files. Slightly slower but maximum security.', icon: 'shield-checkmark' } ] as const).map((option) => ( @@ -667,8 +667,8 @@ export default function SettingsScreen({ onLogout }: Props) { Alert.alert( 'Cache Preference Updated', newValue - ? 'Decrypted notes will be cached for instant loading. Close and reopen the app to see the effect.' - : 'Only encrypted notes will be cached. Decrypted cache has been cleared.', + ? 'Decrypted files will be cached for instant loading. Close and reopen the app to see the effect.' + : 'Only encrypted files will be cached. Decrypted cache has been cleared.', [{ text: 'OK' }] ); }} @@ -848,7 +848,7 @@ export default function SettingsScreen({ onLogout }: Props) { - Your notes are encrypted with AES-256 encryption using your master password. We never have access to your decryption key or unencrypted data. + Your files are encrypted with AES-256 encryption using your master password. We never have access to your decryption key or unencrypted data. @@ -860,7 +860,7 @@ export default function SettingsScreen({ onLogout }: Props) { - {`Your master password is the only key to decrypt your notes. It's never stored on our servers and cannot be recovered if lost.`} + {`Your master password is the only key to decrypt your files. It's never stored on our servers and cannot be recovered if lost.`} @@ -884,7 +884,7 @@ export default function SettingsScreen({ onLogout }: Props) { - {`We can't read your notes, recover your password, or access your data. Your privacy is guaranteed by design.`} + {`We can't read your files, recover your password, or access your data. Your privacy is guaranteed by design.`} @@ -896,7 +896,7 @@ export default function SettingsScreen({ onLogout }: Props) { - Notes are cached encrypted by default - decrypted only when viewed. Enable “Cache Decrypted” for instant loading at the cost of storing decrypted data locally. + Typelets are cached encrypted by default - decrypted only when viewed. Enable “Cache Decrypted” for instant loading at the cost of storing decrypted data locally. @@ -1000,7 +1000,7 @@ export default function SettingsScreen({ onLogout }: Props) { - Deleting your account will permanently remove all your notes, folders, and data. This action cannot be undone. + Deleting your account will permanently remove all your files, folders, and data. This action cannot be undone. diff --git a/apps/mobile/v1/src/screens/ViewNote/NoteContent.tsx b/apps/mobile/v1/src/screens/ViewNote/NoteContent.tsx index a95bceb..837baf8 100644 --- a/apps/mobile/v1/src/screens/ViewNote/NoteContent.tsx +++ b/apps/mobile/v1/src/screens/ViewNote/NoteContent.tsx @@ -2,6 +2,7 @@ import React, { useRef, useState, useMemo } from 'react'; import { Animated, Dimensions, ScrollView, StyleSheet, Text, View } from 'react-native'; import { WebView } from 'react-native-webview'; +import { SheetsViewer } from '../../components/SheetsViewer'; import type { Note } from '../../services/api'; interface NoteContentProps { @@ -10,6 +11,7 @@ interface NoteContentProps { scrollY: Animated.Value; scrollViewRef: React.RefObject; showTitle?: boolean; + bottomInset?: number; theme: { colors: { foreground: string; @@ -31,6 +33,7 @@ export function NoteContent({ note, htmlContent, showTitle = true, + bottomInset = 0, theme, }: NoteContentProps) { const webViewRef = useRef(null); @@ -42,8 +45,9 @@ export function NoteContent({ // Calculate hairline width for CSS (equivalent to StyleSheet.hairlineWidth) const cssHairlineWidth = `${StyleSheet.hairlineWidth}px`; - // Check if this is a diagram note + // Check note type const isDiagram = note.type === 'diagram'; + const isSheet = note.type === 'sheets'; // Enhanced HTML with optional title and metadata // Memoized to prevent expensive re-generation on every render @@ -411,6 +415,25 @@ ${note.content} cssHairlineWidth, // Re-generate if hairline width changes (rare) ]); + // For sheets, render the SheetsViewer component + if (isSheet) { + return ( + + {note.hidden ? ( + + + [HIDDEN] + + + ) : ( + + )} + + ); + } + return ( {note.hidden ? ( @@ -472,6 +495,9 @@ const styles = StyleSheet.create({ container: { // No flex needed - inside ScrollView }, + sheetsContainer: { + flex: 1, + }, webview: { backgroundColor: 'transparent', // Height set inline based on content or screen size diff --git a/apps/mobile/v1/src/screens/ViewNote/ViewHeader.tsx b/apps/mobile/v1/src/screens/ViewNote/ViewHeader.tsx index a0c3f78..ed4e602 100644 --- a/apps/mobile/v1/src/screens/ViewNote/ViewHeader.tsx +++ b/apps/mobile/v1/src/screens/ViewNote/ViewHeader.tsx @@ -17,6 +17,7 @@ interface ViewHeaderProps { showAttachments: boolean; isOffline?: boolean; isTempNote?: boolean; + isEditDisabled?: boolean; insets?: { top: number; bottom: number; left: number; right: number }; onBack: () => void; onToggleStar: () => void; @@ -48,6 +49,7 @@ export function ViewHeader({ showAttachments, isOffline = false, isTempNote = false, + isEditDisabled = false, insets, onBack, onToggleStar, @@ -173,17 +175,19 @@ export function ViewHeader({ - - - - - - - + {!isEditDisabled && ( + + + + + + + + )} diff --git a/apps/mobile/v1/src/screens/ViewNote/index.tsx b/apps/mobile/v1/src/screens/ViewNote/index.tsx index 9cac849..d15b90b 100644 --- a/apps/mobile/v1/src/screens/ViewNote/index.tsx +++ b/apps/mobile/v1/src/screens/ViewNote/index.tsx @@ -40,8 +40,14 @@ export default function ViewNoteScreen() { const { note, loading, htmlContent, handleEdit: handleEditInternal, handleToggleStar, handleToggleHidden, refresh, updateNoteLocally } = useViewNote(noteId as string); const [refreshing, setRefreshing] = useState(false); - // Wrap handleEdit with offline check (allow editing temp notes created offline) + // Wrap handleEdit with offline check and sheets check const handleEdit = () => { + // Show coming soon message for sheets + if (note?.type === 'sheets') { + Alert.alert('Coming Soon', 'Spreadsheet editing is coming in a future update. Stay tuned!'); + return; + } + const isTempNote = (noteId as string).startsWith('temp_'); if (!isOnline && !isTempNote) { Alert.alert('Offline', 'You cannot edit synced notes while offline. Please connect to the internet and try again.'); @@ -192,9 +198,9 @@ export default function ViewNoteScreen() { @@ -286,6 +293,7 @@ export default function ViewNoteScreen() { showAttachments={showAttachments} isOffline={!isOnline} isTempNote={(noteId as string).startsWith('temp_')} + isEditDisabled={false} insets={insets} onBack={() => router.back()} onToggleStar={handleToggleStar} diff --git a/apps/mobile/v1/src/services/api/notes/core.ts b/apps/mobile/v1/src/services/api/notes/core.ts index 76035d1..beb1135 100644 --- a/apps/mobile/v1/src/services/api/notes/core.ts +++ b/apps/mobile/v1/src/services/api/notes/core.ts @@ -634,7 +634,20 @@ export function createNotesApi(getToken: AuthTokenGetter, getUserId: () => strin // Decrypt note if we have a user ID if (userId) { - return await decryptNote(note, userId); + const decryptedNote = await decryptNote(note, userId); + + // Cache the note locally for quick access (especially important for sheets) + try { + await storeCachedNotes([decryptedNote], { storeDecrypted: true }); + if (__DEV__) { + console.log(`[API] ✅ Cached note ${noteId} for quick access`); + } + } catch (cacheError) { + // Don't fail if cache write fails + console.warn('[API] Failed to cache note:', cacheError); + } + + return decryptedNote; } return note; diff --git a/apps/mobile/v1/src/services/api/publicNotes.ts b/apps/mobile/v1/src/services/api/publicNotes.ts index 57cbd50..979db03 100644 --- a/apps/mobile/v1/src/services/api/publicNotes.ts +++ b/apps/mobile/v1/src/services/api/publicNotes.ts @@ -10,7 +10,7 @@ export interface PublishNoteParams { noteId: string; title: string; content: string; - type?: 'note' | 'diagram' | 'code'; + type?: 'note' | 'diagram' | 'code' | 'sheets'; authorName?: string; } @@ -18,7 +18,7 @@ export interface PublishNoteResponse { slug: string; title: string; content: string; - type?: 'note' | 'diagram' | 'code'; + type?: 'note' | 'diagram' | 'code' | 'sheets'; authorName?: string; publishedAt: string; updatedAt: string; diff --git a/apps/mobile/v1/src/services/api/types.ts b/apps/mobile/v1/src/services/api/types.ts index 216b61c..0210b92 100644 --- a/apps/mobile/v1/src/services/api/types.ts +++ b/apps/mobile/v1/src/services/api/types.ts @@ -31,7 +31,7 @@ export interface Note { id: string; title: string; content: string; - type?: 'note' | 'diagram' | 'code'; // Type of note: regular note, diagram, or code + type?: 'note' | 'diagram' | 'code' | 'sheets'; // Type of note: regular note, diagram, code, or spreadsheet folderId?: string; userId: string; starred: boolean; @@ -63,7 +63,7 @@ export interface ApiPublicNote { userId: string; title: string; content: string; - type?: 'note' | 'diagram' | 'code'; + type?: 'note' | 'diagram' | 'code' | 'sheets'; authorName?: string; publishedAt: string; updatedAt: string; diff --git a/apps/mobile/v1/src/utils/noteTypeDetection.ts b/apps/mobile/v1/src/utils/noteTypeDetection.ts index 24a3c51..2eb3547 100644 --- a/apps/mobile/v1/src/utils/noteTypeDetection.ts +++ b/apps/mobile/v1/src/utils/noteTypeDetection.ts @@ -5,7 +5,7 @@ import type { Note } from '@/src/services/api'; -export type NoteType = 'diagram' | 'code' | 'note'; +export type NoteType = 'diagram' | 'code' | 'sheets' | 'note'; /** * Detects if content contains diagram syntax @@ -51,10 +51,28 @@ export function isCodeContent(content: string): boolean { ); } +/** + * Detects if content is a Univer workbook (spreadsheet) + */ +export function isWorkbookContent(content: string): boolean { + if (!content) return false; + + try { + const parsed = JSON.parse(content); + return ( + parsed.sheets !== undefined || + parsed.sheetOrder !== undefined || + (parsed.id && typeof parsed.id === 'string' && parsed.id.startsWith('workbook')) + ); + } catch { + return false; + } +} + /** * Detects the type of a note based on its content * @param note - The note to analyze - * @returns The detected note type: 'diagram', 'code', or 'note' + * @returns The detected note type: 'diagram', 'code', 'sheets', or 'note' */ export function detectNoteType(note: Note): NoteType { // If type is already set, use it @@ -67,7 +85,12 @@ export function detectNoteType(note: Note): NoteType { return 'note'; } - // Check for diagrams first (more specific) + // Check for spreadsheets first + if (isWorkbookContent(note.content)) { + return 'sheets'; + } + + // Check for diagrams (more specific) if (isDiagramContent(note.content)) { return 'diagram'; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c0c74f..82b2b69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16732,7 +16732,7 @@ snapshots: eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -16772,7 +16772,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -16822,7 +16822,7 @@ snapshots: - supports-color - typescript - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9