diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aa187b13..b00f4cf7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,6 +4,8 @@ PODS: - DoubleConversion (1.1.6) - EASClient (0.2.1): - ExpoModulesCore + - EXApplication (4.1.0): + - ExpoModulesCore - EXAV (11.2.3): - ExpoModulesCore - React-runtimeexecutor @@ -23,6 +25,8 @@ PODS: - EXFirebaseCore (5.0.0): - ExpoModulesCore - Firebase/Core (= 8.14.0) + - EXFont (10.1.0): + - ExpoModulesCore - EXImageLoader (3.2.0): - ExpoModulesCore - React-Core @@ -37,8 +41,13 @@ PODS: - ExpoModulesCore - ExpoHaptics (11.2.0): - ExpoModulesCore + - ExpoImageManipulator (10.3.1): + - EXImageLoader + - ExpoModulesCore - ExpoImagePicker (13.1.1): - ExpoModulesCore + - ExpoKeepAwake (10.1.1): + - ExpoModulesCore - ExpoLocalization (13.0.0): - ExpoModulesCore - ExpoModulesCore (0.9.2): @@ -590,6 +599,7 @@ DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - EASClient (from `../node_modules/expo-eas-client/ios`) + - EXApplication (from `../node_modules/expo-application/ios`) - EXAV (from `../node_modules/expo-av/ios`) - EXConstants (from `../node_modules/expo-constants/ios`) - EXDevice (from `../node_modules/expo-device/ios`) @@ -597,6 +607,7 @@ DEPENDENCIES: - EXFileSystem (from `../node_modules/expo-file-system/ios`) - EXFirebaseAnalytics (from `../node_modules/expo-firebase-analytics/ios`) - EXFirebaseCore (from `../node_modules/expo-firebase-core/ios`) + - EXFont (from `../node_modules/expo-font/ios`) - EXImageLoader (from `../node_modules/expo-image-loader/ios`) - EXJSONUtils (from `../node_modules/expo-json-utils/ios`) - EXManifests (from `../node_modules/expo-manifests/ios`) @@ -604,7 +615,9 @@ DEPENDENCIES: - Expo (from `../node_modules/expo/ios`) - ExpoCrypto (from `../node_modules/expo-crypto/ios`) - ExpoHaptics (from `../node_modules/expo-haptics/ios`) + - ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`) - ExpoImagePicker (from `../node_modules/expo-image-picker/ios`) + - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) - ExpoLocalization (from `../node_modules/expo-localization/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core/ios`) - ExpoRandom (from `../node_modules/expo-random/ios`) @@ -701,6 +714,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EASClient: :path: "../node_modules/expo-eas-client/ios" + EXApplication: + :path: "../node_modules/expo-application/ios" EXAV: :path: "../node_modules/expo-av/ios" EXConstants: @@ -715,6 +730,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-firebase-analytics/ios" EXFirebaseCore: :path: "../node_modules/expo-firebase-core/ios" + EXFont: + :path: "../node_modules/expo-font/ios" EXImageLoader: :path: "../node_modules/expo-image-loader/ios" EXJSONUtils: @@ -729,8 +746,12 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-crypto/ios" ExpoHaptics: :path: "../node_modules/expo-haptics/ios" + ExpoImageManipulator: + :path: "../node_modules/expo-image-manipulator/ios" ExpoImagePicker: :path: "../node_modules/expo-image-picker/ios" + ExpoKeepAwake: + :path: "../node_modules/expo-keep-awake/ios" ExpoLocalization: :path: "../node_modules/expo-localization/ios" ExpoModulesCore: @@ -859,6 +880,7 @@ SPEC CHECKSUMS: boost: a7c83b31436843459a1961bfd74b96033dc77234 DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662 EASClient: 93565f4d024559b75eac62bc7d50acaa354614f6 + EXApplication: d6562af1204162e0ac46d341a7d4e5dc720b33de EXAV: 88f61c5af8415715b7ee51f084c1020235b85c56 EXConstants: fdbe52259365b6a6faaa5e99a3b82cfa6bc2eb61 EXDevice: 0115b360059ccd32c1701744e374e3259ffbdd3c @@ -866,6 +888,7 @@ SPEC CHECKSUMS: EXFileSystem: 2aa2d9289f84bca9532b9ccbd81504fa31eb1ded EXFirebaseAnalytics: aeefc63f92277313c3ee86da6a7ecf892f345ed1 EXFirebaseCore: bdfa87df74fa1b74a6b38957561456aabad28a4f + EXFont: 04235cc22e6fef86028feb67db452978dc6f240f EXImageLoader: b88e053d760f85a82405b1db2de4abf11978fc9f EXJSONUtils: 2a74b8f40f1523cc3f92af99c91aa78201737a77 EXManifests: 0c6134b7b6f3236a93a778c3f44ba1cfb3f9fa3d @@ -873,7 +896,9 @@ SPEC CHECKSUMS: Expo: b9fff0a1eac0f424fc68ea49b4347fb308e52e17 ExpoCrypto: d0d0f3e20875dc450b4ec88f0fb608da5c2c6c17 ExpoHaptics: ad58ec96a25e57579c14a47c7d71f0de0de8656a + ExpoImageManipulator: b55580bbc7b10099c7707949903e7176a8542ee8 ExpoImagePicker: d9d6b4f29db437fc7796f13cee5f133f5b4b5f7c + ExpoKeepAwake: c0c494b442ecd8122974c13b93ccfb57bd408e88 ExpoLocalization: 8f619bb6eec64575cd5220bfabbd7b4e2d6f33f8 ExpoModulesCore: e4278a668e8c13c0269ed8b8a4200989deea2973 ExpoRandom: 14df0976aa363a71a730ceb7655250f3047c0e42 diff --git a/package.json b/package.json index 28035f02..53b9592a 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "expo-file-system": "14.0.0", "expo-firebase-analytics": "7.0.0", "expo-haptics": "11.2.0", + "expo-image-manipulator": "^10.3.1", "expo-image-picker": "13.1.1", "expo-linking": "3.1.0", "expo-localization": "13.0.0", @@ -153,4 +154,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/Screens.tsx b/src/Screens.tsx index ae85cbca..b5e1c889 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -178,11 +178,11 @@ const Screens: React.FC = ({ localCorrupt }) => { } let text: string | undefined = undefined - let media: { path: string; mime: string }[] = [] + let media: { uri: string; mime: string }[] = [] const typesImage = ['png', 'jpg', 'jpeg', 'gif'] const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg'] - const filterMedia = ({ path, mime }: { path: string; mime: string }) => { + const filterMedia = ({ uri, mime }: { uri: string; mime: string }) => { if (mime.startsWith('image/')) { if (!typesImage.includes(mime.split('/')[1])) { console.warn('Image type not supported:', mime.split('/')[1]) @@ -195,7 +195,7 @@ const Screens: React.FC = ({ localCorrupt }) => { }) return } - media.push({ path, mime }) + media.push({ uri, mime }) } else if (mime.startsWith('video/')) { if (!typesVideo.includes(mime.split('/')[1])) { console.warn('Video type not supported:', mime.split('/')[1]) @@ -208,17 +208,17 @@ const Screens: React.FC = ({ localCorrupt }) => { }) return } - media.push({ path, mime }) + media.push({ uri, mime }) } else { - if (typesImage.includes(path.split('.').pop() || '')) { - media.push({ path: path, mime: 'image/jpg' }) + if (typesImage.includes(uri.split('.').pop() || '')) { + media.push({ uri, mime: 'image/jpg' }) return } - if (typesVideo.includes(path.split('.').pop() || '')) { - media.push({ path: path, mime: 'video/mp4' }) + if (typesVideo.includes(uri.split('.').pop() || '')) { + media.push({ uri, mime: 'video/mp4' }) return } - text = !text ? path : text.concat(text, `\n${path}`) + text = !text ? uri : text.concat(text, `\n${uri}`) } } @@ -230,7 +230,7 @@ const Screens: React.FC = ({ localCorrupt }) => { for (const d of item.data) { if (typeof d !== 'string') { - filterMedia({ path: d.data, mime: d.mimeType }) + filterMedia({ uri: d.data, mime: d.mimeType }) } } break @@ -245,7 +245,7 @@ const Screens: React.FC = ({ localCorrupt }) => { tempData = item.data } for (const d of item.data) { - filterMedia({ path: d, mime: item.mimeType }) + filterMedia({ uri: d, mime: item.mimeType }) } break } diff --git a/src/components/mediaSelector.ts b/src/components/mediaSelector.ts index ffc92b9f..4d18cb78 100644 --- a/src/components/mediaSelector.ts +++ b/src/components/mediaSelector.ts @@ -2,9 +2,10 @@ import analytics from '@components/analytics' import { ActionSheetOptions } from '@expo/react-native-action-sheet' import { store } from '@root/store' import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice' +import { manipulateAsync, SaveFormat } from 'expo-image-manipulator' import * as ExpoImagePicker from 'expo-image-picker' import i18next from 'i18next' -import { Alert, Linking } from 'react-native' +import { Alert, Linking, Platform } from 'react-native' import ImagePicker, { Image, ImageOrVideo @@ -27,7 +28,7 @@ const mediaSelector = async ({ maximum, indicateMaximum = false, showActionSheetWithOptions -}: Props): Promise => { +}: Props): Promise<({ uri: string } & Omit)[]> => { const checkLibraryPermission = async (): Promise => { const { status } = await ExpoImagePicker.requestMediaLibraryPermissionsAsync() @@ -110,17 +111,40 @@ const mediaSelector = async ({ multiple: true, minFiles: 1, maxFiles: _maximum, - loadingLabelText: '', - compressImageMaxWidth: 4096, - compressImageMaxHeight: 4096 + smartAlbums: ['UserLibrary'], + writeTempFile: false, + loadingLabelText: '' }).catch(() => {}) if (!images) { return reject() } + // react-native-image-crop-picker may return HEIC as JPG that causes upload failure + if (Platform.OS === 'ios') { + for (const [index, image] of images.entries()) { + if (image.mime === 'image/heic') { + const converted = await manipulateAsync(image.sourceURL!, [], { + base64: false, + compress: 0.8, + format: SaveFormat.JPEG + }) + images[index] = { + ...images[index], + sourceURL: converted.uri, + mime: 'image/jpeg' + } + } + } + } + if (!resize) { - return resolve(images) + return resolve( + images.map(image => ({ + ...image, + uri: image.sourceURL || `file://${image.path}` + })) + ) } else { const croppedImages: Image[] = [] for (const image of images) { @@ -135,7 +159,12 @@ const mediaSelector = async ({ }).catch(() => {}) croppedImage && croppedImages.push(croppedImage) } - return resolve(croppedImages) + return resolve( + croppedImages.map(image => ({ + ...image, + uri: `file://${image.path}` + })) + ) } } const selectVideo = async () => { @@ -146,7 +175,9 @@ const mediaSelector = async ({ }).catch(() => {}) if (video) { - return resolve([video]) + return resolve([ + { ...video, uri: video.sourceURL || `file://${video.path}` } + ]) } else { return reject() } diff --git a/src/screens/Compose/EditAttachment/Image.tsx b/src/screens/Compose/EditAttachment/Image.tsx index 7dd754d2..bebc9a15 100644 --- a/src/screens/Compose/EditAttachment/Image.tsx +++ b/src/screens/Compose/EditAttachment/Image.tsx @@ -128,8 +128,8 @@ const ComposeEditAttachmentImage: React.FC = ({ index }) => { height: imageDimensionis.height }} source={{ - uri: theAttachmentLocal?.path - ? `file://${theAttachmentLocal?.path}` + uri: theAttachmentLocal?.uri + ? theAttachmentLocal.uri : theAttachmentRemote?.preview_url }} /> diff --git a/src/screens/Compose/EditAttachment/Root.tsx b/src/screens/Compose/EditAttachment/Root.tsx index eeff4604..bff60bd9 100644 --- a/src/screens/Compose/EditAttachment/Root.tsx +++ b/src/screens/Compose/EditAttachment/Root.tsx @@ -34,7 +34,7 @@ const ComposeEditAttachmentRoot: React.FC = ({ index }) => { video={ video.local ? ({ - url: `file://${video.local.path}`, + url: video.local.uri, preview_url: video.local.local_thumbnail, blurhash: video.remote?.blurhash } as Mastodon.AttachmentVideo) diff --git a/src/screens/Compose/Root/Footer/addAttachment.ts b/src/screens/Compose/Root/Footer/addAttachment.ts index 268efa3c..63e11904 100644 --- a/src/screens/Compose/Root/Footer/addAttachment.ts +++ b/src/screens/Compose/Root/Footer/addAttachment.ts @@ -22,28 +22,25 @@ export const uploadAttachment = async ({ media }: { composeDispatch: Dispatch - media: Pick + media: { uri: string } & Pick }) => { const hash = await Crypto.digestStringAsync( Crypto.CryptoDigestAlgorithm.SHA256, - media.path + Math.random() + media.uri + Math.random() ) - let attachmentType: string switch (media.mime.split('/')[0]) { case 'image': - attachmentType = `image/${media.path.split('.')[1]}` composeDispatch({ type: 'attachment/upload/start', payload: { - local: { ...media, type: 'image', local_thumbnail: media.path, hash }, + local: { ...media, type: 'image', local_thumbnail: media.uri, hash }, uploading: true } }) break case 'video': - attachmentType = `video/${media.path.split('.')[1]}` - VideoThumbnails.getThumbnailAsync(media.path) + VideoThumbnails.getThumbnailAsync(media.uri) .then(({ uri, width, height }) => composeDispatch({ type: 'attachment/upload/start', @@ -71,7 +68,6 @@ export const uploadAttachment = async ({ ) break default: - attachmentType = 'unknown' composeDispatch({ type: 'attachment/upload/start', payload: { @@ -105,9 +101,9 @@ export const uploadAttachment = async ({ const formData = new FormData() formData.append('file', { - uri: `file://${media.path}`, - name: attachmentType, - type: attachmentType + uri: media.uri, + name: media.uri.match(new RegExp(/.*\/(.*)/))?.[1] || 'file.jpg', + type: media.mime } as any) return apiInstance({ diff --git a/src/screens/Compose/Root/Header/TextInput.tsx b/src/screens/Compose/Root/Header/TextInput.tsx index ff272a98..e148cbca 100644 --- a/src/screens/Compose/Root/Header/TextInput.tsx +++ b/src/screens/Compose/Root/Header/TextInput.tsx @@ -90,7 +90,7 @@ const ComposeTextInput: React.FC = () => { uploadAttachment({ composeDispatch, media: { - path: file.uri, + uri: file.uri, mime: file.type, width: 100, height: 100 diff --git a/src/screens/Compose/utils/reducer.ts b/src/screens/Compose/utils/reducer.ts index d727079b..0480393b 100644 --- a/src/screens/Compose/utils/reducer.ts +++ b/src/screens/Compose/utils/reducer.ts @@ -58,7 +58,7 @@ const composeReducer = ( attachments: { ...state.attachments, uploads: state.attachments.uploads.map(upload => - upload.local?.path === action.payload.local?.path + upload.local?.uri === action.payload.local?.uri ? { ...upload, remote: action.payload.remote, uploading: false } : upload ) diff --git a/src/screens/Compose/utils/types.d.ts b/src/screens/Compose/utils/types.d.ts index 6a109fcf..586be113 100644 --- a/src/screens/Compose/utils/types.d.ts +++ b/src/screens/Compose/utils/types.d.ts @@ -2,11 +2,11 @@ import { ImageOrVideo } from 'react-native-image-crop-picker' export type ExtendedAttachment = { remote?: Mastodon.Attachment - local?: Pick & { - type: 'image' | 'video' | 'unknown' - local_thumbnail?: string - hash?: string - } + local?: { uri: string } & Pick & { + type: 'image' | 'video' | 'unknown' + local_thumbnail?: string + hash?: string + } uploading?: boolean } @@ -123,7 +123,7 @@ export type ComposeAction = type: 'attachment/upload/end' payload: { remote: Mastodon.Attachment - local: Pick + local: { uri: string } & Pick } } | { diff --git a/src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx b/src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx index 7e9dfc7b..23d83b9e 100644 --- a/src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx +++ b/src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx @@ -46,7 +46,7 @@ const ProfileAvatarHeader: React.FC = ({ type, messageRef }) => { failed: true }, type, - data: image[0].path + data: image[0].uri }) }} /> diff --git a/src/utils/navigation/navigators.ts b/src/utils/navigation/navigators.ts index 283ccf5d..5937edab 100644 --- a/src/utils/navigation/navigators.ts +++ b/src/utils/navigation/navigators.ts @@ -42,7 +42,7 @@ export type RootStackParamList = { | { type: 'share' text?: string - media?: { path: string; mime: string }[] + media?: { uri: string; mime: string }[] } | undefined 'Screen-ImagesViewer': { diff --git a/yarn.lock b/yarn.lock index 54a33b2c..4a445a9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4428,6 +4428,13 @@ expo-image-loader@~3.2.0: resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-3.2.0.tgz#d98b021660edef7243f7c5ec011b8d0545626d41" integrity sha512-LU3Q2prn64/HxdToDmxgMIRXS1ZvD9Q3iCxRVTZn1fPQNNDciIQFE5okaa74Ogx20DFHs90r6WoUd7w9Af1OGQ== +expo-image-manipulator@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-10.3.1.tgz#e16dd76a550c7f5d653a2a666f26429eba311a6b" + integrity sha512-D08dMD6MerxBu23DmCIhurySQih+eLP7VxKvY5mWqYz9payjDOS1cAGs3HvXPrEDusPQFQ0uIfqc+oqeMNFBIA== + dependencies: + expo-image-loader "~3.2.0" + expo-image-picker@13.1.1: version "13.1.1" resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-13.1.1.tgz#e039bf9748ccb7b89370ff2969c3ef07cc949192"