diff --git a/app/mobile/App.js b/app/mobile/App.js index b7f7066a..08213db9 100644 --- a/app/mobile/App.js +++ b/app/mobile/App.js @@ -1,5 +1,5 @@ import 'react-native-gesture-handler'; -import React from 'react'; +import { useEffect, useState } from 'react'; import { NativeRouter } from "react-router-native"; import { Routes, Route } from 'react-router-dom'; import { StoreContextProvider } from 'context/StoreContext'; @@ -17,12 +17,31 @@ import { Root } from 'src/root/Root'; import { Access } from 'src/access/Access'; import { Dashboard } from 'src/dashboard/Dashboard'; import { Session } from 'src/session/Session'; +import ReceiveSharingIntent from 'react-native-receive-sharing-intent'; // silence warning: Sending `onAnimatedValueUpdate` with no listeners registered //LogBox.ignoreLogs(['Sending']); export default function App() { + const [sharing, setSharing] = useState(); + + useEffect(() => { + ReceiveSharingIntent.getReceivedFiles(files => { + setSharing(files); + }, + (error) =>{ + console.log(error); + }, + 'org.coredb.databag' + ); + }, []); + + const clearSharing = () => { + setSharing(null); + ReceiveSharingIntent.clearReceivedFiles(); + }; + return ( @@ -42,7 +61,7 @@ export default function App() { } /> } /> } /> - } /> + } /> diff --git a/app/mobile/ios/Databag.xcodeproj/project.pbxproj b/app/mobile/ios/Databag.xcodeproj/project.pbxproj index 9a603e42..84b94ec7 100644 --- a/app/mobile/ios/Databag.xcodeproj/project.pbxproj +++ b/app/mobile/ios/Databag.xcodeproj/project.pbxproj @@ -13,6 +13,9 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 46D8108CBA031189090AFC14 /* Pods_Databag_DatabagTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1181227D40684F18A9414840 /* Pods_Databag_DatabagTests.framework */; }; 7B13A774299E21170048D0DD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7B13A773299E21170048D0DD /* GoogleService-Info.plist */; }; + 7B4A533D29F39E250036F3ED /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4A533C29F39E250036F3ED /* ShareViewController.swift */; }; + 7B4A534029F39E250036F3ED /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B4A533E29F39E250036F3ED /* MainInterface.storyboard */; }; + 7B4A534429F39E250036F3ED /* Sharing.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7B4A533A29F39E250036F3ED /* Sharing.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; A0B1EC4533FCFC5940B5FD7F /* Pods_Databag.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9462C028F47F083241BB7941 /* Pods_Databag.framework */; }; /* End PBXBuildFile section */ @@ -25,8 +28,29 @@ remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = Databag; }; + 7B4A534229F39E250036F3ED /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7B4A533929F39E250036F3ED; + remoteInfo = Sharing; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 7B4A534529F39E250036F3ED /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 7B4A534429F39E250036F3ED /* Sharing.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 00E356EE1AD99517003FC87E /* DatabagTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatabagTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -42,6 +66,11 @@ 5709B34CF0A7D63546082F79 /* Pods-Databag.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Databag.release.xcconfig"; path = "Target Support Files/Pods-Databag/Pods-Databag.release.xcconfig"; sourceTree = ""; }; 5B7EB9410499542E8C5724F5 /* Pods-Databag-DatabagTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Databag-DatabagTests.debug.xcconfig"; path = "Target Support Files/Pods-Databag-DatabagTests/Pods-Databag-DatabagTests.debug.xcconfig"; sourceTree = ""; }; 7B13A773299E21170048D0DD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 7B4A533A29F39E250036F3ED /* Sharing.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Sharing.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7B4A533C29F39E250036F3ED /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + 7B4A533F29F39E250036F3ED /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 7B4A534129F39E250036F3ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7B4A534929F39ED90036F3ED /* Sharing.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sharing.entitlements; sourceTree = ""; }; 7B6135A429B68A7B0094A6E7 /* Databag.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Databag.entitlements; path = Databag/Databag.entitlements; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Databag/LaunchScreen.storyboard; sourceTree = ""; }; 89C6BE57DB24E9ADA2F236DE /* Pods-Databag-DatabagTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Databag-DatabagTests.release.xcconfig"; path = "Target Support Files/Pods-Databag-DatabagTests/Pods-Databag-DatabagTests.release.xcconfig"; sourceTree = ""; }; @@ -66,6 +95,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7B4A533729F39E250036F3ED /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -110,6 +146,17 @@ name = Frameworks; sourceTree = ""; }; + 7B4A533B29F39E250036F3ED /* Sharing */ = { + isa = PBXGroup; + children = ( + 7B4A534929F39ED90036F3ED /* Sharing.entitlements */, + 7B4A533C29F39E250036F3ED /* ShareViewController.swift */, + 7B4A533E29F39E250036F3ED /* MainInterface.storyboard */, + 7B4A534129F39E250036F3ED /* Info.plist */, + ); + path = Sharing; + sourceTree = ""; + }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( @@ -124,6 +171,7 @@ 13B07FAE1A68108700A75B9A /* Databag */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* DatabagTests */, + 7B4A533B29F39E250036F3ED /* Sharing */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, BBD78D7AC51CEA395F1C20DB /* Pods */, @@ -138,6 +186,7 @@ children = ( 13B07F961A680F5B00A75B9A /* Databag.app */, 00E356EE1AD99517003FC87E /* DatabagTests.xctest */, + 7B4A533A29F39E250036F3ED /* Sharing.appex */, ); name = Products; sourceTree = ""; @@ -190,22 +239,42 @@ 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */, E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, D347E695755653CE8D39CFDD /* [CP-User] [RNFB] Core Configuration */, + 7B4A534529F39E250036F3ED /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 7B4A534329F39E250036F3ED /* PBXTargetDependency */, ); name = Databag; productName = Databag; productReference = 13B07F961A680F5B00A75B9A /* Databag.app */; productType = "com.apple.product-type.application"; }; + 7B4A533929F39E250036F3ED /* Sharing */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7B4A534829F39E250036F3ED /* Build configuration list for PBXNativeTarget "Sharing" */; + buildPhases = ( + 7B4A533629F39E250036F3ED /* Sources */, + 7B4A533729F39E250036F3ED /* Frameworks */, + 7B4A533829F39E250036F3ED /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sharing; + productName = Sharing; + productReference = 7B4A533A29F39E250036F3ED /* Sharing.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1410; LastUpgradeCheck = 1210; ORGANIZATIONNAME = CoreDB; TargetAttributes = { @@ -216,6 +285,9 @@ 13B07F861A680F5B00A75B9A = { LastSwiftMigration = 1120; }; + 7B4A533929F39E250036F3ED = { + CreatedOnToolsVersion = 14.1; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Databag" */; @@ -233,6 +305,7 @@ targets = ( 13B07F861A680F5B00A75B9A /* Databag */, 00E356ED1AD99517003FC87E /* DatabagTests */, + 7B4A533929F39E250036F3ED /* Sharing */, ); }; /* End PBXProject section */ @@ -255,6 +328,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7B4A533829F39E250036F3ED /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B4A534029F39E250036F3ED /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -438,6 +519,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7B4A533629F39E250036F3ED /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B4A533D29F39E250036F3ED /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -446,8 +535,24 @@ target = 13B07F861A680F5B00A75B9A /* Databag */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; + 7B4A534329F39E250036F3ED /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7B4A533929F39E250036F3ED /* Sharing */; + targetProxy = 7B4A534229F39E250036F3ED /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 7B4A533E29F39E250036F3ED /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7B4A533F29F39E250036F3ED /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; @@ -504,6 +609,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-Databag.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Databag/Databag.entitlements; @@ -538,6 +644,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-Databag.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Databag/Databag.entitlements; @@ -566,6 +673,84 @@ }; name = Release; }; + 7B4A534629F39E250036F3ED /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = Sharing/Sharing.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 3P65PQ7SUR; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Sharing/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Sharing; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 CoreDB. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; + 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 = org.coredb.databag.Sharing; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7B4A534729F39E250036F3ED /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = Sharing/Sharing.entitlements; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 3P65PQ7SUR; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Sharing/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Sharing; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 CoreDB. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.coredb.databag.Sharing; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -724,6 +909,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7B4A534829F39E250036F3ED /* Build configuration list for PBXNativeTarget "Sharing" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7B4A534629F39E250036F3ED /* Debug */, + 7B4A534729F39E250036F3ED /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Databag" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/app/mobile/ios/Databag/AppDelegate.mm b/app/mobile/ios/Databag/AppDelegate.mm index fe2c224a..652656d9 100644 --- a/app/mobile/ios/Databag/AppDelegate.mm +++ b/app/mobile/ios/Databag/AppDelegate.mm @@ -1,6 +1,7 @@ #import "AppDelegate.h" #import +#import #import @implementation AppDelegate @@ -35,4 +36,11 @@ return true; } +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options +{ + return [RCTLinkingManager application:application openURL:url options:options]; +} + @end diff --git a/app/mobile/ios/Databag/Databag.entitlements b/app/mobile/ios/Databag/Databag.entitlements index 903def2a..4217b16c 100644 --- a/app/mobile/ios/Databag/Databag.entitlements +++ b/app/mobile/ios/Databag/Databag.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.security.application-groups + + group.org.coredb.databag + diff --git a/app/mobile/ios/Databag/Info.plist b/app/mobile/ios/Databag/Info.plist index 86510025..cddec778 100644 --- a/app/mobile/ios/Databag/Info.plist +++ b/app/mobile/ios/Databag/Info.plist @@ -88,5 +88,19 @@ UIViewControllerBasedStatusBarAppearance + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + org.coredb.databag + + + + + diff --git a/app/mobile/ios/Podfile.lock b/app/mobile/ios/Podfile.lock index a96da8ab..c32f9a84 100644 --- a/app/mobile/ios/Podfile.lock +++ b/app/mobile/ios/Podfile.lock @@ -325,6 +325,8 @@ PODS: - React-Core - react-native-keep-awake (1.1.0): - React-Core + - react-native-receive-sharing-intent (2.0.0): + - React-Core - react-native-rsa-native (2.0.5): - React - react-native-safe-area-context (4.5.0): @@ -516,6 +518,7 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - react-native-document-picker (from `../node_modules/react-native-document-picker`) - "react-native-keep-awake (from `../node_modules/@sayem314/react-native-keep-awake`)" + - react-native-receive-sharing-intent (from `../node_modules/react-native-receive-sharing-intent`) - react-native-rsa-native (from `../node_modules/react-native-rsa-native`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`) @@ -610,6 +613,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-document-picker" react-native-keep-awake: :path: "../node_modules/@sayem314/react-native-keep-awake" + react-native-receive-sharing-intent: + :path: "../node_modules/react-native-receive-sharing-intent" react-native-rsa-native: :path: "../node_modules/react-native-rsa-native" react-native-safe-area-context: @@ -709,6 +714,7 @@ SPEC CHECKSUMS: React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207 react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c react-native-keep-awake: acbee258db16483744910f0da3ace39eb9ab47fd + react-native-receive-sharing-intent: 62ab28c50e6ae56d32b9e841d7452091312a0bc7 react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261 diff --git a/app/mobile/ios/Sharing/Base.lproj/MainInterface.storyboard b/app/mobile/ios/Sharing/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..286a5089 --- /dev/null +++ b/app/mobile/ios/Sharing/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/mobile/ios/Sharing/Info.plist b/app/mobile/ios/Sharing/Info.plist new file mode 100644 index 00000000..b89404f1 --- /dev/null +++ b/app/mobile/ios/Sharing/Info.plist @@ -0,0 +1,46 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + TRUEPREDICATE + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + NSExtension + + NSExtensionAttributes + + PHSupportedMediaTypes + + Video + Image + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 100 + NSExtensionActivationSupportsMovieWithMaxCount + 100 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + + diff --git a/app/mobile/ios/Sharing/ShareViewController.swift b/app/mobile/ios/Sharing/ShareViewController.swift new file mode 100644 index 00000000..034c7985 --- /dev/null +++ b/app/mobile/ios/Sharing/ShareViewController.swift @@ -0,0 +1,338 @@ + +import UIKit +import Social +import MobileCoreServices +import Photos + +class ShareViewController: SLComposeServiceViewController { + let hostAppBundleIdentifier = "org.coredb.databag" + let shareProtocol = "org.coredb.databag" + let sharedKey = "ShareKey" + var sharedMedia: [SharedMediaFile] = [] + var sharedText: [String] = [] + let imageContentType = kUTTypeImage as String + let videoContentType = kUTTypeMovie as String + let textContentType = kUTTypeText as String + let urlContentType = kUTTypeURL as String + let fileURLType = kUTTypeFileURL as String; + + override func isContentValid() -> Bool { + return true + } + + override func viewDidLoad() { + super.viewDidLoad(); + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let content = extensionContext!.inputItems[0] as? NSExtensionItem { + if let contents = content.attachments { + for (index, attachment) in (contents).enumerated() { + if attachment.hasItemConformingToTypeIdentifier(imageContentType) { + handleImages(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(textContentType) { + handleText(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(fileURLType) { + handleFiles(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) { + handleUrl(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(videoContentType) { + handleVideos(content: content, attachment: attachment, index: index) + } + } + } + } + } + + override func didSelectPost() { + print("didSelectPost"); + } + + override func configurationItems() -> [Any]! { + // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. + return [] + } + + private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in + + if error == nil, let item = data as? String, let this = self { + + this.sharedText.append(item) + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.sharedText, forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .text) + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in + + if error == nil, let item = data as? URL, let this = self { + + this.sharedText.append(item.absoluteString) + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.sharedText, forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .text) + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in + + if error == nil, let url = data as? URL, let this = self { + // this.redirectToHostApp(type: .media) + // Always copy + let fileExtension = this.getExtension(from: url, type: .video) + let newName = UUID().uuidString + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! + .appendingPathComponent("\(newName).\(fileExtension)") + let copied = this.copyFile(at: url, to: newPath) + if(copied) { + this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image)) + } + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .media) + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: videoContentType, options:nil) { [weak self] data, error in + + if error == nil, let url = data as? URL, let this = self { + + // Always copy + let fileExtension = this.getExtension(from: url, type: .video) + let newName = UUID().uuidString + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! + .appendingPathComponent("\(newName).\(fileExtension)") + let copied = this.copyFile(at: url, to: newPath) + if(copied) { + guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else { + return + } + this.sharedMedia.append(sharedFile) + } + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .media) + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in + + if error == nil, let url = data as? URL, let this = self { + + // Always copy + let newName = this.getFileName(from :url) + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! + .appendingPathComponent("\(newName)") + let copied = this.copyFile(at: url, to: newPath) + if (copied) { + this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file)) + } + + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .file) + } + + } else { + self?.dismissWithError() + } + } + } + + private func dismissWithError() { + print("[ERROR] Error loading data!") + let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert) + + let action = UIAlertAction(title: "Error", style: .cancel) { _ in + self.dismiss(animated: true, completion: nil) + } + + alert.addAction(action) + present(alert, animated: true, completion: nil) + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + private func redirectToHostApp(type: RedirectType) { + let url = URL(string: "\(shareProtocol)://dataUrl=\(sharedKey)#\(type)") + var responder = self as UIResponder? + let selectorOpenURL = sel_registerName("openURL:") + + while (responder != nil) { + if (responder?.responds(to: selectorOpenURL))! { + let _ = responder?.perform(selectorOpenURL, with: url) + } + responder = responder!.next + } + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + enum RedirectType { + case media + case text + case file + } + + func getExtension(from url: URL, type: SharedMediaType) -> String { + let parts = url.lastPathComponent.components(separatedBy: ".") + var ex: String? = nil + if (parts.count > 1) { + ex = parts.last + } + + if (ex == nil) { + switch type { + case .image: + ex = "PNG" + case .video: + ex = "MP4" + case .file: + ex = "TXT" + } + } + return ex ?? "Unknown" + } + + func getFileName(from url: URL) -> String { + var name = url.lastPathComponent + + if (name == "") { + name = UUID().uuidString + "." + getExtension(from: url, type: .file) + } + + return name + } + + func copyFile(at srcURL: URL, to dstURL: URL) -> Bool { + do { + if FileManager.default.fileExists(atPath: dstURL.path) { + try FileManager.default.removeItem(at: dstURL) + } + try FileManager.default.copyItem(at: srcURL, to: dstURL) + } catch (let error) { + print("Cannot copy item at \(srcURL) to \(dstURL): \(error)") + return false + } + return true + } + + private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? { + let asset = AVAsset(url: forVideo) + let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded() + let thumbnailPath = getThumbnailPath(for: forVideo) + + if FileManager.default.fileExists(atPath: thumbnailPath.path) { + return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) + } + + var saved = false + let assetImgGenerate = AVAssetImageGenerator(asset: asset) + assetImgGenerate.appliesPreferredTrackTransform = true + // let scale = UIScreen.main.scale + assetImgGenerate.maximumSize = CGSize(width: 360, height: 360) + do { + let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil) + try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath) + saved = true + } catch { + saved = false + } + + return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil + + } + + private func getThumbnailPath(for url: URL) -> URL { + let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "") + let path = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! + .appendingPathComponent("\(fileName).jpg") + return path + } + + class SharedMediaFile: Codable { + var path: String; // can be image, video or url path. It can also be text content + var thumbnail: String?; // video thumbnail + var duration: Double?; // video duration in milliseconds + var type: SharedMediaType; + + + init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) { + self.path = path + self.thumbnail = thumbnail + self.duration = duration + self.type = type + } + + // Debug method to print out SharedMediaFile details in the console + func toString() { + print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)") + } + } + + enum SharedMediaType: Int, Codable { + case image + case video + case file + } + + func toData(data: [SharedMediaFile]) -> Data { + let encodedData = try? JSONEncoder().encode(data) + return encodedData! + } +} + +extension Array { + subscript (safe index: UInt) -> Element? { + return Int(index) < count ? self[Int(index)] : nil + } +} + diff --git a/app/mobile/ios/Sharing/Sharing.entitlements b/app/mobile/ios/Sharing/Sharing.entitlements new file mode 100644 index 00000000..ca30a699 --- /dev/null +++ b/app/mobile/ios/Sharing/Sharing.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.coredb.databag + + + diff --git a/app/mobile/package.json b/app/mobile/package.json index f0bc91dc..8107ce7d 100644 --- a/app/mobile/package.json +++ b/app/mobile/package.json @@ -34,6 +34,7 @@ "react-native-incall-manager": "^4.0.1", "react-native-reanimated": "^2.14.4", "react-native-reanimated-carousel": "^3.3.0", + "react-native-receive-sharing-intent": "^2.0.0", "react-native-rsa-native": "^2.0.5", "react-native-safe-area-context": "^4.5.0", "react-native-screens": "^3.20.0", diff --git a/app/mobile/src/session/Session.jsx b/app/mobile/src/session/Session.jsx index ce6ed02b..c656ed26 100644 --- a/app/mobile/src/session/Session.jsx +++ b/app/mobile/src/session/Session.jsx @@ -25,7 +25,9 @@ import { ProfileIcon } from './profileIcon/ProfileIcon'; import { CardsIcon } from './cardsIcon/CardsIcon'; import { Logo } from 'utils/Logo'; import { Call } from './call/Call'; +import { Sharing } from './sharing/Sharing'; import splash from 'images/session.png'; +import { useNavigate } from 'react-router-dom'; const ConversationStack = createStackNavigator(); const ProfileStack = createStackNavigator(); @@ -314,11 +316,13 @@ function DetailDrawerScreen({ navParams }) { ); } -export function Session() { +export function Session({ sharing, clearSharing }) { + const [intent, setIntent] = useState(sharing) const [ringing, setRinging] = useState([]); const { state, actions } = useSession(); const drawerParams = { drawerPosition: 'right', headerShown: false, swipeEnabled: false, drawerType: 'front' }; + const navigate = useNavigate(); const [ dmChannel, setDmChannel ] = useState(null); const addChannel = async (cardId) => { @@ -326,6 +330,22 @@ export function Session() { setDmChannel({ id }); }; + const setShare = async (cardId, channelId) => { + console.log("SET SHARE CHANNEL"); + clearSharing(); + } + const clearShare = async () => { + console.log("CLEAR SHARE CHANNEL"); + clearSharing(); + } + + useEffect(() => { + console.log("COMPARE", sharing, intent); + if (sharing != intent && sharing != null) { + navigate('/'); + } + }, [sharing, intent, navigate]) + useEffect(() => { let incoming = []; for (let i = 0; i < state.ringing.length; i++) { @@ -440,7 +460,15 @@ export function Session() { supportedOrientations={['portrait', 'landscape']} > - + + + + ); } diff --git a/app/mobile/src/session/sharing/Sharing.jsx b/app/mobile/src/session/sharing/Sharing.jsx new file mode 100644 index 00000000..d95ac8da --- /dev/null +++ b/app/mobile/src/session/sharing/Sharing.jsx @@ -0,0 +1,16 @@ +import { Text, View } from 'react-native'; +import { useSharing } from './useSharing.hook'; +import { styles } from './Sharing.styled'; + +export function Sharing({ setShare, clearShare }) { + + const { state, actions } = useSharing(); + + return ( + + + SHARING + + + ) +} diff --git a/app/mobile/src/session/sharing/Sharing.styled.js b/app/mobile/src/session/sharing/Sharing.styled.js new file mode 100644 index 00000000..80db8078 --- /dev/null +++ b/app/mobile/src/session/sharing/Sharing.styled.js @@ -0,0 +1,21 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + sharingBase: { + display: 'flex', + width: '100%', + height: '100%', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(52, 52, 52, 0.8)' + }, + sharingFrame: { + backgroundColor: Colors.formBackground, + padding: 8, + borderRadius: 4, + display: 'flex', + alignItems: 'center', + }, +}); + diff --git a/app/mobile/src/session/sharing/useSharing.hook.js b/app/mobile/src/session/sharing/useSharing.hook.js new file mode 100644 index 00000000..28f4cbeb --- /dev/null +++ b/app/mobile/src/session/sharing/useSharing.hook.js @@ -0,0 +1,16 @@ +import { useState } from 'react'; + +export function useSharing() { + const [state, setState] = useState({ + }); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const actions = { + }; + + return { state, actions }; +} + diff --git a/app/mobile/yarn.lock b/app/mobile/yarn.lock index f2b7a182..3f833abd 100644 --- a/app/mobile/yarn.lock +++ b/app/mobile/yarn.lock @@ -6366,6 +6366,11 @@ react-native-reanimated@^2.14.4: setimmediate "^1.0.5" string-hash-64 "^1.0.3" +react-native-receive-sharing-intent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-native-receive-sharing-intent/-/react-native-receive-sharing-intent-2.0.0.tgz#0d835387976e2343f85f0172522e83233a09c0d9" + integrity sha512-JFSO8mZm/hU0EJQYhC5z2m1iiMwJhD9CQ/hYQ8t1UQ9mQynoS/yo+hjX2T6hFXa8mtzlwO/BFwKZjBaVNPdWuw== + react-native-rsa-native@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/react-native-rsa-native/-/react-native-rsa-native-2.0.5.tgz"