From 20887ab282505dc7bd512e124c91fc7ea15a5b7f Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Mon, 30 Oct 2023 20:06:58 -0700 Subject: [PATCH] initial implementation for sharing and opening lemmy links on iOS --- ios/Podfile | 5 + ios/Podfile.lock | 2 +- ios/Runner.xcodeproj/project.pbxproj | 276 ++++++++++++++ ios/Runner/Info.plist | 14 + ios/Runner/Runner.entitlements | 10 + .../Base.lproj/MainInterface.storyboard | 24 ++ ios/Share Extension/Info.plist | 46 +++ .../Share Extension.entitlements | 10 + ios/Share Extension/ShareViewController.swift | 360 ++++++++++++++++++ .../deep_links_cubit/deep_links_cubit.dart | 7 +- lib/thunder/pages/thunder_page.dart | 11 + 11 files changed, 763 insertions(+), 2 deletions(-) create mode 100644 ios/Runner/Runner.entitlements create mode 100644 ios/Share Extension/Base.lproj/MainInterface.storyboard create mode 100644 ios/Share Extension/Info.plist create mode 100644 ios/Share Extension/Share Extension.entitlements create mode 100644 ios/Share Extension/ShareViewController.swift diff --git a/ios/Podfile b/ios/Podfile index f1344d775..2e1f41af4 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -32,6 +32,11 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + target 'Share Extension' do + inherit! :search_paths + end + target 'RunnerTests' do inherit! :search_paths end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 310e75c30..de5bece8f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -130,6 +130,6 @@ SPEC CHECKSUMS: url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a -PODFILE CHECKSUM: 0b10e1f269736e810e149457dba01ee3b062862b +PODFILE CHECKSUM: ed0a586c06ef26cfbf592ac2dbb8cfa478196b7c COCOAPODS: 1.12.1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 357efe4e3..b57afec4f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -16,6 +16,10 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + AC830B1060A24BF52B180771 /* Pods_Share_Extension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33A15AB6BAC6E3D071625893 /* Pods_Share_Extension.framework */; }; + D41E4B5E2AF0681400245C07 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41E4B5D2AF0681400245C07 /* ShareViewController.swift */; }; + D41E4B612AF0681400245C07 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D41E4B5F2AF0681400245C07 /* MainInterface.storyboard */; }; + D41E4B652AF0681400245C07 /* Share Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D41E4B5B2AF0681400245C07 /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -26,6 +30,13 @@ remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; + D41E4B632AF0681400245C07 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = D41E4B5A2AF0681400245C07; + remoteInfo = "Share Extension"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -39,6 +50,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + D41E4B662AF0681400245C07 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + D41E4B652AF0681400245C07 /* Share Extension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -47,14 +69,18 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 1E73B8D7EDC35EF93810A262 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 291951A5327D9D467D1488FD /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 2D83F122FD421D376735FF65 /* Pods-Share Extension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.profile.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.profile.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 33A15AB6BAC6E3D071625893 /* Pods_Share_Extension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Share_Extension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 6E18ED6773F06EE553D38BE3 /* Pods-Share Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.release.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.release.xcconfig"; sourceTree = ""; }; 713BDC3B32C9E0A0B3CE939D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 807A4AF0F3FE369F0F15CC80 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 8FA0CE1EFF78D616D4D46434 /* Pods-Share Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.debug.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -62,6 +88,12 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D41E4B5B2AF0681400245C07 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + D41E4B5D2AF0681400245C07 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + D41E4B602AF0681400245C07 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + D41E4B622AF0681400245C07 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D41E4B6B2AF069AB00245C07 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + D41E4B6C2AF06A9A00245C07 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = ""; }; EBDAACB2112528B97EF7E9C7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FC3DF76093C9AE1242276289 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; FD47FFE0663417A4488AFA02 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -76,6 +108,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D41E4B582AF0681400245C07 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AC830B1060A24BF52B180771 /* Pods_Share_Extension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FB718DEF8B5A3A75758D5F87 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -100,6 +140,7 @@ children = ( EBDAACB2112528B97EF7E9C7 /* Pods_Runner.framework */, FD47FFE0663417A4488AFA02 /* Pods_RunnerTests.framework */, + 33A15AB6BAC6E3D071625893 /* Pods_Share_Extension.framework */, ); name = Frameworks; sourceTree = ""; @@ -120,6 +161,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + D41E4B5C2AF0681400245C07 /* Share Extension */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 9C8A64A973C65A5221057239 /* Pods */, @@ -132,6 +174,7 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + D41E4B5B2AF0681400245C07 /* Share Extension.appex */, ); name = Products; sourceTree = ""; @@ -139,6 +182,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + D41E4B6B2AF069AB00245C07 /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -160,10 +204,24 @@ 0C497F05B1916C1BB182243F /* Pods-RunnerTests.debug.xcconfig */, 291951A5327D9D467D1488FD /* Pods-RunnerTests.release.xcconfig */, 807A4AF0F3FE369F0F15CC80 /* Pods-RunnerTests.profile.xcconfig */, + 8FA0CE1EFF78D616D4D46434 /* Pods-Share Extension.debug.xcconfig */, + 6E18ED6773F06EE553D38BE3 /* Pods-Share Extension.release.xcconfig */, + 2D83F122FD421D376735FF65 /* Pods-Share Extension.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; + D41E4B5C2AF0681400245C07 /* Share Extension */ = { + isa = PBXGroup; + children = ( + D41E4B6C2AF06A9A00245C07 /* Share Extension.entitlements */, + D41E4B5D2AF0681400245C07 /* ShareViewController.swift */, + D41E4B5F2AF0681400245C07 /* MainInterface.storyboard */, + D41E4B622AF0681400245C07 /* Info.plist */, + ); + path = "Share Extension"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -192,6 +250,7 @@ buildPhases = ( 4507590DE32484ED80117CCE /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, + D41E4B662AF0681400245C07 /* Embed Foundation Extensions */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, @@ -202,12 +261,31 @@ buildRules = ( ); dependencies = ( + D41E4B642AF0681400245C07 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + D41E4B5A2AF0681400245C07 /* Share Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = D41E4B6A2AF0681400245C07 /* Build configuration list for PBXNativeTarget "Share Extension" */; + buildPhases = ( + A5612180AE549559D50C101C /* [CP] Check Pods Manifest.lock */, + D41E4B572AF0681400245C07 /* Sources */, + D41E4B582AF0681400245C07 /* Frameworks */, + D41E4B592AF0681400245C07 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Share Extension"; + productName = "Share Extension"; + productReference = D41E4B5B2AF0681400245C07 /* Share Extension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -215,6 +293,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1500; LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -226,6 +305,9 @@ CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; + D41E4B5A2AF0681400245C07 = { + CreatedOnToolsVersion = 15.0; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -243,6 +325,7 @@ targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, + D41E4B5A2AF0681400245C07 /* Share Extension */, ); }; /* End PBXProject section */ @@ -266,6 +349,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D41E4B592AF0681400245C07 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D41E4B612AF0681400245C07 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -361,6 +452,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + A5612180AE549559D50C101C /* [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-Share Extension-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; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -381,6 +494,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D41E4B572AF0681400245C07 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D41E4B5E2AF0681400245C07 /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -389,6 +510,11 @@ target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; + D41E4B642AF0681400245C07 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D41E4B5A2AF0681400245C07 /* Share Extension */; + targetProxy = D41E4B632AF0681400245C07 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -408,6 +534,14 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + D41E4B5F2AF0681400245C07 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D41E4B602AF0681400245C07 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -465,9 +599,12 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CUSTOM_GROUP_ID = group.com.hjiangsu.thunder; DEVELOPMENT_TEAM = L7P596HY6P; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -650,9 +787,12 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CUSTOM_GROUP_ID = group.com.hjiangsu.thunder; DEVELOPMENT_TEAM = L7P596HY6P; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -673,9 +813,12 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CUSTOM_GROUP_ID = group.com.hjiangsu.thunder; DEVELOPMENT_TEAM = L7P596HY6P; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -691,6 +834,129 @@ }; name = Release; }; + D41E4B672AF0681400245C07 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8FA0CE1EFF78D616D4D46434 /* Pods-Share Extension.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.com.hjiangsu.thunder; + DEVELOPMENT_TEAM = L7P596HY6P; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Share Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Share Extension"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.hjiangsu.thunder.Share-Extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D41E4B682AF0681400245C07 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6E18ED6773F06EE553D38BE3 /* Pods-Share Extension.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.com.hjiangsu.thunder; + DEVELOPMENT_TEAM = L7P596HY6P; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Share Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Share Extension"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.hjiangsu.thunder.Share-Extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D41E4B692AF0681400245C07 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2D83F122FD421D376735FF65 /* Pods-Share Extension.profile.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.com.hjiangsu.thunder; + DEVELOPMENT_TEAM = L7P596HY6P; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Share Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Share Extension"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.hjiangsu.thunder.Share-Extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -724,6 +990,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D41E4B6A2AF0681400245C07 /* Build configuration list for PBXNativeTarget "Share Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D41E4B672AF0681400245C07 /* Debug */, + D41E4B682AF0681400245C07 /* Release */, + D41E4B692AF0681400245C07 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index c953447e4..ac8051282 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -50,6 +50,20 @@ UIStatusBarHidden + AppGroupId + $(CUSTOM_GROUP_ID) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + + + NSPhotoLibraryUsageDescription We need Photos access to allow you to save media. diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 000000000..cabbcadb4 --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.hjiangsu.thunder + + + diff --git a/ios/Share Extension/Base.lproj/MainInterface.storyboard b/ios/Share Extension/Base.lproj/MainInterface.storyboard new file mode 100644 index 000000000..286a50894 --- /dev/null +++ b/ios/Share Extension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Share Extension/Info.plist b/ios/Share Extension/Info.plist new file mode 100644 index 000000000..e0d88273e --- /dev/null +++ b/ios/Share Extension/Info.plist @@ -0,0 +1,46 @@ + + + + + AppGroupId + $(CUSTOM_GROUP_ID) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + NSExtension + + NSExtensionAttributes + + PHSupportedMediaTypes + + + Video + + Image + + NSExtensionActivationRule + + + NSExtensionActivationSupportsText + + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + NSExtensionActivationSupportsImageWithMaxCount + 100 + + NSExtensionActivationSupportsMovieWithMaxCount + 100 + + + NSExtensionActivationSupportsFileWithMaxCount + 1 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + diff --git a/ios/Share Extension/Share Extension.entitlements b/ios/Share Extension/Share Extension.entitlements new file mode 100644 index 000000000..cabbcadb4 --- /dev/null +++ b/ios/Share Extension/Share Extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.hjiangsu.thunder + + + diff --git a/ios/Share Extension/ShareViewController.swift b/ios/Share Extension/ShareViewController.swift new file mode 100644 index 000000000..d8a986b36 --- /dev/null +++ b/ios/Share Extension/ShareViewController.swift @@ -0,0 +1,360 @@ +// +// ShareViewController.swift +// Share Extension +// +// Created by Hamlet Jiang Su on 2023-10-30. +// +import UIKit +import Social +import MobileCoreServices +import Photos + +class ShareViewController: SLComposeServiceViewController { + var hostAppBundleIdentifier = "" + var appGroupId = "" + 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 + } + + private func loadIds() { + // loading Share extension App Id + let shareExtensionAppBundleIdentifier = Bundle.main.bundleIdentifier!; + + // convert ShareExtension id to host app id + // By default it is remove last part of id after last point + // For example: com.test.ShareExtension -> com.test + let lastIndexOfPoint = shareExtensionAppBundleIdentifier.lastIndex(of: "."); + hostAppBundleIdentifier = String(shareExtensionAppBundleIdentifier[.. + appGroupId = (Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as? String) ?? "group.\(hostAppBundleIdentifier)"; + } + + override func viewDidLoad() { + super.viewDidLoad(); + + // load group and app id from build info + loadIds(); + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. + 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: this.appGroupId) + 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: this.appGroupId) + 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 { + + // Always copy + let fileName = this.getFileName(from: url, type: .image) + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)! + .appendingPathComponent(fileName) + 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: this.appGroupId) + 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 fileName = this.getFileName(from: url, type: .video) + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)! + .appendingPathComponent(fileName) + 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: this.appGroupId) + 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 fileName = this.getFileName(from :url, type: .file) + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)! + .appendingPathComponent(fileName) + 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: this.appGroupId) + 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) { + // ids may not loaded yet so we need loadIds here too + loadIds(); + let url = URL(string: "ShareMedia-\(hostAppBundleIdentifier)://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, type: SharedMediaType) -> String { + var name = url.lastPathComponent + + if (name.isEmpty) { + name = UUID().uuidString + "." + getExtension(from: url, type: type) + } + + 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: appGroupId)! + .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/lib/thunder/cubits/deep_links_cubit/deep_links_cubit.dart b/lib/thunder/cubits/deep_links_cubit/deep_links_cubit.dart index c13d63896..9c6ab7512 100644 --- a/lib/thunder/cubits/deep_links_cubit/deep_links_cubit.dart +++ b/lib/thunder/cubits/deep_links_cubit/deep_links_cubit.dart @@ -61,9 +61,14 @@ class DeepLinksCubit extends Cubit { } } + /// Handle URI parsing + Future handleURI(String uri) async { + emit(state.copyWith(deepLinkStatus: DeepLinkStatus.loading)); + getLinkType(uri); + } + /// Handle incoming links - the ones that the app will recieve from the OS /// while already started. - Future handleIncomingLinks() async { emit(state.copyWith( deepLinkStatus: DeepLinkStatus.loading, diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index 6a1e3465d..e8d30c92e 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -28,6 +28,7 @@ import 'package:thunder/feed/bloc/feed_bloc.dart'; import 'package:thunder/feed/view/feed_page.dart'; import 'package:thunder/feed/widgets/feed_fab.dart'; import 'package:thunder/instance/bloc/instance_bloc.dart'; +import 'package:thunder/instances.dart'; import 'package:thunder/post/utils/post.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/thunder/cubits/deep_links_cubit/deep_links_cubit.dart'; @@ -138,6 +139,16 @@ class _ThunderState extends State { ) { if (value?.isNotEmpty ?? false) { final uri = Uri.tryParse(value!); + + if (Platform.isIOS) { + // When handling incoming lemmy links, open up the corresponding page on Thunder + // Check against a list of lemmy instances first. If none is found, fallback to creating a new post + if (uri?.host != null && instances.contains(uri?.host)) { + context.read().handleURI(uri.toString()); + return; + } + } + if (uri?.isAbsolute == true) { if (context.mounted) navigateToCreatePostPage(context, url: uri.toString(), prePopulated: true); } else {