From 43c229738743280a0988b82fc95c3c656bd6e2e3 Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Mon, 2 May 2022 22:31:22 +0200 Subject: [PATCH] Sharing works in simulator --- index.share.js | 4 + ios/Podfile | 15 + ios/Podfile.lock | 14 +- .../Base.lproj/MainInterface.storyboard | 27 + ios/ShareExtension/Info.plist | 70 +++ .../ShareExtension-Bridging-Header.h | 8 + ios/tooot.xcodeproj/project.pbxproj | 274 ++++++++++ ios/tooot/Info.plist | 4 +- ios/tooot/tooot.entitlements | 4 + package.json | 6 +- ...@types+react-native-share-menu+5.0.2.patch | 25 + patches/react-native-share-menu+5.0.5.patch | 479 ++++++++++++++++++ src/Screens.tsx | 72 +++ src/ShareExtension.tsx | 105 ++++ src/i18n/en/screens/compose.json | 3 +- src/screens/Compose.tsx | 31 ++ src/screens/Compose/Root/Actions.tsx | 4 +- .../Compose/Root/Footer/Attachments.tsx | 6 +- .../Compose/Root/Footer/addAttachment.ts | 223 ++++---- src/screens/Compose/utils/parseState.ts | 2 + src/utils/navigation/navigators.ts | 6 + yarn.lock | 33 ++ 22 files changed, 1301 insertions(+), 114 deletions(-) create mode 100644 index.share.js create mode 100644 ios/ShareExtension/Base.lproj/MainInterface.storyboard create mode 100644 ios/ShareExtension/Info.plist create mode 100644 ios/ShareExtension/ShareExtension-Bridging-Header.h create mode 100644 patches/@types+react-native-share-menu+5.0.2.patch create mode 100644 patches/react-native-share-menu+5.0.5.patch create mode 100644 src/ShareExtension.tsx diff --git a/index.share.js b/index.share.js new file mode 100644 index 00000000..d7072850 --- /dev/null +++ b/index.share.js @@ -0,0 +1,4 @@ +import { AppRegistry } from 'react-native' +import ShareExtension from './src/ShareExtension' + +AppRegistry.registerComponent('ShareMenuModuleComponent', () => ShareExtension) diff --git a/ios/Podfile b/ios/Podfile index 4d27b3b8..e9185d46 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -24,5 +24,20 @@ target 'tooot' do post_install do |installer| react_native_post_install(installer) + + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO' + end + end end end + +target 'ShareExtension' do + use_react_native!( + :hermes_enabled => podfile_properties['expo.jsEngine'] == 'hermes' + ) + + pod 'RNShareMenu', :path => '../node_modules/react-native-share-menu' + pod 'RNFS', :path => '../node_modules/react-native-fs' +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ca3977a7..1b7b5d6a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -460,6 +460,8 @@ PODS: - React-Core - SDWebImage (~> 5.12.5) - SDWebImageWebPCoder (~> 0.8.4) + - RNFS (2.19.0): + - React-Core - RNGestureHandler (2.4.1): - React-Core - RNReanimated (2.8.0): @@ -495,6 +497,8 @@ PODS: - RNSentry (3.4.1): - React-Core - Sentry (= 7.11.0) + - RNShareMenu (5.0.5): + - React - RNSVG (12.3.0): - React-Core - SDWebImage (5.12.5): @@ -585,10 +589,12 @@ DEPENDENCIES: - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - RNFastImage (from `../node_modules/react-native-fast-image`) + - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - "RNSentry (from `../node_modules/@sentry/react-native`)" + - RNShareMenu (from `../node_modules/react-native-share-menu`) - RNSVG (from `../node_modules/react-native-svg`) - UMTaskManagerInterface (from `../node_modules/unimodules-task-manager-interface/ios`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -754,6 +760,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-async-storage/async-storage" RNFastImage: :path: "../node_modules/react-native-fast-image" + RNFS: + :path: "../node_modules/react-native-fs" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNReanimated: @@ -762,6 +770,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-screens" RNSentry: :path: "../node_modules/@sentry/react-native" + RNShareMenu: + :path: "../node_modules/react-native-share-menu" RNSVG: :path: "../node_modules/react-native-svg" UMTaskManagerInterface: @@ -854,10 +864,12 @@ SPEC CHECKSUMS: ReactCommon: 07d0c460b9ba9af3eaf1b8f5abe7daaad28c9c4e RNCAsyncStorage: 005c0e2f09575360f142d0d1f1f15e4ec575b1af RNFastImage: 945abf54742505d790d9024d230c69b1e866bc88 + RNFS: fc610f78fdf8bfc89a9e5cc2f898519f4dba1002 RNGestureHandler: 4f4986408310a43f1606c391f38f76e0d6e790d5 RNReanimated: 46cdb89ca59ab7181334f4ed05a70e82ddb36751 RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19 RNSentry: fbbdcd7213058e3de5fbaa452b25a06a16b4b382 + RNShareMenu: c69282e50ac439737a86949a55c7b023b90027c8 RNSVG: 302bfc9905bd8122f08966dc2ce2d07b7b52b9f8 SDWebImage: 0905f1b7760fc8ac4198cae0036600d67478751e SDWebImageWebPCoder: f93010f3f6c031e2f8fb3081ca4ee6966c539815 @@ -865,6 +877,6 @@ SPEC CHECKSUMS: UMTaskManagerInterface: 3184c93ecc290bd422c6e344badc89b601e9c29b Yoga: d6b6a80659aa3e91aaba01d0012e7edcbedcbecd -PODFILE CHECKSUM: 9bf9d386bac4ff98f76fc93f120c9922660384b5 +PODFILE CHECKSUM: 26ee7ffc1b88088246dc6a04f532230c6d5a5884 COCOAPODS: 1.11.3 diff --git a/ios/ShareExtension/Base.lproj/MainInterface.storyboard b/ios/ShareExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..2e6951b8 --- /dev/null +++ b/ios/ShareExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/ShareExtension/Info.plist b/ios/ShareExtension/Info.plist new file mode 100644 index 00000000..4d392fd1 --- /dev/null +++ b/ios/ShareExtension/Info.plist @@ -0,0 +1,70 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + TRUEPREDICATE + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + HostAppBundleIdentifier + com.xmflsct.app.tooot + HostAppURLScheme + tooot:// + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsImageWithMaxCount + 4 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + ReactShareViewBackgroundColor + + Red + 0 + Green + 0 + Blue + 0 + Alpha + 0 + Transparent + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + + diff --git a/ios/ShareExtension/ShareExtension-Bridging-Header.h b/ios/ShareExtension/ShareExtension-Bridging-Header.h new file mode 100644 index 00000000..f788061b --- /dev/null +++ b/ios/ShareExtension/ShareExtension-Bridging-Header.h @@ -0,0 +1,8 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import +#import +#import +#import diff --git a/ios/tooot.xcodeproj/project.pbxproj b/ios/tooot.xcodeproj/project.pbxproj index ead1ac58..a87453ab 100644 --- a/ios/tooot.xcodeproj/project.pbxproj +++ b/ios/tooot.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 34A37A6C820725DC6DDAA0EE /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B3640FCDF7C4396A68A74D1 /* libPods-ShareExtension.a */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; 5E36538325C9B8BD009F93EE /* RootViewColor.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5E36538225C9B8BD009F93EE /* RootViewColor.xcassets */; }; 5EE088C926297820007E5FEC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5EE088CB26297820007E5FEC /* InfoPlist.strings */; }; @@ -19,8 +20,36 @@ BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; DA8B5B7F0DED488CAC0FF169 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B96B72E5384D44A7B240B27E /* GoogleService-Info.plist */; }; E3BC22F5F8ABE515E14CF199 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D878F932AF7A9974E06E461 /* ExpoModulesProvider.swift */; }; + E633A426281EAEAB000E540F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E633A424281EAEAB000E540F /* MainInterface.storyboard */; }; + E633A42B281EAEAB000E540F /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E633A420281EAEAB000E540F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + E633A430281EAF38000E540F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E633A42F281EAF38000E540F /* ShareViewController.swift */; }; + E633A437281EB5BC000E540F /* ReactShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E633A436281EB5BC000E540F /* ReactShareViewController.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + E633A428281EAEAB000E540F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E633A41F281EAEAB000E540F; + remoteInfo = ShareExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + E633A42A281EAEAB000E540F /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + E633A42B281EAEAB000E540F /* ShareExtension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* tooot.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = tooot.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -30,6 +59,9 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = tooot/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = tooot/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = tooot/main.m; sourceTree = ""; }; + 1B3640FCDF7C4396A68A74D1 /* libPods-ShareExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ShareExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 312CB8F38010C3E0D27A8663 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; + 49AC0972A79258360BEDD73B /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-tooot.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-tooot.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 5E36538225C9B8BD009F93EE /* RootViewColor.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = RootViewColor.xcassets; path = tooot/RootViewColor.xcassets; sourceTree = ""; }; 5EE088CA26297820007E5FEC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -42,6 +74,12 @@ AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = tooot/SplashScreen.storyboard; sourceTree = ""; }; B96B72E5384D44A7B240B27E /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "tooot/GoogleService-Info.plist"; sourceTree = ""; }; BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; + E633A420281EAEAB000E540F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + E633A425281EAEAB000E540F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + E633A427281EAEAB000E540F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E633A42F281EAF38000E540F /* ShareViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareViewController.swift; path = "../../node_modules/react-native-share-menu/ios/ShareViewController.swift"; sourceTree = ""; }; + E633A431281EB55C000E540F /* ShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ShareExtension-Bridging-Header.h"; sourceTree = ""; }; + E633A436281EB5BC000E540F /* ReactShareViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReactShareViewController.swift; path = "../../node_modules/react-native-share-menu/ios/ReactShareViewController.swift"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; /* End PBXFileReference section */ @@ -55,6 +93,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E633A41D281EAEAB000E540F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 34A37A6C820725DC6DDAA0EE /* libPods-ShareExtension.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -91,6 +137,7 @@ ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */, 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-tooot.a */, + 1B3640FCDF7C4396A68A74D1 /* libPods-ShareExtension.a */, ); name = Frameworks; sourceTree = ""; @@ -116,6 +163,7 @@ 5EE44DD52600124E00A9BCED /* File.swift */, 13B07FAE1A68108700A75B9A /* tooot */, 832341AE1AAA6A7D00B99B32 /* Libraries */, + E633A421281EAEAB000E540F /* ShareExtension */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, D65327D7A22EEC0BE12398D9 /* Pods */, @@ -131,6 +179,7 @@ isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* tooot.app */, + E633A420281EAEAB000E540F /* ShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -149,10 +198,24 @@ children = ( 6C2E3173556A471DD304B334 /* Pods-tooot.debug.xcconfig */, 7A4D352CD337FB3A3BF06240 /* Pods-tooot.release.xcconfig */, + 49AC0972A79258360BEDD73B /* Pods-ShareExtension.debug.xcconfig */, + 312CB8F38010C3E0D27A8663 /* Pods-ShareExtension.release.xcconfig */, ); path = Pods; sourceTree = ""; }; + E633A421281EAEAB000E540F /* ShareExtension */ = { + isa = PBXGroup; + children = ( + E633A436281EB5BC000E540F /* ReactShareViewController.swift */, + E633A42F281EAF38000E540F /* ShareViewController.swift */, + E633A424281EAEAB000E540F /* MainInterface.storyboard */, + E633A427281EAEAB000E540F /* Info.plist */, + E633A431281EB55C000E540F /* ShareExtension-Bridging-Header.h */, + ); + path = ShareExtension; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -168,22 +231,45 @@ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, 49D30A53634620EF2A5C6692 /* [CP] Embed Pods Frameworks */, + E633A42A281EAEAB000E540F /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( + E633A429281EAEAB000E540F /* PBXTargetDependency */, ); name = tooot; productName = tooot; productReference = 13B07F961A680F5B00A75B9A /* tooot.app */; productType = "com.apple.product-type.application"; }; + E633A41F281EAEAB000E540F /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = E633A42E281EAEAB000E540F /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + 8E32C2F04B8226F2A839525E /* [CP] Check Pods Manifest.lock */, + E633A41C281EAEAB000E540F /* Sources */, + E633A41D281EAEAB000E540F /* Frameworks */, + E633A41E281EAEAB000E540F /* Resources */, + 9620878489526FB1EDDF9FB7 /* [CP] Copy Pods Resources */, + E633A438281EB628000E540F /* Bundle React Native code and images */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ShareExtension; + productName = ShareExtension; + productReference = E633A420281EAEAB000E540F /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1330; LastUpgradeCheck = 1320; TargetAttributes = { 13B07F861A680F5B00A75B9A = { @@ -191,6 +277,12 @@ LastSwiftMigration = 1240; ProvisioningStyle = Manual; }; + E633A41F281EAEAB000E540F = { + CreatedOnToolsVersion = 13.3.1; + DevelopmentTeam = 8EGBLQ2MA6; + LastSwiftMigration = 1330; + ProvisioningStyle = Automatic; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "tooot" */; @@ -208,6 +300,7 @@ projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* tooot */, + E633A41F281EAEAB000E540F /* ShareExtension */, ); }; /* End PBXProject section */ @@ -227,6 +320,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E633A41E281EAEAB000E540F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E633A426281EAEAB000E540F /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -306,6 +407,64 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-tooot/Pods-tooot-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 8E32C2F04B8226F2A839525E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ShareExtension-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; + }; + 9620878489526FB1EDDF9FB7 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ShareExtension/Pods-ShareExtension-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ShareExtension/Pods-ShareExtension-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E633A438281EB628000E540F /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export NODE_BINARY=node\nexport ENTRY_FILE=index.share.js\n../node_modules/react-native/scripts/react-native-xcode.sh\n"; + }; FD10A7F022414F080027D42C /* Start Packager */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -339,8 +498,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E633A41C281EAEAB000E540F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E633A437281EB5BC000E540F /* ReactShareViewController.swift in Sources */, + E633A430281EAF38000E540F /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + E633A429281EAEAB000E540F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E633A41F281EAEAB000E540F /* ShareExtension */; + targetProxy = E633A428281EAEAB000E540F /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { isa = PBXVariantGroup; @@ -360,6 +536,14 @@ name = InfoPlist.strings; sourceTree = ""; }; + E633A424281EAEAB000E540F /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + E633A425281EAEAB000E540F /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -367,6 +551,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-tooot.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = tooot/tooot.entitlements; @@ -405,6 +590,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-tooot.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = tooot/tooot.entitlements; @@ -553,6 +739,85 @@ }; name = Release; }; + E633A42C281EAEAB000E540F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 49AC0972A79258360BEDD73B /* Pods-ShareExtension.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 8EGBLQ2MA6; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,6"; + }; + name = Debug; + }; + E633A42D281EAEAB000E540F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 312CB8F38010C3E0D27A8663 /* Pods-ShareExtension.release.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 8EGBLQ2MA6; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,6"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -574,6 +839,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + E633A42E281EAEAB000E540F /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E633A42C281EAEAB000E540F /* Debug */, + E633A42D281EAEAB000E540F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; diff --git a/ios/tooot/Info.plist b/ios/tooot/Info.plist index 932e4fba..b50b00ed 100644 --- a/ios/tooot/Info.plist +++ b/ios/tooot/Info.plist @@ -25,8 +25,10 @@ CFBundleURLTypes + CFBundleTypeRole + Editor CFBundleURLName - gizmos + com.xmflsct.app.tooot CFBundleURLSchemes tooot diff --git a/ios/tooot/tooot.entitlements b/ios/tooot/tooot.entitlements index d7a186c6..9dadf2e3 100644 --- a/ios/tooot/tooot.entitlements +++ b/ios/tooot/tooot.entitlements @@ -6,6 +6,10 @@ development com.apple.security.app-sandbox + com.apple.security.application-groups + + group.com.xmflsct.app.tooot + com.apple.security.device.audio-input com.apple.security.device.camera diff --git a/package.json b/package.json index cc04d4c6..71165f76 100644 --- a/package.json +++ b/package.json @@ -75,15 +75,18 @@ "react-native-fast-image": "8.5.11", "react-native-feather": "1.1.2", "react-native-flash-message": "0.2.1", + "react-native-fs": "^2.19.0", "react-native-gesture-handler": "2.4.1", "react-native-htmlview": "0.16.0", "react-native-pager-view": "5.4.11", "react-native-reanimated": "2.8.0", "react-native-safe-area-context": "4.2.5", "react-native-screens": "3.13.1", + "react-native-share-menu": "^5.0.5", "react-native-svg": "12.3.0", "react-native-swipe-list-view": "3.2.9", "react-native-tab-view": "3.1.1", + "react-native-uuid": "^2.0.1", "react-query": "3.38.0", "react-redux": "8.0.1", "react-timeago": "6.2.1", @@ -103,6 +106,7 @@ "@types/react-dom": "17.0.14", "@types/react-native": "0.67.6", "@types/react-native-base64": "^0.2.0", + "@types/react-native-share-menu": "^5.0.2", "@types/react-timeago": "4.1.3", "@types/valid-url": "1.0.3", "@welldone-software/why-did-you-render": "7.0.1", @@ -137,4 +141,4 @@ } } } -} \ No newline at end of file +} diff --git a/patches/@types+react-native-share-menu+5.0.2.patch b/patches/@types+react-native-share-menu+5.0.2.patch new file mode 100644 index 00000000..09c4417f --- /dev/null +++ b/patches/@types+react-native-share-menu+5.0.2.patch @@ -0,0 +1,25 @@ +diff --git a/node_modules/@types/react-native-share-menu/index.d.ts b/node_modules/@types/react-native-share-menu/index.d.ts +index f52822c..23d9f99 100755 +--- a/node_modules/@types/react-native-share-menu/index.d.ts ++++ b/node_modules/@types/react-native-share-menu/index.d.ts +@@ -6,9 +6,8 @@ + // Minimum TypeScript Version: 3.7 + + export interface ShareData { +- mimeType: string; +- data: string | string[]; +- extraData?: object | undefined; ++ data: {data: {mimeType: string; data: string}[]}; ++ extraData?: {share: {mimeType: string; data: string}[]} | undefined; + } + + export type ShareCallback = (share?: ShareData) => void; +@@ -28,7 +27,7 @@ interface ShareMenuReactView { + dismissExtension(error?: string): void; + openApp(): void; + continueInApp(extraData?: object): void; +- data(): Promise<{mimeType: string, data: string}>; ++ data(): Promise<{data: {mimeType: string; data: string}[]}>; + } + + export const ShareMenuReactView: ShareMenuReactView; diff --git a/patches/react-native-share-menu+5.0.5.patch b/patches/react-native-share-menu+5.0.5.patch new file mode 100644 index 00000000..66fe9eb9 --- /dev/null +++ b/patches/react-native-share-menu+5.0.5.patch @@ -0,0 +1,479 @@ +diff --git a/node_modules/react-native-share-menu/ios/Constants.swift b/node_modules/react-native-share-menu/ios/Constants.swift +index 2811008..63761c3 100644 +--- a/node_modules/react-native-share-menu/ios/Constants.swift ++++ b/node_modules/react-native-share-menu/ios/Constants.swift +@@ -23,7 +23,7 @@ public let COULD_NOT_PARSE_IMG_ERROR = "Couldn't parse image" + public let COULD_NOT_SAVE_FILE_ERROR = "Couldn't save file on disk" + public let NO_EXTENSION_CONTEXT_ERROR = "No extension context attached" + public let NO_DELEGATE_ERROR = "No ReactShareViewDelegate attached" +-public let COULD_NOT_FIND_ITEM_ERROR = "Couldn't find item attached to this share" ++public let COULD_NOT_FIND_ITEMS_ERROR = "Couldn't find items attached to this share" + + // MARK: Keys + +diff --git a/node_modules/react-native-share-menu/ios/Modules/ShareMenu.swift b/node_modules/react-native-share-menu/ios/Modules/ShareMenu.swift +index 6c4922a..1277df2 100644 +--- a/node_modules/react-native-share-menu/ios/Modules/ShareMenu.swift ++++ b/node_modules/react-native-share-menu/ios/Modules/ShareMenu.swift +@@ -9,7 +9,7 @@ class ShareMenu: RCTEventEmitter { + } + } + +- var sharedData: [String:String]? ++ var sharedData: [[String:String]?]? + + static var initialShare: (UIApplication, URL, [UIApplication.OpenURLOptionsKey : Any])? + +@@ -91,7 +91,7 @@ class ShareMenu: RCTEventEmitter { + + let extraData = userDefaults.object(forKey: USER_DEFAULTS_EXTRA_DATA_KEY) as? [String:Any] + +- if let data = userDefaults.object(forKey: USER_DEFAULTS_KEY) as? [String:String] { ++ if let data = userDefaults.object(forKey: USER_DEFAULTS_KEY) as? [[String:String]] { + sharedData = data + dispatchEvent(with: data, and: extraData) + userDefaults.removeObject(forKey: USER_DEFAULTS_KEY) +@@ -100,25 +100,22 @@ class ShareMenu: RCTEventEmitter { + + @objc(getSharedText:) + func getSharedText(callback: RCTResponseSenderBlock) { +- guard var data: [String:Any] = sharedData else { +- callback([]) +- return +- } ++ var data = [DATA_KEY: sharedData] as [String: Any] + + if let bundleId = Bundle.main.bundleIdentifier, let userDefaults = UserDefaults(suiteName: "group.\(bundleId)") { +- data[EXTRA_DATA_KEY] = userDefaults.object(forKey: USER_DEFAULTS_EXTRA_DATA_KEY) as? [String:Any] ++ data[EXTRA_DATA_KEY] = userDefaults.object(forKey: USER_DEFAULTS_EXTRA_DATA_KEY) as? [String: Any] + } else { + print("Error: \(NO_APP_GROUP_ERROR)") + } + + callback([data as Any]) +- sharedData = nil ++ sharedData = [] + } + +- func dispatchEvent(with data: [String:String], and extraData: [String:Any]?) { ++ func dispatchEvent(with data: [[String:String]], and extraData: [String:Any]?) { + guard hasListeners else { return } + +- var finalData = data as [String:Any] ++ var finalData = [DATA_KEY: data] as [String: Any] + if (extraData != nil) { + finalData[EXTRA_DATA_KEY] = extraData + } +diff --git a/node_modules/react-native-share-menu/ios/Modules/ShareMenuReactView.swift b/node_modules/react-native-share-menu/ios/Modules/ShareMenuReactView.swift +index 5d21773..d8a0847 100644 +--- a/node_modules/react-native-share-menu/ios/Modules/ShareMenuReactView.swift ++++ b/node_modules/react-native-share-menu/ios/Modules/ShareMenuReactView.swift +@@ -3,8 +3,9 @@ + // RNShareMenu + // + // Created by Gustavo Parreira on 28/07/2020. +-// ++// Modified by Veselin Stoyanov on 17/04/2021. + ++import Foundation + import MobileCoreServices + + @objc(ShareMenuReactView) +@@ -65,12 +66,12 @@ public class ShareMenuReactView: NSObject { + + let extensionContext = viewDelegate.loadExtensionContext() + +- guard let item = extensionContext.inputItems.first as? NSExtensionItem else { +- print("Error: \(COULD_NOT_FIND_ITEM_ERROR)") ++ guard let items = extensionContext.inputItems as? [NSExtensionItem] else { ++ print("Error: \(COULD_NOT_FIND_ITEMS_ERROR)") + return + } + +- viewDelegate.continueInApp(with: item, and: extraData) ++ viewDelegate.continueInApp(with: items, and: extraData) + } + + @objc(data:reject:) +@@ -82,91 +83,96 @@ public class ShareMenuReactView: NSObject { + return + } + +- extractDataFromContext(context: extensionContext) { (data, mimeType, error) in ++ extractDataFromContext(context: extensionContext) { (data, error) in + guard (error == nil) else { + reject("error", error?.description, nil) + return + } + +- resolve([MIME_TYPE_KEY: mimeType, DATA_KEY: data]) ++ resolve([DATA_KEY: data]) + } + } + +- func extractDataFromContext(context: NSExtensionContext, withCallback callback: @escaping (String?, String?, NSException?) -> Void) { +- let item:NSExtensionItem! = context.inputItems.first as? NSExtensionItem +- let attachments:[AnyObject]! = item.attachments +- +- var urlProvider:NSItemProvider! = nil +- var imageProvider:NSItemProvider! = nil +- var textProvider:NSItemProvider! = nil +- var dataProvider:NSItemProvider! = nil +- +- for provider in attachments { +- if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) { +- urlProvider = provider as? NSItemProvider +- break +- } else if provider.hasItemConformingToTypeIdentifier(kUTTypeText as String) { +- textProvider = provider as? NSItemProvider +- break +- } else if provider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) { +- imageProvider = provider as? NSItemProvider +- break +- } else if provider.hasItemConformingToTypeIdentifier(kUTTypeData as String) { +- dataProvider = provider as? NSItemProvider +- break +- } +- } ++ func extractDataFromContext(context: NSExtensionContext, withCallback callback: @escaping ([Any]?, NSException?) -> Void) { ++ DispatchQueue.global().async { ++ let semaphore = DispatchSemaphore(value: 0) ++ let items:[NSExtensionItem]! = context.inputItems as? [NSExtensionItem] ++ var results: [[String: String]] = [] + +- if (urlProvider != nil) { +- urlProvider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { (item, error) in +- let url: URL! = item as? URL ++ for item in items { ++ guard let attachments = item.attachments else { ++ callback(nil, NSException(name: NSExceptionName(rawValue: "Error"), reason:"couldn't find attachments", userInfo:nil)) ++ return ++ } + +- callback(url.absoluteString, "text/plain", nil) +- } +- } else if (imageProvider != nil) { +- imageProvider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil) { (item, error) in +- let imageUrl: URL! = item as? URL ++ for provider in attachments { ++ if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) { ++ provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { (item, error) in ++ let url: URL! = item as? URL + +- if (imageUrl != nil) { +- if let imageData = try? Data(contentsOf: imageUrl) { +- callback(imageUrl.absoluteString, self.extractMimeType(from: imageUrl), nil) +- } +- } else { +- let image: UIImage! = item as? UIImage ++ results.append([DATA_KEY: url.absoluteString, MIME_TYPE_KEY: "text/plain"]) + +- if (image != nil) { +- let imageData: Data! = image.pngData(); ++ semaphore.signal() ++ } ++ semaphore.wait() ++ } else if provider.hasItemConformingToTypeIdentifier(kUTTypeText as String) { ++ provider.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil) { (item, error) in ++ let text:String! = item as? String ++ ++ results.append([DATA_KEY: text, MIME_TYPE_KEY: "text/plain"]) + +- // Creating temporary URL for image data (UIImage) +- guard let imageURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("TemporaryScreenshot.png") else { +- return ++ semaphore.signal() ++ } ++ semaphore.wait() ++ } else if provider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) { ++ provider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil) { (item, error) in ++ let imageUrl: URL! = item as? URL ++ ++ if (imageUrl != nil) { ++ if let imageData = try? Data(contentsOf: imageUrl) { ++ results.append([DATA_KEY: imageUrl.absoluteString, MIME_TYPE_KEY: self.extractMimeType(from: imageUrl)]) ++ } ++ } else { ++ let image: UIImage! = item as? UIImage ++ ++ if (image != nil) { ++ let imageData: Data! = image.pngData(); ++ ++ // Creating temporary URL for image data (UIImage) ++ guard let imageURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("TemporaryScreenshot.png") else { ++ return ++ } ++ ++ do { ++ // Writing the image to the URL ++ try imageData.write(to: imageURL) ++ ++ results.append([DATA_KEY: imageUrl.absoluteString, MIME_TYPE_KEY: imageURL.extractMimeType()]) ++ } catch { ++ callback(nil, NSException(name: NSExceptionName(rawValue: "Error"), reason:"Can't load image", userInfo:nil)) ++ } ++ } ++ } ++ ++ semaphore.signal() + } ++ semaphore.wait() ++ } else if provider.hasItemConformingToTypeIdentifier(kUTTypeData as String) { ++ provider.loadItem(forTypeIdentifier: kUTTypeData as String, options: nil) { (item, error) in ++ let url: URL! = item as? URL + +- do { +- // Writing the image to the URL +- try imageData.write(to: imageURL) ++ results.append([DATA_KEY: url.absoluteString, MIME_TYPE_KEY: self.extractMimeType(from: url)]) + +- callback(imageURL.absoluteString, imageURL.extractMimeType(), nil) +- } catch { +- callback(nil, nil, NSException(name: NSExceptionName(rawValue: "Error"), reason:"Can't load image", userInfo:nil)) ++ semaphore.signal() + } ++ semaphore.wait() ++ } else { ++ callback(nil, NSException(name: NSExceptionName(rawValue: "Error"), reason:"couldn't find provider", userInfo:nil)) + } + } + } +- } else if (textProvider != nil) { +- textProvider.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil) { (item, error) in +- let text:String! = item as? String + +- callback(text, "text/plain", nil) +- } +- } else if (dataProvider != nil) { +- dataProvider.loadItem(forTypeIdentifier: kUTTypeData as String, options: nil) { (item, error) in +- let url: URL! = item as? URL +- +- callback(url.absoluteString, self.extractMimeType(from: url), nil) +- } +- } else { +- callback(nil, nil, NSException(name: NSExceptionName(rawValue: "Error"), reason:"couldn't find provider", userInfo:nil)) ++ callback(results, nil) + } + } + +diff --git a/node_modules/react-native-share-menu/ios/ReactShareViewController.swift b/node_modules/react-native-share-menu/ios/ReactShareViewController.swift +index 0189ef6..e620257 100644 +--- a/node_modules/react-native-share-menu/ios/ReactShareViewController.swift ++++ b/node_modules/react-native-share-menu/ios/ReactShareViewController.swift +@@ -62,7 +62,7 @@ class ReactShareViewController: ShareViewController, RCTBridgeDelegate, ReactSha + self.openHostApp() + } + +- func continueInApp(with item: NSExtensionItem, and extraData: [String:Any]?) { +- handlePost(item, extraData: extraData) ++ func continueInApp(with items: [NSExtensionItem], and extraData: [String:Any]?) { ++ handlePost(items, extraData: extraData) + } + } +\ No newline at end of file +diff --git a/node_modules/react-native-share-menu/ios/ReactShareViewDelegate.swift b/node_modules/react-native-share-menu/ios/ReactShareViewDelegate.swift +index 0aa4c58..d2bc970 100644 +--- a/node_modules/react-native-share-menu/ios/ReactShareViewDelegate.swift ++++ b/node_modules/react-native-share-menu/ios/ReactShareViewDelegate.swift +@@ -10,5 +10,5 @@ public protocol ReactShareViewDelegate { + + func openApp() + +- func continueInApp(with item: NSExtensionItem, and extraData: [String:Any]?) ++ func continueInApp(with items: [NSExtensionItem], and extraData: [String:Any]?) + } +\ No newline at end of file +diff --git a/node_modules/react-native-share-menu/ios/ShareViewController.swift b/node_modules/react-native-share-menu/ios/ShareViewController.swift +index 7faf6e4..81aef73 100644 +--- a/node_modules/react-native-share-menu/ios/ShareViewController.swift ++++ b/node_modules/react-native-share-menu/ios/ShareViewController.swift +@@ -6,7 +6,9 @@ + // + // Created by Gustavo Parreira on 26/07/2020. + // ++// Modified by Veselin Stoyanov on 17/04/2021. + ++import Foundation + import MobileCoreServices + import UIKit + import Social +@@ -15,6 +17,7 @@ import RNShareMenu + class ShareViewController: SLComposeServiceViewController { + var hostAppId: String? + var hostAppUrlScheme: String? ++ var sharedItems: [Any] = [] + + override func viewDidLoad() { + super.viewDidLoad() +@@ -39,12 +42,12 @@ class ShareViewController: SLComposeServiceViewController { + + override func didSelectPost() { + // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. +- guard let item = extensionContext?.inputItems.first as? NSExtensionItem else { ++ guard let items = extensionContext?.inputItems as? [NSExtensionItem] else { + cancelRequest() + return + } + +- handlePost(item) ++ handlePost(items) + } + + override func configurationItems() -> [Any]! { +@@ -52,24 +55,50 @@ class ShareViewController: SLComposeServiceViewController { + return [] + } + +- func handlePost(_ item: NSExtensionItem, extraData: [String:Any]? = nil) { +- guard let provider = item.attachments?.first else { +- cancelRequest() +- return +- } ++ func handlePost(_ items: [NSExtensionItem], extraData: [String:Any]? = nil) { ++ DispatchQueue.global().async { ++ guard let hostAppId = self.hostAppId else { ++ self.exit(withError: NO_INFO_PLIST_INDENTIFIER_ERROR) ++ return ++ } ++ guard let userDefaults = UserDefaults(suiteName: "group.\(hostAppId)") else { ++ self.exit(withError: NO_APP_GROUP_ERROR) ++ return ++ } + +- if let data = extraData { +- storeExtraData(data) +- } else { +- removeExtraData() +- } ++ if let data = extraData { ++ self.storeExtraData(data) ++ } else { ++ self.removeExtraData() ++ } + +- if provider.isText { +- storeText(withProvider: provider) +- } else if provider.isURL { +- storeUrl(withProvider: provider) +- } else { +- storeFile(withProvider: provider) ++ let semaphore = DispatchSemaphore(value: 0) ++ var results: [Any] = [] ++ ++ for item in items { ++ guard let attachments = item.attachments else { ++ self.cancelRequest() ++ return ++ } ++ ++ for provider in attachments { ++ if provider.isText { ++ self.storeText(withProvider: provider, semaphore) ++ } else if provider.isURL { ++ self.storeUrl(withProvider: provider, semaphore) ++ } else { ++ self.storeFile(withProvider: provider, semaphore) ++ } ++ ++ semaphore.wait() ++ } ++ } ++ ++ userDefaults.set(self.sharedItems, ++ forKey: USER_DEFAULTS_KEY) ++ userDefaults.synchronize() ++ ++ self.openHostApp() + } + } + +@@ -99,7 +128,7 @@ class ShareViewController: SLComposeServiceViewController { + userDefaults.synchronize() + } + +- func storeText(withProvider provider: NSItemProvider) { ++ func storeText(withProvider provider: NSItemProvider, _ semaphore: DispatchSemaphore) { + provider.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil) { (data, error) in + guard (error == nil) else { + self.exit(withError: error.debugDescription) +@@ -109,24 +138,13 @@ class ShareViewController: SLComposeServiceViewController { + self.exit(withError: COULD_NOT_FIND_STRING_ERROR) + return + } +- guard let hostAppId = self.hostAppId else { +- self.exit(withError: NO_INFO_PLIST_INDENTIFIER_ERROR) +- return +- } +- guard let userDefaults = UserDefaults(suiteName: "group.\(hostAppId)") else { +- self.exit(withError: NO_APP_GROUP_ERROR) +- return +- } + +- userDefaults.set([DATA_KEY: text, MIME_TYPE_KEY: "text/plain"], +- forKey: USER_DEFAULTS_KEY) +- userDefaults.synchronize() +- +- self.openHostApp() ++ self.sharedItems.append([DATA_KEY: text, MIME_TYPE_KEY: "text/plain"]) ++ semaphore.signal() + } + } + +- func storeUrl(withProvider provider: NSItemProvider) { ++ func storeUrl(withProvider provider: NSItemProvider, _ semaphore: DispatchSemaphore) { + provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { (data, error) in + guard (error == nil) else { + self.exit(withError: error.debugDescription) +@@ -136,24 +154,13 @@ class ShareViewController: SLComposeServiceViewController { + self.exit(withError: COULD_NOT_FIND_URL_ERROR) + return + } +- guard let hostAppId = self.hostAppId else { +- self.exit(withError: NO_INFO_PLIST_INDENTIFIER_ERROR) +- return +- } +- guard let userDefaults = UserDefaults(suiteName: "group.\(hostAppId)") else { +- self.exit(withError: NO_APP_GROUP_ERROR) +- return +- } +- +- userDefaults.set([DATA_KEY: url.absoluteString, MIME_TYPE_KEY: "text/plain"], +- forKey: USER_DEFAULTS_KEY) +- userDefaults.synchronize() + +- self.openHostApp() ++ self.sharedItems.append([DATA_KEY: url.absoluteString, MIME_TYPE_KEY: "text/plain"]) ++ semaphore.signal() + } + } + +- func storeFile(withProvider provider: NSItemProvider) { ++ func storeFile(withProvider provider: NSItemProvider, _ semaphore: DispatchSemaphore) { + provider.loadItem(forTypeIdentifier: kUTTypeData as String, options: nil) { (data, error) in + guard (error == nil) else { + self.exit(withError: error.debugDescription) +@@ -167,10 +174,6 @@ class ShareViewController: SLComposeServiceViewController { + self.exit(withError: NO_INFO_PLIST_INDENTIFIER_ERROR) + return + } +- guard let userDefaults = UserDefaults(suiteName: "group.\(hostAppId)") else { +- self.exit(withError: NO_APP_GROUP_ERROR) +- return +- } + guard let groupFileManagerContainer = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppId)") + else { +@@ -189,11 +192,8 @@ class ShareViewController: SLComposeServiceViewController { + return + } + +- userDefaults.set([DATA_KEY: filePath.absoluteString, MIME_TYPE_KEY: mimeType], +- forKey: USER_DEFAULTS_KEY) +- userDefaults.synchronize() +- +- self.openHostApp() ++ self.sharedItems.append([DATA_KEY: filePath.absoluteString, MIME_TYPE_KEY: mimeType]) ++ semaphore.signal() + } + } + diff --git a/src/Screens.tsx b/src/Screens.tsx index 4bd2f3a0..6d90d6c8 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -27,6 +27,7 @@ import { addScreenshotListener } from 'expo-screen-capture' import React, { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert, Platform, StatusBar } from 'react-native' +import ShareMenu from 'react-native-share-menu' import { useSelector } from 'react-redux' import * as Sentry from 'sentry-expo' import { useAppDispatch } from './store' @@ -158,6 +159,77 @@ const Screens: React.FC = ({ localCorrupt }) => { } }, [instanceActive, instances, deeplinked]) + // Share Extension + const handleShare = useCallback( + (item?: { + extraData?: { share: { mimeType: string; data: string }[] } + }) => { + if (instanceActive < 0) { + return + } + if ( + !item || + !item.extraData || + !Array.isArray(item.extraData.share) || + !item.extraData.share.length + ) { + return + } + + let text: string | undefined = undefined + let images: { type: string; uri: string }[] = [] + let video: { type: string; uri: string } | undefined = undefined + item.extraData.share.forEach((d, i) => { + const typesImage = ['png', 'jpg', 'jpeg', 'gif'] + const typesVideo = ['mp4', 'm4v', 'mov', 'webm'] + const { mimeType, data } = d + console.log('mimeType', mimeType) + console.log('data', data) + if (mimeType.startsWith('image/')) { + if (!typesImage.includes(mimeType.split('/')[1])) { + console.warn('Image type not supported:', mimeType.split('/')[1]) + return + } + images.push({ type: mimeType.split('/')[1], uri: data }) + } else if (mimeType.startsWith('video/')) { + if (!typesVideo.includes(mimeType.split('/')[1])) { + console.warn('Video type not supported:', mimeType.split('/')[1]) + return + } + video = { type: mimeType.split('/')[1], uri: data } + } else { + if (typesImage.includes(data.split('.').pop() || '')) { + images.push({ type: data.split('.').pop()!, uri: data }) + return + } + if (typesVideo.includes(data.split('.').pop() || '')) { + video = { type: data.split('.').pop()!, uri: data } + return + } + text = !text ? data : text.concat(text, `\n${data}`) + } + }) + navigationRef.navigate('Screen-Compose', { + type: 'share', + text, + images, + video + }) + }, + [instanceActive] + ) + useEffect(() => { + console.log('getting intial share') + ShareMenu.getInitialShare(handleShare) + }, []) + useEffect(() => { + console.log('getting just share') + const listener = ShareMenu.addNewShareListener(handleShare) + return () => { + listener.remove() + } + }, []) + return ( <> diff --git a/src/ShareExtension.tsx b/src/ShareExtension.tsx new file mode 100644 index 00000000..ef9fdd00 --- /dev/null +++ b/src/ShareExtension.tsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from 'react' +import { Appearance, Platform, Pressable, Text } from 'react-native' +import { Circle } from 'react-native-animated-spinkit' +import RNFS from 'react-native-fs' +import { ShareMenuReactView } from 'react-native-share-menu' +import uuid from 'react-native-uuid' + +// mimeType +// text/plain - text only, website URL, video?! +// image/jpeg - image +// video/mp4 - video + +const colors = { + primary: { + light: 'rgb(18, 18, 18)', + dark: 'rgb(180, 180, 180)' + }, + background: { + light: 'rgb(250, 250, 250)', + dark: 'rgb(18, 18, 18)' + } +} + +const clearDir = async (dir: string) => { + try { + const files = await RNFS.readDir(dir) + for (const file of files) { + await RNFS.unlink(file.path) + } + } catch (err: any) { + console.warn(err.message) + } +} + +const ShareExtension = () => { + const [errorMessage, setErrorMessage] = useState() + + useEffect(() => { + ShareMenuReactView.data().then(async ({ data }) => { + console.log('length', data.length) + const newData = [] + switch (Platform.OS) { + case 'ios': + for (const d of data) { + if (d.data.startsWith('file:///')) { + const extension = d.data.split('.').pop()?.toLowerCase() + const filename = `${uuid.v4()}.${extension}` + const groupDirectory = await RNFS.pathForGroup( + 'group.com.xmflsct.app.tooot' + ) + await clearDir(groupDirectory) + const newFilepath = `file://${groupDirectory}/${filename}` + console.log('newFilepath', newFilepath) + try { + await RNFS.copyFile(d.data, newFilepath) + newData.push({ ...d, data: newFilepath }) + } catch (err: any) { + setErrorMessage(err.message) + console.warn(err.message) + } + } else { + newData.push(d) + } + } + break + case 'android': + break + default: + return + } + console.log('new data', newData) + if (!errorMessage) { + ShareMenuReactView.continueInApp({ share: newData }) + } + }) + }, []) + + const theme = Appearance.getColorScheme() || 'light' + + return ( + { + if (errorMessage) { + ShareMenuReactView.dismissExtension(errorMessage) + } + }} + > + {!errorMessage ? ( + + {errorMessage} + + ) : ( + + )} + + ) +} + +export default ShareExtension diff --git a/src/i18n/en/screens/compose.json b/src/i18n/en/screens/compose.json index 15eb4537..e49a3e66 100644 --- a/src/i18n/en/screens/compose.json +++ b/src/i18n/en/screens/compose.json @@ -17,7 +17,8 @@ "conversation": "Toot DM", "reply": "Toot reply", "deleteEdit": "Toot", - "edit": "Toot" + "edit": "Toot", + "share": "Toot" }, "alert": { "default": { diff --git a/src/screens/Compose.tsx b/src/screens/Compose.tsx index 92257ebc..285b849f 100644 --- a/src/screens/Compose.tsx +++ b/src/screens/Compose.tsx @@ -1,3 +1,4 @@ +import apiInstance from '@api/instance' import analytics from '@components/analytics' import { HeaderLeft, HeaderRight } from '@components/Header' import { createNativeStackNavigator } from '@react-navigation/native-stack' @@ -41,6 +42,7 @@ import { useSelector } from 'react-redux' import * as Sentry from 'sentry-expo' import ComposeDraftsList from './Compose/DraftsList' import ComposeEditAttachment from './Compose/EditAttachment' +import { uploadAttachment } from './Compose/Root/Footer/addAttachment' import ComposeContext from './Compose/utils/createContext' import composeInitialState from './Compose/utils/initialState' import composeParseState from './Compose/utils/parseState' @@ -135,7 +137,36 @@ const ScreenCompose: React.FC> = ({ ]) useEffect(() => { + const uploadImage = async ({ + type, + uri + }: { + type: 'image' | 'video' + uri: string + }) => { + await uploadAttachment({ + composeDispatch, + imageInfo: { type, uri, width: 100, height: 100 } + }) + } switch (params?.type) { + case 'share': + if (params.text) { + formatText({ + textInput: 'text', + composeDispatch, + content: params.text, + disableDebounce: true + }) + } + if (params.images?.length) { + params.images.forEach(image => { + uploadImage({ type: 'image', uri: image.uri }) + }) + } else if (params.video) { + uploadImage({ type: 'video', uri: params.video.uri }) + } + break case 'edit': case 'deleteEdit': if (params.incomingStatus.spoiler_text) { diff --git a/src/screens/Compose/Root/Actions.tsx b/src/screens/Compose/Root/Actions.tsx index 85a9d22a..29bd4ff9 100644 --- a/src/screens/Compose/Root/Actions.tsx +++ b/src/screens/Compose/Root/Actions.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next' import { Pressable, StyleSheet, View } from 'react-native' import { useSelector } from 'react-redux' import ComposeContext from '../utils/createContext' -import addAttachment from './Footer/addAttachment' +import chooseAndUploadAttachment from './Footer/addAttachment' const ComposeActions: React.FC = () => { const { showActionSheetWithOptions } = useActionSheet() @@ -41,7 +41,7 @@ const ComposeActions: React.FC = () => { analytics('compose_actions_attachment_press', { count: composeState.attachments.uploads.length }) - return await addAttachment({ + return await chooseAndUploadAttachment({ composeDispatch, showActionSheetWithOptions }) diff --git a/src/screens/Compose/Root/Footer/Attachments.tsx b/src/screens/Compose/Root/Footer/Attachments.tsx index 60c9e1b7..60d47e20 100644 --- a/src/screens/Compose/Root/Footer/Attachments.tsx +++ b/src/screens/Compose/Root/Footer/Attachments.tsx @@ -27,7 +27,7 @@ import { import { Circle } from 'react-native-animated-spinkit' import ComposeContext from '../../utils/createContext' import { ExtendedAttachment } from '../../utils/types' -import addAttachment from './addAttachment' +import chooseAndUploadAttachment from './addAttachment' export interface Props { accessibleRefAttachments: RefObject @@ -218,7 +218,7 @@ const ComposeAttachments: React.FC = ({ accessibleRefAttachments }) => { ]} onPress={async () => { analytics('compose_attachment_add_container_press') - await addAttachment({ composeDispatch, showActionSheetWithOptions }) + await chooseAndUploadAttachment({ composeDispatch, showActionSheetWithOptions }) }} >