Skip to content

Commit 3fce92b

Browse files
Merge branch 'master' into nmc/1992-Sharing_customisation
2 parents db44647 + fef419a commit 3fce92b

37 files changed

Lines changed: 1041 additions & 182 deletions
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//
2+
// ActionViewController.swift
3+
// Action Assistant
4+
//
5+
// Created by Marino Faggiana on 14/05/2026.
6+
// Copyright © 2026 Marino Faggiana. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import NextcloudKit
11+
import UniformTypeIdentifiers
12+
13+
final class ActionViewController: UIViewController {
14+
override func viewDidLoad() {
15+
super.viewDidLoad()
16+
17+
view.isHidden = true
18+
view.alpha = 0
19+
view.backgroundColor = .clear
20+
preferredContentSize = .zero
21+
22+
Task {
23+
await handleAction()
24+
}
25+
}
26+
27+
private func handleAction() async {
28+
guard let text = await loadText() else {
29+
extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
30+
return
31+
}
32+
33+
NCAssistantSharedTextStore.save(text)
34+
openMainAppForAssistantSharedText()
35+
}
36+
37+
private func loadText() async -> String? {
38+
guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
39+
return nil
40+
}
41+
42+
for extensionItem in extensionItems {
43+
guard let attachments = extensionItem.attachments else {
44+
continue
45+
}
46+
47+
for provider in attachments {
48+
if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
49+
return await loadText(from: provider, typeIdentifier: UTType.plainText.identifier)
50+
}
51+
52+
if provider.hasItemConformingToTypeIdentifier(UTType.utf8PlainText.identifier) {
53+
return await loadText(from: provider, typeIdentifier: UTType.utf8PlainText.identifier)
54+
}
55+
56+
if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
57+
return await loadText(from: provider, typeIdentifier: UTType.text.identifier)
58+
}
59+
}
60+
}
61+
62+
return nil
63+
}
64+
65+
private func loadText(from provider: NSItemProvider, typeIdentifier: String) async -> String? {
66+
await withCheckedContinuation { continuation in
67+
provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in
68+
let text: String?
69+
70+
if let string = item as? String {
71+
text = string
72+
} else if let attributedString = item as? NSAttributedString {
73+
text = attributedString.string
74+
} else if let data = item as? Data {
75+
text = String(data: data, encoding: .utf8)
76+
} else if let url = item as? URL {
77+
text = try? String(contentsOf: url, encoding: .utf8)
78+
} else {
79+
text = nil
80+
}
81+
82+
guard let text, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
83+
continuation.resume(returning: nil)
84+
return
85+
}
86+
87+
continuation.resume(returning: text)
88+
}
89+
}
90+
}
91+
92+
private func openMainAppForAssistantSharedText() {
93+
guard let url = URL(string: "nextcloud://assistant/shared-text") else {
94+
extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
95+
return
96+
}
97+
98+
openAssistantSharedTextURLThroughResponderChain(url)
99+
100+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
101+
self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
102+
}
103+
}
104+
105+
/// Opens the Assistant shared-text deep link from the Share extension.
106+
///
107+
/// Share extensions cannot use `UIApplication.shared` directly because it is not
108+
/// extension-safe. This method walks the responder chain until it finds the hidden
109+
/// `UIApplication` responder and invokes the modern `open(_:options:completionHandler:)`
110+
/// Objective-C selector dynamically.
111+
///
112+
/// This is intentionally isolated because it relies on Objective-C runtime dispatch.
113+
///
114+
/// - Parameter url: Deep link URL to open in the containing application.
115+
private func openAssistantSharedTextURLThroughResponderChain(_ url: URL) {
116+
let selector = NSSelectorFromString("openURL:options:completionHandler:")
117+
let applicationClass: AnyClass? = NSClassFromString("UIApplication")
118+
var responder: UIResponder? = self
119+
120+
while let currentResponder = responder {
121+
guard let applicationClass,
122+
currentResponder.isKind(of: applicationClass),
123+
currentResponder.responds(to: selector),
124+
let implementation = currentResponder.method(for: selector) else {
125+
responder = currentResponder.next
126+
continue
127+
}
128+
129+
typealias CompletionBlock = @convention(block) (Bool) -> Void
130+
typealias OpenURLFunction = @convention(c) (AnyObject, Selector, NSURL, NSDictionary, CompletionBlock?) -> Void
131+
132+
let openURL = unsafeBitCast(implementation, to: OpenURLFunction.self)
133+
134+
let completion: CompletionBlock = { success in
135+
if success {
136+
nkLog(debug: "Assistant shared text deep link performed through modern responder chain")
137+
} else {
138+
nkLog(error: "Assistant shared text deep link modern responder chain returned false")
139+
}
140+
}
141+
142+
openURL(currentResponder, selector, url as NSURL, NSDictionary(), completion)
143+
return
144+
}
145+
146+
nkLog(error: "Assistant shared text deep link failed because no UIApplication responder can open URL")
147+
}
148+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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">
3+
<device id="retina6_12" orientation="portrait" appearance="light"/>
4+
<dependencies>
5+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24743"/>
6+
</dependencies>
7+
<scenes>
8+
<!--Image-->
9+
<scene sceneID="7MM-of-jgj">
10+
<objects>
11+
<viewController title="Image" id="ObA-dk-sSI" customClass="ActionViewController" customModule="Action_Assistant" sceneMemberID="viewController">
12+
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
13+
<size key="freeformSize" width="320" height="528"/>
14+
</viewController>
15+
<placeholder placeholderIdentifier="IBFirstResponder" id="X47-rx-isc" userLabel="First Responder" sceneMemberID="firstResponder"/>
16+
</objects>
17+
<point key="canvasLocation" x="139" y="131"/>
18+
</scene>
19+
</scenes>
20+
</document>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "Senza titolo.png",
5+
"idiom" : "universal",
6+
"platform" : "ios",
7+
"size" : "1024x1024"
8+
},
9+
{
10+
"appearances" : [
11+
{
12+
"appearance" : "luminosity",
13+
"value" : "dark"
14+
}
15+
],
16+
"filename" : "Senza titolo 1.png",
17+
"idiom" : "universal",
18+
"platform" : "ios",
19+
"size" : "1024x1024"
20+
},
21+
{
22+
"appearances" : [
23+
{
24+
"appearance" : "luminosity",
25+
"value" : "tinted"
26+
}
27+
],
28+
"filename" : "Senza titolo 2.png",
29+
"idiom" : "universal",
30+
"platform" : "ios",
31+
"size" : "1024x1024"
32+
}
33+
],
34+
"info" : {
35+
"author" : "xcode",
36+
"version" : 1
37+
}
38+
}
8.22 KB
Loading
8.22 KB
Loading
8.22 KB
Loading
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.application-groups</key>
6+
<array>
7+
<string>group.it.twsweb.Crypto-Cloud</string>
8+
</array>
9+
<key>keychain-access-groups</key>
10+
<array>
11+
<string>$(AppIdentifierPrefix)it.twsweb.Crypto-Cloud</string>
12+
</array>
13+
</dict>
14+
</plist>

