UNPKG

fleetback-camera-roll-picker

Version:

A React Native component providing images selection from camera roll

334 lines (295 loc) 8.51 kB
import React, { Component } from 'react'; import { Platform, StyleSheet, View, Text, FlatList, ActivityIndicator, AppState, Linking, } from 'react-native'; import { CameraRoll } from "@react-native-camera-roll/camera-roll"; import {TextPropTypes} from 'deprecated-react-native-prop-types'; import PropTypes from 'prop-types'; import Row from './Row'; const styles = StyleSheet.create({ list: { flex: 1, }, wrapper: { flexGrow: 1, }, loader: { flexGrow: 1, justifyContent: 'center', alignItems: 'center', }, limitedText: { padding: 20, color: '#808080', fontSize: 18, textAlign: 'center', } }); // helper functions const arrayObjectIndexOf = (array, property, value) => array.map(o => o[property]).indexOf(value); const nEveryRow = (data, n) => { const result = []; let temp = []; for (let i = 0; i < data.length; ++i) { if (i > 0 && i % n === 0) { result.push(temp); temp = []; } temp.push(data[i]); } if (temp.length > 0) { while (temp.length !== n) { temp.push(null); } result.push(temp); } return result; }; // Set to `true` if you have <key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key><true/> in your Info.plist, // set to false otherwise let didRefreshForLimitedAccess = false; // needs to be reset on each app launch function getDefaultState(props) { return { images: [], selected: props.selected, lastCursor: null, initialLoading: true, loadingMore: false, noMore: false, data: [], limited: false, }; } class CameraRollPicker extends Component { state = getDefaultState(this.props); appState = 'unknown'; appStateChangeEventListener; componentDidMount() { this.fetch(); didRefreshForLimitedAccess = this.props.PHPhotoLibraryPreventAutomaticLimitedAccessAlert; this.appStateChangeEventListener = AppState.addEventListener('change', this.appStateListener); } componentWillUnmount() { this.appStateChangeEventListener?.remove(); } UNSAFE_componentWillReceiveProps(nextProps) { this.setState({ selected: nextProps.selected, }); } appStateListener = (newState) => { if (this.appState !== newState) { if (this.appState === 'background' && newState === 'active') { // The user went from background (possibly the settings app) and back to the app // The the limited photos available may have changed (iOS), we should fetch them again from the start this.refreshPhotosDataForLimited(); } this.appState = newState; } }; onEndReached = () => { if (!this.state.noMore) { this.fetch(); } } appendImages = (data) => { let assets = data.edges; const newState = { loadingMore: false, initialLoading: false, limited: data.limited, }; if (!data.page_info.has_next_page) { newState.noMore = true; } if (assets.length > 0) { if (this.props.assetFilter) { assets = assets.filter(this.props.assetFilter); } newState.lastCursor = data.page_info.end_cursor; newState.images = this.state.images.concat(assets); newState.data = nEveryRow(newState.images, this.props.imagesPerRow); } this.setState(newState); } fetch = () => { if (!this.state.loadingMore) { this.setState({ loadingMore: true }); this.doFetch(); } } doFetch = () => { const { groupTypes, assetType } = this.props; const fetchParams = { first: 100, groupTypes: Platform.OS === 'android' ? undefined : groupTypes, assetType, include: ['filename', 'imageSize', 'playableDuration'] }; if (this.state.lastCursor) { fetchParams.after = this.state.lastCursor; } CameraRoll.getPhotos(fetchParams).then(this.appendImages).catch(console.warn); } selectImage = (image) => { const { maximum, imagesPerRow, callback, selectSingleItem, } = this.props; const { selected } = this.state; const index = arrayObjectIndexOf(selected, 'uri', image.uri); if (index >= 0) { selected.splice(index, 1); } else { if (selectSingleItem) { selected.splice(0, selected.length); } if (selected.length < maximum) { selected.push(image); } } this.setState({ selected, data: nEveryRow(this.state.images, imagesPerRow), }); callback(selected, image); } refreshPhotosDataForLimited = () => { const state = getDefaultState(this.props); didRefreshForLimitedAccess = true; this.setState(state, this.doFetch); } renderRow = ({ item, index }) => { // item is an array of objects const isSelected = item.map((imageItem) => { if (!imageItem) return false; const { uri } = imageItem.node.image; return arrayObjectIndexOf(this.state.selected, 'uri', uri) >= 0; }); return ( <Row rowData={item} isSelected={isSelected} selectImage={this.selectImage} imagesPerRow={this.props.imagesPerRow} imageMargin={this.props.imageMargin} selectedMarker={this.props.selectedMarker} rowIndex={index} /> ); } renderFooterSpinner = () => { if (!this.state.noMore) { return <ActivityIndicator style={styles.spinner} />; } if (this.state.limited) { if (didRefreshForLimitedAccess) { return ( <Text style={[styles.limitedText, this.props.limitedTextStyle]} onPress={() => Linking.openSettings()}> {this.props.limitedAccessText} </Text> ); } else { return ( <Text style={[styles.limitedText, this.props.limitedTextStyle]} onPress={this.refreshPhotosDataForLimited}> {this.props.limitedAccessRefreshText} </Text> ); } } return null; } render() { const { initialNumToRender, imageMargin, backgroundColor, emptyText, emptyTextStyle, loader, } = this.props; if (this.state.initialLoading) { return ( <View style={[styles.loader, { backgroundColor }]}> { loader || <ActivityIndicator /> } </View> ); } return ( <View style={[styles.wrapper, { padding: imageMargin, paddingRight: 0, backgroundColor }]}> <FlatList style={styles.list} contentContainerStyle={this.props.contentContainerStyle} ListFooterComponent={this.renderFooterSpinner} ListEmptyComponent={( <Text style={[{ textAlign: 'center' }, emptyTextStyle]}>{emptyText}</Text> )} initialNumToRender={initialNumToRender} onEndReached={this.onEndReached} renderItem={this.renderRow} keyExtractor={item => item[0].node.image.uri} data={this.state.data} extraData={this.state.selected} /> </View> ); } } CameraRollPicker.propTypes = { initialNumToRender: PropTypes.number, groupTypes: PropTypes.oneOf([ 'Album', 'All', 'Event', 'Faces', 'Library', 'PhotoStream', 'SavedPhotos', ]), maximum: PropTypes.number, assetType: PropTypes.oneOf([ 'Photos', 'Videos', 'All', ]), assetFilter: PropTypes.func, selectSingleItem: PropTypes.bool, imagesPerRow: PropTypes.number, imageMargin: PropTypes.number, callback: PropTypes.func, selected: PropTypes.array, selectedMarker: PropTypes.element, backgroundColor: PropTypes.string, emptyText: PropTypes.string, emptyTextStyle: TextPropTypes.style, limitedAccessText: PropTypes.string, limitedAccessRefreshText: PropTypes.string, limitedTextStyle: TextPropTypes.style, loader: PropTypes.node, contentContainerStyle: PropTypes.any, }; CameraRollPicker.defaultProps = { initialNumToRender: 5, groupTypes: 'SavedPhotos', maximum: 15, imagesPerRow: 3, imageMargin: 5, selectSingleItem: false, assetType: 'Photos', backgroundColor: 'white', selected: [], callback(selectedImages, currentImage) { console.log(currentImage); console.log(selectedImages); }, PHPhotoLibraryPreventAutomaticLimitedAccessAlert: false, emptyText: 'No photos.', limitedAccessText: 'Access is limited. Tap here to access Settings and change the pictures the app is allowed to use.', limitedAccessRefreshText: 'Access is limited. Tap here to refresh the selection you made.', }; export default CameraRollPicker;