carsten-klaffke / send-intent

Repository for send-intent Capacitor plugin
MIT License
106 stars 12 forks source link

Sharing multiple files from WhatsApp on iOS #75

Open StepZEROsk opened 1 year ago

StepZEROsk commented 1 year ago

Describe the bug Sharing works correctly when I select multiple photos from image gallery and share them to app. But when I receive photos in message using WhatsApp app, I select them in WhatsApp and share them to my app. I receive the intent with first photo multiple times.

To Reproduce Steps to reproduce the behavior:

  1. Go to WhatsApp
  2. Select multiple photos received by WhatsApp message
  3. Select Share and select the app which has the plugin implemented
  4. The received intent contains only one photo multiple times

Expected behavior Receive intent with multiple photos

Screenshots If applicable, add screenshots to help explain your problem.

Smartphone (please complete the following information):

Additional context

Data received from plugin:

[log] - intent {"type":"application%2Fjpg","url":"file%3A%2F%2F%2Fprivate%2Fvar%2Fmobile%2FContainers%2FShared%2FAppGroup%2FAF4D217D-A918-4245-9C60-ACDF619D2EBC%2F%2FPHOTO-2023-03-24-09-49-39.jpg","additionalItems":[{"url":"file%3A%2F%2F%2Fprivate%2Fvar%2Fmobile%2FContainers%2FShared%2FAppGroup%2FAF4D217D-A918-4245-9C60-ACDF619D2EBC%2F%2FPHOTO-2023-03-24-09-49-39.jpg","type":"application%2Fjpg","description":"","title":"PHOTO-2023-03-24-09-49-39.jpg"},{"url":"file%3A%2F%2F%2Fprivate%2Fvar%2Fmobile%2FContainers%2FShared%2FAppGroup%2FAF4D217D-A918-4245-9C60-ACDF619D2EBC%2F%2FPHOTO-2023-03-24-09-49-39.jpg","type":"application%2Fjpg","description":"","title":"PHOTO-2023-03-24-09-49-39.jpg"},{"title":"PHOTO-2023-03-24-09-49-39.jpg","description":"","url":"file%3A%2F%2F%2Fprivate%2Fvar%2Fmobile%2FContainers%2FShared%2FAppGroup%2FAF4D217D-A918-4245-9C60-ACDF619D2EBC%2F%2FPHOTO-2023-03-24-09-49-39.jpg","type":"application%2Fjpg"},{"title":"PHOTO-2023-03-24-09-49-39.jpg","description":"","url":"file%3A%2F%2F%2Fprivate%2Fvar%2Fmobile%2FContainers%2FShared%2FAppGroup%2FAF4D217D-A918-4245-9C60-ACDF619D2EBC%2F%2FPHOTO-2023-03-24-09-49-39.jpg","type":"application%2Fjpg"}],"description":"","title":"PHOTO-2023-03-24-09-49-39.jpg"}

tarangshah19 commented 1 year ago

hi i dont have solution but i am not able to do successfully on ios can you please tell how you add and how you create group id or if you dont mind can you share code please

StepZEROsk commented 1 year ago

Hello,

thankYou for your answer.

Im using your code from doc. See details below.

AppDelegate.swift `import UIKit import Capacitor import SendIntent

@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
let store = ShareStore.store

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    return true
}

func applicationWillResignActive(_ application: UIApplication) {
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}

func applicationDidEnterBackground(_ application: UIApplication) {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

func applicationWillEnterForeground(_ application: UIApplication) {
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}

func applicationDidBecomeActive(_ application: UIApplication) {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

func applicationWillTerminate(_ application: UIApplication) {
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {

    var success = true
    if CAPBridge.handleOpenUrl(url, options) {
        success = ApplicationDelegateProxy.shared.application(app, open: url, options: options)
    }

    guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
          let params = components.queryItems else {
              return false
          }

    let titles = params.filter { $0.name == "title" }
    let descriptions = params.filter { $0.name == "description" }
    let types = params.filter { $0.name == "type" }
    let urls = params.filter { $0.name == "url" }

    store.shareItems.removeAll()
    if(titles.count > 0){
        for index in 0...titles.count-1 {
            var shareItem: JSObject = JSObject()
            shareItem["title"] = titles[index].value!
            shareItem["description"] = descriptions[index].value!
            shareItem["type"] = types[index].value!
            shareItem["url"] = urls[index].value!
            store.shareItems.append(shareItem)
        }
    }

    dump(store.shareItems)

    store.processed = false
    let nc = NotificationCenter.default
    nc.post(name: Notification.Name("triggerSendIntent"), object: nil )

    return success
}

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    // Called when the app was launched with an activity, including Universal Links.
    // Feel free to add additional processing here, but if you want the App API to support
    // tracking app url opens, make sure to keep this call
    return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
  NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
  NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
}

} ` ShareViewController.swift