Brand/Action_Assistant.plist

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDisplayName</key>
6+
<string>Nextcloud Assistant</string>
7+
8+
<key>NSExtension</key>
9+
<dict>
10+
<key>NSExtensionAttributes</key>
11+
<dict>
12+
<key>NSExtensionActivationRule</key>
13+
<string>TRUEPREDICATE</string>
14+
</dict>
15+
16+
<key>NSExtensionMainStoryboard</key>
17+
<string>MainInterface</string>
18+
19+
<key>NSExtensionPointIdentifier</key>
20+
<string>com.apple.ui-services</string>
21+
</dict>
22+
</dict>
23+
</plist>

Brand/Share.plist

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,12 @@
3030
<key>NSExtensionAttributes</key>
3131
<dict>
3232
<key>NSExtensionActivationRule</key>
33-
<string>SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments,$attachment,(ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data")).@count == $extensionItem.attachments.@count).@count &gt; 0
34-
</string>
33+
<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>
3534
</dict>
3635
<key>NSExtensionMainStoryboard</key>
3736
<string>MainInterface</string>
3837
<key>NSExtensionPointIdentifier</key>
3938
<string>com.apple.share-services</string>
4039
</dict>
4140
</dict>
42-
</plist>
41+
</plist>

0 commit comments

Comments
 (0)