Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions Action Assistant/ActionViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//
// ActionViewController.swift
// Action Assistant
//
// Created by Marino Faggiana on 14/05/2026.
// Copyright © 2026 Marino Faggiana. All rights reserved.
//

import UIKit
import NextcloudKit
import UniformTypeIdentifiers

final class ActionViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

view.isHidden = true
view.alpha = 0
view.backgroundColor = .clear
preferredContentSize = .zero

Task {
await handleAction()
}
}

private func handleAction() async {
guard let text = await loadText() else {
extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
return
}

NCAssistantSharedTextStore.save(text)
openMainAppForAssistantSharedText()
}

private func loadText() async -> String? {
guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
return nil
}

for extensionItem in extensionItems {
guard let attachments = extensionItem.attachments else {
continue
}

for provider in attachments {
if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
return await loadText(from: provider, typeIdentifier: UTType.plainText.identifier)
}

if provider.hasItemConformingToTypeIdentifier(UTType.utf8PlainText.identifier) {
return await loadText(from: provider, typeIdentifier: UTType.utf8PlainText.identifier)
}

if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
return await loadText(from: provider, typeIdentifier: UTType.text.identifier)
}
}
}

return nil
}

private func loadText(from provider: NSItemProvider, typeIdentifier: String) async -> String? {
await withCheckedContinuation { continuation in
provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in
let text: String?

if let string = item as? String {
text = string
} else if let attributedString = item as? NSAttributedString {
text = attributedString.string
} else if let data = item as? Data {
text = String(data: data, encoding: .utf8)
} else if let url = item as? URL {
text = try? String(contentsOf: url, encoding: .utf8)
} else {
text = nil
}

guard let text, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
continuation.resume(returning: nil)
return
}

continuation.resume(returning: text)
}
}
}

private func openMainAppForAssistantSharedText() {
guard let url = URL(string: "nextcloud://assistant/shared-text") else {
extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
return
}

openAssistantSharedTextURLThroughResponderChain(url)

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
}

/// Opens the Assistant shared-text deep link from the Share extension.
///
/// Share extensions cannot use `UIApplication.shared` directly because it is not
/// extension-safe. This method walks the responder chain until it finds the hidden
/// `UIApplication` responder and invokes the modern `open(_:options:completionHandler:)`
/// Objective-C selector dynamically.
///
/// This is intentionally isolated because it relies on Objective-C runtime dispatch.
///
/// - Parameter url: Deep link URL to open in the containing application.
private func openAssistantSharedTextURLThroughResponderChain(_ url: URL) {
let selector = NSSelectorFromString("openURL:options:completionHandler:")
let applicationClass: AnyClass? = NSClassFromString("UIApplication")
var responder: UIResponder? = self

while let currentResponder = responder {
guard let applicationClass,
currentResponder.isKind(of: applicationClass),
currentResponder.responds(to: selector),
let implementation = currentResponder.method(for: selector) else {
responder = currentResponder.next
continue
}

typealias CompletionBlock = @convention(block) (Bool) -> Void
typealias OpenURLFunction = @convention(c) (AnyObject, Selector, NSURL, NSDictionary, CompletionBlock?) -> Void

let openURL = unsafeBitCast(implementation, to: OpenURLFunction.self)

let completion: CompletionBlock = { success in
if success {
nkLog(debug: "Assistant shared text deep link performed through modern responder chain")
} else {
nkLog(error: "Assistant shared text deep link modern responder chain returned false")
}
}

openURL(currentResponder, selector, url as NSURL, NSDictionary(), completion)
return
}

nkLog(error: "Assistant shared text deep link failed because no UIApplication responder can open URL")
}
}
20 changes: 20 additions & 0 deletions Action Assistant/Base.lproj/MainInterface.storyboard
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24765" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="ObA-dk-sSI">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24743"/>
</dependencies>
<scenes>
<!--Image-->
<scene sceneID="7MM-of-jgj">
<objects>
<viewController title="Image" id="ObA-dk-sSI" customClass="ActionViewController" customModule="Action_Assistant" sceneMemberID="viewController">
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="320" height="528"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="X47-rx-isc" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="139" y="131"/>
</scene>
</scenes>
</document>
38 changes: 38 additions & 0 deletions Action Assistant/Images.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"images" : [
{
"filename" : "Senza titolo.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Senza titolo 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"filename" : "Senza titolo 2.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions Action Assistant/Images.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
14 changes: 14 additions & 0 deletions Brand/Action_Assistant.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?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.it.twsweb.Crypto-Cloud</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)it.twsweb.Crypto-Cloud</string>
</array>
</dict>
</plist>
23 changes: 23 additions & 0 deletions Brand/Action_Assistant.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?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>CFBundleDisplayName</key>
<string>Nextcloud Assistant</string>

<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<string>TRUEPREDICATE</string>
</dict>

<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>

<key>NSExtensionPointIdentifier</key>
<string>com.apple.ui-services</string>
</dict>
</dict>
</plist>
5 changes: 2 additions & 3 deletions Brand/Share.plist
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<string>SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments,$attachment,(ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data")).@count == $extensionItem.attachments.@count).@count &gt; 0
</string>
<string>SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments, $attachment, (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data" OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text")).@count == $extensionItem.attachments.@count).@count &gt; 0</string>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
</plist>
Loading
Loading