migrate media library api
This commit is contained in:
+7
-9
@@ -75,18 +75,16 @@ jest.mock('expo-camera', () => ({
|
||||
|
||||
jest.mock('expo-media-library', () => ({
|
||||
__esModule: true, // this property makes it work
|
||||
Album: {
|
||||
create: jest.fn(),
|
||||
get: jest.fn(),
|
||||
},
|
||||
Asset: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
default: jest.fn(),
|
||||
usePermissions: jest.fn(() => [true]),
|
||||
requestPermissionsAsync: jest.fn().mockResolvedValue({granted: true}),
|
||||
saveToLibraryAsync: jest.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
jest.mock('expo-media-library/legacy', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
usePermissions: jest.fn(() => [true]),
|
||||
requestPermissionsAsync: jest.fn().mockResolvedValue({granted: true}),
|
||||
saveToLibraryAsync: jest.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
jest.mock('@bsky.app/expo-guess-language', () => ({
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import {Suspense, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type ViewShotRef} from 'react-native-view-shot'
|
||||
import {
|
||||
requestPermissionsAsync,
|
||||
saveToLibraryAsync,
|
||||
} from 'expo-media-library/legacy'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {Asset, requestPermissionsAsync} from '#/lib/media/media-library'
|
||||
import {logger} from '#/logger'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -77,9 +74,8 @@ export function QrCodeDialog({
|
||||
|
||||
// Incase of a FS failure, don't crash the app
|
||||
try {
|
||||
// saveToLibraryAsync writes without reading the asset back, so it
|
||||
// works with the add-only permission on iOS (APP-2374)
|
||||
await saveToLibraryAsync(`file://${uri}`)
|
||||
// Asset.create supports the add-only permission on iOS (APP-2374).
|
||||
await Asset.create(`file://${uri}`)
|
||||
} catch (e: unknown) {
|
||||
Toast.show(_(msg`An error occurred while saving the QR code!`), {
|
||||
type: 'error',
|
||||
|
||||
@@ -2,13 +2,10 @@ import {Suspense, useRef} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {type ViewShotRef} from 'react-native-view-shot'
|
||||
import {setStringAsync} from 'expo-clipboard'
|
||||
import {
|
||||
requestPermissionsAsync,
|
||||
saveToLibraryAsync,
|
||||
} from 'expo-media-library/legacy'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {Asset, requestPermissionsAsync} from '#/lib/media/media-library'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {shareUrl as nativeShareUrl} from '#/lib/sharing'
|
||||
import {logger} from '#/logger'
|
||||
@@ -96,10 +93,8 @@ export function InviteFriendsDialogInner({
|
||||
}
|
||||
|
||||
try {
|
||||
// saveToLibraryAsync writes without reading the asset back, so it works
|
||||
// with the add-only permission. createAssetAsync fetches the created
|
||||
// asset, which triggers the full library read prompt on iOS (APP-2374).
|
||||
await saveToLibraryAsync(`file://${uri}`)
|
||||
// Asset.create supports the add-only permission on iOS (APP-2374).
|
||||
await Asset.create(`file://${uri}`)
|
||||
ax.metric('invite:action:download', {})
|
||||
Toast.show(l`QR code saved to your camera roll!`)
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Linking} from 'react-native'
|
||||
import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera'
|
||||
import * as MediaLibrary from 'expo-media-library/legacy'
|
||||
|
||||
import {usePermissions as useMediaLibraryPermissions} from '#/lib/media/media-library'
|
||||
import {Alert} from '#/view/com/util/Alert'
|
||||
import {IS_WEB} from '#/env'
|
||||
|
||||
@@ -20,7 +20,7 @@ const openPermissionAlert = (perm: string) => {
|
||||
}
|
||||
|
||||
export function usePhotoLibraryPermission() {
|
||||
const [res, requestPermission] = MediaLibrary.usePermissions({
|
||||
const [res, requestPermission] = useMediaLibraryPermissions({
|
||||
granularPermissions: ['photo'],
|
||||
})
|
||||
const requestPhotoAccessIfNeeded = async () => {
|
||||
@@ -49,7 +49,7 @@ export function usePhotoLibraryPermission() {
|
||||
}
|
||||
|
||||
export function useVideoLibraryPermission() {
|
||||
const [res, requestPermission] = MediaLibrary.usePermissions({
|
||||
const [res, requestPermission] = useMediaLibraryPermissions({
|
||||
granularPermissions: ['video'],
|
||||
})
|
||||
const requestVideoAccessIfNeeded = async () => {
|
||||
|
||||
+7
-28
@@ -13,12 +13,12 @@ import {
|
||||
writeAsStringAsync,
|
||||
} from 'expo-file-system/legacy'
|
||||
import {SaveFormat} from 'expo-image-manipulator'
|
||||
import * as MediaLibrary from 'expo-media-library/legacy'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {IS_ANDROID, IS_IOS} from '#/env'
|
||||
import {renderImage} from './image-manipulator'
|
||||
import {Album, Asset} from './media-library'
|
||||
import {type PickerImage} from './picker.shared'
|
||||
import {type Dimensions} from './types'
|
||||
import {convertCdnPreset, getResizedDimensions} from './util'
|
||||
@@ -123,52 +123,31 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
||||
// android triggers an annoying permission prompt if you try and move an image
|
||||
// between albums. therefore, we need to either create the album with the image
|
||||
// as the starting image, or put it directly into the album
|
||||
const album = await MediaLibrary.getAlbumAsync(ALBUM_NAME)
|
||||
const album = await Album.get(ALBUM_NAME)
|
||||
if (album) {
|
||||
// try and migrate if needed
|
||||
try {
|
||||
if (await MediaLibrary.albumNeedsMigrationAsync(album)) {
|
||||
await MediaLibrary.migrateAlbumIfNeededAsync(album)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.info('Attempted and failed to migrate album', {
|
||||
safeMessage: err,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
// if album exists, put the image straight in there
|
||||
await MediaLibrary.createAssetAsync(imagePath, album)
|
||||
await Asset.create(imagePath, album)
|
||||
} catch (err) {
|
||||
logger.info('Failed to create asset', {safeMessage: err})
|
||||
// however, it's possible that we don't have write permission to the album
|
||||
// try making a new one!
|
||||
try {
|
||||
await MediaLibrary.createAlbumAsync(
|
||||
ALBUM_NAME,
|
||||
undefined,
|
||||
undefined,
|
||||
imagePath,
|
||||
)
|
||||
await Album.create(ALBUM_NAME, [imagePath])
|
||||
} catch (err2) {
|
||||
logger.info('Failed to create asset in a fresh album', {
|
||||
safeMessage: err2,
|
||||
})
|
||||
// ... and if all else fails, just put it in DCIM
|
||||
await MediaLibrary.createAssetAsync(imagePath)
|
||||
await Asset.create(imagePath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// otherwise, create album with asset (albums must always have at least one asset)
|
||||
await MediaLibrary.createAlbumAsync(
|
||||
ALBUM_NAME,
|
||||
undefined,
|
||||
undefined,
|
||||
imagePath,
|
||||
)
|
||||
await Album.create(ALBUM_NAME, [imagePath])
|
||||
}
|
||||
} else {
|
||||
await MediaLibrary.saveToLibraryAsync(imagePath)
|
||||
await Asset.create(imagePath)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err instanceof Error ? err : String(err), {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
Album,
|
||||
Asset,
|
||||
requestPermissionsAsync,
|
||||
usePermissions,
|
||||
} from 'expo-media-library'
|
||||
@@ -0,0 +1,54 @@
|
||||
import {type PermissionResponse, PermissionStatus} from 'expo'
|
||||
import type {
|
||||
Album as NativeAlbum,
|
||||
Asset as NativeAsset,
|
||||
GranularPermission,
|
||||
} from 'expo-media-library'
|
||||
|
||||
const noPermissionResponse: PermissionResponse = {
|
||||
status: PermissionStatus.UNDETERMINED,
|
||||
canAskAgain: true,
|
||||
granted: false,
|
||||
expires: 'never',
|
||||
}
|
||||
|
||||
function unavailable(): Promise<never> {
|
||||
return Promise.reject(new Error('Media library is unavailable on web'))
|
||||
}
|
||||
|
||||
export const Album = {
|
||||
create(
|
||||
_name: string,
|
||||
_assetRefs: string[] | NativeAsset[],
|
||||
): Promise<NativeAlbum> {
|
||||
return unavailable()
|
||||
},
|
||||
get(_title: string): Promise<NativeAlbum | null> {
|
||||
return unavailable()
|
||||
},
|
||||
}
|
||||
|
||||
export const Asset = {
|
||||
create(_filePath: string, _album?: NativeAlbum): Promise<NativeAsset> {
|
||||
return unavailable()
|
||||
},
|
||||
}
|
||||
|
||||
export function requestPermissionsAsync(
|
||||
_writeOnly?: boolean,
|
||||
_granularPermissions?: GranularPermission[],
|
||||
): Promise<PermissionResponse> {
|
||||
return Promise.resolve(noPermissionResponse)
|
||||
}
|
||||
|
||||
export function usePermissions(_options?: {
|
||||
writeOnly?: boolean
|
||||
granularPermissions?: GranularPermission[]
|
||||
}): [
|
||||
PermissionResponse | null,
|
||||
() => Promise<PermissionResponse>,
|
||||
() => Promise<PermissionResponse>,
|
||||
] {
|
||||
const getPermission = () => Promise.resolve(noPermissionResponse)
|
||||
return [null, getPermission, getPermission]
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
import {useCallback} from 'react'
|
||||
import * as MediaLibrary from 'expo-media-library/legacy'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {saveImageToMediaLibrary} from './manip'
|
||||
import {usePermissions} from './media-library'
|
||||
|
||||
/**
|
||||
* Same as `saveImageToMediaLibrary`, but also handles permissions and toasts
|
||||
*/
|
||||
export function useSaveImageToMediaLibrary() {
|
||||
const {_} = useLingui()
|
||||
const [permissionResponse, requestPermission, getPermission] =
|
||||
MediaLibrary.usePermissions({
|
||||
const [permissionResponse, requestPermission, getPermission] = usePermissions(
|
||||
{
|
||||
granularPermissions: ['photo'],
|
||||
})
|
||||
},
|
||||
)
|
||||
return useCallback(
|
||||
async (uri: string) => {
|
||||
if (!IS_NATIVE) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as MediaLibrary from 'expo-media-library/legacy'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useCameraPermission} from '#/lib/hooks/usePermissions'
|
||||
import {Asset, usePermissions} from '#/lib/media/media-library'
|
||||
import {openCamera} from '#/lib/media/picker'
|
||||
import {logger} from '#/logger'
|
||||
import {createComposerImage} from '#/state/gallery'
|
||||
@@ -15,8 +15,9 @@ import {type OpenCameraBtnProps} from './OpenCameraBtn.shared'
|
||||
export function OpenCameraBtn({disabled, onAdd}: OpenCameraBtnProps) {
|
||||
const {_} = useLingui()
|
||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||
const [mediaPermissionRes, requestMediaPermission] =
|
||||
MediaLibrary.usePermissions({granularPermissions: ['photo']})
|
||||
const [mediaPermissionRes, requestMediaPermission] = usePermissions({
|
||||
granularPermissions: ['photo'],
|
||||
})
|
||||
const t = useTheme()
|
||||
|
||||
const mediaGranted = mediaPermissionRes?.granted
|
||||
@@ -32,9 +33,11 @@ export function OpenCameraBtn({disabled, onAdd}: OpenCameraBtnProps) {
|
||||
if (!(await requestCameraAccessIfNeeded())) {
|
||||
return
|
||||
}
|
||||
let canSaveToMediaLibrary = mediaGranted === true
|
||||
if (!mediaGranted) {
|
||||
if (mediaCanAskAgain) {
|
||||
await requestMediaPermission()
|
||||
const permission = await requestMediaPermission()
|
||||
canSaveToMediaLibrary = permission.granted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +48,14 @@ export function OpenCameraBtn({disabled, onAdd}: OpenCameraBtnProps) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we don't have permissions it's fine, we just wont save it. The post itself will still have access to
|
||||
// the image even without these permissions
|
||||
if (mediaPermissionRes) {
|
||||
await MediaLibrary.createAssetAsync(img.path)
|
||||
if (canSaveToMediaLibrary) {
|
||||
try {
|
||||
await Asset.create(img.path)
|
||||
} catch (err) {
|
||||
logger.warn('Failed to save camera image to media library', {
|
||||
safeMessage: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const res = await createComposerImage(img)
|
||||
|
||||
Reference in New Issue
Block a user