From 7582fb5ab5ae8c9fac121b9647d7b3f27bc5ce04 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 6 Jan 2023 13:14:18 +0100 Subject: [PATCH 1/7] fix(deeplinking): Fix profile resolving didn't use WebFinger so resolving non-local profiles might fail --- .../Profile/RemoteProfileViewModel.swift | 74 +++++++++---------- .../Service/API/APIService+Account.swift | 31 -------- 2 files changed, 37 insertions(+), 68 deletions(-) diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index 89ff03660..705d2806f 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -90,41 +90,41 @@ final class RemoteProfileViewModel: ProfileViewModel { } // end Task } - init(context: AppContext, authContext: AuthContext, acct: String) { - super.init(context: context, authContext: authContext, optionalMastodonUser: nil) - - let domain = authContext.mastodonAuthenticationBox.domain - let authorization = authContext.mastodonAuthenticationBox.userAuthorization - Just(acct) - .asyncMap { acct in - try await context.apiService.accountSearch( - domain: domain, - query: .init(acct: acct), - authorization: authorization - ) - } - .retry(3) - .receive(on: DispatchQueue.main) - .sink { completion in - switch completion { - case .failure(let error): - // TODO: handle error - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, acct, error.localizedDescription) - case .finished: - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetched", ((#file as NSString).lastPathComponent), #line, #function, acct) - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - let managedObjectContext = context.managedObjectContext - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id) - guard let mastodonUser = managedObjectContext.safeFetch(request).first else { - assertionFailure() - return - } - self.user = mastodonUser - } - .store(in: &disposeBag) - } + init(context: AppContext, authContext: AuthContext, acct: String){ + super.init(context: context, authContext: authContext, optionalMastodonUser: nil) + + let domain = authContext.mastodonAuthenticationBox.domain + let authenticationBox = authContext.mastodonAuthenticationBox + + Just(acct) + .asyncMap { acct -> Mastodon.Response.Content in + try await context.apiService.search( + query: .init(q: acct, type: .accounts, resolve: true), + authenticationBox: authenticationBox + ).map { $0.accounts.first } + } + .retry(3) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .failure(let error): + // TODO: handle error + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, acct, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetched", ((#file as NSString).lastPathComponent), #line, #function, acct) + } + } receiveValue: { [weak self] response in + guard let self = self, let value = response.value else { return } + let managedObjectContext = context.managedObjectContext + let request = MastodonUser.sortedFetchRequest + request.fetchLimit = 1 + request.predicate = MastodonUser.predicate(domain: domain, id: value.id) + guard let mastodonUser = managedObjectContext.safeFetch(request).first else { + assertionFailure() + return + } + self.user = mastodonUser + } + .store(in: &disposeBag) + } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index 34d398633..d68984587 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -240,34 +240,3 @@ extension APIService { return result } } - -extension APIService { - public func accountSearch( - domain: String, - query: Mastodon.API.Account.AccountLookupQuery, - authorization: Mastodon.API.OAuth.Authorization - ) async throws -> Mastodon.Response.Content { - let response = try await Mastodon.API.Account.lookupAccount( - session: session, - domain: domain, - query: query, - authorization: authorization - ).singleOutput() - - // user - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: response.value, - cache: nil, - networkDate: response.networkDate - ) - ) - } - - return response - } -} From 09cec923bbfa8eede2696ca54b7639104627dc8e Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 6 Jan 2023 13:54:42 +0100 Subject: [PATCH 2/7] WIP: Begin implementing FollowActionExtension --- FollowActionExtension/Action.js | 38 +++ .../ActionRequestHandler.swift | 91 ++++++++ FollowActionExtension/Info.plist | 41 ++++ .../Media.xcassets/Contents.json | 6 + .../Icon.appiconset/Contents.json | 14 ++ .../MastodonActionExtensionIcon@3x.png | Bin 0 -> 87538 bytes .../TouchBarBezel.colorset/Contents.json | 14 ++ Mastodon.xcodeproj/project.pbxproj | 219 +++++++++++++++++- 8 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 FollowActionExtension/Action.js create mode 100644 FollowActionExtension/ActionRequestHandler.swift create mode 100644 FollowActionExtension/Info.plist create mode 100644 FollowActionExtension/Media.xcassets/Contents.json create mode 100644 FollowActionExtension/Media.xcassets/Icon.appiconset/Contents.json create mode 100644 FollowActionExtension/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png create mode 100644 FollowActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json diff --git a/FollowActionExtension/Action.js b/FollowActionExtension/Action.js new file mode 100644 index 000000000..3f8f93723 --- /dev/null +++ b/FollowActionExtension/Action.js @@ -0,0 +1,38 @@ +// +// Action.js +// FollowActionExtension +// +// Created by Marcus Kida on 03.01.23. +// + +var Action = function() {}; + +Action.prototype = { + + run: function(arguments) { + var username = document.documentURI.match("@(.+)@([a-z0-9]+\.[a-z0-9]+)")[0]; + + if (!username) { + username = document.head.querySelector('[property="profile:username"]').content + } + + console.log("username" + username) + + arguments.completionFunction({ "username" : username }) + }, + + finalize: function(arguments) { + + var openURL = arguments["openURL"] + var error = arguments["error"] + + if (error) { + alert(error) + } else if (openURL) { + window.location = openURL + } + } + +}; + +var ExtensionPreprocessingJS = new Action diff --git a/FollowActionExtension/ActionRequestHandler.swift b/FollowActionExtension/ActionRequestHandler.swift new file mode 100644 index 000000000..bcfc2a66b --- /dev/null +++ b/FollowActionExtension/ActionRequestHandler.swift @@ -0,0 +1,91 @@ +// +// ActionRequestHandler.swift +// FollowActionExtension +// +// Created by Marcus Kida on 03.01.23. +// + +import UIKit +import MobileCoreServices +import UniformTypeIdentifiers + +class ActionRequestHandler: NSObject, NSExtensionRequestHandling { + + var extensionContext: NSExtensionContext? + + func beginRequest(with context: NSExtensionContext) { + // Do not call super in an Action extension with no user interface + self.extensionContext = context + + var found = false + + // Find the item containing the results from the JavaScript preprocessing. + outer: + for item in context.inputItems as! [NSExtensionItem] { + if let attachments = item.attachments { + for itemProvider in attachments { + if itemProvider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) { + itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { (item, error) in + guard + let dictionary = item as? [String: Any], + let res = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any]? ?? [:] + else { + + self.doneWithResults( + ["error": "Failed to find username. Are you sure this is a Mastodon Profile page?"] + ) + return + } + + OperationQueue.main.addOperation { + self.itemLoadCompletedWithPreprocessingResults(res) + } + }) + found = true + break outer + } + } + } + } + + if !found { + self.doneWithResults(nil) + } + } + + func itemLoadCompletedWithPreprocessingResults(_ javaScriptPreprocessingResults: [String: Any]) { + guard let username = javaScriptPreprocessingResults["username"] as? String else { return } + + doneWithResults([ + "openURL": "mastodon://profile/\(username)" + ]) + } + + func doneWithResults(_ resultsForJavaScriptFinalizeArg: [String: Any]?) { + if let resultsForJavaScriptFinalize = resultsForJavaScriptFinalizeArg { + // Construct an NSExtensionItem of the appropriate type to return our + // results dictionary in. + + // These will be used as the arguments to the JavaScript finalize() + // method. + + let resultsDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize] + + let resultsProvider = NSItemProvider(item: resultsDictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier) + + let resultsItem = NSExtensionItem() + resultsItem.attachments = [resultsProvider] + + // Signal that we're complete, returning our results. + self.extensionContext!.completeRequest(returningItems: [resultsItem], completionHandler: nil) + } else { + // We still need to signal that we're done even if we have nothing to + // pass back. + self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + // Don't hold on to this after we finished with it. + self.extensionContext = nil + } + +} diff --git a/FollowActionExtension/Info.plist b/FollowActionExtension/Info.plist new file mode 100644 index 000000000..0ac1aced5 --- /dev/null +++ b/FollowActionExtension/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/FollowActionExtension/Media.xcassets/Contents.json b/FollowActionExtension/Media.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/FollowActionExtension/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FollowActionExtension/Media.xcassets/Icon.appiconset/Contents.json b/FollowActionExtension/Media.xcassets/Icon.appiconset/Contents.json new file mode 100644 index 000000000..a09e0e886 --- /dev/null +++ b/FollowActionExtension/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/FollowActionExtension/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png b/FollowActionExtension/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..1a9999052c067c8acf4c28c8b4afc3b66dc7fd8d GIT binary patch literal 87538 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

x<~k{A`H9|`>g&-RvRz%)L3P^Hejm}em1y4 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 Date: Tue, 10 Jan 2023 16:40:20 +0100 Subject: [PATCH 3/7] feat(ActionExtension): Improve ActionRequestHandler and Action.js, rename Extension --- .../ActionRequestHandler.swift | 91 ---- Mastodon.xcodeproj/project.pbxproj | 80 +-- .../xcshareddata/swiftpm/Package.resolved | 492 +++++++++--------- .../Action.js | 28 +- .../ActionRequestHandler.swift | 75 +++ .../Info.plist | 0 .../Media.xcassets/Contents.json | 0 .../Icon.appiconset/Contents.json | 0 .../MastodonActionExtensionIcon@3x.png | Bin .../TouchBarBezel.colorset/Contents.json | 0 10 files changed, 380 insertions(+), 386 deletions(-) delete mode 100644 FollowActionExtension/ActionRequestHandler.swift rename {FollowActionExtension => OpenInActionExtension}/Action.js (51%) create mode 100644 OpenInActionExtension/ActionRequestHandler.swift rename {FollowActionExtension => OpenInActionExtension}/Info.plist (100%) rename {FollowActionExtension => OpenInActionExtension}/Media.xcassets/Contents.json (100%) rename {FollowActionExtension => OpenInActionExtension}/Media.xcassets/Icon.appiconset/Contents.json (100%) rename {FollowActionExtension => OpenInActionExtension}/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png (100%) rename {FollowActionExtension => OpenInActionExtension}/Media.xcassets/TouchBarBezel.colorset/Contents.json (100%) diff --git a/FollowActionExtension/ActionRequestHandler.swift b/FollowActionExtension/ActionRequestHandler.swift deleted file mode 100644 index bcfc2a66b..000000000 --- a/FollowActionExtension/ActionRequestHandler.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// ActionRequestHandler.swift -// FollowActionExtension -// -// Created by Marcus Kida on 03.01.23. -// - -import UIKit -import MobileCoreServices -import UniformTypeIdentifiers - -class ActionRequestHandler: NSObject, NSExtensionRequestHandling { - - var extensionContext: NSExtensionContext? - - func beginRequest(with context: NSExtensionContext) { - // Do not call super in an Action extension with no user interface - self.extensionContext = context - - var found = false - - // Find the item containing the results from the JavaScript preprocessing. - outer: - for item in context.inputItems as! [NSExtensionItem] { - if let attachments = item.attachments { - for itemProvider in attachments { - if itemProvider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) { - itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { (item, error) in - guard - let dictionary = item as? [String: Any], - let res = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any]? ?? [:] - else { - - self.doneWithResults( - ["error": "Failed to find username. Are you sure this is a Mastodon Profile page?"] - ) - return - } - - OperationQueue.main.addOperation { - self.itemLoadCompletedWithPreprocessingResults(res) - } - }) - found = true - break outer - } - } - } - } - - if !found { - self.doneWithResults(nil) - } - } - - func itemLoadCompletedWithPreprocessingResults(_ javaScriptPreprocessingResults: [String: Any]) { - guard let username = javaScriptPreprocessingResults["username"] as? String else { return } - - doneWithResults([ - "openURL": "mastodon://profile/\(username)" - ]) - } - - func doneWithResults(_ resultsForJavaScriptFinalizeArg: [String: Any]?) { - if let resultsForJavaScriptFinalize = resultsForJavaScriptFinalizeArg { - // Construct an NSExtensionItem of the appropriate type to return our - // results dictionary in. - - // These will be used as the arguments to the JavaScript finalize() - // method. - - let resultsDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize] - - let resultsProvider = NSItemProvider(item: resultsDictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier) - - let resultsItem = NSExtensionItem() - resultsItem.attachments = [resultsProvider] - - // Signal that we're complete, returning our results. - self.extensionContext!.completeRequest(returningItems: [resultsItem], completionHandler: nil) - } else { - // We still need to signal that we're done even if we have nothing to - // pass back. - self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) - } - - // Don't hold on to this after we finished with it. - self.extensionContext = nil - } - -} diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b9259986a..a187164b5 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -29,10 +29,10 @@ 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 */; }; - 2A64516129642A8B00CD8B8A /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A64516029642A8B00CD8B8A /* Media.xcassets */; }; - 2A64516329642A8B00CD8B8A /* ActionRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A64516229642A8B00CD8B8A /* ActionRequestHandler.swift */; }; - 2A64516529642A8B00CD8B8A /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = 2A64516429642A8B00CD8B8A /* Action.js */; }; - 2A64516929642A8B00CD8B8A /* FollowActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2A64515D29642A8A00CD8B8A /* FollowActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 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 */; }; 2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */; }; @@ -533,7 +533,7 @@ dstSubfolderSpec = 13; files = ( DB8FABCE26AEC7B2008E5AF4 /* MastodonIntent.appex in Embed Foundation Extensions */, - 2A64516929642A8B00CD8B8A /* FollowActionExtension.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 */, ); @@ -567,11 +567,11 @@ 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 /* FollowActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FollowActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 2A64516029642A8B00CD8B8A /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = ""; }; - 2A64516229642A8B00CD8B8A /* ActionRequestHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionRequestHandler.swift; sourceTree = ""; }; - 2A64516429642A8B00CD8B8A /* Action.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Action.js; sourceTree = ""; }; - 2A64516629642A8B00CD8B8A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 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 = ""; }; @@ -1318,15 +1318,15 @@ path = FollowedTags; sourceTree = ""; }; - 2A64515F29642A8B00CD8B8A /* FollowActionExtension */ = { + 2A71F53C296DBDA80049F54A /* OpenInActionExtension */ = { isa = PBXGroup; children = ( - 2A64516029642A8B00CD8B8A /* Media.xcassets */, - 2A64516229642A8B00CD8B8A /* ActionRequestHandler.swift */, - 2A64516429642A8B00CD8B8A /* Action.js */, - 2A64516629642A8B00CD8B8A /* Info.plist */, + 2A71F53D296DBDA80049F54A /* Media.xcassets */, + 2A71F53E296DBDA80049F54A /* Action.js */, + 2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */, + 2A71F540296DBDA80049F54A /* Info.plist */, ); - path = FollowActionExtension; + path = OpenInActionExtension; sourceTree = ""; }; 2D152A8A25C295B8009AA50C /* Content */ = { @@ -1865,7 +1865,7 @@ DBF8AE14263293E400C9C23C /* NotificationService */, DBC6461326A170AB00B0E31B /* ShareActionExtension */, DB8FABC826AEC7B2008E5AF4 /* MastodonIntent */, - 2A64515F29642A8B00CD8B8A /* FollowActionExtension */, + 2A71F53C296DBDA80049F54A /* OpenInActionExtension */, DB427DD325BAA00100D1B89D /* Products */, 1EBA4F56E920856A3FC84ACB /* Pods */, 3FE14AD363ED19AE7FF210A6 /* Frameworks */, @@ -1883,7 +1883,7 @@ DBF8AE13263293E400C9C23C /* NotificationService.appex */, DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */, DB8FABC626AEC7B2008E5AF4 /* MastodonIntent.appex */, - 2A64515D29642A8A00CD8B8A /* FollowActionExtension.appex */, + 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */, ); name = Products; sourceTree = ""; @@ -2825,9 +2825,9 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 2A64515C29642A8A00CD8B8A /* FollowActionExtension */ = { + 2A64515C29642A8A00CD8B8A /* OpenInActionExtension */ = { isa = PBXNativeTarget; - buildConfigurationList = 2A64516A29642A8B00CD8B8A /* Build configuration list for PBXNativeTarget "FollowActionExtension" */; + buildConfigurationList = 2A64516A29642A8B00CD8B8A /* Build configuration list for PBXNativeTarget "OpenInActionExtension" */; buildPhases = ( 2A64515929642A8A00CD8B8A /* Sources */, 2A64515A29642A8A00CD8B8A /* Frameworks */, @@ -2837,9 +2837,9 @@ ); dependencies = ( ); - name = FollowActionExtension; + name = OpenInActionExtension; productName = FollowActionExtension; - productReference = 2A64515D29642A8A00CD8B8A /* FollowActionExtension.appex */; + productReference = 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */; productType = "com.apple.product-type.app-extension"; }; DB427DD125BAA00100D1B89D /* Mastodon */ = { @@ -3057,7 +3057,7 @@ DBF8AE12263293E400C9C23C /* NotificationService */, DBC6461126A170AB00B0E31B /* ShareActionExtension */, DB8FABC526AEC7B2008E5AF4 /* MastodonIntent */, - 2A64515C29642A8A00CD8B8A /* FollowActionExtension */, + 2A64515C29642A8A00CD8B8A /* OpenInActionExtension */, ); }; /* End PBXProject section */ @@ -3067,8 +3067,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A64516529642A8B00CD8B8A /* Action.js in Resources */, - 2A64516129642A8B00CD8B8A /* Media.xcassets in Resources */, + 2A71F541296DBDA80049F54A /* Media.xcassets in Resources */, + 2A71F542296DBDA80049F54A /* Action.js in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3294,7 +3294,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A64516329642A8B00CD8B8A /* ActionRequestHandler.swift in Sources */, + 2A71F543296DBDA80049F54A /* ActionRequestHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3760,7 +3760,7 @@ /* Begin PBXTargetDependency section */ 2A64516829642A8B00CD8B8A /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 2A64515C29642A8A00CD8B8A /* FollowActionExtension */; + target = 2A64515C29642A8A00CD8B8A /* OpenInActionExtension */; targetProxy = 2A64516729642A8B00CD8B8A /* PBXContainerItemProxy */; }; DB427DEA25BAA00100D1B89D /* PBXTargetDependency */ = { @@ -3929,8 +3929,8 @@ DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 5Z4GVSS33P; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = FollowActionExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Follow on Mastodon"; + INFOPLIST_FILE = OpenInActionExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Open using Mastodon"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -3939,7 +3939,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.FollowActionExtension; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.OpenInActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -3958,8 +3958,8 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 5Z4GVSS33P; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = FollowActionExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Follow on Mastodon"; + INFOPLIST_FILE = OpenInActionExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Open using Mastodon"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -3968,7 +3968,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.FollowActionExtension; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.OpenInActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -3987,8 +3987,8 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 5Z4GVSS33P; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = FollowActionExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Follow on Mastodon"; + INFOPLIST_FILE = OpenInActionExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Open using Mastodon"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -3997,7 +3997,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.FollowActionExtension; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.OpenInActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -4016,8 +4016,8 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 5Z4GVSS33P; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = FollowActionExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Follow on Mastodon"; + INFOPLIST_FILE = OpenInActionExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Open using Mastodon"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -4026,7 +4026,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.FollowActionExtension; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.OpenInActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -4857,7 +4857,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 2A64516A29642A8B00CD8B8A /* Build configuration list for PBXNativeTarget "FollowActionExtension" */ = { + 2A64516A29642A8B00CD8B8A /* Build configuration list for PBXNativeTarget "OpenInActionExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( 2A64516B29642A8B00CD8B8A /* Debug */, diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index e029f5bed..d2dce549d 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,250 +1,248 @@ { - "object": { - "pins": [ - { - "package": "Alamofire", - "repositoryURL": "https://github.com/Alamofire/Alamofire.git", - "state": { - "branch": null, - "revision": "8dd85aee02e39dd280c75eef88ffdb86eed4b07b", - "version": "5.6.2" - } - }, - { - "package": "AlamofireImage", - "repositoryURL": "https://github.com/Alamofire/AlamofireImage.git", - "state": { - "branch": null, - "revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", - "version": "4.2.0" - } - }, - { - "package": "CommonOSLog", - "repositoryURL": "https://github.com/MainasuK/CommonOSLog", - "state": { - "branch": null, - "revision": "c121624a30698e9886efe38aebb36ff51c01b6c2", - "version": "0.1.1" - } - }, - { - "package": "FaviconFinder", - "repositoryURL": "https://github.com/will-lumley/FaviconFinder.git", - "state": { - "branch": null, - "revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", - "version": "3.3.0" - } - }, - { - "package": "FLAnimatedImage", - "repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git", - "state": { - "branch": null, - "revision": "d4f07b6f164d53c1212c3e54d6460738b1981e9f", - "version": "1.0.17" - } - }, - { - "package": "FPSIndicator", - "repositoryURL": "https://github.com/MainasuK/FPSIndicator.git", - "state": { - "branch": null, - "revision": "e4a5067ccd5293b024c767f09e51056afd4a4796", - "version": "1.1.0" - } - }, - { - "package": "Fuzi", - "repositoryURL": "https://github.com/cezheng/Fuzi.git", - "state": { - "branch": null, - "revision": "f08c8323da21e985f3772610753bcfc652c2103f", - "version": "3.1.3" - } - }, - { - "package": "KeychainAccess", - "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state": { - "branch": null, - "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", - "version": "4.2.2" - } - }, - { - "package": "Kingfisher", - "repositoryURL": "https://github.com/onevcat/Kingfisher.git", - "state": { - "branch": null, - "revision": "44e891bdb61426a95e31492a67c7c0dfad1f87c5", - "version": "7.4.1" - } - }, - { - "package": "MetaTextKit", - "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", - "state": { - "branch": null, - "revision": "dcd5255d6930c2fab408dc8562c577547e477624", - "version": "2.2.5" - } - }, - { - "package": "NextLevelSessionExporter", - "repositoryURL": "https://github.com/NextLevel/NextLevelSessionExporter.git", - "state": { - "branch": null, - "revision": "b6c0cce1aa37fe1547d694f958fac3c3524b74da", - "version": "0.4.6" - } - }, - { - "package": "Nuke", - "repositoryURL": "https://github.com/kean/Nuke.git", - "state": { - "branch": null, - "revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97", - "version": "10.11.2" - } - }, - { - "package": "NukeFLAnimatedImagePlugin", - "repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", - "state": { - "branch": null, - "revision": "b59c346a7d536336db3b0f12c72c6e53ee709e16", - "version": "8.0.0" - } - }, - { - "package": "Pageboy", - "repositoryURL": "https://github.com/uias/Pageboy", - "state": { - "branch": null, - "revision": "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", - "version": "3.7.0" - } - }, - { - "package": "PanModal", - "repositoryURL": "https://github.com/slackhq/PanModal.git", - "state": { - "branch": null, - "revision": "b012aecb6b67a8e46369227f893c12544846613f", - "version": "1.2.7" - } - }, - { - "package": "SDWebImage", - "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", - "state": { - "branch": null, - "revision": "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb", - "version": "5.14.2" - } - }, - { - "package": "Stripes", - "repositoryURL": "https://github.com/eneko/Stripes.git", - "state": { - "branch": null, - "revision": "d533fd44b8043a3abbf523e733599173d6f98c11", - "version": "0.2.0" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "546610d52b19be3e19935e0880bb06b9c03f5cef", - "version": "1.14.4" - } - }, - { - "package": "swift-nio-zlib-support", - "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", - "state": { - "branch": null, - "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", - "version": "1.0.0" - } - }, - { - "package": "SwiftSoup", - "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", - "state": { - "branch": null, - "revision": "6778575285177365cbad3e5b8a72f2a20583cfec", - "version": "2.4.3" - } - }, - { - "package": "Introspect", - "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", - "state": { - "branch": null, - "revision": "f2616860a41f9d9932da412a8978fec79c06fe24", - "version": "0.1.4" - } - }, - { - "package": "TabBarPager", - "repositoryURL": "https://github.com/TwidereProject/TabBarPager.git", - "state": { - "branch": null, - "revision": "488aa66d157a648901b61721212c0dec23d27ee5", - "version": "0.1.0" - } - }, - { - "package": "Tabman", - "repositoryURL": "https://github.com/uias/Tabman", - "state": { - "branch": null, - "revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465", - "version": "2.13.0" - } - }, - { - "package": "TOCropViewController", - "repositoryURL": "https://github.com/TimOliver/TOCropViewController.git", - "state": { - "branch": null, - "revision": "d0470491f56e734731bbf77991944c0dfdee3e0e", - "version": "2.6.1" - } - }, - { - "package": "UIHostingConfigurationBackport", - "repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git", - "state": { - "branch": null, - "revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3", - "version": "0.1.0" - } - }, - { - "package": "UITextView+Placeholder", - "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", - "state": { - "branch": null, - "revision": "20f513ded04a040cdf5467f0891849b1763ede3b", - "version": "1.4.1" - } + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b", + "version" : "5.6.2" } - ] - }, - "version": 1 + }, + { + "identity" : "alamofireimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/AlamofireImage.git", + "state" : { + "revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", + "version" : "4.2.0" + } + }, + { + "identity" : "commonoslog", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/CommonOSLog", + "state" : { + "revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2", + "version" : "0.1.1" + } + }, + { + "identity" : "faviconfinder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/will-lumley/FaviconFinder.git", + "state" : { + "revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", + "version" : "3.3.0" + } + }, + { + "identity" : "flanimatedimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flipboard/FLAnimatedImage.git", + "state" : { + "revision" : "d4f07b6f164d53c1212c3e54d6460738b1981e9f", + "version" : "1.0.17" + } + }, + { + "identity" : "fpsindicator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/FPSIndicator.git", + "state" : { + "revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796", + "version" : "1.1.0" + } + }, + { + "identity" : "fuzi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cezheng/Fuzi.git", + "state" : { + "revision" : "f08c8323da21e985f3772610753bcfc652c2103f", + "version" : "3.1.3" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5", + "version" : "7.4.1" + } + }, + { + "identity" : "metatextkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TwidereProject/MetaTextKit.git", + "state" : { + "revision" : "dcd5255d6930c2fab408dc8562c577547e477624", + "version" : "2.2.5" + } + }, + { + "identity" : "nextlevelsessionexporter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/NextLevel/NextLevelSessionExporter.git", + "state" : { + "revision" : "b6c0cce1aa37fe1547d694f958fac3c3524b74da", + "version" : "0.4.6" + } + }, + { + "identity" : "nuke", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke.git", + "state" : { + "revision" : "a002b7fd786f2df2ed4333fe73a9727499fd9d97", + "version" : "10.11.2" + } + }, + { + "identity" : "nuke-flanimatedimage-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", + "state" : { + "revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16", + "version" : "8.0.0" + } + }, + { + "identity" : "pageboy", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Pageboy", + "state" : { + "revision" : "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", + "version" : "3.7.0" + } + }, + { + "identity" : "panmodal", + "kind" : "remoteSourceControl", + "location" : "https://github.com/slackhq/PanModal.git", + "state" : { + "revision" : "b012aecb6b67a8e46369227f893c12544846613f", + "version" : "1.2.7" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage.git", + "state" : { + "revision" : "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb", + "version" : "5.14.2" + } + }, + { + "identity" : "stripes", + "kind" : "remoteSourceControl", + "location" : "https://github.com/eneko/Stripes.git", + "state" : { + "revision" : "d533fd44b8043a3abbf523e733599173d6f98c11", + "version" : "0.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "f504716c27d2e5d4144fa4794b12129301d17729", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef", + "version" : "1.14.4" + } + }, + { + "identity" : "swift-nio-zlib-support", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-zlib-support.git", + "state" : { + "revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version" : "1.0.0" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "6778575285177365cbad3e5b8a72f2a20583cfec", + "version" : "2.4.3" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24", + "version" : "0.1.4" + } + }, + { + "identity" : "tabbarpager", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TwidereProject/TabBarPager.git", + "state" : { + "revision" : "488aa66d157a648901b61721212c0dec23d27ee5", + "version" : "0.1.0" + } + }, + { + "identity" : "tabman", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Tabman", + "state" : { + "revision" : "4a4f7c755b875ffd4f9ef10d67a67883669d2465", + "version" : "2.13.0" + } + }, + { + "identity" : "tocropviewcontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TimOliver/TOCropViewController.git", + "state" : { + "revision" : "d0470491f56e734731bbf77991944c0dfdee3e0e", + "version" : "2.6.1" + } + }, + { + "identity" : "uihostingconfigurationbackport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/woxtu/UIHostingConfigurationBackport.git", + "state" : { + "revision" : "6091f2d38faa4b24fc2ca0389c651e2f666624a3", + "version" : "0.1.0" + } + }, + { + "identity" : "uitextview-placeholder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/UITextView-Placeholder.git", + "state" : { + "revision" : "20f513ded04a040cdf5467f0891849b1763ede3b", + "version" : "1.4.1" + } + } + ], + "version" : 2 } diff --git a/FollowActionExtension/Action.js b/OpenInActionExtension/Action.js similarity index 51% rename from FollowActionExtension/Action.js rename to OpenInActionExtension/Action.js index 3f8f93723..75982b50f 100644 --- a/FollowActionExtension/Action.js +++ b/OpenInActionExtension/Action.js @@ -1,6 +1,6 @@ // // Action.js -// FollowActionExtension +// OpenInActionExtension // // Created by Marcus Kida on 03.01.23. // @@ -10,15 +10,13 @@ var Action = function() {}; Action.prototype = { run: function(arguments) { - var username = document.documentURI.match("@(.+)@([a-z0-9]+\.[a-z0-9]+)")[0]; - if (!username) { - username = document.head.querySelector('[property="profile:username"]').content + var username = detectUsername() + + if (username) { + arguments.completionFunction({ "username" : username }) } - - console.log("username" + username) - - arguments.completionFunction({ "username" : username }) + }, finalize: function(arguments) { @@ -34,5 +32,19 @@ Action.prototype = { } }; + +function detectUsername() { + var uriUsername = document.documentURI.match("@(.+)@([a-z0-9]+\.[a-z0-9]+)") + + if (typeof uriUsername === "Array") { + return uriUsername[0] + } + + return document.head.querySelector('[property="profile:username"]').content +} + +function detectPost() { + +} var ExtensionPreprocessingJS = new Action diff --git a/OpenInActionExtension/ActionRequestHandler.swift b/OpenInActionExtension/ActionRequestHandler.swift new file mode 100644 index 000000000..f8fb0f8e8 --- /dev/null +++ b/OpenInActionExtension/ActionRequestHandler.swift @@ -0,0 +1,75 @@ +// +// ActionRequestHandler.swift +// OpenInActionExtension +// +// Created by Marcus Kida on 03.01.23. +// + +import UIKit +import MobileCoreServices +import UniformTypeIdentifiers + +class ActionRequestHandler: NSObject, NSExtensionRequestHandling { + + var extensionContext: NSExtensionContext? + + func beginRequest(with context: NSExtensionContext) { + // Do not call super in an Action extension with no user interface + self.extensionContext = context + + guard + 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 + else { + return self.completeWithNotFoundError() + } + + itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { (item, error) in + guard + let dictionary = item as? [String: Any], + let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any]? ?? [:] + else { + return self.completeWithNotFoundError() + } + + DispatchQueue.main.async { + self.completeWithOpenUserProfile(results) + } + }) + } +} + +private extension ActionRequestHandler { + func completeWithOpenUserProfile(_ results: [String: Any]) { + guard let username = results["username"] as? String else { return } + doneWithResults([ + "openURL": "mastodon://profile/\(username)" + ]) + } + + func completeWithNotFoundError() { + doneWithResults( + ["error": "Failed to find username. Are you sure this is a Mastodon Profile page?"] + ) + } + + 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/FollowActionExtension/Info.plist b/OpenInActionExtension/Info.plist similarity index 100% rename from FollowActionExtension/Info.plist rename to OpenInActionExtension/Info.plist diff --git a/FollowActionExtension/Media.xcassets/Contents.json b/OpenInActionExtension/Media.xcassets/Contents.json similarity index 100% rename from FollowActionExtension/Media.xcassets/Contents.json rename to OpenInActionExtension/Media.xcassets/Contents.json diff --git a/FollowActionExtension/Media.xcassets/Icon.appiconset/Contents.json b/OpenInActionExtension/Media.xcassets/Icon.appiconset/Contents.json similarity index 100% rename from FollowActionExtension/Media.xcassets/Icon.appiconset/Contents.json rename to OpenInActionExtension/Media.xcassets/Icon.appiconset/Contents.json diff --git a/FollowActionExtension/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png b/OpenInActionExtension/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png similarity index 100% rename from FollowActionExtension/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png rename to OpenInActionExtension/Media.xcassets/Icon.appiconset/MastodonActionExtensionIcon@3x.png diff --git a/FollowActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json b/OpenInActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json similarity index 100% rename from FollowActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json rename to OpenInActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json From 68fe3cb8f2bc6ed28f92ba8405d7d26c5c0ff939 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 11 Jan 2023 13:09:23 +0100 Subject: [PATCH 4/7] feat(ActionExtension): Implement Open in Search as Fallback --- Mastodon/Supporting Files/SceneDelegate.swift | 65 ++++++++++++------- OpenInActionExtension/Action.js | 32 ++++----- .../ActionRequestHandler.swift | 61 +++++++++-------- 3 files changed, 87 insertions(+), 71 deletions(-) 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/OpenInActionExtension/Action.js b/OpenInActionExtension/Action.js index 75982b50f..e86c5d255 100644 --- a/OpenInActionExtension/Action.js +++ b/OpenInActionExtension/Action.js @@ -10,25 +10,16 @@ var Action = function() {}; Action.prototype = { run: function(arguments) { - - var username = detectUsername() - - if (username) { - arguments.completionFunction({ "username" : username }) + var payload = { + "username": detectUsername(), + "url": document.documentURI } - + + arguments.completionFunction(payload) }, finalize: function(arguments) { - - var openURL = arguments["openURL"] - var error = arguments["error"] - - if (error) { - alert(error) - } else if (openURL) { - window.location = openURL - } + window.location = arguments["openURL"] } }; @@ -39,12 +30,13 @@ function detectUsername() { if (typeof uriUsername === "Array") { return uriUsername[0] } + + var querySelector = document.head.querySelector('[property="profile:username"]') + if (typeof querySelector === "Object") { + return querySelector.content + } - return document.head.querySelector('[property="profile:username"]').content + return undefined } -function detectPost() { - -} - var ExtensionPreprocessingJS = new Action diff --git a/OpenInActionExtension/ActionRequestHandler.swift b/OpenInActionExtension/ActionRequestHandler.swift index f8fb0f8e8..15c51cc37 100644 --- a/OpenInActionExtension/ActionRequestHandler.swift +++ b/OpenInActionExtension/ActionRequestHandler.swift @@ -16,47 +16,56 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling { func beginRequest(with context: NSExtensionContext) { // Do not call super in an Action extension with no user interface self.extensionContext = context - - guard - 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 - else { - return self.completeWithNotFoundError() + + 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 doneWithResults(nil) } - itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { (item, error) in - guard - let dictionary = item as? [String: Any], - let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any]? ?? [:] - else { - return self.completeWithNotFoundError() - } - + itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] item, error in DispatchQueue.main.async { - self.completeWithOpenUserProfile(results) + guard + let dictionary = item as? NSDictionary, + let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary + else { + self?.doneWithResults(nil) + return + } + + if let username = results["username"] as? String { + self?.completeWithOpenUserProfile(username) + } else if let url = results["url"] as? String { + self?.completeWithSearch(url) + } else { + self?.doneWithResults(nil) + } } }) } } private extension ActionRequestHandler { - func completeWithOpenUserProfile(_ results: [String: Any]) { - guard let username = results["username"] as? String else { return } + func completeWithOpenUserProfile(_ username: String) { doneWithResults([ "openURL": "mastodon://profile/\(username)" ]) } - func completeWithNotFoundError() { + func completeWithSearch(_ query: String) { + guard let query = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + return doneWithResults(nil) + } doneWithResults( - ["error": "Failed to find username. Are you sure this is a Mastodon Profile page?"] + ["openURL": "mastodon://search?query=\(query)"] ) } From 5daaa5a32fccc8c0e5aef96e5694286a5c5b4402 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 11 Jan 2023 15:12:07 +0100 Subject: [PATCH 5/7] feat(AppExtension): Improve open in link validation, add L10n --- Localization/app.json | 5 + Mastodon.xcodeproj/project.pbxproj | 20 + .../xcshareddata/swiftpm/Package.resolved | 492 +++++++++--------- .../Generated/Strings.swift | 6 + .../Resources/Base.lproj/Localizable.strings | 1 + OpenInActionExtension/Action.js | 11 +- .../ActionRequestHandler.swift | 53 +- 7 files changed, 329 insertions(+), 259 deletions(-) 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 a187164b5..4e03ca0c8 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 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 */; }; @@ -504,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; @@ -1131,6 +1142,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2A90A157296EEE500026C155 /* MastodonSDKDynamic in Frameworks */, 2A64515E29642A8A00CD8B8A /* UniformTypeIdentifiers.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2832,12 +2844,16 @@ 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"; @@ -4948,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.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index d2dce549d..e029f5bed 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,248 +1,250 @@ { - "pins" : [ - { - "identity" : "alamofire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/Alamofire.git", - "state" : { - "revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b", - "version" : "5.6.2" + "object": { + "pins": [ + { + "package": "Alamofire", + "repositoryURL": "https://github.com/Alamofire/Alamofire.git", + "state": { + "branch": null, + "revision": "8dd85aee02e39dd280c75eef88ffdb86eed4b07b", + "version": "5.6.2" + } + }, + { + "package": "AlamofireImage", + "repositoryURL": "https://github.com/Alamofire/AlamofireImage.git", + "state": { + "branch": null, + "revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", + "version": "4.2.0" + } + }, + { + "package": "CommonOSLog", + "repositoryURL": "https://github.com/MainasuK/CommonOSLog", + "state": { + "branch": null, + "revision": "c121624a30698e9886efe38aebb36ff51c01b6c2", + "version": "0.1.1" + } + }, + { + "package": "FaviconFinder", + "repositoryURL": "https://github.com/will-lumley/FaviconFinder.git", + "state": { + "branch": null, + "revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", + "version": "3.3.0" + } + }, + { + "package": "FLAnimatedImage", + "repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git", + "state": { + "branch": null, + "revision": "d4f07b6f164d53c1212c3e54d6460738b1981e9f", + "version": "1.0.17" + } + }, + { + "package": "FPSIndicator", + "repositoryURL": "https://github.com/MainasuK/FPSIndicator.git", + "state": { + "branch": null, + "revision": "e4a5067ccd5293b024c767f09e51056afd4a4796", + "version": "1.1.0" + } + }, + { + "package": "Fuzi", + "repositoryURL": "https://github.com/cezheng/Fuzi.git", + "state": { + "branch": null, + "revision": "f08c8323da21e985f3772610753bcfc652c2103f", + "version": "3.1.3" + } + }, + { + "package": "KeychainAccess", + "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state": { + "branch": null, + "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", + "version": "4.2.2" + } + }, + { + "package": "Kingfisher", + "repositoryURL": "https://github.com/onevcat/Kingfisher.git", + "state": { + "branch": null, + "revision": "44e891bdb61426a95e31492a67c7c0dfad1f87c5", + "version": "7.4.1" + } + }, + { + "package": "MetaTextKit", + "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", + "state": { + "branch": null, + "revision": "dcd5255d6930c2fab408dc8562c577547e477624", + "version": "2.2.5" + } + }, + { + "package": "NextLevelSessionExporter", + "repositoryURL": "https://github.com/NextLevel/NextLevelSessionExporter.git", + "state": { + "branch": null, + "revision": "b6c0cce1aa37fe1547d694f958fac3c3524b74da", + "version": "0.4.6" + } + }, + { + "package": "Nuke", + "repositoryURL": "https://github.com/kean/Nuke.git", + "state": { + "branch": null, + "revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97", + "version": "10.11.2" + } + }, + { + "package": "NukeFLAnimatedImagePlugin", + "repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", + "state": { + "branch": null, + "revision": "b59c346a7d536336db3b0f12c72c6e53ee709e16", + "version": "8.0.0" + } + }, + { + "package": "Pageboy", + "repositoryURL": "https://github.com/uias/Pageboy", + "state": { + "branch": null, + "revision": "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", + "version": "3.7.0" + } + }, + { + "package": "PanModal", + "repositoryURL": "https://github.com/slackhq/PanModal.git", + "state": { + "branch": null, + "revision": "b012aecb6b67a8e46369227f893c12544846613f", + "version": "1.2.7" + } + }, + { + "package": "SDWebImage", + "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", + "state": { + "branch": null, + "revision": "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb", + "version": "5.14.2" + } + }, + { + "package": "Stripes", + "repositoryURL": "https://github.com/eneko/Stripes.git", + "state": { + "branch": null, + "revision": "d533fd44b8043a3abbf523e733599173d6f98c11", + "version": "0.2.0" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "f504716c27d2e5d4144fa4794b12129301d17729", + "version": "1.0.3" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "546610d52b19be3e19935e0880bb06b9c03f5cef", + "version": "1.14.4" + } + }, + { + "package": "swift-nio-zlib-support", + "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", + "state": { + "branch": null, + "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version": "1.0.0" + } + }, + { + "package": "SwiftSoup", + "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", + "state": { + "branch": null, + "revision": "6778575285177365cbad3e5b8a72f2a20583cfec", + "version": "2.4.3" + } + }, + { + "package": "Introspect", + "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", + "state": { + "branch": null, + "revision": "f2616860a41f9d9932da412a8978fec79c06fe24", + "version": "0.1.4" + } + }, + { + "package": "TabBarPager", + "repositoryURL": "https://github.com/TwidereProject/TabBarPager.git", + "state": { + "branch": null, + "revision": "488aa66d157a648901b61721212c0dec23d27ee5", + "version": "0.1.0" + } + }, + { + "package": "Tabman", + "repositoryURL": "https://github.com/uias/Tabman", + "state": { + "branch": null, + "revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465", + "version": "2.13.0" + } + }, + { + "package": "TOCropViewController", + "repositoryURL": "https://github.com/TimOliver/TOCropViewController.git", + "state": { + "branch": null, + "revision": "d0470491f56e734731bbf77991944c0dfdee3e0e", + "version": "2.6.1" + } + }, + { + "package": "UIHostingConfigurationBackport", + "repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git", + "state": { + "branch": null, + "revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3", + "version": "0.1.0" + } + }, + { + "package": "UITextView+Placeholder", + "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", + "state": { + "branch": null, + "revision": "20f513ded04a040cdf5467f0891849b1763ede3b", + "version": "1.4.1" + } } - }, - { - "identity" : "alamofireimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/AlamofireImage.git", - "state" : { - "revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", - "version" : "4.2.0" - } - }, - { - "identity" : "commonoslog", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MainasuK/CommonOSLog", - "state" : { - "revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2", - "version" : "0.1.1" - } - }, - { - "identity" : "faviconfinder", - "kind" : "remoteSourceControl", - "location" : "https://github.com/will-lumley/FaviconFinder.git", - "state" : { - "revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", - "version" : "3.3.0" - } - }, - { - "identity" : "flanimatedimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Flipboard/FLAnimatedImage.git", - "state" : { - "revision" : "d4f07b6f164d53c1212c3e54d6460738b1981e9f", - "version" : "1.0.17" - } - }, - { - "identity" : "fpsindicator", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MainasuK/FPSIndicator.git", - "state" : { - "revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796", - "version" : "1.1.0" - } - }, - { - "identity" : "fuzi", - "kind" : "remoteSourceControl", - "location" : "https://github.com/cezheng/Fuzi.git", - "state" : { - "revision" : "f08c8323da21e985f3772610753bcfc652c2103f", - "version" : "3.1.3" - } - }, - { - "identity" : "keychainaccess", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state" : { - "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", - "version" : "4.2.2" - } - }, - { - "identity" : "kingfisher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/onevcat/Kingfisher.git", - "state" : { - "revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5", - "version" : "7.4.1" - } - }, - { - "identity" : "metatextkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/TwidereProject/MetaTextKit.git", - "state" : { - "revision" : "dcd5255d6930c2fab408dc8562c577547e477624", - "version" : "2.2.5" - } - }, - { - "identity" : "nextlevelsessionexporter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/NextLevel/NextLevelSessionExporter.git", - "state" : { - "revision" : "b6c0cce1aa37fe1547d694f958fac3c3524b74da", - "version" : "0.4.6" - } - }, - { - "identity" : "nuke", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kean/Nuke.git", - "state" : { - "revision" : "a002b7fd786f2df2ed4333fe73a9727499fd9d97", - "version" : "10.11.2" - } - }, - { - "identity" : "nuke-flanimatedimage-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", - "state" : { - "revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16", - "version" : "8.0.0" - } - }, - { - "identity" : "pageboy", - "kind" : "remoteSourceControl", - "location" : "https://github.com/uias/Pageboy", - "state" : { - "revision" : "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", - "version" : "3.7.0" - } - }, - { - "identity" : "panmodal", - "kind" : "remoteSourceControl", - "location" : "https://github.com/slackhq/PanModal.git", - "state" : { - "revision" : "b012aecb6b67a8e46369227f893c12544846613f", - "version" : "1.2.7" - } - }, - { - "identity" : "sdwebimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SDWebImage/SDWebImage.git", - "state" : { - "revision" : "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb", - "version" : "5.14.2" - } - }, - { - "identity" : "stripes", - "kind" : "remoteSourceControl", - "location" : "https://github.com/eneko/Stripes.git", - "state" : { - "revision" : "d533fd44b8043a3abbf523e733599173d6f98c11", - "version" : "0.2.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "f504716c27d2e5d4144fa4794b12129301d17729", - "version" : "1.0.3" - } - }, - { - "identity" : "swift-nio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio.git", - "state" : { - "revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef", - "version" : "1.14.4" - } - }, - { - "identity" : "swift-nio-zlib-support", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-zlib-support.git", - "state" : { - "revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd", - "version" : "1.0.0" - } - }, - { - "identity" : "swiftsoup", - "kind" : "remoteSourceControl", - "location" : "https://github.com/scinfu/SwiftSoup.git", - "state" : { - "revision" : "6778575285177365cbad3e5b8a72f2a20583cfec", - "version" : "2.4.3" - } - }, - { - "identity" : "swiftui-introspect", - "kind" : "remoteSourceControl", - "location" : "https://github.com/siteline/SwiftUI-Introspect.git", - "state" : { - "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24", - "version" : "0.1.4" - } - }, - { - "identity" : "tabbarpager", - "kind" : "remoteSourceControl", - "location" : "https://github.com/TwidereProject/TabBarPager.git", - "state" : { - "revision" : "488aa66d157a648901b61721212c0dec23d27ee5", - "version" : "0.1.0" - } - }, - { - "identity" : "tabman", - "kind" : "remoteSourceControl", - "location" : "https://github.com/uias/Tabman", - "state" : { - "revision" : "4a4f7c755b875ffd4f9ef10d67a67883669d2465", - "version" : "2.13.0" - } - }, - { - "identity" : "tocropviewcontroller", - "kind" : "remoteSourceControl", - "location" : "https://github.com/TimOliver/TOCropViewController.git", - "state" : { - "revision" : "d0470491f56e734731bbf77991944c0dfdee3e0e", - "version" : "2.6.1" - } - }, - { - "identity" : "uihostingconfigurationbackport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/woxtu/UIHostingConfigurationBackport.git", - "state" : { - "revision" : "6091f2d38faa4b24fc2ca0389c651e2f666624a3", - "version" : "0.1.0" - } - }, - { - "identity" : "uitextview-placeholder", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MainasuK/UITextView-Placeholder.git", - "state" : { - "revision" : "20f513ded04a040cdf5467f0891849b1763ede3b", - "version" : "1.4.1" - } - } - ], - "version" : 2 + ] + }, + "version": 1 } 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 index e86c5d255..b425558b6 100644 --- a/OpenInActionExtension/Action.js +++ b/OpenInActionExtension/Action.js @@ -19,7 +19,12 @@ Action.prototype = { }, finalize: function(arguments) { - window.location = arguments["openURL"] + let alertMessage = arguments["alert"] + if (alertMessage) { + alert(alertMessage) + } else { + window.location = arguments["openURL"] + } } }; @@ -27,12 +32,12 @@ Action.prototype = { function detectUsername() { var uriUsername = document.documentURI.match("@(.+)@([a-z0-9]+\.[a-z0-9]+)") - if (typeof uriUsername === "Array") { + if (Array.isArray(uriUsername)) { return uriUsername[0] } var querySelector = document.head.querySelector('[property="profile:username"]') - if (typeof querySelector === "Object") { + if (querySelector !== null && typeof querySelector === "object") { return querySelector.content } diff --git a/OpenInActionExtension/ActionRequestHandler.swift b/OpenInActionExtension/ActionRequestHandler.swift index 15c51cc37..fdc73f03a 100644 --- a/OpenInActionExtension/ActionRequestHandler.swift +++ b/OpenInActionExtension/ActionRequestHandler.swift @@ -5,13 +5,16 @@ // 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 @@ -28,7 +31,7 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling { .first guard let itemProvider = itemProvider else { - return doneWithResults(nil) + return doneWithInvalidLink() } itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] item, error in @@ -37,16 +40,16 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling { let dictionary = item as? NSDictionary, let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary else { - self?.doneWithResults(nil) + self?.doneWithInvalidLink() return } if let username = results["username"] as? String { self?.completeWithOpenUserProfile(username) } else if let url = results["url"] as? String { - self?.completeWithSearch(url) + self?.continueWithSearch(url) } else { - self?.doneWithResults(nil) + self?.doneWithInvalidLink() } } }) @@ -60,13 +63,41 @@ private extension ActionRequestHandler { ]) } - func completeWithSearch(_ query: String) { - guard let query = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { - return doneWithResults(nil) + func continueWithSearch(_ query: String) { + guard + let url = URL(string: query), + let host = url.host + else { + return doneWithInvalidLink() } - doneWithResults( - ["openURL": "mastodon://search?query=\(query)"] - ) + + 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]?) { From 1121cdd80b5e877834953bf977e6cf88f40c74b8 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 12 Jan 2023 10:10:44 +0100 Subject: [PATCH 6/7] feat(ActionExtension): Update TouchBarBezel Color --- .../TouchBarBezel.colorset/Contents.json | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/OpenInActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json b/OpenInActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json index 94a9fc218..3a6e6ec7e 100644 --- a/OpenInActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json +++ b/OpenInActionExtension/Media.xcassets/TouchBarBezel.colorset/Contents.json @@ -1,14 +1,38 @@ { - "info" : { - "version" : 1, - "author" : "xcode" - }, "colors" : [ { - "idiom" : "mac", "color" : { - "reference" : "systemPurpleColor" - } + "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" } - ] -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From fde4c14b4971f272bd0c321e6da3a951b33472ff Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 12 Jan 2023 10:11:11 +0100 Subject: [PATCH 7/7] feat(ActionExtension): Use same Regex for Username matching as in App --- OpenInActionExtension/Action.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenInActionExtension/Action.js b/OpenInActionExtension/Action.js index b425558b6..b99051b15 100644 --- a/OpenInActionExtension/Action.js +++ b/OpenInActionExtension/Action.js @@ -30,7 +30,7 @@ Action.prototype = { }; function detectUsername() { - var uriUsername = document.documentURI.match("@(.+)@([a-z0-9]+\.[a-z0-9]+)") + var uriUsername = document.documentURI.match("(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))") if (Array.isArray(uriUsername)) { return uriUsername[0]