From b9e52a1b36db41bdcd270744efb86915c86174ac Mon Sep 17 00:00:00 2001
From: David Narbutovich <4661784+retyui@users.noreply.github.com>
Date: Fri, 11 Nov 2022 08:32:28 +0100
Subject: [PATCH] feat(all): Add `useCameraRoll()` hook (#354)
* Add `useCameraRoll()` hook
* Bump React Native patch ver. to fix Android build
---
README.md | 25 +++++++
package.json | 13 ++--
src/CameraRoll.ts | 1 +
src/__tests__/useCameraRoll.ts | 120 +++++++++++++++++++++++++++++++++
src/index.ts | 1 +
src/useCameraRoll.ts | 55 +++++++++++++++
yarn.lock | 35 ++++++++--
7 files changed, 240 insertions(+), 10 deletions(-)
create mode 100644 src/__tests__/useCameraRoll.ts
create mode 100644 src/useCameraRoll.ts
diff --git a/README.md b/README.md
index a6febb8..a5494a5 100644
--- a/README.md
+++ b/README.md
@@ -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 <>
+
+ {
+ photos.map((photo, index) => /* render photos */)
+ }
+ >;
+};
+```
+
### Known issues
#### IOS
diff --git a/package.json b/package.json
index 691c4d1..cafb3f4 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/CameraRoll.ts b/src/CameraRoll.ts
index cee9b3d..b0769c6 100644
--- a/src/CameraRoll.ts
+++ b/src/CameraRoll.ts
@@ -193,6 +193,7 @@ export class CameraRoll {
);
return CameraRoll.save(tag, {type});
}
+
static getAlbums(
params: GetAlbumsParams = {assetType: 'All'},
): Promise {
diff --git a/src/__tests__/useCameraRoll.ts b/src/__tests__/useCameraRoll.ts
new file mode 100644
index 0000000..140c93c
--- /dev/null
+++ b/src/__tests__/useCameraRoll.ts
@@ -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);
+ });
+ });
+});
diff --git a/src/index.ts b/src/index.ts
index ef7be36..62bbb32 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,2 +1,3 @@
+export * from './useCameraRoll';
export * from './CameraRoll';
export * from './CameraRollIOSPermission';
diff --git a/src/useCameraRoll.ts b/src/useCameraRoll.ts
new file mode 100644
index 0000000..8a9d621
--- /dev/null
+++ b/src/useCameraRoll.ts
@@ -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,
+ (tag: string, options?: SaveToCameraRollOptions) => Promise,
+];
+
+export function useCameraRoll(): UseCameraRollResult {
+ const [photos, setPhotos] = useState(initialState);
+
+ async function getPhotos(
+ config: GetPhotosParams = defaultConfig,
+ ): Promise {
+ 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
+ ): Promise {
+ try {
+ await CameraRoll.save(...args);
+ } catch (error) {
+ if (__DEV__)
+ console.log('[useCameraRoll] Error saving to camera roll: ', error);
+ }
+ }
+
+ return [photos, getPhotos, save];
+}
diff --git a/yarn.lock b/yarn.lock
index 7dab313..362fad4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"