fleetback-camera-roll-picker
Version:
A React Native component providing images selection from camera roll
334 lines (295 loc) • 8.51 kB
JavaScript
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;