diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4e03ca0c8..4caf3ebe5 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -573,6 +573,7 @@ 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+URL.swift"; sourceTree = ""; }; 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = ""; }; 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = ""; }; + 2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = ""; }; 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = ""; }; 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = ""; }; 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = ""; }; @@ -1333,6 +1334,7 @@ 2A71F53C296DBDA80049F54A /* OpenInActionExtension */ = { isa = PBXGroup; children = ( + 2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */, 2A71F53D296DBDA80049F54A /* Media.xcassets */, 2A71F53E296DBDA80049F54A /* Action.js */, 2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */, @@ -3939,6 +3941,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = Icon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = OpenInActionExtension/OpenInActionExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -3969,6 +3972,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = Icon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = OpenInActionExtension/OpenInActionExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -3998,6 +4002,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = Icon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = OpenInActionExtension/OpenInActionExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -4027,6 +4032,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = Icon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = OpenInActionExtension/OpenInActionExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; diff --git a/OpenInActionExtension/Action.js b/OpenInActionExtension/Action.js index b99051b15..12a659ef9 100644 --- a/OpenInActionExtension/Action.js +++ b/OpenInActionExtension/Action.js @@ -8,40 +8,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"] + const alertMessage = arguments["alert"] + const openURL = arguments["openURL"] + if (alertMessage) { alert(alertMessage) - } else { - window.location = arguments["openURL"] + } else if (openURL) { + window.location = 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 index fdc73f03a..69215dcb2 100644 --- a/OpenInActionExtension/ActionRequestHandler.swift +++ b/OpenInActionExtension/ActionRequestHandler.swift @@ -9,6 +9,7 @@ import Combine import UIKit import MobileCoreServices import UniformTypeIdentifiers +import MastodonCore import MastodonSDK import MastodonLocalization @@ -16,6 +17,11 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling { var extensionContext: NSExtensionContext? var cancellables = [AnyCancellable]() + /// Capturing a static shared instance of AppContext here as otherwise there + /// will be lifecycle issues and we don't want to keep multiple AppContexts around + /// in case there another Action Extension process is spawned + private static let appContext = AppContext() + func beginRequest(with context: NSExtensionContext) { // Do not call super in an Action extension with no user interface self.extensionContext = context @@ -44,10 +50,8 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling { return } - if let username = results["username"] as? String { - self?.completeWithOpenUserProfile(username) - } else if let url = results["url"] as? String { - self?.continueWithSearch(url) + if let url = results["url"] as? String { + self?.performSearch(for: url) } else { self?.doneWithInvalidLink() } @@ -56,13 +60,53 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling { } } +// Search API private extension ActionRequestHandler { - func completeWithOpenUserProfile(_ username: String) { - doneWithResults([ - "openURL": "mastodon://profile/\(username)" - ]) + func performSearch(for url: String) { + guard + let activeAuthenticationBox = Self.appContext + .authenticationService + .mastodonAuthenticationBoxes + .first + else { + return doneWithResults(nil) + } + + Mastodon.API + .V2 + .Search + .search( + session: .shared, + domain: activeAuthenticationBox.domain, + query: .init(q: url, resolve: true), + authorization: activeAuthenticationBox.userAuthorization + ) + .receive(on: DispatchQueue.main) + .sink { completion in + // no-op + } receiveValue: { [weak self] result in + let value = result.value + if let foundAccount = value.accounts.first { + self?.doneWithResults([ + "openURL": "mastodon://profile/\(foundAccount.acct)" + ]) + } else if let foundStatus = value.statuses.first { + self?.doneWithResults([ + "openURL": "mastodon://status/\(foundStatus.id)" + ]) + } else if let foundHashtag = value.hashtags.first { + self?.continueWithSearch(foundHashtag.name) + } else { + self?.continueWithSearch(url) + } + } + .store(in: &cancellables) + } - +} + +// Fallback to In-App Search +private extension ActionRequestHandler { func continueWithSearch(_ query: String) { guard let url = URL(string: query), @@ -95,7 +139,10 @@ private extension ActionRequestHandler { } .store(in: &cancellables) } - +} + +// Action response handling +private extension ActionRequestHandler { func doneWithInvalidLink() { doneWithResults(["alert": L10n.Extension.OpenIn.invalidLinkError]) } diff --git a/OpenInActionExtension/OpenInActionExtension.entitlements b/OpenInActionExtension/OpenInActionExtension.entitlements new file mode 100644 index 000000000..c3bc3f816 --- /dev/null +++ b/OpenInActionExtension/OpenInActionExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.joinmastodon.app + + +