From d5ccf6bc0bbb0146766fd090021d0f50180358be Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 8 Jul 2025 17:47:53 +0200 Subject: [PATCH] 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