diff --git a/__mocks__/react-native-fs.ts b/__mocks__/@dr.pogodin/react-native-fs.ts similarity index 96% rename from __mocks__/react-native-fs.ts rename to __mocks__/@dr.pogodin/react-native-fs.ts index 5a3963564..259b79660 100644 --- a/__mocks__/react-native-fs.ts +++ b/__mocks__/@dr.pogodin/react-native-fs.ts @@ -1,4 +1,4 @@ -jest.mock('react-native-fs', () => { +jest.mock('@dr.pogodin/react-native-fs', () => { return { mkdir: jest.fn(), moveFile: jest.fn(), diff --git a/android/app/build.gradle b/android/app/build.gradle index f89304bbe..fc9e0cd28 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -88,8 +88,8 @@ android { applicationId 'com.internxt.cloud' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 95 - versionName "1.5.38" + versionCode 99 + versionName "1.6.0" buildConfigField("boolean", "REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS", (findProperty("reactNative.unstable_useRuntimeSchedulerAlways") ?: true).toString()) } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index cdc2e15b8..0950d4d66 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ - + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index ff9c51baf..55cf690b4 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2,5 +2,5 @@ Internxt cover false - 1.5.38 + 1.6.0 \ No newline at end of file diff --git a/assets/lang/strings.ts b/assets/lang/strings.ts index 9e3b935eb..7fb800b1e 100644 --- a/assets/lang/strings.ts +++ b/assets/lang/strings.ts @@ -14,6 +14,7 @@ const strings = new LocalizedStrings({ current: 'Current', new: 'New', calculating: 'Calculating', + decrypting: 'Decrypting', atTime: 'at', loading: 'Loading', downloading: 'Downloading...', @@ -647,6 +648,7 @@ const strings = new LocalizedStrings({ confirmDeleteSharedLink: 'Users with the link will lose access to the shared content.', linkDeleted: 'Link deleted successfully', trashEmpty: 'Trash is empty', + downloadLimit: 'The download limit in mobile app is 5GB.', }, errors: { runtimeLogsMissing: 'The logs file is missing or empty', @@ -675,7 +677,6 @@ const strings = new LocalizedStrings({ unknown: 'Unknown error', uploadFile: 'File upload error: {0}', storageLimitReached: 'You have reached your storage limit', - inviteAFriend: 'Error sending invitation: {0}', loadProducts: 'Cannot load products: {0}', passwordsDontMatch: "Passwords don't match", @@ -697,6 +698,7 @@ const strings = new LocalizedStrings({ changePassword: 'Error changing password', loadPrices: 'Error loading prices', cancelSubscription: 'Error cancelling subscription', + notEnoughSpaceOnDevice: 'Not enough storage space available for download', }, }, es: { @@ -708,6 +710,7 @@ const strings = new LocalizedStrings({ current: 'Actual', new: 'Nuevo', calculating: 'Calculando', + decrypting: 'Desencriptando', atTime: 'a las', loading: 'Cargando', security: 'Seguridad', @@ -1345,6 +1348,7 @@ const strings = new LocalizedStrings({ confirmDeleteSharedLink: 'Los usuarios con el link compartido perderán el acceso a este contenido compartido.', linkDeleted: 'Link eliminado correctamente', trashEmpty: 'Papelera vaciada', + downloadLimit: 'El límite de descarga en la app movil son 5GB', }, errors: { runtimeLogsMissing: 'El archivo no se encuentra o está vacío', @@ -1394,6 +1398,7 @@ const strings = new LocalizedStrings({ changePassword: 'Error cambiando contraseña', loadPrices: 'Error cargando precios', cancelSubscription: 'Error cancelando suscripción', + notEnoughSpaceOnDevice: 'No hay suficiente espacio de almacenamiento disponible para la descarga', }, }, }); diff --git a/ios/Internxt.xcodeproj/project.pbxproj b/ios/Internxt.xcodeproj/project.pbxproj index 40b9f8fcc..3b1951d7f 100644 --- a/ios/Internxt.xcodeproj/project.pbxproj +++ b/ios/Internxt.xcodeproj/project.pbxproj @@ -280,6 +280,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Rudder/Rudder.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/dr-pogodin-react-native-fs/RNFS_PrivacyInfo.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( @@ -293,6 +294,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Rudder.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNFS_PrivacyInfo.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -343,7 +345,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.internxt.snacks; - PRODUCT_NAME = "Internxt"; + PRODUCT_NAME = Internxt; SWIFT_OBJC_BRIDGING_HEADER = "Internxt/Internxt-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -374,7 +376,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.internxt.snacks; - PRODUCT_NAME = "Internxt"; + PRODUCT_NAME = Internxt; SWIFT_OBJC_BRIDGING_HEADER = "Internxt/Internxt-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/Internxt/Info.plist b/ios/Internxt/Info.plist index ce8ab22ab..afbe342b5 100644 --- a/ios/Internxt/Info.plist +++ b/ios/Internxt/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.5.38 + 1.6.0 CFBundleSignature ???? CFBundleURLTypes @@ -33,7 +33,7 @@ CFBundleVersion - 5 + 11 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/ios/Internxt/Supporting/Expo.plist b/ios/Internxt/Supporting/Expo.plist index 94c4a058b..1ed739e0e 100644 --- a/ios/Internxt/Supporting/Expo.plist +++ b/ios/Internxt/Supporting/Expo.plist @@ -9,7 +9,7 @@ EXUpdatesLaunchWaitMs 0 EXUpdatesRuntimeVersion - 1.5.38 + 1.6.0 EXUpdatesURL https://u.expo.dev/680f4feb-6315-4a50-93ec-36dcd0b831d2 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c72ea57fe..e3d08c19f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,6 +1,10 @@ PODS: - boost (1.83.0) - DoubleConversion (1.1.6) + - dr-pogodin-react-native-fs (2.27.0): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core - EASClient (0.11.2): - ExpoModulesCore - EXConstants (15.4.6): @@ -938,9 +942,13 @@ PODS: - React-Mapbuffer (0.73.6): - glog - React-debug + - react-native-create-thumbnail (2.0.0): + - React-Core - react-native-document-picker (4.3.0): - React-Core - - react-native-image-picker (4.10.3): + - react-native-image-picker (7.1.2): + - glog + - RCT-Folly (= 2022.05.16.00) - React-Core - react-native-pdf (6.7.5): - React-Core @@ -1134,7 +1142,7 @@ PODS: - React-Core - RealmJS (11.10.2): - React - - rn-crypto (0.1.12): + - rn-crypto (0.1.14): - IDZSwiftCommonCrypto (~> 0.13) - React-Core - rn-fetch-blob (0.11.2): @@ -1151,8 +1159,6 @@ PODS: - React-Core - RNFlashList (1.6.3): - React-Core - - RNFS (2.20.0): - - React-Core - RNGestureHandler (2.14.1): - glog - RCT-Folly (= 2022.05.16.00) @@ -1195,6 +1201,7 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - "dr-pogodin-react-native-fs (from `../node_modules/@dr.pogodin/react-native-fs`)" - EASClient (from `../node_modules/expo-eas-client/ios`) - EXConstants (from `../node_modules/expo-constants/ios`) - EXFont (from `../node_modules/expo-font/ios`) @@ -1246,6 +1253,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - react-native-create-thumbnail (from `../node_modules/react-native-create-thumbnail`) - react-native-document-picker (from `../node_modules/react-native-document-picker`) - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-pdf (from `../node_modules/react-native-pdf`) @@ -1284,7 +1292,6 @@ DEPENDENCIES: - RNFastImage (from `../node_modules/react-native-fast-image`) - RNFileViewer (from `../node_modules/react-native-file-viewer`) - "RNFlashList (from `../node_modules/@shopify/flash-list`)" - - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNPermissions (from `../node_modules/react-native-permissions`) - RNReanimated (from `../node_modules/react-native-reanimated`) @@ -1314,6 +1321,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + dr-pogodin-react-native-fs: + :path: "../node_modules/@dr.pogodin/react-native-fs" EASClient: :path: "../node_modules/expo-eas-client/ios" EXConstants: @@ -1412,6 +1421,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: :path: "../node_modules/react-native/ReactCommon" + react-native-create-thumbnail: + :path: "../node_modules/react-native-create-thumbnail" react-native-document-picker: :path: "../node_modules/react-native-document-picker" react-native-image-picker: @@ -1488,8 +1499,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-file-viewer" RNFlashList: :path: "../node_modules/@shopify/flash-list" - RNFS: - :path: "../node_modules/react-native-fs" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNPermissions: @@ -1510,6 +1519,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: d3f49c53809116a5d38da093a8aa78bf551aed09 DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 + dr-pogodin-react-native-fs: 3a0fa8611f662cafd0059aede51c56099deada1e EASClient: a42ee8bf36c93b3128352faf2ae49405ab4f80bd EXConstants: a5f6276e565d98f9eb4280f81241fc342d641590 EXFont: f20669cb266ef48b004f1eb1f2b20db96cd1df9f @@ -1564,8 +1574,9 @@ SPEC CHECKSUMS: React-jsinspector: 85583ef014ce53d731a98c66a0e24496f7a83066 React-logger: 3eb80a977f0d9669468ef641a5e1fabbc50a09ec React-Mapbuffer: 84ea43c6c6232049135b1550b8c60b2faac19fab + react-native-create-thumbnail: ab55d24aea01723cf386f18b0b542aabb1982f27 react-native-document-picker: 20f652c2402d3ddc81f396d8167c3bd978add4a2 - react-native-image-picker: 60f4246eb5bb7187fc15638a8c1f13abd3820695 + react-native-image-picker: d3db110a3ded6e48c93aef7e8e51afdde8b16ed0 react-native-pdf: 103940c90d62adfd259f63cca99c7c0c306b514c react-native-pdf-thumbnail: 390b1bc4b115b613ca61d6b14cbc712b3621b1df react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 @@ -1595,14 +1606,13 @@ SPEC CHECKSUMS: ReactCommon: 447281ad2034ea3252bf81a60d1f77d5afb0b636 ReactNativeLocalization: fb171138cdc80d5d0d4f20243d2fc82c2b3cc48f RealmJS: 73a36da3cbbe85e1bdcbf55683172b51f35070d3 - rn-crypto: 160ce10c618571e5c051c8e12315df0b04ac7c3e + rn-crypto: 11206fba572b93aa936f225fa7e9dec24a330a58 rn-fetch-blob: f525a73a78df9ed5d35e67ea65e79d53c15255bc RNCAsyncStorage: 618d03a5f52fbccb3d7010076bc54712844c18ef RNDeviceInfo: aad3c663b25752a52bf8fce93f2354001dd185aa RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592 RNFlashList: 4b4b6b093afc0df60ae08f9cbf6ccd4c836c667a - RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 15c6ef51acba34c49ff03003806cf5dd6098f383 RNPermissions: 4e3714e18afe7141d000beae3755e5b5fb2f5e05 RNReanimated: f6b02d8f5eaa2830296411d4ec3b8ef5442dd13d diff --git a/package.json b/package.json index 2eab6152e..b8ad0e610 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "drive-mobile", - "version": "v1.5.38", + "version": "v1.6.0", "private": true, "license": "GNU", "scripts": { @@ -30,12 +30,13 @@ }, "dependencies": { "@burstware/react-native-portal": "^1.0.2", + "@dr.pogodin/react-native-fs": "2.27.0", "@expo/config-plugins": "7.8.4", "@expo/config-types": "^47.0.0", "@hookform/resolvers": "^2.9.1", "@internxt/lib": "^1.2.0", "@internxt/mobile-sdk": "^0.2.41", - "@internxt/rn-crypto": "^0.1.12", + "@internxt/rn-crypto": "0.1.14", "@internxt/sdk": "^1.4.96", "@react-native-async-storage/async-storage": "1.21.0", "@react-navigation/bottom-tabs": "^6.2.0", @@ -92,14 +93,14 @@ "react-native": "0.73.6", "react-native-bip39": "^2.3.0", "react-native-collapsible": "^1.6.0", + "react-native-create-thumbnail": "^2.0.0", "react-native-crypto": "^2.2.0", "react-native-device-info": "^8.4.8", "react-native-document-picker": "^4.1.0", "react-native-fast-image": "^8.5.11", "react-native-file-viewer": "^2.1.4", - "react-native-fs": "^2.16.6", "react-native-gesture-handler": "~2.14.0", - "react-native-image-picker": "^4.0.6", + "react-native-image-picker": "^7.1.2", "react-native-image-zoom-viewer": "^3.0.1", "react-native-localization": "^2.3.1", "react-native-logs": "^5.0.1", diff --git a/patches/react-native-image-picker+7.1.2.patch b/patches/react-native-image-picker+7.1.2.patch new file mode 100644 index 000000000..650b8f677 --- /dev/null +++ b/patches/react-native-image-picker+7.1.2.patch @@ -0,0 +1,24 @@ +diff --git a/node_modules/react-native-image-picker/ios/ImagePickerUtils.mm b/node_modules/react-native-image-picker/ios/ImagePickerUtils.mm +index 443500d..6f8d0ba 100644 +--- a/node_modules/react-native-image-picker/ios/ImagePickerUtils.mm ++++ b/node_modules/react-native-image-picker/ios/ImagePickerUtils.mm +@@ -97,6 +97,8 @@ + const uint8_t firstByteJpg = 0xFF; + const uint8_t firstBytePng = 0x89; + const uint8_t firstByteGif = 0x47; ++ const uint8_t firstByteWebp = 0x52; ++ const uint8_t firstByteHeic = 0x00; + + uint8_t firstByte; + [imageData getBytes:&firstByte length:1]; +@@ -107,6 +109,10 @@ + return @"png"; + case firstByteGif: + return @"gif"; ++ case firstByteWebp: ++ return @"webp"; ++ case firstByteHeic: ++ return @"heic"; + default: + return @"jpg"; + } diff --git a/src/components/AppTextInput/index.tsx b/src/components/AppTextInput/index.tsx index 90c218266..e883cbb77 100644 --- a/src/components/AppTextInput/index.tsx +++ b/src/components/AppTextInput/index.tsx @@ -1,7 +1,16 @@ import { isString } from 'lodash'; -import { useState } from 'react'; -import { StyleProp, TextInput, TextInputProps, View, ViewStyle } from 'react-native'; -import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; +import { useRef, useState } from 'react'; +import { + NativeSyntheticEvent, + Platform, + StyleProp, + TextInput, + TextInputFocusEventData, + TextInputProps, + TextInputSelectionChangeEventData, + View, + ViewStyle, +} from 'react-native'; import { useTailwind } from 'tailwind-rn'; import useGetColor from '../../hooks/useColor'; import AppText from '../AppText'; @@ -19,8 +28,59 @@ const AppTextInput = (props: AppTextInputProps): JSX.Element => { const tailwind = useTailwind(); const getColor = useGetColor(); const [isFocused, setIsFocused] = useState(false); + const [selection, setSelection] = useState<{ start: number; end: number } | undefined>(); + const localInputRef = useRef(null); const editable = props.editable !== false; const [status, statusMessage] = props.status || ['idle', '']; + const isAndroid = Platform.OS === 'android'; + + const handleSelectionChange = (event: NativeSyntheticEvent) => { + if (isAndroid) { + const newSelection = event.nativeEvent.selection; + setSelection(newSelection); + } + }; + + const handleOnFocusInput = (e: NativeSyntheticEvent) => { + setIsFocused(true); + if (isAndroid && props.value && (!selection || (selection.start === 0 && selection.end === 0))) { + const position = props.value.length; + setSelection({ start: position, end: position }); + } + props.onFocus?.(e); + }; + + // Added selection prop handler to resolve Android input cursor position bug + const handleChangeText = (newText: string) => { + if (isAndroid && selection) { + const oldText = props.value || ''; + let newPosition: number; + + const hasNothingSelected = selection.start === selection.end; + const selectionStart = Math.min(selection.start, selection.end); + + if (hasNothingSelected) { + newPosition = selection.start + (newText.length - oldText.length); + } else { + const insertedLength = newText.length - (oldText.length - (selection.end - selection.start)); + newPosition = selectionStart + insertedLength; + } + newPosition = Math.max(0, newPosition); + + setSelection({ start: newPosition, end: newPosition }); + + setTimeout(() => { + if (localInputRef.current) { + localInputRef.current.setNativeProps({ + selection: { start: newPosition, end: newPosition }, + }); + } + }, 0); + } + + props.onChangeText?.(newText); + }; + const renderStatusMessage = () => { let template: JSX.Element | undefined = undefined; @@ -47,6 +107,14 @@ const AppTextInput = (props: AppTextInputProps): JSX.Element => { return template; }; + const inputProps = { + ...props, + ref: props.inputRef || localInputRef, + onChangeText: isAndroid ? handleChangeText : props.onChangeText, + onSelectionChange: isAndroid ? handleSelectionChange : props.onSelectionChange, + selection: isAndroid ? selection : props.selection, + }; + return ( {props.label && {props.label}} @@ -63,11 +131,13 @@ const AppTextInput = (props: AppTextInputProps): JSX.Element => { > setIsFocused(true)} - onBlur={() => setIsFocused(false)} - ref={props.inputRef} + onFocus={handleOnFocusInput} + onBlur={(e) => { + setIsFocused(false); + props.onBlur?.(e); + }} /> {props.renderAppend && {props.renderAppend({ isFocused })}} diff --git a/src/components/SwipeBackHandler/index.tsx b/src/components/SwipeBackHandler/index.tsx new file mode 100644 index 000000000..f8fc18d5a --- /dev/null +++ b/src/components/SwipeBackHandler/index.tsx @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import { BackHandler } from 'react-native'; + +interface SwipeBackProps { + swipeBack: () => void; +} + +const SwipeBackHandler = ({ swipeBack }: SwipeBackProps) => { + useEffect(() => { + const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress); + + return () => backHandler.remove(); + }, []); + + const handleBackPress = () => { + swipeBack(); + return true; + }; + + return null; +}; + +export default SwipeBackHandler; diff --git a/src/components/modals/AddModal/index.tsx b/src/components/modals/AddModal/index.tsx index 7b9e7a191..92da3e5e2 100644 --- a/src/components/modals/AddModal/index.tsx +++ b/src/components/modals/AddModal/index.tsx @@ -1,13 +1,14 @@ +import * as RNFS from '@dr.pogodin/react-native-fs'; import * as FileSystem from 'expo-file-system'; import { launchCameraAsync, requestCameraPermissionsAsync, requestMediaLibraryPermissionsAsync, } from 'expo-image-picker'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { Alert, PermissionsAndroid, Platform, TouchableHighlight, View } from 'react-native'; import DocumentPicker, { DocumentPickerResponse } from 'react-native-document-picker'; -import RNFS from 'react-native-fs'; + import { launchImageLibrary } from 'react-native-image-picker'; import { useDrive } from '@internxt-mobile/hooks/drive'; @@ -41,6 +42,7 @@ import BottomModal from '../BottomModal'; import CreateFolderModal from '../CreateFolderModal'; const MAX_FILES_BULK_UPLOAD = 25; + function AddModal(): JSX.Element { const tailwind = useTailwind(); const getColor = useGetColor(); @@ -53,6 +55,7 @@ function AddModal(): JSX.Element { const { limit } = useAppSelector((state) => state.storage); const usage = useAppSelector(storageSelectors.usage); + async function uploadIOS(file: UploadingFile, fileType: 'document' | 'image', progressCallback: ProgressCallback) { const name = decodeURI(file.uri).split('/').pop() || ''; const regex = /^(.*:\/{0,2})\/?(.*)$/gm; @@ -81,6 +84,7 @@ function AddModal(): JSX.Element { await SLEEP_BECAUSE_MAYBE_BACKEND_IS_NOT_RETURNING_FRESHLY_MODIFIED_OR_CREATED_ITEMS_YET(500); await driveCtx.loadFolderContent(focusedFolder.id, { pullFrom: ['network'], resetPagination: true }); }; + async function uploadAndroid( fileToUpload: UploadingFile, fileType: 'document' | 'image', @@ -217,6 +221,7 @@ function AddModal(): JSX.Element { thumbnails: uploadedThumbnail ? [uploadedThumbnail] : null, } as DriveFileData; } + const uploadFile = async (uploadingFile: UploadingFile, fileType: 'document' | 'image') => { logger.info('Starting file upload process: ', JSON.stringify(uploadingFile)); function progressCallback(progress: number) { @@ -445,6 +450,7 @@ function AddModal(): JSX.Element { } errorService.reportError(error); + logger.error('Error on handleUploadFiles function:', JSON.stringify(err)); notificationsService.show({ type: NotificationType.Error, @@ -464,51 +470,56 @@ function AddModal(): JSX.Element { const { status } = await requestMediaLibraryPermissionsAsync(false); if (status === 'granted') { - launchImageLibrary({ mediaType: 'mixed', selectionLimit: MAX_FILES_BULK_UPLOAD }, async (response) => { - if (response.errorMessage) { - return Alert.alert(response.errorMessage); - } - if (response.assets) { - const documents: DocumentPickerResponse[] = []; - - for (const asset of response.assets) { - const decodedURI = decodeURIComponent(asset.uri as string); - const stat = await RNFS.stat(decodedURI); - - documents.push({ - fileCopyUri: asset.uri || '', - name: decodeURIComponent( - asset.fileName || asset.uri?.substring((asset.uri || '').lastIndexOf('/') + 1) || '', - ), - size: asset.fileSize || stat.size, - type: asset.type || '', - uri: asset.uri || '', - }); + launchImageLibrary( + { mediaType: 'mixed', selectionLimit: MAX_FILES_BULK_UPLOAD, assetRepresentationMode: 'current' }, + async (response) => { + if (response.errorMessage) { + return Alert.alert(response.errorMessage); } - - dispatch(uiActions.setShowUploadFileModal(false)); - uploadDocuments(documents) - .then(() => { - dispatch(driveThunks.loadUsageThunk()); - - if (focusedFolder) { - driveCtx.loadFolderContent(focusedFolder.id); - } - }) - .catch((err) => { - if (err.message === 'User canceled document picker') { - return; - } - notificationsService.show({ - type: NotificationType.Error, - text1: strings.formatString(strings.errors.uploadFile, err.message) as string, + if (response.assets) { + const documents: DocumentPickerResponse[] = []; + + for (const asset of response.assets) { + const decodedURI = decodeURIComponent(asset.uri as string); + const stat = await RNFS.stat(decodedURI); + + documents.push({ + fileCopyUri: asset.uri || '', + name: decodeURIComponent( + asset.fileName || asset.uri?.substring((asset.uri || '').lastIndexOf('/') + 1) || '', + ), + size: asset.fileSize || stat.size, + type: asset.type || '', + uri: asset.uri || '', }); - }) - .finally(() => { - dispatch(uiActions.setShowUploadFileModal(false)); - }); - } - }); + } + + dispatch(uiActions.setShowUploadFileModal(false)); + uploadDocuments(documents) + .then(() => { + dispatch(driveThunks.loadUsageThunk()); + + if (focusedFolder) { + driveCtx.loadFolderContent(focusedFolder.id); + } + }) + .catch((err) => { + if (err.message === 'User canceled document picker') { + return; + } + + logger.error('Error on handleUploadFromCameraRoll function:', JSON.stringify(err)); + notificationsService.show({ + type: NotificationType.Error, + text1: strings.formatString(strings.errors.uploadFile, err.message) as string, + }); + }) + .finally(() => { + dispatch(uiActions.setShowUploadFileModal(false)); + }); + } + }, + ); } } else { DocumentPicker.pickMultiple({ @@ -527,6 +538,9 @@ function AddModal(): JSX.Element { if (err.message === 'User canceled document picker') { return; } + + logger.error('Error on hadleUploadFromCameraRoll function:', JSON.stringify(err)); + notificationsService.show({ type: NotificationType.Error, text1: strings.formatString(strings.errors.uploadFile, err.message) as string, @@ -593,10 +607,12 @@ function AddModal(): JSX.Element { } } } - } catch (err) { + } catch (error) { + logger.error('Error on handleTakePhotoAndUpload function:', JSON.stringify(error)); + notificationsService.show({ type: NotificationType.Error, - text1: strings.formatString(strings.errors.uploadFile, (err as Error).message) as string, + text1: strings.formatString(strings.errors.uploadFile, (error as Error)?.message) as string, }); } } @@ -671,8 +687,8 @@ function AddModal(): JSX.Element { style={tailwind('flex-grow')} underlayColor={getColor('text-gray-40')} onPress={() => { - setShowCreateFolderModal(true); dispatch(uiActions.setShowUploadFileModal(false)); + setShowCreateFolderModal(true); }} > diff --git a/src/components/modals/BottomModal/index.tsx b/src/components/modals/BottomModal/index.tsx index 46a66a32a..0f0388bab 100644 --- a/src/components/modals/BottomModal/index.tsx +++ b/src/components/modals/BottomModal/index.tsx @@ -4,10 +4,9 @@ import Modal from 'react-native-modalbox'; import { StatusBar } from 'expo-status-bar'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { X } from 'phosphor-react-native'; +import { INCREASED_TOUCH_AREA } from 'src/styles/global'; import { useTailwind } from 'tailwind-rn'; import useGetColor from '../../../hooks/useColor'; -import { INCREASED_TOUCH_AREA } from 'src/styles/global'; export interface BottomModalProps { isOpen: boolean; @@ -38,7 +37,7 @@ const BottomModal = (props: BottomModalProps): JSX.Element => { { + if (parseInt(item.size?.toString() ?? '0') > MAX_SIZE_TO_DOWNLOAD['5GB']) { + notificationsService.info(strings.messages.downloadLimit); + return false; + } + return true; + }; + const handleUndoMoveToTrash = async () => { const { success } = await driveUseCases.restoreDriveItems( [ @@ -116,30 +125,52 @@ function DriveItemInfoModal(): JSX.Element { return; }; - const downloadItem = async (fileId: string, bucketId: string, decryptedFilePath: string) => { + const downloadItem = async (fileId: string, bucketId: string, decryptedFilePath: string, fileSize: number) => { const { credentials } = await AuthService.getAuthCredentials(); + try { + const hasEnoughSpace = await fileSystemService.checkAvailableStorage(fileSize); + if (!hasEnoughSpace) { + notifications.error(strings.errors.notEnoughSpaceOnDevice); + throw new Error(strings.errors.notEnoughSpaceOnDevice); + } + } catch (error) { + logger.error('Error on downloadItem function:', JSON.stringify(error)); + errorService.reportError(error); + } - const { downloadPath } = await drive.file.downloadFile(credentials.user, bucketId, fileId, { - downloadPath: decryptedFilePath, - downloadProgressCallback(progress, bytesReceived, totalBytes) { - setDownloadProgress({ - progress, - bytesReceived, - totalBytes, - }); - }, - onAbortableReady(abortable) { - downloadAbortableRef.current = abortable; + const { downloadPath } = await drive.file.downloadFile( + credentials.user, + bucketId, + fileId, + { + downloadPath: decryptedFilePath, + downloadProgressCallback(progress, bytesReceived, totalBytes) { + setDownloadProgress({ + progress, + bytesReceived, + totalBytes, + }); + }, + onAbortableReady(abortable) { + downloadAbortableRef.current = abortable; + }, }, - }); + fileSize, + ); return downloadPath; }; + const handleExportFile = async () => { try { if (!item.fileId) { throw new Error('Item fileID not found'); } + const canDownloadFile = isFileDownloadable(); + if (!canDownloadFile) { + dispatch(uiActions.setShowItemModal(false)); + return; + } const decryptedFilePath = drive.file.getDecryptedFilePath(item.name, item.type); const exists = await drive.file.existsDecrypted(item.name, item.type); @@ -154,7 +185,12 @@ function DriveItemInfoModal(): JSX.Element { setDownloadProgress({ totalBytes: 0, progress: 0, bytesReceived: 0 }); setExporting(true); - const downloadPath = await downloadItem(item.fileId, item.bucket as string, decryptedFilePath); + const downloadPath = await downloadItem( + item.fileId, + item.bucket as string, + decryptedFilePath, + parseInt(item.size?.toString() ?? '0'), + ); setExporting(false); await fs.shareFile({ title: item.name, @@ -162,6 +198,7 @@ function DriveItemInfoModal(): JSX.Element { }); } catch (error) { notifications.error(strings.errors.generic.message); + logger.error('Error on handleExportFile function:', JSON.stringify(error)); errorService.reportError(error); } finally { setExporting(false); @@ -179,6 +216,11 @@ function DriveItemInfoModal(): JSX.Element { if (!item.fileId) { throw new Error('Item fileID not found'); } + const canDownloadFile = isFileDownloadable(); + if (!canDownloadFile) { + dispatch(uiActions.setShowItemModal(false)); + return; + } const decryptedFilePath = drive.file.getDecryptedFilePath(item.name, item.type); @@ -190,7 +232,12 @@ function DriveItemInfoModal(): JSX.Element { // 2. If the file doesn't exists, download it if (!existsDecrypted) { setExporting(true); - await downloadItem(item.fileId, item.bucket as string, decryptedFilePath); + await downloadItem( + item.fileId, + item.bucket as string, + decryptedFilePath, + parseInt(item.size?.toString() ?? '0'), + ); setExporting(false); } @@ -200,6 +247,7 @@ function DriveItemInfoModal(): JSX.Element { notifications.success(strings.messages.driveDownloadSuccess); } catch (error) { notifications.error(strings.errors.generic.message); + logger.error('Error on handleAndroidDownloadFile function:', JSON.stringify(error)); errorService.reportError(error); } finally { setExporting(false); @@ -212,6 +260,11 @@ function DriveItemInfoModal(): JSX.Element { if (!item.fileId) { throw new Error('Item fileID not found'); } + const canDownloadFile = isFileDownloadable(); + if (!canDownloadFile) { + dispatch(uiActions.setShowItemModal(false)); + return; + } const decryptedFilePath = drive.file.getDecryptedFilePath(item.name, item.type); @@ -223,7 +276,12 @@ function DriveItemInfoModal(): JSX.Element { // 2. If the file doesn't exists, download it if (!existsDecrypted) { setExporting(true); - await downloadItem(item.fileId, item.bucket as string, decryptedFilePath); + await downloadItem( + item.fileId, + item.bucket as string, + decryptedFilePath, + parseInt(item.size?.toString() ?? '0'), + ); setExporting(false); } @@ -235,6 +293,7 @@ function DriveItemInfoModal(): JSX.Element { }); } catch (error) { notifications.error(strings.errors.generic.message); + logger.error('Error on handleiOSSaveToFiles function:', JSON.stringify(error)); errorService.reportError(error); } finally { setExporting(false); @@ -300,6 +359,15 @@ function DriveItemInfoModal(): JSX.Element { if (!downloadProgress.totalBytes) { return strings.generic.calculating + '...'; } + + if ( + item?.size && + downloadProgress?.bytesReceived && + downloadProgress?.bytesReceived >= parseInt(item?.size?.toString()) + ) { + return strings.generic.decrypting + '...'; + } + const bytesReceivedStr = prettysize(downloadProgress.bytesReceived); const totalBytesStr = prettysize(downloadProgress.totalBytes); return `${bytesReceivedStr} ${strings.modals.downloadingFile.of} ${totalBytesStr}`; diff --git a/src/components/modals/DriveRenameModal/index.tsx b/src/components/modals/DriveRenameModal/index.tsx index fd3d90f16..3d80bfbab 100644 --- a/src/components/modals/DriveRenameModal/index.tsx +++ b/src/components/modals/DriveRenameModal/index.tsx @@ -1,24 +1,24 @@ -import React, { useState } from 'react'; -import { View, TextInput, Platform } from 'react-native'; +import { useState } from 'react'; +import { View } from 'react-native'; +import Portal from '@burstware/react-native-portal'; +import { useDrive } from '@internxt-mobile/hooks/drive'; +import drive from '@internxt-mobile/services/drive'; +import uuid from 'react-native-uuid'; +import AppText from 'src/components/AppText'; +import AppTextInput from 'src/components/AppTextInput'; +import { useTailwind } from 'tailwind-rn'; import strings from '../../../../assets/lang/strings'; -import { FolderIcon, getFileTypeIcon } from '../../../helpers'; +import useGetColor from '../../../hooks/useColor'; +import errorService from '../../../services/ErrorService'; +import notificationsService from '../../../services/NotificationsService'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { driveActions } from '../../../store/slices/drive'; import { uiActions } from '../../../store/slices/ui'; -import errorService from '../../../services/ErrorService'; -import AppButton from '../../AppButton'; -import notificationsService from '../../../services/NotificationsService'; import { NotificationType } from '../../../types'; +import AppButton from '../../AppButton'; import CenterModal from '../CenterModal'; -import { useTailwind } from 'tailwind-rn'; -import useGetColor from '../../../hooks/useColor'; -import { useDrive } from '@internxt-mobile/hooks/drive'; -import drive from '@internxt-mobile/services/drive'; -import uuid from 'react-native-uuid'; -import Portal from '@burstware/react-native-portal'; -import AppTextInput from 'src/components/AppTextInput'; -import AppText from 'src/components/AppText'; + function RenameModal(): JSX.Element { const tailwind = useTailwind(); const getColor = useGetColor(); @@ -103,8 +103,7 @@ function RenameModal(): JSX.Element { onItemRenameFinally(); } }; - const IconFile = getFileTypeIcon(focusedItem?.type || ''); - const IconFolder = FolderIcon; + const onClosed = () => { dispatch(uiActions.setShowRenameModal(false)); setNewName(''); diff --git a/src/components/modals/PlansModal/index.tsx b/src/components/modals/PlansModal/index.tsx index a97561df4..8e44a303e 100644 --- a/src/components/modals/PlansModal/index.tsx +++ b/src/components/modals/PlansModal/index.tsx @@ -1,27 +1,30 @@ import React, { useEffect, useState } from 'react'; import { ScrollView, TouchableWithoutFeedback, View } from 'react-native'; -import strings from '../../../../assets/lang/strings'; -import FileIcon from '../../../../assets/icons/file-icon.svg'; -import BottomModal from '../BottomModal'; -import { BaseModalProps } from '../../../types/ui'; -import { useTailwind } from 'tailwind-rn'; +import Animated, { FadeInDown } from 'react-native-reanimated'; import AppButton from 'src/components/AppButton'; import AppText from 'src/components/AppText'; +import StorageUsageBar from 'src/components/StorageUsageBar'; +import storageService from 'src/services/StorageService'; import { useAppDispatch, useAppSelector } from 'src/store/hooks'; import { paymentsSelectors, paymentsThunks } from 'src/store/slices/payments'; -import StorageUsageBar from 'src/components/StorageUsageBar'; import { storageSelectors } from 'src/store/slices/storage'; -import storageService from 'src/services/StorageService'; +import { useTailwind } from 'tailwind-rn'; +import FileIcon from '../../../../assets/icons/file-icon.svg'; +import strings from '../../../../assets/lang/strings'; +import { BaseModalProps } from '../../../types/ui'; +import BottomModal from '../BottomModal'; import { ConfirmationStep } from './ConfirmationStep'; -import Animated, { FadeInDown } from 'react-native-reanimated'; -import { getLineHeight } from 'src/styles/global'; -import prettysize from 'prettysize'; -import { notifications } from '@internxt-mobile/services/NotificationsService'; import errorService from '@internxt-mobile/services/ErrorService'; +import { notifications } from '@internxt-mobile/services/NotificationsService'; +import prettysize from 'prettysize'; +import { getLineHeight } from 'src/styles/global'; export type SubscriptionInterval = 'month' | 'year'; + +const formatAmount = (amount: number | undefined) => ((amount ?? 0) * 0.01).toFixed(2); + const PlansModal = (props: BaseModalProps) => { const [selectedStorageBytes, setSelectedStorageBytes] = useState(); const [selectedInterval, setSelectedInterval] = useState(); @@ -184,12 +187,13 @@ const PlansModal = (props: BaseModalProps) => { ); }); + const renderButtons = (selectedBytes: number) => { const displayPrices = getDisplayPriceWithIntervals(selectedBytes); const monthlyPrice = displayPrices.find((display) => display.interval === 'month'); const yearlyPrice = displayPrices.find((display) => display.interval === 'year'); - const monthlyAmount = (monthlyPrice?.amount || 0) * 0.01; - const yearlyAmount = (yearlyPrice?.amount || 0) * 0.01; + const monthlyAmount = formatAmount(monthlyPrice?.amount); + const yearlyAmount = formatAmount(yearlyPrice?.amount); return ( <> diff --git a/src/contexts/Drive/Drive.context.tsx b/src/contexts/Drive/Drive.context.tsx index 2ac5bc661..9ef0fc78f 100644 --- a/src/contexts/Drive/Drive.context.tsx +++ b/src/contexts/Drive/Drive.context.tsx @@ -5,7 +5,6 @@ import React, { useEffect, useRef, useState } from 'react'; import appService from '@internxt-mobile/services/AppService'; import errorService from '@internxt-mobile/services/ErrorService'; -import { BaseLogger } from '@internxt-mobile/services/common'; import { AppStateStatus, NativeEventSubscription } from 'react-native'; import { driveFolderService } from '@internxt-mobile/services/drive/folder'; @@ -48,10 +47,6 @@ interface DriveContextProviderProps { children: React.ReactNode; } -const logger = new BaseLogger({ - tag: 'DRIVE_CONTEXT', -}); - const FILES_LIMIT_PER_PAGE = 50; const FOLDERS_LIMIT_PER_PAGE = 50; @@ -146,6 +141,8 @@ export const DriveContextProvider: React.FC = ({ chil userId: folder.userId, // @ts-expect-error - API is returning status, missing from SDK status: folder.status, + // @ts-expect-error - API is returning status, missing from SDK + isFolder: true, }; return driveFolder; @@ -269,7 +266,6 @@ export const DriveContextProvider: React.FC = ({ chil setViewMode(newViewMode); asyncStorageService.saveItem(AsyncStorageKey.PreferredDriveViewMode, newViewMode); }; - return ( { @@ -159,7 +159,13 @@ export class NetworkFacade { return [wrapUploadWithCleanup(), () => null]; } - download(fileId: string, bucketId: string, mnemonic: string, params: DownloadFileParams): [Promise, Abortable] { + download( + fileId: string, + bucketId: string, + mnemonic: string, + params: DownloadFileParams, + fileSize: number, + ): [Promise, Abortable] { if (!fileId) { throw new Error('Download error code 1'); } @@ -172,7 +178,8 @@ export class NetworkFacade { throw new Error('Download error code 3'); } - let downloadJob: { jobId: number; promise: Promise }; + let downloadJob: { jobId: number; promise: Promise }; + let expectedFileHash: string; const decryptFileFromFs: DecryptFileFromFsFunction = @@ -183,6 +190,40 @@ export class NetworkFacade { const encryptedFileName = `${fileId}.enc`; let encryptedFileIsCached = false; let totalBytes = 0; + let chunkFiles: string[] = []; + + const cleanupChunks = async () => { + if (chunkFiles.length > 0) { + await Promise.all(chunkFiles.map((file) => fileSystemService.deleteFile([file]))); + chunkFiles = []; + } + }; + const abortDownload = () => { + if (downloadJob) { + RNFS.stopDownload(downloadJob.jobId); + } + cleanupChunks(); + }; + + const cleanupExistingChunks = async (encFileName: string) => { + try { + const tmpDir = fileSystemService.getTemporaryDir(); + const files = await RNFS.readDir(tmpDir); + const chunkPattern = new RegExp(`${encFileName}\\-chunk-\\d+$`); + + const existingChunks = files.filter((file) => chunkPattern.test(file.name)); + if (existingChunks.length > 0) { + await Promise.all(existingChunks.map((file) => fileSystemService.deleteFile([file.path]))); + } + } catch (error) { + logger.error('Error cleaning up existing chunks:', JSON.stringify(error)); + } + }; + + if (params.signal) { + params.signal.addEventListener('abort', abortDownload); + } + const downloadPromise = downloadFile( fileId, bucketId, @@ -204,49 +245,99 @@ export class NetworkFacade { encryptedFileIsCached = true; encryptedFileURI = path; } else { - encryptedFileURI = fileSystemService.tmpFilePath(encryptedFileName); - // Create an empty file so RNFS can write to it directly - await fileSystemService.createEmptyFile(encryptedFileURI); - - downloadJob = RNFS.downloadFile({ - fromUrl: downloadables[0].url, - toFile: encryptedFileURI, - discretionary: true, - cacheable: false, - progressDivider: 5, - progressInterval: 150, - begin: () => { - params.downloadProgressCallback(0, 0, 0); - }, - progress: (res) => { - if (res.contentLength) { - totalBytes = res.contentLength; - } - params.downloadProgressCallback( - res.bytesWritten / res.contentLength, - res.bytesWritten, - res.contentLength, - ); - }, - }); + await cleanupExistingChunks(encryptedFileName); + const FIFTY_MB = 50 * 1024 * 1024; + const downloadChunkSize = FIFTY_MB; + const ranges: { start: number; end: number }[] = []; + + for (let start = 0; start < fileSize; start += downloadChunkSize) { + const end = Math.min(start + downloadChunkSize - 1, fileSize - 1); + ranges.push({ start, end }); + } + params.downloadProgressCallback(0, 0, 0); + + for (let i = 0; i < ranges.length; i++) { + const isFirstChunk = i === 0; - driveEvents.setJobId(downloadJob.jobId); + if (params.signal?.aborted) { + throw new Error('Download aborted'); + } + + const range = ranges[i]; + const chunkFileName = `${encryptedFileName}-chunk-${i + 1}`; + const chunkFileURI = fileSystemService.tmpFilePath(chunkFileName); + chunkFiles.push(chunkFileURI); + + await fileSystemService.createEmptyFile(chunkFileURI); + + try { + downloadJob = RNFS.downloadFile({ + fromUrl: downloadables[0].url, + toFile: chunkFileURI, + discretionary: true, + cacheable: false, + headers: { + Range: `bytes=${range.start}-${range.end}`, + }, + progressDivider: 5, + progressInterval: 150, + begin: () => { + if (isFirstChunk) { + params.downloadProgressCallback(0, 0, 0); + } + }, + progress: (res) => { + // NOT WORKING ON IOS. CHECK + params.downloadProgressCallback( + (totalBytes + res.bytesWritten) / fileSize, + res.bytesWritten + totalBytes, + fileSize, + ); + }, + }); + // BECAUSE PROGRESS IS NOT WORKING ON IOS + if (isFirstChunk) params.downloadProgressCallback(0, 0, 0); + + driveEvents.setJobId(downloadJob.jobId); + await downloadJob.promise; + + totalBytes = downloadChunkSize * (i + 1); + // BECAUSE PROGRESS IS NOT WORKING ON IOS + if (Platform.OS === 'ios') { + params.downloadProgressCallback(totalBytes / fileSize, totalBytes, fileSize); + } + } catch (error) { + await cleanupChunks(); + throw error; + } + } expectedFileHash = downloadables[0].hash; + encryptedFileURI = fileSystemService.tmpFilePath(encryptedFileName); + + joinFiles(chunkFiles, encryptedFileURI, async (error) => { + if (error) { + logger.error('Error on joinFiles in download function:', JSON.stringify(error)); + throw error; + } + + await cleanupChunks(); + }); } }, async (_, key, iv) => { if (!encryptedFileURI) throw new Error('No encrypted file URI found'); + // commented because it is giving errors, we should check if it is necessary // Maybe we should save the expected hash and compare even if the file is cached - if (!encryptedFileIsCached) { - await downloadJob.promise; - const sha256Hash = await RNFS.hash(encryptedFileURI, 'sha256'); - const receivedFileHash = ripemd160(Buffer.from(sha256Hash, 'hex')).toString('hex'); + // if (!encryptedFileIsCached) { + // await downloadJob.promise; + // const sha256Hash = await RNFS.hash(encryptedFileURI, 'sha256'); + // const receivedFileHash = ripemd160(Buffer.from(sha256Hash, 'hex')).toString('hex'); - if (receivedFileHash !== expectedFileHash) { - throw new Error('Hash mismatch'); - } - } + // if (receivedFileHash !== expectedFileHash) { + // throw new Error('Hash mismatch'); + // } + // } params.downloadProgressCallback(1, totalBytes, totalBytes); @@ -297,7 +388,7 @@ export class NetworkFacade { } }; - return [wrapDownloadWithCleanup(), () => RNFS.stopDownload(downloadJob.jobId)]; + return [wrapDownloadWithCleanup(), abortDownload]; } } diff --git a/src/network/crypto.ts b/src/network/crypto.ts index 7eef775c4..29b3c899b 100644 --- a/src/network/crypto.ts +++ b/src/network/crypto.ts @@ -1,6 +1,6 @@ -import RNFetchBlob from 'rn-fetch-blob'; -import { stat, read } from 'react-native-fs'; +import { read, stat } from '@dr.pogodin/react-native-fs'; import { createDecipheriv, Decipher } from 'react-native-crypto'; +import RNFetchBlob from 'rn-fetch-blob'; function getAes256CtrDecipher(key: Buffer, iv: Buffer): Decipher { return createDecipheriv('aes-256-ctr', key, iv); diff --git a/src/network/download.ts b/src/network/download.ts index 575cef1d7..af9158ee0 100644 --- a/src/network/download.ts +++ b/src/network/download.ts @@ -1,13 +1,13 @@ -import RNFS from 'react-native-fs'; +import * as RNFS from '@dr.pogodin/react-native-fs'; -import { getNetwork } from './NetworkFacade'; +import { FileVersionOneError } from '@internxt/sdk/dist/network/download'; +import FileManager from '../@inxt-js/api/FileManager'; +import { constants } from '../services/AppService'; import { downloadFile as downloadFileV1, LegacyDownloadRequiredError } from '../services/NetworkService/download'; import { downloadFile as downloadFileV1Legacy } from '../services/NetworkService/downloadLegacy'; -import { constants } from '../services/AppService'; -import { NetworkCredentials } from './requests'; -import { FileVersionOneError } from '@internxt/sdk/dist/network/download'; import { Abortable } from '../types'; -import FileManager from '../@inxt-js/api/FileManager'; +import { getNetwork } from './NetworkFacade'; +import { NetworkCredentials } from './requests'; export type EncryptedFileDownloadedParams = { path: string; @@ -27,11 +27,12 @@ export async function downloadFile( mnemonic: string, creds: NetworkCredentials, params: DownloadFileParams, + fileSize: number, onAbortableReady: (abortable: Abortable) => void, ): Promise { const network = getNetwork(constants.BRIDGE_URL, creds); - const [downloadPromise, abortable] = network.download(fileId, bucketId, mnemonic, params); + const [downloadPromise, abortable] = network.download(fileId, bucketId, mnemonic, params, fileSize); onAbortableReady(abortable); diff --git a/src/screens/SignInScreen/index.tsx b/src/screens/SignInScreen/index.tsx index 6dc357766..4232abf3d 100644 --- a/src/screens/SignInScreen/index.tsx +++ b/src/screens/SignInScreen/index.tsx @@ -118,7 +118,7 @@ function SignInScreen({ navigation }: RootStackScreenProps<'SignIn'>): JSX.Eleme setIsLoading(false); - setErrors({ loginFailed: strings.errors.missingAuthCredentials }); + setErrors({ loginFailed: castedError.message }); } }; const onGoToSignUpButtonPressed = () => { diff --git a/src/screens/drive/DriveFolderScreen/DriveFolderScreen.tsx b/src/screens/drive/DriveFolderScreen/DriveFolderScreen.tsx index b7246036a..007d943f6 100644 --- a/src/screens/drive/DriveFolderScreen/DriveFolderScreen.tsx +++ b/src/screens/drive/DriveFolderScreen/DriveFolderScreen.tsx @@ -4,7 +4,7 @@ import { useDrive } from '@internxt-mobile/hooks/drive'; import drive from '@internxt-mobile/services/drive'; import errorService from '@internxt-mobile/services/ErrorService'; import { RouteProp, useRoute } from '@react-navigation/native'; -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { View } from 'react-native'; import { uiActions } from 'src/store/slices/ui'; import { useTailwind } from 'tailwind-rn'; @@ -138,8 +138,9 @@ export function DriveFolderScreen({ navigation }: DriveScreenProps<'DriveFolder' navigation.navigate('DrivePreview'); }; + const handleDriveItemPress = (driveItem: DriveListItem) => { - const isFolder = driveItem.data.type ? false : true; + const isFolder = driveItem?.data?.isFolder; if (!isFolder) { dispatch( driveActions.setFocusedItem({ diff --git a/src/screens/drive/DrivePreviewScreen/DrivePreviewScreen.tsx b/src/screens/drive/DrivePreviewScreen/DrivePreviewScreen.tsx index 1a5e42db1..0eacb0f78 100644 --- a/src/screens/drive/DrivePreviewScreen/DrivePreviewScreen.tsx +++ b/src/screens/drive/DrivePreviewScreen/DrivePreviewScreen.tsx @@ -1,4 +1,4 @@ -import { GeneratedThumbnail } from '@internxt-mobile/services/common'; +import { GeneratedThumbnail, imageService } from '@internxt-mobile/services/common'; import { time } from '@internxt-mobile/services/common/time'; import errorService from '@internxt-mobile/services/ErrorService'; import { fs } from '@internxt-mobile/services/FileSystemService'; @@ -20,6 +20,7 @@ import { useAppDispatch, useAppSelector } from 'src/store/hooks'; import { uiActions } from 'src/store/slices/ui'; import { getLineHeight } from 'src/styles/global'; import { useTailwind } from 'tailwind-rn'; +import { driveThunks } from '../../../store/slices/drive'; import { DriveImagePreview } from './DriveImagePreview'; import { DrivePdfPreview } from './DrivePdfPreview'; import { DRIVE_PREVIEW_HEADER_HEIGHT, DrivePreviewScreenHeader } from './DrivePreviewScreenHeader'; @@ -51,12 +52,12 @@ export const DrivePreviewScreen: React.FC> VIDEO_PREVIEW_TYPES.includes(downloadingFile.data.type as FileExtension) && !generatedThumbnail ) { - // imageService - // .generateVideoThumbnail(downloadingFile.downloadedFilePath) - // .then((generatedThumbnail) => { - // setGeneratedThumbnail(generatedThumbnail); - // }) - // .catch((err) => errorService.reportError(err)); + imageService + .generateVideoThumbnail(downloadingFile.downloadedFilePath) + .then((generatedThumbnail) => { + setGeneratedThumbnail(generatedThumbnail); + }) + .catch((err) => errorService.reportError(err)); } }, [downloadingFile?.downloadedFilePath]); @@ -88,18 +89,18 @@ export const DrivePreviewScreen: React.FC> return <>; } const filename = `${focusedItem.name || ''}${focusedItem.type ? `.${focusedItem.type}` : ''}`; - const currentProgress = downloadingFile.downloadProgress * 0.5 + downloadingFile.decryptProgress * 0.5; + const currentProgress = downloadingFile.downloadProgress * 0.95 + downloadingFile.decryptProgress * 0.05; const FileIcon = getFileTypeIcon(focusedItem.type || ''); - const hasImagePreview = IMAGE_PREVIEW_TYPES.includes(downloadingFile.data.type.toLowerCase() as FileExtension); - const hasVideoPreview = VIDEO_PREVIEW_TYPES.includes(downloadingFile.data.type.toLowerCase() as FileExtension); - const hasPdfPreview = PDF_PREVIEW_TYPES.includes(downloadingFile.data.type.toLowerCase() as FileExtension); + const hasImagePreview = IMAGE_PREVIEW_TYPES.includes(downloadingFile.data.type?.toLowerCase() as FileExtension); + const hasVideoPreview = VIDEO_PREVIEW_TYPES.includes(downloadingFile.data.type?.toLowerCase() as FileExtension); + const hasPdfPreview = PDF_PREVIEW_TYPES.includes(downloadingFile.data.type?.toLowerCase() as FileExtension); const getProgressMessage = () => { if (!downloadingFile) { return; } const progressMessage = strings.formatString( - currentProgress < 0.5 ? strings.screens.drive.downloadingPercent : strings.screens.drive.decryptingPercent, + currentProgress < 0.95 ? strings.screens.drive.downloadingPercent : strings.screens.drive.decryptingPercent, (currentProgress * 100).toFixed(0), ); @@ -152,12 +153,14 @@ export const DrivePreviewScreen: React.FC> {error} - downloadingFile.retry && downloadingFile.retry()} - > + {downloadingFile && error !== strings.messages.downloadLimit && ( + downloadingFile.retry && downloadingFile.retry()} + > + )} ) : null} {isDownloaded && downloadedFilePath ? ( @@ -240,7 +243,10 @@ export const DrivePreviewScreen: React.FC> { + dispatch(driveThunks.cancelDownloadThunk()); + props.navigation.goBack(); + }} onActionsButtonPress={handleActionsButtonPress} /> diff --git a/src/screens/drive/DrivePreviewScreen/DrivePreviewScreenHeader.tsx b/src/screens/drive/DrivePreviewScreen/DrivePreviewScreenHeader.tsx index f3005f243..fb074038f 100644 --- a/src/screens/drive/DrivePreviewScreen/DrivePreviewScreenHeader.tsx +++ b/src/screens/drive/DrivePreviewScreen/DrivePreviewScreenHeader.tsx @@ -4,6 +4,7 @@ import { TouchableOpacity, View } from 'react-native'; import AppText from 'src/components/AppText'; import { INCREASED_TOUCH_AREA } from 'src/styles/global'; import { useTailwind } from 'tailwind-rn'; +import SwipeBackHandler from '../../../components/SwipeBackHandler'; export interface DrivePreviewScreenHeaderProps { onCloseButtonPress: () => void; @@ -21,6 +22,7 @@ export const DrivePreviewScreenHeader: React.FC = { height: DRIVE_PREVIEW_HEADER_HEIGHT }, ]} > + diff --git a/src/services/FileSystemService.ts b/src/services/FileSystemService.ts index 926295930..112d2c233 100644 --- a/src/services/FileSystemService.ts +++ b/src/services/FileSystemService.ts @@ -1,13 +1,15 @@ +import * as RNFS from '@dr.pogodin/react-native-fs'; import { Platform } from 'react-native'; -import RNFS from 'react-native-fs'; -import RNFetchBlob, { RNFetchBlobStat } from 'rn-fetch-blob'; -import FileViewer from 'react-native-file-viewer'; + +import { internxtFS } from '@internxt/mobile-sdk'; import * as FileSystem from 'expo-file-system'; +import { shareAsync } from 'expo-sharing'; import prettysize from 'prettysize'; +import FileViewer from 'react-native-file-viewer'; import Share from 'react-native-share'; import uuid from 'react-native-uuid'; -import { shareAsync } from 'expo-sharing'; -import { internxtFS } from '@internxt/mobile-sdk'; +import RNFetchBlob, { RNFetchBlobStat } from 'rn-fetch-blob'; + enum AcceptedEncodings { Utf8 = 'utf8', Ascii = 'ascii', @@ -21,13 +23,25 @@ export interface FileWriter { const ANDROID_URI_PREFIX = 'file://'; -export type UsageStatsResult = Record; +export type UsageStatsResult = Record; class FileSystemService { private timestamp = Date.now(); public async prepareFileSystem() { await this.prepareTmpDir(); } + public async deleteFile(files: string[]): Promise { + try { + await Promise.all( + files.map(async (file) => { + await this.unlinkIfExists(file); + }), + ); + } catch (error) { + console.error('Error in bulk file deletion:', error); + throw error; + } + } public pathToUri(path: string): string { if (path.startsWith(ANDROID_URI_PREFIX)) return path; @@ -50,7 +64,7 @@ class FileSystemService { return `internxt_mobile_runtime_logs_${this.timestamp}.txt`; } - public getDownloadsDir(): string { + public getDownloadsDir(): string | undefined { // MainBundlePath is only available on iOS return Platform.OS === 'ios' ? RNFS.MainBundlePath : RNFS.DownloadDirectoryPath; } @@ -320,6 +334,19 @@ class FileSystemService { await this.unlinkIfExists(RNFetchBlob.fs.dirs.DocumentDir + '/RNFetchBlob_tmp'); await this.clearTempDir(); } + + public async checkAvailableStorage(requiredSpace: number): Promise { + try { + const fsInfo = await RNFS.getFSInfo(); + const freeSpace = fsInfo.freeSpace; + + const spaceWithBuffer = requiredSpace * 1.3; + + return freeSpace >= spaceWithBuffer; + } catch (error) { + throw new Error('Could not check available storage'); + } + } } const fileSystemService = new FileSystemService(); diff --git a/src/services/NetworkService/download.ts b/src/services/NetworkService/download.ts index fa40671b6..6134f2129 100644 --- a/src/services/NetworkService/download.ts +++ b/src/services/NetworkService/download.ts @@ -1,18 +1,19 @@ +import * as RNFS from '@dr.pogodin/react-native-fs'; import { request } from '@internxt/lib'; -import RNFS from 'react-native-fs'; + import axios, { AxiosRequestConfig } from 'axios'; -import RNFetchBlob from 'rn-fetch-blob'; import { createDecipheriv } from 'react-native-crypto'; +import RNFetchBlob from 'rn-fetch-blob'; import { GenerateFileKey, ripemd160, sha256 } from '../../@inxt-js/lib/crypto'; import { eachLimit } from 'async'; -import fileSystemService from '../FileSystemService'; -import { NetworkCredentials } from '../../types'; -import { Platform } from 'react-native'; import { decryptFile as nativeDecryptFile } from '@internxt/rn-crypto'; import { FileId } from '@internxt/sdk/dist/photos'; +import { Platform } from 'react-native'; +import { NetworkCredentials } from '../../types'; +import fileSystemService from '../FileSystemService'; type FileDecryptedURI = string; diff --git a/src/services/common/filesystem/fileCacheManager.spec.ts b/src/services/common/filesystem/fileCacheManager.spec.ts index ea5f26883..d59e16455 100644 --- a/src/services/common/filesystem/fileCacheManager.spec.ts +++ b/src/services/common/filesystem/fileCacheManager.spec.ts @@ -1,5 +1,5 @@ +import { ReadDirResItemT } from '@dr.pogodin/react-native-fs'; import fileSystemService from '@internxt-mobile/services/FileSystemService'; -import { ReadDirItem } from 'react-native-fs'; import { FileCacheManagerConfigError, FileDoesntExistsError } from './errors'; import { FileCacheManager, FileCacheManagerConfig } from './fileCacheManager'; @@ -132,7 +132,7 @@ describe('File Cache Manager', () => { it('Should remove files in the directory by the oldest mtime if not enough space', async () => { // Total 90MB - const itemsInDir: ReadDirItem[] = [ + const itemsInDir: ReadDirResItemT[] = [ { name: 'file_1.png', ctime: new Date('2022/12/01'), diff --git a/src/services/common/logger/logger.service.ts b/src/services/common/logger/logger.service.ts index f6ec489ac..d30102898 100644 --- a/src/services/common/logger/logger.service.ts +++ b/src/services/common/logger/logger.service.ts @@ -1,5 +1,6 @@ +import * as RNFS from '@dr.pogodin/react-native-fs'; import { logger as RNLogger, consoleTransport, fileAsyncTransport } from 'react-native-logs'; -import RNFS from 'react-native-fs'; + import { fs } from '@internxt-mobile/services/FileSystemService'; import { InteractionManager } from 'react-native'; diff --git a/src/services/common/media/image.service.ts b/src/services/common/media/image.service.ts index 9b90a7d4c..140245961 100644 --- a/src/services/common/media/image.service.ts +++ b/src/services/common/media/image.service.ts @@ -3,7 +3,9 @@ import { manipulateAsync, SaveFormat } from 'expo-image-manipulator'; import fileSystemService, { fs } from '@internxt-mobile/services/FileSystemService'; import { FileExtension } from '@internxt-mobile/types/drive'; -import RNFS from 'react-native-fs'; +import * as RNFS from '@dr.pogodin/react-native-fs'; + +import { createThumbnail } from 'react-native-create-thumbnail'; import PdfThumbnail from 'react-native-pdf-thumbnail'; import uuid from 'react-native-uuid'; import RNFetchBlob from 'rn-fetch-blob'; @@ -24,20 +26,15 @@ export type ThumbnailGenerateConfig = { height?: number; }; -// added this to omit video extension types, the package that produces the video thumbnail is -// causing compilation errors, leave it to solve in other task -type OmittedExtensions = 'avi' | 'mp4' | 'mov'; -type IncludedFileExtension = Exclude; - class ImageService { private get thumbnailGenerators(): Record< - IncludedFileExtension, + FileExtension, (filePath: string, config: ThumbnailGenerateConfig) => Promise > { return { - // [FileExtension.AVI]: this.generateVideoThumbnail, - // [FileExtension.MP4]: this.generateVideoThumbnail, - // [FileExtension.MOV]: this.generateVideoThumbnail, + [FileExtension.AVI]: this.generateVideoThumbnail, + [FileExtension.MP4]: this.generateVideoThumbnail, + [FileExtension.MOV]: this.generateVideoThumbnail, [FileExtension.JPEG]: this.generateImageThumbnail, [FileExtension.JPG]: this.generateImageThumbnail, [FileExtension.PNG]: this.generateImageThumbnail, @@ -154,7 +151,7 @@ class ImageService { filePath: string, config: { outputPath: string; quality?: number; extension: string; thumbnailFormat: SaveFormat }, ): Promise { - const generator = this.thumbnailGenerators[config.extension.toLowerCase() as IncludedFileExtension]; + const generator = this.thumbnailGenerators[config.extension.toLowerCase() as FileExtension]; if (!generator) { // eslint-disable-next-line no-console @@ -165,24 +162,23 @@ class ImageService { return this.resizeThumbnail(await generator(filePath, config)); } - // TODO: FIND A WAY TO GENERATE VIDEO THUMBNAILS /** * Generates a thumbnail for a video file */ - // public generateVideoThumbnail = async (filePath: string): Promise => { - // const result = await createThumbnail({ - // url: fileSystemService.pathToUri(filePath), - // dirSize: 100, - // }); - - // return { - // size: result.size, - // type: 'JPEG', - // width: result.width, - // height: result.height, - // path: result.path, - // }; - // }; + public generateVideoThumbnail = async (filePath: string): Promise => { + const result = await createThumbnail({ + url: fileSystemService.pathToUri(filePath), + dirSize: 100, + }); + + return { + size: result.size, + type: 'JPEG', + width: result.width, + height: result.height, + path: result.path, + }; + }; /** * Generates a thumbnail for an image diff --git a/src/services/drive/constants.ts b/src/services/drive/constants.ts index a8a2f3283..64d281cad 100644 --- a/src/services/drive/constants.ts +++ b/src/services/drive/constants.ts @@ -1,4 +1,5 @@ -import RNFS from 'react-native-fs'; +import * as RNFS from '@dr.pogodin/react-native-fs'; + export const DRIVE_ROOT_DIRECTORY = `${RNFS.DocumentDirectoryPath}/drive`; export const DRIVE_THUMBNAILS_DIRECTORY = `${DRIVE_ROOT_DIRECTORY}/thumbnails`; export const DRIVE_CACHE_DIRECTORY = `${DRIVE_ROOT_DIRECTORY}/cache`; @@ -6,3 +7,8 @@ export const MAX_CACHE_DIRECTORY_SIZE_IN_BYTES = 1024 * 1024 * 500; // 10% of the directory size export const MAX_FILE_SIZE_FOR_CACHING = MAX_CACHE_DIRECTORY_SIZE_IN_BYTES * 0.1; + +export const MAX_SIZE_TO_DOWNLOAD = { + '3GB': 3221225472, + '5GB': 5368709120, +}; diff --git a/src/services/drive/events/driveEvents.ts b/src/services/drive/events/driveEvents.ts index 714a5d8ad..2d74d5709 100644 --- a/src/services/drive/events/driveEvents.ts +++ b/src/services/drive/events/driveEvents.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import * as RNFS from '@dr.pogodin/react-native-fs'; import EventEmitter from 'events'; -import RNFS from 'react-native-fs'; import { Abortable } from '../../../types'; import { DriveEventKey } from '../../../types/drive'; diff --git a/src/services/drive/file/driveFile.service.ts b/src/services/drive/file/driveFile.service.ts index efe1a450d..7d793a0a4 100644 --- a/src/services/drive/file/driveFile.service.ts +++ b/src/services/drive/file/driveFile.service.ts @@ -1,6 +1,7 @@ -import { createHash } from 'crypto'; import axios from 'axios'; +import { createHash } from 'crypto'; +import { getHeaders } from '../../../helpers/headers'; import { DownloadedThumbnail, DriveFileMetadataPayload, @@ -10,20 +11,19 @@ import { SortDirection, SortType, } from '../../../types/drive'; -import { getHeaders } from '../../../helpers/headers'; import { constants } from '../../AppService'; +import asyncStorageService from '@internxt-mobile/services/AsyncStorageService'; import { SdkManager } from '@internxt-mobile/services/common'; -import { MoveFileResponse, Thumbnail } from '@internxt/sdk/dist/drive/storage/types'; -import { getEnvironmentConfig } from 'src/lib/network'; -import { DRIVE_THUMBNAILS_DIRECTORY } from '../constants'; import fileSystemService, { fs } from '@internxt-mobile/services/FileSystemService'; -import * as networkDownload from 'src/network/download'; -import { Image } from 'react-native'; +import { Abortable, AsyncStorageKey } from '@internxt-mobile/types/index'; +import { MoveFileResponse } from '@internxt/sdk/dist/drive/storage/types'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; +import { Image } from 'react-native'; +import { getEnvironmentConfig } from 'src/lib/network'; +import * as networkDownload from 'src/network/download'; +import { DRIVE_THUMBNAILS_DIRECTORY } from '../constants'; import { driveFileCache } from './driveFileCache.service'; -import { Abortable, AsyncStorageKey } from '@internxt-mobile/types/index'; -import asyncStorageService from '@internxt-mobile/services/AsyncStorageService'; export type ArraySortFunction = (a: DriveListItem, b: DriveListItem) => number; export type DriveFileDownloadOptions = { @@ -301,6 +301,7 @@ class DriveFileService { /** NOOP */ }, }, + 0, function () { /** NOOP */ }, @@ -329,6 +330,7 @@ class DriveFileService { disableCache, signal, }: DriveFileDownloadOptions, + fileSize: number, ) { const noop = () => { /** NOOP */ @@ -353,6 +355,7 @@ class DriveFileService { }, signal, }, + fileSize, (abortable) => { if (onAbortableReady) { onAbortableReady(abortable); diff --git a/src/store/slices/drive/index.ts b/src/store/slices/drive/index.ts index c3ba1f6e9..14ad52424 100644 --- a/src/store/slices/drive/index.ts +++ b/src/store/slices/drive/index.ts @@ -1,31 +1,32 @@ -import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { DriveFileData, DriveFolderData } from '@internxt/sdk/dist/drive/storage/types'; +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import analytics, { DriveAnalyticsEvent } from '../../../services/AnalyticsService'; -import { NotificationType } from '../../../types'; +import { logger } from '@internxt-mobile/services/common'; +import drive from '@internxt-mobile/services/drive'; +import { items } from '@internxt/lib'; +import { isValidFilename } from 'src/helpers'; +import authService from 'src/services/AuthService'; +import errorService from 'src/services/ErrorService'; +import { ErrorCodes } from 'src/types/errors'; import { RootState } from '../..'; import strings from '../../../../assets/lang/strings'; +import { MAX_SIZE_TO_DOWNLOAD } from '../../../services/drive/constants'; +import fileSystemService from '../../../services/FileSystemService'; import notificationsService from '../../../services/NotificationsService'; +import { NotificationType } from '../../../types'; import { + DownloadingFile, + DriveEventKey, DriveItemData, + DriveItemFocused, DriveItemStatus, DriveListItem, - UploadingFile, - DownloadingFile, - DriveEventKey, DriveNavigationStack, DriveNavigationStackItem, - DriveItemFocused, + UploadingFile, } from '../../../types/drive'; -import fileSystemService from '../../../services/FileSystemService'; -import { items } from '@internxt/lib'; -import drive from '@internxt-mobile/services/drive'; -import authService from 'src/services/AuthService'; -import errorService from 'src/services/ErrorService'; -import { ErrorCodes } from 'src/types/errors'; -import { isValidFilename } from 'src/helpers'; -import { logger } from '@internxt-mobile/services/common'; export enum ThunkOperationStatus { SUCCESS = 'SUCCESS', @@ -117,6 +118,8 @@ const cancelDownloadThunk = createAsyncThunk(' drive.events.emit({ event: DriveEventKey.CancelDownload }); }); +const DOWNLOAD_ERROR_CODES = { MAX_SIZE_TO_DOWNLOAD_REACHED: 1 }; + const downloadFileThunk = createAsyncThunk< void, { @@ -139,6 +142,16 @@ const downloadFileThunk = createAsyncThunk< ) => { logger.info('Starting file download...'); const { user } = getState().auth; + + if (parseInt(size?.toString() ?? '0') > MAX_SIZE_TO_DOWNLOAD['5GB']) { + dispatch( + driveActions.updateDownloadingFile({ + error: strings.messages.downloadLimit, + }), + ); + return rejectWithValue(DOWNLOAD_ERROR_CODES.MAX_SIZE_TO_DOWNLOAD_REACHED); + } + dispatch( driveActions.updateDownloadingFile({ retry: async () => { @@ -182,13 +195,19 @@ const downloadFileThunk = createAsyncThunk< return; } - return drive.file.downloadFile(user, bucketId, params.fileId, { - downloadPath: params.to, - decryptionProgressCallback, - downloadProgressCallback, - signal, - onAbortableReady: drive.events.setLegacyAbortable, - }); + return drive.file.downloadFile( + user, + bucketId, + params.fileId, + { + downloadPath: params.to, + decryptionProgressCallback, + downloadProgressCallback, + signal, + onAbortableReady: drive.events.setLegacyAbortable, + }, + size, + ); }; const trackDownloadStart = () => { @@ -231,6 +250,7 @@ const downloadFileThunk = createAsyncThunk< if (!fileAlreadyExists) { trackDownloadStart(); downloadProgressCallback(0); + await download({ fileId, to: destinationPath }); } @@ -242,6 +262,7 @@ const downloadFileThunk = createAsyncThunk< trackDownloadSuccess(); } catch (err) { + logger.error('Error in downloadFileThunk ', JSON.stringify(err)); dispatch(driveActions.updateDownloadingFile({ error: (err as Error).message })); /** * In case something fails, we remove the file in case it exists, that way @@ -482,7 +503,12 @@ export const driveSlice = createSlice({ }; }) .addCase(downloadFileThunk.fulfilled, () => undefined) - .addCase(downloadFileThunk.rejected, () => undefined); + .addCase(downloadFileThunk.rejected, (_, action) => { + const errorCode = action.payload; + if (errorCode === DOWNLOAD_ERROR_CODES.MAX_SIZE_TO_DOWNLOAD_REACHED) { + notificationsService.info(strings.messages.downloadLimit); + } + }); builder .addCase(createFolderThunk.pending, (state) => { diff --git a/tsconfig.json b/tsconfig.json index 31823ac13..c54b8f53e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "strict": true, "baseUrl": ".", + "jsx": "react-jsx", "paths": { "@internxt-mobile/ui-kit": ["src/components/ui-kit/index.ts"], "@internxt-mobile/hooks/*": ["src/hooks/*"], diff --git a/yarn.lock b/yarn.lock index 2c8364e90..c48d12eb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1279,6 +1279,14 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@dr.pogodin/react-native-fs@2.27.0": + version "2.27.0" + resolved "https://registry.yarnpkg.com/@dr.pogodin/react-native-fs/-/react-native-fs-2.27.0.tgz#2545a3414b24282aa3b311b97f7e5f9a238916f3" + integrity sha512-1wxXI0Y1LCfUhl5S1p3HxLVvOAS4ooc83KF5uc5gIfgk24JeSYyHbeKDpSwJrZhN5uFtsC+MZG3Gyq8nBV5QFQ== + dependencies: + buffer "^6.0.3" + http-status-codes "^2.3.0" + "@egjs/hammerjs@^2.0.17": version "2.0.17" resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" @@ -1751,10 +1759,10 @@ resolved "https://npm.pkg.github.com/download/@internxt/prettier-config/1.0.2/5bd220b8de76734448db5475b3e0c01f9d22c19b#5bd220b8de76734448db5475b3e0c01f9d22c19b" integrity sha512-t4HiqvCbC7XgQepwWlIaFJe3iwW7HCf6xOSU9nKTV0tiGqOPz7xMtIgLEloQrDA34Cx4PkOYBXrvFPV6RxSFAA== -"@internxt/rn-crypto@^0.1.12": - version "0.1.12" - resolved "https://npm.pkg.github.com/download/@internxt/rn-crypto/0.1.12/7650e6442987720aae1723edb139f73ee2669b98#7650e6442987720aae1723edb139f73ee2669b98" - integrity sha512-IsrJph8uJaO4yz2amUjga3d6hlsEDvWFF7mF+QcAqY2XzsdTaZWH3n6o2G+1IGy9OXeTImou8ob3LMUpGSzu6Q== +"@internxt/rn-crypto@0.1.14": + version "0.1.14" + resolved "https://npm.pkg.github.com/download/@internxt/rn-crypto/0.1.14/9b9f99227744f050b4f197e880ad5dfabca77854#9b9f99227744f050b4f197e880ad5dfabca77854" + integrity sha512-9ATRLw4O77tltmfB6Yk0gNfbHTSpQ/1h5wR80YqnnHQkk4k7W/9wCKQb0binMCV+XtZKhDJMgsK11S0SB+durw== dependencies: buffer "^6.0.3" @@ -7427,6 +7435,11 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http-status-codes@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.3.0.tgz#987fefb28c69f92a43aecc77feec2866349a8bfc" + integrity sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA== + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" @@ -10705,6 +10718,11 @@ react-native-collapsible@^1.6.0: resolved "https://registry.yarnpkg.com/react-native-collapsible/-/react-native-collapsible-1.6.2.tgz#3b67fa402a6ba3c291022f5db8f345083862c3d8" integrity sha512-MCOBVJWqHNjnDaGkvxX997VONmJeebh6wyJxnHEgg0L1PrlcXU1e/bo6eK+CDVFuMrCafw8Qh4DOv/C4V/+Iew== +react-native-create-thumbnail@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-native-create-thumbnail/-/react-native-create-thumbnail-2.0.0.tgz#4eae9b4198cd484a1420608b53ce5872209b9a25" + integrity sha512-1GA0PHGlhSirKUqbtlLLk++3Wkd/fo8H8YUi71Lp5Y2z+AxZMKmY1/IcB9ooDiMzI1Pxr006Kq99JWuXowoykg== + react-native-crypto@^2.0.1, react-native-crypto@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/react-native-crypto/-/react-native-crypto-2.2.0.tgz#c999ed7c96064f830e1f958687f53d0c44025770" @@ -10741,14 +10759,6 @@ react-native-file-viewer@^2.1.4: resolved "https://registry.yarnpkg.com/react-native-file-viewer/-/react-native-file-viewer-2.1.5.tgz#cd4544f573108e79002b5c7e1ebfce4371885250" integrity sha512-MGC6sx9jsqHdefhVQ6o0akdsPGpkXgiIbpygb2Sg4g4bh7v6K1cardLV1NwGB9A6u1yICOSDT/MOC//9Ez6EUg== -react-native-fs@^2.16.6: - version "2.20.0" - resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" - integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ== - dependencies: - base-64 "^0.1.0" - utf8 "^3.0.0" - react-native-gesture-handler@~2.14.0: version "2.14.1" resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.14.1.tgz#930640231024b7921435ab476aa501dd4a6b2e01" @@ -10765,10 +10775,10 @@ react-native-image-pan-zoom@^2.1.12: resolved "https://registry.yarnpkg.com/react-native-image-pan-zoom/-/react-native-image-pan-zoom-2.1.12.tgz#eb98bf56fb5610379bdbfdb63219cc1baca98fd2" integrity sha512-BF66XeP6dzuANsPmmFsJshM2Jyh/Mo1t8FsGc1L9Q9/sVP8MJULDabB1hms+eAoqgtyhMr5BuXV3E1hJ5U5H6Q== -react-native-image-picker@^4.0.6: - version "4.10.3" - resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-4.10.3.tgz#cdc11d9836b4cfa57e658c0700201babf8fdca10" - integrity sha512-gLX8J6jCBkUt6jogpSdA7YyaGVLGYywRzMEwBciXshihpFZjc/cRlKymAVlu6Q7HMw0j3vrho6pI8ZGC5O/FGg== +react-native-image-picker@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-7.1.2.tgz#383849d1953caf4578874a1f5e5dd11c737bd5cd" + integrity sha512-b5y5nP60RIPxlAXlptn2QwlIuZWCUDWa/YPUVjgHc0Ih60mRiOg1PSzf0IjHSLeOZShCpirpvSPGnDExIpTRUg== react-native-image-zoom-viewer@^3.0.1: version "3.0.1" @@ -13130,11 +13140,6 @@ utf8-byte-length@^1.0.1: resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz#f9f63910d15536ee2b2d5dd4665389715eac5c1e" integrity sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA== -utf8@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" - integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== - util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"