feat(all): Add useCameraRoll() hook (#354)

* Add `useCameraRoll()` hook

* Bump React Native patch ver. to fix Android build
This commit is contained in:
David Narbutovich
2022-11-11 08:32:28 +01:00
committed by GitHub
parent 1d4185fe9e
commit b9e52a1b36
7 changed files with 240 additions and 10 deletions
+25
View File
@@ -143,6 +143,8 @@ On Android 13 the `READ_EXTERNAL_STORAGE` has been [replace](https://developer.a
* [`getAlbums`](#getalbums)
* [`getPhotos`](#getphotos)
* [`deletePhotos`](#deletephotos)
* [`iosGetImageDataById`](#iosgetimagedatabyid)
* [`useCameraRoll`](#usecameraroll)
---
@@ -472,6 +474,29 @@ CameraRoll.iosGetImageDataById(internalID, true);
| internalID | string | Yes | Ios internal ID 'PH://xxxx'. |
| convertHeic | boolean | False | Whether to convert or not to JPEG image. |
### `useCameraRoll()`
`useCameraRoll` is a utility hooks for the CameraRoll module. data contains the content stored in the clipboard.
```javascript
import React, {useEffect} from 'react';
import {Button} from 'react-native';
import {useCameraRoll} from "@react-native-camera-roll/camera-roll";
function Example() {
const [photos, getPhotos, save] = useCameraRoll();
return <>
<Button title='Get Photos' onPress={() => getPhotos()}>Get Photos</Button>
{
photos.map((photo, index) => /* render photos */)
}
</>;
};
```
### Known issues
#### IOS
+7 -6
View File
@@ -54,29 +54,30 @@
"react-native": ">=0.59"
},
"devDependencies": {
"react-native-builder-bob": "0.18.3",
"@babel/core": "7.18.10",
"@babel/runtime": "7.18.9",
"@react-native-community/eslint-config": "3.1.0",
"@semantic-release/git": "10.0.1",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "28.1.7",
"@types/react-native": "0.62.10",
"babel-core": "7.0.0-bridge.0",
"babel-plugin-module-resolver": "4.1.0",
"babel-jest": "28.1.3",
"babel-plugin-module-resolver": "4.1.0",
"detox": "19.9.3",
"eslint": "8.22.0",
"eslint-plugin-prettier": "4.2.1",
"husky": "8.0.1",
"jest": "28.1.3",
"@types/jest": "28.1.7",
"prettier": "2.7.1",
"metro-react-native-babel-preset": "0.72.0",
"prettier": "2.7.1",
"pretty-quick": "3.1.3",
"react": "17.0.2",
"react-native": "0.66.0",
"react-native": "0.66.5",
"react-native-builder-bob": "0.18.3",
"react-test-renderer": "17.0.2",
"semantic-release": "19.0.3",
"rimraf": "3.0.2",
"semantic-release": "19.0.3",
"typescript": "4.7.4"
},
"react-native-builder-bob": {
+1
View File
@@ -193,6 +193,7 @@ export class CameraRoll {
);
return CameraRoll.save(tag, {type});
}
static getAlbums(
params: GetAlbumsParams = {assetType: 'All'},
): Promise<Album[]> {
+120
View File
@@ -0,0 +1,120 @@
import {renderHook} from '@testing-library/react-hooks';
import {useCameraRoll} from '../useCameraRoll';
import RNCCameraRoll from '../nativeInterface';
jest.mock('../nativeInterface', () => ({
getPhotos: jest.fn(),
saveToCameraRoll: jest.fn(),
}));
describe('useCameraRoll()', () => {
it('should return initial photos by default', () => {
const {result} = renderHook(() => useCameraRoll());
expect(result.current).toEqual([
{
edges: [],
page_info: {end_cursor: '', has_next_page: false, start_cursor: ''},
},
expect.any(Function),
expect.any(Function),
]);
});
describe('saveToCameraRoll()', () => {
it('should invoke save with passed params', () => {
const tag = 'mock-tag';
const type = 'video';
const album = 'test-album';
const {result} = renderHook(() => useCameraRoll());
const [, , saveToCameraRoll] = result.current;
RNCCameraRoll.saveToCameraRoll.mockResolvedValueOnce('');
saveToCameraRoll(tag, {type, album});
expect(RNCCameraRoll.saveToCameraRoll).toBeCalledWith(tag, {album, type});
});
});
describe('getPhotos()', () => {
const createPhotosMock = ({
edges = [] as Array<{node: {type: string}}>,
has_next_page = false,
start_cursor = '',
end_cursor = '',
limited = false,
} = {}) => ({
edges,
limited,
page_info: {has_next_page, start_cursor, end_cursor},
});
it('should invoke getPhotos with default params', async () => {
const {result, waitForNextUpdate} = renderHook(() => useCameraRoll());
const [, getPhotos] = result.current;
RNCCameraRoll.getPhotos.mockResolvedValueOnce(createPhotosMock());
getPhotos();
await waitForNextUpdate();
expect(RNCCameraRoll.getPhotos).toHaveBeenCalledWith({
assetType: 'All',
first: 20,
groupTypes: 'All',
});
});
it('should invoke getPhotos with custom params', async () => {
const customParams = {
first: 1,
assetType: 'Photos' as const,
include: ['filename' as const],
};
const {result, waitForNextUpdate} = renderHook(() => useCameraRoll());
const [, getPhotos] = result.current;
RNCCameraRoll.getPhotos.mockResolvedValueOnce(createPhotosMock());
getPhotos(customParams);
await waitForNextUpdate();
expect(RNCCameraRoll.getPhotos).toHaveBeenCalledWith({
assetType: 'Photos',
first: 1,
groupTypes: 'All',
include: ['filename'],
});
});
it('should return result of getPhotos', async () => {
const mockPhotos = createPhotosMock({
edges: [{node: {type: 'mock-type'}}],
});
const {result, waitForNextUpdate} = renderHook(() => useCameraRoll());
const [, getPhotos] = result.current;
RNCCameraRoll.getPhotos.mockResolvedValueOnce(mockPhotos);
getPhotos();
await waitForNextUpdate();
const [photos] = result.current;
expect(photos).toEqual(mockPhotos);
});
it('should handle an error when invoke getPhotos', async () => {
const error = new Error('Ops...');
const {result} = renderHook(() => useCameraRoll());
const [initialPhotos, getPhotos] = result.current;
RNCCameraRoll.getPhotos.mockRejectedValueOnce(error);
getPhotos();
const [afterError] = result.current;
expect(initialPhotos).toBe(afterError);
});
});
});
+1
View File
@@ -1,2 +1,3 @@
export * from './useCameraRoll';
export * from './CameraRoll';
export * from './CameraRollIOSPermission';
+55
View File
@@ -0,0 +1,55 @@
import {useState} from 'react';
import type {
GetPhotosParams,
PhotoIdentifiersPage,
SaveToCameraRollOptions,
} from './CameraRoll';
import {CameraRoll} from './CameraRoll';
const initialState: PhotoIdentifiersPage = {
edges: [],
page_info: {
end_cursor: '',
has_next_page: false,
start_cursor: '',
},
};
const defaultConfig: GetPhotosParams = {
first: 20,
groupTypes: 'All',
};
type UseCameraRollResult = [
PhotoIdentifiersPage,
(config?: GetPhotosParams) => Promise<void>,
(tag: string, options?: SaveToCameraRollOptions) => Promise<void>,
];
export function useCameraRoll(): UseCameraRollResult {
const [photos, setPhotos] = useState<PhotoIdentifiersPage>(initialState);
async function getPhotos(
config: GetPhotosParams = defaultConfig,
): Promise<void> {
try {
const result = await CameraRoll.getPhotos(config);
setPhotos(result);
} catch (error) {
if (__DEV__) console.log('[useCameraRoll] Error getting photos: ', error);
}
}
async function save(
...args: Parameters<typeof CameraRoll.save>
): Promise<void> {
try {
await CameraRoll.save(...args);
} catch (error) {
if (__DEV__)
console.log('[useCameraRoll] Error saving to camera roll: ', error);
}
}
return [photos, getPhotos, save];
}
+31 -4
View File
@@ -1761,6 +1761,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.5":
version "7.20.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9"
integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==
dependencies:
regenerator-runtime "^0.13.10"
"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.2.2":
version "7.2.2"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907"
@@ -2856,6 +2863,14 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@testing-library/react-hooks@8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12"
integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-boundary "^3.1.0"
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@@ -9338,6 +9353,13 @@ react-devtools-core@^4.13.0:
shell-quote "^1.6.1"
ws "^7"
react-error-boundary@^3.1.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
dependencies:
"@babel/runtime" "^7.12.5"
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
@@ -9394,10 +9416,10 @@ react-native-codegen@^0.0.7:
jscodeshift "^0.11.0"
nullthrows "^1.1.1"
react-native@0.66.0:
version "0.66.0"
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.66.0.tgz#99bdd83a9a612a71b94242767989d666d445b007"
integrity sha512-m26TKwzsfHVdZ1hDG+7mZ4M4ftxFFZrhtiT0OXuwfBzmNtB3xhsJtYszPeizw33c9YNp8rvehKT3c4ldDCW6kA==
react-native@0.66.5:
version "0.66.5"
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.66.5.tgz#9056a2f7ad04d5e75b3a00dab847b3366d69f26a"
integrity sha512-dC5xmE1anT+m8eGU0N/gv2XUWZygii6TTqbwZPsN+uMhVvjxt4FsTqpZOFFvA5sxLPR/NDEz8uybTvItNBMClw==
dependencies:
"@jest/create-cache-key-function" "^27.0.1"
"@react-native-community/cli" "^6.0.0"
@@ -9607,6 +9629,11 @@ regenerate@^1.4.2:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
regenerator-runtime@^0.13.10:
version "0.13.10"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee"
integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==
regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4:
version "0.13.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"