diff --git a/examples/ExpoMessaging/app.json b/examples/ExpoMessaging/app.json index 32df3d0ad..934ffc367 100644 --- a/examples/ExpoMessaging/app.json +++ b/examples/ExpoMessaging/app.json @@ -47,6 +47,17 @@ { "microphonePermission": "$(PRODUCT_NAME) would like to use your microphone for voice recording." } + ], + [ + "expo-build-properties", + { + "android": { + "newArchEnabled": true + }, + "ios": { + "newArchEnabled": true + } + } ] ] } diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index a992059af..fcc57494a 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -1439,6 +1439,27 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-cameraroll (7.8.4): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-document-picker (9.3.1): - DoubleConversion - glog @@ -2233,6 +2254,7 @@ DEPENDENCIES: - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - react-native-blob-util (from `../node_modules/react-native-blob-util`) + - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" - react-native-document-picker (from `../node_modules/react-native-document-picker`) - react-native-image-picker (from `../node_modules/react-native-image-picker`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" @@ -2381,6 +2403,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" react-native-blob-util: :path: "../node_modules/react-native-blob-util" + react-native-cameraroll: + :path: "../node_modules/@react-native-camera-roll/camera-roll" react-native-document-picker: :path: "../node_modules/react-native-document-picker" react-native-image-picker: @@ -2531,6 +2555,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 858e7be7b51da78fd6a12b2461d92ea44e88c0f2 React-microtasksnativemodule: a221789bbd16b09f083a60a4bec8623e2dfbb59f react-native-blob-util: 356047c561b3506396852bc0d7988243f74dd77d + react-native-cameraroll: 55de4896c31042eedcb0629a58ef5ff0d5a2c428 react-native-document-picker: 1e082836a633ca9e7c75758036ff42535baaf36b react-native-image-picker: df6597d4b1878a443796be11eb2b7286ed10ece6 react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index b58496856..8ab5f30f2 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -25,6 +25,7 @@ "@notifee/react-native": "^9.1.2", "@op-engineering/op-sqlite": "^9.3.0", "@react-native-async-storage/async-storage": "^1.21.0", + "@react-native-camera-roll/camera-roll": "^7.8.4", "@react-native-community/netinfo": "^11.3.2", "@react-native-firebase/app": "^19.3.0", "@react-native-firebase/messaging": "^19.3.0", @@ -40,7 +41,6 @@ "react-native-fast-image": "^8.6.3", "react-native-gesture-handler": "^2.18.1", "react-native-haptic-feedback": "^2.3.3", - "react-native-image-picker": "^7.1.2", "react-native-reanimated": "^3.16.0", "react-native-safe-area-context": "^4.14.0", "react-native-screens": "^3.34.0", diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock index 5c60dd24b..d36fe3193 100644 --- a/examples/SampleApp/yarn.lock +++ b/examples/SampleApp/yarn.lock @@ -1882,6 +1882,11 @@ dependencies: merge-options "^3.0.4" +"@react-native-camera-roll/camera-roll@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-7.8.4.tgz#2422ad5ca4e55fa2973e8c03255e3dc7b330925c" + integrity sha512-CraoJLSOpiPnPsuzbJoydKrtCJY6rPAqHn7fzN/4uCOqoah4IlauMcNVSvr4N9qbemIztPrHBnDgLJtYDlgo+A== + "@react-native-community/cli-clean@15.0.0": version "15.0.0" resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-15.0.0.tgz#10c7cfde8379aaa7a60eaf4bd3920542d4af1b81" @@ -6688,11 +6693,6 @@ react-native-haptic-feedback@^2.3.3: resolved "https://registry.yarnpkg.com/react-native-haptic-feedback/-/react-native-haptic-feedback-2.3.3.tgz#88b6876e91399a69bd1b551fe1681b2f3dc1214e" integrity sha512-svS4D5PxfNv8o68m9ahWfwje5NqukM3qLS48+WTdhbDkNUkOhP9rDfDSRHzlhk4zq+ISjyw95EhLeh8NkKX5vQ== -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-lightbox@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/react-native-lightbox/-/react-native-lightbox-0.7.0.tgz#e52b4d7fcc141f59d7b23f0180de535e35b20ec9" diff --git a/package/expo-package/src/optionalDependencies/getLocalAssetUri.ts b/package/expo-package/src/optionalDependencies/getLocalAssetUri.ts index 3d79743c9..5f51479f7 100644 --- a/package/expo-package/src/optionalDependencies/getLocalAssetUri.ts +++ b/package/expo-package/src/optionalDependencies/getLocalAssetUri.ts @@ -1,3 +1,5 @@ +import { Platform } from 'react-native'; + let MediaLibrary; try { @@ -12,10 +14,14 @@ if (!MediaLibrary) { ); } +// TODO: The API is different for Expo and RN CLI. We should unify it. export const getLocalAssetUri = async (assetId: string): Promise => { try { - const { localUri } = await MediaLibrary.getAssetInfoAsync(assetId); - return localUri; + if (Platform.OS === 'ios') { + const { localUri } = await MediaLibrary.getAssetInfoAsync(assetId); + return localUri; + } + return null; } catch { throw new Error('getLocalAssetUri Error'); } diff --git a/package/expo-package/src/optionalDependencies/getPhotos.ts b/package/expo-package/src/optionalDependencies/getPhotos.ts index b40c74513..41ab02a34 100644 --- a/package/expo-package/src/optionalDependencies/getPhotos.ts +++ b/package/expo-package/src/optionalDependencies/getPhotos.ts @@ -1,3 +1,5 @@ +import { Platform } from 'react-native'; + let MediaLibrary; try { @@ -13,6 +15,8 @@ if (!MediaLibrary) { } import type { Asset } from 'stream-chat-react-native-core'; +import { getLocalAssetUri } from './getLocalAssetUri'; + type ReturnType = { assets: Array & { source: 'picker' }>; endCursor: string | undefined; @@ -40,16 +44,22 @@ export const getPhotos = MediaLibrary mediaType: [MediaLibrary.MediaType.photo, MediaLibrary.MediaType.video], sortBy: [MediaLibrary.SortBy.modificationTime], }); - const assets = results.assets.map((asset) => ({ - duration: asset.duration * 1000, - height: asset.height, - id: asset.id, - name: asset.filename, - source: 'picker' as const, - type: asset.mediaType, - uri: asset.uri, - width: asset.width, - })); + const assets = await Promise.all( + results.assets.map(async (asset) => { + const localUri = await getLocalAssetUri(asset.id); + return { + duration: asset.duration * 1000, + height: asset.height, + id: asset.id, + name: asset.filename, + originalUri: asset.uri, + source: 'picker' as const, + type: asset.mediaType, + uri: localUri || asset.uri, + width: asset.width, + }; + }), + ); const hasNextPage = results.hasNextPage; const endCursor = results.endCursor; diff --git a/package/native-package/src/optionalDependencies/getLocalAssetUri.ts b/package/native-package/src/optionalDependencies/getLocalAssetUri.ts index fe1803422..d7f627384 100644 --- a/package/native-package/src/optionalDependencies/getLocalAssetUri.ts +++ b/package/native-package/src/optionalDependencies/getLocalAssetUri.ts @@ -1,3 +1,5 @@ +import { Platform } from 'react-native'; + let CameraRollDependency; try { @@ -12,8 +14,11 @@ try { export const getLocalAssetUri = CameraRollDependency ? async (remoteUri: string) => { try { - const localUri = await CameraRollDependency.CameraRoll.save(remoteUri); - return localUri; + if (Platform.OS === 'ios') { + const imageData = await CameraRollDependency.CameraRoll.iosGetImageDataById(remoteUri); + return imageData?.node?.image?.filepath; + } + return remoteUri; } catch { throw new Error('getLocalAssetUri Error'); } diff --git a/package/native-package/src/optionalDependencies/getPhotos.ts b/package/native-package/src/optionalDependencies/getPhotos.ts index 6c70de68a..73dbe307a 100644 --- a/package/native-package/src/optionalDependencies/getPhotos.ts +++ b/package/native-package/src/optionalDependencies/getPhotos.ts @@ -13,6 +13,8 @@ try { import type { Asset } from 'stream-chat-react-native-core'; +import { getLocalAssetUri } from './getLocalAssetUri'; + type ReturnType = { assets: Array & { source: 'picker' }>; endCursor: string | undefined; @@ -82,15 +84,23 @@ export const getPhotos = CameraRollDependency first, include: ['fileSize', 'filename', 'imageSize', 'playableDuration'], }); - const assets = results.edges.map((edge) => ({ - ...edge.node.image, - duration: edge.node.image.playableDuration * 1000, - // since we include filename, fileSize in the query, we can safely assume it will be defined - name: edge.node.image.filename as string, - size: edge.node.image.fileSize as number, - source: 'picker' as const, - type: edge.node.type, - })); + const assets = await Promise.all( + results.edges.map(async (edge) => { + const originalUri = edge.node?.image?.uri; + const uri = getLocalAssetUri ? await getLocalAssetUri(originalUri) : originalUri; + return { + ...edge.node.image, + duration: edge.node.image.playableDuration * 1000, + // since we include filename, fileSize in the query, we can safely assume it will be defined + name: edge.node.image.filename as string, + originalUri, + size: edge.node.image.fileSize as number, + source: 'picker' as const, + type: edge.node.type, + uri, + }; + }), + ); const hasNextPage = results.page_info.has_next_page; const endCursor = results.page_info.end_cursor; return { assets, endCursor, hasNextPage, iOSLimited: !!results.limited }; diff --git a/package/src/components/AttachmentPicker/AttachmentPicker.tsx b/package/src/components/AttachmentPicker/AttachmentPicker.tsx index abc762837..13060d26c 100644 --- a/package/src/components/AttachmentPicker/AttachmentPicker.tsx +++ b/package/src/components/AttachmentPicker/AttachmentPicker.tsx @@ -237,9 +237,13 @@ export const AttachmentPicker = React.forwardRef( // `id` is available for Expo MediaLibrary while Cameraroll doesn't share id therefore we use `uri` selected: selectedImages.some((image) => - image.id ? image.id === asset.id : image.uri === asset.uri, + image.id + ? image.id === asset.id + : image.uri === asset.uri || image.originalUri === asset.uri, ) || - selectedFiles.some((file) => (file.id ? file.id === asset.id : file.uri === asset.uri)), + selectedFiles.some((file) => + file.id ? file.id === asset.id : file.uri === asset.uri || file.originalUri === asset.uri, + ), selectedFiles, selectedImages, setSelectedFiles, diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx index d89e0bfdd..24ce41cd5 100644 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx +++ b/package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Alert, ImageBackground, Platform, StyleSheet, Text, View } from 'react-native'; +import { Alert, ImageBackground, StyleSheet, Text, View } from 'react-native'; import { TouchableOpacity } from '@gorhom/bottom-sheet'; import { lookup } from 'mime-types'; @@ -10,7 +10,6 @@ import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; import { useViewport } from '../../../hooks/useViewport'; import { Recorder } from '../../../icons'; -import { getLocalAssetUri } from '../../../native'; import type { Asset, File } from '../../../types/types'; import { getDurationLabelFromDuration } from '../../../utils/utils'; @@ -49,18 +48,14 @@ const AttachmentVideo = (props: AttachmentVideoProps) => { }, } = useTheme(); - const { duration: videoDuration, uri } = asset; + const { duration: videoDuration, id: assetId, originalUri, uri } = asset; const durationLabel = getDurationLabelFromDuration(videoDuration); const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2; /* Patches video files with uri and mimetype */ - const patchVideoFile = async (files: File[]) => { - // For the case of Expo CLI where you need to fetch the file uri from file id. Here it is only done for iOS since for android the file.uri is fine. - const localAssetURI = - Platform.OS === 'ios' && asset.id && getLocalAssetUri && (await getLocalAssetUri(asset.id)); - const uri = localAssetURI || asset.uri || ''; + const patchVideoFile = (files: File[]) => { // We need a mime-type to upload a video file. const mimeType = lookup(asset.name) || 'multipart/form-data'; return [ @@ -70,18 +65,19 @@ const AttachmentVideo = (props: AttachmentVideoProps) => { id: asset.id, mimeType, name: asset.name, + originalUri, size: asset.size, uri, }, ]; }; - const updateSelectedFiles = async () => { + const updateSelectedFiles = () => { if (numberOfUploads >= maxNumberOfFiles) { Alert.alert(t('Maximum number of files reached')); return; } - const files = await patchVideoFile(selectedFiles); + const files = patchVideoFile(selectedFiles); setSelectedFiles(files); }; @@ -89,7 +85,9 @@ const AttachmentVideo = (props: AttachmentVideoProps) => { if (selected) { setSelectedFiles((files) => // `id` is available for Expo MediaLibrary while Cameraroll doesn't share id therefore we use `uri` - files.filter((file) => (file.id ? file.id !== asset.id : file.uri !== asset.uri)), + files.filter((file) => + file.id ? file.id !== assetId : file.uri !== uri && file.originalUri !== uri, + ), ); } else { updateSelectedFiles(); @@ -99,7 +97,7 @@ const AttachmentVideo = (props: AttachmentVideoProps) => { return ( { const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2; - const { uri } = asset; + const { id: assetId, originalUri, uri } = asset; - /* Patches image files with uri */ - const patchImageFile = async (images: Asset[]) => { - // For the case of Expo CLI where you need to fetch the file uri from file id. Here it is only done for iOS since for android the file.uri is fine. - const localAssetURI = - Platform.OS === 'ios' && asset.id && getLocalAssetUri && (await getLocalAssetUri(asset.id)); - const uri = localAssetURI || asset.uri || ''; - return [ - ...images, - { - ...asset, - uri, - }, - ]; - }; - - const updateSelectedImages = async () => { + const updateSelectedImages = () => { if (numberOfUploads >= maxNumberOfFiles) { Alert.alert(t('Maximum number of files reached')); return; } - const images = await patchImageFile(selectedImages); - setSelectedImages(images); + setSelectedImages([...selectedImages, asset]); }; const onPressImage = () => { if (selected) { // `id` is available for Expo MediaLibrary while Cameraroll doesn't share id therefore we use `uri` setSelectedImages((images) => - images.filter((image) => (image.id ? image.id !== asset.id : image.uri !== asset.uri)), + images.filter((image) => + assetId ? image.id !== assetId : image.uri !== uri && originalUri !== uri, + ), ); } else { updateSelectedImages(); } }; + console.log('URIS: IMG: ', uri, originalUri); + return (