From 766ab5465771c086d65677324f33eded229777e7 Mon Sep 17 00:00:00 2001 From: Armaan Ahluwalia Date: Mon, 8 Oct 2018 12:28:52 +0530 Subject: [PATCH 1/6] ComposeMenu: Extract default filename for new image picker. This commit adds a function to extract the filename from an image uri. This will be useful in the following commit when we switch to a new image picker library. On android, the filename is currently not returned by the react-native-image-crop-picker so we edit chooseUploadImageFilename to be able to infer the filename from the uri instead. --- src/compose/ComposeMenu.js | 11 +++++++++-- src/compose/__tests__/ComposeMenu-test.js | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/compose/ComposeMenu.js b/src/compose/ComposeMenu.js index b482ad64ee0..0b4ef0b3346 100644 --- a/src/compose/ComposeMenu.js +++ b/src/compose/ComposeMenu.js @@ -18,6 +18,11 @@ type Props = { onExpandContract: () => void, }; +/* +* Extract the image name from its uri in case the fileName is empty. +*/ +export const getDefaultFilenameFromUri = (uri: string) => uri.replace(/^.*[\\/]/, ''); + /** * Adjust `fileName` to one with the right extension for the file format. * @@ -30,7 +35,10 @@ type Props = { * actual format. The clue we get in the image-picker response is the extension * found in `uri`. */ -export const chooseUploadImageFilename = (uri: string, fileName: string): string => { +export const chooseUploadImageFilename = (uri: string, fileName?: string): string => { + if (typeof fileName !== 'string' || fileName === '') { + fileName = getDefaultFilenameFromUri(uri); + } /* * Photos in an iPhone's camera roll (taken since iOS 11) are typically in * HEIF format and have file names with the extension `.HEIC`. When the user @@ -41,7 +49,6 @@ export const chooseUploadImageFilename = (uri: string, fileName: string): string if (/\.jpe?g$/i.test(uri)) { return fileName.replace(/\.heic$/i, '.jpeg'); } - return fileName; }; diff --git a/src/compose/__tests__/ComposeMenu-test.js b/src/compose/__tests__/ComposeMenu-test.js index 152701dbfeb..5d09944f46f 100644 --- a/src/compose/__tests__/ComposeMenu-test.js +++ b/src/compose/__tests__/ComposeMenu-test.js @@ -1,5 +1,5 @@ /* @flow strict-local */ -import { chooseUploadImageFilename } from '../ComposeMenu'; +import { chooseUploadImageFilename, getDefaultFilenameFromUri } from '../ComposeMenu'; describe('chooseUploadImageFilename', () => { test('Does nothing if the image uri does not end with an extension for the JPEG format', () => { @@ -17,3 +17,10 @@ describe('chooseUploadImageFilename', () => { }, ); }); + +describe('getDefaultFilenameFromUri', () => { + test('Returns extracted file name if fileName is left empty', () => { + expect(getDefaultFilenameFromUri('path/to/fileName.jpg')).toBe('fileName.jpg'); + expect(getDefaultFilenameFromUri('path/to/fileName.jpg')).toBe('fileName.jpg'); + }); +}); From 03930f8d91993752a7e017ff303c5fa1c4fe19de Mon Sep 17 00:00:00 2001 From: Armaan Ahluwalia Date: Thu, 11 Oct 2018 23:43:35 +0530 Subject: [PATCH 2/6] ImagePicker: Add dependency for image-picker library change to follow. This commit adds a dependency required by the new image picker library react-native-image-crop-picker we will be switching to in a following commit. This dependency is required by the library to enable its image cropping functionality and even though we dont have that feature enabled currently the app will not build without it. See the Post-Install steps in https://github.com/ivpusic/react-native-image-crop-picker/blob/07d321e3bc279b0ad218817245264cda6a7c77cb/README.md for details. Note: We are skipping adding --------- vectorDrawables.useSupportLibrary = true --------- as stated by the Readme because the app seems to build and work fine without it and its something required by the image cropping feature which we don't have enabled. If we enable that feature we will want to add this line as well. Addtionally, we want to take care to read the "Production build" instructions while building the app for production. --- android/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/android/build.gradle b/android/build.gradle index aa98ebaf839..82635d2a110 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -29,6 +29,7 @@ allprojects { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url "$rootDir/../node_modules/react-native/android" } + maven { url "https://jitpack.io" } } } From de7b69fee68cdd44202fac07102e3f65a5cb4932 Mon Sep 17 00:00:00 2001 From: Armaan Ahluwalia Date: Fri, 19 Oct 2018 22:26:30 +0530 Subject: [PATCH 3/6] ImagePicker: Change dependency of image-picker. This commit replaces react-native-image-picker in favor of react-native-image-crop-picker. It solves a few key issues like being able to - 1) Select multiple images 2) Being able to downsample image quality. See issue #2749 3) Being able to scale down images. 4) Being faster at processing images. The removal of react-native-image-picker was done by: 1) Removing dependency from yarn 2) Running react-native unlink react-native-image-picker 3) Checking the initial commit and removing any code that may not be covered by unlink. For reference the initial commit that introduced changes was 515436a93deb25f. The addition of react-native-image-crop-picker is done by following the installation instructions at https://github.com/ivpusic/react-native-image-crop-picker/blob/07d321e3bc279b0ad218817245264cda6a7c77cb/README.md This involved: 1) Adding the dependency via yarn 2) Running react-native link react-native-image-crop-picker 3) Adding the frameworks under embedded binaries as described by this comment https://github.com/ivpusic/react-native-image-crop-picker/issues/61#issuecomment-244724797 and also under the "Manual" section of the PostInstall steps. Note: We are ignoring a few of the PostInstall steps described in the Readme namely: 1) Changing the deployment target to 8.0 - We already have a higher target set. 2) The steps described in "To localizate the camera / gallery text buttons" - I dont believe this is required and the instructions seem vague. 3) Adding "useSupportLibrary" as described in a previous commit - This is required for cropping images and we don't have that feature enabled currently. When we enable that feature we will want to add this as well. Note: We want to test this commit is working by archiving the project and uploading to TestFlight. --- android/app/build.gradle | 2 +- .../java/com/zulipmobile/MainApplication.java | 4 +- android/settings.gradle | 4 +- ios/ZulipMobile.xcodeproj/project.pbxproj | 124 +++++++++--------- package.json | 4 +- yarn.lock | 7 +- 6 files changed, 72 insertions(+), 73 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e32444c00c0..033a4923388 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -179,7 +179,7 @@ repositories { dependencies { implementation project(':react-native-text-input-reset') implementation project(':react-native-notifications') - implementation project(':react-native-image-picker') + implementation project(':react-native-image-crop-picker') implementation project(':react-native-orientation') implementation project(':react-native-sentry') implementation project(':@remobile_react-native-toast') diff --git a/android/app/src/main/java/com/zulipmobile/MainApplication.java b/android/app/src/main/java/com/zulipmobile/MainApplication.java index eb529b7515a..c0cf6e4d05a 100644 --- a/android/app/src/main/java/com/zulipmobile/MainApplication.java +++ b/android/app/src/main/java/com/zulipmobile/MainApplication.java @@ -10,7 +10,7 @@ import com.facebook.react.ReactApplication; import com.nikolaiwarner.RNTextInputReset.RNTextInputResetPackage; import com.wix.reactnativenotifications.RNNotificationsPackage; -import com.imagepicker.ImagePickerPackage; +import com.reactnative.ivpusic.imagepicker.PickerPackage; import com.github.yamill.orientation.OrientationPackage; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; @@ -53,7 +53,7 @@ protected List getPackages() { return Arrays.asList( new MainReactPackage(), new RNTextInputResetPackage(), - new ImagePickerPackage(), + new PickerPackage(), new OrientationPackage(), new RNSentryPackage(MainApplication.this), new PhotoViewPackage(), diff --git a/android/settings.gradle b/android/settings.gradle index 4a51e265d3b..5ca9addd1b8 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -3,8 +3,8 @@ include ':react-native-text-input-reset' project(':react-native-text-input-reset').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-text-input-reset/android') include ':react-native-notifications' project(':react-native-notifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android') -include ':react-native-image-picker' -project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android') +include ':react-native-image-crop-picker' +project(':react-native-image-crop-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-crop-picker/android') include ':react-native-orientation' project(':react-native-orientation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation/android') include ':react-native-sentry' diff --git a/ios/ZulipMobile.xcodeproj/project.pbxproj b/ios/ZulipMobile.xcodeproj/project.pbxproj index 0660f4f2578..26ad396808d 100644 --- a/ios/ZulipMobile.xcodeproj/project.pbxproj +++ b/ios/ZulipMobile.xcodeproj/project.pbxproj @@ -30,11 +30,14 @@ 3556F3DAC2424EBE9ABF0A31 /* Zocial.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2E41C86FA25744F998C2BB02 /* Zocial.ttf */; }; 3C289EE01FF361C9002AF37A /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C289E9B1FF3617C002AF37A /* libRCTPushNotification.a */; }; 3C4249EB1EF6E09F00D245F1 /* libRNNotifications.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C4249E61EF6E05C00D245F1 /* libRNNotifications.a */; }; + 4191D6ED217A5C9900167844 /* RSKImageCropper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4191D6EC217A5C9900167844 /* RSKImageCropper.framework */; }; + 4191D6EE217A5C9900167844 /* RSKImageCropper.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4191D6EC217A5C9900167844 /* RSKImageCropper.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 4191D6F1217A5D1B00167844 /* QBImagePicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4191D6F0217A5D1B00167844 /* QBImagePicker.framework */; }; + 4191D6F2217A5D1B00167844 /* QBImagePicker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4191D6F0217A5D1B00167844 /* QBImagePicker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 48D1FC73615948D79D3BD31E /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A7F287C57D8A4354A8B6A3E7 /* Octicons.ttf */; }; 49692FC0562C40F3946EC6D4 /* Foundation.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A752523B8D8E49E792F653E1 /* Foundation.ttf */; }; 4B12DC82ECA043809695F227 /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 9DB2E7AE39A448A98E7A4E4A /* Ionicons.ttf */; }; 4CD3C48024294A3A816F4CA9 /* libRNPhotoView.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 09AC2386C1724D5BA93B51E9 /* libRNPhotoView.a */; }; - 757248247DDE47D3A310353E /* libRNImagePicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 875BCBF7DC0645ACBA61563B /* libRNImagePicker.a */; }; 78FA95D8058C4372AB199525 /* libRNSound.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DE2C81628724C8AA4862247 /* libRNSound.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 840D44FCCBB14F97B77D9443 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B2BC2F95A8684C44BAFA7B11 /* libz.tbd */; }; @@ -53,6 +56,7 @@ CFA67D201EC23BCB0070048E /* UtilManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CFA67D1F1EC23BCB0070048E /* UtilManager.m */; }; CFCFE5491F00158500C295CF /* libRCTCameraRoll.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A155BFEC1DD8E54100A8B695 /* libRCTCameraRoll.a */; }; D6D40984ED07473499B3F0C9 /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D5664A74FA8048439CBAB734 /* libRNDeviceInfo.a */; }; + DA25CCABE65E479BBE829484 /* libimageCropPicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B5D89A5BDB98410894CAD5CF /* libimageCropPicker.a */; }; DBC5C3186FE64F94BD3CDC19 /* MaterialCommunityIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A2070A88714C43ED8AF708C3 /* MaterialCommunityIcons.ttf */; }; FF359794CBFF4ABF92423426 /* libRCTToast.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CFB39BBD9094475BB03A3E4 /* libRCTToast.a */; }; /* End PBXBuildFile section */ @@ -149,13 +153,6 @@ remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = RCTOrientation; }; - 0A955E371FACB50000801C8D /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = CFBB80590829494E985F601B /* RNImagePicker.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 014A3B5C1C6CF33500B6D375; - remoteInfo = RNImagePicker; - }; 0AE8C1771F1B39AB00E5534E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BBB8896EA77146E19DF4FF88 /* LRDRCTSimpleToast.xcodeproj */; @@ -247,6 +244,13 @@ remoteGlobalIDString = 3D383D621EBD27B9005632C8; remoteInfo = "double-conversion-tvOS"; }; + 4191D6EA217A5AA100167844 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D0D4F8D2115D4FEF82895FAF /* imageCropPicker.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3400A8081CEB54A6008A0BC7; + remoteInfo = imageCropPicker; + }; 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; @@ -459,6 +463,21 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 4191D6EF217A5C9900167844 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 4191D6EE217A5C9900167844 /* RSKImageCropper.framework in Embed Frameworks */, + 4191D6F2217A5D1B00167844 /* QBImagePicker.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; }; @@ -490,6 +509,8 @@ 3C289E861FF3617C002AF37A /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = ""; }; 3C4249C61EF6E05C00D245F1 /* RNNotifications.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNNotifications.xcodeproj; path = "../node_modules/react-native-notifications/RNNotifications/RNNotifications.xcodeproj"; sourceTree = ""; }; 3C4249EC1EF6E16500D245F1 /* ZulipMobile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = ZulipMobile.entitlements; path = ZulipMobile/ZulipMobile.entitlements; sourceTree = ""; }; + 4191D6EC217A5C9900167844 /* RSKImageCropper.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSKImageCropper.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4191D6F0217A5D1B00167844 /* QBImagePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = QBImagePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 51E1EDA028654E17A35C5BCA /* MaterialIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf"; sourceTree = ""; }; 5597847AA2A04FEE894C9C9E /* libRCTOrientation.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTOrientation.a; sourceTree = ""; }; 66E34CC6219226D10091B852 /* ZulipMobile-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ZulipMobile-Bridging-Header.h"; sourceTree = ""; }; @@ -499,7 +520,6 @@ 713A523038564C27B0D2C2F7 /* RCTOrientation.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTOrientation.xcodeproj; path = "../node_modules/react-native-orientation/iOS/RCTOrientation.xcodeproj"; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; - 875BCBF7DC0645ACBA61563B /* libRNImagePicker.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNImagePicker.a; sourceTree = ""; }; 8B355454DEEB4F28A5B4F8CA /* RNSafeArea.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSafeArea.xcodeproj; path = "../node_modules/react-native-safe-area/ios/RNSafeArea.xcodeproj"; sourceTree = ""; }; 96B2280917D24AE692AD70FE /* SafariViewManager.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = SafariViewManager.xcodeproj; path = "../node_modules/react-native-safari-view/SafariViewManager.xcodeproj"; sourceTree = ""; }; 9DB2E7AE39A448A98E7A4E4A /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = ""; }; @@ -516,6 +536,7 @@ AEF58326BC25479294083E9C /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = ""; }; AF03F5DAC0924A13BDD49574 /* RNSound.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSound.xcodeproj; path = "../node_modules/react-native-sound/RNSound.xcodeproj"; sourceTree = ""; }; B2BC2F95A8684C44BAFA7B11 /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; + B5D89A5BDB98410894CAD5CF /* libimageCropPicker.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libimageCropPicker.a; sourceTree = ""; }; B83DDAD2507A4F0EB47660BD /* libRNSentry.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSentry.a; sourceTree = ""; }; BBB8896EA77146E19DF4FF88 /* LRDRCTSimpleToast.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = LRDRCTSimpleToast.xcodeproj; path = "../node_modules/react-native-simple-toast/ios/LRDRCTSimpleToast.xcodeproj"; sourceTree = ""; }; C2F7E19951E64FC7B6BBE612 /* libRNFetchBlob.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFetchBlob.a; sourceTree = ""; }; @@ -524,7 +545,7 @@ CF6D016517D74B509DBD05DC /* Feather.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Feather.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = ""; }; CFA67D1F1EC23BCB0070048E /* UtilManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UtilManager.m; path = ZulipMobile/UtilManager.m; sourceTree = ""; }; CFA67D211EC23BDD0070048E /* UtilManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = UtilManager.h; path = ZulipMobile/UtilManager.h; sourceTree = ""; }; - CFBB80590829494E985F601B /* RNImagePicker.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNImagePicker.xcodeproj; path = "../node_modules/react-native-image-picker/ios/RNImagePicker.xcodeproj"; sourceTree = ""; }; + D0D4F8D2115D4FEF82895FAF /* imageCropPicker.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = imageCropPicker.xcodeproj; path = "../node_modules/react-native-image-crop-picker/ios/imageCropPicker.xcodeproj"; sourceTree = ""; }; D5664A74FA8048439CBAB734 /* libRNDeviceInfo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNDeviceInfo.a; sourceTree = ""; }; E518466F398E458DA2C685AF /* libSafariViewManager.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libSafariViewManager.a; sourceTree = ""; }; F56CBB1B9A6449F895C858C6 /* Entypo.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Entypo.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"; sourceTree = ""; }; @@ -547,8 +568,10 @@ files = ( 3C289EE01FF361C9002AF37A /* libRCTPushNotification.a in Frameworks */, A146BE491FDB4C640090EA06 /* libART.a in Frameworks */, + 4191D6ED217A5C9900167844 /* RSKImageCropper.framework in Frameworks */, 3C4249EB1EF6E09F00D245F1 /* libRNNotifications.a in Frameworks */, CFCFE5491F00158500C295CF /* libRCTCameraRoll.a in Frameworks */, + 4191D6F1217A5D1B00167844 /* QBImagePicker.framework in Frameworks */, A14EA8771EACE522009D9E83 /* libRCTAnimation.a in Frameworks */, 146834051AC3E58100842450 /* libReact.a in Frameworks */, 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, @@ -572,7 +595,7 @@ 4CD3C48024294A3A816F4CA9 /* libRNPhotoView.a in Frameworks */, C866080E83494D1C8B535591 /* libRNSafeArea.a in Frameworks */, C9F58827F1CF47999850925A /* libRCTOrientation.a in Frameworks */, - 757248247DDE47D3A310353E /* libRNImagePicker.a in Frameworks */, + DA25CCABE65E479BBE829484 /* libimageCropPicker.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -654,14 +677,6 @@ name = Products; sourceTree = ""; }; - 0A955E311FACB50000801C8D /* Products */ = { - isa = PBXGroup; - children = ( - 0A955E381FACB50000801C8D /* libRNImagePicker.a */, - ); - name = Products; - sourceTree = ""; - }; 0AE8C1741F1B39AA00E5534E /* Products */ = { isa = PBXGroup; children = ( @@ -772,6 +787,14 @@ name = Products; sourceTree = ""; }; + 4191D6E7217A5AA100167844 /* Products */ = { + isa = PBXGroup; + children = ( + 4191D6EB217A5AA100167844 /* libimageCropPicker.a */, + ); + name = Products; + sourceTree = ""; + }; 78C398B11ACF4ADC00677621 /* Products */ = { isa = PBXGroup; children = ( @@ -811,7 +834,7 @@ 06D71A26F8704D96B8C2D342 /* RNPhotoView.xcodeproj */, 8B355454DEEB4F28A5B4F8CA /* RNSafeArea.xcodeproj */, 713A523038564C27B0D2C2F7 /* RCTOrientation.xcodeproj */, - CFBB80590829494E985F601B /* RNImagePicker.xcodeproj */, + D0D4F8D2115D4FEF82895FAF /* imageCropPicker.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -828,6 +851,8 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + 4191D6F0217A5D1B00167844 /* QBImagePicker.framework */, + 4191D6EC217A5C9900167844 /* RSKImageCropper.framework */, A148FEFB1E9D8CB900479280 /* zulip.mp3 */, CF6CFE2C1E7DC55100F687C7 /* Build-Phases */, 13B07FAE1A68108700A75B9A /* ZulipMobile */, @@ -925,7 +950,7 @@ 09AC2386C1724D5BA93B51E9 /* libRNPhotoView.a */, 1AED53B5E6AA4E0AA52DB0C1 /* libRNSafeArea.a */, 5597847AA2A04FEE894C9C9E /* libRCTOrientation.a */, - 875BCBF7DC0645ACBA61563B /* libRNImagePicker.a */, + B5D89A5BDB98410894CAD5CF /* libimageCropPicker.a */, ); name = "Recovered References"; sourceTree = ""; @@ -1003,6 +1028,7 @@ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, CF6CFE0C1E7DC27200F687C7 /* Run Script */, 6FD412E618344A7FB938E4AC /* Upload Debug Symbols to Sentry */, + 4191D6EF217A5C9900167844 /* Embed Frameworks */, ); buildRules = ( ); @@ -1057,6 +1083,10 @@ ProductGroup = A1E053B11FDB490D002A87B7 /* Products */; ProjectRef = A1E053B01FDB490D002A87B7 /* ART.xcodeproj */; }, + { + ProductGroup = 4191D6E7217A5AA100167844 /* Products */; + ProjectRef = D0D4F8D2115D4FEF82895FAF /* imageCropPicker.xcodeproj */; + }, { ProductGroup = 0AE8C1741F1B39AA00E5534E /* Products */; ProjectRef = BBB8896EA77146E19DF4FF88 /* LRDRCTSimpleToast.xcodeproj */; @@ -1133,10 +1163,6 @@ ProductGroup = CFCFE50D1F0011FA00C295CF /* Products */; ProjectRef = FC7EC6A752C243FB9F1B8442 /* RNFetchBlob.xcodeproj */; }, - { - ProductGroup = 0A955E311FACB50000801C8D /* Products */; - ProjectRef = CFBB80590829494E985F601B /* RNImagePicker.xcodeproj */; - }, { ProductGroup = 3C4249C71EF6E05C00D245F1 /* Products */; ProjectRef = 3C4249C61EF6E05C00D245F1 /* RNNotifications.xcodeproj */; @@ -1259,13 +1285,6 @@ remoteRef = 0A955E341FACB50000801C8D /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 0A955E381FACB50000801C8D /* libRNImagePicker.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNImagePicker.a; - remoteRef = 0A955E371FACB50000801C8D /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; 0AE8C1781F1B39AB00E5534E /* libLRDRCTSimpleToast.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -1357,6 +1376,13 @@ remoteRef = 3C5B4CA01F298ECA00A22BBE /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 4191D6EB217A5AA100167844 /* libimageCropPicker.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libimageCropPicker.a; + remoteRef = 4191D6EA217A5AA100167844 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -1711,7 +1737,7 @@ "$(SRCROOT)/../node_modules/react-native-photo-view/ios/**", "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", - "$(SRCROOT)/../node_modules/react-native-image-picker/ios", + "$(SRCROOT)/../node_modules/react-native-image-crop-picker/ios/**", ); INFOPLIST_FILE = ZulipMobileTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; @@ -1719,19 +1745,6 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "$(inherited)", @@ -1763,7 +1776,7 @@ "$(SRCROOT)/../node_modules/react-native-photo-view/ios/**", "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", - "$(SRCROOT)/../node_modules/react-native-image-picker/ios", + "$(SRCROOT)/../node_modules/react-native-image-crop-picker/ios/**", ); INFOPLIST_FILE = ZulipMobileTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; @@ -1771,19 +1784,6 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "$(inherited)", @@ -1820,7 +1820,7 @@ "$(SRCROOT)/../node_modules/react-native-photo-view/ios/**", "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", - "$(SRCROOT)/../node_modules/react-native-image-picker/ios", + "$(SRCROOT)/../node_modules/react-native-image-crop-picker/ios/**", ); INFOPLIST_FILE = ZulipMobile/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; @@ -1865,7 +1865,7 @@ "$(SRCROOT)/../node_modules/react-native-photo-view/ios/**", "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", - "$(SRCROOT)/../node_modules/react-native-image-picker/ios", + "$(SRCROOT)/../node_modules/react-native-image-crop-picker/ios/**", ); INFOPLIST_FILE = ZulipMobile/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; diff --git a/package.json b/package.json index c1af8c15b53..25b0235597c 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,7 @@ "react-intl": "^2.4.0", "react-native": "0.57.1", "react-native-device-info": "^0.21.5", - "rn-fetch-blob": "^0.10.13", - "react-native-image-picker": "^0.26.10", + "react-native-image-crop-picker": "^0.21.2", "react-native-notifications": "^1.2.0", "react-native-orientation": "^3.1.3", "react-native-photo-view": "alwx/react-native-photo-view#c58fd6b30", @@ -79,6 +78,7 @@ "redux-persist": "^4.10.2", "redux-thunk": "^2.1.0", "reselect": "^3.0.1", + "rn-fetch-blob": "^0.10.13", "string.fromcodepoint": "^0.2.1", "timezone": "^1.0.13", "url-parse": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index 01d3dcfa425..b71b484fb3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7202,10 +7202,9 @@ react-native-drawer-layout@1.3.2: dependencies: react-native-dismiss-keyboard "1.0.0" -react-native-image-picker@^0.26.10: - version "0.26.10" - resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-0.26.10.tgz#0bb9ab928984948c67aee0b9e64216bee007a9fc" - integrity sha512-z6gAbru2E6SyGWm4ZTbiM9hPHZ5Tsl9kXGfRxW6YQXf9us7zybKoS7dKE1fQPsssv/OSvpPDKannJNncE+ATRA== +react-native-image-crop-picker@^0.21.2: + version "0.21.2" + resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.21.2.tgz#f0e7ad4615f1f7c79ba25e2e7708042e306d258e" react-native-notifications@^1.2.0: version "1.2.0" From 6f613b4c629f980f45ca84bd4acd329c77069a34 Mon Sep 17 00:00:00 2001 From: Armaan Ahluwalia Date: Mon, 8 Oct 2018 12:40:26 +0530 Subject: [PATCH 4/6] ImagePicker: Change ImagePicker Library Implementation. Update the implementation of image uploading in ComposeMenu.js We adopt react-native-image-crop-picker in favor of the old library (react-native-image-picker). This gives us the additional ability to downsample and downscale images before uploading them and also allows selecting multiple images in the same selection ( iOS only for now ). These changes will be implemented in a following commit. Currently, there is a bug in the image picker which results in the max width and height to only be applied on android. This is still an improvement over the previous library but will need to be kept in mind so when that bug is fixed we can upgrade. See https://github.com/ivpusic/react-native-image-crop-picker/issues/604 --- src/compose/ComposeMenu.js | 61 ++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/src/compose/ComposeMenu.js b/src/compose/ComposeMenu.js index 0b4ef0b3346..146a96491d3 100644 --- a/src/compose/ComposeMenu.js +++ b/src/compose/ComposeMenu.js @@ -1,8 +1,7 @@ /* @flow */ import React, { PureComponent } from 'react'; import { View } from 'react-native'; -// $FlowFixMe -import ImagePicker from 'react-native-image-picker'; +import ImagePicker from 'react-native-image-crop-picker'; import { connect } from 'react-redux'; import type { Context, Dispatch, Narrow } from '../types'; @@ -32,8 +31,8 @@ export const getDefaultFilenameFromUri = (uri: string) => uri.replace(/^.*[\\/]/ * * The Zulip server will infer the file format from the filename's * extension, so in this case we need to adjust the extension to match the - * actual format. The clue we get in the image-picker response is the extension - * found in `uri`. + * actual format. The clue we get in the image picker response is the + * extension found in `uri`. */ export const chooseUploadImageFilename = (uri: string, fileName?: string): string => { if (typeof fileName !== 'string' || fileName === '') { @@ -44,7 +43,9 @@ export const chooseUploadImageFilename = (uri: string, fileName?: string): strin * HEIF format and have file names with the extension `.HEIC`. When the user * selects one of these photos through the image picker, the file gets * automatically converted to JPEG format... but the `fileName` property in - * the react-native-image-picker response still has the `.HEIC` extension. + * the react-native-image-crop-picker response **MAY** still have the `.HEIC` + * extension. This is untested across physical ios devices but needs to + * be confirmed. */ if (/\.jpe?g$/i.test(uri)) { return fileName.replace(/\.heic$/i, '.jpeg'); @@ -60,49 +61,39 @@ class ComposeMenu extends PureComponent { styles: () => null, }; - handleImagePickerResponse = (response: Object) => { - if (response.didCancel) { - return; - } - - if (response.error) { - showErrorAlert(response.error, 'Error'); + handleImageRequest = async (requestType: 'openPicker' | 'openCamera') => { + let image; + const { dispatch, destinationNarrow } = this.props; + try { + image = await ImagePicker[requestType]({ + mediaType: 'photo', + compressImageMaxWidth: 2000, + compressImageMaxHeight: 2000, + forceJpg: true, + compressImageQuality: 0.7, + }); + } catch (e) { + if (e.code === 'E_PICKER_CANCELLED') { + return; + } + showErrorAlert(e.toString(), 'Error'); return; } - const { dispatch, destinationNarrow } = this.props; dispatch( uploadImage( destinationNarrow, - response.uri, - chooseUploadImageFilename(response.uri, response.fileName), + image.path, + chooseUploadImageFilename(image.path, image.filename), ), ); }; - handleImageUpload = () => { - ImagePicker.launchImageLibrary( - { - quality: 1.0, - noData: true, - storageOptions: { - skipBackup: true, - path: 'images', - }, - }, - this.handleImagePickerResponse, - ); + this.handleImageRequest('openPicker'); }; handleCameraCapture = () => { - const options = { - storageOptions: { - cameraRoll: true, - waitUntilSaved: true, - }, - }; - - ImagePicker.launchCamera(options, this.handleImagePickerResponse); + this.handleImageRequest('openCamera'); }; render() { From 1cf8d448c3791b9adf78661b1c902da00fb60697 Mon Sep 17 00:00:00 2001 From: Armaan Ahluwalia Date: Thu, 27 Sep 2018 21:10:34 +0530 Subject: [PATCH 5/6] draftImages: Adds a new category of component for draft images. This commit adds a new category of reducers, selectors and actions along with their associated tests and types for saving image drafts. This component will be used to show previews of images selected from the image picker and will be utilized in a following commit. --- src/actionConstants.js | 6 + src/actionTypes.js | 67 ++++++++ src/actions.js | 1 + src/boot/reducers.js | 2 + src/boot/store.js | 2 +- src/directSelectors.js | 3 + .../__tests__/draftImagesActions-test.js | 102 +++++++++++ .../__tests__/draftImagesReducer-test.js | 162 ++++++++++++++++++ .../__tests__/draftImagesSelectors-test.js | 27 +++ src/draftImages/draftImagesActions.js | 43 +++++ src/draftImages/draftImagesReducers.js | 110 ++++++++++++ src/draftImages/draftImagesSelectors.js | 6 + src/types.js | 26 +++ 13 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 src/draftImages/__tests__/draftImagesActions-test.js create mode 100644 src/draftImages/__tests__/draftImagesReducer-test.js create mode 100644 src/draftImages/__tests__/draftImagesSelectors-test.js create mode 100644 src/draftImages/draftImagesActions.js create mode 100644 src/draftImages/draftImagesReducers.js create mode 100644 src/draftImages/draftImagesSelectors.js diff --git a/src/actionConstants.js b/src/actionConstants.js index b3720197e1e..84bc2fc7800 100644 --- a/src/actionConstants.js +++ b/src/actionConstants.js @@ -88,4 +88,10 @@ export const DELETE_OUTBOX_MESSAGE = 'DELETE_OUTBOX_MESSAGE'; export const DRAFT_UPDATE = 'DRAFT_UPDATE'; +export const DRAFT_IMAGE_ADD = 'DRAFT_IMAGE_ADD'; +export const DRAFT_IMAGE_REMOVE = 'DRAFT_IMAGE_REMOVE'; +export const DRAFT_IMAGE_UPLOADING = 'DRAFT_IMAGE_UPLOADING'; +export const DRAFT_IMAGE_UPLOADED = 'DRAFT_IMAGE_UPLOADED'; +export const DRAFT_IMAGE_ERROR = 'DRAFT_IMAGE_ERROR'; + export const CLEAR_TYPING = 'CLEAR_TYPING'; diff --git a/src/actionTypes.js b/src/actionTypes.js index 496ce378118..ce1e422a98c 100644 --- a/src/actionTypes.js +++ b/src/actionTypes.js @@ -28,6 +28,11 @@ import { INIT_REALM_FILTER, SETTINGS_CHANGE, DRAFT_UPDATE, + DRAFT_IMAGE_ADD, + DRAFT_IMAGE_UPLOADING, + DRAFT_IMAGE_UPLOADED, + DRAFT_IMAGE_ERROR, + DRAFT_IMAGE_REMOVE, DO_NARROW, PRESENCE_RESPONSE, MESSAGE_SEND_START, @@ -527,6 +532,68 @@ export type DraftUpdateAction = { export type DraftsAction = DraftUpdateAction | LogoutAction; +/** + * **Draft Image Actions** + * Used by ComposeBox to show previews of images selected for + * upload while composing a message + */ + +/** + * To add a draft image + * @prop id - A unique id for the image + * @prop fileName - A name for the file being uploaded + * @prop uri - uri of the file on the users device + */ + +export type DraftImageAddAction = { + type: typeof DRAFT_IMAGE_ADD, + id: string, + fileName: string, + uri: string, +}; + +/** + * To remove a draft image + * @prop id - The id of the draft image being removed + */ + +export type DraftImageRemoveAction = { + type: typeof DRAFT_IMAGE_REMOVE, + id: string, +}; + +/** + * Mark a draft image as currently being uploaded + * @prop id - The id of the file being uploaded + */ + +export type DraftImageUploadingAction = { + type: typeof DRAFT_IMAGE_UPLOADING, + id: string, +}; + +/** + * Mark a draft image as successfully uploaded + * @prop id - The id of the file being uploaded + * @prop serverUri - uri of the uploaded file on the server + */ + +export type DraftImageUploadedAction = { + type: typeof DRAFT_IMAGE_UPLOADED, + id: string, + serverUri: string, +}; + +/** + * Mark a draft image as having errored out while uplaoding + * @prop id - The id of the file being uploaded + */ + +export type DraftImageErrorAction = { + type: typeof DRAFT_IMAGE_ERROR, + id: string, +}; + export type DoNarrowAction = { type: typeof DO_NARROW, narrow: Narrow, diff --git a/src/actions.js b/src/actions.js index dc8347d0e2a..7caf113bbc1 100644 --- a/src/actions.js +++ b/src/actions.js @@ -3,6 +3,7 @@ export * from './account/accountActions'; export * from './events/eventActions'; export * from './nav/navActions'; export * from './drafts/draftsActions'; +export * from './draftImages/draftImagesActions'; export * from './message/fetchActions'; export * from './message/messagesActions'; export * from './realm/realmActions'; diff --git a/src/boot/reducers.js b/src/boot/reducers.js index 6087b2562ec..56893b87a00 100644 --- a/src/boot/reducers.js +++ b/src/boot/reducers.js @@ -11,6 +11,7 @@ import accounts from '../account/accountsReducers'; import alertWords from '../alertWords/alertWordsReducer'; import caughtUp from '../caughtup/caughtUpReducers'; import drafts from '../drafts/draftsReducers'; +import draftImages from '../draftImages/draftImagesReducers'; import fetching from '../chat/fetchingReducers'; import flags from '../chat/flagsReducers'; import loading from '../loading/loadingReducers'; @@ -40,6 +41,7 @@ const reducers = { alertWords, caughtUp, drafts, + draftImages, fetching, flags, loading, diff --git a/src/boot/store.js b/src/boot/store.js index dc3a4f2fd3b..8291da80086 100644 --- a/src/boot/store.js +++ b/src/boot/store.js @@ -31,7 +31,7 @@ export const discardKeys = [ * install of the app), where things wouldn't work right if we didn't * persist them. */ -export const storeKeys = ['migrations', 'accounts', 'drafts', 'outbox', 'settings']; +export const storeKeys = ['migrations', 'accounts', 'drafts', 'draftImages', 'outbox', 'settings']; /** * Properties on the global store which we persist for caching's sake. diff --git a/src/directSelectors.js b/src/directSelectors.js index 6b74b24cd13..6d832c6ef6b 100644 --- a/src/directSelectors.js +++ b/src/directSelectors.js @@ -3,6 +3,7 @@ import type { GlobalState, SessionState, DraftState, + DraftImagesState, FetchingState, FlagsState, LoadingState, @@ -42,6 +43,8 @@ export const getCanCreateStreams = (state: GlobalState): boolean => state.realm. export const getDrafts = (state: GlobalState): DraftState => state.drafts; +export const getDraftImages = (state: GlobalState): DraftImagesState => state.draftImages; + export const getLoading = (state: GlobalState): LoadingState => state.loading; export const getMessages = (state: GlobalState): MessagesState => state.messages; diff --git a/src/draftImages/__tests__/draftImagesActions-test.js b/src/draftImages/__tests__/draftImagesActions-test.js new file mode 100644 index 00000000000..3f76958b179 --- /dev/null +++ b/src/draftImages/__tests__/draftImagesActions-test.js @@ -0,0 +1,102 @@ +import mockStore from 'redux-mock-store'; // eslint-disable-line + +import { + draftImageAdd, + draftImageRemove, + draftImageUploading, + draftImageUploaded, + draftImageError, +} from '../draftImagesActions'; + +global.fetch = jest.fn(); + +describe('add draft image', () => { + test('adding a draft image with DRAFT_IMAGE_ADD', () => { + const store = mockStore({ + draftImages: {}, + }); + store.dispatch(draftImageAdd('12345', 'testFileName', 'path/to/file')); + const expectedAction = { + type: 'DRAFT_IMAGE_ADD', + id: '12345', + fileName: 'testFileName', + uri: 'path/to/file', + }; + + const actions = store.getActions(); + expect(actions[0]).toEqual(expectedAction); + }); + test('removing a draft image with DRAFT_IMAGE_REMOVE', () => { + const store = mockStore({ + draftImages: { + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + }, + }, + }); + store.dispatch(draftImageRemove('12345')); + const expectedAction = { + type: 'DRAFT_IMAGE_REMOVE', + id: '12345', + }; + + const actions = store.getActions(); + expect(actions[0]).toEqual(expectedAction); + }); + test('uploading state for draft image with DRAFT_IMAGE_UPLOADING', () => { + const store = mockStore({ + draftImages: { + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + }, + }, + }); + store.dispatch(draftImageUploading('12345')); + const expectedAction = { + type: 'DRAFT_IMAGE_UPLOADING', + id: '12345', + }; + + const actions = store.getActions(); + expect(actions[0]).toEqual(expectedAction); + }); + test('uploaded state for draft image with DRAFT_IMAGE_UPLOADED', () => { + const store = mockStore({ + draftImages: { + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + }, + }, + }); + store.dispatch(draftImageUploaded('12345', 'path/to/file')); + const expectedAction = { + type: 'DRAFT_IMAGE_UPLOADED', + id: '12345', + serverUri: 'path/to/file', + }; + + const actions = store.getActions(); + expect(actions[0]).toEqual(expectedAction); + }); + test('error state for draft image with DRAFT_IMAGE_ERROR', () => { + const store = mockStore({ + draftImages: { + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + }, + }, + }); + store.dispatch(draftImageError('12345')); + const expectedAction = { + type: 'DRAFT_IMAGE_ERROR', + id: '12345', + }; + + const actions = store.getActions(); + expect(actions[0]).toEqual(expectedAction); + }); +}); diff --git a/src/draftImages/__tests__/draftImagesReducer-test.js b/src/draftImages/__tests__/draftImagesReducer-test.js new file mode 100644 index 00000000000..7c1df292fbe --- /dev/null +++ b/src/draftImages/__tests__/draftImagesReducer-test.js @@ -0,0 +1,162 @@ +/* @flow */ +import deepFreeze from 'deep-freeze'; +import draftImagesReducers from '../draftImagesReducers'; +import { + DRAFT_IMAGE_ADD, + DRAFT_IMAGE_REMOVE, + DRAFT_IMAGE_UPLOADING, + DRAFT_IMAGE_UPLOADED, + DRAFT_IMAGE_ERROR, +} from '../../actionConstants'; + +describe('draftImagesReducers', () => { + describe(DRAFT_IMAGE_ADD, () => { + test('add a new draft image', () => { + const action = deepFreeze({ + type: DRAFT_IMAGE_ADD, + id: '12345', + fileName: 'testFileName', + uri: 'path/to/file', + }); + const expectedState = { + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + }, + }; + const actualState = draftImagesReducers(undefined, action); + expect(actualState).toEqual(expectedState); + }); + }); + describe(DRAFT_IMAGE_REMOVE, () => { + test('add a new draft image', () => { + const initialState = { + '45678': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'local' + }, + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'local' + }, + }; + const action = deepFreeze({ + type: DRAFT_IMAGE_REMOVE, + id: '12345', + }); + const expectedState = { + '45678': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'local' + }, + }; + const actualState = draftImagesReducers(initialState, action); + expect(actualState).toEqual(expectedState); + }); + }); + describe(DRAFT_IMAGE_UPLOADING, () => { + test('add uploading state to draft image', () => { + const initialState = { + '45678': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'local' + }, + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'local' + }, + }; + const action = deepFreeze({ + type: DRAFT_IMAGE_UPLOADING, + id: '12345', + }); + const expectedState = { + '45678': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'local' + }, + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'uploading', + }, + }; + const actualState = draftImagesReducers(initialState, action); + expect(actualState).toEqual(expectedState); + }); + }); + describe(DRAFT_IMAGE_UPLOADED, () => { + test('add uploading state to draft image', () => { + const initialState = { + '45678': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'local' + }, + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'uploading', + }, + }; + const action = deepFreeze({ + type: DRAFT_IMAGE_UPLOADED, + id: '12345', + }); + const expectedState = { + '45678': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'local' + }, + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'uploaded', + }, + }; + const actualState = draftImagesReducers(initialState, action); + expect(actualState).toEqual(expectedState); + }); + }); + describe(DRAFT_IMAGE_ERROR, () => { + test('add error state to draft image', () => { + const initialState = { + '45678': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'local' + }, + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'uploading', + }, + }; + const action = deepFreeze({ + type: DRAFT_IMAGE_ERROR, + id: '12345', + }); + const expectedState = { + '45678': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'local' + }, + '12345': { + fileName: 'testFileName', + uri: 'path/to/file', + uploadStatus: 'error', + }, + }; + const actualState = draftImagesReducers(initialState, action); + expect(actualState).toEqual(expectedState); + }); + }); +}); diff --git a/src/draftImages/__tests__/draftImagesSelectors-test.js b/src/draftImages/__tests__/draftImagesSelectors-test.js new file mode 100644 index 00000000000..46e87db0424 --- /dev/null +++ b/src/draftImages/__tests__/draftImagesSelectors-test.js @@ -0,0 +1,27 @@ +import deepFreeze from 'deep-freeze'; + +import { getDraftImageData } from '../draftImagesSelectors'; + +describe('getDraftImageData', () => { + test('return draft images if they exists', () => { + const state = deepFreeze({ + draftImages: { + '12345': 'some/img/url', + }, + }); + + const draftImages = getDraftImageData(state); + expect(Object.keys(draftImages)).toHaveLength(1); + expect(draftImages['12345']).toEqual('some/img/url'); + }); + + test('return empty draft images', () => { + const state = deepFreeze({ + draftImages: {}, + }); + + const draftImages = getDraftImageData(state); + expect(Object.keys(draftImages)).toHaveLength(0); + expect(draftImages['12345']).toEqual(undefined); + }); +}); diff --git a/src/draftImages/draftImagesActions.js b/src/draftImages/draftImagesActions.js new file mode 100644 index 00000000000..8ae0a31e107 --- /dev/null +++ b/src/draftImages/draftImagesActions.js @@ -0,0 +1,43 @@ +/* @flow */ +import type { + DraftImageAddAction, + DraftImageRemoveAction, + DraftImageUploadingAction, + DraftImageUploadedAction, + DraftImageErrorAction, +} from '../types'; +import { + DRAFT_IMAGE_ADD, + DRAFT_IMAGE_REMOVE, + DRAFT_IMAGE_UPLOADING, + DRAFT_IMAGE_UPLOADED, + DRAFT_IMAGE_ERROR, +} from '../actionConstants'; + +export const draftImageAdd = (id: string, fileName: string, uri: string): DraftImageAddAction => ({ + type: DRAFT_IMAGE_ADD, + id, + fileName, + uri, +}); + +export const draftImageRemove = (id: string): DraftImageRemoveAction => ({ + type: DRAFT_IMAGE_REMOVE, + id, +}); + +export const draftImageUploading = (id: string): DraftImageUploadingAction => ({ + type: DRAFT_IMAGE_UPLOADING, + id, +}); + +export const draftImageUploaded = (id: string, serverUri: string): DraftImageUploadedAction => ({ + type: DRAFT_IMAGE_UPLOADED, + id, + serverUri, +}); + +export const draftImageError = (id: string): DraftImageErrorAction => ({ + type: DRAFT_IMAGE_ERROR, + id, +}); diff --git a/src/draftImages/draftImagesReducers.js b/src/draftImages/draftImagesReducers.js new file mode 100644 index 00000000000..074c9450c08 --- /dev/null +++ b/src/draftImages/draftImagesReducers.js @@ -0,0 +1,110 @@ +/* @flow */ +import type { + DraftImagesState, + DraftImageAddAction, + DraftImageRemoveAction, + DraftImageUploadingAction, + DraftImageUploadedAction, + DraftImageErrorAction, +} from '../types'; +import { + DRAFT_IMAGE_ADD, + DRAFT_IMAGE_REMOVE, + DRAFT_IMAGE_UPLOADING, + DRAFT_IMAGE_UPLOADED, + DRAFT_IMAGE_ERROR, + LOGOUT, +} from '../actionConstants'; + +const initialState = {}; + +const draftImageAdd = (state: DraftImagesState, action: DraftImageAddAction): DraftImagesState => { + const { fileName, uri } = action; + return { ...state, [action.id]: { fileName, uri } }; +}; + +const draftImageRemove = ( + state: DraftImagesState, + action: DraftImageRemoveAction, +): DraftImagesState => { + const newState = { + ...state, + }; + delete newState[action.id]; + return newState; +}; + +const draftImageUploading = ( + state: DraftImagesState, + action: DraftImageUploadingAction, +): DraftImagesState => { + const newState = { + ...state, + [action.id]: { + ...state[action.id], + uploadStatus: 'uploading', + }, + }; + return newState; +}; + +const draftImageUploaded = ( + state: DraftImagesState, + action: DraftImageUploadedAction, +): DraftImagesState => { + const newState = { + ...state, + [action.id]: { + ...state[action.id], + serverUri: action.serverUri, + uploadStatus: 'uploaded', + }, + }; + return newState; +}; + +const draftImageError = ( + state: DraftImagesState, + action: DraftImageErrorAction, +): DraftImagesState => { + const newState = { + ...state, + [action.id]: { + ...state[action.id], + uploadStatus: 'error', + }, + }; + return newState; +}; + +export default ( + state: DraftImagesState = initialState, + action: | DraftImageAddAction + | DraftImageRemoveAction + | DraftImageUploadingAction + | DraftImageUploadedAction + | DraftImageErrorAction, +): DraftImagesState => { + switch (action.type) { + case LOGOUT: + return initialState; + + case DRAFT_IMAGE_ADD: + return draftImageAdd(state, action); + + case DRAFT_IMAGE_REMOVE: + return draftImageRemove(state, action); + + case DRAFT_IMAGE_UPLOADING: + return draftImageUploading(state, action); + + case DRAFT_IMAGE_UPLOADED: + return draftImageUploaded(state, action); + + case DRAFT_IMAGE_ERROR: + return draftImageError(state, action); + + default: + return state; + } +}; diff --git a/src/draftImages/draftImagesSelectors.js b/src/draftImages/draftImagesSelectors.js new file mode 100644 index 00000000000..467f931f359 --- /dev/null +++ b/src/draftImages/draftImagesSelectors.js @@ -0,0 +1,6 @@ +/* @flow */ +import { createSelector } from 'reselect'; + +import { getDraftImages } from '../directSelectors'; + +export const getDraftImageData = createSelector(getDraftImages, draftImages => draftImages || {}); diff --git a/src/types.js b/src/types.js index 46293f6db37..7b4ec337542 100644 --- a/src/types.js +++ b/src/types.js @@ -439,6 +439,31 @@ export type DraftsState = { [narrow: string]: string, }; +/** + * Draft Image which represents the data required for image upload. + * + * @prop uri - The uri of the file on the user device. + * @prop fileName - The name of the file selected. + * @prop uploadStatus - A string indicating the upload status of the file + * @prop (serverUri) - The uri of the file on the server once upl + * @prop (error) - True if there was an error while uploading the file. + */ +export type DraftImage = {| + uri: string, + fileName: string, + uploadStatus: 'uploading' | 'uploaded' | 'error' | 'local', + serverUri?: string, +|}; + +/** + * Images selected by the user for upload. + * + * @prop (id) - Id of the image. Can be any unique string. + */ +export type DraftImagesState = { + [id: string]: DraftImage, +}; + /** * A collection of (almost) all users in the Zulip org; our `users` state subtree. * @@ -478,6 +503,7 @@ export type GlobalState = {| alertWords: AlertWordsState, caughtUp: CaughtUpState, drafts: DraftsState, + draftImages: DraftImagesState, fetching: FetchingState, flags: FlagsState, migrations: MigrationsState, From 4e577377b3fb770537f1d22bea289b6a3fe2a3c3 Mon Sep 17 00:00:00 2001 From: Armaan Ahluwalia Date: Thu, 27 Sep 2018 21:14:40 +0530 Subject: [PATCH 6/6] ComposeBox: Allow showing a thumbnail preview of images before sending. This commit makes the necessary changes to ComposeMenu and ComposeBox in order to select and upload images per message while composing while showing a thumbnail preview. It also allows you to delete a selected image before sending the message and choose another one. The code in this commit is written to handle multiple images but the max limit is currently set to 1. Can enable more in the future. It also allows you to select a topic for an image. --- src/compose/ComposeBox.js | 121 ++++++++++++++++-- src/compose/ComposeMenu.js | 111 +++++++++++----- .../__tests__/draftImagesReducer-test.js | 20 +-- src/styles/composeBoxStyles.js | 25 +++- 4 files changed, 221 insertions(+), 56 deletions(-) diff --git a/src/compose/ComposeBox.js b/src/compose/ComposeBox.js index 4992f774422..18c983f66e6 100644 --- a/src/compose/ComposeBox.js +++ b/src/compose/ComposeBox.js @@ -1,6 +1,6 @@ /* @flow */ import React, { PureComponent } from 'react'; -import { View, TextInput, findNodeHandle } from 'react-native'; +import { View, TextInput, findNodeHandle, Image, FlatList } from 'react-native'; import { connect } from 'react-redux'; import TextInputReset from 'react-native-text-input-reset'; @@ -14,20 +14,23 @@ import type { Dispatch, Dimensions, GlobalState, + DraftImagesState, } from '../types'; import { addToOutbox, cancelEditMessage, draftUpdate, + draftImageAdd, + draftImageRemove, fetchTopicsForActiveStream, sendTypingEvent, } from '../actions'; -import { updateMessage } from '../api'; +import { updateMessage, uploadFile } from '../api'; import { FloatingActionButton, Input, MultilineInput } from '../common'; import { showErrorAlert } from '../utils/info'; -import { IconDone, IconSend } from '../common/Icons'; +import { IconDone, IconSend, IconCross } from '../common/Icons'; import { isStreamNarrow, isStreamOrTopicNarrow, topicNarrow } from '../utils/narrow'; -import ComposeMenu from './ComposeMenu'; +import ComposeMenu, { handleImagePickerError } from './ComposeMenu'; import AutocompleteViewWrapper from '../autocomplete/AutocompleteViewWrapper'; import getComposeInputPlaceholder from './getComposeInputPlaceholder'; import NotSubscribed from '../message/NotSubscribed'; @@ -47,6 +50,7 @@ import { getIsActiveStreamAnnouncementOnly, } from '../subscriptions/subscriptionSelectors'; import { getDraftForActiveNarrow } from '../drafts/draftsSelectors'; +import { getDraftImageData } from '../draftImages/draftImagesSelectors'; type Props = { auth: Auth, @@ -54,6 +58,7 @@ type Props = { narrow: Narrow, users: User[], draft: string, + draftImages: DraftImagesState, lastMessageTopic: string, isAdmin: boolean, isAnnouncementOnly: boolean, @@ -113,14 +118,15 @@ class ComposeBox extends PureComponent { getCanSelectTopic = () => { const { isMessageFocused, isTopicFocused } = this.state; - const { editMessage, narrow } = this.props; + const { editMessage, narrow, draftImages } = this.props; if (editMessage) { return isStreamOrTopicNarrow(narrow); } if (!isStreamNarrow(narrow)) { return false; } - return isMessageFocused || isTopicFocused; + const hasImages = Boolean(Object.keys(draftImages).length); + return isMessageFocused || isTopicFocused || hasImages; }; setMessageInputValue = (message: string) => { @@ -139,6 +145,23 @@ class ComposeBox extends PureComponent { })); }; + handleImageSelect = (imageEventObj: Object) => { + const { dispatch, response } = imageEventObj; + if (!response.images || !response.images.length) { + return; + } + const newTopic = this.state.topic || this.props.lastMessageTopic; + response.images.forEach(image => { + dispatch(draftImageAdd(image.uri, image.fileName, image.uri)); + }); + setTimeout(() => { + this.setTopicInputValue(newTopic); + }, 200); // wait, to hope the component is shown + }; + handleRemoveDraftImage = (id: string) => { + const { dispatch } = this.props; + dispatch(draftImageRemove(id)); + }; handleLayoutChange = (event: Object) => { this.setState({ height: event.nativeEvent.layout.height, @@ -218,13 +241,54 @@ class ComposeBox extends PureComponent { return isStreamNarrow(narrow) ? topicNarrow(narrow[0].operand, topic || '(no topic)') : narrow; }; + uploadAllDrafts = () => { + const { dispatch, draftImages, auth } = this.props; + const messageUriArr = []; + const imageIds = Object.keys(draftImages); + imageIds.forEach(id => { + const imageObj = draftImages[id]; + const uriPromise = new Promise(async (resolve, reject) => { + try { + const remoteUri = await uploadFile(auth, imageObj.uri, imageObj.fileName); + resolve(`[${imageObj.fileName}](${remoteUri})`); + dispatch(draftImageRemove(id)); + } catch (e) { + reject(e); + } + }); + messageUriArr.push(uriPromise); + }); + return Promise.all(messageUriArr); + }; + + getFormattedMessage = async (message?: string): Promise => { + let draftImages = []; + message = message != null ? message : ''; + try { + draftImages = await this.uploadAllDrafts(); + } catch (e) { + throw e; + } + if (!draftImages.length) { + return message; + } + return `${message}\n${draftImages.join('\n')}`; + }; + handleSend = () => { const { dispatch } = this.props; const { message } = this.state; - dispatch(addToOutbox(this.getDestinationNarrow(), message)); - - this.setMessageInputValue(''); + this.getFormattedMessage(message) + .then(formattedMsg => { + if (formattedMsg.length) { + dispatch(addToOutbox(this.getDestinationNarrow(), formattedMsg)); + } + this.setMessageInputValue(''); + }) + .catch(e => { + showErrorAlert('Error', e.toString()); + }); }; handleEdit = () => { @@ -275,8 +339,11 @@ class ComposeBox extends PureComponent { isAdmin, isAnnouncementOnly, isSubscribed, + draftImages, } = this.props; + const { handleRemoveDraftImage } = this; + if (!isSubscribed) { return ; } else if (isAnnouncementOnly && !isAdmin) { @@ -288,7 +355,28 @@ class ComposeBox extends PureComponent { marginBottom: safeAreaInsets.bottom, ...(canSend ? {} : { opacity: 0, position: 'absolute' }), }; - + const renderImagePreview = ({ item }) => { + const { key } = item; + return ( + + handleRemoveDraftImage(key)} + /> + + + ); + }; + const imagePreviewData = Object.keys(draftImages).map(id => ({ key: id })); + const numberOfDraftImages = Object.keys(draftImages).length; return ( { destinationNarrow={this.getDestinationNarrow()} expanded={isMenuExpanded} onExpandContract={this.handleComposeMenuToggle} + onImageSelect={this.handleImageSelect} + onImageError={handleImagePickerError} + disableCamera={numberOfDraftImages >= 1} + disableUpload={numberOfDraftImages >= 1} /> @@ -325,6 +417,12 @@ class ComposeBox extends PureComponent { onTouchStart={this.handleInputTouchStart} /> )} + { style={styles.composeSendButton} Icon={editMessage === null ? IconSend : IconDone} size={32} - disabled={message.trim().length === 0} + disabled={message.trim().length === 0 && numberOfDraftImages === 0} onPress={editMessage === null ? this.handleSend : this.handleEdit} /> @@ -365,5 +463,6 @@ export default connect((state: GlobalState, props) => ({ canSend: canSendToActiveNarrow(props.narrow) && !getShowMessagePlaceholders(props.narrow)(state), editMessage: getSession(state).editMessage, draft: getDraftForActiveNarrow(props.narrow)(state), + draftImages: getDraftImageData(state), lastMessageTopic: getLastMessageTopic(props.narrow)(state), }))(ComposeBox); diff --git a/src/compose/ComposeMenu.js b/src/compose/ComposeMenu.js index 146a96491d3..5c5f4cda687 100644 --- a/src/compose/ComposeMenu.js +++ b/src/compose/ComposeMenu.js @@ -8,13 +8,17 @@ import type { Context, Dispatch, Narrow } from '../types'; import { showErrorAlert } from '../utils/info'; import { IconPlus, IconLeft, IconPeople, IconImage, IconCamera } from '../common/Icons'; import AnimatedComponent from '../animation/AnimatedComponent'; -import { navigateToCreateGroup, uploadImage } from '../actions'; +import { navigateToCreateGroup } from '../actions'; type Props = { dispatch: Dispatch, expanded: boolean, destinationNarrow: Narrow, onExpandContract: () => void, + onImageSelect: Object => void, + onImageError: Object => void, + disableUpload?: boolean, + disableCamera?: boolean, }; /* @@ -53,6 +57,13 @@ export const chooseUploadImageFilename = (uri: string, fileName?: string): strin return fileName; }; +export const handleImagePickerError = (e: Object) => { + if (e.code === 'E_PICKER_CANCELLED') { + return; + } + showErrorAlert(e.toString(), 'Error'); +}; + class ComposeMenu extends PureComponent { context: Context; props: Props; @@ -62,31 +73,47 @@ class ComposeMenu extends PureComponent { }; handleImageRequest = async (requestType: 'openPicker' | 'openCamera') => { - let image; - const { dispatch, destinationNarrow } = this.props; + const { dispatch, destinationNarrow, onImageSelect, onImageError } = this.props; + const defaults = { + mediaType: 'photo', + compressImageMaxWidth: 2000, + compressImageMaxHeight: 2000, + forceJpg: true, + compressImageQuality: 0.7, + }; + let response; + let requestObj = { + ...defaults, + }; + + if (requestType === 'openPicker') { + requestObj = { + ...defaults, + // multiple: true, + maxFiles: 1, + }; + } try { - image = await ImagePicker[requestType]({ - mediaType: 'photo', - compressImageMaxWidth: 2000, - compressImageMaxHeight: 2000, - forceJpg: true, - compressImageQuality: 0.7, - }); + let images = await ImagePicker[requestType](requestObj); + images = Array.isArray(images) ? images : [images]; + response = { + images: images.map(image => { + const inferredFileName = chooseUploadImageFilename(image.path, image.filename); + return { + uri: image.path, + fileName: inferredFileName, + }; + }), + }; } catch (e) { - if (e.code === 'E_PICKER_CANCELLED') { - return; - } - showErrorAlert(e.toString(), 'Error'); + onImageError(e); return; } - - dispatch( - uploadImage( - destinationNarrow, - image.path, - chooseUploadImageFilename(image.path, image.filename), - ), - ); + onImageSelect({ + destinationNarrow, + response, + dispatch, + }); }; handleImageUpload = () => { this.handleImageRequest('openPicker'); @@ -98,26 +125,42 @@ class ComposeMenu extends PureComponent { render() { const { styles } = this.context; - const { dispatch, expanded, onExpandContract } = this.props; + const { dispatch, expanded, onExpandContract, disableUpload, disableCamera } = this.props; + let animatedWidth = 40; + if (!disableCamera) { + animatedWidth += 40; + } + if (!disableUpload) { + animatedWidth += 40; + } return ( - + dispatch(navigateToCreateGroup())} /> - - + {!disableUpload && ( + + )} + {!disableCamera && ( + + )} {!expanded && } diff --git a/src/draftImages/__tests__/draftImagesReducer-test.js b/src/draftImages/__tests__/draftImagesReducer-test.js index 7c1df292fbe..706668e250f 100644 --- a/src/draftImages/__tests__/draftImagesReducer-test.js +++ b/src/draftImages/__tests__/draftImagesReducer-test.js @@ -34,12 +34,12 @@ describe('draftImagesReducers', () => { '45678': { fileName: 'testFileName', uri: 'path/to/file', - uploadStatus: 'local' + uploadStatus: 'local', }, '12345': { fileName: 'testFileName', uri: 'path/to/file', - uploadStatus: 'local' + uploadStatus: 'local', }, }; const action = deepFreeze({ @@ -50,7 +50,7 @@ describe('draftImagesReducers', () => { '45678': { fileName: 'testFileName', uri: 'path/to/file', - uploadStatus: 'local' + uploadStatus: 'local', }, }; const actualState = draftImagesReducers(initialState, action); @@ -63,12 +63,12 @@ describe('draftImagesReducers', () => { '45678': { fileName: 'testFileName', uri: 'path/to/file', - uploadStatus: 'local' + uploadStatus: 'local', }, '12345': { fileName: 'testFileName', uri: 'path/to/file', - uploadStatus: 'local' + uploadStatus: 'local', }, }; const action = deepFreeze({ @@ -79,7 +79,7 @@ describe('draftImagesReducers', () => { '45678': { fileName: 'testFileName', uri: 'path/to/file', - uploadStatus: 'local' + uploadStatus: 'local', }, '12345': { fileName: 'testFileName', @@ -97,7 +97,7 @@ describe('draftImagesReducers', () => { '45678': { fileName: 'testFileName', uri: 'path/to/file', - uploadStatus: 'local' + uploadStatus: 'local', }, '12345': { fileName: 'testFileName', @@ -113,7 +113,7 @@ describe('draftImagesReducers', () => { '45678': { fileName: 'testFileName', uri: 'path/to/file', - uploadStatus: 'local' + uploadStatus: 'local', }, '12345': { fileName: 'testFileName', @@ -131,7 +131,7 @@ describe('draftImagesReducers', () => { '45678': { fileName: 'testFileName', uri: 'path/to/file', - uploadStatus: 'local' + uploadStatus: 'local', }, '12345': { fileName: 'testFileName', @@ -147,7 +147,7 @@ describe('draftImagesReducers', () => { '45678': { fileName: 'testFileName', uri: 'path/to/file', - uploadStatus: 'local' + uploadStatus: 'local', }, '12345': { fileName: 'testFileName', diff --git a/src/styles/composeBoxStyles.js b/src/styles/composeBoxStyles.js index 1882c35af0e..06ad5b820b9 100644 --- a/src/styles/composeBoxStyles.js +++ b/src/styles/composeBoxStyles.js @@ -6,6 +6,10 @@ import { BRAND_COLOR } from './'; export type ComposeBoxStyles = { composeBox: Style, + composeImage: Style, + composeImages: Style, + composeImageDeleteButton: Style, + composeImageContainer: Style, composeText: Style, composeTextInput: Style, topicInput: Style, @@ -40,13 +44,32 @@ export default ({ color, backgroundColor, borderColor }: Props) => ({ composeBox: { flexDirection: 'row', backgroundColor: 'rgba(127, 127, 127, 0.1)', + flexShrink: 1, }, composeText: { - flex: 1, + flexGrow: 1, + flexShrink: 1, paddingVertical: 8, justifyContent: 'center', + alignSelf: 'stretch', + }, + composeImage: { + flex: 1, + }, + composeImageDeleteButton: { + position: 'absolute', + top: 3, + right: 3, + zIndex: 1, + }, + composeImageContainer: { + minWidth: 90, + height: 90, + margin: 5, }, composeTextInput: { + flexGrow: 0, + flexShrink: 1, borderWidth: 0, borderRadius: 5, backgroundColor,