diff --git a/android/app/build.gradle b/android/app/build.gradle index d88f4ac..8edcc31 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -131,7 +131,7 @@ android { applicationId "com.aretherecookies" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 103 + versionCode 104 versionName "1.0" } splits { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index acc37e9..2353538 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,7 +4,6 @@ - 3.1.0) - GooglePlaces (~> 3.1.0) - React - - react-native-image-picker (1.1.0): - - React - react-native-maps (0.25.0): - React - React-RCTActionSheet (0.60.5): @@ -98,10 +97,18 @@ PODS: - React-Core (= 0.60.5) - React-RCTWebSocket (0.60.5): - React-Core (= 0.60.5) + - RNCAsyncStorage (1.6.2): + - React + - RNImageCropPicker (0.25.2): + - QBImagePickerController + - React-Core + - React-RCTImage + - RSKImageCropper - RNSnackbar (2.0.2): - React - RNVectorIcons (6.6.0): - React + - RSKImageCropper (2.2.3) - yoga (0.60.5.React) DEPENDENCIES: @@ -118,7 +125,6 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" - react-native-google-places (from `../node_modules/react-native-google-places`) - - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-maps (from `../node_modules/react-native-maps`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) @@ -130,6 +136,8 @@ DEPENDENCIES: - React-RCTText (from `../node_modules/react-native/Libraries/Text`) - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - React-RCTWebSocket (from `../node_modules/react-native/Libraries/WebSocket`) + - "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)" + - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNSnackbar (from `../node_modules/react-native-snackbar`) - RNVectorIcons (from `../node_modules/react-native-vector-icons`) - yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -139,6 +147,8 @@ SPEC REPOS: - boost-for-react-native - GoogleMaps - GooglePlaces + - QBImagePickerController + - RSKImageCropper EXTERNAL SOURCES: A0Auth0: @@ -167,8 +177,6 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/geolocation" react-native-google-places: :path: "../node_modules/react-native-google-places" - react-native-image-picker: - :path: "../node_modules/react-native-image-picker" react-native-maps: :path: "../node_modules/react-native-maps" React-RCTActionSheet: @@ -191,6 +199,10 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/Libraries/Vibration" React-RCTWebSocket: :path: "../node_modules/react-native/Libraries/WebSocket" + RNCAsyncStorage: + :path: "../node_modules/@react-native-community/async-storage" + RNImageCropPicker: + :path: "../node_modules/react-native-image-crop-picker" RNSnackbar: :path: "../node_modules/react-native-snackbar" RNVectorIcons: @@ -206,6 +218,7 @@ SPEC CHECKSUMS: glog: 1f3da668190260b06b429bb211bfbee5cd790c28 GoogleMaps: 5c13302e6fe6bb6e686b267196586b91cd594225 GooglePlaces: e874db179f2675c4f3eeda0b686b540273a578b0 + QBImagePickerController: d54cf93db6decf26baf6ed3472f336ef35cae022 React: 53c53c4d99097af47cf60594b8706b4e3321e722 React-Core: ba421f6b4f4cbe2fb17c0b6fc675f87622e78a64 React-cxxreact: 8384287780c4999351ad9b6e7a149d9ed10a2395 @@ -215,7 +228,6 @@ SPEC CHECKSUMS: React-jsinspector: e08662d1bf5b129a3d556eb9ea343a3f40353ae4 react-native-geolocation: a7b94614afbd5fd8350e0233a2025c8228fc8041 react-native-google-places: 34e976a0e13cb55cc9603ef2fa6764ed534af806 - react-native-image-picker: 7a85cf7b0a53845f03ae52fb4592a2748ded069b react-native-maps: 190c02ca533fddac5bb49cf17bdece3644612107 React-RCTActionSheet: b0f1ea83f4bf75fb966eae9bfc47b78c8d3efd90 React-RCTAnimation: 359ba1b5690b1e87cc173558a78e82d35919333e @@ -227,10 +239,13 @@ SPEC CHECKSUMS: React-RCTText: b074d89033583d4f2eb5faf7ea2db3a13c7553a2 React-RCTVibration: 2105b2e0e2b66a6408fc69a46c8a7fb5b2fdade0 React-RCTWebSocket: cd932a16b7214898b6b7f788c8bddb3637246ac4 + RNCAsyncStorage: 60a80e72d95bf02a01cace55d3697d9724f0d77f + RNImageCropPicker: f675353bbe18f66113a39b319c0aeb36655a6e4c RNSnackbar: f6d5a0f66f5e75794e1d94b62827b78ce37bd530 RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4 + RSKImageCropper: a446db0e8444a036b34f3c43db01b2373baa4b2a yoga: 312528f5bbbba37b4dcea5ef00e8b4033fdd9411 -PODFILE CHECKSUM: 702016785759f22e0f5c7560c54f1797f93efccf +PODFILE CHECKSUM: ee2999c6a9ba049d4cee2b2a6ec09d970f38352a COCOAPODS: 1.8.0.beta.2 diff --git a/ios/aretherecookies.xcodeproj/project.pbxproj b/ios/aretherecookies.xcodeproj/project.pbxproj index e3a7ac4..1817894 100644 --- a/ios/aretherecookies.xcodeproj/project.pbxproj +++ b/ios/aretherecookies.xcodeproj/project.pbxproj @@ -518,6 +518,7 @@ "${PODS_ROOT}/Target Support Files/Pods-aretherecookies/Pods-aretherecookies-resources.sh", "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", "${PODS_ROOT}/GooglePlaces/Frameworks/GooglePlaces.framework/Resources/GooglePlaces.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/QBImagePickerController/QBImagePicker.bundle", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf", @@ -534,11 +535,13 @@ "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf", + "${PODS_ROOT}/RSKImageCropper/RSKImageCropper/RSKImageCropperStrings.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GooglePlaces.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf", @@ -555,6 +558,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RSKImageCropperStrings.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/ios/aretherecookies/Info.plist b/ios/aretherecookies/Info.plist index 8abbaa2..bf2296d 100644 --- a/ios/aretherecookies/Info.plist +++ b/ios/aretherecookies/Info.plist @@ -38,7 +38,7 @@ NSLocationWhenInUseUsageDescription - + UIAppFonts MaterialIcons.ttf @@ -75,6 +75,8 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSPhotoLibraryUsageDescription + Photos can be uploaded to create new food items for business you love! UIViewControllerBasedStatusBarAppearance diff --git a/js/apis/ImagesApi.js b/js/apis/ImagesApi.js index 44fe327..78e1d39 100644 --- a/js/apis/ImagesApi.js +++ b/js/apis/ImagesApi.js @@ -1,37 +1,37 @@ // @flow -import type { ImageRaw } from "../records/ImageRecord"; -import { fetchRequest, fetchRequestBinary } from "./FetchApi"; +import type { ImageRaw } from '../records/ImageRecord'; +import { fetchRequest, fetchRequestBinary } from './FetchApi'; export const getImages = (foodItemId: string): Promise> => { return fetchRequest({ endpoint: `/images/${foodItemId}`, - method: "GET" + method: 'GET', }); }; export const addImage = async ({ foodItemId, username, - imageUri + imageUri, }: { foodItemId: string, username: string, - imageUri: string + imageUri: string, }) => { const body = new FormData(); // $FlowFixMe - react-native does different stuff with FormData - body.append("photo", { + body.append('photo', { uri: imageUri, - type: "image/jpeg", - name: "photo.jpg" + type: 'image/jpeg', + name: 'photo.jpg', }); - body.append("username", username); + body.append('username', username); const res = await fetchRequestBinary({ endpoint: `/images/${foodItemId}`, - body + body, }); if (!res.ok) { diff --git a/js/enhancers/createFoodItemEnhancers.js b/js/enhancers/createFoodItemEnhancers.js index d0b7ad1..2b0b191 100644 --- a/js/enhancers/createFoodItemEnhancers.js +++ b/js/enhancers/createFoodItemEnhancers.js @@ -19,7 +19,11 @@ export const withCreateFoodItemState = mapPropsStream(props$ => { // insert new item into db and cast it into FoodItemRecord const newItem = buildFoodItem(await createFoodItem(foodItem)); - Snackbar.show({ title: foodItem.name + ' added' }); + Snackbar.show({ + title: foodItem.name + ' added', + backgroundColor: 'black', + color: 'white', + }); // notify food items state of new item emitFoodItemsState(newItem); @@ -32,7 +36,12 @@ export const withCreateFoodItemState = mapPropsStream(props$ => { } catch (err) { const error = formatError(err); - Snackbar.show({ title: error, duration: Snackbar.LENGTH_LONG }); + Snackbar.show({ + title: error, + duration: Snackbar.LENGTH_LONG, + backgroundColor: 'black', + color: 'white', + }); throw error; } diff --git a/js/helpers/ImagePickerHelpers.js b/js/helpers/ImagePickerHelpers.js index 5f70284..a3fb724 100644 --- a/js/helpers/ImagePickerHelpers.js +++ b/js/helpers/ImagePickerHelpers.js @@ -1,13 +1,11 @@ // @flow -import * as ReactNativeImagePicker from 'react-native-image-picker'; +import ImagePicker from 'react-native-image-crop-picker'; const IMAGE_MAX_WIDTH = 512; -const IMAGE_QUALITY = 0.8; +const IMAGE_MAX_HEIGHT = 384; export const openImagePicker = () => - new Promise((resolve, reject) => - ReactNativeImagePicker.showImagePicker( - { maxWidth: IMAGE_MAX_WIDTH, quality: IMAGE_QUALITY }, - ({ didCancel, error, uri }) => (!didCancel && !error ? resolve(uri) : reject(error)) - ) - ); + ImagePicker.openPicker({ + width: IMAGE_MAX_WIDTH, + height: IMAGE_MAX_HEIGHT, + }); diff --git a/js/pages/CreateFoodItem.js b/js/pages/CreateFoodItem.js index 3d24fa9..959c52c 100644 --- a/js/pages/CreateFoodItem.js +++ b/js/pages/CreateFoodItem.js @@ -39,7 +39,7 @@ type Props = { place: ?PlaceRecord, updatePlace: (place: GooglePlaceObject) => void, addImage: (uri: string) => void, - setImagePreview: (index?: number) => void, + setImagePreview: (imageURI?: string) => void, loading: boolean, setLoading: (arg: boolean) => void, emitPlace: (place: Object) => void, @@ -116,8 +116,8 @@ const CreateFoodItem = (props: Props) => { placeholder="Quantity" /> - {foodItem.images.map((image, index) => ( - setImagePreview(index)} /> + {foodItem.images.map((imageURI: string) => ( + setImagePreview(imageURI)} /> ))} @@ -131,7 +131,7 @@ const ImagePreviewComp = compose( onClose: () => setImagePreview(-1), onDelete: () => { setFoodItem(foodItem.deleteIn(['images', imagePreview])); - setImagePreview(-1); + setImagePreview(null); }, imageSrc: foodItem.images.get(imagePreview), }; @@ -143,11 +143,11 @@ const setPropOfFoodItem = ({ foodItem, setFoodItem }: Props) => (prop: string) = }; const addImage = ({ foodItem, setFoodItem }: Props) => async () => { - const uri = await openImagePicker(); + const { path } = await openImagePicker(); Snackbar.show({ title: 'Photo added.', }); - setFoodItem(foodItem.update('images', images => images.add(uri))); + setFoodItem(foodItem.update('images', images => images.add(path))); }; const updatePlace = ({ foodItem, setFoodItem, emitPlace }: Props) => placeDetails => { @@ -175,7 +175,7 @@ export default compose( withPlaceForFoodItem, withPlaceActions, withState('nameModalOpen', 'setNameModalOpen', false), - withState('imagePreview', 'setImagePreview', -1), + withState('imagePreview', 'setImagePreview', null), withState('categoryModalOpen', 'setCategoryModalOpen', false), withState('quantityModalOpen', 'setQuantityModalOpen', false), withHandlers({ @@ -211,7 +211,7 @@ export default compose( onUpdateProp: 'setName', }) ), - branch(({ imagePreview }) => imagePreview > -1, ImagePreviewComp), + branch(({ imagePreview }) => !!imagePreview, ImagePreviewComp), branch( ({ categoryModalOpen }) => !!categoryModalOpen, wrapModalComponent({ diff --git a/js/pages/FoodItemDetail.js b/js/pages/FoodItemDetail.js index a308dc2..10420cd 100644 --- a/js/pages/FoodItemDetail.js +++ b/js/pages/FoodItemDetail.js @@ -60,7 +60,11 @@ const FoodItemImages = ({ )} {visibleImages.size > 1 && ( - + {visibleImages.map(image => ( { return ; } - if (quantityModalOpen) { - return ( - - ); - } - return ( @@ -188,6 +186,9 @@ export const FoodItemDetail = (props: Props) => { /> )} + {quantityModalOpen && ( + + )} ); }; @@ -208,18 +209,26 @@ export default compose( if (!foodItem) { return; } - const imageUri = await openImagePicker(); + const { path: imageUri } = await openImagePicker(); setImagesLoading(true); await addImage({ foodItemId: foodItem.id, imageUri }); setImagesLoading(false); - setTimeout(() => Snackbar.show({ title: 'Food updated.' }), 700); + setTimeout( + () => + Snackbar.show({ + title: 'Food updated.', + backgroundColor: 'black', + color: 'white', + }), + 700 + ); }, updateAmount: ({ updateQuantity, foodItem }: Props) => async (quantity: Quantity) => { if (!foodItem) { return; } await updateQuantity({ foodItemId: foodItem.id, quantity }); - Snackbar.show({ title: 'Food updated.' }); + Snackbar.show({ title: 'Food updated.', backgroundColor: 'black', color: 'white' }); }, toggleQuantityModal: ({ quantityModalOpen, setQuantityModalOpen }) => () => { setQuantityModalOpen(!quantityModalOpen); diff --git a/js/pages/FoodMap.js b/js/pages/FoodMap.js index bcc10ba..45cb89e 100644 --- a/js/pages/FoodMap.js +++ b/js/pages/FoodMap.js @@ -28,13 +28,15 @@ type Props = { }; const FoodMap = ({ foodItemsMap, region, onRegionChange, pushRoute, initialRegion }: Props) => { - return !foodItemsMap ? null : ( + if (!foodItemsMap) { + return null; + } + return ( + onRegionChangeComplete={onRegionChange}> {foodItemsMap .map((foodItem, id) => { return ( diff --git a/js/pages/Nav.js b/js/pages/Nav.js index 4066e43..8049efd 100644 --- a/js/pages/Nav.js +++ b/js/pages/Nav.js @@ -34,30 +34,55 @@ const Nav = (props: Props) => { icon="local-pizza" label="Food" onPress={setRoute('/list/food')} + style={{ + container: { + minWidth: 40, + }, + }} /> diff --git a/js/pages/ProfilePage.js b/js/pages/ProfilePage.js index c0a99d4..bc4ce82 100644 --- a/js/pages/ProfilePage.js +++ b/js/pages/ProfilePage.js @@ -109,7 +109,7 @@ export default compose( await AsyncStorage.setItem('zipcode', props.zipcode); props.setLoading(false); getLocation(); - Snackbar.show({ title: 'Zipcode updated.' }); + Snackbar.show({ title: 'Zipcode updated.', backgroundColor: 'black', color: 'white' }); }, }), withHandlers({ diff --git a/js/records/ImageRecord.js b/js/records/ImageRecord.js index d80aa8b..45bf698 100644 --- a/js/records/ImageRecord.js +++ b/js/records/ImageRecord.js @@ -1,5 +1,5 @@ //@flow -import { Record } from 'immutable'; +import { Record, OrderedSet } from 'immutable'; export type ImageRaw = { url: string, @@ -15,7 +15,7 @@ const ImageRecord = Record({ foodItemId: '', }); -export type ImageFragment = { id: string, images: Set }; +export type ImageFragment = { id: string, images: OrderedSet }; export const buildImageRecord = (imageRaw: ImageRaw) => { return new ImageRecord({ diff --git a/js/streams/ImagesStream.js b/js/streams/ImagesStream.js index fd28697..c5020c0 100644 --- a/js/streams/ImagesStream.js +++ b/js/streams/ImagesStream.js @@ -1,10 +1,10 @@ //@flow -import { Map, Set } from 'immutable'; +import { Map, OrderedSet } from 'immutable'; import { ReplaySubject } from 'rxjs'; -import ImageRecord, { buildImageRecord } from '../records/ImageRecord'; +import { buildImageRecord, type ImageFragment } from '../records/ImageRecord'; import type { ImageRaw } from '../records/ImageRecord'; -const observable: ReplaySubject = new ReplaySubject(); +const observable: ReplaySubject = new ReplaySubject(); export function emit(val: ?ImageRaw) { observable.next(val); @@ -13,15 +13,18 @@ export function emit(val: ?ImageRaw) { // force our observable to emit an initial empty map so that food items will load emit(null); -export default observable.scan((imagesByFoodItemId: Map, image: ImageRaw) => { - if (!image || !image.food_item_id) { - return imagesByFoodItemId; - } +export default observable.scan( + (imagesByFoodItemId: Map, image: ImageRaw) => { + if (!image || !image.food_item_id) { + return imagesByFoodItemId; + } - return imagesByFoodItemId.update(image.food_item_id, ({ images = Set() } = {}) => { - return { - id: image.food_item_id, - images: images.add(buildImageRecord(image)), - }; - }); -}, new Map()); + return imagesByFoodItemId.update(image.food_item_id, ({ images = OrderedSet() } = {}) => { + return { + id: image.food_item_id, + images: images.add(buildImageRecord(image)), + }; + }); + }, + new Map() +); diff --git a/package.json b/package.json index 395020d..32002a4 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "react-native-auth0": "^1.5.0", "react-native-geolocation-service": "^3.1.0", "react-native-google-places": "^3.1.2", - "react-native-image-picker": "^1.1.0", + "react-native-image-crop-picker": "^0.25.2", "react-native-loading-spinner-overlay": "^1.0.1", "react-native-looped-carousel": "^0.1.13", "react-native-maps": "^0.25.0", diff --git a/yarn.lock b/yarn.lock index 14da4dc..e2483b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5775,10 +5775,10 @@ react-native-google-places@^3.1.2: resolved "https://registry.yarnpkg.com/react-native-google-places/-/react-native-google-places-3.1.2.tgz#6aa11f76da6bd58aa9ec1d1b3e2c85edb4ced2ce" integrity sha512-aKPbGHga3k/Vkfw9NMf/1t0sivdqxn4qzCbEyAfmhxO7Mlqv9Nljjnez6rp6MZGwh89ss3/a9lhM4ulyZ6o0ew== -react-native-image-picker@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-1.1.0.tgz#c2a0523886edb4cf2cdc008ad0a71433142a4d53" - integrity sha512-/KjHf4NNAjl6XM7FQuqvGDz1wB9sRdLf86+2yksLW/QTRR7CitX4TLCM8ZF9CX6Y0MsCTndkZia3zWE+nt/GiA== +react-native-image-crop-picker@^0.25.2: + version "0.25.2" + resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.25.2.tgz#241e598e6bd601248c319483c19508f9bc9a203e" + integrity sha512-LZX9gJb5q1q6Oo1jXj1FyNEEDkvfynZMeE3M9jhFjJdvknMf/eui7GlmSokzQ4A1MLxKmcLSVDGw2L1HTBAO6A== react-native-loading-spinner-overlay@^1.0.1: version "1.0.1"