react-native-camera-roll-gallery
Version:
An easy and simple to use React Native component to render a custom layout for CameraRoll photos (next update will have videos) and displayed on a custom interactive image viewer for manipulation. Includes animations and support for both iOS and Android.
449 lines (399 loc) • 11.3 kB
JavaScript
import React from "react";
import {
PermissionsAndroid,
Platform,
StyleSheet,
FlatList,
View,
Text,
ActivityIndicator
} from "react-native";
import { findUri } from "../utils";
import PropTypes from "prop-types";
import ImageCell from "./ImageCell";
export default class CameraRollBrowser extends React.PureComponent {
static propTypes = {
enableCameraRoll: PropTypes.bool,
onGetData: PropTypes.func,
itemCount: PropTypes.number,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
imagesPerRow: PropTypes.number,
initialNumToRender: PropTypes.number,
removeClippedSubviews: PropTypes.bool,
cameraRollFlatListProps: PropTypes.object,
catchGetPhotosError: PropTypes.func,
groupTypes: PropTypes.oneOf([
"Album",
"All",
"Event",
"Faces",
"Library",
"PhotoStream",
"SavedPhotos",
]),
assetType: PropTypes.oneOf([
"Photos",
"Videos",
"All",
]),
imageMargin: PropTypes.number,
containerWidth: PropTypes.number,
backgroundColor: PropTypes.string,
emptyText: PropTypes.string,
emptyTextStyle: Text.propTypes.style,
loader: PropTypes.node,
cameraRollListHeader: PropTypes.func,
cameraRollListFooter: PropTypes.func,
imageContainerStyle: PropTypes.object,
renderIndividualHeader: PropTypes.func,
renderIndividualFooter: PropTypes.func,
onEndReached: PropTypes.func,
onEndReachedThreshold: PropTypes.number,
openImageViewer: PropTypes.func.isRequired,
loaderColor: PropTypes.string,
permissionDialogTitle: PropTypes.string,
permissionDialogMessage: PropTypes.string,
pendingAuthorizedView: PropTypes.oneOfType([
PropTypes.node,
// PropTypes.func
]),
notAuthorizedView: PropTypes.oneOfType([
PropTypes.node,
// PropTypes.func
]),
keyExtractor: PropTypes.func,
setMainState: PropTypes.func.isRequired,
setAssets: PropTypes.func.isRequired,
totalCount: PropTypes.number.isRequired,
loadingMore: PropTypes.bool.isRequired,
noMore: PropTypes.bool.isRequired
}
constructor(props) {
super(props);
this.state = {
lastCursor: null,
permissionGranted: !this.props.enableCameraRoll
? "granted" : Platform.OS === "ios"
? "granted" : "denied",
initialLoading: true,
};
}
componentDidMount = async () => {
if (this.props.enableCameraRoll) {
if (Platform.OS === "android") {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
{
"title": this.props.permissionDialogTitle,
"message": this.props.permissionDialogMessage
}
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
this.setState({
permissionGranted: "granted"
});
this.fetch();
} else if (granted === PermissionsAndroid.RESULTS.DENIED) {
this.setState({
permissionGranted: "denied"
});
} else if (granted === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
this.setState({
permissionGranted: "never_ask_again"
});
}
}
if (Platform.OS === "ios") {
this.fetch();
}
} else {
this.fetch();
}
}
fetch = (ref = false) => {
if (!this.props.loadingMore) {
this.props.setMainState({ loadingMore: true });
this._fetch(ref);
}
}
_fetch = (ref = false) => {
var {
totalCount,
groupTypes,
assetType
} = this.props;
var itemCount = this.props.itemCount;
var fetchParams = {
first: totalCount + itemCount,
groupTypes: groupTypes,
assetType: assetType,
};
if (Platform.OS === "android") {
// not supported in android
delete fetchParams.groupTypes;
}
if (this.state.lastCursor) {
fetchParams.after = this.state.lastCursor;
}
if (this.props.enableCameraRoll) {
this._fetchImages(fetchParams, ref);
} else {
this._fetchRemoteImages({
previousCount: totalCount,
itemCount: this.props.itemCount,
groupTypes: fetchParams.groupTypes,
assetType: fetchParams.assetType
});
}
}
_fetchImages = async (fetchParams, ref = false) => {
var { totalCount } = this.props;
var {
catchGetPhotosError
} = this.props;
var newMainState = {
loadingMore: false,
};
var newState = {
initialLoading: false,
};
var data = await new Promise((resolve) => {
var CameraRoll;
if (parseFloat(require("react-native/package.json").version) >= 0.6) {
CameraRoll = require("@react-native-community/cameraroll");
} else {
CameraRoll = require("react-native").CameraRoll;
}
CameraRoll.getPhotos(fetchParams)
.then((data) => {
const images = data.edges;
const parseData = images
.slice(totalCount)
.map((item) => {
return {
...item.node,
...item.node.image
};
});
resolve({
assets: parseData,
page_info: data.page_info
});
})
.catch((e) => {
catchGetPhotosError &&
catchGetPhotosError(e);
});
});
if (typeof data === "object") {
var assets = data.assets;
if (assets.length > 0) {
var extractedData = assets
.map((asset, index) => {
return {
index: index,
id: Math.random().toString(36).substring(7),
source: {
uri: asset.uri
},
uri: asset.uri,
...asset,
};
});
newMainState.totalCount = totalCount + extractedData.length;
this.props.setAssets(extractedData);
}
}
// newState.lastCursor = data.page_info.end_cursor;
if (!data.page_info.has_next_page) {
newMainState.noMore = true;
}
this.props.setMainState(newMainState);
if (!ref) {
this.setState(newState);
}
}
_fetchRemoteImages = async (fetchParams, ref = false) => {
var { totalCount } = this.props;
var data;
var newMainState = {
loadingMore: false,
};
var newState = {
initialLoading: false,
};
if (this.props.onGetData) {
data = await new Promise((resolve) => {
this.props.onGetData(fetchParams, resolve);
});
}
if (typeof data === "object") {
var assets = data.assets;
if (assets && assets.length > 0) {
var extractedData = assets
.filter((asset) => findUri(asset) ? true : false)
.map((asset, index) => {
const source = findUri(asset) ? findUri(asset) : "";
if (source) {
return {
index: index,
id: Math.random().toString(36).substring(7),
...asset,
source: source,
uri: source.uri
};
}
});
newMainState.totalCount = totalCount + extractedData.length;
this.props.setAssets(extractedData);
}
}
if (
!data.pageInfo
|| (data.pageInfo && !data.pageInfo.hasNextPage)
) {
newMainState.noMore = true;
}
this.props.setMainState(newMainState);
if (!ref) {
this.setState(newState);
}
}
render() {
var {
images,
imagesPerRow,
initialNumToRender,
removeClippedSubviews,
cameraRollFlatListProps,
backgroundColor,
emptyText,
emptyTextStyle,
loader,
loaderColor,
pendingAuthorizedView,
notAuthorizedView,
} = this.props;
if (this.state.permissionGranted === "denied") {
this._renderData([{
customElement: pendingAuthorizedView,
text: "Waiting on access permission to camera roll.",
}]);
}
if (this.state.permissionGranted === "never_ask_again") {
this._renderData([{
customElement: notAuthorizedView,
text: "Access denied to camera roll.",
}]);
}
if (this.state.initialLoading) {
return (
<View style={[styles.loader, {backgroundColor}]}>
{ loader || <ActivityIndicator size="large" color={loaderColor} style={styles.spinner} /> }
</View>
);
}
var flatListOrEmptyText = images.length > 0
? <FlatList
style={{flex: 1}}
numColumns={imagesPerRow}
initialNumToRender={initialNumToRender}
removeClippedSubviews={removeClippedSubviews}
ListHeaderComponent={this.props.cameraRollListHeader}
ListFooterComponent={this.props.cameraRollListFooter}
onEndReached={() => {
this._onEndReached();
this.props.onEndReached &&
this.props.onEndReached();
}}
onEndReachedThreshold={this.props.onEndReachedThreshold}
{...cameraRollFlatListProps}
data={images}
renderItem={this._renderImage}
keyExtractor={(item, index) => "imageCell#" + index.toString()}
/>
: this._renderData([{
text: emptyText,
textStyle: emptyTextStyle,
}]);
return (
<View
style={[styles.wrapper, {paddingRight: 0, backgroundColor: backgroundColor},]}>
{flatListOrEmptyText}
</View>
);
}
_renderData = (data) => {
return (
<FlatList
style={[styles.error, { backgroundColor: this.props.backgroundColor}]}
ListHeaderComponent={this.props.cameraRollListHeader}
ListFooterComponent={this.props.cameraRollListFooter}
data={data}
renderItem={this._renderError}
keyExtractor={(item, index) => {
if (this.props.keyExtractor) {
this.props.keyExtractor();
} else {
index.toString();
}
}}
/>
);
}
_renderImage = ({ item, index }) => {
var {
imageMargin,
imagesPerRow,
containerWidth,
imageContainerStyle,
renderIndividualHeader,
renderIndividualFooter,
openImageViewer
} = this.props;
return (
<ImageCell
index={index}
data={item}
imageId={item.id}
source={{ uri: item.uri }}
imageMargin={imageMargin}
imagesPerRow={imagesPerRow}
containerWidth={containerWidth}
imageContainerStyle={imageContainerStyle}
renderIndividualHeader={renderIndividualHeader}
renderIndividualFooter={renderIndividualFooter}
onPressImage={openImageViewer}
/>
);
}
_renderError = ({ item }) => {
const {
customElement,
textStyle,
text,
} = item;
return (
customElement ||
<Text style={[{textAlign: "center"}, textStyle]}>{text}</Text>
);
}
_onEndReached = () => {
if (!this.props.noMore) {
this.fetch();
}
}
}
const styles = StyleSheet.create({
wrapper:{
flexGrow: 1
},
loader: {
flexGrow: 1,
justifyContent: "center",
alignItems: "center",
},
error: {
flexGrow: 1,
}
});