` import MobileCoreServices import Social import UIKit

class ShareItem {

public var title: String?
public var type: String?
public var url: String?

}

class ShareViewController: UIViewController {

private var shareItems: [ShareItem] = []

override public func viewDidAppear(_ animated: Bool) {
   super.viewDidAppear(animated)
   self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}

private func sendData() {
    let queryItems = shareItems.map {
        [
            URLQueryItem(
                name: "title",
                value: $0.title?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
            URLQueryItem(name: "description", value: ""),
            URLQueryItem(
                name: "type",
                value: $0.type?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
            URLQueryItem(
                name: "url",
                value: $0.url?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
        ]
    }.flatMap({ $0 })

    var urlComps = URLComponents(string: "vcpgo://")!
    urlComps.queryItems = queryItems
    openURL(urlComps.url!)
}

fileprivate func createSharedFileUrl(_ url: URL?) -> String {
    let fileManager = FileManager.default

    let copyFileUrl =
    fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.vcpgo")!
        .absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + "/" + url!
        .lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
    try? Data(contentsOf: url!).write(to: URL(string: copyFileUrl)!)

    return copyFileUrl
}

func saveScreenshot(_ image: UIImage) -> String {
    let fileManager = FileManager.default

    let copyFileUrl =
    fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.vcpgo")!
        .absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
    + "/screenshot.png"
    do {
        try image.pngData()?.write(to: URL(string: copyFileUrl)!)
        return copyFileUrl
    } catch {
        print(error.localizedDescription)
        return ""
    }
}

fileprivate func handleTypeUrl(_ attachment: NSItemProvider)
async throws -> ShareItem
{
    let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil)
    let url = results as! URL?
    let shareItem: ShareItem = ShareItem()

    if url!.isFileURL {
        shareItem.title = url!.lastPathComponent
        shareItem.type = "application/" + url!.pathExtension.lowercased()
        shareItem.url = createSharedFileUrl(url)
    } else {
        shareItem.title = url!.absoluteString
        shareItem.url = url!.absoluteString
        shareItem.type = "text/plain"
    }

    return shareItem
}

fileprivate func handleTypeText(_ attachment: NSItemProvider)
async throws -> ShareItem
{
    let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil)
    let shareItem: ShareItem = ShareItem()
    let text = results as! String
    shareItem.title = text
    shareItem.type = "text/plain"
    return shareItem
}

fileprivate func handleTypeMovie(_ attachment: NSItemProvider)
async throws -> ShareItem
{
    let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeMovie as String, options: nil)
    let shareItem: ShareItem = ShareItem()

    let url = results as! URL?
    shareItem.title = url!.lastPathComponent
    shareItem.type = "video/" + url!.pathExtension.lowercased()
    shareItem.url = createSharedFileUrl(url)
    return shareItem
}

fileprivate func handleTypeImage(_ attachment: NSItemProvider)
async throws -> ShareItem
{
    let data = try await attachment.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil)

    let shareItem: ShareItem = ShareItem()
    switch data {
    case let image as UIImage:
        shareItem.title = "screenshot"
        shareItem.type = "image/png"
        shareItem.url = self.saveScreenshot(image)
    case let url as URL:
        shareItem.title = url.lastPathComponent
        shareItem.type = "image/" + url.pathExtension.lowercased()
        shareItem.url = self.createSharedFileUrl(url)
    default:
        print("Unexpected image data:", type(of: data))
    }
    return shareItem
}

override public func viewDidLoad() {
    super.viewDidLoad()

    shareItems.removeAll()

    let extensionItem = extensionContext?.inputItems[0] as! NSExtensionItem
    Task {
        try await withThrowingTaskGroup(
            of: ShareItem.self,
            body: { taskGroup in

                for attachment in extensionItem.attachments! {
                    if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
                        taskGroup.addTask {
                            return try await self.handleTypeUrl(attachment)
                        }
                    } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
                        taskGroup.addTask {
                            return try await self.handleTypeText(attachment)
                        }
                    } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
                        taskGroup.addTask {
                            return try await self.handleTypeMovie(attachment)
                        }
                    } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
                        taskGroup.addTask {
                            return try await self.handleTypeImage(attachment)
                        }
                    }
                }

                for try await item in taskGroup {
                    self.shareItems.append(item)
                }
            })

        self.sendData()

    }
}

@objc func openURL(_ url: URL) -> Bool {
    var responder: UIResponder? = self
    while responder != nil {
        if let application = responder as? UIApplication {
            return application.perform(#selector(openURL(_:)), with: url) != nil
        }
        responder = responder?.next
    }
    return false
}

} ` Dump from AppDelegate.swift shows following data. Even if Im sharing 5 different files, in the dump you can see one exact file 5 times instead of 5 different files.

`5 elements ▿ 4 key/value pairs ▿ (2 elements)

tarangshah19 commented 1 year ago

thanks

carsten-klaffke commented 1 year ago

Hello! Is this only on Whatsapp? Have you tried debugging ShareViewController.swift to see if the data arrives correctly at the plugin? (If not, I will test it later but I will have to set up a case first) Best regards

StepZEROsk commented 1 year ago

@carsten-klaffke Hi, it only happens when I share from WhatsApp, sharing multiple files from gallery works great. I tried to debug ShareViewController but I was not able to come to anything. ThankYou very much.

StepZEROsk commented 1 year ago

@tarangshah19 @carsten-klaffke Guys, any update on this issue please?

carsten-klaffke commented 1 year ago

I created an Example (uploaded it to this repository) to test multiple file share. But I only have an iPad (no iPhone) and Whatsapp is not available for that. If you'd find another app where this happens, I could test it. Otherwise I have to wait to get an iPhone borrowed.

StepZEROsk commented 1 year ago

@carsten-klaffke same thing happens when you share from Telegram or Signal messaging app, can you please test it in one of those apps, they might be available for iPad, ThankYou!

carsten-klaffke commented 1 year ago

Hey @StepZEROsk, I was now able to reproduce the bug (with Signal). There was an error in ShareViewController.swift, which put the same name for all images if they come as type image and not URL. I fixed it in the Code of SendIntentExample and also updated the Readme accordingly. Please let me know if this solves it for you!

StepZEROsk commented 1 year ago

@carsten-klaffke it works with Signal and Telegram, but still doesn't work with WhatsApp. Im not sure what the different is, but when I share from WhatsApp i'm getting the actual file name, not the name generated with the index, like Im getting from Signal and Telegram.

StepZEROsk commented 1 year ago

@carsten-klaffke From WhatsApp they comes like URL