This commit is contained in:
Zhiyuan Zheng 2022-06-05 17:58:18 +02:00
parent d6768c0f6f
commit fc8fdec12f
18 changed files with 282 additions and 318 deletions

View File

@ -41,9 +41,6 @@ PODS:
- ExpoModulesCore
- ExpoHaptics (11.2.0):
- ExpoModulesCore
- ExpoImageManipulator (10.3.1):
- EXImageLoader
- ExpoModulesCore
- ExpoImagePicker (13.1.1):
- ExpoModulesCore
- ExpoKeepAwake (10.1.1):
@ -531,6 +528,15 @@ PODS:
- SDWebImageWebPCoder (~> 0.8.4)
- RNGestureHandler (2.4.2):
- React-Core
- RNImageCropPicker (0.37.3):
- React-Core
- React-RCTImage
- RNImageCropPicker/QBImagePickerController (= 0.37.3)
- TOCropViewController
- RNImageCropPicker/QBImagePickerController (0.37.3):
- React-Core
- React-RCTImage
- TOCropViewController
- RNReanimated (2.8.0):
- DoubleConversion
- FBLazyVector
@ -577,6 +583,7 @@ PODS:
- Sentry (7.11.0):
- Sentry/Core (= 7.11.0)
- Sentry/Core (7.11.0)
- TOCropViewController (2.6.1)
- Yoga (1.14.0)
DEPENDENCIES:
@ -599,7 +606,6 @@ 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`)
@ -658,6 +664,7 @@ DEPENDENCIES:
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- RNFastImage (from `../node_modules/react-native-fast-image`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- "RNSentry (from `../node_modules/@sentry/react-native`)"
@ -685,6 +692,7 @@ SPEC REPOS:
- SDWebImage
- SDWebImageWebPCoder
- Sentry
- TOCropViewController
EXTERNAL SOURCES:
boost:
@ -725,8 +733,6 @@ 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:
@ -835,6 +841,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-fast-image"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNImageCropPicker:
:path: "../node_modules/react-native-image-crop-picker"
RNReanimated:
:path: "../node_modules/react-native-reanimated"
RNScreens:
@ -869,7 +877,6 @@ SPEC CHECKSUMS:
Expo: 64d52669fa3b9342919b5b44b2b4f15f19b0cf76
ExpoCrypto: d0d0f3e20875dc450b4ec88f0fb608da5c2c6c17
ExpoHaptics: ad58ec96a25e57579c14a47c7d71f0de0de8656a
ExpoImageManipulator: b55580bbc7b10099c7707949903e7176a8542ee8
ExpoImagePicker: d9d6b4f29db437fc7796f13cee5f133f5b4b5f7c
ExpoKeepAwake: c0c494b442ecd8122974c13b93ccfb57bd408e88
ExpoLocalization: 8f619bb6eec64575cd5220bfabbd7b4e2d6f33f8
@ -938,6 +945,7 @@ SPEC CHECKSUMS:
RNCAsyncStorage: 466b9df1a14bccda91da86e0b7d9a345d78e1673
RNFastImage: 945abf54742505d790d9024d230c69b1e866bc88
RNGestureHandler: 61628a2c859172551aa2100d3e73d1e57878392f
RNImageCropPicker: 44e2807bc410741f35d4c45b6586aedfe3da39d2
RNReanimated: 64573e25e078ae6bec03b891586d50b9ec284393
RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19
RNSentry: 85f6525b5fe8d2ada065858026b338605b3c09da
@ -946,6 +954,7 @@ SPEC CHECKSUMS:
SDWebImage: 0905f1b7760fc8ac4198cae0036600d67478751e
SDWebImageWebPCoder: f93010f3f6c031e2f8fb3081ca4ee6966c539815
Sentry: 0c5cd63d714187b4a39c331c1f0eb04ba7868341
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
Yoga: 99652481fcd320aefa4a7ef90095b95acd181952
PODFILE CHECKSUM: d6d20fa7c51228cebc309aed987ed7d8f4274844

View File

@ -6,6 +6,5 @@
*/
"NSCameraUsageDescription" = "Allow tooot to capture photo or video and attach it to your toot";
"NSPhotoLibraryAddUsageDescription" = "Allow tooot to save an image to your camera roll";
"NSPhotoLibraryUsageDescription" = "Allow tooot to access your camera roll to attach photos or videos to your toot";

View File

@ -386,13 +386,17 @@
"${PODS_ROOT}/Target Support Files/Pods-tooot/Pods-tooot-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;

View File

@ -55,8 +55,6 @@
</dict>
</dict>
</dict>
<key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to capture photo or video and attach it to your toot</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSMainNibFile</key>

View File

@ -6,6 +6,5 @@
*/
"NSCameraUsageDescription" = "允许tooot拍摄图片或视频以添加嘟文附件";
"NSPhotoLibraryAddUsageDescription" = "允许tooot保存图片至相册";
"NSPhotoLibraryUsageDescription" = "允许tooot读取相册图片或视频以添加嘟文附件";

View File

@ -55,7 +55,6 @@
"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",
@ -84,6 +83,7 @@
"react-native-flash-message": "0.2.1",
"react-native-gesture-handler": "2.4.2",
"react-native-htmlview": "0.16.0",
"react-native-image-crop-picker": "^0.37.3",
"react-native-image-keyboard": "^2.2.0",
"react-native-pager-view": "5.4.11",
"react-native-reanimated": "2.8.0",

7
src/@types/app.d.ts vendored
View File

@ -13,11 +13,4 @@ declare namespace App {
| 'Conversations'
| 'Bookmarks'
| 'Favourites'
interface IImageInfo {
uri: string
width: number
height: number
type?: 'image' | 'video'
}
}

View File

@ -178,8 +178,49 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
}
let text: string | undefined = undefined
let images: { type: string; uri: string }[] = []
let video: { type: string; uri: string } | undefined = undefined
let media: { path: string; mime: string }[] = []
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg']
const filterMedia = ({ path, mime }: { path: string; mime: string }) => {
if (mime.startsWith('image/')) {
if (!typesImage.includes(mime.split('/')[1])) {
console.warn('Image type not supported:', mime.split('/')[1])
displayMessage({
message: t('shareError.imageNotSupported', {
type: mime.split('/')[1]
}),
type: 'error',
theme
})
return
}
media.push({ path, mime })
} else if (mime.startsWith('video/')) {
if (!typesVideo.includes(mime.split('/')[1])) {
console.warn('Video type not supported:', mime.split('/')[1])
displayMessage({
message: t('shareError.videoNotSupported', {
type: mime.split('/')[1]
}),
type: 'error',
theme
})
return
}
media.push({ path, mime })
} else {
if (typesImage.includes(path.split('.').pop() || '')) {
media.push({ path: path, mime: 'image/jpg' })
return
}
if (typesVideo.includes(path.split('.').pop() || '')) {
media.push({ path: path, mime: 'video/mp4' })
return
}
text = !text ? path : text.concat(text, `\n${path}`)
}
}
switch (Platform.OS) {
case 'ios':
@ -187,55 +228,11 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
return
}
item.data.forEach(d => {
if (typeof d === 'string') return
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
const typesVideo = ['mp4', 'm4v', 'mov', 'webm']
const { mimeType, data } = d
if (mimeType.startsWith('image/')) {
if (!typesImage.includes(mimeType.split('/')[1])) {
console.warn(
'Image type not supported:',
mimeType.split('/')[1]
)
displayMessage({
message: t('shareError.imageNotSupported', {
type: mimeType.split('/')[1]
}),
type: 'error',
theme
})
return
}
images.push({ type: mimeType.split('/')[1], uri: data })
} else if (mimeType.startsWith('video/')) {
if (!typesVideo.includes(mimeType.split('/')[1])) {
console.warn(
'Video type not supported:',
mimeType.split('/')[1]
)
displayMessage({
message: t('shareError.videoNotSupported', {
type: mimeType.split('/')[1]
}),
type: 'error',
theme
})
return
}
video = { type: mimeType.split('/')[1], uri: data }
} else {
if (typesImage.includes(data.split('.').pop() || '')) {
images.push({ type: data.split('.').pop()!, uri: data })
return
}
if (typesVideo.includes(data.split('.').pop() || '')) {
video = { type: data.split('.').pop()!, uri: data }
return
}
text = !text ? data : text.concat(text, `\n${data}`)
for (const d of item.data) {
if (typeof d !== 'string') {
filterMedia({ path: d.data, mime: d.mimeType })
}
})
}
break
case 'android':
if (!item.mimeType) {
@ -247,65 +244,16 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
} else {
tempData = item.data
}
tempData.forEach(d => {
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg']
if (item.mimeType!.startsWith('image/')) {
if (!typesImage.includes(item.mimeType.split('/')[1])) {
console.warn(
'Image type not supported:',
item.mimeType.split('/')[1]
)
displayMessage({
message: t('shareError.imageNotSupported', {
type: item.mimeType.split('/')[1]
}),
type: 'error',
theme
})
return
}
images.push({ type: item.mimeType.split('/')[1], uri: d })
} else if (item.mimeType.startsWith('video/')) {
if (!typesVideo.includes(item.mimeType.split('/')[1])) {
console.warn(
'Video type not supported:',
item.mimeType.split('/')[1]
)
displayMessage({
message: t('shareError.videoNotSupported', {
type: item.mimeType.split('/')[1]
}),
type: 'error',
theme
})
return
}
video = { type: item.mimeType.split('/')[1], uri: d }
} else {
if (typesImage.includes(d.split('.').pop() || '')) {
images.push({ type: d.split('.').pop()!, uri: d })
return
}
if (typesVideo.includes(d.split('.').pop() || '')) {
video = { type: d.split('.').pop()!, uri: d }
return
}
text = !text ? d : text.concat(text, `\n${d}`)
}
})
for (const d of item.data) {
filterMedia({ path: d, mime: item.mimeType })
}
break
}
if (!text && (!images || !images.length) && !video) {
if (!text && !media.length) {
return
} else {
navigationRef.navigate('Screen-Compose', {
type: 'share',
text,
images,
video
})
navigationRef.navigate('Screen-Compose', { type: 'share', text, media })
}
},
[instanceActive]

View File

@ -1,17 +1,20 @@
import analytics from '@components/analytics'
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import * as ImageManipulator from 'expo-image-manipulator'
import * as ImagePicker from 'expo-image-picker'
import {
ImageInfo,
UIImagePickerPresentationStyle
} from 'expo-image-picker/build/ImagePicker.types'
import { store } from '@root/store'
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
import * as ExpoImagePicker from 'expo-image-picker'
import i18next from 'i18next'
import { Alert, Linking, Platform } from 'react-native'
import { Alert, Linking } from 'react-native'
import ImagePicker, {
Image,
ImageOrVideo
} from 'react-native-image-crop-picker'
export interface Props {
mediaTypes?: ImagePicker.MediaTypeOptions
resize?: { width?: number; height?: number } // Resize mode contain
mediaType?: 'photo' | 'video'
resize?: { width?: number; height?: number }
maximum?: number
indicateMaximum?: boolean
showActionSheetWithOptions: (
options: ActionSheetOptions,
callback: (i?: number | undefined) => void | Promise<void>
@ -19,134 +22,157 @@ export interface Props {
}
const mediaSelector = async ({
mediaTypes = ImagePicker.MediaTypeOptions.All,
mediaType,
resize,
maximum,
indicateMaximum = false,
showActionSheetWithOptions
}: Props): Promise<ImageInfo> => {
return new Promise((resolve, reject) => {
const resolveResult = async (result: ImageInfo) => {
if (resize && result.type === 'image') {
let newResult: ImageManipulator.ImageResult
if (resize.width && resize.height) {
if (resize.width / resize.height > result.width / result.height) {
newResult = await ImageManipulator.manipulateAsync(result.uri, [
{ resize: { width: resize.width } }
])
} else {
newResult = await ImageManipulator.manipulateAsync(result.uri, [
{ resize: { height: resize.height } }
])
}: Props): Promise<ImageOrVideo[]> => {
const checkLibraryPermission = async (): Promise<boolean> => {
const { status } =
await ExpoImagePicker.requestMediaLibraryPermissionsAsync()
if (status !== 'granted') {
Alert.alert(
i18next.t('componentMediaSelector:library.alert.title'),
i18next.t('componentMediaSelector:library.alert.message'),
[
{
text: i18next.t('common:buttons.cancel'),
style: 'cancel',
onPress: () =>
analytics('mediaSelector_nopermission', {
action: 'cancel'
})
},
{
text: i18next.t(
'componentMediaSelector:library.alert.buttons.settings'
),
style: 'default',
onPress: () => {
analytics('mediaSelector_nopermission', {
action: 'settings'
})
Linking.openURL('app-settings:')
}
}
} else {
newResult = await ImageManipulator.manipulateAsync(result.uri, [
{ resize }
])
}
resolve({ ...newResult, cancelled: false })
]
)
return false
} else {
return true
}
}
const _maximum =
maximum ||
getInstanceConfigurationStatusMaxAttachments(store.getState()) ||
4
const options = () => {
switch (mediaType) {
case 'photo':
return [
i18next.t(
'componentMediaSelector:options.image',
indicateMaximum ? { context: 'max', max: _maximum } : undefined
),
i18next.t('common:buttons.cancel')
]
case 'video':
return [
i18next.t(
'componentMediaSelector:options.video',
indicateMaximum ? { context: 'max', max: 1 } : undefined
),
i18next.t('common:buttons.cancel')
]
default:
return [
i18next.t(
'componentMediaSelector:options.image',
indicateMaximum ? { context: 'max', max: _maximum } : undefined
),
i18next.t(
'componentMediaSelector:options.video',
indicateMaximum ? { context: 'max', max: 1 } : undefined
),
i18next.t('common:buttons.cancel')
]
}
}
return new Promise((resolve, reject) => {
const selectImage = async () => {
const images = await ImagePicker.openPicker({
mediaType: 'photo',
includeExif: false,
multiple: true,
minFiles: 1,
maxFiles: _maximum
}).catch(() => {})
if (!images) {
return reject()
}
if (!resize) {
return resolve(images)
} else {
resolve(result)
const croppedImages: Image[] = []
for (const image of images) {
const croppedImage = await ImagePicker.openCropper({
mediaType: 'photo',
path: image.path,
width: resize.width,
height: resize.height
}).catch(() => {})
croppedImage && croppedImages.push(croppedImage)
}
return resolve(croppedImages)
}
}
const selectVideo = async () => {
const video = await ImagePicker.openPicker({
mediaType: 'video',
includeExif: false
}).catch(() => {})
if (video) {
return resolve([video])
} else {
return reject()
}
}
showActionSheetWithOptions(
{
title: i18next.t('componentMediaSelector:title'),
options: [
i18next.t('componentMediaSelector:options.library'),
i18next.t('componentMediaSelector:options.photo'),
i18next.t('componentMediaSelector:options.cancel')
],
cancelButtonIndex: 2
options: options(),
cancelButtonIndex: mediaType ? 1 : 2
},
async buttonIndex => {
if (buttonIndex === 0) {
const { status } =
await ImagePicker.requestMediaLibraryPermissionsAsync()
if (status !== 'granted') {
Alert.alert(
i18next.t('componentMediaSelector:library.alert.title'),
i18next.t('componentMediaSelector:library.alert.message'),
[
{
text: i18next.t(
'componentMediaSelector:library.alert.buttons.cancel'
),
style: 'cancel',
onPress: () =>
analytics('mediaSelector_nopermission', {
action: 'cancel'
})
},
{
text: i18next.t(
'componentMediaSelector:library.alert.buttons.settings'
),
style: 'default',
onPress: () => {
analytics('mediaSelector_nopermission', {
action: 'settings'
})
Linking.openURL('app-settings:')
}
}
]
)
} else {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes,
exif: false,
presentationStyle:
Platform.OS === 'ios' && parseInt(Platform.Version) < 13
? UIImagePickerPresentationStyle.FULL_SCREEN
: UIImagePickerPresentationStyle.AUTOMATIC
})
if (!(await checkLibraryPermission())) {
return reject()
}
if (!result.cancelled) {
await resolveResult(result)
switch (mediaType) {
case 'photo':
if (buttonIndex === 0) {
await selectImage()
}
}
} else if (buttonIndex === 1) {
const { status } = await ImagePicker.requestCameraPermissionsAsync()
if (status !== 'granted') {
Alert.alert(
i18next.t('componentMediaSelector:photo.alert.title'),
i18next.t('componentMediaSelector:photo.alert.message'),
[
{
text: i18next.t(
'componentMediaSelector:photo.alert.buttons.cancel'
),
style: 'cancel',
onPress: () => {
analytics('compose_addattachment_camera_nopermission', {
action: 'cancel'
})
}
},
{
text: i18next.t(
'componentMediaSelector:photo.alert.buttons.settings'
),
style: 'default',
onPress: () => {
analytics('compose_addattachment_camera_nopermission', {
action: 'settings'
})
Linking.openURL('app-settings:')
}
}
]
)
} else {
const result = await ImagePicker.launchCameraAsync({
mediaTypes,
exif: false
})
if (!result.cancelled) {
await resolveResult(result)
break
case 'video':
if (buttonIndex === 0) {
await selectVideo()
}
}
break
default:
if (buttonIndex === 0) {
await selectImage()
} else if (buttonIndex === 1) {
await selectVideo()
}
break
}
}
)

View File

@ -1,27 +1,17 @@
{
"title": "Select media source",
"options": {
"library": "Upload from library",
"photo": "Take a photo",
"cancel": "$t(common:buttons.cancel)"
"image": "Upload photos",
"image_max": "Upload photos (max {{max}})",
"video": "Upload video",
"video_max": "Upload video (max {{max}})"
},
"library": {
"alert": {
"title": "No permission",
"message": "Require photo library read permission to upload",
"buttons": {
"settings": "Update setting",
"cancel": "$t(common:buttons.cancel)"
}
}
},
"photo": {
"alert": {
"title": "No permission",
"message": "Require camera usage permission to upload",
"buttons": {
"settings": "Update setting",
"cancel": "$t(common:buttons.cancel)"
"settings": "Update setting"
}
}
}

View File

@ -136,18 +136,6 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
])
useEffect(() => {
const uploadImage = async ({
type,
uri
}: {
type: 'image' | 'video'
uri: string
}) => {
await uploadAttachment({
composeDispatch,
imageInfo: { type, uri, width: 100, height: 100 }
})
}
switch (params?.type) {
case 'share':
if (params.text) {
@ -158,12 +146,13 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
disableDebounce: true
})
}
if (params.images?.length) {
params.images.forEach(image => {
uploadImage({ type: 'image', uri: image.uri })
})
} else if (params.video) {
uploadImage({ type: 'video', uri: params.video.uri })
if (params.media?.length) {
for (const m of params.media) {
uploadAttachment({
composeDispatch,
media: { ...m, width: 100, height: 100 }
})
}
}
break
case 'edit':

View File

@ -1,5 +1,4 @@
import * as Crypto from 'expo-crypto'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
import * as VideoThumbnails from 'expo-video-thumbnails'
import { Dispatch } from 'react'
import { Alert } from 'react-native'
@ -8,6 +7,7 @@ import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import i18next from 'i18next'
import apiInstance from '@api/instance'
import mediaSelector from '@components/mediaSelector'
import { ImageOrVideo } from 'react-native-image-crop-picker'
export interface Props {
composeDispatch: Dispatch<ComposeAction>
@ -19,39 +19,38 @@ export interface Props {
export const uploadAttachment = async ({
composeDispatch,
imageInfo
media
}: {
composeDispatch: Dispatch<ComposeAction>
imageInfo: Pick<ImageInfo, 'type' | 'uri' | 'width' | 'height'>
media: Pick<ImageOrVideo, 'path' | 'mime' | 'width' | 'height'>
}) => {
const hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
imageInfo.uri + Math.random()
media.path + Math.random()
)
let attachmentType: string
switch (imageInfo.type) {
switch (media.mime.split('/')[0]) {
case 'image':
console.log('uri', imageInfo.uri)
attachmentType = `image/${imageInfo.uri.split('.')[1]}`
attachmentType = `image/${media.path.split('.')[1]}`
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: { ...imageInfo, local_thumbnail: imageInfo.uri, hash },
local: { ...media, type: 'image', local_thumbnail: media.path, hash },
uploading: true
}
})
break
case 'video':
attachmentType = `video/${imageInfo.uri.split('.')[1]}`
VideoThumbnails.getThumbnailAsync(imageInfo.uri)
attachmentType = `video/${media.path.split('.')[1]}`
VideoThumbnails.getThumbnailAsync(media.path)
.then(({ uri, width, height }) =>
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: {
...imageInfo,
...media,
type: 'video',
local_thumbnail: uri,
hash,
width,
@ -65,7 +64,7 @@ export const uploadAttachment = async ({
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: { ...imageInfo, hash },
local: { ...media, type: 'video', hash },
uploading: true
}
})
@ -76,7 +75,7 @@ export const uploadAttachment = async ({
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: { ...imageInfo, hash },
local: { ...media, type: 'unknown', hash },
uploading: true
}
})
@ -106,14 +105,14 @@ export const uploadAttachment = async ({
const formData = new FormData()
formData.append('file', {
// @ts-ignore
uri: imageInfo.uri,
uri: `file://${media.path}`,
name: attachmentType,
type: attachmentType
})
} as any)
return apiInstance<Mastodon.Attachment>({
method: 'post',
version: 'v2',
url: 'media',
body: formData
})
@ -121,7 +120,7 @@ export const uploadAttachment = async ({
if (res.body.id) {
composeDispatch({
type: 'attachment/upload/end',
payload: { remote: res.body, local: imageInfo }
payload: { remote: res.body, local: media }
})
} else {
uploadFailed()
@ -136,8 +135,13 @@ const chooseAndUploadAttachment = async ({
composeDispatch,
showActionSheetWithOptions
}: Props): Promise<any> => {
const result = await mediaSelector({ showActionSheetWithOptions })
await uploadAttachment({ composeDispatch, imageInfo: result })
const result = await mediaSelector({
indicateMaximum: true,
showActionSheetWithOptions
})
for (const media of result) {
uploadAttachment({ composeDispatch, media })
}
}
export default chooseAndUploadAttachment

View File

@ -83,9 +83,9 @@ const ComposeTextInput: React.FC = () => {
if (nativeEvent.linkUri) {
uploadAttachment({
composeDispatch,
imageInfo: {
uri: nativeEvent.linkUri,
type: 'image',
media: {
path: nativeEvent.linkUri,
mime: 'image/gif',
width: 100,
height: 100
}

View File

@ -1,6 +1,12 @@
import { ImageOrVideo } from 'react-native-image-crop-picker'
export type ExtendedAttachment = {
remote?: Mastodon.Attachment
local?: App.IImageInfo & { local_thumbnail?: string; hash?: string }
local?: Pick<ImageOrVideo, 'path' | 'width' | 'height'> & {
type: 'image' | 'video' | 'unknown'
local_thumbnail?: string
hash?: string
}
uploading?: boolean
}

View File

@ -3,7 +3,6 @@ import { MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import { useTheme } from '@utils/styles/ThemeManager'
import * as ImagePicker from 'expo-image-picker'
import React, { RefObject } from 'react'
import { useTranslation } from 'react-i18next'
import FlashMessage from 'react-native-flash-message'
@ -30,9 +29,13 @@ const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
iconBack='ChevronRight'
onPress={async () => {
const image = await mediaSelector({
mediaType: 'photo',
maximum: 1,
showActionSheetWithOptions,
mediaTypes: ImagePicker.MediaTypeOptions.Images,
resize: { width: 400, height: 400 }
resize:
type === 'avatar'
? { width: 400, height: 400 }
: { width: 1500, height: 500 }
})
mutation.mutate({
theme,
@ -43,7 +46,7 @@ const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
failed: true
},
type,
data: image.uri
data: image[0].path
})
}}
/>

View File

@ -52,8 +52,7 @@ export type RootStackParamList = {
| {
type: 'share'
text?: string
images?: { type: string; uri: string }[]
video?: { type: string; uri: string }
media?: { path: string; mime: string }[]
}
| undefined
'Screen-ImagesViewer': {

View File

@ -71,11 +71,10 @@ const mutationFunction = async ({ type, data }: MutationVarsProfile) => {
})
} else if (type === 'avatar' || type === 'header') {
formData.append(type, {
// @ts-ignore
uri: data,
uri: `file://${data}`,
name: 'image/jpeg',
type: 'image/jpeg'
})
} as any)
} else {
// @ts-ignore
formData.append(type, data)

View File

@ -4421,13 +4421,6 @@ 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"
@ -7517,6 +7510,11 @@ react-native-htmlview@0.16.0:
entities "^1.1.1"
htmlparser2-without-node-native "^3.9.2"
react-native-image-crop-picker@^0.37.3:
version "0.37.3"
resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.37.3.tgz#f260e40b6a6ba8e98f4db3dde25a8f09e0936385"
integrity sha512-ih+0pWWRUNEFQyaHwGbH9rqJNOb7EBYMwKJhTY0VmsKIA9E+usfwMmQXAFIfOnee7fTn0A2vOXkBCPQZwyvnQw==
react-native-image-keyboard@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/react-native-image-keyboard/-/react-native-image-keyboard-2.2.0.tgz#dc3f90aaaac20a79315015a330e62e85547e0674"