diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d64c279c155d..68c269fe1bbe 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ + + /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 387B40C2520CE2773517F705 /* [CP] Check Pods Manifest.lock */ = { + 67625BD6E56D7B445CE0B75D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -334,14 +334,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ReactNativeChat-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-ReactNativeChat-ReactNativeChatTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - ABE62262A44D31B78B23818B /* [CP] Copy Pods Resources */ = { + 740ED6E57E4D77AE97454014 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -359,7 +359,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeChat-ReactNativeChatTests/Pods-ReactNativeChat-ReactNativeChatTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; - EC2DD7546E5E118A6D3073B2 /* [CP] Copy Pods Resources */ = { + 930B6ED2A0414681598DB7E1 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -429,7 +429,7 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4DAAA8DFB4A4EF50B076BE3D /* Pods-ReactNativeChat-ReactNativeChatTests.debug.xcconfig */; + baseConfigurationReference = BA6C613DE6755E40F7EDDC68 /* Pods-ReactNativeChat-ReactNativeChatTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -453,7 +453,7 @@ }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 45D4E4039B764281ECADF639 /* Pods-ReactNativeChat-ReactNativeChatTests.release.xcconfig */; + baseConfigurationReference = 1CFBC073CF30A72CCF109A40 /* Pods-ReactNativeChat-ReactNativeChatTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -474,7 +474,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 05C75E1764EA836C54E53E91 /* Pods-ReactNativeChat.debug.xcconfig */; + baseConfigurationReference = 9B9FEA91E46D2B2609028E69 /* Pods-ReactNativeChat.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -503,7 +503,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D232EFF53A6330C07E8BC0F8 /* Pods-ReactNativeChat.release.xcconfig */; + baseConfigurationReference = C6C1D4F5C262ACDAF1800CAD /* Pods-ReactNativeChat.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; diff --git a/ios/ReactNativeChat/Info.plist b/ios/ReactNativeChat/Info.plist index 7fcd8cdc638b..56b565943add 100644 --- a/ios/ReactNativeChat/Info.plist +++ b/ios/ReactNativeChat/Info.plist @@ -23,33 +23,33 @@ CFBundleVersion 60 ITSAppUsesNonExemptEncryption - + LSRequiresIPhoneOS - + NSAppTransportSecurity NSAllowsArbitraryLoads - + NSExceptionDomains localhost NSExceptionAllowsInsecureHTTPLoads - + NSIncludesSubdomains - + www.expensify.com.dev NSExceptionAllowsInsecureHTTPLoads - + NSIncludesSubdomains - + NSLocationWhenInUseUsageDescription - + UIAppFonts GTAmericaExp-Bold.otf @@ -70,7 +70,13 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSPhotoLibraryUsageDescription + Your photos are used to create chat attachments. + NSCameraUsageDescription + Your camera is used to create chat attachments. + NSPhotoLibraryAddUsageDescription + Your camera roll is used to store chat attachments. UIViewControllerBasedStatusBarAppearance - + diff --git a/package-lock.json b/package-lock.json index 0776bb1606f6..d3412f26b307 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2372,6 +2372,11 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" + }, "abab": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", @@ -8228,6 +8233,104 @@ "path-exists": "^4.0.0" } }, + "find-yarn-workspace-root": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz", + "integrity": "sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==", + "requires": { + "fs-extra": "^4.0.3", + "micromatch": "^3.1.4" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, "findup-sync": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", @@ -10815,6 +10918,14 @@ "graceful-fs": "^4.1.9" } }, + "klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "requires": { + "graceful-fs": "^4.1.11" + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -13634,6 +13745,93 @@ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" }, + "patch-package": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.2.2.tgz", + "integrity": "sha512-YqScVYkVcClUY0v8fF0kWOjDYopzIM8e3bj/RU1DPeEF14+dCGm6UeOYm4jvCyxqIEQ5/eJzmbWfDWnUleFNMg==", + "requires": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^2.4.2", + "cross-spawn": "^6.0.5", + "find-yarn-workspace-root": "^1.2.1", + "fs-extra": "^7.0.1", + "is-ci": "^2.0.0", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.0", + "rimraf": "^2.6.3", + "semver": "^5.6.0", + "slash": "^2.0.0", + "tmp": "^0.0.33" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -14681,6 +14879,11 @@ "resolved": "https://registry.npmjs.org/react-native-config/-/react-native-config-1.3.3.tgz", "integrity": "sha512-uc84IFVLqu2dC7Zutg3OlK6RZYvJlGbIdi0v1ZxjUhU379Nz9IkZR1t8J1L5QSnGV8885mtwWDPJRdQyVNFsAw==" }, + "react-native-image-picker": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-2.3.3.tgz", + "integrity": "sha512-i/7JDnxKkUGSbFY2i7YqFNn3ncJm1YlcrPKXrXmJ/YUElz8tHkuwknuqBd9QCJivMfHX41cmq4XvdBDwIOtO+A==" + }, "react-native-keyboard-spacer": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/react-native-keyboard-spacer/-/react-native-keyboard-spacer-0.4.1.tgz", diff --git a/package.json b/package.json index 7bf4e01296a5..4c98fd3d4cd8 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "android-build": "fastlane android build", "test": "jest", "lint": "eslint .", + "postinstall": "patch-package", "postversion": "react-native-version", "print-version": "echo $npm_package_version" }, @@ -38,6 +39,7 @@ "lodash.orderby": "^4.6.0", "moment": "^2.27.0", "moment-timezone": "^0.5.31", + "patch-package": "^6.2.2", "prop-types": "^15.7.2", "pusher-js": "^7.0.0", "react": "^16.13.1", @@ -45,6 +47,7 @@ "react-dom": "^16.13.1", "react-native": "0.63.2", "react-native-config": "^1.3.3", + "react-native-image-picker": "^2.3.3", "react-native-keyboard-spacer": "^0.4.1", "react-native-render-html": "^4.2.3", "react-native-safe-area-context": "^3.1.4", diff --git a/patches/react-native+0.63.2.patch b/patches/react-native+0.63.2.patch new file mode 100644 index 000000000000..bc5e41d5a9d5 --- /dev/null +++ b/patches/react-native+0.63.2.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-native/Libraries/Image/RCTImageLoader.mm b/node_modules/react-native/Libraries/Image/RCTImageLoader.mm +index 3571647..ffb64b1 100644 +--- a/node_modules/react-native/Libraries/Image/RCTImageLoader.mm ++++ b/node_modules/react-native/Libraries/Image/RCTImageLoader.mm +@@ -1040,7 +1040,7 @@ - (BOOL)canHandleRequest:(NSURLRequest *)request + + - (id)sendRequest:(NSURLRequest *)request withDelegate:(id)delegate + { +- __block RCTImageLoaderCancellationBlock requestToken; ++ __block RCTImageLoaderCancellationBlock requestToken = ^{}; + requestToken = [self loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) { + if (error) { + [delegate URLRequest:requestToken didCompleteWithError:error]; diff --git a/src/components/AnchorForCommentsOnly/index.native.js b/src/components/AnchorForCommentsOnly/index.native.js index 2cdd9956a648..d4ed3dfc944b 100644 --- a/src/components/AnchorForCommentsOnly/index.native.js +++ b/src/components/AnchorForCommentsOnly/index.native.js @@ -8,7 +8,7 @@ import {Linking, StyleSheet, Text} from 'react-native'; const propTypes = { // The URL to open - href: PropTypes.string.isRequired, + href: PropTypes.string, // What headers to send to the linked page (usually noopener and noreferrer) // This is unused in native, but is here for parity with web @@ -28,6 +28,7 @@ const propTypes = { }; const defaultProps = { + href: '', rel: null, target: null, children: null, diff --git a/src/lib/ImagePicker/index.js b/src/lib/ImagePicker/index.js new file mode 100644 index 000000000000..2129bd12face --- /dev/null +++ b/src/lib/ImagePicker/index.js @@ -0,0 +1,31 @@ +// Implementation adapted from https://github.com/QuantumBA/foqum-react-native-document-picker/blob/master/web/index.js + +const ImagePicker = { + showImagePicker(options, callback) { + const input = document.createElement('input'); + input.type = 'file'; + input.onchange = function () { + const file = input.files[0]; + file.uri = URL.createObjectURL(file); + + callback(file); + }; + + input.click(); + }, + + /** + * + * The data returned from `showImagePicker` is different on web and mobile, so use this function to ensure the + * data we send to the xhr will be handled properly by the API. On web, we just want to send the file data returned + * from the input. + * + * @param {object} fileData + * @returns {object} + */ + getDataForUpload(fileData) { + return fileData; + }, +}; + +export default ImagePicker; diff --git a/src/lib/ImagePicker/index.native.js b/src/lib/ImagePicker/index.native.js new file mode 100644 index 000000000000..e8fe1a5ad576 --- /dev/null +++ b/src/lib/ImagePicker/index.native.js @@ -0,0 +1,17 @@ +/** + * The react native document picker already works for iOS/Android, so just export the imported document picker + */ +import RNImagePicker from 'react-native-image-picker'; + +const ImagePicker = RNImagePicker; + +/* + * The data returned from `showImagePicker` is different on web and mobile, so use this function to ensure the data we + * send to the xhr will be handled properly. + */ +ImagePicker.getDataForUpload = fileData => ({ + name: fileData.fileName || 'chat_attachment', + type: fileData.type, + uri: fileData.uri, +}); +export default ImagePicker; diff --git a/src/lib/actions/Report.js b/src/lib/actions/Report.js index c7d5f3bfc7c4..a86f0bc72c80 100644 --- a/src/lib/actions/Report.js +++ b/src/lib/actions/Report.js @@ -369,13 +369,15 @@ function fetchOrCreateChatReport(participants) { * * @param {number} reportID * @param {string} text + * @param {object} file */ -function addAction(reportID, text) { +function addAction(reportID, text, file) { const actionKey = `${IONKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`; // Convert the comment from MD into HTML because that's how it is stored in the database const parser = new ExpensiMark(); const htmlComment = parser.replace(text); + const isAttachment = _.isEmpty(text) && file !== undefined; // The new sequence number will be one higher than the highest let highestSequenceNumber = reportMaxSequenceNumbers[reportID] || 0; @@ -405,20 +407,22 @@ function addAction(reportID, text) { message: [ { type: 'COMMENT', - html: htmlComment, + html: isAttachment ? 'Uploading Attachment...' : htmlComment, // Remove HTML from text when applying optimistic offline comment - text: htmlComment.replace(/<[^>]*>?/gm, ''), + text: isAttachment ? 'Uploading Attachment...' + : htmlComment.replace(/<[^>]*>?/gm, ''), } ], isFirstItem: false, - isAttachmentPlaceHolder: false, + isAttachment, } }); queueRequest('Report_AddComment', { reportID, reportComment: htmlComment, + file }); } diff --git a/src/page/home/report/ReportActionCompose.js b/src/page/home/report/ReportActionCompose.js index 4880aae22c3f..cf1650647d19 100644 --- a/src/page/home/report/ReportActionCompose.js +++ b/src/page/home/report/ReportActionCompose.js @@ -7,11 +7,9 @@ import TextInputFocusable from '../../../components/TextInputFocusable'; import sendIcon from '../../../../assets/images/icon-send.png'; import IONKEYS from '../../../IONKEYS'; import paperClipIcon from '../../../../assets/images/icon-paper-clip.png'; -import CONFIG from '../../../CONFIG'; -import openURLInNewTab from '../../../lib/openURLInNewTab'; +import ImagePicker from '../../../lib/ImagePicker'; import withIon from '../../../components/withIon'; -import {saveReportComment} from '../../../lib/actions/Report'; - +import {addAction, saveReportComment} from '../../../lib/actions/Report'; const propTypes = { // A method to call when the form is submitted @@ -37,6 +35,7 @@ class ReportActionCompose extends React.Component { this.submitForm = this.submitForm.bind(this); this.triggerSubmitShortcut = this.triggerSubmitShortcut.bind(this); this.submitForm = this.submitForm.bind(this); + this.showAttachmentPicker = this.showAttachmentPicker.bind(this); this.comment = ''; } @@ -104,13 +103,44 @@ class ReportActionCompose extends React.Component { this.updateComment(''); } + /** + * Handle the attachment icon being tapped + * + * @param {SyntheticEvent} e + */ + showAttachmentPicker(e) { + e.preventDefault(); + + /** + * See https://github.com/react-native-community/react-native-image-picker/blob/master/docs/Reference.md#options + * for option definitions + */ + const options = { + storageOptions: { + skipBackup: true, + }, + }; + + ImagePicker.showImagePicker(options, (response) => { + if (response.didCancel) { + return; + } + + if (response.error) { + console.error(`Error occurred picking image: ${response.error}`); + return; + } + + addAction(this.props.reportID, '', ImagePicker.getDataForUpload(response)); + }); + } + render() { - const href = `${CONFIG.PUSHER.AUTH_URL}/report?reportID=${this.props.reportID}&shouldScrollToLastUnread=true`; return ( openURLInNewTab(href)} + onPress={this.showAttachmentPicker} style={[styles.chatItemAttachButton]} underlayColor={colors.componentBG} > diff --git a/src/page/home/report/ReportActionItemFragment.js b/src/page/home/report/ReportActionItemFragment.js index 12f123efc9e7..b92d9f0dd0ef 100644 --- a/src/page/home/report/ReportActionItemFragment.js +++ b/src/page/home/report/ReportActionItemFragment.js @@ -1,9 +1,12 @@ import React from 'react'; import HTML from 'react-native-render-html'; -import {Linking, Platform} from 'react-native'; +import { + Linking, ActivityIndicator, View, Platform +} from 'react-native'; +import PropTypes from 'prop-types'; import Str from '../../../lib/Str'; import ReportActionFragmentPropTypes from './ReportActionFragmentPropTypes'; -import styles, {webViewStyles} from '../../../style/StyleSheet'; +import styles, {webViewStyles, colors} from '../../../style/StyleSheet'; import Text from '../../../components/Text'; import AnchorForCommentsOnly from '../../../components/AnchorForCommentsOnly'; import {getAuthToken} from '../../../lib/API'; @@ -11,6 +14,13 @@ import {getAuthToken} from '../../../lib/API'; const propTypes = { // The message fragment needing to be displayed fragment: ReportActionFragmentPropTypes.isRequired, + + // Is this fragment an attachment? + isAttachment: PropTypes.bool, +}; + +const defaultProps = { + isAttachment: false }; class ReportActionItemFragment extends React.PureComponent { @@ -57,6 +67,19 @@ class ReportActionItemFragment extends React.PureComponent { const {fragment} = this.props; switch (fragment.type) { case 'COMMENT': + // If this is an attachment placeholder, return the placeholder component + if (this.props.isAttachment && fragment.html === fragment.text) { + return ( + + + + ); + } + // Only render HTML if we have html in the fragment return fragment.html !== fragment.text ? ( @@ -113,6 +136,7 @@ class ReportActionItemFragment extends React.PureComponent { } ReportActionItemFragment.propTypes = propTypes; +ReportActionItemFragment.defaultProps = defaultProps; ReportActionItemFragment.displayName = 'ReportActionItemFragment'; export default ReportActionItemFragment; diff --git a/src/page/home/report/ReportActionItemMessage.js b/src/page/home/report/ReportActionItemMessage.js index 7566e149750f..906596e880ff 100644 --- a/src/page/home/report/ReportActionItemMessage.js +++ b/src/page/home/report/ReportActionItemMessage.js @@ -15,6 +15,7 @@ const ReportActionItemMessage = ({action}) => ( ))} diff --git a/src/style/StyleSheet.js b/src/style/StyleSheet.js index d0107b14d52a..b2c2c22111ed 100644 --- a/src/style/StyleSheet.js +++ b/src/style/StyleSheet.js @@ -9,6 +9,7 @@ const colors = { black: '#000000', blue: '#2EAAE2', border: '#ECECEC', + borderLight: '#E0E0E0', green: '#2ECB70', heading: '#37444C', icon: '#C6C9CA', @@ -584,6 +585,17 @@ const styles = { width: 39, }, + chatItemAttachmentPlaceholder: { + backgroundColor: colors.border, + borderColor: colors.borderLight, + borderWidth: 1, + borderRadius: 8, + height: 150, + textAlign: 'center', + verticalAlign: 'middle', + width: 200, + }, + chatSwitcherInputClear: { alignSelf: 'center', },