better support for iOS 14
Co-Authored-by: Jean-François Puissant <jean-francois.puissant@fleetback.com>
This commit is contained in:
parent
8bb2ea468b
commit
337c5a515e
|
@ -241,6 +241,7 @@ Returns a Promise which when resolved will be of the following shape:
|
|||
* `has_next_page`: {boolean}
|
||||
* `start_cursor`: {string}
|
||||
* `end_cursor`: {string}
|
||||
* `limited` : {boolean | undefined} : true if the app can only access a subset of the gallery pictures (authorization is `PHAuthorizationStatusLimited`), false otherwise (iOS only)
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
|
@ -20,6 +20,9 @@ const {
|
|||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
Linking,
|
||||
} = ReactNative;
|
||||
|
||||
import CameraRoll from '../../js/CameraRoll';
|
||||
|
@ -61,6 +64,7 @@ class CameraRollView extends React.Component {
|
|||
lastCursor: null,
|
||||
noMore: false,
|
||||
loadingMore: false,
|
||||
isLimited: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -148,6 +152,16 @@ class CameraRollView extends React.Component {
|
|||
if (!this.state.noMore) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
if (this.state.isLimited) {
|
||||
return (
|
||||
<TouchableOpacity onPress={Linking.openSettings}>
|
||||
<Text style={styles.footerText}>
|
||||
Not all pictures are available. Tap here to go to Settings and
|
||||
change which media the app is allowed to access.
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
@ -162,7 +176,7 @@ class CameraRollView extends React.Component {
|
|||
|
||||
_appendAssets(data) {
|
||||
const assets = data.edges;
|
||||
const newState = {loadingMore: false};
|
||||
const newState = {loadingMore: false, isLimited: data.limited};
|
||||
|
||||
if (!data.page_info.has_next_page) {
|
||||
newState.noMore = true;
|
||||
|
@ -210,6 +224,10 @@ const styles = StyleSheet.create({
|
|||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
footerText: {
|
||||
padding: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = CameraRollView;
|
||||
|
|
|
@ -49,7 +49,7 @@ RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{
|
|||
NSString *const lowercase = [mediaType lowercaseString];
|
||||
NSMutableArray *format = [NSMutableArray new];
|
||||
NSMutableArray *arguments = [NSMutableArray new];
|
||||
|
||||
|
||||
if ([lowercase isEqualToString:@"photos"]) {
|
||||
[format addObject:@"mediaType = %d"];
|
||||
[arguments addObject:@(PHAssetMediaTypeImage)];
|
||||
|
@ -62,7 +62,7 @@ RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{
|
|||
"'videos' or 'all'.", mediaType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (fromTime > 0) {
|
||||
NSDate* fromDate = [NSDate dateWithTimeIntervalSince1970:fromTime/1000];
|
||||
[format addObject:@"creationDate > %@"];
|
||||
|
@ -73,7 +73,7 @@ RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{
|
|||
[format addObject:@"creationDate <= %@"];
|
||||
[arguments addObject:toDate];
|
||||
}
|
||||
|
||||
|
||||
// This case includes the "all" mediatype
|
||||
PHFetchOptions *const options = [PHFetchOptions new];
|
||||
if ([format count] > 0) {
|
||||
|
@ -96,18 +96,35 @@ static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD";
|
|||
static NSString *const kErrorAuthRestricted = @"E_PHOTO_LIBRARY_AUTH_RESTRICTED";
|
||||
static NSString *const kErrorAuthDenied = @"E_PHOTO_LIBRARY_AUTH_DENIED";
|
||||
|
||||
typedef void (^PhotosAuthorizedBlock)(void);
|
||||
typedef void (^PhotosAuthorizedBlock)(bool isLimited);
|
||||
|
||||
static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthorizedBlock authorizedBlock) {
|
||||
PHAuthorizationStatus authStatus = [PHPhotoLibrary authorizationStatus];
|
||||
static void requestPhotoLibraryAccess(bool addOnly, RCTPromiseRejectBlock reject, PhotosAuthorizedBlock authorizedBlock) {
|
||||
PHAuthorizationStatus authStatus;
|
||||
if (@available(iOS 14, *)) {
|
||||
authStatus = [PHPhotoLibrary authorizationStatusForAccessLevel:(addOnly ? PHAccessLevelAddOnly : PHAccessLevelReadWrite)];
|
||||
} else {
|
||||
authStatus = [PHPhotoLibrary authorizationStatus];
|
||||
}
|
||||
if (authStatus == PHAuthorizationStatusRestricted) {
|
||||
reject(kErrorAuthRestricted, @"Access to photo library is restricted", nil);
|
||||
} else if (authStatus == PHAuthorizationStatusAuthorized) {
|
||||
authorizedBlock();
|
||||
authorizedBlock(false);
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
|
||||
} else if (authStatus == PHAuthorizationStatusLimited) {
|
||||
#pragma clang diagnostic pop
|
||||
authorizedBlock(true);
|
||||
} else if (authStatus == PHAuthorizationStatusNotDetermined) {
|
||||
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
|
||||
requestPhotoLibraryAccess(reject, authorizedBlock);
|
||||
}];
|
||||
if (@available(iOS 14, *)) {
|
||||
[PHPhotoLibrary requestAuthorizationForAccessLevel:(addOnly ? PHAccessLevelAddOnly : PHAccessLevelReadWrite)
|
||||
handler:^(PHAuthorizationStatus status) {
|
||||
requestPhotoLibraryAccess(addOnly, reject, authorizedBlock);
|
||||
}];
|
||||
} else {
|
||||
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
|
||||
requestPhotoLibraryAccess(addOnly, reject, authorizedBlock);
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
reject(kErrorAuthDenied, @"Access to photo library was denied", nil);
|
||||
}
|
||||
|
@ -159,7 +176,7 @@ RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request
|
|||
};
|
||||
void (^saveWithOptions)(void) = ^void() {
|
||||
if (![options[@"album"] isEqualToString:@""]) {
|
||||
|
||||
|
||||
PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
|
||||
fetchOptions.predicate = [NSPredicate predicateWithFormat:@"title = %@", options[@"album"] ];
|
||||
collection = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum
|
||||
|
@ -188,12 +205,12 @@ RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request
|
|||
}
|
||||
};
|
||||
|
||||
void (^loadBlock)(void) = ^void() {
|
||||
void (^loadBlock)(bool isLimited) = ^void(bool isLimited) {
|
||||
inputURI = request.URL;
|
||||
saveWithOptions();
|
||||
};
|
||||
|
||||
requestPhotoLibraryAccess(reject, loadBlock);
|
||||
requestPhotoLibraryAccess(true, reject, loadBlock);
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(getAlbums:(NSDictionary *)params
|
||||
|
@ -220,14 +237,16 @@ RCT_EXPORT_METHOD(getAlbums:(NSDictionary *)params
|
|||
|
||||
static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
|
||||
NSArray<NSDictionary<NSString *, id> *> *assets,
|
||||
BOOL hasNextPage)
|
||||
BOOL hasNextPage,
|
||||
bool isLimited)
|
||||
{
|
||||
if (!assets.count) {
|
||||
resolve(@{
|
||||
@"edges": assets,
|
||||
@"page_info": @{
|
||||
@"has_next_page": @NO,
|
||||
}
|
||||
},
|
||||
@"limited": @(isLimited)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -237,7 +256,8 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
|
|||
@"start_cursor": assets[0][@"node"][@"image"][@"uri"],
|
||||
@"end_cursor": assets[assets.count - 1][@"node"][@"image"][@"uri"],
|
||||
@"has_next_page": @(hasNextPage),
|
||||
}
|
||||
},
|
||||
@"limited": @(isLimited)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -262,14 +282,14 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
BOOL __block includeLocation = [include indexOfObject:@"location"] != NSNotFound;
|
||||
BOOL __block includeImageSize = [include indexOfObject:@"imageSize"] != NSNotFound;
|
||||
BOOL __block includePlayableDuration = [include indexOfObject:@"playableDuration"] != NSNotFound;
|
||||
|
||||
|
||||
// If groupTypes is "all", we want to fetch the SmartAlbum "all photos". Otherwise, all
|
||||
// other groupTypes values require the "album" collection type.
|
||||
PHAssetCollectionType const collectionType = ([groupTypes isEqualToString:@"all"]
|
||||
? PHAssetCollectionTypeSmartAlbum
|
||||
: PHAssetCollectionTypeAlbum);
|
||||
PHAssetCollectionSubtype const collectionSubtype = [RCTConvert PHAssetCollectionSubtype:groupTypes];
|
||||
|
||||
|
||||
// Predicate for fetching assets within a collection
|
||||
PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType fromTime:fromTime toTime:toTime];
|
||||
// We can directly set the limit if we guarantee every image fetched will be
|
||||
|
@ -285,29 +305,29 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
assetFetchOptions.fetchLimit = first + 1;
|
||||
}
|
||||
assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
|
||||
|
||||
|
||||
BOOL __block foundAfter = NO;
|
||||
BOOL __block hasNextPage = NO;
|
||||
BOOL __block resolvedPromise = NO;
|
||||
NSMutableArray<NSDictionary<NSString *, id> *> *assets = [NSMutableArray new];
|
||||
|
||||
|
||||
// Filter collection name ("group")
|
||||
PHFetchOptions *const collectionFetchOptions = [PHFetchOptions new];
|
||||
collectionFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate" ascending:NO]];
|
||||
if (groupName != nil) {
|
||||
collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", groupName];
|
||||
}
|
||||
|
||||
|
||||
BOOL __block stopCollections_;
|
||||
NSString __block *currentCollectionName;
|
||||
|
||||
requestPhotoLibraryAccess(reject, ^{
|
||||
requestPhotoLibraryAccess(false, reject, ^(bool isLimited){
|
||||
void (^collectAsset)(PHAsset*, NSUInteger, BOOL*) = ^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) {
|
||||
NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]];
|
||||
NSString *_Nullable originalFilename = NULL;
|
||||
PHAssetResource *_Nullable resource = NULL;
|
||||
NSNumber* fileSize = [NSNumber numberWithInt:0];
|
||||
|
||||
|
||||
if (includeFilename || includeFileSize || [mimeTypes count] > 0) {
|
||||
// Get underlying resources of an asset - this includes files as well as details about edited PHAssets
|
||||
// This is required for the filename and mimeType filtering
|
||||
|
@ -316,7 +336,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
originalFilename = resource.originalFilename;
|
||||
fileSize = [resource valueForKey:@"fileSize"];
|
||||
}
|
||||
|
||||
|
||||
// WARNING: If you add any code to `collectAsset` that may skip adding an
|
||||
// asset to the `assets` output array, you should do it inside this
|
||||
// block and ensure the logic for `collectAssetMayOmitAsset` above is
|
||||
|
@ -354,7 +374,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
stopCollections_ = YES;
|
||||
hasNextPage = YES;
|
||||
RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results.");
|
||||
RCTResolvePromise(resolve, assets, hasNextPage);
|
||||
RCTResolvePromise(resolve, assets, hasNextPage, isLimited);
|
||||
resolvedPromise = YES;
|
||||
return;
|
||||
}
|
||||
|
@ -412,7 +432,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
// If we get this far and haven't resolved the promise yet, we reached the end of the list of photos
|
||||
if (!resolvedPromise) {
|
||||
hasNextPage = NO;
|
||||
RCTResolvePromise(resolve, assets, hasNextPage);
|
||||
RCTResolvePromise(resolve, assets, hasNextPage, isLimited);
|
||||
resolvedPromise = YES;
|
||||
}
|
||||
});
|
||||
|
@ -423,7 +443,7 @@ RCT_EXPORT_METHOD(deletePhotos:(NSArray<NSString *>*)assets
|
|||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
NSMutableArray *convertedAssets = [NSMutableArray array];
|
||||
|
||||
|
||||
for (NSString *asset in assets) {
|
||||
[convertedAssets addObject: [asset stringByReplacingOccurrencesOfString:@"ph://" withString:@""]];
|
||||
}
|
||||
|
|
|
@ -122,7 +122,9 @@ export type PhotoIdentifiersPage = {
|
|||
start_cursor?: string,
|
||||
end_cursor?: string,
|
||||
},
|
||||
limited?: boolean,
|
||||
};
|
||||
|
||||
export type SaveToCameraRollOptions = {
|
||||
type?: 'photo' | 'video' | 'auto',
|
||||
album?: string,
|
||||
|
|
Loading…
Reference in New Issue