about summary refs log tree commit diff
path: root/modules/Share-with-Bluesky
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-02-27 15:22:03 -0800
committerGitHub <noreply@github.com>2024-02-27 15:22:03 -0800
commitd451f82f54974b7b3da1477a7e1f221628860f62 (patch)
tree631d60b9d9ef47129529068753b10029fb99c34f /modules/Share-with-Bluesky
parentac726497a475f7492ee0269851979817b17d98c2 (diff)
downloadvoidsky-d451f82f54974b7b3da1477a7e1f221628860f62.tar.zst
Share Extension/Intents (#2587)
* add native ios code outside of ios project

* helper script

* going to be a lot of these commits to squash...backing up

* save

* start of an expo plugin

* create info.plist

* copy the view controller

* maybe working

* working

* wait working now

* working plugin

* use current scheme

* update intent path

* use better params

* support text in uri

* build

* use better encoding

* handle images

* cleanup ios plugin

* android

* move bash script to /scripts

* handle cases where loaded data is uiimage rather than uri

* remove unnecessary logic, allow more than 4 images and just take first 4

* android build plugin

* limit images to four on android

* use js for plugins, no need to build

* revert changes to app config

* use correct scheme on android

* android readme

* move ios extension to /modules

* remove unnecessary event

* revert typo

* plugin readme

* scripts readme

* add configurable scheme to .env, default to `bluesky`

* remove debug

* revert .gitignore change

* add comment about updating .env to app.config.js for those modifying scheme

* modify .env

* update android module to use the proper url

* update ios extension

* remove comment

* parse and validate incoming image uris

* fix types

* rm oops

* fix a few typos
Diffstat (limited to 'modules/Share-with-Bluesky')
-rw-r--r--modules/Share-with-Bluesky/Info.plist41
-rw-r--r--modules/Share-with-Bluesky/Share-with-Bluesky.entitlements10
-rw-r--r--modules/Share-with-Bluesky/ShareViewController.swift153
3 files changed, 204 insertions, 0 deletions
diff --git a/modules/Share-with-Bluesky/Info.plist b/modules/Share-with-Bluesky/Info.plist
new file mode 100644
index 000000000..90fe92345
--- /dev/null
+++ b/modules/Share-with-Bluesky/Info.plist
@@ -0,0 +1,41 @@
+<?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>NSExtension</key>
+    <dict>
+      <key>NSExtensionPrincipalClass</key>
+      <string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
+      <key>NSExtensionAttributes</key>
+      <dict>
+        <key>NSExtensionActivationRule</key>
+        <dict>
+          <key>NSExtensionActivationSupportsText</key>
+          <true/>
+          <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
+          <integer>1</integer>
+          <key>NSExtensionActivationSupportsImageWithMaxCount</key>
+          <integer>10</integer>
+        </dict>
+      </dict>
+      <key>NSExtensionPointIdentifier</key>
+      <string>com.apple.share-services</string>
+    </dict>
+    <key>MainAppScheme</key>
+    <string>bluesky</string>
+    <key>CFBundleName</key>
+    <string>$(PRODUCT_NAME)</string>
+    <key>CFBundleDisplayName</key>
+    <string>Extension</string>
+    <key>CFBundleIdentifier</key>
+    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+    <key>CFBundleVersion</key>
+    <string>$(CURRENT_PROJECT_VERSION)</string>
+    <key>CFBundleExecutable</key>
+    <string>$(EXECUTABLE_NAME)</string>
+    <key>CFBundlePackageType</key>
+    <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+    <key>CFBundleShortVersionString</key>
+    <string>$(MARKETING_VERSION)</string>
+  </dict>
+</plist>
diff --git a/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements b/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements
new file mode 100644
index 000000000..22ca9157f
--- /dev/null
+++ b/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements
@@ -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.xyz.blueskyweb.app</string>
+    </array>
+  </dict>
+</plist>
diff --git a/modules/Share-with-Bluesky/ShareViewController.swift b/modules/Share-with-Bluesky/ShareViewController.swift
new file mode 100644
index 000000000..a16a290bf
--- /dev/null
+++ b/modules/Share-with-Bluesky/ShareViewController.swift
@@ -0,0 +1,153 @@
+import UIKit
+
+class ShareViewController: UIViewController {
+  // This allows other forks to use this extension while also changing their
+  // scheme.
+  let appScheme = Bundle.main.object(forInfoDictionaryKey: "MainAppScheme") as? String ?? "bluesky"
+
+  //
+  override func viewDidAppear(_ animated: Bool) {
+    super.viewDidAppear(animated)
+
+    guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
+          let attachments = extensionItem.attachments,
+          let firstAttachment = extensionItem.attachments?.first
+    else {
+      self.completeRequest()
+      return
+    }
+
+    Task {
+      if firstAttachment.hasItemConformingToTypeIdentifier("public.text") {
+        await self.handleText(item: firstAttachment)
+      } else if firstAttachment.hasItemConformingToTypeIdentifier("public.url") {
+        await self.handleUrl(item: firstAttachment)
+      } else if firstAttachment.hasItemConformingToTypeIdentifier("public.image") {
+        await self.handleImages(items: attachments)
+      } else {
+        self.completeRequest()
+      }
+    }
+  }
+
+  private func handleText(item: NSItemProvider) async -> Void {
+    do {
+      if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String {
+        if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
+           let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
+        {
+          _ = self.openURL(url)
+        }
+      }
+      self.completeRequest()
+    } catch {
+      self.completeRequest()
+    }
+  }
+
+  private func handleUrl(item: NSItemProvider) async -> Void {
+    do {
+      if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL {
+        if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
+           let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
+        {
+          _ = self.openURL(url)
+        }
+      }
+      self.completeRequest()
+    } catch {
+      self.completeRequest()
+    }
+  }
+
+  private func handleImages(items: [NSItemProvider]) async -> Void {
+    let firstFourItems: [NSItemProvider]
+    if items.count < 4 {
+      firstFourItems = items
+    } else {
+      firstFourItems = Array(items[0...3])
+    }
+
+    var valid = true
+    var imageUris = ""
+
+    for (index, item) in firstFourItems.enumerated() {
+      var imageUriInfo: String? = nil
+
+      do {
+        if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL {
+          // We need to duplicate this image, since we don't have access to the outgoing temp directory
+          // We also will get the image dimensions here, sinze RN makes it difficult to get those dimensions for local files
+          let data = try Data(contentsOf: dataUri)
+          let image = UIImage(data: data)
+          imageUriInfo = self.saveImageWithInfo(image)
+        } else if let image = try await item.loadItem(forTypeIdentifier: "public.image") as? UIImage {
+          imageUriInfo = self.saveImageWithInfo(image)
+        }
+      } catch {
+        valid = false
+      }
+
+      if let imageUriInfo = imageUriInfo {
+        imageUris.append(imageUriInfo)
+        if index < items.count - 1 {
+          imageUris.append(",")
+        }
+      } else {
+        valid = false
+      }
+    }
+
+    if valid,
+       let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
+       let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)")
+    {
+      _ = self.openURL(url)
+    }
+
+    self.completeRequest()
+  }
+
+  private func saveImageWithInfo(_ image: UIImage?) -> String? {
+    guard let image = image else {
+      return nil
+    }
+
+    do {
+      // Saving this file to the bundle group's directory lets us access it from
+      // inside of the app. Otherwise, we wouldn't have access even though the
+      // extension does.
+      if let dir = FileManager()
+        .containerURL(
+          forSecurityApplicationGroupIdentifier: "group.\(Bundle.main.bundleIdentifier?.replacingOccurrences(of: ".Share-with-Bluesky", with: "") ?? "")")
+      {
+        let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"
+
+        if let newUri = URL(string: filePath),
+           let jpegData = image.jpegData(compressionQuality: 1)
+        {
+          try jpegData.write(to: newUri)
+          return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)"
+        }
+      }
+      return nil
+    } catch {
+      return nil
+    }
+  }
+
+  private func completeRequest() -> Void {
+    self.extensionContext?.completeRequest(returningItems: nil)
+  }
+
+  @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
+  }
+}