From 3e01dd413f7c6755adcf6b7f34e50ee04cd38511 Mon Sep 17 00:00:00 2001 From: Kushdeep Singh <63536883+meKushdeepSingh@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:21:25 +0530 Subject: [PATCH] DMAPP-142: File Download Issue in Chat Module (#148) - Resolved file download handling for the chat module: - On Android devices, files will now be saved directly to the public Download directory. - On iOS devices, files will be temporarily cached, allowing users to manually save them permanently or share them as needed. --- android/app/src/main/AndroidManifest.xml | 1 + ios/Podfile.lock | 6 + package.json | 1 + .../components/messageTypes/FileMessage.tsx | 29 +++-- .../screens/chats/helpers/useFileDownload.ts | 107 ++++++++++++++++++ yarn.lock | 21 ++++ 6 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 src/navigation/screens/chats/helpers/useFileDownload.ts diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f146a2f1e..82900570a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4c9319b78..11b6e465f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -408,6 +408,8 @@ PODS: - React-jsinspector (0.71.14) - React-logger (0.71.14): - glog + - react-native-blob-util (0.19.11): + - React-Core - react-native-config (1.5.0): - react-native-config/App (= 1.5.0) - react-native-config/App (1.5.0): @@ -643,6 +645,7 @@ DEPENDENCIES: - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - react-native-blob-util (from `../node_modules/react-native-blob-util`) - react-native-config (from `../node_modules/react-native-config`) - react-native-encrypted-storage (from `../node_modules/react-native-encrypted-storage`) - react-native-fast-openpgp (from `../node_modules/react-native-fast-openpgp`) @@ -800,6 +803,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsinspector" React-logger: :path: "../node_modules/react-native/ReactCommon/logger" + react-native-blob-util: + :path: "../node_modules/react-native-blob-util" react-native-config: :path: "../node_modules/react-native-config" react-native-encrypted-storage: @@ -959,6 +964,7 @@ SPEC CHECKSUMS: React-jsiexecutor: 94cfc1788637ceaf8841ef1f69b10cc0d62baadc React-jsinspector: 7bf923954b4e035f494b01ac16633963412660d7 React-logger: 655ff5db8bd922acfbe76a4983ffab048916343e + react-native-blob-util: 39a20f2ef11556d958dc4beb0aa07d1ef2690745 react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727 react-native-encrypted-storage: db300a3f2f0aba1e818417c1c0a6be549038deb7 react-native-fast-openpgp: f4158eb50244457e12f9d2aec06bf7cfcba314ef diff --git a/package.json b/package.json index 64e8b38d6..43ab12c2d 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "node-libs-browser": "^2.2.1", "react": "18.2.0", "react-native": "0.71.14", + "react-native-blob-util": "^0.19.11", "react-native-cache": "^2.0.3", "react-native-callkeep": "4.3.8", "react-native-config": "^1.5.0", diff --git a/src/navigation/screens/chats/components/messageTypes/FileMessage.tsx b/src/navigation/screens/chats/components/messageTypes/FileMessage.tsx index 39af30104..b7eb5e212 100644 --- a/src/navigation/screens/chats/components/messageTypes/FileMessage.tsx +++ b/src/navigation/screens/chats/components/messageTypes/FileMessage.tsx @@ -2,9 +2,13 @@ import {FontAwesome} from '@expo/vector-icons'; import {IMessageIPFS} from '@pushprotocol/restapi'; import React from 'react'; import {Linking, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; +import {ActivityIndicator} from 'react-native'; import {SvgUri} from 'react-native-svg'; +import {useToaster} from 'src/contexts/ToasterContext'; import {formatAMPM} from 'src/helpers/DateTimeHelper'; +import {useFileDownload} from '../../helpers/useFileDownload'; + export interface FileMessageContent { content: string; name: string; @@ -42,6 +46,13 @@ export const FileMessageComponent = ({ const content = fileContent.content as string; const size = fileContent.size; + const {toastRef} = useToaster(); + const [isDownloading, saveBase64File] = useFileDownload( + content, + name, + toastRef, + ); + return ( - {!messageType && ( - - Linking.openURL(content).catch(e => { - console.log('err', e); - }) - }> - - - )} + {!messageType && + (isDownloading ? ( + + ) : ( + saveBase64File()}> + + + ))} {!messageType && ( {formatAMPM(chatMessage.timestamp!)} diff --git a/src/navigation/screens/chats/helpers/useFileDownload.ts b/src/navigation/screens/chats/helpers/useFileDownload.ts new file mode 100644 index 000000000..82277df9b --- /dev/null +++ b/src/navigation/screens/chats/helpers/useFileDownload.ts @@ -0,0 +1,107 @@ +import {useState} from 'react'; +import {PermissionsAndroid, Platform, Share} from 'react-native'; +import RNBlobUtil from 'react-native-blob-util'; +import {Toaster, ToasterOptions} from 'src/components/indicators/Toaster'; + +const androidDownloadDir = '/storage/emulated/0/Download'; + +const useFileDownload = ( + base64DataWithPrefix: string, + fileName: string, + toastRef?: React.RefObject, +): [boolean, Function] => { + const [isDownloading, setIsDownloading] = useState(false); + + // Request Storage Permissions (Android) + const requestStoragePermission = async () => { + if (Platform.OS === 'android' && Platform.Version <= 28) { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, + { + title: 'Storage Permission Required', + message: + "The PUSH app requires access to your device's storage to download files.", + buttonNeutral: 'Ask Me Later', + buttonNegative: 'Cancel', + buttonPositive: 'OK', + }, + ); + + return granted === PermissionsAndroid.RESULTS.GRANTED; + } + return true; // Permissions are automatically granted for iOS and Android 10+ + }; + + const handleIOSDownload = (filePath: string) => { + // Share will give option to download the file on iOS Public directory + Share.share({ + url: `file://${filePath}`, + }) + .then(res => { + if ( + res.action === 'sharedAction' && + res.activityType === 'com.apple.DocumentManagerUICore.SaveToFiles' + ) { + if (toastRef) { + toastRef.current?.showToast( + 'File Saved successfully!', + '', + ToasterOptions.TYPE.GRADIENT_PRIMARY, + ); + } + } + }) + .catch(err => console.log('iOS share error', err)) + .finally(() => setIsDownloading(false)); + }; + + const handleAndroidDownload = () => { + // file is already saved in android public directory + setIsDownloading(false); + if (toastRef) { + toastRef.current?.showToast( + 'File downloaded successfully!', + '', + ToasterOptions.TYPE.GRADIENT_PRIMARY, + ); + } + }; + + // Save Base64 File + const saveBase64File = async () => { + setIsDownloading(true); + try { + // Request permission for Android + const hasPermission = await requestStoragePermission(); + if (!hasPermission) { + console.log('Storage permission denied'); + setIsDownloading(false); + return; + } + + // Remove MIME type prefix from Base64 string + const base64Data = base64DataWithPrefix.split(',')[1]; + + // Define the file path + const filePath = + Platform.OS === 'android' + ? `${androidDownloadDir}/${fileName}` // Public Downloads on Android + : `${RNBlobUtil.fs.dirs.CacheDir}/${fileName}`; // Documents directory on iOS + + // Write the file + await RNBlobUtil.fs.writeFile(filePath, base64Data, 'base64'); + if (Platform.OS === 'ios') { + handleIOSDownload(filePath); + } else { + handleAndroidDownload(); + } + } catch (error) { + console.error('Error saving file:', error); + setIsDownloading(false); + } + }; + + return [isDownloading, saveBase64File]; +}; + +export {useFileDownload}; diff --git a/yarn.lock b/yarn.lock index f6bb82663..f56ff3241 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6929,6 +6929,13 @@ __metadata: languageName: node linkType: hard +"base-64@npm:0.1.0": + version: 0.1.0 + resolution: "base-64@npm:0.1.0" + checksum: 10/5a42938f82372ab5392cbacc85a5a78115cbbd9dbef9f7540fa47d78763a3a8bd7d598475f0d92341f66285afd377509851a9bb5c67bbecb89686e9255d5b3eb + languageName: node + linkType: hard + "base-64@npm:^1.0.0": version: 1.0.0 resolution: "base-64@npm:1.0.0" @@ -16261,6 +16268,7 @@ __metadata: prettier: "npm:^2.7.1" react: "npm:18.2.0" react-native: "npm:0.71.14" + react-native-blob-util: "npm:^0.19.11" react-native-cache: "npm:^2.0.3" react-native-callkeep: "npm:4.3.8" react-native-config: "npm:^1.5.0" @@ -16538,6 +16546,19 @@ __metadata: languageName: node linkType: hard +"react-native-blob-util@npm:^0.19.11": + version: 0.19.11 + resolution: "react-native-blob-util@npm:0.19.11" + dependencies: + base-64: "npm:0.1.0" + glob: "npm:^10.3.10" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/b5496b2d41ea76fc4ff85d3f88ec1c6b2d7d409b88b1d71d5a51edf64564fa0f9f444d1cd634e641f043387ca04425a9e7e337670870a528095afd64223186f9 + languageName: node + linkType: hard + "react-native-cache@npm:^2.0.3": version: 2.0.3 resolution: "react-native-cache@npm:2.0.3"