about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--android/settings.gradle4
-rw-r--r--ios/Podfile2
-rw-r--r--ios/Podfile.lock24
-rw-r--r--ios/app.xcodeproj/project.pbxproj90
-rw-r--r--ios/app/Info.plist2
-rw-r--r--package.json2
-rw-r--r--src/state/models/user-local-photos.ts25
-rw-r--r--src/view/com/composer/ComposePost.tsx51
-rw-r--r--src/view/com/composer/PhotoCarouselPicker.tsx93
-rw-r--r--src/view/com/composer/SelectedPhoto.tsx83
-rw-r--r--src/view/index.ts6
-rw-r--r--yarn.lock10
12 files changed, 341 insertions, 51 deletions
diff --git a/android/settings.gradle b/android/settings.gradle
index 3c22b7a2c..e6c53dc6b 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -1,4 +1,8 @@
 rootProject.name = 'app'
+include ':@react-native-camera-roll_camera-roll'
+project(':@react-native-camera-roll_camera-roll').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-camera-roll/camera-roll/android')
+include ':@react-native-camera-roll_camera-roll'
+project(':@react-native-camera-roll_camera-roll').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-camera-roll/camera-roll/android')
 apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
 include ':app'
 includeBuild('../node_modules/react-native-gradle-plugin')
diff --git a/ios/Podfile b/ios/Podfile
index f9a45704c..b4fa1f55f 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -20,6 +20,8 @@ target 'app' do
     :app_path => "#{Pod::Config.instance.installation_root}/.."
   )
 
+  pod 'react-native-cameraroll', :path => '../node_modules/@react-native-camera-roll/camera-roll'
+
   target 'appTests' do
     inherit! :complete
     # Pods for testing
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 39b5a5ad0..37e55dacd 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -220,6 +220,8 @@ PODS:
   - React-jsinspector (0.68.2)
   - React-logger (0.68.2):
     - glog
+  - react-native-cameraroll (5.1.0):
+    - React-Core
   - react-native-pager-view (6.0.2):
     - React-Core
   - react-native-safe-area-context (4.4.1):
@@ -301,6 +303,15 @@ PODS:
     - React-Core
   - RNGestureHandler (2.6.1):
     - React-Core
+  - RNImageCropPicker (0.38.1):
+    - React-Core
+    - React-RCTImage
+    - RNImageCropPicker/QBImagePickerController (= 0.38.1)
+    - TOCropViewController
+  - RNImageCropPicker/QBImagePickerController (0.38.1):
+    - React-Core
+    - React-RCTImage
+    - TOCropViewController
   - RNInAppBrowser (3.7.0):
     - React-Core
   - RNReanimated (2.10.0):
@@ -335,6 +346,7 @@ PODS:
     - React-RCTImage
   - RNSVG (12.4.4):
     - React-Core
+  - TOCropViewController (2.6.1)
   - Yoga (1.14.0)
 
 DEPENDENCIES:
@@ -359,6 +371,7 @@ DEPENDENCIES:
   - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
   - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
   - React-logger (from `../node_modules/react-native/ReactCommon/logger`)
+  - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
   - react-native-pager-view (from `../node_modules/react-native-pager-view`)
   - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
   - react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
@@ -377,6 +390,7 @@ DEPENDENCIES:
   - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
   - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
   - RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
