From 83d70639fc89e69ba5339503b07946a7640d07b2 Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Thu, 28 Nov 2024 03:21:30 -0500 Subject: [PATCH 1/3] Working fix + Native Swift Functions --- apps/mobile/app.json | 3 +- .../native-functions/NativeFunctions.m | 24 +++ .../native-functions/NativeFunctions.swift | 150 ++++++++++++++++++ apps/mobile/scripts/withNativeFunctions.js | 82 ++++++++++ .../src/components/explorer/Explorer.tsx | 52 ++++-- .../src/components/modal/ImportModal.tsx | 53 +++++-- 6 files changed, 338 insertions(+), 26 deletions(-) create mode 100644 apps/mobile/modules/native-functions/NativeFunctions.m create mode 100644 apps/mobile/modules/native-functions/NativeFunctions.swift create mode 100644 apps/mobile/scripts/withNativeFunctions.js diff --git a/apps/mobile/app.json b/apps/mobile/app.json index 70f61335f..27df7419a 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -64,7 +64,8 @@ } ], ["./scripts/withRiveAssets.js"], - ["./scripts/withAndroidIntent.js"] + ["./scripts/withAndroidIntent.js"], + ["./scripts/withNativeFunctions.js"] ] } } diff --git a/apps/mobile/modules/native-functions/NativeFunctions.m b/apps/mobile/modules/native-functions/NativeFunctions.m new file mode 100644 index 000000000..20558c67d --- /dev/null +++ b/apps/mobile/modules/native-functions/NativeFunctions.m @@ -0,0 +1,24 @@ +// +// NativeFunctions.m +// Spacedrive +// +// Created by Arnab Chakraborty on November 27, 2024. +// + +#import +#import + +@interface RCT_EXTERN_MODULE(NativeFunctions, NSObject) + +RCT_EXTERN_METHOD(saveLocation:(nonnull NSString *)path + locationId:(nonnull NSNumber *)locationId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(previewFile:(nonnull NSString *)path + locationId:(nonnull NSNumber *)locationId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +@end + diff --git a/apps/mobile/modules/native-functions/NativeFunctions.swift b/apps/mobile/modules/native-functions/NativeFunctions.swift new file mode 100644 index 000000000..d28ff6fa1 --- /dev/null +++ b/apps/mobile/modules/native-functions/NativeFunctions.swift @@ -0,0 +1,150 @@ +// +// NativeFunctions.swift +// Spacedrive +// +// Created by Arnab Chakraborty on November 27, 2024. +// + +import Foundation +import UIKit +import QuickLook + +@objc(NativeFunctions) +class NativeFunctions: NSObject, QLPreviewControllerDataSource { + private var fileURL: URL? + + @objc + static func requiresMainQueueSetup() -> Bool { + return true + } + + private func getBookmarkStoragePath(for id: Int) -> URL { + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + return documentsDirectory.appendingPathComponent("\(id).sd_bookmark") + } + + @objc + func saveLocation(_ path: String, + locationId: NSNumber, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + do { + let url = URL(fileURLWithPath: path) + guard url.startAccessingSecurityScopedResource() else { + reject("ERROR", "Cannot access directory", nil) + return + } + defer { url.stopAccessingSecurityScopedResource() } + + let bookmarkData = try url.bookmarkData( + options: .minimalBookmark, + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + + let bookmarkPath = getBookmarkStoragePath(for: locationId.intValue) + try bookmarkData.write(to: bookmarkPath, options: .atomicWrite) + + resolve(["success": true]) + } catch { + reject("ERROR", "Failed to create bookmark: \(error.localizedDescription)", nil) + } + } + + @objc + func previewFile(_ path: String, + locationId: NSNumber, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + print("🔍 PreviewFile called with path: \(path), locationId: \(locationId)") + + do { + let bookmarkPath = getBookmarkStoragePath(for: locationId.intValue) + print("📁 Bookmark path: \(bookmarkPath)") + + let fileURL = URL(fileURLWithPath: path) + print("📄 File URL: \(fileURL)") + + if FileManager.default.fileExists(atPath: bookmarkPath.path) { + print("✅ Bookmark exists at path") + let bookmarkData = try Data(contentsOf: bookmarkPath) + print("📊 Bookmark data size: \(bookmarkData.count) bytes") + + var isStale = false + let directoryURL = try URL( + resolvingBookmarkData: bookmarkData, + options: [], + relativeTo: nil, + bookmarkDataIsStale: &isStale + ) + print("📂 Resolved directory URL: \(directoryURL)") + print("🔄 Is bookmark stale? \(isStale)") + + guard directoryURL.startAccessingSecurityScopedResource() else { + print("❌ Failed to access security-scoped resource for directory") + reject("ERROR", "Cannot access directory", nil) + return + } + defer { + directoryURL.stopAccessingSecurityScopedResource() + print("🔒 Stopped accessing security-scoped resource") + } + + let fileName = fileURL.lastPathComponent + print("📝 File name: \(fileName)") + + let resolvedFileURL = directoryURL.appendingPathComponent(fileName) + print("🎯 Resolved file URL: \(resolvedFileURL)") + + // Check if file exists at resolved path + if FileManager.default.fileExists(atPath: resolvedFileURL.path) { + print("✅ File exists at resolved path") + } else { + print("⚠️ File does not exist at resolved path") + } + + self.fileURL = resolvedFileURL + print("💾 Set fileURL for QuickLook: \(resolvedFileURL)") + } else { + print("❌ Bookmark not found at path: \(bookmarkPath)") + reject("ERROR", "Bookmark not found for this location", nil) + return + } + + print("🚀 Preparing to present QuickLook controller") + DispatchQueue.main.async { + let previewController = QLPreviewController() + previewController.dataSource = self + + guard let presentedVC = RCTPresentedViewController() else { + print("❌ Failed to get presented view controller") + reject("ERROR", "Cannot present preview", nil) + return + } + + print("📱 Presenting QuickLook controller") + presentedVC.present(previewController, animated: true) { + print("✨ QuickLook controller presented successfully") + resolve(["success": true]) + } + } + } catch { + print("💥 Error occurred: \(error.localizedDescription)") + print("🔍 Detailed error: \(error)") + reject("ERROR", "Failed to preview file: \(error.localizedDescription)", nil) + } + } + + + // MARK: - QLPreviewControllerDataSource + func numberOfPreviewItems(in controller: QLPreviewController) -> Int { + print("📊 numberOfPreviewItems called, returning: \(fileURL != nil ? 1 : 0)") + return fileURL != nil ? 1 : 0 + } + + func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { + print("🎯 previewItemAt called for index: \(index)") + print("📄 Returning fileURL: \(String(describing: fileURL))") + return fileURL! as QLPreviewItem + } +} diff --git a/apps/mobile/scripts/withNativeFunctions.js b/apps/mobile/scripts/withNativeFunctions.js new file mode 100644 index 000000000..8aed22d2f --- /dev/null +++ b/apps/mobile/scripts/withNativeFunctions.js @@ -0,0 +1,82 @@ +const { withXcodeProject } = require('@expo/config-plugins'); +const fs = require('fs'); +const path = require('path'); + +/** + * @typedef {Object} XcodeProject + * @property {Function} pbxGroupByName - Gets a PBX group by name + * @property {Function} findPBXGroupKey - Finds PBX group key + * @property {(filePath: string, target?: string | null, groupKey: string) => string} addSourceFile - Adds source file to project and returns the file reference + */ + +/** + * @typedef {Object} ExpoConfig + * @property {XcodeProject} modResults - Xcode project modification results + */ + +/** + * Adds native Swift functions to iOS Xcode project + * @param {import('@expo/config-plugins').ExpoConfig} config - Expo config object + * @returns {Promise} Modified config + */ +/** + * Enhances the provided configuration with native functions for an iOS project. + * + * This function modifies the Xcode project by copying necessary `.swift` and `.m` files + * to the iOS project directory and adding them to the project. It also updates the + * `Spacedrive-Bridging-Header.h` file with the required imports. + * + * @param {object} config - The configuration object to enhance. + * @returns {object} The modified configuration object. + * + * @modifies Spacedrive-Bridging-Header.h + * // This file is autogenerated by `withNativeFunctions.js`. Do not modify this file + * #import + */ +const withNativeFunctions = (config) => { + const mod = withXcodeProject(config, async (config) => { + /** @type {XcodeProject} */ + const project = config.modResults; + + /** @type {{name: string, path: string}} */ + const group = project.pbxGroupByName('Spacedrive'); + /** @type {string} */ + const key = project.findPBXGroupKey({ name: group.name, path: group.path }); + + const iosProjectFolder = path.join(__dirname, '../ios'); + + // Copy the .swift and .m files to the iOS project + fs.copyFileSync( + path.join(__dirname, '../modules/native-functions/NativeFunctions.swift'), + path.join(iosProjectFolder, 'NativeFunctions.swift') + ); + fs.copyFileSync( + path.join(__dirname, '../modules/native-functions/NativeFunctions.m'), + path.join(iosProjectFolder, 'NativeFunctions.m') + ); + + // Add the .swift file to the project + config.modResults.addSourceFile('NativeFunctions.swift', null, key); + // Add the .m file to the project + config.modResults.addSourceFile('NativeFunctions.m', null, key); + + // Update the Spacedrive-Bridging-Header.h file + const bridgingHeaderPath = path.join(iosProjectFolder, '/Spacedrive/Spacedrive-Bridging-Header.h'); + // Empty the file first + fs.writeFileSync(bridgingHeaderPath, ''); + + const comment = '// This file is autogenerated by `withNativeFunctions.js`. Do not modify this file, as it will be overwritten by the build process.\n'; + const importStatement = '#import \n'; + + // Write new content + fs.writeFileSync(bridgingHeaderPath, comment + importStatement); + + + + return config; + }); + + return mod; +}; + +module.exports = withNativeFunctions; diff --git a/apps/mobile/src/components/explorer/Explorer.tsx b/apps/mobile/src/components/explorer/Explorer.tsx index d3281aa7d..2139bde76 100644 --- a/apps/mobile/src/components/explorer/Explorer.tsx +++ b/apps/mobile/src/components/explorer/Explorer.tsx @@ -1,9 +1,10 @@ +import RNFS from '@dr.pogodin/react-native-fs'; import { useNavigation } from '@react-navigation/native'; import { FlashList } from '@shopify/flash-list'; import { InfiniteData, UseInfiniteQueryResult } from '@tanstack/react-query'; import * as Haptics from 'expo-haptics'; -import { useRef } from 'react'; -import { ActivityIndicator } from 'react-native'; +import React, { useRef } from 'react'; +import { ActivityIndicator, NativeModules, Platform } from 'react-native'; import FileViewer from 'react-native-file-viewer'; import { getIndexedItemFilePath, @@ -27,6 +28,8 @@ import FileMedia from './FileMedia'; import FileRow from './FileRow'; import Menu from './menu/Menu'; +const { NativeFunctions } = NativeModules; + type ExplorerProps = { tabHeight?: boolean; items: ExplorerItem[] | null; @@ -54,19 +57,40 @@ const Explorer = (props: Props) => { //Open file with native api async function handleOpen(data: ExplorerItem) { - try { - const filePath = getIndexedItemFilePath(data); - const absolutePath = await libraryClient.query(['files.getPath', filePath?.id ?? -1]); + const filePath = getIndexedItemFilePath(data); + if (Platform.OS === 'android') { + try { + const absolutePath = await libraryClient.query([ + 'files.getPath', + filePath?.id ?? -1 + ]); + if (!absolutePath) return; + await FileViewer.open(absolutePath, { + // Android only + showAppsSuggestions: false, // If there is not an installed app that can open the file, open the Play Store with suggested apps + showOpenWithDialog: true // if there is more than one app that can open the file, show an Open With dialogue box + }); + if (filePath && filePath.object_id) + await libraryClient.mutation(['files.updateAccessTime', [filePath.object_id]]); + } catch (error) { + console.error('Error opening object', error); + toast.error('Error opening object'); + } + } else { + // iOS + const absolutePath = await libraryClient.query([ + 'files.getPath', + filePath?.id ?? -1 + ]); if (!absolutePath) return; - await FileViewer.open(absolutePath, { - // Android only - showAppsSuggestions: false, // If there is not an installed app that can open the file, open the Play Store with suggested apps - showOpenWithDialog: true // if there is more than one app that can open the file, show an Open With dialogue box - }); - if (filePath && filePath.object_id) - await libraryClient.mutation(['files.updateAccessTime', [filePath.object_id]]); - } catch (error) { - toast.error('Error opening object'); + if (!filePath?.location_id) return; + try { + // These arguments cannot be null due to compatability with Android (React Native throws an error if even the type is nullable) + await NativeFunctions.previewFile(absolutePath!, filePath!.location_id!); + } catch (error) { + console.error('Error previewing file:', error); + toast.error('Error previewing file'); + } } } diff --git a/apps/mobile/src/components/modal/ImportModal.tsx b/apps/mobile/src/components/modal/ImportModal.tsx index d304b5700..08e53bfb4 100644 --- a/apps/mobile/src/components/modal/ImportModal.tsx +++ b/apps/mobile/src/components/modal/ImportModal.tsx @@ -1,8 +1,8 @@ import * as RNFS from '@dr.pogodin/react-native-fs'; import { forwardRef, useCallback } from 'react'; -import { Alert, Platform, Text, View } from 'react-native'; +import { Alert, NativeModules, Platform, Text, View } from 'react-native'; import DocumentPicker from 'react-native-document-picker'; -import { useLibraryMutation, useRspcLibraryContext } from '@sd/client'; +import { useLibraryMutation, useLibraryQuery, useRspcLibraryContext } from '@sd/client'; import { Modal, ModalRef } from '~/components/layout/Modal'; import { Button } from '~/components/primitive/Button'; import useForwardedRef from '~/hooks/useForwardedRef'; @@ -11,6 +11,18 @@ import { tw } from '~/lib/tailwind'; import { Icon } from '../icons/Icon'; import { toast } from '../primitive/Toast'; +const { NativeFunctions } = NativeModules; + +interface DirectoryPickerResult { + path: string; + bookmarkFile: string; +} + +interface DirectoryPickerModule { + pickDirectory(): Promise; + resolveBookmark(bookmarkFileName: string): Promise<{ path: string }>; +} + // import * as ML from 'expo-media-library'; // WIP component @@ -43,7 +55,18 @@ const ImportModal = forwardRef((_, ref) => { throw new Error('Unimplemented custom remote error handling'); } }, - onSuccess: () => { + onSuccess: async (data) => { + // Fetch the location's path using the location number + const location = await rspc.client.query(['locations.get', data!]); + const locationPath = location?.path; + try { + // These arguments cannot be null due to compatability with Android (React Native throws an error if even the type is nullable) + await NativeFunctions.saveLocation(locationPath!, data!); + } catch (error) { + console.error('Error saving location:', error); + toast.error('Error saving location bookmark'); + return; + } toast.success('Location added successfully'); }, onSettled: () => { @@ -53,16 +76,24 @@ const ImportModal = forwardRef((_, ref) => { }); const handleFilesButton = useCallback(async () => { + const response = await DocumentPicker.pickDirectory({ + presentationStyle: 'pageSheet' + }); + + if (!response) return; + + const uri = response.uri; + try { - const response = await DocumentPicker.pickDirectory({ - presentationStyle: 'pageSheet' - }); - - if (!response) return; - - const uri = response.uri; - if (Platform.OS === 'android') { + const response = await DocumentPicker.pickDirectory({ + presentationStyle: 'pageSheet' + }); + + if (!response) return; + + const uri = response.uri; + // The following code turns this: content://com.android.externalstorage.documents/tree/[filePath] into this: /storage/emulated/0/[directoryName] // Example: content://com.android.externalstorage.documents/tree/primary%3ADownload%2Ftest into /storage/emulated/0/Download/test const dirName = decodeURIComponent(uri).split('/'); From aba5e44bca959bb6a58d5eadf8dd8898e4acabbc Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Thu, 28 Nov 2024 03:44:21 -0500 Subject: [PATCH 2/3] Only print when in debug mode --- .../native-functions/NativeFunctions.swift | 82 ++++++++++++++----- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/apps/mobile/modules/native-functions/NativeFunctions.swift b/apps/mobile/modules/native-functions/NativeFunctions.swift index d28ff6fa1..6a3ea4d76 100644 --- a/apps/mobile/modules/native-functions/NativeFunctions.swift +++ b/apps/mobile/modules/native-functions/NativeFunctions.swift @@ -12,17 +12,17 @@ import QuickLook @objc(NativeFunctions) class NativeFunctions: NSObject, QLPreviewControllerDataSource { private var fileURL: URL? - + @objc static func requiresMainQueueSetup() -> Bool { return true } - + private func getBookmarkStoragePath(for id: Int) -> URL { let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] return documentsDirectory.appendingPathComponent("\(id).sd_bookmark") } - + @objc func saveLocation(_ path: String, locationId: NSNumber, @@ -35,41 +35,51 @@ class NativeFunctions: NSObject, QLPreviewControllerDataSource { return } defer { url.stopAccessingSecurityScopedResource() } - + let bookmarkData = try url.bookmarkData( options: .minimalBookmark, includingResourceValuesForKeys: nil, relativeTo: nil ) - + let bookmarkPath = getBookmarkStoragePath(for: locationId.intValue) try bookmarkData.write(to: bookmarkPath, options: .atomicWrite) - + resolve(["success": true]) } catch { reject("ERROR", "Failed to create bookmark: \(error.localizedDescription)", nil) } } - + @objc func previewFile(_ path: String, locationId: NSNumber, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + #if DEBUG print("🔍 PreviewFile called with path: \(path), locationId: \(locationId)") - + #endif + do { let bookmarkPath = getBookmarkStoragePath(for: locationId.intValue) + #if DEBUG print("📁 Bookmark path: \(bookmarkPath)") - + #endif + let fileURL = URL(fileURLWithPath: path) + #if DEBUG print("📄 File URL: \(fileURL)") - + #endif + if FileManager.default.fileExists(atPath: bookmarkPath.path) { + #if DEBUG print("✅ Bookmark exists at path") + #endif let bookmarkData = try Data(contentsOf: bookmarkPath) + #if DEBUG print("📊 Bookmark data size: \(bookmarkData.count) bytes") - + #endif + var isStale = false let directoryURL = try URL( resolvingBookmarkData: bookmarkData, @@ -77,74 +87,106 @@ class NativeFunctions: NSObject, QLPreviewControllerDataSource { relativeTo: nil, bookmarkDataIsStale: &isStale ) + #if DEBUG print("📂 Resolved directory URL: \(directoryURL)") print("🔄 Is bookmark stale? \(isStale)") - + #endif + guard directoryURL.startAccessingSecurityScopedResource() else { + #if DEBUG print("❌ Failed to access security-scoped resource for directory") + #endif reject("ERROR", "Cannot access directory", nil) return } defer { directoryURL.stopAccessingSecurityScopedResource() + #if DEBUG print("🔒 Stopped accessing security-scoped resource") + #endif } - + let fileName = fileURL.lastPathComponent + #if DEBUG print("📝 File name: \(fileName)") - + #endif + let resolvedFileURL = directoryURL.appendingPathComponent(fileName) + #if DEBUG print("🎯 Resolved file URL: \(resolvedFileURL)") - + #endif + // Check if file exists at resolved path if FileManager.default.fileExists(atPath: resolvedFileURL.path) { + #if DEBUG print("✅ File exists at resolved path") + #endif } else { + #if DEBUG print("⚠️ File does not exist at resolved path") + #endif } - + self.fileURL = resolvedFileURL + #if DEBUG print("💾 Set fileURL for QuickLook: \(resolvedFileURL)") + #endif } else { + #if DEBUG print("❌ Bookmark not found at path: \(bookmarkPath)") + #endif reject("ERROR", "Bookmark not found for this location", nil) return } - + + #if DEBUG print("🚀 Preparing to present QuickLook controller") + #endif DispatchQueue.main.async { let previewController = QLPreviewController() previewController.dataSource = self - + guard let presentedVC = RCTPresentedViewController() else { + #if DEBUG print("❌ Failed to get presented view controller") + #endif reject("ERROR", "Cannot present preview", nil) return } - + + #if DEBUG print("📱 Presenting QuickLook controller") + #endif presentedVC.present(previewController, animated: true) { + #if DEBUG print("✨ QuickLook controller presented successfully") + #endif resolve(["success": true]) } } } catch { + #if DEBUG print("💥 Error occurred: \(error.localizedDescription)") print("🔍 Detailed error: \(error)") + #endif reject("ERROR", "Failed to preview file: \(error.localizedDescription)", nil) } } - + // MARK: - QLPreviewControllerDataSource func numberOfPreviewItems(in controller: QLPreviewController) -> Int { + #if DEBUG print("📊 numberOfPreviewItems called, returning: \(fileURL != nil ? 1 : 0)") + #endif return fileURL != nil ? 1 : 0 } func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { + #if DEBUG print("🎯 previewItemAt called for index: \(index)") print("📄 Returning fileURL: \(String(describing: fileURL))") + #endif return fileURL! as QLPreviewItem } } From cc429f34a8850405cf833232613fbc33e4aa3f83 Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Thu, 28 Nov 2024 03:51:33 -0500 Subject: [PATCH 3/3] Lint --- apps/mobile/scripts/withNativeFunctions.js | 10 ++++++---- apps/mobile/src/components/explorer/Explorer.tsx | 9 +++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/mobile/scripts/withNativeFunctions.js b/apps/mobile/scripts/withNativeFunctions.js index 8aed22d2f..580e01dce 100644 --- a/apps/mobile/scripts/withNativeFunctions.js +++ b/apps/mobile/scripts/withNativeFunctions.js @@ -61,18 +61,20 @@ const withNativeFunctions = (config) => { config.modResults.addSourceFile('NativeFunctions.m', null, key); // Update the Spacedrive-Bridging-Header.h file - const bridgingHeaderPath = path.join(iosProjectFolder, '/Spacedrive/Spacedrive-Bridging-Header.h'); + const bridgingHeaderPath = path.join( + iosProjectFolder, + '/Spacedrive/Spacedrive-Bridging-Header.h' + ); // Empty the file first fs.writeFileSync(bridgingHeaderPath, ''); - const comment = '// This file is autogenerated by `withNativeFunctions.js`. Do not modify this file, as it will be overwritten by the build process.\n'; + const comment = + '// This file is autogenerated by `withNativeFunctions.js`. Do not modify this file, as it will be overwritten by the build process.\n'; const importStatement = '#import \n'; // Write new content fs.writeFileSync(bridgingHeaderPath, comment + importStatement); - - return config; }); diff --git a/apps/mobile/src/components/explorer/Explorer.tsx b/apps/mobile/src/components/explorer/Explorer.tsx index 2139bde76..803044e4e 100644 --- a/apps/mobile/src/components/explorer/Explorer.tsx +++ b/apps/mobile/src/components/explorer/Explorer.tsx @@ -78,19 +78,16 @@ const Explorer = (props: Props) => { } } else { // iOS - const absolutePath = await libraryClient.query([ - 'files.getPath', - filePath?.id ?? -1 - ]); + const absolutePath = await libraryClient.query(['files.getPath', filePath?.id ?? -1]); if (!absolutePath) return; if (!filePath?.location_id) return; try { // These arguments cannot be null due to compatability with Android (React Native throws an error if even the type is nullable) await NativeFunctions.previewFile(absolutePath!, filePath!.location_id!); - } catch (error) { + } catch (error) { console.error('Error previewing file:', error); toast.error('Error previewing file'); - } + } } }