mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
Add iOS Share Extension support
- Create share extension to handle shared content from other apps - Support sharing text, URLs, and prepare for future image support - Use app groups for data sharing between extension and main app - Add URL scheme handling for app communication - Add entitlements for app groups - Display system message when content is shared - Update project configuration to include share extension
This commit is contained in:
@@ -26,6 +26,14 @@ struct BitchatApp: App {
|
|||||||
.environmentObject(chatViewModel)
|
.environmentObject(chatViewModel)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
NotificationDelegate.shared.chatViewModel = chatViewModel
|
NotificationDelegate.shared.chatViewModel = chatViewModel
|
||||||
|
#if os(iOS)
|
||||||
|
appDelegate.chatViewModel = chatViewModel
|
||||||
|
#endif
|
||||||
|
// Check for shared content
|
||||||
|
checkForSharedContent()
|
||||||
|
}
|
||||||
|
.onOpenURL { url in
|
||||||
|
handleURL(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@@ -33,10 +41,57 @@ struct BitchatApp: App {
|
|||||||
.windowResizability(.contentSize)
|
.windowResizability(.contentSize)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleURL(_ url: URL) {
|
||||||
|
if url.scheme == "bitchat" && url.host == "share" {
|
||||||
|
// Handle shared content
|
||||||
|
checkForSharedContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkForSharedContent() {
|
||||||
|
// Check app group for shared content from extension
|
||||||
|
guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat"),
|
||||||
|
let sharedContent = userDefaults.string(forKey: "sharedContent"),
|
||||||
|
let sharedDate = userDefaults.object(forKey: "sharedContentDate") as? Date else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process if shared within last 30 seconds
|
||||||
|
if Date().timeIntervalSince(sharedDate) < 30 {
|
||||||
|
let contentType = userDefaults.string(forKey: "sharedContentType") ?? "text"
|
||||||
|
|
||||||
|
// Clear the shared content
|
||||||
|
userDefaults.removeObject(forKey: "sharedContent")
|
||||||
|
userDefaults.removeObject(forKey: "sharedContentType")
|
||||||
|
userDefaults.removeObject(forKey: "sharedContentDate")
|
||||||
|
userDefaults.synchronize()
|
||||||
|
|
||||||
|
// Show notification about shared content
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
// Add system message about sharing
|
||||||
|
let systemMessage = BitchatMessage(
|
||||||
|
sender: "system",
|
||||||
|
content: "preparing to share \(contentType)...",
|
||||||
|
timestamp: Date(),
|
||||||
|
isRelay: false
|
||||||
|
)
|
||||||
|
self.chatViewModel.messages.append(systemMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the shared content after a short delay
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||||
|
let prefix = contentType == "url" ? "Shared link: " : ""
|
||||||
|
self.chatViewModel.sendMessage(prefix + sharedContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
|
weak var chatViewModel: ChatViewModel?
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,5 +40,16 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>bitchat</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>chat.bitchat</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?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.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.chat.bitchat</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?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>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>bitchat</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionAttributes</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionActivationRule</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionActivationSupportsText</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.share-services</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
//
|
||||||
|
// ShareViewController.swift
|
||||||
|
// bitchatShareExtension
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
// For more information, see <https://unlicense.org>
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Social
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
class ShareViewController: SLComposeServiceViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
// Set placeholder text
|
||||||
|
placeholder = "Share to bitchat..."
|
||||||
|
// Set character limit (optional)
|
||||||
|
charactersRemaining = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
override func isContentValid() -> Bool {
|
||||||
|
// Validate that we have text content or attachments
|
||||||
|
if let text = contentText, !text.isEmpty {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Check if we have attachments
|
||||||
|
if let item = extensionContext?.inputItems.first as? NSExtensionItem,
|
||||||
|
let attachments = item.attachments,
|
||||||
|
!attachments.isEmpty {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didSelectPost() {
|
||||||
|
// Get the shared content
|
||||||
|
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else {
|
||||||
|
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process different types of shared content
|
||||||
|
for itemProvider in extensionItem.attachments ?? [] {
|
||||||
|
if itemProvider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
|
||||||
|
itemProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { [weak self] (item, error) in
|
||||||
|
if let text = item as? String {
|
||||||
|
self?.handleSharedText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
||||||
|
itemProvider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] (item, error) in
|
||||||
|
if let url = item as? URL {
|
||||||
|
self?.handleSharedURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||||||
|
itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (item, error) in
|
||||||
|
if let image = item as? UIImage {
|
||||||
|
self?.handleSharedImage(image)
|
||||||
|
} else if let data = item as? Data {
|
||||||
|
if let image = UIImage(data: data) {
|
||||||
|
self?.handleSharedImage(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have content text, share it
|
||||||
|
if let text = contentText, !text.isEmpty {
|
||||||
|
handleSharedText(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete the share action
|
||||||
|
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func configurationItems() -> [Any]! {
|
||||||
|
// No configuration items needed
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
|
private func handleSharedText(_ text: String) {
|
||||||
|
// Save to shared user defaults to pass to main app
|
||||||
|
saveToSharedDefaults(content: text, type: "text")
|
||||||
|
openMainApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleSharedURL(_ url: URL) {
|
||||||
|
// Convert URL to text and share
|
||||||
|
let text = url.absoluteString
|
||||||
|
saveToSharedDefaults(content: text, type: "url")
|
||||||
|
openMainApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleSharedImage(_ image: UIImage) {
|
||||||
|
// For now, we'll just notify that image sharing isn't supported
|
||||||
|
// In the future, we could implement image sharing via the mesh
|
||||||
|
saveToSharedDefaults(content: "Image sharing coming soon!", type: "image")
|
||||||
|
openMainApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveToSharedDefaults(content: String, type: String) {
|
||||||
|
// Use app groups to share data between extension and main app
|
||||||
|
if let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") {
|
||||||
|
userDefaults.set(content, forKey: "sharedContent")
|
||||||
|
userDefaults.set(type, forKey: "sharedContentType")
|
||||||
|
userDefaults.set(Date(), forKey: "sharedContentDate")
|
||||||
|
userDefaults.synchronize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openMainApp() {
|
||||||
|
// Note: Share extensions cannot directly open the containing app
|
||||||
|
// The user will need to tap on the notification or manually open the app
|
||||||
|
// to see the shared content
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?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.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.chat.bitchat</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
+26
@@ -47,6 +47,32 @@ targets:
|
|||||||
CODE_SIGNING_ALLOWED: YES
|
CODE_SIGNING_ALLOWED: YES
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
||||||
|
CODE_SIGN_ENTITLEMENTS: bitchat/bitchat.entitlements
|
||||||
|
dependencies:
|
||||||
|
- target: bitchatShareExtension
|
||||||
|
platformFilter: ios
|
||||||
|
embed: true
|
||||||
|
|
||||||
|
bitchatShareExtension:
|
||||||
|
type: app-extension
|
||||||
|
platform: iOS
|
||||||
|
sources:
|
||||||
|
- bitchatShareExtension
|
||||||
|
info:
|
||||||
|
path: bitchatShareExtension/Info.plist
|
||||||
|
properties:
|
||||||
|
CFBundleDisplayName: bitchat
|
||||||
|
CFBundleShortVersionString: $(MARKETING_VERSION)
|
||||||
|
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||||
|
settings:
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat.ShareExtension
|
||||||
|
INFOPLIST_FILE: bitchatShareExtension/Info.plist
|
||||||
|
SWIFT_VERSION: 5.0
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET: 16.0
|
||||||
|
CODE_SIGN_STYLE: Automatic
|
||||||
|
CODE_SIGNING_REQUIRED: YES
|
||||||
|
CODE_SIGNING_ALLOWED: YES
|
||||||
|
CODE_SIGN_ENTITLEMENTS: bitchatShareExtension/bitchatShareExtension.entitlements
|
||||||
|
|
||||||
bitchatTests_iOS:
|
bitchatTests_iOS:
|
||||||
type: bundle.unit-test
|
type: bundle.unit-test
|
||||||
|
|||||||
Reference in New Issue
Block a user