+  - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
   - RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`)
   - RNReanimated (from `../node_modules/react-native-reanimated`)
   - RNScreens (from `../node_modules/react-native-screens`)
@@ -386,6 +400,7 @@ DEPENDENCIES:
 SPEC REPOS:
   trunk:
     - fmt
+    - TOCropViewController
 
 EXTERNAL SOURCES:
   boost:
@@ -426,6 +441,8 @@ EXTERNAL SOURCES:
     :path: "../node_modules/react-native/ReactCommon/jsinspector"
   React-logger:
     :path: "../node_modules/react-native/ReactCommon/logger"
+  react-native-cameraroll:
+    :path: "../node_modules/@react-native-camera-roll/camera-roll"
   react-native-pager-view:
     :path: "../node_modules/react-native-pager-view"
   react-native-safe-area-context:
@@ -462,6 +479,8 @@ EXTERNAL SOURCES:
     :path: "../node_modules/@react-native-clipboard/clipboard"
   RNGestureHandler:
     :path: "../node_modules/react-native-gesture-handler"
+  RNImageCropPicker:
+    :path: "../node_modules/react-native-image-crop-picker"
   RNInAppBrowser:
     :path: "../node_modules/react-native-inappbrowser-reborn"
   RNReanimated:
@@ -494,6 +513,7 @@ SPEC CHECKSUMS:
   React-jsiexecutor: b7b553412f2ec768fe6c8f27cd6bafdb9d8719e6
   React-jsinspector: c5989c77cb89ae6a69561095a61cce56a44ae8e8
   React-logger: a0833912d93b36b791b7a521672d8ee89107aff1
+  react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8
   react-native-pager-view: 592421df0259bf7a7a4fe85b74c24f3f39905605
   react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
   react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
@@ -512,12 +532,14 @@ SPEC CHECKSUMS:
   RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca
   RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
   RNGestureHandler: 28ad20bf02257791f7f137b31beef34b9549f54b
+  RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda
   RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364
   RNReanimated: 5bdcbcc3a72aedeee7bb099604262403aa75a1e5
   RNScreens: 0df01424e9e0ed7827200d6ed1087ddd06c493f9
   RNSVG: ecd661f380a07ba690c9c5929c475a44f432d674
+  TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
   Yoga: 99652481fcd320aefa4a7ef90095b95acd181952
 
-PODFILE CHECKSUM: cf94853ebcb0d8e0d027dca9ab7a4ede886a8f20
+PODFILE CHECKSUM: 1c470a84819c5f003f8861cf58774e2fedb862b3
 
 COCOAPODS: 1.11.3
diff --git a/ios/app.xcodeproj/project.pbxproj b/ios/app.xcodeproj/project.pbxproj
index 914a272df..f964b1232 100644
--- a/ios/app.xcodeproj/project.pbxproj
+++ b/ios/app.xcodeproj/project.pbxproj
@@ -11,10 +11,10 @@
 		13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
 		13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
 		13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
-		5CEAE7B7A55582F96F1D5952 /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB672808307A6013805A3FE /* libPods-app.a */; };
+		6D37A166CEFC7E8937DE72CB /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 596ED3134AA9EF6DBAB6EB96 /* libPods-app.a */; };
 		81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
 		E4BBD590292C1F5200296224 /* app.entitlements in Resources */ = {isa = PBXBuildFile; fileRef = E4437C9E28581FA7006DA9E7 /* app.entitlements */; };
-		FEB90D21557517F9279AECA4 /* libPods-app-appTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BAD3BC60FA05CF2D4F6F9BA2 /* libPods-app-appTests.a */; };
+		F19C1FC2EC3C7162CB25E815 /* libPods-app-appTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 16A4C892E71787BF5849EC83 /* libPods-app-appTests.a */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -31,21 +31,21 @@
 		00E356EE1AD99517003FC87E /* appTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = appTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		00E356F21AD99517003FC87E /* appTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = appTests.m; sourceTree = "<group>"; };
-		03F7088487057F17D25D2F4B /* Pods-app-appTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app-appTests.release.xcconfig"; path = "Target Support Files/Pods-app-appTests/Pods-app-appTests.release.xcconfig"; sourceTree = "<group>"; };
+		0C0949EE4E80361188F0C002 /* Pods-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.debug.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.debug.xcconfig"; sourceTree = "<group>"; };
 		13B07F961A680F5B00A75B9A /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = app/AppDelegate.h; sourceTree = "<group>"; };
 		13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = app/AppDelegate.mm; sourceTree = "<group>"; };
 		13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = app/Images.xcassets; sourceTree = "<group>"; };
 		13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = app/Info.plist; sourceTree = "<group>"; };
 		13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = app/main.m; sourceTree = "<group>"; };
-		19C43486502F2DE23E091FE3 /* Pods-app-appTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app-appTests.debug.xcconfig"; path = "Target Support Files/Pods-app-appTests/Pods-app-appTests.debug.xcconfig"; sourceTree = "<group>"; };
+		16A4C892E71787BF5849EC83 /* libPods-app-appTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app-appTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		30A0A65C98339E6009F0927D /* Pods-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.release.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.release.xcconfig"; sourceTree = "<group>"; };
+		4DFF35494A5D1C813CE7EF11 /* Pods-app-appTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app-appTests.debug.xcconfig"; path = "Target Support Files/Pods-app-appTests/Pods-app-appTests.debug.xcconfig"; sourceTree = "<group>"; };
+		596ED3134AA9EF6DBAB6EB96 /* libPods-app.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		685FA16DF9AC2B99AE3B5DBD /* Pods-app-appTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app-appTests.release.xcconfig"; path = "Target Support Files/Pods-app-appTests/Pods-app-appTests.release.xcconfig"; sourceTree = "<group>"; };
 		81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = app/LaunchScreen.storyboard; sourceTree = "<group>"; };
-		970005155A83960D1D13380F /* Pods-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.release.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.release.xcconfig"; sourceTree = "<group>"; };
-		BAD3BC60FA05CF2D4F6F9BA2 /* libPods-app-appTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app-appTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		E4437C9E28581FA7006DA9E7 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = app.entitlements; path = app/app.entitlements; sourceTree = "<group>"; };
-		ED22CAA45207BC18E75DB44B /* Pods-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.debug.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.debug.xcconfig"; sourceTree = "<group>"; };
 		ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
-		FCB672808307A6013805A3FE /* libPods-app.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -53,7 +53,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				FEB90D21557517F9279AECA4 /* libPods-app-appTests.a in Frameworks */,
+				F19C1FC2EC3C7162CB25E815 /* libPods-app-appTests.a in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -61,7 +61,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				5CEAE7B7A55582F96F1D5952 /* libPods-app.a in Frameworks */,
+				6D37A166CEFC7E8937DE72CB /* libPods-app.a in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -103,8 +103,8 @@
 			isa = PBXGroup;
 			children = (
 				ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
-				FCB672808307A6013805A3FE /* libPods-app.a */,
-				BAD3BC60FA05CF2D4F6F9BA2 /* libPods-app-appTests.a */,
+				596ED3134AA9EF6DBAB6EB96 /* libPods-app.a */,
+				16A4C892E71787BF5849EC83 /* libPods-app-appTests.a */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -143,10 +143,10 @@
 		BBD78D7AC51CEA395F1C20DB /* Pods */ = {
 			isa = PBXGroup;
 			children = (
-				ED22CAA45207BC18E75DB44B /* Pods-app.debug.xcconfig */,
-				970005155A83960D1D13380F /* Pods-app.release.xcconfig */,
-				19C43486502F2DE23E091FE3 /* Pods-app-appTests.debug.xcconfig */,
-				03F7088487057F17D25D2F4B /* Pods-app-appTests.release.xcconfig */,
+				0C0949EE4E80361188F0C002 /* Pods-app.debug.xcconfig */,
+				30A0A65C98339E6009F0927D /* Pods-app.release.xcconfig */,
+				4DFF35494A5D1C813CE7EF11 /* Pods-app-appTests.debug.xcconfig */,
+				685FA16DF9AC2B99AE3B5DBD /* Pods-app-appTests.release.xcconfig */,
 			);
 			path = Pods;
 			sourceTree = "<group>";
@@ -158,11 +158,11 @@
 			isa = PBXNativeTarget;
 			buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "appTests" */;
 			buildPhases = (
-				F309585457BFF312EF73FD95 /* [CP] Check Pods Manifest.lock */,
+				D25A2C8B8B280E54CDBF5192 /* [CP] Check Pods Manifest.lock */,
 				00E356EA1AD99517003FC87E /* Sources */,
 				00E356EB1AD99517003FC87E /* Frameworks */,
 				00E356EC1AD99517003FC87E /* Resources */,
-				2772CBCCE24036517E86445D /* [CP] Copy Pods Resources */,
+				93C4A008E3B1F5BDF90AA282 /* [CP] Copy Pods Resources */,
 			);
 			buildRules = (
 			);
@@ -178,13 +178,13 @@
 			isa = PBXNativeTarget;
 			buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "app" */;
 			buildPhases = (
-				4D45C29570A58E7A8A8D177D /* [CP] Check Pods Manifest.lock */,
+				E5C10128C4EC047B9CD6EEFB /* [CP] Check Pods Manifest.lock */,
 				FD10A7F022414F080027D42C /* Start Packager */,
 				13B07F871A680F5B00A75B9A /* Sources */,
 				13B07F8C1A680F5B00A75B9A /* Frameworks */,
 				13B07F8E1A680F5B00A75B9A /* Resources */,
 				00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
-				9CEFF2A5EA3D60C120442CA6 /* [CP] Copy Pods Resources */,
+				29E5C2625DDCB23B01CF3257 /* [CP] Copy Pods Resources */,
 			);
 			buildRules = (
 			);
@@ -266,63 +266,63 @@
 			shellPath = /bin/sh;
 			shellScript = "set -e\n\nexport NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n";
 		};
-		2772CBCCE24036517E86445D /* [CP] Copy Pods Resources */ = {
+		29E5C2625DDCB23B01CF3257 /* [CP] Copy Pods Resources */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
 			);
 			inputFileListPaths = (
-				"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources-${CONFIGURATION}-input-files.xcfilelist",
+				"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources-${CONFIGURATION}-input-files.xcfilelist",
 			);
 			name = "[CP] Copy Pods Resources";
 			outputFileListPaths = (
-				"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources-${CONFIGURATION}-output-files.xcfilelist",
+				"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources-${CONFIGURATION}-output-files.xcfilelist",
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
-			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources.sh\"\n";
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh\"\n";
 			showEnvVarsInLog = 0;
 		};
-		4D45C29570A58E7A8A8D177D /* [CP] Check Pods Manifest.lock */ = {
+		93C4A008E3B1F5BDF90AA282 /* [CP] Copy Pods Resources */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
 			);
 			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources-${CONFIGURATION}-input-files.xcfilelist",
 			);
-			inputPaths = (
-				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
-				"${PODS_ROOT}/Manifest.lock",
-			);
-			name = "[CP] Check Pods Manifest.lock";
+			name = "[CP] Copy Pods Resources";
 			outputFileListPaths = (
-			);
-			outputPaths = (
-				"$(DERIVED_FILE_DIR)/Pods-app-checkManifestLockResult.txt",
+				"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources-${CONFIGURATION}-output-files.xcfilelist",
 			);
 			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";
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources.sh\"\n";
 			showEnvVarsInLog = 0;
 		};
-		9CEFF2A5EA3D60C120442CA6 /* [CP] Copy Pods Resources */ = {
+		D25A2C8B8B280E54CDBF5192 /* [CP] Check Pods Manifest.lock */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
 			);
 			inputFileListPaths = (
-				"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources-${CONFIGURATION}-input-files.xcfilelist",
 			);
-			name = "[CP] Copy Pods Resources";
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
 			outputFileListPaths = (
-				"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-app-appTests-checkManifestLockResult.txt",
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
-			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh\"\n";
+			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;
 		};
-		F309585457BFF312EF73FD95 /* [CP] Check Pods Manifest.lock */ = {
+		E5C10128C4EC047B9CD6EEFB /* [CP] Check Pods Manifest.lock */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
@@ -337,7 +337,7 @@
 			outputFileListPaths = (
 			);
 			outputPaths = (
-				"$(DERIVED_FILE_DIR)/Pods-app-appTests-checkManifestLockResult.txt",
+				"$(DERIVED_FILE_DIR)/Pods-app-checkManifestLockResult.txt",
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
@@ -396,7 +396,7 @@
 /* Begin XCBuildConfiguration section */
 		00E356F61AD99517003FC87E /* Debug */ = {
 			isa = XCBuildConfiguration;
-			baseConfigurationReference = 19C43486502F2DE23E091FE3 /* Pods-app-appTests.debug.xcconfig */;
+			baseConfigurationReference = 4DFF35494A5D1C813CE7EF11 /* Pods-app-appTests.debug.xcconfig */;
 			buildSettings = {
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				BUNDLE_LOADER = "$(TEST_HOST)";
@@ -424,7 +424,7 @@
 		};
 		00E356F71AD99517003FC87E /* Release */ = {
 			isa = XCBuildConfiguration;
-			baseConfigurationReference = 03F7088487057F17D25D2F4B /* Pods-app-appTests.release.xcconfig */;
+			baseConfigurationReference = 685FA16DF9AC2B99AE3B5DBD /* Pods-app-appTests.release.xcconfig */;
 			buildSettings = {
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				BUNDLE_LOADER = "$(TEST_HOST)";
@@ -449,7 +449,7 @@
 		};
 		13B07F941A680F5B00A75B9A /* Debug */ = {
 			isa = XCBuildConfiguration;
-			baseConfigurationReference = ED22CAA45207BC18E75DB44B /* Pods-app.debug.xcconfig */;
+			baseConfigurationReference = 0C0949EE4E80361188F0C002 /* Pods-app.debug.xcconfig */;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
@@ -478,7 +478,7 @@
 		};
 		13B07F951A680F5B00A75B9A /* Release */ = {
 			isa = XCBuildConfiguration;
-			baseConfigurationReference = 970005155A83960D1D13380F /* Pods-app.release.xcconfig */;
+			baseConfigurationReference = 30A0A65C98339E6009F0927D /* Pods-app.release.xcconfig */;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
diff --git a/ios/app/Info.plist b/ios/app/Info.plist
index 416d55759..5a9e7e53a 100644
--- a/ios/app/Info.plist
+++ b/ios/app/Info.plist
@@ -48,6 +48,8 @@
 			</dict>
 		</dict>
 	</dict>
+	<key>NSPhotoLibraryUsageDescription</key>
+	<string></string>
 	<key>NSLocationWhenInUseUsageDescription</key>
 	<string></string>
 	<key>UILaunchStoryboardName</key>
diff --git a/package.json b/package.json
index 44b47d53c..3e0afb334 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
     "@fortawesome/react-native-fontawesome": "^0.3.0",
     "@gorhom/bottom-sheet": "^4",
     "@react-native-async-storage/async-storage": "^1.17.6",
+    "@react-native-camera-roll/camera-roll": "^5.1.0",
     "@react-native-clipboard/clipboard": "^1.10.0",
     "@zxing/text-encoding": "^0.9.0",
     "base64-js": "^1.5.1",
@@ -34,6 +35,7 @@
     "react-native": "0.68.2",
     "react-native-appstate-hook": "^1.0.6",
     "react-native-gesture-handler": "^2.5.0",
+    "react-native-image-crop-picker": "^0.38.1",
     "react-native-inappbrowser-reborn": "^3.6.3",
     "react-native-linear-gradient": "^2.6.2",
     "react-native-pager-view": "^6.0.2",
diff --git a/src/state/models/user-local-photos.ts b/src/state/models/user-local-photos.ts
new file mode 100644
index 000000000..4e01f1d94
--- /dev/null
+++ b/src/state/models/user-local-photos.ts
@@ -0,0 +1,25 @@
+import {PhotoIdentifier} from './../../../node_modules/@react-native-camera-roll/camera-roll/src/CameraRoll'
+import {makeAutoObservable} from 'mobx'
+import {CameraRoll} from '@react-native-camera-roll/camera-roll'
+import {RootStoreModel} from './root-store'
+
+export class UserLocalPhotosModel {
+  // state
+  photos: PhotoIdentifier[] = []
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(this, {
+      rootStore: false,
+    })
+  }
+
+  async setup() {
+    await this._getPhotos()
+  }
+
+  private async _getPhotos() {
+    CameraRoll.getPhotos({first: 20}).then(r => {
+      this.photos = r.edges
+    })
+  }
+}
diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx
index a61759c24..114586f47 100644
--- a/src/view/com/composer/ComposePost.tsx
+++ b/src/view/com/composer/ComposePost.tsx
@@ -23,6 +23,9 @@ import * as apilib from '../../../state/lib/api'
 import {ComposerOpts} from '../../../state/models/shell-ui'
 import {s, colors, gradients} from '../../lib/styles'
 import {detectLinkables} from '../../../lib/strings'
+import {UserLocalPhotosModel} from '../../../state/models/user-local-photos'
+import {PhotoCarouselPicker} from './PhotoCarouselPicker'
+import {SelectedPhoto} from './SelectedPhoto'
 
 const MAX_TEXT_LENGTH = 256
 const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
@@ -41,14 +44,25 @@ export const ComposePost = observer(function ComposePost({
   const [isProcessing, setIsProcessing] = useState(false)
   const [error, setError] = useState('')
   const [text, setText] = useState('')
+  const [selectedPhotos, setSelectedPhotos] = useState<string[]>([])
+
   const autocompleteView = useMemo<UserAutocompleteViewModel>(
     () => new UserAutocompleteViewModel(store),
     [],
   )
+  const localPhotos = useMemo<UserLocalPhotosModel>(
+    () => new UserLocalPhotosModel(store),
+    [],
+  )
 
   useEffect(() => {
     autocompleteView.setup()
   })
+
+  useEffect(() => {
+    localPhotos.setup()
+  }, [localPhotos])
+
   useEffect(() => {
     // HACK
     // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
@@ -60,7 +74,9 @@ export const ComposePost = observer(function ComposePost({
       }, 250)
     }
     return () => {
-      if (to) clearTimeout(to)
+      if (to) {
+        clearTimeout(to)
+      }
     }
   }, [textInput.current])
 
@@ -115,6 +131,10 @@ export const ComposePost = observer(function ComposePost({
 
   const canPost = text.length <= MAX_TEXT_LENGTH
   const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined
+  const selectTextInputLayout =
+    selectedPhotos.length !== 0
+      ? styles.textInputLayoutWithPhoto
+      : styles.textInputLayoutWithoutPhoto
 
   const textDecorated = useMemo(() => {
     let i = 0
@@ -192,7 +212,7 @@ export const ComposePost = observer(function ComposePost({
             </View>
           </View>
         ) : undefined}
-        <View style={styles.textInputLayout}>
+        <View style={[styles.textInputLayout, selectTextInputLayout]}>
           <UserAvatar
             handle={store.me.handle || ''}
             displayName={store.me.displayName}
@@ -208,8 +228,18 @@ export const ComposePost = observer(function ComposePost({
             {textDecorated}
           </TextInput>
         </View>
-        <View
-          style={[s.flexRow, {alignItems: 'center'}, s.pt10, s.pb10, s.pr5]}>
+        <SelectedPhoto
+          selectedPhotos={selectedPhotos}
+          setSelectedPhotos={setSelectedPhotos}
+        />
+        <PhotoCarouselPicker
+          selectedPhotos={selectedPhotos}
+          setSelectedPhotos={setSelectedPhotos}
+          localPhotos={localPhotos}
+          inputText={text}
+        />
+        <View style={styles.separator} />
+        <View style={[s.flexRow, s.pt10, s.pb10, s.pr5, styles.contentCenter]}>
           <View style={s.flex1} />
           <Text style={[s.mr10, {color: progressColor}]}>
             {MAX_TEXT_LENGTH - text.length}
@@ -282,9 +312,14 @@ const styles = StyleSheet.create({
     justifyContent: 'center',
     marginRight: 5,
   },
+  textInputLayoutWithPhoto: {
+    flexWrap: 'wrap',
+  },
+  textInputLayoutWithoutPhoto: {
+    flex: 1,
+  },
   textInputLayout: {
     flexDirection: 'row',
-    flex: 1,
     borderTopWidth: 1,
     borderTopColor: colors.gray2,
     paddingTop: 16,
@@ -307,4 +342,10 @@ const styles = StyleSheet.create({
     paddingLeft: 13,
     paddingRight: 8,
   },
+  contentCenter: {alignItems: 'center'},
+  separator: {
+    borderBottomColor: 'black',
+    borderBottomWidth: StyleSheet.hairlineWidth,
+    width: '100%',
+  },
 })
diff --git a/src/view/com/composer/PhotoCarouselPicker.tsx b/src/view/com/composer/PhotoCarouselPicker.tsx
new file mode 100644
index 000000000..17be8a5f5
--- /dev/null
+++ b/src/view/com/composer/PhotoCarouselPicker.tsx
@@ -0,0 +1,93 @@
+import React from 'react'
+import {Image, StyleSheet, TouchableOpacity, ScrollView} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {colors} from '../../lib/styles'
+import {openPicker, openCamera} from 'react-native-image-crop-picker'
+import {observer} from 'mobx-react-lite'
+
+export const PhotoCarouselPicker = observer(function PhotoCarouselPicker({
+  selectedPhotos,
+  setSelectedPhotos,
+  inputText,
+  localPhotos,
+}: {
+  selectedPhotos: string[]
+  setSelectedPhotos: React.Dispatch<React.SetStateAction<string[]>>
+  inputText: string
+  localPhotos: any
+}) {
+  return localPhotos.photos != null &&
+    inputText === '' &&
+    selectedPhotos.length === 0 ? (
+    <ScrollView
+      horizontal
+      style={styles.photosContainer}
+      showsHorizontalScrollIndicator={false}>
+      <TouchableOpacity
+        style={[styles.galleryButton, styles.photo]}
+        onPress={() => {
+          openCamera({multiple: true, maxFiles: 4}).then()
+        }}>
+        <FontAwesomeIcon
+          icon="camera"
+          size={24}
+          style={{color: colors.blue3}}
+        />
+      </TouchableOpacity>
+      {localPhotos.photos.map((item: any, index: number) => (
+        <TouchableOpacity
+          key={`local-image-${index}`}
+          style={styles.photoButton}
+          onPress={() => {
+            setSelectedPhotos([item.node.image.uri, ...selectedPhotos])
+          }}>
+          <Image style={styles.photo} source={{uri: item.node.image.uri}} />
+        </TouchableOpacity>
+      ))}
+      <TouchableOpacity
+        style={[styles.galleryButton, styles.photo]}
+        onPress={() => {
+          openPicker({multiple: true, maxFiles: 4}).then(items => {
+            setSelectedPhotos([
+              ...items.reduce(
+                (accum, cur) => accum.concat(cur.sourceURL!),
+                [] as string[],
+              ),
+              ...selectedPhotos,
+            ])
+          })
+        }}>
+        <FontAwesomeIcon icon="image" style={{color: colors.blue3}} size={24} />
+      </TouchableOpacity>
+    </ScrollView>
+  ) : null
+})
+
+const styles = StyleSheet.create({
+  photosContainer: {
+    width: '100%',
+    maxHeight: 96,
+    padding: 8,
+    overflow: 'hidden',
+  },
+  galleryButton: {
+    borderWidth: 1,
+    borderColor: colors.gray3,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  photoButton: {
+    width: 75,
+    height: 75,
+    marginRight: 8,
+    borderWidth: 1,
+    borderRadius: 16,
+    borderColor: colors.gray3,
+  },
+  photo: {
+    width: 75,
+    height: 75,
+    marginRight: 8,
+    borderRadius: 16,
+  },
+})
diff --git a/src/view/com/composer/SelectedPhoto.tsx b/src/view/com/composer/SelectedPhoto.tsx
new file mode 100644
index 000000000..1fe147483
--- /dev/null
+++ b/src/view/com/composer/SelectedPhoto.tsx
@@ -0,0 +1,83 @@
+import React from 'react'
+import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {colors} from '../../lib/styles'
+import {observer} from 'mobx-react-lite'
+
+export const SelectedPhoto = ({
+  selectedPhotos,
+  setSelectedPhotos,
+}: {
+  selectedPhotos: string[]
+  setSelectedPhotos: React.Dispatch<React.SetStateAction<string[]>>
+}) => {
+  const imageStyle =
+    selectedPhotos.length === 1
+      ? styles.image250
+      : selectedPhotos.length === 2
+      ? styles.image175
+      : styles.image85
+
+  return selectedPhotos.length !== 0 ? (
+    <View style={styles.imageContainer}>
+      {selectedPhotos.length !== 0 &&
+        selectedPhotos.map((item, index) => (
+          <View
+            key={`selected-image-${index}`}
+            style={[styles.image, imageStyle]}>
+            <TouchableOpacity
+              onPress={() => {
+                setSelectedPhotos(
+                  selectedPhotos.filter(filterItem => filterItem !== item),
+                )
+              }}
+              style={styles.removePhotoButton}>
+              <FontAwesomeIcon
+                icon="xmark"
+                size={16}
+                style={{color: colors.white}}
+              />
+            </TouchableOpacity>
+
+            <Image style={[styles.image, imageStyle]} source={{uri: item}} />
+          </View>
+        ))}
+    </View>
+  ) : null
+}
+
+const styles = StyleSheet.create({
+  imageContainer: {
+    flex: 1,
+    flexDirection: 'row',
+    marginTop: 16,
+  },
+  image: {
+    borderRadius: 8,
+    margin: 2,
+  },
+  image250: {
+    width: 250,
+    height: 250,
+  },
+  image175: {
+    width: 175,
+    height: 175,
+  },
+  image85: {
+    width: 85,
+    height: 85,
+  },
+  removePhotoButton: {
+    position: 'absolute',
+    top: 8,
+    right: 8,
+    width: 24,
+    height: 24,
+    borderRadius: 12,
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: colors.black,
+    zIndex: 1,
+  },
+})
diff --git a/src/view/index.ts b/src/view/index.ts
index e38e1debf..bd0e33cbe 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -56,6 +56,9 @@ import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark'
 import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
 import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
 import {faX} from '@fortawesome/free-solid-svg-icons/faX'
+import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera'
+import {faImage} from '@fortawesome/free-solid-svg-icons/faImage'
+import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
 
 export function setup() {
   library.add(
@@ -115,5 +118,8 @@ export function setup() {
     faTicket,
     faTrashCan,
     faX,
+    faCamera,
+    faImage,
+    faXmark,
   )
 }
diff --git a/yarn.lock b/yarn.lock
index 1160158e1..bbaaa2d6a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1897,6 +1897,11 @@
   dependencies:
     merge-options "^3.0.4"
 
+"@react-native-camera-roll/camera-roll@^5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.1.0.tgz#5cfb3cf02d72ab03b3d6a0bdda392e2896c8d55f"
+  integrity sha512-74pavpt2T2U3V0r5d+pn4NChJbRNcydqakp3NVmosod35Lzxrt9My7kCLDdHXW2S6J6DhgXb/n36/heZQB4AUA==
+
 "@react-native-clipboard/clipboard@^1.10.0":
   version "1.11.1"
   resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.11.1.tgz#d3a9e685ce2383b1e92b89a334896c5575cc103d"
@@ -10191,6 +10196,11 @@ react-native-gradle-plugin@^0.0.6:
   resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.6.tgz#b61a9234ad2f61430937911003cddd7e15c72b45"
   integrity sha512-eIlgtsmDp1jLC24dRn43hB3kEcZVqx6DUQbR0N1ABXGnMEafm9I3V3dUUeD1vh+Dy5WqijSoEwLNUPLgu5zDMg==
 
+react-native-image-crop-picker@^0.38.1:
+  version "0.38.1"
+  resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.38.1.tgz#5973b4a8b55835b987e6be2064de411e849ac005"
+  integrity sha512-cF5UQnWplzHCeiCO+aiGS/0VomWaLmFf3nSsgTMPfY+8+99h8N/eHQvVdSF7RsGw50B8394wGeGyqHjjp8YRWw==
+
 react-native-inappbrowser-reborn@^3.6.3:
   version "3.7.0"
   resolved "https://registry.yarnpkg.com/react-native-inappbrowser-reborn/-/react-native-inappbrowser-reborn-3.7.0.tgz#849a43c3c7da22b65147649fe596836bcb494083"