diff --git a/example/App.js b/example/App.js index 6a1f2b878..b11db5a6a 100644 --- a/example/App.js +++ b/example/App.js @@ -10,6 +10,7 @@ import React, {Component} from 'react'; import {StyleSheet, Text, View} from 'react-native'; import CameraRoll from '@react-native-community/cameraroll'; +import CameraRollExample from './js/CameraRollExample'; type Props = {}; export default class App extends Component { @@ -20,7 +21,7 @@ export default class App extends Component { render() { return ( - Welcome + ); } diff --git a/example/e2e/config.json b/example/e2e/config.json new file mode 100644 index 000000000..1f6588a1f --- /dev/null +++ b/example/e2e/config.json @@ -0,0 +1,4 @@ +{ + "setupFilesAfterEnv": ["./init.js"], + "testEnvironment": "node" +} diff --git a/example/e2e/init.js b/example/e2e/init.js new file mode 100644 index 000000000..d4bea54b4 --- /dev/null +++ b/example/e2e/init.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ +/* eslint-env jest, jasmine */ + +const detox = require('detox'); +const config = require('../../package.json').detox; +const adapter = require('detox/runners/jest/adapter'); + +jest.setTimeout(300000); +jasmine.getEnv().addReporter(adapter); + +beforeAll(async () => { + await detox.init(config, {initGlobals: false, launchApp: false}); +}); + +beforeEach(async () => { + await adapter.beforeEach(); +}); + +afterAll(async () => { + await adapter.afterAll(); + await detox.cleanup(); +}); diff --git a/example/e2e/sanityTest.spec.js b/example/e2e/sanityTest.spec.js new file mode 100644 index 000000000..211b8e5c6 --- /dev/null +++ b/example/e2e/sanityTest.spec.js @@ -0,0 +1,15 @@ +const {device, expect, element, by, waitFor} = require('detox'); + +describe('CameraRoll', () => { + beforeEach(async () => { + await device.launchApp({permissions: { + photos: 'YES', + // camera: 'YES', + }}); + }); + + it('should load example app with no errors and show all the examples by default', async () => { + await expect(element(by.text('Big Images'))).toExist(); + }); + +}); diff --git a/example/js/AssetScaledImageExample.js b/example/js/AssetScaledImageExample.js new file mode 100644 index 000000000..fb50653a1 --- /dev/null +++ b/example/js/AssetScaledImageExample.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const {Image, StyleSheet, View, ScrollView} = ReactNative; + +import type {PhotoIdentifier} from 'CameraRoll'; + +type Props = $ReadOnly<{| + asset: PhotoIdentifier, +|}>; + +type State = {| + asset: PhotoIdentifier, +|}; + +class AssetScaledImageExample extends React.Component { + state = { + asset: this.props.asset, + }; + + render() { + const image = this.state.asset.node.image; + return ( + + + + + + + + + + + + + + + ); + } +} + +const styles = StyleSheet.create({ + row: { + padding: 5, + flex: 1, + flexDirection: 'row', + alignSelf: 'center', + }, + imageWide: { + borderWidth: 1, + borderColor: 'black', + width: 320, + height: 240, + margin: 5, + }, + imageThumb: { + borderWidth: 1, + borderColor: 'black', + width: 100, + height: 100, + margin: 5, + }, + imageT1: { + borderWidth: 1, + borderColor: 'black', + width: 212, + height: 320, + margin: 5, + }, + imageT2: { + borderWidth: 1, + borderColor: 'black', + width: 100, + height: 320, + margin: 5, + }, +}); + +exports.title = ''; +exports.description = + 'Example component that displays the automatic scaling capabilities of the tag'; +module.exports = AssetScaledImageExample; diff --git a/example/js/CameraRollExample.js b/example/js/CameraRollExample.js new file mode 100644 index 000000000..5df8be508 --- /dev/null +++ b/example/js/CameraRollExample.js @@ -0,0 +1,148 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + CameraRoll, + Image, + Slider, + StyleSheet, + Switch, + Text, + View, + TouchableOpacity, +} = ReactNative; + +const invariant = require('invariant'); + +const CameraRollView = require('./CameraRollView'); + +const AssetScaledImageExampleView = require('./AssetScaledImageExample'); + +import type {PhotoIdentifier, GroupTypes} from 'CameraRoll'; + +type Props = $ReadOnly<{| + navigator?: ?Array< + $ReadOnly<{| + title: string, + component: Class>, + backButtonTitle: string, + passProps: $ReadOnly<{|asset: PhotoIdentifier|}>, + |}>, + >, +|}>; + +type State = {| + groupTypes: GroupTypes, + sliderValue: number, + bigImages: boolean, +|}; + +export default class CameraRollExample extends React.Component { + state = { + groupTypes: 'SavedPhotos', + sliderValue: 1, + bigImages: true, + }; + _cameraRollView: ?React.ElementRef; + render() { + return ( + + + {(this.state.bigImages ? 'Big' : 'Small') + ' Images'} + + {'Group Type: ' + this.state.groupTypes} + { + this._cameraRollView = ref; + }} + batchSize={20} + groupTypes={this.state.groupTypes} + renderImage={this._renderImage} + bigImages={this.state.bigImages} + /> + + ); + } + + loadAsset(asset) { + if (this.props.navigator) { + this.props.navigator.push({ + title: 'Camera Roll Image', + component: AssetScaledImageExampleView, + backButtonTitle: 'Back', + passProps: {asset: asset}, + }); + } + } + + _renderImage = (asset: PhotoIdentifier) => { + const imageSize = this.state.bigImages ? 150 : 75; + const imageStyle = [styles.image, {width: imageSize, height: imageSize}]; + const {location} = asset.node; + const locationStr = location + ? JSON.stringify(location) + : 'Unknown location'; + return ( + + + + + {asset.node.image.uri} + {locationStr} + {asset.node.group_name} + {new Date(asset.node.timestamp).toString()} + + + + ); + }; + + _onSliderChange = value => { + const options = Object.keys(CameraRoll.GroupTypesOptions); + const index = Math.floor(value * options.length * 0.99); + const groupTypes = options[index]; + if (groupTypes !== this.state.groupTypes) { + this.setState({groupTypes: groupTypes}); + } + }; + + _onSwitchChange = value => { + invariant(this._cameraRollView, 'ref should be set'); + this.setState({bigImages: value}); + }; +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + flex: 1, + }, + url: { + fontSize: 9, + marginBottom: 14, + }, + image: { + margin: 4, + }, + info: { + flex: 1, + }, +}); diff --git a/example/js/CameraRollView.js b/example/js/CameraRollView.js new file mode 100644 index 000000000..1b7276e0f --- /dev/null +++ b/example/js/CameraRollView.js @@ -0,0 +1,265 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + ActivityIndicator, + Alert, + CameraRoll, + Image, + FlatList, + PermissionsAndroid, + Platform, + StyleSheet, + View, +} = ReactNative; + +const groupByEveryN = require('groupByEveryN'); +const logError = require('logError'); + +import type { + PhotoIdentifier, + PhotoIdentifiersPage, + GetPhotosParams, +} from 'CameraRoll'; + +type Props = $ReadOnly<{| + /** + * The group where the photos will be fetched from. Possible + * values are 'Album', 'All', 'Event', 'Faces', 'Library', 'PhotoStream' + * and SavedPhotos. + */ + groupTypes: + | 'Album' + | 'All' + | 'Event' + | 'Faces' + | 'Library' + | 'PhotoStream' + | 'SavedPhotos', + + /** + * Number of images that will be fetched in one page. + */ + batchSize: number, + + /** + * A function that takes a single image as a parameter and renders it. + */ + renderImage: PhotoIdentifier => React.Node, + + /** + * imagesPerRow: Number of images to be shown in each row. + */ + imagesPerRow: number, + + /** + * A boolean that indicates if we should render large or small images. + */ + bigImages?: boolean, + + /** + * The asset type, one of 'Photos', 'Videos' or 'All' + */ + assetType: 'Photos' | 'Videos' | 'All', +|}>; + +type State = {| + assets: Array, + data: Array>, + seen: Set, + lastCursor: ?string, + noMore: boolean, + loadingMore: boolean, +|}; + +type Row = { + item: Array, +}; + +class CameraRollView extends React.Component { + static defaultProps = { + groupTypes: 'SavedPhotos', + batchSize: 5, + imagesPerRow: 1, + assetType: 'Photos', + renderImage: function(asset: PhotoIdentifier) { + const imageSize = 150; + const imageStyle = [styles.image, {width: imageSize, height: imageSize}]; + return ; + }, + }; + + state = this.getInitialState(); + + getInitialState() { + return { + assets: [], + data: [], + seen: new Set(), + lastCursor: null, + noMore: false, + loadingMore: false, + }; + } + + componentDidMount() { + this.fetch(); + } + + UNSAFE_componentWillReceiveProps(nextProps: Props) { + if (this.props.groupTypes !== nextProps.groupTypes) { + this.fetch(true); + } + } + + async _fetch(clear?: boolean) { + if (clear) { + this.setState(this.getInitialState(), this.fetch); + return; + } + + if (Platform.OS === 'android') { + const result = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, + { + title: 'Permission Explanation', + message: 'RNTester would like to access your pictures.', + }, + ); + if (result !== 'granted') { + Alert.alert('Access to pictures was denied.'); + return; + } + } + + const fetchParams: GetPhotosParams = { + first: this.props.batchSize, + groupTypes: this.props.groupTypes, + assetType: this.props.assetType, + }; + if (Platform.OS === 'android') { + // not supported in android + delete fetchParams.groupTypes; + } + + if (this.state.lastCursor) { + fetchParams.after = this.state.lastCursor; + } + + try { + const data = await CameraRoll.getPhotos(fetchParams); + this._appendAssets(data); + } catch (e) { + logError(e); + } + } + + /** + * Fetches more images from the camera roll. If clear is set to true, it will + * set the component to its initial state and re-fetch the images. + */ + fetch = (clear?: boolean) => { + if (!this.state.loadingMore) { + this.setState({loadingMore: true}, () => { + this._fetch(clear); + }); + } + }; + + render() { + return ( + String(idx)} + renderItem={this._renderItem} + ListFooterComponent={this._renderFooterSpinner} + onEndReached={this._onEndReached} + onEndReachedThreshold={0.2} + style={styles.container} + data={this.state.data || []} + extraData={this.props.bigImages + this.state.noMore} + /> + ); + } + + _renderFooterSpinner = () => { + if (!this.state.noMore) { + return ; + } + return null; + }; + + _renderItem = (row: Row) => { + return ( + + {row.item.map(image => (image ? this.props.renderImage(image) : null))} + + ); + }; + + _appendAssets(data: PhotoIdentifiersPage) { + const assets = data.edges; + const newState: $Shape = {loadingMore: false}; + + if (!data.page_info.has_next_page) { + newState.noMore = true; + } + + if (assets.length > 0) { + newState.lastCursor = data.page_info.end_cursor; + newState.seen = new Set(this.state.seen); + + // Unique assets efficiently + // Checks new pages against seen objects + const uniqAssets = []; + for (let index = 0; index < assets.length; index++) { + const asset = assets[index]; + let value = asset.node.image.uri; + if (newState.seen.has(value)) { + continue; + } + newState.seen.add(value); + uniqAssets.push(asset); + } + + newState.assets = this.state.assets.concat(uniqAssets); + newState.data = groupByEveryN( + newState.assets, + this.props.imagesPerRow, + ); + } + + this.setState(newState); + } + + _onEndReached = () => { + if (!this.state.noMore) { + this.fetch(); + } + }; +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + flex: 1, + }, + image: { + margin: 4, + }, + container: { + flex: 1, + }, +}); + +module.exports = CameraRollView;