feat(lib): save photos or videos to an album
* add option to specify album in saveToCameraRoll and move the optional type param to options * check platform before setting default value for group types to prevent exception * adjust typings * update invariant message * format code * extract new implementation to function to avoid breaking change * format code * add missing spaces * fix(lib): add accidentally removed savedphotos back to the enum to prevent crash * chore(lib): formatting * chore(lib): add doc for the new save method
This commit is contained in:
parent
344b3a93a4
commit
dc00a4f115
11
README.md
11
README.md
|
@ -71,6 +71,7 @@ On Android permission is required to read the external storage. Add below line t
|
||||||
### Methods
|
### Methods
|
||||||
|
|
||||||
* [`saveToCameraRoll`](#savetocameraroll)
|
* [`saveToCameraRoll`](#savetocameraroll)
|
||||||
|
* [`save`](#save)
|
||||||
* [`getPhotos`](#getphotos)
|
* [`getPhotos`](#getphotos)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -79,13 +80,21 @@ On Android permission is required to read the external storage. Add below line t
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
|
### `save()`
|
||||||
|
|
||||||
|
Saves the photo or video of a particular type to an album.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
CameraRoll.save(tag, { type, album })
|
||||||
|
```
|
||||||
|
|
||||||
### `saveToCameraRoll()`
|
### `saveToCameraRoll()`
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
CameraRoll.saveToCameraRoll(tag, [type]);
|
CameraRoll.saveToCameraRoll(tag, [type]);
|
||||||
```
|
```
|
||||||
|
|
||||||
Saves the photo or video to the camera roll or photo library.
|
Saves the photo or video to the photo library.
|
||||||
|
|
||||||
On Android, the tag must be a local image or video URI, such as `"file:///sdcard/img.png"`.
|
On Android, the tag must be a local image or video URI, such as `"file:///sdcard/img.png"`.
|
||||||
|
|
||||||
|
|
|
@ -100,8 +100,8 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
* @param promise to be resolved or rejected
|
* @param promise to be resolved or rejected
|
||||||
*/
|
*/
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void saveToCameraRoll(String uri, String type, Promise promise) {
|
public void saveToCameraRoll(String uri, ReadableMap options, Promise promise) {
|
||||||
new SaveToCameraRoll(getReactApplicationContext(), Uri.parse(uri), promise)
|
new SaveToCameraRoll(getReactApplicationContext(), Uri.parse(uri), options, promise)
|
||||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,12 +110,14 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final Uri mUri;
|
private final Uri mUri;
|
||||||
private final Promise mPromise;
|
private final Promise mPromise;
|
||||||
|
private final ReadableMap mOptions;
|
||||||
|
|
||||||
public SaveToCameraRoll(ReactContext context, Uri uri, Promise promise) {
|
public SaveToCameraRoll(ReactContext context, Uri uri, ReadableMap options, Promise promise) {
|
||||||
super(context);
|
super(context);
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mUri = uri;
|
mUri = uri;
|
||||||
mPromise = promise;
|
mPromise = promise;
|
||||||
|
mOptions = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -123,8 +125,25 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
File source = new File(mUri.getPath());
|
File source = new File(mUri.getPath());
|
||||||
FileChannel input = null, output = null;
|
FileChannel input = null, output = null;
|
||||||
try {
|
try {
|
||||||
File exportDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
|
File environment;
|
||||||
exportDir.mkdirs();
|
if ("mov".equals(mOptions.getString("type"))) {
|
||||||
|
environment = Environment.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_MOVIES);
|
||||||
|
} else {
|
||||||
|
environment = Environment.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_PICTURES);
|
||||||
|
}
|
||||||
|
File exportDir;
|
||||||
|
if (!"".equals(mOptions.getString("album"))) {
|
||||||
|
exportDir = new File(environment, mOptions.getString("album"));
|
||||||
|
if (!exportDir.exists() && !exportDir.mkdirs()) {
|
||||||
|
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Album Directory not created. Did you request WRITE_EXTERNAL_STORAGE?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exportDir = environment;
|
||||||
|
}
|
||||||
|
|
||||||
if (!exportDir.isDirectory()) {
|
if (!exportDir.isDirectory()) {
|
||||||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available");
|
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available");
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -33,7 +33,7 @@ RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{
|
||||||
@"library": @(PHAssetCollectionSubtypeSmartAlbumUserLibrary),
|
@"library": @(PHAssetCollectionSubtypeSmartAlbumUserLibrary),
|
||||||
@"photo-stream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream), // incorrect, but legacy
|
@"photo-stream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream), // incorrect, but legacy
|
||||||
@"photostream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream),
|
@"photostream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream),
|
||||||
@"saved-photos": @(PHAssetCollectionSubtypeAny), // incorrect, but legacy
|
@"saved-photos": @(PHAssetCollectionSubtypeAny), // incorrect, but legacy correspondence in PHAssetCollectionSubtype
|
||||||
@"savedphotos": @(PHAssetCollectionSubtypeAny), // This was ALAssetsGroupSavedPhotos, seems to have no direct correspondence in PHAssetCollectionSubtype
|
@"savedphotos": @(PHAssetCollectionSubtypeAny), // This was ALAssetsGroupSavedPhotos, seems to have no direct correspondence in PHAssetCollectionSubtype
|
||||||
}), PHAssetCollectionSubtypeAny, integerValue)
|
}), PHAssetCollectionSubtypeAny, integerValue)
|
||||||
|
|
||||||
|
@ -98,36 +98,40 @@ static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthor
|
||||||
}
|
}
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request
|
RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request
|
||||||
type:(NSString *)type
|
options:(NSDictionary *)options
|
||||||
resolve:(RCTPromiseResolveBlock)resolve
|
resolve:(RCTPromiseResolveBlock)resolve
|
||||||
reject:(RCTPromiseRejectBlock)reject)
|
reject:(RCTPromiseRejectBlock)reject)
|
||||||
{
|
{
|
||||||
__block PHObjectPlaceholder *placeholder;
|
|
||||||
|
|
||||||
// We load images and videos differently.
|
// We load images and videos differently.
|
||||||
// Images have many custom loaders which can load images from ALAssetsLibrary URLs, PHPhotoLibrary
|
// Images have many custom loaders which can load images from ALAssetsLibrary URLs, PHPhotoLibrary
|
||||||
// URLs, `data:` URIs, etc. Video URLs are passed directly through for now; it may be nice to support
|
// URLs, `data:` URIs, etc. Video URLs are passed directly through for now; it may be nice to support
|
||||||
// more ways of loading videos in the future.
|
// more ways of loading videos in the future.
|
||||||
__block NSURL *inputURI = nil;
|
__block NSURL *inputURI = nil;
|
||||||
__block UIImage *inputImage = nil;
|
__block UIImage *inputImage = nil;
|
||||||
|
__block PHFetchResult *photosAsset;
|
||||||
|
__block PHAssetCollection *collection;
|
||||||
|
__block PHObjectPlaceholder *placeholder;
|
||||||
|
|
||||||
void (^saveBlock)(void) = ^void() {
|
void (^saveBlock)(void) = ^void() {
|
||||||
// performChanges and the completionHandler are called on
|
// performChanges and the completionHandler are called on
|
||||||
// arbitrary threads, not the main thread - this is safe
|
// arbitrary threads, not the main thread - this is safe
|
||||||
// for now since all JS is queued and executed on a single thread.
|
// for now since all JS is queued and executed on a single thread.
|
||||||
// We should reevaluate this if that assumption changes.
|
// We should reevaluate this if that assumption changes.
|
||||||
|
|
||||||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||||
PHAssetChangeRequest *changeRequest;
|
PHAssetChangeRequest *assetRequest ;
|
||||||
|
if ([options[@"type"] isEqualToString:@"video"]) {
|
||||||
// Defaults to "photo". `type` is an optional param.
|
assetRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:inputURI];
|
||||||
if ([type isEqualToString:@"video"]) {
|
|
||||||
changeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:inputURI];
|
|
||||||
} else {
|
} else {
|
||||||
changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:inputImage];
|
assetRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:inputImage];
|
||||||
}
|
}
|
||||||
|
placeholder = [assetRequest placeholderForCreatedAsset];
|
||||||
placeholder = [changeRequest placeholderForCreatedAsset];
|
if (![options[@"album"] isEqualToString:@""]) {
|
||||||
} completionHandler:^(BOOL success, NSError * _Nullable error) {
|
photosAsset = [PHAsset fetchAssetsInAssetCollection:collection options:nil];
|
||||||
|
PHAssetCollectionChangeRequest *albumChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:collection assets:photosAsset];
|
||||||
|
[albumChangeRequest addAssets:@[placeholder]];
|
||||||
|
}
|
||||||
|
} completionHandler:^(BOOL success, NSError *error) {
|
||||||
if (success) {
|
if (success) {
|
||||||
NSString *uri = [NSString stringWithFormat:@"ph://%@", [placeholder localIdentifier]];
|
NSString *uri = [NSString stringWithFormat:@"ph://%@", [placeholder localIdentifier]];
|
||||||
resolve(uri);
|
resolve(uri);
|
||||||
|
@ -136,11 +140,41 @@ 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
|
||||||
|
subtype:PHAssetCollectionSubtypeAny
|
||||||
|
options:fetchOptions].firstObject;
|
||||||
|
// Create the album
|
||||||
|
if (!collection) {
|
||||||
|
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||||
|
PHAssetCollectionChangeRequest *createAlbum = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:options[@"album"]];
|
||||||
|
placeholder = [createAlbum placeholderForCreatedAssetCollection];
|
||||||
|
} completionHandler:^(BOOL success, NSError *error) {
|
||||||
|
if (success) {
|
||||||
|
PHFetchResult *collectionFetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[placeholder.localIdentifier]
|
||||||
|
options:nil];
|
||||||
|
collection = collectionFetchResult.firstObject;
|
||||||
|
saveBlock();
|
||||||
|
} else {
|
||||||
|
reject(kErrorUnableToSave, nil, error);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
saveBlock();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveBlock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
void (^loadBlock)(void) = ^void() {
|
void (^loadBlock)(void) = ^void() {
|
||||||
if ([type isEqualToString:@"video"]) {
|
if ([options[@"type"] isEqualToString:@"video"]) {
|
||||||
inputURI = request.URL;
|
inputURI = request.URL;
|
||||||
saveBlock();
|
saveWithOptions();
|
||||||
} else {
|
} else {
|
||||||
[self.bridge.imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) {
|
[self.bridge.imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -149,7 +183,7 @@ RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request
|
||||||
}
|
}
|
||||||
|
|
||||||
inputImage = image;
|
inputImage = image;
|
||||||
saveBlock();
|
saveWithOptions();
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -201,7 +235,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||||
PHAssetCollectionSubtype const collectionSubtype = [RCTConvert PHAssetCollectionSubtype:groupTypes];
|
PHAssetCollectionSubtype const collectionSubtype = [RCTConvert PHAssetCollectionSubtype:groupTypes];
|
||||||
|
|
||||||
// Predicate for fetching assets within a collection
|
// Predicate for fetching assets within a collection
|
||||||
PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType];
|
PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetcihOptionsFromMediaType:mediaType];
|
||||||
assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
|
assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
|
||||||
|
|
||||||
BOOL __block foundAfter = NO;
|
BOOL __block foundAfter = NO;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
import {Platform} from 'react-native';
|
||||||
import RNCCameraRoll from './nativeInterface';
|
import RNCCameraRoll from './nativeInterface';
|
||||||
|
|
||||||
const invariant = require('fbjs/lib/invariant');
|
const invariant = require('fbjs/lib/invariant');
|
||||||
|
@ -100,7 +100,10 @@ export type PhotoIdentifiersPage = {
|
||||||
end_cursor?: string,
|
end_cursor?: string,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
export type SaveToCameraRollOptions = {
|
||||||
|
type?: 'photo' | 'video' | 'auto',
|
||||||
|
album?: string,
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* `CameraRoll` provides access to the local camera roll or photo library.
|
* `CameraRoll` provides access to the local camera roll or photo library.
|
||||||
*
|
*
|
||||||
|
@ -117,7 +120,7 @@ class CameraRoll {
|
||||||
console.warn(
|
console.warn(
|
||||||
'`CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.',
|
'`CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.',
|
||||||
);
|
);
|
||||||
return this.saveToCameraRoll(tag, 'photo');
|
return this.saveToCameraRoll(tag, {type: 'photo'});
|
||||||
}
|
}
|
||||||
|
|
||||||
static deletePhotos(photos: Array<string>) {
|
static deletePhotos(photos: Array<string>) {
|
||||||
|
@ -128,31 +131,35 @@ class CameraRoll {
|
||||||
* Saves the photo or video to the camera roll or photo library.
|
* Saves the photo or video to the camera roll or photo library.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
static saveToCameraRoll(
|
static save(
|
||||||
tag: string,
|
tag: string,
|
||||||
type?: 'photo' | 'video',
|
options: SaveToCameraRollOptions = {},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
let {type = 'auto', album = ''} = options;
|
||||||
invariant(
|
invariant(
|
||||||
typeof tag === 'string',
|
typeof tag === 'string',
|
||||||
'CameraRoll.saveToCameraRoll must be a valid string.',
|
'CameraRoll.saveToCameraRoll must be a valid string.',
|
||||||
);
|
);
|
||||||
|
|
||||||
invariant(
|
invariant(
|
||||||
type === 'photo' || type === 'video' || type === undefined,
|
options.type === 'photo' ||
|
||||||
`The second argument to saveToCameraRoll must be 'photo' or 'video'. You passed ${type ||
|
options.type === 'video' ||
|
||||||
|
options.type === 'auto' ||
|
||||||
|
options.type === undefined,
|
||||||
|
`The second argument to saveToCameraRoll must be 'photo' or 'video' or 'auto'. You passed ${type ||
|
||||||
'unknown'}`,
|
'unknown'}`,
|
||||||
);
|
);
|
||||||
|
if (type === 'auto') {
|
||||||
let mediaType = 'photo';
|
if (['mov', 'mp4'].indexOf(tag.split('.').slice(-1)[0]) >= 0) {
|
||||||
if (type) {
|
type = 'video';
|
||||||
mediaType = type;
|
} else {
|
||||||
} else if (['mov', 'mp4'].indexOf(tag.split('.').slice(-1)[0]) >= 0) {
|
type = 'photo';
|
||||||
mediaType = 'video';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return RNCCameraRoll.saveToCameraRoll(tag, mediaType);
|
|
||||||
}
|
}
|
||||||
|
return RNCCameraRoll.saveToCameraRoll(tag, {type, album});
|
||||||
|
}
|
||||||
|
static saveToCameraRoll(tag: string, type?: photo | video) {
|
||||||
|
CameraRoll.save(tag, {type});
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Returns a Promise with photo identifier objects from the local camera
|
* Returns a Promise with photo identifier objects from the local camera
|
||||||
* roll of the device matching shape defined by `getPhotosReturnChecker`.
|
* roll of the device matching shape defined by `getPhotosReturnChecker`.
|
||||||
|
@ -163,7 +170,7 @@ class CameraRoll {
|
||||||
if (!params.assetType) {
|
if (!params.assetType) {
|
||||||
params.assetType = ASSET_TYPE_OPTIONS.All;
|
params.assetType = ASSET_TYPE_OPTIONS.All;
|
||||||
}
|
}
|
||||||
if (!params.groupTypes) {
|
if (!params.groupTypes && Platform.OS !== 'android') {
|
||||||
params.groupTypes = GROUP_TYPES_OPTIONS.All;
|
params.groupTypes = GROUP_TYPES_OPTIONS.All;
|
||||||
}
|
}
|
||||||
if (arguments.length > 1) {
|
if (arguments.length > 1) {
|
||||||
|
|
|
@ -25,6 +25,12 @@ describe('CameraRoll', () => {
|
||||||
expect(NativeModule.saveToCameraRoll.mock.calls).toMatchSnapshot();
|
expect(NativeModule.saveToCameraRoll.mock.calls).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should call save', async () => {
|
||||||
|
await CameraRoll.save('a tag', {type:'photo'});
|
||||||
|
expect(NativeModule.saveToCameraRoll.mock.calls).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('Should call getPhotos', async () => {
|
it('Should call getPhotos', async () => {
|
||||||
await CameraRoll.getPhotos({first: 0});
|
await CameraRoll.getPhotos({first: 0});
|
||||||
expect(NativeModule.getPhotos.mock.calls).toMatchSnapshot();
|
expect(NativeModule.getPhotos.mock.calls).toMatchSnapshot();
|
||||||
|
|
|
@ -22,11 +22,26 @@ Array [
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`CameraRoll Should call save 1`] = `
|
||||||
|
Array [
|
||||||
|
Array [
|
||||||
|
"a tag",
|
||||||
|
Object {
|
||||||
|
"album": "",
|
||||||
|
"type": "photo",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`CameraRoll Should call saveToCameraRoll 1`] = `
|
exports[`CameraRoll Should call saveToCameraRoll 1`] = `
|
||||||
Array [
|
Array [
|
||||||
Array [
|
Array [
|
||||||
"a tag",
|
"a tag",
|
||||||
"photo",
|
Object {
|
||||||
|
"album": "",
|
||||||
|
"type": "photo",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -60,6 +60,10 @@ declare namespace CameraRoll {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SaveToCameraRollOptions = {
|
||||||
|
type?: 'photo' | 'video' | 'auto',
|
||||||
|
album?: string,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.
|
* `CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.
|
||||||
|
@ -77,6 +81,11 @@ declare namespace CameraRoll {
|
||||||
*/
|
*/
|
||||||
function saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<string>;
|
function saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the photo or video to the camera roll or photo library.
|
||||||
|
*/
|
||||||
|
function save(tag: string, options?: SaveToCameraRollOptions): Promise<string>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a Promise with photo identifier objects from the local camera
|
* Returns a Promise with photo identifier objects from the local camera
|
||||||
* roll of the device matching shape defined by `getPhotosReturnChecker`.
|
* roll of the device matching shape defined by `getPhotosReturnChecker`.
|
||||||
|
|
Loading…
Reference in New Issue