From 7f6a32e350e173855846dd33775df92d7ea6ab65 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 8 Jul 2025 04:18:42 +0200 Subject: [PATCH 1/5] 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 --- bitchat/BitchatApp.swift | 55 ++++++++ bitchat/Info.plist | 11 ++ bitchat/bitchat.entitlements | 12 ++ bitchatShareExtension/Info.plist | 43 ++++++ .../ShareViewController.swift | 122 ++++++++++++++++++ .../bitchatShareExtension.entitlements | 12 ++ project.yml | 26 ++++ 7 files changed, 281 insertions(+) create mode 100644 bitchat/bitchat.entitlements create mode 100644 bitchatShareExtension/Info.plist create mode 100644 bitchatShareExtension/ShareViewController.swift create mode 100644 bitchatShareExtension/bitchatShareExtension.entitlements 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 From 6a7b33047a9c3b4b0f298ef6d6f9c5b7a51324a6 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 8 Jul 2025 04:22:56 +0200 Subject: [PATCH 2/5] Fix share extension Info.plist - add missing NSExtension dictionary --- bitchatShareExtension/Info.plist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitchatShareExtension/Info.plist b/bitchatShareExtension/Info.plist index d2b306a6..4f43a179 100644 --- a/bitchatShareExtension/Info.plist +++ b/bitchatShareExtension/Info.plist @@ -15,7 +15,7 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) + XPC! CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion @@ -40,4 +40,4 @@ com.apple.share-services - \ No newline at end of file + From 244af2197cffc1aa792166be2d6014ead2e2c429 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 8 Jul 2025 04:26:02 +0200 Subject: [PATCH 3/5] Fix share extension functionality - Add debugging logs to help diagnose sharing issues - Improve completion handling in share extension - Check for shared content when app becomes active - Fix async handling of shared content processing --- bitchat.xcodeproj/project.pbxproj | 186 +++++++++++++++++- bitchat/BitchatApp.swift | 15 +- bitchat/Info.plist | 11 -- .../ShareViewController.swift | 49 +++-- 4 files changed, 226 insertions(+), 35 deletions(-) diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index 3b59b2f9..72ab5d37 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -10,9 +10,10 @@ 0245710AEAA58AD0A1425234 /* OptimizedBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */; }; 0DAFF1DDE9BA83FF648D5AB3 /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; }; 10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; }; + 17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; }; 1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */; }; - 230B11C5BF035D35638B21C8 /* PasswordProtectedRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12482E4AD78B861C538449 /* PasswordProtectedRoomTests.swift */; }; + 24F17B1446E13F42652B7B08 /* PasswordProtectedChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */; }; 2E71E320EA921498C57E023B /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; }; 4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */; }; 4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; }; @@ -32,7 +33,10 @@ 8F0BFC2D2B2A5E7B70C3B485 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */; }; 8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; }; 923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; }; + 9269B4230187A9EA969BEDB7 /* PasswordProtectedChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */; }; 92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; }; + 97BC7E9FAB24FFE643DB5EB2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 9C7D287C8E67AAE576A5ECB7 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B378C16594575FCC7F9C75 /* ShareViewController.swift */; }; ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; }; AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; }; ADC66F95FBD513B10411ADB3 /* MessagePaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */; }; @@ -49,7 +53,6 @@ DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC1563390A15C042D059CF9 /* EncryptionService.swift */; }; F00B713D5053617FB5F3F1BE /* MessagePaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */; }; F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; }; - F4A689F5F34125AE1BFD5599 /* PasswordProtectedRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12482E4AD78B861C538449 /* PasswordProtectedRoomTests.swift */; }; FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; }; /* End PBXBuildFile section */ @@ -61,6 +64,20 @@ remoteGlobalIDString = AF077EA0474EDEDE2C72716C; remoteInfo = bitchat_iOS; }; + DCB3AD9121A64769FAD7BAD0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 475D96681D0EA0AE57A4E06E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 57CA17A36A2532A6CFF367BB; + remoteInfo = bitchatShareExtension; + }; + E35E7AF9854A2E72452DD34F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 475D96681D0EA0AE57A4E06E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 57CA17A36A2532A6CFF367BB; + remoteInfo = bitchatShareExtension; + }; FF470234EF8C6BB8865B80B5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 475D96681D0EA0AE57A4E06E /* Project object */; @@ -70,7 +87,33 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 1AE5E5D867B14EFDBE2C8889 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 97BC7E9FAB24FFE643DB5EB2 /* bitchatShareExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + B6C356449BAE4E0F650565D1 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ + 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordProtectedChannelTests.swift; sourceTree = ""; }; 03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryTracker.swift; sourceTree = ""; }; 136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = ""; }; @@ -78,9 +121,13 @@ 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = ""; }; 32F149C43D1915831B60FE09 /* CompressionUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompressionUtil.swift; sourceTree = ""; }; 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 3668EEBB42FD4A24D5D83B7B /* bitchatShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchatShareExtension.entitlements; sourceTree = ""; }; + 3A556661F74B7D5AE2F0521B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatMessageTests.swift; sourceTree = ""; }; + 527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchat.entitlements; sourceTree = ""; }; 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = ""; }; + 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetentionService.swift; sourceTree = ""; }; 6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = ""; }; 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = ""; }; @@ -91,6 +138,7 @@ A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = ""; }; AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetryService.swift; sourceTree = ""; }; C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = bitchatTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + C1B378C16594575FCC7F9C75 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizedBloomFilter.swift; sourceTree = ""; }; D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = ""; }; D69A18D27F9A565FD6041E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -98,7 +146,6 @@ EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryOptimizer.swift; sourceTree = ""; }; EF625BB3AD919322C01A46B2 /* BitchatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatApp.swift; sourceTree = ""; }; - FB12482E4AD78B861C538449 /* PasswordProtectedRoomTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordProtectedRoomTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXGroup section */ @@ -106,6 +153,7 @@ isa = PBXGroup; children = ( 2F82C5FC8433F4064F079D1F /* bitchat */, + A2E8C336FA1ADBEC03261DFD /* bitchatShareExtension */, C3D98EB3E1B455E321F519F4 /* bitchatTests */, 9F37F9F2C353B58AC809E93B /* Products */, ); @@ -115,6 +163,7 @@ isa = PBXGroup; children = ( 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */, + 527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */, EF625BB3AD919322C01A46B2 /* BitchatApp.swift */, EA706D8E5097785414646A8E /* Info.plist */, ADD53BCDA233C02E53458926 /* Protocols */, @@ -149,12 +198,23 @@ children = ( 997D512074C64904D75DDD40 /* bitchat.app */, 7EEBDA723E1CFD88758DA4AC /* bitchat.app */, + 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */, C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */, 03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */, ); name = Products; sourceTree = ""; }; + A2E8C336FA1ADBEC03261DFD /* bitchatShareExtension */ = { + isa = PBXGroup; + children = ( + 3668EEBB42FD4A24D5D83B7B /* bitchatShareExtension.entitlements */, + 3A556661F74B7D5AE2F0521B /* Info.plist */, + C1B378C16594575FCC7F9C75 /* ShareViewController.swift */, + ); + path = bitchatShareExtension; + sourceTree = ""; + }; A55126E93155456CAA8D6656 /* Views */ = { isa = PBXGroup; children = ( @@ -181,7 +241,7 @@ 1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */, D69A18D27F9A565FD6041E12 /* Info.plist */, 8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */, - FB12482E4AD78B861C538449 /* PasswordProtectedRoomTests.swift */, + 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */, ); path = bitchatTests; sourceTree = ""; @@ -209,10 +269,12 @@ buildPhases = ( 137ABE739BF20ACDDF8CC605 /* Sources */, 0214973A876129753D39EB47 /* Resources */, + 1AE5E5D867B14EFDBE2C8889 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + D8120CFFA88B852C3D106FA6 /* PBXTargetDependency */, ); name = bitchat_macOS; packageProductDependencies = ( @@ -239,6 +301,23 @@ productReference = 03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 57CA17A36A2532A6CFF367BB /* bitchatShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = E4EA6DC648DF55FF84032EB5 /* Build configuration list for PBXNativeTarget "bitchatShareExtension" */; + buildPhases = ( + 0A08E70F08F55FD5BA8C7EF3 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = bitchatShareExtension; + packageProductDependencies = ( + ); + productName = bitchatShareExtension; + productReference = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */ = { isa = PBXNativeTarget; buildConfigurationList = 38C4AF6313E5037F25CEF30B /* Build configuration list for PBXNativeTarget "bitchatTests_iOS" */; @@ -263,10 +342,12 @@ buildPhases = ( 4E49E34F00154C051AE90FED /* Sources */, CD6E8F32BC38357473954F97 /* Resources */, + B6C356449BAE4E0F650565D1 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 6EB655BA5DB11909C1DEC460 /* PBXTargetDependency */, ); name = bitchat_iOS; packageProductDependencies = ( @@ -287,6 +368,9 @@ 0576A29205865664C0937536 = { ProvisioningStyle = Automatic; }; + 57CA17A36A2532A6CFF367BB = { + ProvisioningStyle = Automatic; + }; AF077EA0474EDEDE2C72716C = { ProvisioningStyle = Automatic; }; @@ -305,6 +389,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( + 57CA17A36A2532A6CFF367BB /* bitchatShareExtension */, 6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */, 47FF23248747DD7CB666CB91 /* bitchatTests_macOS */, AF077EA0474EDEDE2C72716C /* bitchat_iOS */, @@ -333,6 +418,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 0A08E70F08F55FD5BA8C7EF3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9C7D287C8E67AAE576A5ECB7 /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 137ABE739BF20ACDDF8CC605 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -387,7 +480,7 @@ 2E71E320EA921498C57E023B /* BitchatMessageTests.swift in Sources */, C91FDE97070433E6CFE95C55 /* BloomFilterTests.swift in Sources */, ADC66F95FBD513B10411ADB3 /* MessagePaddingTests.swift in Sources */, - F4A689F5F34125AE1BFD5599 /* PasswordProtectedRoomTests.swift in Sources */, + 24F17B1446E13F42652B7B08 /* PasswordProtectedChannelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -399,7 +492,7 @@ 0DAFF1DDE9BA83FF648D5AB3 /* BitchatMessageTests.swift in Sources */, 846E2B446E36639159704730 /* BloomFilterTests.swift in Sources */, F00B713D5053617FB5F3F1BE /* MessagePaddingTests.swift in Sources */, - 230B11C5BF035D35638B21C8 /* PasswordProtectedRoomTests.swift in Sources */, + 9269B4230187A9EA969BEDB7 /* PasswordProtectedChannelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -411,6 +504,16 @@ target = 0576A29205865664C0937536 /* bitchat_macOS */; targetProxy = FF470234EF8C6BB8865B80B5 /* PBXContainerItemProxy */; }; + 6EB655BA5DB11909C1DEC460 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 57CA17A36A2532A6CFF367BB /* bitchatShareExtension */; + targetProxy = E35E7AF9854A2E72452DD34F /* PBXContainerItemProxy */; + }; + D8120CFFA88B852C3D106FA6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 57CA17A36A2532A6CFF367BB /* bitchatShareExtension */; + targetProxy = DCB3AD9121A64769FAD7BAD0 /* PBXContainerItemProxy */; + }; D8C09F21DB7DC06E8E672C21 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = AF077EA0474EDEDE2C72716C /* bitchat_iOS */; @@ -476,6 +579,28 @@ }; name = Release; }; + 3DCF45111852FB2AEBE05E31 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGNING_ALLOWED = YES; + CODE_SIGNING_REQUIRED = YES; + CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = L3N5LHJD5Y; + INFOPLIST_FILE = bitchatShareExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 702E7395723CADA4B830F4A9 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -483,6 +608,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGNING_ALLOWED = YES; CODE_SIGNING_REQUIRED = YES; + CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = L3N5LHJD5Y; @@ -497,9 +623,12 @@ PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -529,6 +658,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGNING_ALLOWED = YES; CODE_SIGNING_REQUIRED = YES; + CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = L3N5LHJD5Y; @@ -543,9 +673,12 @@ PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; @@ -556,6 +689,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGNING_ALLOWED = YES; CODE_SIGNING_REQUIRED = YES; + CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; ENABLE_PREVIEWS = YES; @@ -639,6 +773,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGNING_ALLOWED = YES; CODE_SIGNING_REQUIRED = YES; + CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; ENABLE_PREVIEWS = YES; @@ -722,6 +857,28 @@ }; name = Debug; }; + DAC5E82049F8A97360BE63D6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGNING_ALLOWED = YES; + CODE_SIGNING_REQUIRED = YES; + CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = L3N5LHJD5Y; + INFOPLIST_FILE = bitchatShareExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -770,6 +927,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + E4EA6DC648DF55FF84032EB5 /* Build configuration list for PBXNativeTarget "bitchatShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DAC5E82049F8A97360BE63D6 /* Debug */, + 3DCF45111852FB2AEBE05E31 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; /* End XCConfigurationList section */ }; rootObject = 475D96681D0EA0AE57A4E06E /* Project object */; diff --git a/bitchat/BitchatApp.swift b/bitchat/BitchatApp.swift index efc1d6f6..4bfa649d 100644 --- a/bitchat/BitchatApp.swift +++ b/bitchat/BitchatApp.swift @@ -35,6 +35,12 @@ struct BitchatApp: App { .onOpenURL { url in handleURL(url) } + #if os(iOS) + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in + // Check for shared content when app becomes active + checkForSharedContent() + } + #endif } #if os(macOS) .windowStyle(.hiddenTitleBar) @@ -51,9 +57,14 @@ struct BitchatApp: App { 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"), + guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") else { + print("Failed to access app group UserDefaults") + return + } + + guard let sharedContent = userDefaults.string(forKey: "sharedContent"), let sharedDate = userDefaults.object(forKey: "sharedContentDate") as? Date else { + print("No shared content found in UserDefaults") return } diff --git a/bitchat/Info.plist b/bitchat/Info.plist index 20ca8dfe..6b60e722 100644 --- a/bitchat/Info.plist +++ b/bitchat/Info.plist @@ -40,16 +40,5 @@ UIInterfaceOrientationPortrait - CFBundleURLTypes - - - CFBundleURLSchemes - - bitchat - - CFBundleURLName - chat.bitchat - - diff --git a/bitchatShareExtension/ShareViewController.swift b/bitchatShareExtension/ShareViewController.swift index c323653a..27a65c84 100644 --- a/bitchatShareExtension/ShareViewController.swift +++ b/bitchatShareExtension/ShareViewController.swift @@ -35,46 +35,66 @@ class ShareViewController: SLComposeServiceViewController { } override func didSelectPost() { - // Get the shared content + // If we have content text from the compose view, handle it directly + if let text = contentText, !text.isEmpty { + handleSharedText(text) + // Complete the share action after saving + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + } + return + } + + // Otherwise, process attachments guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else { self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) return } + var hasProcessedContent = false + let group = DispatchGroup() + // Process different types of shared content for itemProvider in extensionItem.attachments ?? [] { if itemProvider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { + group.enter() itemProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { [weak self] (item, error) in if let text = item as? String { self?.handleSharedText(text) + hasProcessedContent = true } + group.leave() } } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { + group.enter() itemProvider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] (item, error) in if let url = item as? URL { self?.handleSharedURL(url) + hasProcessedContent = true } + group.leave() } } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { + group.enter() itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (item, error) in if let image = item as? UIImage { self?.handleSharedImage(image) + hasProcessedContent = true } else if let data = item as? Data { if let image = UIImage(data: data) { self?.handleSharedImage(image) + hasProcessedContent = true } } + group.leave() } } } - // If we have content text, share it - if let text = contentText, !text.isEmpty { - handleSharedText(text) + // Complete after all items are processed + group.notify(queue: .main) { + self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) } - - // Complete the share action - self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) } override func configurationItems() -> [Any]! { @@ -106,12 +126,17 @@ class ShareViewController: SLComposeServiceViewController { 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() + guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") else { + print("ShareExtension: Failed to access app group UserDefaults") + return } + + userDefaults.set(content, forKey: "sharedContent") + userDefaults.set(type, forKey: "sharedContentType") + userDefaults.set(Date(), forKey: "sharedContentDate") + userDefaults.synchronize() + + print("ShareExtension: Saved content of type \(type) to shared defaults") } private func openMainApp() { From b8d930614e4009a7cee3f8a87c88da0ccad54af1 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 8 Jul 2025 04:32:57 +0200 Subject: [PATCH 4/5] Add rich link previews for shared URLs - Capture both URL and title when sharing links - Create LinkPreviewView component with clickable cards - Support markdown-style links [title](url) - Auto-detect plain URLs in messages - Add link metadata fetching on iOS - Display clean preview cards with title, URL, and description --- bitchat/BitchatApp.swift | 17 +- bitchat/Views/ContentView.swift | 37 ++-- bitchat/Views/LinkPreviewView.swift | 163 ++++++++++++++++++ .../ShareViewController.swift | 24 ++- 4 files changed, 225 insertions(+), 16 deletions(-) create mode 100644 bitchat/Views/LinkPreviewView.swift diff --git a/bitchat/BitchatApp.swift b/bitchat/BitchatApp.swift index 4bfa649d..2e291d21 100644 --- a/bitchat/BitchatApp.swift +++ b/bitchat/BitchatApp.swift @@ -92,8 +92,21 @@ struct BitchatApp: App { // 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 contentType == "url" { + // Try to parse as JSON first + if let data = sharedContent.data(using: .utf8), + let urlData = try? JSONSerialization.jsonObject(with: data) as? [String: String], + let url = urlData["url"], + let title = urlData["title"] { + // Send formatted link with title and URL + self.chatViewModel.sendMessage("[\(title)](\(url))") + } else { + // Fallback to simple URL + self.chatViewModel.sendMessage("Shared link: \(sharedContent)") + } + } else { + self.chatViewModel.sendMessage(sharedContent) + } } } } diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 23e21ed8..6f95d07c 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -415,18 +415,33 @@ struct ContentView: View { .frame(maxWidth: .infinity, alignment: .leading) } else { // Regular messages with natural text wrapping - HStack(alignment: .top, spacing: 0) { - // Single text view for natural wrapping - Text(viewModel.formatMessageAsText(message, colorScheme: colorScheme)) - .textSelection(.enabled) - .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: .infinity, alignment: .leading) + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .top, spacing: 0) { + // Single text view for natural wrapping + Text(viewModel.formatMessageAsText(message, colorScheme: colorScheme)) + .textSelection(.enabled) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + + // Delivery status indicator for private messages + if message.isPrivate && message.sender == viewModel.nickname, + let status = message.deliveryStatus { + DeliveryStatusView(status: status, colorScheme: colorScheme) + .padding(.leading, 4) + } + } - // Delivery status indicator for private messages - if message.isPrivate && message.sender == viewModel.nickname, - let status = message.deliveryStatus { - DeliveryStatusView(status: status, colorScheme: colorScheme) - .padding(.leading, 4) + // Check for links and show preview + if let markdownLink = message.content.extractMarkdownLink() { + LinkPreviewView(url: markdownLink.url, title: markdownLink.title) + .padding(.top, 4) + } else { + // Check for plain URLs + let urls = message.content.extractURLs() + ForEach(urls.prefix(3), id: \.url) { urlInfo in + LinkPreviewView(url: urlInfo.url, title: nil) + .padding(.top, 4) + } } } } diff --git a/bitchat/Views/LinkPreviewView.swift b/bitchat/Views/LinkPreviewView.swift new file mode 100644 index 00000000..79b5c9aa --- /dev/null +++ b/bitchat/Views/LinkPreviewView.swift @@ -0,0 +1,163 @@ +// +// LinkPreviewView.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import SwiftUI +#if os(iOS) +import LinkPresentation +#endif + +struct LinkPreviewView: View { + let url: URL + let title: String? + @Environment(\.colorScheme) var colorScheme + @State private var isLoadingMetadata = false + @State private var metadata: LinkMetadata? + + private var textColor: Color { + colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0) + } + + private var backgroundColor: Color { + colorScheme == .dark ? Color.black : Color.white + } + + private var borderColor: Color { + textColor.opacity(0.3) + } + + var body: some View { + Button(action: { + #if os(iOS) + UIApplication.shared.open(url) + #else + NSWorkspace.shared.open(url) + #endif + }) { + VStack(alignment: .leading, spacing: 6) { + // Title + Text(metadata?.title ?? title ?? url.host ?? "Link") + .font(.system(size: 14, weight: .medium, design: .monospaced)) + .foregroundColor(textColor) + .lineLimit(2) + .multilineTextAlignment(.leading) + + // URL + Text(url.absoluteString) + .font(.system(size: 12, design: .monospaced)) + .foregroundColor(textColor.opacity(0.7)) + .lineLimit(1) + .truncationMode(.middle) + + // Description if available + if let description = metadata?.description { + Text(description) + .font(.system(size: 12, design: .monospaced)) + .foregroundColor(textColor.opacity(0.8)) + .lineLimit(2) + .multilineTextAlignment(.leading) + } + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(backgroundColor.opacity(0.05)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(borderColor, lineWidth: 1) + ) + } + .buttonStyle(.plain) + .onAppear { + loadMetadata() + } + } + + private func loadMetadata() { + #if os(iOS) + guard metadata == nil && !isLoadingMetadata else { return } + + isLoadingMetadata = true + let provider = LPMetadataProvider() + provider.startFetchingMetadata(for: url) { metadata, error in + DispatchQueue.main.async { + if let metadata = metadata { + self.metadata = LinkMetadata( + title: metadata.title, + description: metadata.value(forKey: "_summary") as? String, + imageURL: metadata.imageProvider?.value(forKey: "_URL") as? URL + ) + } + self.isLoadingMetadata = false + } + } + #endif + } +} + +struct LinkMetadata { + let title: String? + let description: String? + let imageURL: URL? +} + +// Helper to extract URLs from text +extension String { + func extractURLs() -> [(url: URL, range: Range)] { + var urls: [(URL, Range)] = [] + + // Check for markdown-style links [title](url) + let markdownPattern = #"\[([^\]]+)\]\(([^)]+)\)"# + if let regex = try? NSRegularExpression(pattern: markdownPattern) { + let matches = regex.matches(in: self, range: NSRange(location: 0, length: self.utf16.count)) + for match in matches { + if let urlRange = Range(match.range(at: 2), in: self), + let url = URL(string: String(self[urlRange])), + let fullRange = Range(match.range, in: self) { + urls.append((url, fullRange)) + } + } + } + + // Also check for plain URLs + let types: NSTextCheckingResult.CheckingType = .link + if let detector = try? NSDataDetector(types: types.rawValue) { + let matches = detector.matches(in: self, range: NSRange(location: 0, length: self.utf16.count)) + for match in matches { + if let range = Range(match.range, in: self), + let url = match.url { + // Don't add if this URL is already part of a markdown link + let isPartOfMarkdown = urls.contains { $0.range.overlaps(range) } + if !isPartOfMarkdown { + urls.append((url, range)) + } + } + } + } + + return urls + } + + func extractMarkdownLink() -> (title: String, url: URL)? { + let pattern = #"\[([^\]]+)\]\(([^)]+)\)"# + if let regex = try? NSRegularExpression(pattern: pattern), + let match = regex.firstMatch(in: self, range: NSRange(location: 0, length: self.utf16.count)) { + if let titleRange = Range(match.range(at: 1), in: self), + let urlRange = Range(match.range(at: 2), in: self), + let url = URL(string: String(self[urlRange])) { + return (String(self[titleRange]), url) + } + } + return nil + } +} + +#Preview { + VStack { + LinkPreviewView(url: URL(string: "https://example.com")!, title: "Example Website") + .padding() + } +} \ No newline at end of file diff --git a/bitchatShareExtension/ShareViewController.swift b/bitchatShareExtension/ShareViewController.swift index 27a65c84..941f0f86 100644 --- a/bitchatShareExtension/ShareViewController.swift +++ b/bitchatShareExtension/ShareViewController.swift @@ -111,9 +111,27 @@ class ShareViewController: SLComposeServiceViewController { } private func handleSharedURL(_ url: URL) { - // Convert URL to text and share - let text = url.absoluteString - saveToSharedDefaults(content: text, type: "url") + // Get the page title if available from the extension context + var pageTitle: String? = nil + if let item = extensionContext?.inputItems.first as? NSExtensionItem { + pageTitle = item.attributedContentText?.string ?? item.attributedTitle?.string + } + + // Create a structured format for URL sharing + let urlData: [String: String] = [ + "url": url.absoluteString, + "title": pageTitle ?? url.host ?? "Shared Link" + ] + + // Convert to JSON string + if let jsonData = try? JSONSerialization.data(withJSONObject: urlData), + let jsonString = String(data: jsonData, encoding: .utf8) { + saveToSharedDefaults(content: jsonString, type: "url") + } else { + // Fallback to simple URL + saveToSharedDefaults(content: url.absoluteString, type: "url") + } + openMainApp() } From d5ccf6bc0bbb0146766fd090021d0f50180358be Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 8 Jul 2025 17:47:53 +0200 Subject: [PATCH 5/5] Add iOS share extension with rich link previews - Implement share extension to receive URLs from other apps - Display shared links with emoji indicator and rich preview cards - Add custom compact link preview with image, title, and domain - Configure app groups for data sharing between extension and main app - Handle URL detection and parsing in share extension - Update message formatting to show only emoji for shared links - Add proper entitlements and activation rules for share extension --- bitchat.xcodeproj/project.pbxproj | 32 +-- bitchat/BitchatApp.swift | 20 +- bitchat/Info.plist | 9 + bitchat/ViewModels/ChatViewModel.swift | 30 ++- bitchat/Views/ContentView.swift | 9 +- bitchat/Views/LinkPreviewView.swift | 209 ++++++++++++++---- bitchatShareExtension/Info.plist | 8 +- .../ShareViewController.swift | 157 ++++++++----- project.yml | 13 ++ 9 files changed, 369 insertions(+), 118 deletions(-) diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index 72ab5d37..1306435d 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 63; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -15,6 +15,7 @@ 1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */; }; 24F17B1446E13F42652B7B08 /* PasswordProtectedChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */; }; 2E71E320EA921498C57E023B /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; }; + 31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; }; 4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */; }; 4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; }; 4E778E5A414571ACAC2A0F01 /* MessageRetentionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */; }; @@ -26,6 +27,7 @@ 749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; }; 7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; }; 7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; }; + 7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; }; 7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F149C43D1915831B60FE09 /* CompressionUtil.swift */; }; 7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; }; 846E2B446E36639159704730 /* BloomFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */; }; @@ -127,13 +129,14 @@ 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatMessageTests.swift; sourceTree = ""; }; 527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchat.entitlements; sourceTree = ""; }; 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = ""; }; - 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetentionService.swift; sourceTree = ""; }; 6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = ""; }; 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = ""; }; 7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePaddingTests.swift; sourceTree = ""; }; - 997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = ""; }; A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = ""; }; AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetryService.swift; sourceTree = ""; }; @@ -220,6 +223,7 @@ children = ( 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */, A08E03AA0C63E97C91749AEC /* ContentView.swift */, + 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */, ); path = Views; sourceTree = ""; @@ -366,12 +370,15 @@ LastUpgradeCheck = 1430; TargetAttributes = { 0576A29205865664C0937536 = { + DevelopmentTeam = L3N5LHJD5Y; ProvisioningStyle = Automatic; }; 57CA17A36A2532A6CFF367BB = { + DevelopmentTeam = L3N5LHJD5Y; ProvisioningStyle = Automatic; }; AF077EA0474EDEDE2C72716C = { + DevelopmentTeam = L3N5LHJD5Y; ProvisioningStyle = Automatic; }; }; @@ -386,6 +393,7 @@ ); mainGroup = 18198ED912AAF495D8AF7763; minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 54; projectDirPath = ""; projectRoot = ""; targets = ( @@ -442,6 +450,7 @@ 8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.swift in Sources */, 739429DFDE5C5829CF70DA7D /* EncryptionService.swift in Sources */, FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */, + 31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */, 4E778E5A414571ACAC2A0F01 /* MessageRetentionService.swift in Sources */, C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */, 749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */, @@ -465,6 +474,7 @@ CD0AE423F03AC52BAFC16834 /* DeliveryTracker.swift in Sources */, DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */, 8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */, + 7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */, 4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */, CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */, 61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */, @@ -623,12 +633,9 @@ PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -673,12 +680,9 @@ PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -692,6 +696,7 @@ CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = L3N5LHJD5Y; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -776,6 +781,7 @@ CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = L3N5LHJD5Y; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; diff --git a/bitchat/BitchatApp.swift b/bitchat/BitchatApp.swift index 2e291d21..10964f5d 100644 --- a/bitchat/BitchatApp.swift +++ b/bitchat/BitchatApp.swift @@ -58,19 +58,24 @@ struct BitchatApp: App { private func checkForSharedContent() { // Check app group for shared content from extension guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") else { - print("Failed to access app group UserDefaults") + print("DEBUG: Failed to access app group UserDefaults") return } guard let sharedContent = userDefaults.string(forKey: "sharedContent"), let sharedDate = userDefaults.object(forKey: "sharedContentDate") as? Date else { - print("No shared content found in UserDefaults") + print("DEBUG: No shared content found in UserDefaults") return } + print("DEBUG: Found shared content: \(sharedContent)") + print("DEBUG: Shared date: \(sharedDate)") + print("DEBUG: Time since shared: \(Date().timeIntervalSince(sharedDate)) seconds") + // Only process if shared within last 30 seconds if Date().timeIntervalSince(sharedDate) < 30 { let contentType = userDefaults.string(forKey: "sharedContentType") ?? "text" + print("DEBUG: Content type: \(contentType)") // Clear the shared content userDefaults.removeObject(forKey: "sharedContent") @@ -93,21 +98,28 @@ struct BitchatApp: App { // Send the shared content after a short delay DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { if contentType == "url" { + print("DEBUG: Processing URL content") // Try to parse as JSON first if let data = sharedContent.data(using: .utf8), let urlData = try? JSONSerialization.jsonObject(with: data) as? [String: String], let url = urlData["url"], let title = urlData["title"] { - // Send formatted link with title and URL - self.chatViewModel.sendMessage("[\(title)](\(url))") + // Send just emoji with hidden markdown link + let markdownLink = "👇 [\(title)](\(url))" + print("DEBUG: Sending markdown link: \(markdownLink)") + self.chatViewModel.sendMessage(markdownLink) } else { // Fallback to simple URL + print("DEBUG: Failed to parse JSON, sending as plain URL") self.chatViewModel.sendMessage("Shared link: \(sharedContent)") } } else { + print("DEBUG: Sending plain text: \(sharedContent)") self.chatViewModel.sendMessage(sharedContent) } } + } else { + print("DEBUG: Shared content is too old, ignoring") } } } diff --git a/bitchat/Info.plist b/bitchat/Info.plist index 6b60e722..a056533c 100644 --- a/bitchat/Info.plist +++ b/bitchat/Info.plist @@ -40,5 +40,14 @@ UIInterfaceOrientationPortrait + CFBundleURLTypes + + + CFBundleURLSchemes + + bitchat + + + diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 71f54b24..f9bdd262 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -1498,8 +1498,34 @@ class ChatViewModel: ObservableObject { senderStyle.font = .system(size: 14, weight: .medium, design: .monospaced) result.append(sender.mergingAttributes(senderStyle)) - // Process content with hashtags and mentions - let content = message.content + // Process content with hashtags, mentions, and markdown links + var content = message.content + + // First, check if content starts with 👇 followed by markdown link + if content.hasPrefix("👇 [") { + // This is a URL share - remove everything after the emoji + if let linkStart = content.firstIndex(of: "[") { + let indexBeforeLink = content.index(before: linkStart) + content = String(content[.. UIView { + let containerView = UIView() + containerView.backgroundColor = .clear + + let linkView = LPLinkView(metadata: metadata) + linkView.isUserInteractionEnabled = false // We handle taps at the SwiftUI level + linkView.translatesAutoresizingMaskIntoConstraints = false + + containerView.addSubview(linkView) + NSLayoutConstraint.activate([ + linkView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + linkView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + linkView.topAnchor.constraint(equalTo: containerView.topAnchor), + linkView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + ]) + + return containerView + } + + func updateUIView(_ uiView: UIView, context: Context) { + // Update if needed + } } +#endif // Helper to extract URLs from text extension String { @@ -130,7 +253,7 @@ extension String { if let range = Range(match.range, in: self), let url = match.url { // Don't add if this URL is already part of a markdown link - let isPartOfMarkdown = urls.contains { $0.range.overlaps(range) } + let isPartOfMarkdown = urls.contains { $0.1.overlaps(range) } if !isPartOfMarkdown { urls.append((url, range)) } @@ -142,15 +265,19 @@ extension String { } func extractMarkdownLink() -> (title: String, url: URL)? { + print("DEBUG: Checking for markdown link in: \(self)") let pattern = #"\[([^\]]+)\]\(([^)]+)\)"# if let regex = try? NSRegularExpression(pattern: pattern), let match = regex.firstMatch(in: self, range: NSRange(location: 0, length: self.utf16.count)) { if let titleRange = Range(match.range(at: 1), in: self), let urlRange = Range(match.range(at: 2), in: self), let url = URL(string: String(self[urlRange])) { - return (String(self[titleRange]), url) + let result = (String(self[titleRange]), url) + print("DEBUG: Found markdown link - title: \(result.0), url: \(result.1)") + return result } } + print("DEBUG: No markdown link found") return nil } } diff --git a/bitchatShareExtension/Info.plist b/bitchatShareExtension/Info.plist index 4f43a179..caa4c793 100644 --- a/bitchatShareExtension/Info.plist +++ b/bitchatShareExtension/Info.plist @@ -26,18 +26,18 @@ NSExtensionActivationRule + NSExtensionActivationSupportsImageWithMaxCount + 1 NSExtensionActivationSupportsText NSExtensionActivationSupportsWebURLWithMaxCount 1 - NSExtensionActivationSupportsImageWithMaxCount - 1 - NSExtensionPrincipalClass - $(PRODUCT_MODULE_NAME).ShareViewController NSExtensionPointIdentifier com.apple.share-services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareViewController diff --git a/bitchatShareExtension/ShareViewController.swift b/bitchatShareExtension/ShareViewController.swift index 941f0f86..17175263 100644 --- a/bitchatShareExtension/ShareViewController.swift +++ b/bitchatShareExtension/ShareViewController.swift @@ -35,65 +35,110 @@ class ShareViewController: SLComposeServiceViewController { } override func didSelectPost() { - // If we have content text from the compose view, handle it directly - if let text = contentText, !text.isEmpty { - handleSharedText(text) - // Complete the share action after saving - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) - } - return - } - - // Otherwise, process attachments guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else { self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) return } - var hasProcessedContent = false + print("ShareExtension: Processing share with \(extensionItem.attachments?.count ?? 0) attachments") + + // Get the page title from the compose view or extension item + let pageTitle = self.contentText ?? extensionItem.attributedContentText?.string ?? extensionItem.attributedTitle?.string + print("ShareExtension: Page title: \(pageTitle ?? "none")") + + var foundURL: URL? = nil let group = DispatchGroup() - // Process different types of shared content - for itemProvider in extensionItem.attachments ?? [] { - if itemProvider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { - group.enter() - itemProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { [weak self] (item, error) in - if let text = item as? String { - self?.handleSharedText(text) - hasProcessedContent = true - } - group.leave() - } - } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { - group.enter() - itemProvider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] (item, error) in - if let url = item as? URL { - self?.handleSharedURL(url) - hasProcessedContent = true - } - group.leave() - } - } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { - group.enter() - itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (item, error) in - if let image = item as? UIImage { - self?.handleSharedImage(image) - hasProcessedContent = true - } else if let data = item as? Data { - if let image = UIImage(data: data) { - self?.handleSharedImage(image) - hasProcessedContent = true - } - } - group.leave() - } + // IMPORTANT: Check if the NSExtensionItem itself has a URL + // Safari often provides the URL as an attributedString with a link + if let attributedText = extensionItem.attributedContentText { + print("ShareExtension: Checking attributed text for URLs") + let text = attributedText.string + let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + let matches = detector?.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) + if let firstMatch = matches?.first, let url = firstMatch.url { + print("ShareExtension: Found URL in attributed text: \(url.absoluteString)") + foundURL = url } } - // Complete after all items are processed - group.notify(queue: .main) { - self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + // Only check attachments if we haven't found a URL yet + if foundURL == nil { + for (index, itemProvider) in (extensionItem.attachments ?? []).enumerated() { + print("ShareExtension: Attachment \(index) types: \(itemProvider.registeredTypeIdentifiers)") + + // Try multiple URL type identifiers that Safari might use + let urlTypes = [ + UTType.url.identifier, + "public.url", + "public.file-url" + ] + + for urlType in urlTypes { + if itemProvider.hasItemConformingToTypeIdentifier(urlType) { + group.enter() + itemProvider.loadItem(forTypeIdentifier: urlType, options: nil) { (item, error) in + defer { group.leave() } + + if let url = item as? URL { + print("ShareExtension: Found URL: \(url.absoluteString)") + foundURL = url + } else if let data = item as? Data, + let urlString = String(data: data, encoding: .utf8), + let url = URL(string: urlString) { + print("ShareExtension: Found URL from data: \(url.absoluteString)") + foundURL = url + } else if let string = item as? String, + let url = URL(string: string) { + print("ShareExtension: Found URL from string: \(url.absoluteString)") + foundURL = url + } + } + break // Found a URL type, no need to check other types + } + } + + // Also check for plain text that might be a URL + if foundURL == nil && itemProvider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { + group.enter() + itemProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { (item, error) in + defer { group.leave() } + + if let text = item as? String { + // Check if the text is actually a URL + if let url = URL(string: text), + (url.scheme == "http" || url.scheme == "https") { + print("ShareExtension: Found URL in plain text: \(url.absoluteString)") + foundURL = url + } + } + } + } + } + } // End of if foundURL == nil + + // Process after all checks complete + group.notify(queue: .main) { [weak self] in + if let url = foundURL { + // We have a URL! Create the JSON data + let urlData: [String: String] = [ + "url": url.absoluteString, + "title": pageTitle ?? url.host ?? "Shared Link" + ] + + print("ShareExtension: Saving URL share - url: \(url.absoluteString), title: \(urlData["title"] ?? "")") + + if let jsonData = try? JSONSerialization.data(withJSONObject: urlData), + let jsonString = String(data: jsonData, encoding: .utf8) { + self?.saveToSharedDefaults(content: jsonString, type: "url") + } + } else if let title = pageTitle, !title.isEmpty { + // No URL found, just share the text + print("ShareExtension: No URL found, sharing as text: \(title)") + self?.saveToSharedDefaults(content: title, type: "text") + } + + self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) } } @@ -155,11 +200,19 @@ class ShareViewController: SLComposeServiceViewController { userDefaults.synchronize() print("ShareExtension: Saved content of type \(type) to shared defaults") + print("ShareExtension: Content: \(content)") + + // Force open the main app + self.openMainApp() } 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 + // Share extensions cannot directly open the containing app + // The app will check for shared content when it becomes active + // Show success feedback to user + DispatchQueue.main.async { + self.textView.text = "✓ Shared to bitchat" + self.textView.isEditable = false + } } } \ No newline at end of file diff --git a/project.yml b/project.yml index ba00597c..ca6d40df 100644 --- a/project.yml +++ b/project.yml @@ -34,6 +34,9 @@ targets: UIColorName: Black UISupportedInterfaceOrientations: - UIInterfaceOrientationPortrait + CFBundleURLTypes: + - CFBundleURLSchemes: + - bitchat settings: PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat INFOPLIST_FILE: bitchat/Info.plist @@ -48,6 +51,7 @@ targets: ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES CODE_SIGN_ENTITLEMENTS: bitchat/bitchat.entitlements + DEVELOPMENT_TEAM: L3N5LHJD5Y dependencies: - target: bitchatShareExtension platformFilter: ios @@ -64,6 +68,14 @@ targets: CFBundleDisplayName: bitchat CFBundleShortVersionString: $(MARKETING_VERSION) CFBundleVersion: $(CURRENT_PROJECT_VERSION) + NSExtension: + NSExtensionPointIdentifier: com.apple.share-services + NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareViewController + NSExtensionAttributes: + NSExtensionActivationRule: + NSExtensionActivationSupportsText: true + NSExtensionActivationSupportsWebURLWithMaxCount: 1 + NSExtensionActivationSupportsImageWithMaxCount: 1 settings: PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat.ShareExtension INFOPLIST_FILE: bitchatShareExtension/Info.plist @@ -73,6 +85,7 @@ targets: CODE_SIGNING_REQUIRED: YES CODE_SIGNING_ALLOWED: YES CODE_SIGN_ENTITLEMENTS: bitchatShareExtension/bitchatShareExtension.entitlements + DEVELOPMENT_TEAM: L3N5LHJD5Y bitchatTests_iOS: type: bundle.unit-test