Improve "Open in Mastodon" by using Search API (#888)

* feat(ActionExtension): Improve "Open in Mastodon" by using Search aPI

* Add code comment
This commit is contained in:
Marcus Kida 2023-01-16 23:36:00 +01:00 committed by GitHub
parent 8e7d115944
commit 3ec9e603df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 78 additions and 32 deletions

View File

@ -573,6 +573,7 @@
27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+URL.swift"; sourceTree = "<group>"; }; 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+URL.swift"; sourceTree = "<group>"; };
2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = "<group>"; }; 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = "<group>"; };
2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = "<group>"; }; 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = "<group>"; };
2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = "<group>"; };
2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = "<group>"; }; 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = "<group>"; };
2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = "<group>"; }; 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = "<group>"; };
2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = "<group>"; }; 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = "<group>"; };
@ -1333,6 +1334,7 @@
2A71F53C296DBDA80049F54A /* OpenInActionExtension */ = { 2A71F53C296DBDA80049F54A /* OpenInActionExtension */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */,
2A71F53D296DBDA80049F54A /* Media.xcassets */, 2A71F53D296DBDA80049F54A /* Media.xcassets */,
2A71F53E296DBDA80049F54A /* Action.js */, 2A71F53E296DBDA80049F54A /* Action.js */,
2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */, 2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */,
@ -3939,6 +3941,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = Icon; ASSETCATALOG_COMPILER_APPICON_NAME = Icon;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = OpenInActionExtension/OpenInActionExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@ -3969,6 +3972,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = Icon; ASSETCATALOG_COMPILER_APPICON_NAME = Icon;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = OpenInActionExtension/OpenInActionExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@ -3998,6 +4002,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = Icon; ASSETCATALOG_COMPILER_APPICON_NAME = Icon;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = OpenInActionExtension/OpenInActionExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@ -4027,6 +4032,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = Icon; ASSETCATALOG_COMPILER_APPICON_NAME = Icon;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = OpenInActionExtension/OpenInActionExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;

View File

@ -8,40 +8,23 @@
var Action = function() {}; var Action = function() {};
Action.prototype = { Action.prototype = {
run: function(arguments) { run: function(arguments) {
var payload = { var payload = {
"username": detectUsername(),
"url": document.documentURI "url": document.documentURI
} }
arguments.completionFunction(payload) arguments.completionFunction(payload)
}, },
finalize: function(arguments) { finalize: function(arguments) {
let alertMessage = arguments["alert"] const alertMessage = arguments["alert"]
const openURL = arguments["openURL"]
if (alertMessage) { if (alertMessage) {
alert(alertMessage) alert(alertMessage)
} else { } else if (openURL) {
window.location = arguments["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 var ExtensionPreprocessingJS = new Action

View File

@ -9,6 +9,7 @@ import Combine
import UIKit import UIKit
import MobileCoreServices import MobileCoreServices
import UniformTypeIdentifiers import UniformTypeIdentifiers
import MastodonCore
import MastodonSDK import MastodonSDK
import MastodonLocalization import MastodonLocalization
@ -16,6 +17,11 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
var extensionContext: NSExtensionContext? var extensionContext: NSExtensionContext?
var cancellables = [AnyCancellable]() 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) { func beginRequest(with context: NSExtensionContext) {
// Do not call super in an Action extension with no user interface // Do not call super in an Action extension with no user interface
self.extensionContext = context self.extensionContext = context
@ -44,10 +50,8 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
return return
} }
if let username = results["username"] as? String { if let url = results["url"] as? String {
self?.completeWithOpenUserProfile(username) self?.performSearch(for: url)
} else if let url = results["url"] as? String {
self?.continueWithSearch(url)
} else { } else {
self?.doneWithInvalidLink() self?.doneWithInvalidLink()
} }
@ -56,13 +60,53 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
} }
} }
// Search API
private extension ActionRequestHandler { private extension ActionRequestHandler {
func completeWithOpenUserProfile(_ username: String) { func performSearch(for url: String) {
doneWithResults([ guard
"openURL": "mastodon://profile/\(username)" 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) { func continueWithSearch(_ query: String) {
guard guard
let url = URL(string: query), let url = URL(string: query),
@ -95,7 +139,10 @@ private extension ActionRequestHandler {
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
}
// Action response handling
private extension ActionRequestHandler {
func doneWithInvalidLink() { func doneWithInvalidLink() {
doneWithResults(["alert": L10n.Extension.OpenIn.invalidLinkError]) doneWithResults(["alert": L10n.Extension.OpenIn.invalidLinkError])
} }

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.joinmastodon.app</string>
</array>
</dict>
</plist>