From 8a37b714eeb4d0dff84daebf8a0bbaf2b801a3f9 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Fri, 6 Mar 2026 14:31:30 +0100 Subject: [PATCH 1/2] feat: create main sharing extesion interface --- app.config.ts | 3 +- assets/icons/stacked-files.svg | 14 ++ assets/lang/strings.ts | 26 +++ index.share.js | 10 +- ios/Internxt/AppDelegate.swift | 31 +--- .../ShareExtensionViewController.swift | 29 +-- package.json | 1 + src/App.tsx | 1 + src/services/AsyncStorageService.ts | 11 ++ src/services/SecureStorageService.ts | 11 +- src/shareExtension/AndroidShareScreen.tsx | 104 ----------- src/shareExtension/ShareExtensionApp.tsx | 148 ---------------- .../ShareExtensionView.android.tsx | 51 ++++++ src/shareExtension/ShareExtensionView.ios.tsx | 51 ++++++ src/shareExtension/components/DriveHeader.tsx | 39 ++++ .../components/DriveScreen/DriveList.tsx | 85 +++++++++ .../components/DriveScreen/RootHeader.tsx | 40 +++++ .../components/DriveScreen/SortRow.tsx | 35 ++++ .../DriveScreen/SubfolderHeader.tsx | 88 ++++++++++ .../components/FileListItem.tsx | 82 +++++++++ src/shareExtension/components/TextButton.tsx | 27 +++ src/shareExtension/hooks/useNavAnimation.ts | 50 ++++++ .../hooks/useSearchAnimation.ts | 66 +++++++ src/shareExtension/screens/DriveScreen.tsx | 141 +++++++++++++++ .../screens/NotSignedInScreen.tsx | 166 +++++++----------- src/shareExtension/theme.ts | 37 ++++ src/shareExtension/types.ts | 16 ++ src/types/tailwind-rn.d.ts | 18 +- yarn.lock | 5 + 29 files changed, 985 insertions(+), 401 deletions(-) create mode 100644 assets/icons/stacked-files.svg delete mode 100644 src/shareExtension/AndroidShareScreen.tsx delete mode 100644 src/shareExtension/ShareExtensionApp.tsx create mode 100644 src/shareExtension/ShareExtensionView.android.tsx create mode 100644 src/shareExtension/ShareExtensionView.ios.tsx create mode 100644 src/shareExtension/components/DriveHeader.tsx create mode 100644 src/shareExtension/components/DriveScreen/DriveList.tsx create mode 100644 src/shareExtension/components/DriveScreen/RootHeader.tsx create mode 100644 src/shareExtension/components/DriveScreen/SortRow.tsx create mode 100644 src/shareExtension/components/DriveScreen/SubfolderHeader.tsx create mode 100644 src/shareExtension/components/FileListItem.tsx create mode 100644 src/shareExtension/components/TextButton.tsx create mode 100644 src/shareExtension/hooks/useNavAnimation.ts create mode 100644 src/shareExtension/hooks/useSearchAnimation.ts create mode 100644 src/shareExtension/screens/DriveScreen.tsx create mode 100644 src/shareExtension/theme.ts diff --git a/app.config.ts b/app.config.ts index 6005db89a..8009a16c0 100644 --- a/app.config.ts +++ b/app.config.ts @@ -151,8 +151,7 @@ const appConfig: ExpoConfig & { extra: AppEnv & { NODE_ENV: AppStage; RELEASE_ID 'react-native-create-thumbnail', 'jail-monkey', ], - backgroundColor: { red: 255, green: 255, blue: 255, alpha: 1 }, - height: 700, + backgroundColor: { red: 0, green: 0, blue: 0, alpha: 0 }, }, ], ], diff --git a/assets/icons/stacked-files.svg b/assets/icons/stacked-files.svg new file mode 100644 index 000000000..06ab0c64d --- /dev/null +++ b/assets/icons/stacked-files.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/lang/strings.ts b/assets/lang/strings.ts index 6962db664..2d2593951 100644 --- a/assets/lang/strings.ts +++ b/assets/lang/strings.ts @@ -329,6 +329,19 @@ const translations = { subtitle: 'Log in to upload files securely', openLogin: 'Open Internxt login', }, + multipleFormats: 'Multiple formats', + fileNameFallback: 'File', + rename: 'Rename', + itemsSelected: '{0} items selected', + searchPlaceholder: 'Search in Drive', + rootFolderName: 'Drive', + noResults: 'No results found', + emptyFolder: 'This folder is empty', + sortByName: 'Name ↑', + folderNameLabel: 'Name', + folderNamePlaceholder: 'Folder name', + folderNameEmpty: 'Folder name cannot be empty', + folderCreateError: 'Failed to create folder. Please try again.', }, }, buttons: { @@ -1141,6 +1154,19 @@ const translations = { subtitle: 'Inicia sesión para subir archivos de forma segura', openLogin: 'Abrir login de Internxt', }, + multipleFormats: 'Múltiples formatos', + fileNameFallback: 'Archivo', + rename: 'Renombrar', + itemsSelected: '{0} elementos seleccionados', + searchPlaceholder: 'Buscar en Drive', + rootFolderName: 'Drive', + noResults: 'Sin resultados', + emptyFolder: 'Esta carpeta está vacía', + sortByName: 'Nombre ↑', + folderNameLabel: 'Nombre', + folderNamePlaceholder: 'Nombre de carpeta', + folderNameEmpty: 'El nombre no puede estar vacío', + folderCreateError: 'Error al crear la carpeta. Inténtalo de nuevo.', }, }, buttons: { diff --git a/index.share.js b/index.share.js index 301ebbfec..4b7880070 100644 --- a/index.share.js +++ b/index.share.js @@ -1,4 +1,10 @@ +import { createElement } from 'react'; import { AppRegistry } from 'react-native'; -import ShareExtensionApp from './src/shareExtension/ShareExtensionApp'; +import { TailwindProvider } from 'tailwind-rn'; +import ShareExtensionView from './src/shareExtension/ShareExtensionView.ios'; +import utilities from './src/styles/tailwind.json'; -AppRegistry.registerComponent('shareExtension', () => ShareExtensionApp); +AppRegistry.registerComponent( + 'shareExtension', + () => (props) => createElement(TailwindProvider, { utilities }, createElement(ShareExtensionView, props)), +); diff --git a/ios/Internxt/AppDelegate.swift b/ios/Internxt/AppDelegate.swift index 0ec1e60c0..6bd807c8b 100644 --- a/ios/Internxt/AppDelegate.swift +++ b/ios/Internxt/AppDelegate.swift @@ -44,37 +44,24 @@ public class AppDelegate: ExpoAppDelegate { // MARK: - App Group auth sync private func syncAuthStatusToAppGroup() { - guard let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String, - let defaults = UserDefaults(suiteName: appGroup), - let sharedGroup = Bundle.main.object(forInfoDictionaryKey: "SharedKeychainGroup") as? String + guard let sharedGroup = Bundle.main.object(forInfoDictionaryKey: "SharedKeychainGroup") as? String else { return } let isAuthenticated = privateKeychainItemExists(key: "photosToken") - defaults.set(isAuthenticated, forKey: "isAuthenticated") if isAuthenticated { - copyToSharedKeychain(privateKey: "photosToken", sharedKey: "shared_photosToken", accessGroup: sharedGroup) - copyToSharedKeychain(privateKey: "xUser_mnemonic", sharedKey: "shared_mnemonic", accessGroup: sharedGroup) - defaults.set(readEmailFromKeychain(), forKey: "userEmail") + copyToSharedKeychain(privateKey: "photosToken", sharedKey: "shared_photosToken", accessGroup: sharedGroup) + copyToSharedKeychain(privateKey: "xUser_mnemonic", sharedKey: "shared_mnemonic", accessGroup: sharedGroup) + copyToSharedKeychain(privateKey: "xUser_rootFolderId", sharedKey: "shared_rootFolderId", accessGroup: sharedGroup) + copyToSharedKeychain(privateKey: "xUser_bucket", sharedKey: "shared_bucket", accessGroup: sharedGroup) } else { - deleteFromSharedKeychain(key: "shared_photosToken", accessGroup: sharedGroup) - deleteFromSharedKeychain(key: "shared_mnemonic", accessGroup: sharedGroup) - defaults.removeObject(forKey: "userEmail") + deleteFromSharedKeychain(key: "shared_photosToken", accessGroup: sharedGroup) + deleteFromSharedKeychain(key: "shared_mnemonic", accessGroup: sharedGroup) + deleteFromSharedKeychain(key: "shared_rootFolderId", accessGroup: sharedGroup) + deleteFromSharedKeychain(key: "shared_bucket", accessGroup: sharedGroup) } } - private func readEmailFromKeychain() -> String? { - guard let data = readFromPrivateKeychain(key: "xUser_data"), - var raw = String(data: data, encoding: .utf8) else { return nil } - if raw.hasPrefix("\"") && raw.hasSuffix("\"") { - raw = String(raw.dropFirst().dropLast()) - } - guard let jsonData = raw.data(using: .utf8), - let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any], - let email = json["email"] as? String else { return nil } - return email - } - private func privateKeychainItemExists(key: String) -> Bool { return readFromPrivateKeychain(key: key) != nil } diff --git a/ios/InternxtShareExtension/ShareExtensionViewController.swift b/ios/InternxtShareExtension/ShareExtensionViewController.swift index 4958ed645..ad235979a 100644 --- a/ios/InternxtShareExtension/ShareExtensionViewController.swift +++ b/ios/InternxtShareExtension/ShareExtensionViewController.swift @@ -64,6 +64,7 @@ class ShareExtensionViewController: UIViewController { super.viewDidLoad() setupLoadingIndicator() isCleanedUp = false + self.view.backgroundColor = .clear self.view.contentScaleFactor = UIScreen.main.scale #if canImport(FirebaseCore) @@ -96,21 +97,12 @@ class ShareExtensionViewController: UIViewController { reactNativeFactory = RCTReactNativeFactory(delegate: reactNativeFactoryDelegate!) var initialProps = sharedData ?? [:] - // ── Internxt: inject auth state from UserDefaults and Keychain ────────── - if let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String, - let defaults = UserDefaults(suiteName: appGroup) { - initialProps["isAuthenticated"] = defaults.bool(forKey: "isAuthenticated") - initialProps["userEmail"] = defaults.string(forKey: "userEmail") - } - + // ── Internxt: inject auth state from Keychain ─────────────────────────── if let sharedGroup = Bundle.main.object(forInfoDictionaryKey: "SharedKeychainGroup") as? String { - initialProps["photosToken"] = readFromSharedKeychain(key: "shared_photosToken", accessGroup: sharedGroup) - if let raw = readFromSharedKeychain(key: "shared_mnemonic", accessGroup: sharedGroup) { - // Strip JSON string encoding added by expo-secure-store: '"words"' → 'words' - initialProps["mnemonic"] = (raw.hasPrefix("\"") && raw.hasSuffix("\"")) - ? String(raw.dropFirst().dropLast()) - : raw - } + initialProps["photosToken"] = readFromSharedKeychain(key: "shared_photosToken", accessGroup: sharedGroup) + initialProps["mnemonic"] = readFromSharedKeychainStripped(key: "shared_mnemonic", accessGroup: sharedGroup) + initialProps["rootFolderId"] = readFromSharedKeychainStripped(key: "shared_rootFolderId", accessGroup: sharedGroup) + initialProps["bucket"] = readFromSharedKeychainStripped(key: "shared_bucket", accessGroup: sharedGroup) } // ── From expo-share-extension library ────────────────────────────────── let currentBounds = self.view.bounds @@ -275,6 +267,15 @@ class ShareExtensionViewController: UIViewController { let value = String(data: data, encoding: .utf8) else { return nil } return value } + + /// Reads a shared-keychain entry and strips surrounding JSON quotes if present. + private func readFromSharedKeychainStripped(key: String, accessGroup: String) -> String? { + guard let raw = readFromSharedKeychain(key: key, accessGroup: accessGroup) else { return nil } + if raw.hasPrefix("\"") && raw.hasSuffix("\"") { + return String(raw.dropFirst().dropLast()) + } + return raw + } // ───────────────────────────────────────────────────────────────────────── // ── From expo-share-extension library ───────────────────────────────────── diff --git a/package.json b/package.json index 8ad3bd39e..be0aa0110 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "axios": "^1.13.5", "base-64": "^1.0.0", "crypto-js": "=3.1.9-1", + "dayjs": "^1.11.19", "events": "^3.3.0", "expo": "^54", "expo-asset": "~12.0.12", diff --git a/src/App.tsx b/src/App.tsx index f0bbb4214..5a3fa42ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -125,6 +125,7 @@ function AppContent(): JSX.Element { if (needsMigration) { await asyncStorageService.migrateToSecureStorage(); } + await asyncStorageService.migrateShareExtensionCriticalFields(); }; useEffect(() => { diff --git a/src/services/AsyncStorageService.ts b/src/services/AsyncStorageService.ts index 1883ba9b1..00bdfd846 100644 --- a/src/services/AsyncStorageService.ts +++ b/src/services/AsyncStorageService.ts @@ -185,6 +185,17 @@ class AsyncStorageService { } } + async migrateShareExtensionCriticalFields(): Promise { + const alreadyMigrated = await secureStorageService.hasItem('xUser_rootFolderId'); + if (alreadyMigrated) return; + + const user = await this.getUser(); + if (!user?.rootFolderId) return; + + await this.saveItem(AsyncStorageKey.User, JSON.stringify(user)); + logger.info('Share extension critical fields migrated (rootFolderId, bucket)'); + } + async checkNeedsMigration(): Promise<{ needsMigration: boolean; itemsToMigrate: string[] }> { const itemsToMigrate: string[] = []; diff --git a/src/services/SecureStorageService.ts b/src/services/SecureStorageService.ts index 2ff6fb9af..b4f8d6927 100644 --- a/src/services/SecureStorageService.ts +++ b/src/services/SecureStorageService.ts @@ -3,7 +3,16 @@ import * as SecureStore from 'expo-secure-store'; import { AsyncStorageKey } from '../types'; import { UserData, UserKeysHandler } from './UserKeysHandler'; -const CRITICAL_USER_FIELDS = ['mnemonic', 'privateKey', 'publicKey', 'keys', 'revocationKey', 'revocateKey']; +const CRITICAL_USER_FIELDS = [ + 'mnemonic', + 'privateKey', + 'publicKey', + 'keys', + 'revocationKey', + 'revocateKey', + 'rootFolderId', + 'bucket', +]; class SecureStorageService { private readonly MAX_CHUNK_SIZE = 1800; diff --git a/src/shareExtension/AndroidShareScreen.tsx b/src/shareExtension/AndroidShareScreen.tsx deleted file mode 100644 index 117c30863..000000000 --- a/src/shareExtension/AndroidShareScreen.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useEffect, useState } from 'react'; -import { ActivityIndicator, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import { useTailwind } from 'tailwind-rn'; -import strings from '../../assets/lang/strings'; -import asyncStorageService from '../services/AsyncStorageService'; -import { AsyncStorageKey } from '../types'; -import { RootStackScreenProps } from '../types/navigation'; -import { useShareAuth } from './hooks/useShareAuth.android'; -import { NotSignedInScreen } from './screens/NotSignedInScreen'; - -interface DebugInfo { - userEmail: string | null; - photosToken: string | null; - mnemonic: string | null; -} - -const AndroidShareScreen = ({ navigation, route }: RootStackScreenProps<'AndroidShare'>) => { - const tailwind = useTailwind(); - const authStatus = useShareAuth(); - const [debug, setDebug] = useState({ userEmail: null, photosToken: null, mnemonic: null }); - - useEffect(() => { - if (authStatus !== 'authenticated') return; - Promise.all([asyncStorageService.getUser(), asyncStorageService.getItem(AsyncStorageKey.PhotosToken)]).then( - ([user, photosToken]) => { - setDebug({ - userEmail: user?.email ?? null, - photosToken, - mnemonic: user?.mnemonic ?? null, - }); - }, - ); - }, [authStatus]); - - if (authStatus === 'loading') { - return ( - - - - ); - } - - if (authStatus === 'unauthenticated') { - return navigation.goBack()} onOpenLogin={() => navigation.navigate('SignIn')} />; - } - - const translations = strings.screens.ShareExtension; - const files = route.params?.files ?? []; - - return ( - - - navigation.goBack()} style={tailwind('w-8 h-8 items-center justify-center')}> - - - {translations.title} - - - - {debug.userEmail ? ( - - Signed in as - {debug.userEmail} - - ) : null} - {debug.photosToken ? ( - - Signed new token - {debug.photosToken} - - ) : null} - {debug.mnemonic ? ( - - Signed in with mnemonic - {debug.mnemonic} - - ) : null} - - - {files.map((file) => { - const name = file.fileName ?? file.uri.split('/').pop() ?? file.uri; - return ( - - - {name} - - - ); - })} - - - ); -}; - -const styles = StyleSheet.create({ - semibold: { fontWeight: '600' }, - blueSection: { - backgroundColor: 'rgba(0,102,255,0.05)', - borderBottomWidth: 1, - borderBottomColor: 'rgba(0,102,255,0.2)', - }, -}); - -export default AndroidShareScreen; diff --git a/src/shareExtension/ShareExtensionApp.tsx b/src/shareExtension/ShareExtensionApp.tsx deleted file mode 100644 index f8c6c0c74..000000000 --- a/src/shareExtension/ShareExtensionApp.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { close, openHostApp } from 'expo-share-extension'; -import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import strings from '../../assets/lang/strings'; -import { NotSignedInScreen } from './screens/NotSignedInScreen'; - -interface ShareExtensionProps { - isAuthenticated?: boolean; - userEmail?: string; - photosToken?: string; - mnemonic?: string; - files?: string[]; - images?: string[]; - videos?: string[]; - url?: string; - text?: string; -} - -const ShareExtensionApp = (props: ShareExtensionProps) => { - const translations = strings.screens.ShareExtension; - - if (!props.isAuthenticated) { - return openHostApp('sign-in')} />; - } - - const allFiles = [...(props.files ?? []), ...(props.images ?? []), ...(props.videos ?? [])]; - - return ( - - - - - - {translations.title} - - - {props.userEmail ? ( - - Signed in as - {props.userEmail} - - ) : null} - {props.photosToken ? ( - - Signed new token - {props.photosToken} - - ) : null} - {props.mnemonic ? ( - - Signed in with mnemonic - {props.mnemonic} - - ) : null} - - {allFiles.map((filePath) => { - const name = filePath.split('/').pop() ?? filePath; - return ( - - - {name} - - - ); - })} - {props.url ? ( - - - {props.url} - - - ) : null} - {props.text ? ( - - - {props.text} - - - ) : null} - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#ffffff', - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 16, - paddingTop: 16, - paddingBottom: 12, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#e5e7eb', - }, - closeBtn: { - width: 32, - height: 32, - alignItems: 'center', - justifyContent: 'center', - }, - closeText: { - fontSize: 18, - color: '#6b7280', - }, - title: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - }, - content: { - padding: 16, - gap: 8, - }, - fileRow: { - backgroundColor: '#f9fafb', - borderRadius: 8, - padding: 12, - borderWidth: StyleSheet.hairlineWidth, - borderColor: '#e5e7eb', - }, - fileName: { - fontSize: 14, - color: '#374151', - }, - sessionBanner: { - backgroundColor: '#f0f7ff', - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#bfdbfe', - paddingHorizontal: 16, - paddingVertical: 10, - }, - sessionLabel: { - fontSize: 11, - color: '#6b7280', - marginBottom: 2, - }, - sessionEmail: { - fontSize: 13, - fontWeight: '600', - color: '#0066FF', - }, -}); - -export default ShareExtensionApp; diff --git a/src/shareExtension/ShareExtensionView.android.tsx b/src/shareExtension/ShareExtensionView.android.tsx new file mode 100644 index 000000000..c0c59c72a --- /dev/null +++ b/src/shareExtension/ShareExtensionView.android.tsx @@ -0,0 +1,51 @@ +import { useCallback } from 'react'; +import { ActivityIndicator, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTailwind } from 'tailwind-rn'; +import { RootStackScreenProps } from '../types/navigation'; +import { useShareExtension } from './hooks/useShareExtension.android'; +import { DriveScreen } from './screens/DriveScreen'; +import { NotSignedInScreen } from './screens/NotSignedInScreen'; +import { colors } from './theme'; + +const ShareExtensionView = ({ navigation, route }: RootStackScreenProps<'AndroidShare'>) => { + const tailwind = useTailwind(); + const { status, rootFolderUuid, sharedFiles } = useShareExtension(route.params?.files ?? []); + + const handleClose = useCallback(() => navigation.goBack(), [navigation]); + const handleOpenLogin = useCallback(() => navigation.navigate('SignIn'), [navigation]); + const handleSave = useCallback(() => { + //upload logic + }, []); + + if (status === 'unauthenticated') { + return ( + + + + ); + } + + if (status === 'loading' || !rootFolderUuid) { + return ( + + + + + + ); + } + + return ( + + + + ); +}; + +export default ShareExtensionView; diff --git a/src/shareExtension/ShareExtensionView.ios.tsx b/src/shareExtension/ShareExtensionView.ios.tsx new file mode 100644 index 000000000..1dbc7ed36 --- /dev/null +++ b/src/shareExtension/ShareExtensionView.ios.tsx @@ -0,0 +1,51 @@ +import { close, openHostApp } from 'expo-share-extension'; +import { ActivityIndicator, View } from 'react-native'; +import { useTailwind } from 'tailwind-rn'; +import { useShareExtension } from './hooks/useShareExtension.ios'; +import { DriveScreen } from './screens/DriveScreen'; +import { NotSignedInScreen } from './screens/NotSignedInScreen'; +import { colors } from './theme'; + +interface ShareExtensionProps { + photosToken?: string; + mnemonic?: string; + rootFolderId?: string; + bucket?: string; + files?: string[]; + images?: string[]; + videos?: string[]; + url?: string; + text?: string; +} + +const ShareExtensionView = (props: ShareExtensionProps) => { + const tailwind = useTailwind(); + const { sdkReady, sharedFiles } = useShareExtension(props); + + const handleSave = () => { + // Upload logic + }; + + if (!props.photosToken) { + return openHostApp('sign-in')} />; + } + + if (!sdkReady || !props.rootFolderId) { + return ( + + + + ); + } + + return ( + + ); +}; + +export default ShareExtensionView; diff --git a/src/shareExtension/components/DriveHeader.tsx b/src/shareExtension/components/DriveHeader.tsx new file mode 100644 index 000000000..0ddd89277 --- /dev/null +++ b/src/shareExtension/components/DriveHeader.tsx @@ -0,0 +1,39 @@ +import { XIcon } from 'phosphor-react-native'; +import { Text, TouchableOpacity, View } from 'react-native'; +import { useTailwind } from 'tailwind-rn'; +import strings from '../../../assets/lang/strings'; +import { colors, fontStyles } from '../theme'; + +interface DriveHeaderProps { + onClose: () => void; + onSave: () => void; + saveEnabled: boolean; +} + +export const DriveHeader = ({ onClose, onSave, saveEnabled }: DriveHeaderProps) => { + const tailwind = useTailwind(); + + return ( + + + + + + + {strings.screens.ShareExtension.title} + + + + {strings.buttons.save} + + + ); +}; diff --git a/src/shareExtension/components/DriveScreen/DriveList.tsx b/src/shareExtension/components/DriveScreen/DriveList.tsx new file mode 100644 index 000000000..63f7db054 --- /dev/null +++ b/src/shareExtension/components/DriveScreen/DriveList.tsx @@ -0,0 +1,85 @@ +import { DriveListViewMode } from '@internxt-mobile/types/drive/ui'; +import { useCallback } from 'react'; +import { FlatList, Keyboard, Text, View } from 'react-native'; +import DriveItemSkinSkeleton from 'src/components/DriveItemSkinSkeleton'; +import { useTailwind } from 'tailwind-rn'; +import strings from '../../../../assets/lang/strings'; +import { fontStyles } from '../../theme'; +import { ShareFileItem, ShareFolderItem } from '../../types'; +import { FileListItem } from '../FileListItem'; + +export type DriveListItem = { type: 'folder'; data: ShareFolderItem } | { type: 'file'; data: ShareFileItem }; + +const keyExtractor = (item: DriveListItem) => item.data.uuid; + +interface DriveListProps { + listData: DriveListItem[]; + viewMode: DriveListViewMode; + loading: boolean; + loadingMore: boolean; + searchQuery: string; + onNavigate: (uuid: string, name: string) => void; + onLoadMore: () => void; +} + +export const DriveList = ({ + listData, + viewMode, + loading, + loadingMore, + searchQuery, + onNavigate, + onLoadMore, +}: DriveListProps) => { + const tailwind = useTailwind(); + const numColumns = viewMode === DriveListViewMode.Grid ? 3 : 1; + + const renderItem = useCallback( + ({ item }: { item: DriveListItem }) => { + const isFolder = item.type === 'folder'; + const handleItemPress = isFolder ? () => onNavigate(item.data.uuid, item.data.plainName) : undefined; + return ; + }, + [onNavigate, viewMode], + ); + + if (loading) { + return ( + + {Array.from({ length: 10 }).map((_, i) => ( + + + + ))} + + ); + } + + return ( + + + {searchQuery ? strings.screens.ShareExtension.noResults : strings.screens.ShareExtension.emptyFolder} + + + } + ListFooterComponent={ + loadingMore ? ( + + + + ) : null + } + onEndReached={onLoadMore} + onEndReachedThreshold={0.5} + onScrollBeginDrag={Keyboard.dismiss} + /> + ); +}; diff --git a/src/shareExtension/components/DriveScreen/RootHeader.tsx b/src/shareExtension/components/DriveScreen/RootHeader.tsx new file mode 100644 index 000000000..ed7574c35 --- /dev/null +++ b/src/shareExtension/components/DriveScreen/RootHeader.tsx @@ -0,0 +1,40 @@ +import { MagnifyingGlassIcon } from 'phosphor-react-native'; +import { Text, TextInput, View } from 'react-native'; +import { useTailwind } from 'tailwind-rn'; +import strings from '../../../../assets/lang/strings'; +import { colors, fontStyles } from '../../theme'; +import { TextButton } from '../TextButton'; + +interface RootHeaderProps { + searchQuery: string; + onChangeSearch: (value: string) => void; + onNewFolder: () => void; +} + +export const RootHeader = ({ searchQuery, onChangeSearch, onNewFolder }: RootHeaderProps) => { + const tailwind = useTailwind(); + + return ( + <> + + + {strings.screens.ShareExtension.rootFolderName} + + + + + + + + + + ); +}; diff --git a/src/shareExtension/components/DriveScreen/SortRow.tsx b/src/shareExtension/components/DriveScreen/SortRow.tsx new file mode 100644 index 000000000..3b6f2967f --- /dev/null +++ b/src/shareExtension/components/DriveScreen/SortRow.tsx @@ -0,0 +1,35 @@ +import { DriveListViewMode } from '@internxt-mobile/types/drive/ui'; +import { RowsIcon, SquaresFourIcon } from 'phosphor-react-native'; +import { Text, TouchableOpacity, View } from 'react-native'; +import { useTailwind } from 'tailwind-rn'; +import strings from '../../../../assets/lang/strings'; +import { colors, fontStyles } from '../../theme'; + +interface SortRowProps { + viewMode: DriveListViewMode; + onToggleViewMode: () => void; +} + +export const SortRow = ({ viewMode, onToggleViewMode }: SortRowProps) => { + const tailwind = useTailwind(); + + return ( + + + {strings.screens.ShareExtension.sortByName} + + + {viewMode === DriveListViewMode.List ? ( + + ) : ( + + )} + + + ); +}; diff --git a/src/shareExtension/components/DriveScreen/SubfolderHeader.tsx b/src/shareExtension/components/DriveScreen/SubfolderHeader.tsx new file mode 100644 index 000000000..cb0c13ad0 --- /dev/null +++ b/src/shareExtension/components/DriveScreen/SubfolderHeader.tsx @@ -0,0 +1,88 @@ +import { CaretLeftIcon, MagnifyingGlassIcon } from 'phosphor-react-native'; +import { Animated, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { useTailwind } from 'tailwind-rn'; +import strings from '../../../../assets/lang/strings'; +import { UseSearchAnimationResult } from '../../hooks/useSearchAnimation'; +import { colors, fontStyles } from '../../theme'; +import { TextButton } from '../TextButton'; + +interface SubfolderHeaderProps { + folderName: string; + parentName: string; + onBack: () => void; + searchQuery: string; + onChangeSearch: (q: string) => void; + onClearSearch: () => void; + onNewFolder: () => void; + searchAnim: UseSearchAnimationResult; +} + +export const SubfolderHeader = ({ + folderName, + parentName, + onBack, + searchQuery, + onChangeSearch, + onClearSearch, + onNewFolder, + searchAnim, +}: SubfolderHeaderProps) => { + const tailwind = useTailwind(); + const { searchHeight, searchOpacity, isSearchOpen, searchRef, toggleSearch } = searchAnim; + + const handleSearchPress = () => toggleSearch(onClearSearch); + + return ( + <> + + + + + {parentName} + + + + + + + + + + + + + + + + + {folderName} + + + + + ); +}; diff --git a/src/shareExtension/components/FileListItem.tsx b/src/shareExtension/components/FileListItem.tsx new file mode 100644 index 000000000..824b4f9e6 --- /dev/null +++ b/src/shareExtension/components/FileListItem.tsx @@ -0,0 +1,82 @@ +import { DriveListViewMode } from '@internxt-mobile/types/drive/ui'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { useTailwind } from 'tailwind-rn'; +import { FolderIcon, getFileTypeIcon } from '../../helpers/filetypes'; +import { colors, fontStyles } from '../theme'; +import { ShareFileItem, ShareFolderItem } from '../types'; +import { formatBytes, formatDate } from '../utils'; + +interface FileListItemProps { + item: ShareFolderItem | ShareFileItem; + isFolder: boolean; + viewMode: DriveListViewMode; + onPress?: () => void; +} + +export const FileListItem = ({ item, isFolder, viewMode, onPress }: FileListItemProps) => { + const tailwind = useTailwind(); + const IconComponent = isFolder ? FolderIcon : getFileTypeIcon((item as ShareFileItem).type ?? ''); + const fileItem = !isFolder ? (item as ShareFileItem) : null; + const fileSize = fileItem ? parseInt(fileItem.size, 10) : NaN; + const fileSizeText = isNaN(fileSize) ? '' : formatBytes(fileSize); + + if (viewMode === DriveListViewMode.Grid) { + return ( + + + + + + {item.plainName} + + + {formatDate(item.updatedAt)} + + + ); + } + + return ( + + + + + + + {item.plainName} + + + {fileItem ? `${fileSizeText} · ${formatDate(item.updatedAt)}` : formatDate(item.updatedAt)} + + + + + ); +}; + +const styles = StyleSheet.create({ + rowItem: { + paddingRight: 16, + }, + disabled: { + opacity: 0.4, + }, + separator: { + position: 'absolute', + bottom: 0, + left: 16, + right: 16, + height: StyleSheet.hairlineWidth, + backgroundColor: colors.gray10, + }, +}); diff --git a/src/shareExtension/components/TextButton.tsx b/src/shareExtension/components/TextButton.tsx new file mode 100644 index 000000000..05b33835b --- /dev/null +++ b/src/shareExtension/components/TextButton.tsx @@ -0,0 +1,27 @@ +import { StyleProp, Text, TouchableOpacity, ViewStyle } from 'react-native'; +import { useTailwind } from 'tailwind-rn'; +import { colors, fontStyles } from '../theme'; + +interface TextButtonProps { + title: string; + onPress: () => void; + disabled?: boolean; + style?: StyleProp; +} + +export const TextButton = ({ title, onPress, disabled, style }: TextButtonProps) => { + const tailwind = useTailwind(); + return ( + + + {title} + + + ); +}; diff --git a/src/shareExtension/hooks/useNavAnimation.ts b/src/shareExtension/hooks/useNavAnimation.ts new file mode 100644 index 000000000..fcd4e01c2 --- /dev/null +++ b/src/shareExtension/hooks/useNavAnimation.ts @@ -0,0 +1,50 @@ +import { useEffect, useRef } from 'react'; +import { Animated, Easing } from 'react-native'; + +const NAV_SLIDE_OFFSET = 40; +const NAV_SLIDE_DURATION_MS = 220; +const NAV_FADE_DURATION_MS = 200; + +interface UseNavAnimationResult { + translateX: Animated.Value; + contentOpacity: Animated.Value; +} + +export const useNavAnimation = (currentFolderUuid: string, depth: number): UseNavAnimationResult => { + const translateX = useRef(new Animated.Value(0)).current; + const contentOpacity = useRef(new Animated.Value(1)).current; + const prevDepthRef = useRef(1); + const isFirstNavRef = useRef(true); + + useEffect(() => { + if (isFirstNavRef.current) { + isFirstNavRef.current = false; + return; + } + + const goingForward = depth > prevDepthRef.current; + prevDepthRef.current = depth; + + translateX.setValue(goingForward ? NAV_SLIDE_OFFSET : -NAV_SLIDE_OFFSET); + contentOpacity.setValue(0); + + const anim = Animated.parallel([ + Animated.timing(translateX, { + toValue: 0, + duration: NAV_SLIDE_DURATION_MS, + easing: Easing.out(Easing.quad), + useNativeDriver: true, + }), + Animated.timing(contentOpacity, { + toValue: 1, + duration: NAV_FADE_DURATION_MS, + easing: Easing.out(Easing.quad), + useNativeDriver: true, + }), + ]); + anim.start(); + return () => anim.stop(); + }, [currentFolderUuid]); + + return { translateX, contentOpacity }; +}; diff --git a/src/shareExtension/hooks/useSearchAnimation.ts b/src/shareExtension/hooks/useSearchAnimation.ts new file mode 100644 index 000000000..f60dd5b08 --- /dev/null +++ b/src/shareExtension/hooks/useSearchAnimation.ts @@ -0,0 +1,66 @@ +import { useCallback, useRef, useState } from 'react'; +import { Animated, Keyboard, TextInput } from 'react-native'; + +const SEARCH_BAR_EXPANDED_HEIGHT = 46; +const SEARCH_OPEN_DURATION_MS = 200; +const SEARCH_OPEN_OPACITY_DURATION_MS = 180; +const SEARCH_CLOSE_DURATION_MS = 180; +const SEARCH_CLOSE_OPACITY_DURATION_MS = 150; + +export interface UseSearchAnimationResult { + searchHeight: Animated.Value; + searchOpacity: Animated.Value; + isSearchOpen: boolean; + searchRef: React.RefObject; + toggleSearch: (onClose?: () => void) => void; + resetSearch: () => void; +} + +export const useSearchAnimation = (): UseSearchAnimationResult => { + const searchHeight = useRef(new Animated.Value(0)).current; + const searchOpacity = useRef(new Animated.Value(0)).current; + const [isSearchOpen, setIsSearchOpen] = useState(false); + const searchRef = useRef(null); + + const toggleSearch = useCallback( + (onClose?: () => void) => { + if (isSearchOpen) { + Keyboard.dismiss(); + Animated.parallel([ + Animated.timing(searchHeight, { toValue: 0, duration: SEARCH_CLOSE_DURATION_MS, useNativeDriver: false }), + Animated.timing(searchOpacity, { + toValue: 0, + duration: SEARCH_CLOSE_OPACITY_DURATION_MS, + useNativeDriver: false, + }), + ]).start(() => { + setIsSearchOpen(false); + onClose?.(); + }); + } else { + setIsSearchOpen(true); + Animated.parallel([ + Animated.timing(searchHeight, { + toValue: SEARCH_BAR_EXPANDED_HEIGHT, + duration: SEARCH_OPEN_DURATION_MS, + useNativeDriver: false, + }), + Animated.timing(searchOpacity, { + toValue: 1, + duration: SEARCH_OPEN_OPACITY_DURATION_MS, + useNativeDriver: false, + }), + ]).start(() => searchRef.current?.focus()); + } + }, + [isSearchOpen, searchHeight, searchOpacity], + ); + + const resetSearch = useCallback(() => { + searchHeight.setValue(0); + searchOpacity.setValue(0); + setIsSearchOpen(false); + }, [searchHeight, searchOpacity]); + + return { searchHeight, searchOpacity, isSearchOpen, searchRef, toggleSearch, resetSearch }; +}; diff --git a/src/shareExtension/screens/DriveScreen.tsx b/src/shareExtension/screens/DriveScreen.tsx new file mode 100644 index 000000000..39abc7b18 --- /dev/null +++ b/src/shareExtension/screens/DriveScreen.tsx @@ -0,0 +1,141 @@ +import { DriveListViewMode } from '@internxt-mobile/types/drive/ui'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Animated, Keyboard, View } from 'react-native'; +import { useTailwind } from 'tailwind-rn'; +import strings from '../../../assets/lang/strings'; +import { BottomFilePanel } from '../components/BottomFilePanel'; +import { DriveHeader } from '../components/DriveHeader'; +import { DriveList, DriveListItem } from '../components/DriveScreen/DriveList'; +import { RootHeader } from '../components/DriveScreen/RootHeader'; +import { SortRow } from '../components/DriveScreen/SortRow'; +import { SubfolderHeader } from '../components/DriveScreen/SubfolderHeader'; +import { NewFolderModal } from '../components/NewFolderModal'; +import { useFolderNavigation } from '../hooks/useFolderNavigation'; +import { useNavAnimation } from '../hooks/useNavAnimation'; +import { useSearchAnimation } from '../hooks/useSearchAnimation'; +import { SharedFile } from '../types'; + +interface DriveScreenProps { + sharedFiles: SharedFile[]; + rootFolderUuid: string; + onClose: () => void; + onSave: (destinationFolderUuid: string, finalFileName?: string) => void; +} + +export const DriveScreen = ({ sharedFiles, rootFolderUuid, onClose, onSave }: DriveScreenProps) => { + const tailwind = useTailwind(); + const { + currentFolder, + folders, + files, + loading, + loadingMore, + searchQuery, + setSearchQuery, + viewMode, + setViewMode, + breadcrumb, + navigate, + goBack, + loadMore, + createFolder, + } = useFolderNavigation(rootFolderUuid); + const [showNewFolderModal, setShowNewFolderModal] = useState(false); + const [isRenaming, setIsRenaming] = useState(false); + const [editingName, setEditingName] = useState(sharedFiles[0]?.fileName ?? ''); + + const isRoot = breadcrumb.length === 1; + const parentFolderIndex = breadcrumb.length - 2; + const parentFolder = breadcrumb[parentFolderIndex]; + + const { translateX, contentOpacity } = useNavAnimation(currentFolder.uuid, breadcrumb.length); + const searchAnimation = useSearchAnimation(); + + useEffect(() => { + searchAnimation.resetSearch(); + }, [currentFolder.uuid]); + + const listItems = useMemo( + () => [ + ...folders.map((folder): DriveListItem => ({ type: 'folder', data: folder })), + ...files.map((file): DriveListItem => ({ type: 'file', data: file })), + ], + [folders, files], + ); + + const handleSave = useCallback(() => { + Keyboard.dismiss(); + onSave(currentFolder.uuid, isRenaming ? editingName : undefined); + }, [onSave, currentFolder.uuid, isRenaming, editingName]); + + const handleCreateFolder = useCallback( + async (name: string) => { + await createFolder(name); + setShowNewFolderModal(false); + }, + [createFolder], + ); + + const handleOpenNewFolderModal = useCallback(() => setShowNewFolderModal(true), []); + const handleCloseNewFolderModal = useCallback(() => setShowNewFolderModal(false), []); + const handleClearSearch = useCallback(() => setSearchQuery(''), []); + const handleToggleViewMode = useCallback( + () => setViewMode(viewMode === DriveListViewMode.List ? DriveListViewMode.Grid : DriveListViewMode.List), + [viewMode, setViewMode], + ); + const handleStartRename = useCallback(() => setIsRenaming(true), []); + const handleEndRename = useCallback(() => setIsRenaming(false), []); + + return ( + + {/* saveEnabled will be blocked when files are uploading (next tasks) */} + + + + {isRoot ? ( + + ) : ( + + )} + + + + + + + + + + + ); +}; diff --git a/src/shareExtension/screens/NotSignedInScreen.tsx b/src/shareExtension/screens/NotSignedInScreen.tsx index 008bec8fa..5f6720431 100644 --- a/src/shareExtension/screens/NotSignedInScreen.tsx +++ b/src/shareExtension/screens/NotSignedInScreen.tsx @@ -1,6 +1,8 @@ -import { Platform, Pressable, StyleSheet, Text, View } from 'react-native'; +import { Platform, TouchableOpacity, StyleSheet, Text, View } from 'react-native'; import Svg, { Path } from 'react-native-svg'; +import { useTailwind } from 'tailwind-rn'; import strings from '../../../assets/lang/strings'; +import { colors, fontStyles } from '../theme'; interface NotSignedInScreenProps { onClose: () => void; @@ -12,7 +14,7 @@ function LoginIcon() { - + + - - - - - {translations.title} - + + + + + {translations.title} + - + - {translations.notSignedIn.title} - {translations.notSignedIn.subtitle} - - {translations.notSignedIn.openLogin} - + + {translations.notSignedIn.title} + + + {translations.notSignedIn.subtitle} + + + + {translations.notSignedIn.openLogin} + + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#ffffff', - }, - handle: { - width: 36, - height: 4, - borderRadius: 2, - backgroundColor: '#d1d5db', - alignSelf: 'center', - marginTop: 8, - marginBottom: 4, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 16, - paddingTop: 12, - paddingBottom: 12, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#e5e7eb', - }, - closeButton: { - width: 32, - height: 32, - alignItems: 'center', - justifyContent: 'center', - }, - closeText: { - fontSize: 18, - color: '#6b7280', - }, - headerTitle: { - flex: 1, - textAlign: 'center', - fontSize: 16, - fontWeight: '600', - color: '#111827', - }, - headerSpacer: { - width: 32, - }, - body: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 24, - paddingBottom: 36, - gap: 12, - }, - title: { - fontFamily: Platform.select({ android: 'InstrumentSans-SemiBold' }), - fontWeight: '600', - fontSize: Platform.select({ ios: 36, android: 30 }), - lineHeight: Platform.select({ ios: 44, android: 36 }), - color: '#1C1C1C', - textAlign: 'center', - marginTop: 8, - }, - subtitle: { - fontFamily: Platform.select({ android: 'InstrumentSans-Regular' }), - fontWeight: '400', - fontSize: Platform.select({ ios: 20, android: 18 }), - lineHeight: Platform.select({ ios: 24, android: 22 }), - color: '#737373', - textAlign: 'center', - }, - loginButton: { - backgroundColor: '#0066FF', - borderRadius: 12, - paddingVertical: 16, - alignSelf: 'stretch', - alignItems: 'center', - justifyContent: 'center', - marginTop: 12, - }, - loginButtonText: { - fontFamily: Platform.select({ android: 'InstrumentSans-SemiBold' }), - fontWeight: '600', - fontSize: 16, - lineHeight: 20, - textAlign: 'center', - color: '#ffffff', - }, -}); diff --git a/src/shareExtension/theme.ts b/src/shareExtension/theme.ts new file mode 100644 index 000000000..04d12efd1 --- /dev/null +++ b/src/shareExtension/theme.ts @@ -0,0 +1,37 @@ +import { Platform } from 'react-native'; + +/** + * Used for icon colors, fonts, and values not expressed as tailwind classes (e.g. primaryDisabled). + */ +export const fontStyles = { + regular: { + fontFamily: Platform.select({ android: 'InstrumentSans-Regular' }), + }, + medium: { + fontWeight: '500' as const, + fontFamily: Platform.select({ android: 'InstrumentSans-Medium' }), + }, + semibold: { + fontWeight: '600' as const, + fontFamily: Platform.select({ android: 'InstrumentSans-SemiBold' }), + }, + bold: { + fontWeight: '700' as const, + fontFamily: Platform.select({ android: 'InstrumentSans-Bold' }), + }, +} as const; + +export const colors = { + primary: 'rgb(0, 102, 255)', + primaryDisabled: 'rgba(0, 102, 255, 0.5)', + red: 'rgb(255, 13, 0)', + gray100: 'rgb(24, 24, 27)', + gray80: 'rgb(58, 58, 59)', + gray60: 'rgb(99, 99, 103)', + gray40: 'rgb(174, 174, 179)', + gray20: 'rgb(209, 209, 215)', + gray10: 'rgb(229, 229, 235)', + gray5: 'rgb(243, 243, 248)', + gray1: 'rgb(249, 249, 252)', + surface: '#ffffff', +} as const; diff --git a/src/shareExtension/types.ts b/src/shareExtension/types.ts index 705b0bfe3..52c7c7165 100644 --- a/src/shareExtension/types.ts +++ b/src/shareExtension/types.ts @@ -2,4 +2,20 @@ export interface SharedFile { uri: string; mimeType: string | null; fileName: string | null; + size: number | null; } + +export interface ShareFolderItem { + uuid: string; + plainName: string; + updatedAt: string; +} + +export interface ShareFileItem { + uuid: string; + plainName: string; + size: string; + type: string; + updatedAt: string; +} + diff --git a/src/types/tailwind-rn.d.ts b/src/types/tailwind-rn.d.ts index 39c9ce53e..247ae26b3 100644 --- a/src/types/tailwind-rn.d.ts +++ b/src/types/tailwind-rn.d.ts @@ -1,10 +1,18 @@ declare module 'tailwind-rn' { - const tailwind: (_classNames: string) => { - [x: string]: unknown; - }; - declare const useTailwind = () => tailwind; + import type { ReactNode } from 'react'; + + type TailwindFn = (_classNames: string) => { [x: string]: unknown }; + + declare const useTailwind: () => TailwindFn; + + interface TailwindProviderProps { + utilities: Record; + colorScheme?: 'light' | 'dark'; + children?: ReactNode; + } + declare const TailwindProvider: (props: TailwindProviderProps) => JSX.Element; // TODO: type 'color' - export { useTailwind }; + export { useTailwind, TailwindProvider }; } diff --git a/yarn.lock b/yarn.lock index 285779e6e..92a81b226 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3802,6 +3802,11 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" +dayjs@^1.11.19: + version "1.11.19" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938" + integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== + debug@2.6.9, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" From cda1fb6cf0e914bc00f6b135e3aa9adb77f0814a Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Mon, 9 Mar 2026 18:21:21 +0100 Subject: [PATCH 2/2] fix: sonar cloud issues --- src/shareExtension/components/DriveScreen/DriveList.tsx | 8 ++++---- src/shareExtension/components/FileListItem.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/shareExtension/components/DriveScreen/DriveList.tsx b/src/shareExtension/components/DriveScreen/DriveList.tsx index 63f7db054..c423ecf63 100644 --- a/src/shareExtension/components/DriveScreen/DriveList.tsx +++ b/src/shareExtension/components/DriveScreen/DriveList.tsx @@ -35,10 +35,10 @@ export const DriveList = ({ const numColumns = viewMode === DriveListViewMode.Grid ? 3 : 1; const renderItem = useCallback( - ({ item }: { item: DriveListItem }) => { - const isFolder = item.type === 'folder'; - const handleItemPress = isFolder ? () => onNavigate(item.data.uuid, item.data.plainName) : undefined; - return ; + ({ item: listItem }: { item: DriveListItem }) => { + const isFolder = listItem.type === 'folder'; + const handleItemPress = isFolder ? () => onNavigate(listItem.data.uuid, listItem.data.plainName) : undefined; + return ; }, [onNavigate, viewMode], ); diff --git a/src/shareExtension/components/FileListItem.tsx b/src/shareExtension/components/FileListItem.tsx index 824b4f9e6..700ce2ecd 100644 --- a/src/shareExtension/components/FileListItem.tsx +++ b/src/shareExtension/components/FileListItem.tsx @@ -16,9 +16,9 @@ interface FileListItemProps { export const FileListItem = ({ item, isFolder, viewMode, onPress }: FileListItemProps) => { const tailwind = useTailwind(); const IconComponent = isFolder ? FolderIcon : getFileTypeIcon((item as ShareFileItem).type ?? ''); - const fileItem = !isFolder ? (item as ShareFileItem) : null; - const fileSize = fileItem ? parseInt(fileItem.size, 10) : NaN; - const fileSizeText = isNaN(fileSize) ? '' : formatBytes(fileSize); + const fileItem = isFolder ? null : (item as ShareFileItem); + const fileSize = fileItem ? Number.parseInt(fileItem.size, 10) : Number.NaN; + const fileSizeText = Number.isNaN(fileSize) ? '' : formatBytes(fileSize); if (viewMode === DriveListViewMode.Grid) { return (