diff --git a/bitchat/BitchatApp.swift b/bitchat/BitchatApp.swift index e9152714..efc1d6f6 100644 --- a/bitchat/BitchatApp.swift +++ b/bitchat/BitchatApp.swift @@ -26,6 +26,14 @@ struct BitchatApp: App { .environmentObject(chatViewModel) .onAppear { NotificationDelegate.shared.chatViewModel = chatViewModel + #if os(iOS) + appDelegate.chatViewModel = chatViewModel + #endif + // Check for shared content + checkForSharedContent() + } + .onOpenURL { url in + handleURL(url) } } #if os(macOS) @@ -33,10 +41,57 @@ struct BitchatApp: App { .windowResizability(.contentSize) #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) class AppDelegate: NSObject, UIApplicationDelegate { + weak var chatViewModel: ChatViewModel? + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { return true } diff --git a/bitchat/Info.plist b/bitchat/Info.plist index 6b60e722..20ca8dfe 100644 --- a/bitchat/Info.plist +++ b/bitchat/Info.plist @@ -40,5 +40,16 @@ UIInterfaceOrientationPortrait + CFBundleURLTypes + + + CFBundleURLSchemes + + bitchat + + CFBundleURLName + chat.bitchat + + diff --git a/bitchat/bitchat.entitlements b/bitchat/bitchat.entitlements new file mode 100644 index 00000000..80566616 --- /dev/null +++ b/bitchat/bitchat.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.chat.bitchat + + + \ No newline at end of file diff --git a/bitchatShareExtension/Info.plist b/bitchatShareExtension/Info.plist new file mode 100644 index 00000000..d2b306a6 --- /dev/null +++ b/bitchatShareExtension/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + bitchat + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 1 + + + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareViewController + NSExtensionPointIdentifier + com.apple.share-services + + + \ No newline at end of file diff --git a/bitchatShareExtension/ShareViewController.swift b/bitchatShareExtension/ShareViewController.swift new file mode 100644 index 00000000..c323653a --- /dev/null +++ b/bitchatShareExtension/ShareViewController.swift @@ -0,0 +1,122 @@ +// +// ShareViewController.swift +// bitchatShareExtension +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +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 + } +} \ No newline at end of file diff --git a/bitchatShareExtension/bitchatShareExtension.entitlements b/bitchatShareExtension/bitchatShareExtension.entitlements new file mode 100644 index 00000000..80566616 --- /dev/null +++ b/bitchatShareExtension/bitchatShareExtension.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.chat.bitchat + + + \ No newline at end of file diff --git a/project.yml b/project.yml index 6741aebd..ba00597c 100644 --- a/project.yml +++ b/project.yml @@ -47,6 +47,32 @@ targets: CODE_SIGNING_ALLOWED: YES ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon 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: type: bundle.unit-test