diff --git a/Localization/app.json b/Localization/app.json index dc758bb7b..0f471d973 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -805,5 +805,10 @@ "unfollow": "Unfollow" } } + }, + "extension": { + "open_in": { + "invalid_link_error": "This doesn't seem to be a valid Mastodon link." + } } } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 8272d8427..4e03ca0c8 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -28,8 +28,14 @@ 2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; }; 2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; }; 2A506CF6292D040100059C37 /* HashtagTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF5292D040100059C37 /* HashtagTimelineHeaderView.swift */; }; + 2A64515E29642A8A00CD8B8A /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */; }; + 2A64516929642A8B00CD8B8A /* OpenInActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 2A71F541296DBDA80049F54A /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A71F53D296DBDA80049F54A /* Media.xcassets */; }; + 2A71F542296DBDA80049F54A /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = 2A71F53E296DBDA80049F54A /* Action.js */; }; + 2A71F543296DBDA80049F54A /* ActionRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */; }; 2A76F75C2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */; }; 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */; }; + 2A90A157296EEE500026C155 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A90A156296EEE500026C155 /* MastodonSDKDynamic */; }; 2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */; }; 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; @@ -454,6 +460,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 2A64516729642A8B00CD8B8A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A64515C29642A8A00CD8B8A; + remoteInfo = FollowActionExtension; + }; DB427DE925BAA00100D1B89D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; @@ -492,6 +505,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 2A90A159296EEE500026C155 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; 9E44C7212967AD17004B2A72 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -521,6 +544,7 @@ dstSubfolderSpec = 13; files = ( DB8FABCE26AEC7B2008E5AF4 /* MastodonIntent.appex in Embed Foundation Extensions */, + 2A64516929642A8B00CD8B8A /* OpenInActionExtension.appex in Embed Foundation Extensions */, DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed Foundation Extensions */, DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed Foundation Extensions */, ); @@ -553,6 +577,12 @@ 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = ""; }; 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = ""; }; 2A506CF5292D040100059C37 /* HashtagTimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineHeaderView.swift; sourceTree = ""; }; + 2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; + 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A71F53D296DBDA80049F54A /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = ""; }; + 2A71F53E296DBDA80049F54A /* Action.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = Action.js; sourceTree = ""; }; + 2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionRequestHandler.swift; sourceTree = ""; }; + 2A71F540296DBDA80049F54A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineHeaderViewActionButton.swift; sourceTree = ""; }; 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+NextAccount.swift"; sourceTree = ""; }; 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Translate.swift"; sourceTree = ""; }; @@ -1108,6 +1138,15 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2A64515A29642A8A00CD8B8A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A90A157296EEE500026C155 /* MastodonSDKDynamic in Frameworks */, + 2A64515E29642A8A00CD8B8A /* UniformTypeIdentifiers.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DB427DCF25BAA00100D1B89D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1291,6 +1330,17 @@ path = FollowedTags; sourceTree = ""; }; + 2A71F53C296DBDA80049F54A /* OpenInActionExtension */ = { + isa = PBXGroup; + children = ( + 2A71F53D296DBDA80049F54A /* Media.xcassets */, + 2A71F53E296DBDA80049F54A /* Action.js */, + 2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */, + 2A71F540296DBDA80049F54A /* Info.plist */, + ); + path = OpenInActionExtension; + sourceTree = ""; + }; 2D152A8A25C295B8009AA50C /* Content */ = { isa = PBXGroup; children = ( @@ -1509,6 +1559,7 @@ F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */, DB8FAB9E26AEC3A2008E5AF4 /* Intents.framework */, DB8FABA926AEC3A2008E5AF4 /* IntentsUI.framework */, + 2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */, ); name = Frameworks; sourceTree = ""; @@ -1826,6 +1877,7 @@ DBF8AE14263293E400C9C23C /* NotificationService */, DBC6461326A170AB00B0E31B /* ShareActionExtension */, DB8FABC826AEC7B2008E5AF4 /* MastodonIntent */, + 2A71F53C296DBDA80049F54A /* OpenInActionExtension */, DB427DD325BAA00100D1B89D /* Products */, 1EBA4F56E920856A3FC84ACB /* Pods */, 3FE14AD363ED19AE7FF210A6 /* Frameworks */, @@ -1843,6 +1895,7 @@ DBF8AE13263293E400C9C23C /* NotificationService.appex */, DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */, DB8FABC626AEC7B2008E5AF4 /* MastodonIntent.appex */, + 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */, ); name = Products; sourceTree = ""; @@ -2784,6 +2837,27 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 2A64515C29642A8A00CD8B8A /* OpenInActionExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A64516A29642A8B00CD8B8A /* Build configuration list for PBXNativeTarget "OpenInActionExtension" */; + buildPhases = ( + 2A64515929642A8A00CD8B8A /* Sources */, + 2A64515A29642A8A00CD8B8A /* Frameworks */, + 2A64515B29642A8A00CD8B8A /* Resources */, + 2A90A159296EEE500026C155 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OpenInActionExtension; + packageProductDependencies = ( + 2A90A156296EEE500026C155 /* MastodonSDKDynamic */, + ); + productName = FollowActionExtension; + productReference = 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; DB427DD125BAA00100D1B89D /* Mastodon */ = { isa = PBXNativeTarget; buildConfigurationList = DB427DFC25BAA00100D1B89D /* Build configuration list for PBXNativeTarget "Mastodon" */; @@ -2805,6 +2879,7 @@ DBF8AE19263293E400C9C23C /* PBXTargetDependency */, DBC6461B26A170AB00B0E31B /* PBXTargetDependency */, DB8FABCD26AEC7B2008E5AF4 /* PBXTargetDependency */, + 2A64516829642A8B00CD8B8A /* PBXTargetDependency */, ); name = Mastodon; packageProductDependencies = ( @@ -2923,9 +2998,12 @@ DB427DCA25BAA00100D1B89D /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1250; + LastSwiftUpdateCheck = 1420; LastUpgradeCheck = 1400; TargetAttributes = { + 2A64515C29642A8A00CD8B8A = { + CreatedOnToolsVersion = 14.2; + }; DB427DD125BAA00100D1B89D = { CreatedOnToolsVersion = 12.4; LastSwiftMigration = 1300; @@ -2995,11 +3073,21 @@ DBF8AE12263293E400C9C23C /* NotificationService */, DBC6461126A170AB00B0E31B /* ShareActionExtension */, DB8FABC526AEC7B2008E5AF4 /* MastodonIntent */, + 2A64515C29642A8A00CD8B8A /* OpenInActionExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 2A64515B29642A8A00CD8B8A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A71F541296DBDA80049F54A /* Media.xcassets in Resources */, + 2A71F542296DBDA80049F54A /* Action.js in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DB427DD025BAA00100D1B89D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3218,6 +3306,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 2A64515929642A8A00CD8B8A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A71F543296DBDA80049F54A /* ActionRequestHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DB427DCE25BAA00100D1B89D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -3678,6 +3774,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 2A64516829642A8B00CD8B8A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A64515C29642A8A00CD8B8A /* OpenInActionExtension */; + targetProxy = 2A64516729642A8B00CD8B8A /* PBXContainerItemProxy */; + }; DB427DEA25BAA00100D1B89D /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DB427DD125BAA00100D1B89D /* Mastodon */; @@ -3833,6 +3934,123 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 2A64516B29642A8B00CD8B8A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Icon; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = OpenInActionExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Open using Mastodon"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.OpenInActionExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A64516C29642A8B00CD8B8A /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Icon; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = OpenInActionExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Open using Mastodon"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.OpenInActionExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; + 2A64516D29642A8B00CD8B8A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Icon; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = OpenInActionExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Open using Mastodon"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.OpenInActionExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2A64516E29642A8B00CD8B8A /* Release Snapshot */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Icon; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = OpenInActionExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Open using Mastodon"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.OpenInActionExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Release Snapshot"; + }; DB427DFA25BAA00100D1B89D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3959,6 +4177,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; @@ -3989,6 +4208,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; @@ -4176,6 +4396,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7CB58D292DA7ACEF179A9050 /* Pods-Mastodon.profile.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; @@ -4466,6 +4687,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 0655B257371274BEB7EB1C19 /* Pods-Mastodon.release snapshot.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; @@ -4651,6 +4873,17 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 2A64516A29642A8B00CD8B8A /* Build configuration list for PBXNativeTarget "OpenInActionExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A64516B29642A8B00CD8B8A /* Debug */, + 2A64516C29642A8B00CD8B8A /* Profile */, + 2A64516D29642A8B00CD8B8A /* Release */, + 2A64516E29642A8B00CD8B8A /* Release Snapshot */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; DB427DCD25BAA00100D1B89D /* Build configuration list for PBXProject "Mastodon" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -4731,6 +4964,10 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 2A90A156296EEE500026C155 /* MastodonSDKDynamic */ = { + isa = XCSwiftPackageProductDependency; + productName = MastodonSDKDynamic; + }; 357FEEAE29523D470021C9DC /* MastodonSDKDynamic */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDKDynamic; diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 382755c77..3e4669901 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -242,41 +242,56 @@ extension SceneDelegate { if !UIApplication.shared.canOpenURL(url) { return } + #if DEBUG print("source application = \(sendingAppID ?? "Unknown")") print("url = \(url)") + #endif switch url.host { case "post": showComposeViewController() case "profile": let components = url.pathComponents - if components.count == 2 && components[0] == "/" { - let addr = components[1] - if let authContext = coordinator?.authContext { - let profileViewModel = RemoteProfileViewModel( - context: AppContext.shared, - authContext: authContext, - acct: components[1] - ) - self.coordinator?.present( - scene: .profile(viewModel: profileViewModel), - from: nil, - transition: .show - ) - } - } + guard + components.count == 2, + components[0] == "/", + let authContext = coordinator?.authContext + else { return } + + let profileViewModel = RemoteProfileViewModel( + context: AppContext.shared, + authContext: authContext, + acct: components[1] + ) + self.coordinator?.present( + scene: .profile(viewModel: profileViewModel), + from: nil, + transition: .show + ) case "status": let components = url.pathComponents - if components.count == 2 && components[0] == "/" { - let statusId = components[1] - // View post from user - if let authContext = coordinator?.authContext { - let threadViewModel = RemoteThreadViewModel(context: AppContext.shared, - authContext: authContext, - statusID: statusId) - coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) - } - } + guard + components.count == 2, + components[0] == "/", + let authContext = coordinator?.authContext + else { return } + let statusId = components[1] + // View post from user + let threadViewModel = RemoteThreadViewModel( + context: AppContext.shared, + authContext: authContext, + statusID: statusId + ) + coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) + case "search": + let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems + guard + let authContext = coordinator?.authContext, + let searchQuery = queryItems?.first(where: { $0.name == "query" })?.value + else { return } + + let viewModel = SearchDetailViewModel(authContext: authContext, initialSearchText: searchQuery) + coordinator?.present(scene: .searchDetail(viewModel: viewModel), from: nil, transition: .show) default: return } diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 9034cf0c5..fb516496a 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -469,6 +469,12 @@ public enum L10n { } } } + public enum Extension { + public enum OpenIn { + /// This doesn't seem to be a valid Mastodon link. + public static let invalidLinkError = L10n.tr("Localizable", "Extension.OpenIn.InvalidLinkError", fallback: "This doesn't seem to be a valid Mastodon link.") + } + } public enum Scene { public enum AccountList { /// Add Account diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index acafb44ab..2c2d295b3 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -513,3 +513,4 @@ You can’t go wrong with any of our recommend servers, so regardless of which o "Scene.Privacy.Button.confirm" = "I agree"; "Scene.Privacy.Policy.Ios" = "Privacy Policy - Mastodon for iOS"; "Scene.Privacy.Policy.Server" = "Privacy Policy - %@"; +"Extension.OpenIn.InvalidLinkError" = "This doesn't seem to be a valid Mastodon link."; diff --git a/OpenInActionExtension/Action.js b/OpenInActionExtension/Action.js new file mode 100644 index 000000000..b99051b15 --- /dev/null +++ b/OpenInActionExtension/Action.js @@ -0,0 +1,47 @@ +// +// Action.js +// OpenInActionExtension +// +// Created by Marcus Kida on 03.01.23. +// + +var Action = function() {}; + +Action.prototype = { + + run: function(arguments) { + var payload = { + "username": detectUsername(), + "url": document.documentURI + } + + arguments.completionFunction(payload) + }, + + finalize: function(arguments) { + let alertMessage = arguments["alert"] + if (alertMessage) { + alert(alertMessage) + } else { + window.location = arguments["openURL"] + } + } + +}; + +function detectUsername() { + var uriUsername = document.documentURI.match("(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))") + + if (Array.isArray(uriUsername)) { + return uriUsername[0] + } + + var querySelector = document.head.querySelector('[property="profile:username"]') + if (querySelector !== null && typeof querySelector === "object") { + return querySelector.content + } + + return undefined +} + +var ExtensionPreprocessingJS = new Action diff --git a/OpenInActionExtension/ActionRequestHandler.swift b/OpenInActionExtension/ActionRequestHandler.swift new file mode 100644 index 000000000..fdc73f03a --- /dev/null +++ b/OpenInActionExtension/ActionRequestHandler.swift @@ -0,0 +1,115 @@ +// +// ActionRequestHandler.swift +// OpenInActionExtension +// +// Created by Marcus Kida on 03.01.23. +// + +import Combine +import UIKit +import MobileCoreServices +import UniformTypeIdentifiers +import MastodonSDK +import MastodonLocalization + +class ActionRequestHandler: NSObject, NSExtensionRequestHandling { + var extensionContext: NSExtensionContext? + var cancellables = [AnyCancellable]() + + func beginRequest(with context: NSExtensionContext) { + // Do not call super in an Action extension with no user interface + self.extensionContext = context + + let itemProvider = context.inputItems + .compactMap({ $0 as? NSExtensionItem }) + .reduce([NSItemProvider](), { partialResult, acc in + var nextResult = partialResult + nextResult += acc.attachments ?? [] + return nextResult + }) + .filter({ $0.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) }) + .first + + guard let itemProvider = itemProvider else { + return doneWithInvalidLink() + } + + itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] item, error in + DispatchQueue.main.async { + guard + let dictionary = item as? NSDictionary, + let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary + else { + self?.doneWithInvalidLink() + return + } + + if let username = results["username"] as? String { + self?.completeWithOpenUserProfile(username) + } else if let url = results["url"] as? String { + self?.continueWithSearch(url) + } else { + self?.doneWithInvalidLink() + } + } + }) + } +} + +private extension ActionRequestHandler { + func completeWithOpenUserProfile(_ username: String) { + doneWithResults([ + "openURL": "mastodon://profile/\(username)" + ]) + } + + func continueWithSearch(_ query: String) { + guard + let url = URL(string: query), + let host = url.host + else { + return doneWithInvalidLink() + } + + Mastodon.API + .Instance + .instance( + session: .shared, + domain: host + ) + .receive(on: DispatchQueue.main) + .sink { _ in + // no-op + } receiveValue: { [weak self] response in + guard response.value.version != nil else { + self?.doneWithInvalidLink() + return + } + guard let query = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + self?.doneWithInvalidLink() + return + } + self?.doneWithResults( + ["openURL": "mastodon://search?query=\(query)"] + ) + } + .store(in: &cancellables) + } + + func doneWithInvalidLink() { + doneWithResults(["alert": L10n.Extension.OpenIn.invalidLinkError]) + } + + func doneWithResults(_ resultsForJavaScriptFinalizeArg: [String: Any]?) { + if let resultsForJavaScriptFinalize = resultsForJavaScriptFinalizeArg { + let resultsDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize] + let resultsProvider = NSItemProvider(item: resultsDictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier) + let resultsItem = NSExtensionItem() + resultsItem.attachments = [resultsProvider] + self.extensionContext!.completeRequest(returningItems: [resultsItem], completionHandler: nil) + } else { + self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + self.extensionContext = nil + } +} diff --git a/OpenInActionExtension/Info.plist b/OpenInActionExtension/Info.plist new file mode 100644 index 000000000..0ac1aced5 --- /dev/null +++ b/OpenInActionExtension/Info.plist @@ -0,0 +1,41 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsFileWithMaxCount + 0 + NSExtensionActivationSupportsImageWithMaxCount + 0 + NSExtensionActivationSupportsMovieWithMaxCount + 0 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + NSExtensionJavaScriptPreprocessingFile + Action + NSExtensionServiceAllowsFinderPreviewItem + + NSExtensionServiceAllowsTouchBarItem + + NSExtensionServiceFinderPreviewIconName + NSActionTemplate + NSExtensionServiceTouchBarBezelColorName + TouchBarBezel + NSExtensionServiceTouchBarIconName + NSActionTemplate + + NSExtensionPointIdentifier + com.apple.services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ActionRequestHandler + + + diff --git a/OpenInActionExtension/Media.xcassets/Contents.json b/OpenInActionExtension/Media.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/OpenInActionExtension/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OpenInActionExtension/Media.xcassets/Icon.appiconset/Contents.json b/OpenInActionExtension/Media.xcassets/Icon.appiconset/Contents.json new file mode 100644 index 000000000..a09e0e886 --- /dev/null +++ b/OpenInActionExtension/Media.xcassets/Icon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "MastodonActionExtensionIcon@3x.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OpenInActionExtension/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png b/OpenInActionExtension/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png new file mode 100644 index 000000000..1a9999052 Binary files /dev/null and b/OpenInActionExtension/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png differ diff --git a/OpenInActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json b/OpenInActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json new file mode 100644 index 000000000..3a6e6ec7e --- /dev/null +++ b/OpenInActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.733", + "green" : "0.110", + "red" : "0.263" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.600", + "red" : "0.600" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +}