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"