From 60b0deee7ba835b76947951717b77ae58c363930 Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Mon, 25 Aug 2025 18:01:19 +0200 Subject: [PATCH] =?UTF-8?q?Cleanup:=20remove=20dead=20code,=20normalize=20?= =?UTF-8?q?fingerprints,=20modernize=20share=20extension,=20trim=20test=20?= =?UTF-8?q?noise,=20and=20drop=20=E2=80=98preparing=20to=20share=E2=80=A6?= =?UTF-8?q?=E2=80=99=20message=20(#520)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove dead code and artifacts: drop PeerManager, unused views/types; delete LegacyTestProtocolTypes; update .gitignore; purge TestResult.xcresult and build.log * Tests: gate verbose prints under DEBUG; ChatViewModel: remove legacy fingerprint helper and rely on UnifiedPeerService * Share Extension: migrate to UIKit + UTTypes; drop Social/SLComposeServiceViewController * Remove 'preparing to share …' system message; send shared content immediately * Inline comment cleanup: drop legacy 'removed' breadcrumbs across protocols, services, view model, and views --------- Co-authored-by: jack --- .gitignore | 4 + bitchat/BitchatApp.swift | 14 +- bitchat/Identity/IdentityModels.swift | 53 +--- bitchat/Models/BitchatPeer.swift | 217 +-------------- bitchat/Nostr/NostrProtocol.swift | 2 +- bitchat/Protocols/BitchatProtocol.swift | 8 +- bitchat/Services/BLEService.swift | 2 +- bitchat/Services/CommandProcessor.swift | 2 +- bitchat/ViewModels/ChatViewModel.swift | 36 +-- bitchat/Views/ContentView.swift | 32 +-- .../ShareViewController.swift | 260 +++++++++--------- bitchatTests/NostrProtocolTests.swift | 10 + .../LegacyTestProtocolTypes.swift | 45 --- build.log | 13 - 14 files changed, 166 insertions(+), 532 deletions(-) delete mode 100644 bitchatTests/TestUtilities/LegacyTestProtocolTypes.swift delete mode 100644 build.log diff --git a/.gitignore b/.gitignore index 37547a85..e0538486 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,7 @@ __pycache__/ # Local build results .Result*/ .Result*.xcresult/ +TestResult.xcresult/ +*.xcresult/ +build.log +*.log diff --git a/bitchat/BitchatApp.swift b/bitchat/BitchatApp.swift index cf156194..6650ee40 100644 --- a/bitchat/BitchatApp.swift +++ b/bitchat/BitchatApp.swift @@ -109,20 +109,8 @@ struct BitchatApp: App { userDefaults.removeObject(forKey: "sharedContentDate") userDefaults.synchronize() - // Show notification about shared content + // Send the shared content immediately on the main queue 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) { if contentType == "url" { // Try to parse as JSON first if let data = sharedContent.data(using: .utf8), diff --git a/bitchat/Identity/IdentityModels.swift b/bitchat/Identity/IdentityModels.swift index 51a58c6c..00eff84e 100644 --- a/bitchat/Identity/IdentityModels.swift +++ b/bitchat/Identity/IdentityModels.swift @@ -173,56 +173,7 @@ struct PendingActions { var setPetname: String? } -// MARK: - Privacy Settings - -struct PrivacySettings: Codable { - // Level 1: Maximum privacy (default) - var persistIdentityCache = false - var showLastSeen = false - - // Level 2: Convenience - var autoAcceptKnownFingerprints = false - var rememberNicknameHistory = false - - // Level 3: Social - var shareTrustNetworkHints = false // "3 mutual contacts trust this person" -} - -// MARK: - Conflict Resolution - -/// Strategies for resolving identity conflicts in the decentralized network. -/// Handles cases where multiple peers claim the same nickname or when -/// identity mappings become ambiguous due to network partitions. -enum ConflictResolution { - case acceptNew(petname: String) // "John (2)" - case rejectNew - case blockFingerprint(String) - case alertUser(message: String) -} - -// MARK: - UI State - -struct PeerUIState { - let peerID: String - let nickname: String - var identityState: IdentityState - var connectionQuality: ConnectionQuality - - enum IdentityState { - case unknown // Gray - No identity info - case unverifiedKnown(String) // Blue - Handshake done, matches cache - case verified(String) // Green - Cryptographically verified - case conflict(String, String) // Red - Nickname doesn't match fingerprint - case pending // Yellow - Handshake in progress - } -} - -enum ConnectionQuality { - case excellent - case good - case poor - case disconnected -} +// // MARK: - Migration Support -// Removed LegacyFavorite - no longer needed +// diff --git a/bitchat/Models/BitchatPeer.swift b/bitchat/Models/BitchatPeer.swift index beba8b2e..e33ce464 100644 --- a/bitchat/Models/BitchatPeer.swift +++ b/bitchat/Models/BitchatPeer.swift @@ -89,219 +89,4 @@ struct BitchatPeer: Identifiable, Equatable { } } -// MARK: - Peer Manager - -/// Manages the collection of peers and their states -@MainActor -class PeerManager: ObservableObject { - @Published var peers: [BitchatPeer] = [] - @Published var favorites: [BitchatPeer] = [] - @Published var mutualFavorites: [BitchatPeer] = [] - - private let meshService: Transport - private let favoritesService = FavoritesPersistenceService.shared - - init(meshService: Transport) { - self.meshService = meshService - updatePeers() - - // Listen for updates - NotificationCenter.default.addObserver( - self, - selector: #selector(handleFavoriteChanged), - name: .favoriteStatusChanged, - object: nil - ) - } - - @objc private func handleFavoriteChanged() { - SecureLogger.log("⭐ Favorite status changed notification received, updating peers", - category: SecureLogger.session, level: .debug) - updatePeers() - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - func updatePeers() { - // Reduce log verbosity - only log when count changes - let previousCount = peers.count - - // Get current mesh peers - let meshPeers = meshService.getPeerNicknames() - - // Build peer list - var allPeers: [BitchatPeer] = [] - var connectedNicknames: Set = [] - var addedPeerIDs: Set = [] - - // Add connected mesh peers (only if actually connected or relay connected) - for (peerID, nickname) in meshPeers { - guard let noiseKey = Data(hexString: peerID) else { continue } - - // Safety check: Never add our own peer ID - if peerID == meshService.myPeerID { - continue - } - - // Check if this peer is actually connected - let isConnected = meshService.isPeerConnected(peerID) - - // Skip disconnected peers unless they're favorites (handled later) - if !isConnected { - continue - } - - if isConnected { - connectedNicknames.insert(nickname) - } - - // Track that we've added this peer ID - addedPeerIDs.insert(peerID) - - var peer = BitchatPeer( - id: peerID, - noisePublicKey: noiseKey, - nickname: nickname, - isConnected: isConnected - ) - // Set favorite status - check both by current noise key and by nickname - if let favoriteStatus = favoritesService.getFavoriteStatus(for: noiseKey) { - peer.favoriteStatus = favoriteStatus - peer.nostrPublicKey = favoriteStatus.peerNostrPublicKey - } else { - // Check if we have a favorite for this nickname (peer may have reconnected with new ID) - let favoriteByNickname = favoritesService.favorites.values.first { $0.peerNickname == nickname } - if let favorite = favoriteByNickname { - SecureLogger.log("🔄 Found favorite for '\(nickname)' by nickname, updating noise key", - category: SecureLogger.session, level: .info) - // Update the favorite's noise key to match the current connection - favoritesService.updateNoisePublicKey(from: favorite.peerNoisePublicKey, to: noiseKey, peerNickname: nickname) - // Get the updated favorite with the new key - peer.favoriteStatus = favoritesService.getFavoriteStatus(for: noiseKey) - peer.nostrPublicKey = peer.favoriteStatus?.peerNostrPublicKey ?? favorite.peerNostrPublicKey - } - } - allPeers.append(peer) - } - - // Add offline favorites (only those not currently connected AND that we actively favorite) - - for (favoriteKey, favorite) in favoritesService.favorites { - let favoriteID = favorite.peerNoisePublicKey.hexEncodedString() - - // Skip if this peer is already connected (by nickname) - if connectedNicknames.contains(favorite.peerNickname) { - // Skipping favorite - already connected - continue - } - - // Skip if we already added a peer with this ID (prevents duplicates) - if addedPeerIDs.contains(favoriteID) { - // Skipping favorite - peer ID already added - continue - } - - // Only add peers that WE favorite (not just ones who favorite us) - if !favorite.isFavorite { - // Skipping - we don't favorite them - continue - } - - // Add this favorite as an offline peer - SecureLogger.log(" - Adding offline favorite '\(favorite.peerNickname)' (key: \(favoriteKey.hexEncodedString()), ID: \(favoriteID), mutual: \(favorite.isMutual))", - category: SecureLogger.session, level: .info) - - var peer = BitchatPeer( - id: favoriteID, - noisePublicKey: favorite.peerNoisePublicKey, - nickname: favorite.peerNickname, - isConnected: false - ) - // Set favorite status - peer.favoriteStatus = favorite - peer.nostrPublicKey = favorite.peerNostrPublicKey - addedPeerIDs.insert(favoriteID) // Track that we've added this ID - allPeers.append(peer) - } - - // Filter out "Unknown" peers unless they are favorites or have a favorite relationship - allPeers = allPeers.filter { peer in - !(peer.displayName == "Unknown" && peer.favoriteStatus == nil) - } - - // Sort: Connected first, then favorites, then alphabetical - allPeers.sort { lhs, rhs in - // Direct connections first - if lhs.isConnected != rhs.isConnected { - return lhs.isConnected - } - // Then favorites - if lhs.isFavorite != rhs.isFavorite { - return lhs.isFavorite - } - // Finally alphabetical - return lhs.displayName < rhs.displayName - } - - // Single pass to compute all subsets and counts - var favorites: [BitchatPeer] = [] - var mutualFavorites: [BitchatPeer] = [] - var connectedCount = 0 - var offlineCount = 0 - - for peer in allPeers { - if peer.isFavorite { - favorites.append(peer) - } - if peer.isMutualFavorite { - mutualFavorites.append(peer) - } - if peer.isConnected { - connectedCount += 1 - } else { - offlineCount += 1 - } - } - - // Final safety check: ensure no duplicate IDs - var finalPeers: [BitchatPeer] = [] - var seenIDs: Set = [] - for peer in allPeers { - if !seenIDs.contains(peer.id) { - seenIDs.insert(peer.id) - finalPeers.append(peer) - } else { - SecureLogger.log("⚠️ Removing duplicate peer ID in final check: \(peer.id) (\(peer.displayName))", - category: SecureLogger.session, level: .warning) - } - } - - self.peers = finalPeers - self.favorites = favorites - self.mutualFavorites = mutualFavorites - - // Log peer list summary sparingly at debug level - if favoritesService.favorites.count > 0 { - SecureLogger.log("📊 Peer list update: \(allPeers.count) total (\(connectedCount) connected, \(offlineCount) offline), \(favorites.count) favorites, \(mutualFavorites.count) mutual", - category: SecureLogger.session, level: .debug) - } else if previousCount != allPeers.count { - SecureLogger.log("✅ Updated peer list: \(allPeers.count) total peers", - category: SecureLogger.session, level: .debug) - } - } - - func toggleFavorite(_ peer: BitchatPeer) { - if peer.isFavorite { - favoritesService.removeFavorite(peerNoisePublicKey: peer.noisePublicKey) - } else { - favoritesService.addFavorite( - peerNoisePublicKey: peer.noisePublicKey, - peerNostrPublicKey: peer.nostrPublicKey, - peerNickname: peer.nickname - ) - } - updatePeers() - } -} +// diff --git a/bitchat/Nostr/NostrProtocol.swift b/bitchat/Nostr/NostrProtocol.swift index 5284dd70..46673e21 100644 --- a/bitchat/Nostr/NostrProtocol.swift +++ b/bitchat/Nostr/NostrProtocol.swift @@ -416,7 +416,7 @@ struct NostrProtocol { // Log with explicit UTC and local time for debugging let formatter = DateFormatter() - // Removed unnecessary date formatting operations + // formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" formatter.timeZone = TimeZone(abbreviation: "UTC") diff --git a/bitchat/Protocols/BitchatProtocol.swift b/bitchat/Protocols/BitchatProtocol.swift index 9b3838ac..758b0a61 100644 --- a/bitchat/Protocols/BitchatProtocol.swift +++ b/bitchat/Protocols/BitchatProtocol.swift @@ -181,8 +181,7 @@ enum LazyHandshakeState { case failed(Error) // Handshake failed } -// MARK: - Special Recipients (removed) -// Previously defined broadcast identifiers were unused; removed for simplicity. +// // MARK: - Core Protocol Structures @@ -268,8 +267,7 @@ struct BitchatPacket: Codable { } } -// MARK: - Delivery Acknowledgments (removed) -// Legacy DeliveryAck structures are no longer used; delivery status flows via Noise payloads. +// // MARK: - Read Receipts @@ -361,7 +359,7 @@ struct ReadReceipt: Codable { } -// PeerIdentityBinding removed (unused). +// // MARK: - Delivery Status diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index 5f58cc55..0110bf08 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -730,7 +730,7 @@ final class BLEService: NSObject { } } - // Removed unused getPeers(): use getPeerNicknames() from Transport + // // MARK: - Private Message Handling diff --git a/bitchat/Services/CommandProcessor.swift b/bitchat/Services/CommandProcessor.swift index 662a4fd4..7a7abcce 100644 --- a/bitchat/Services/CommandProcessor.swift +++ b/bitchat/Services/CommandProcessor.swift @@ -63,7 +63,7 @@ class CommandProcessor { case "/unfav": if inGeoPublic || inGeoDM { return .error(message: "favorites are only for mesh peers in #mesh") } return handleFavorite(args, add: false) - // /help removed + // case "/help", "/h": return .error(message: "unknown command: \(cmd)") default: diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 260d5308..20dee000 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -297,7 +297,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { } } - // Missing properties that were removed during refactoring + // private var peerIDToPublicKeyFingerprint: [String: String] = [:] private var selectedPrivateChatFingerprint: String? = nil // Map stable short peer IDs (16-hex) to full Noise public key hex (64-hex) for session continuity @@ -1561,7 +1561,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { } catch { // ignore } - // Presence announcement removed; we will tag actual chat events instead + // } // MARK: - Geohash Participants @@ -1714,7 +1714,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { let toAdd = desired.subtracting(current) let toRemove = current.subtracting(desired) - // Unsubscribe removed + // for (subID, gh) in geoSamplingSubs where toRemove.contains(gh) { NostrRelayManager.shared.unsubscribe(id: subID) geoSamplingSubs.removeValue(forKey: subID) @@ -3617,26 +3617,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate { return unifiedPeerService.getFingerprint(for: peerID) } - private func getFingerprint_old(for peerID: String) -> String? { - // Remove debug logging to prevent console spam during view updates - - // First try to get fingerprint from mesh service's peer ID rotation mapping - if let fingerprint = meshService.getFingerprint(for: peerID) { - return fingerprint - } - - // Check noise service (direct Noise session fingerprint) - if let fingerprint = meshService.getNoiseService().getPeerFingerprint(peerID) { - return fingerprint - } - - // Last resort: check local mapping - if let fingerprint = peerIDToPublicKeyFingerprint[peerID] { - return fingerprint - } - - return nil - } + // + // Helper to resolve nickname for a peer ID through various sources @MainActor @@ -4034,7 +4016,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { messageRouter.flushOutbox(for: peerID) } - // Connection messages removed to reduce chat noise + // } func didDisconnectFromPeer(_ peerID: String) { @@ -4101,7 +4083,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { } } - // Disconnection messages removed to reduce chat noise + // } func didUpdatePeerList(_ peers: [String]) { @@ -4414,7 +4396,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { // MARK: - Simplified Nostr Integration (Inlined from MessageRouter) - // Removed inlined Nostr send helpers in favor of MessageRouter + // @MainActor private func setupNostrMessageHandling() { @@ -4745,7 +4727,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { return Data(base64Encoded: str) } - // Removed local TLV decoder; using PrivateMessagePacket.decode from Protocols + // @MainActor private func handleFavoriteNotificationFromMesh(_ content: String, from peerID: String, senderNickname: String) { diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 5f16f016..e630be50 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -13,19 +13,9 @@ import UIKit // MARK: - Supporting Types -// Pre-computed peer data for performance -struct PeerDisplayData: Identifiable { - let id: String - let displayName: String - let isFavorite: Bool - let isMe: Bool - let hasUnreadMessages: Bool - let encryptionStatus: EncryptionStatus - let connectionState: BitchatPeer.ConnectionState - let isMutualFavorite: Bool -} +// -// (Link previews removed; URLs are now clickable inline) +// // MARK: - Main Content View @@ -1382,23 +1372,7 @@ private struct PaymentChipView: View { } } -// Helper view for rendering message content (plain, no hashtag/mention formatting) -struct MessageContentView: View { - let message: BitchatMessage - let viewModel: ChatViewModel - let colorScheme: ColorScheme - let isMentioned: Bool - - var body: some View { - Text(message.content) - .font(.system(size: 14, design: .monospaced)) - .fontWeight(isMentioned ? .bold : .regular) - } - - // MARK: - Helper Methods - - // buildTextSegments removed: content is rendered plain. -} +// // Delivery status indicator view struct DeliveryStatusView: View { diff --git a/bitchatShareExtension/ShareViewController.swift b/bitchatShareExtension/ShareViewController.swift index 65668cd6..e26b0dc5 100644 --- a/bitchatShareExtension/ShareViewController.swift +++ b/bitchatShareExtension/ShareViewController.swift @@ -7,160 +7,160 @@ // import UIKit -import Social import UniformTypeIdentifiers -class ShareViewController: SLComposeServiceViewController { - +/// Modern share extension using UIKit + UTTypes. +/// Avoids deprecated Social framework and SLComposeServiceViewController. +final class ShareViewController: UIViewController { + private let statusLabel: UILabel = { + let l = UILabel() + l.translatesAutoresizingMaskIntoConstraints = false + l.font = .systemFont(ofSize: 15, weight: .semibold) + l.textAlignment = .center + l.numberOfLines = 0 + l.textColor = .label + return l + }() + override func viewDidLoad() { super.viewDidLoad() - // Set placeholder text - placeholder = "Share to bitchat..." - // Set character limit (optional) - charactersRemaining = 500 + view.backgroundColor = .systemBackground + view.addSubview(statusLabel) + NSLayoutConstraint.activate([ + statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + statusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), + statusLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.layoutMarginsGuide.leadingAnchor), + statusLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.layoutMarginsGuide.trailingAnchor) + ]) + + processShare() } - - 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() { - guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else { - self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + + // MARK: - Processing + private func processShare() { + guard let ctx = self.extensionContext, + let item = ctx.inputItems.first as? NSExtensionItem else { + finishWithMessage("Nothing to share") return } - - - // Get the page title from the compose view or extension item - let pageTitle = self.contentText ?? extensionItem.attributedContentText?.string ?? extensionItem.attributedTitle?.string - - var foundURL: URL? = nil - let group = DispatchGroup() - - // 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 { - 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 { - foundURL = url - } + + // Try content from attributed text first (Safari often passes URL here) + if let url = detectURL(in: item.attributedContentText?.string ?? "") { + saveAndFinish(url: url, title: item.attributedTitle?.string) + return } - - // Only check attachments if we haven't found a URL yet - if foundURL == nil { - for (_, itemProvider) in (extensionItem.attachments ?? []).enumerated() { - - // 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 { - foundURL = url - } else if let data = item as? Data, - let urlString = String(data: data, encoding: .utf8), - let url = URL(string: urlString) { - foundURL = url - } else if let string = item as? String, - let url = URL(string: string) { - foundURL = url - } - } - break // Found a URL type, no need to check other types - } + + // Scan attachments for URL/text + let providers = item.attachments ?? [] + if providers.isEmpty { + // Fallback: use attributed title as plain text + if let title = item.attributedTitle?.string, !title.isEmpty { + saveAndFinish(text: title) + } else { + finishWithMessage("No shareable content") } - - // 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") { - foundURL = url + return + } + + // Load URL or text asynchronously + loadFirstURL(from: providers) { [weak self] url in + guard let self = self else { return } + if let url = url { + self.saveAndFinish(url: url, title: item.attributedTitle?.string) + } else { + self.loadFirstPlainText(from: providers) { text in + if let t = text, !t.isEmpty { + // Treat as URL if parseable http(s), else plain text + if let u = URL(string: t), ["http","https"].contains(u.scheme?.lowercased() ?? "") { + self.saveAndFinish(url: u, title: item.attributedTitle?.string) + } else { + self.saveAndFinish(text: t) } + } else { + self.finishWithMessage("No shareable content") } } } } - } // 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" - ] - - - if let jsonData = try? JSONSerialization.data(withJSONObject: urlData), - let jsonString = String(data: jsonData, encoding: .utf8) { - self?.saveToSharedDefaults(content: jsonString, type: "url") + } + + private func detectURL(in text: String) -> URL? { + guard !text.isEmpty else { return nil } + let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + let range = NSRange(location: 0, length: (text as NSString).length) + let match = detector?.matches(in: text, options: [], range: range).first + return match?.url + } + + private func loadFirstURL(from providers: [NSItemProvider], completion: @escaping (URL?) -> Void) { + let identifiers = [UTType.url.identifier, "public.url", "public.file-url"] + let grp = DispatchGroup() + var found: URL? + + for p in providers where found == nil { + for id in identifiers where p.hasItemConformingToTypeIdentifier(id) { + grp.enter() + p.loadItem(forTypeIdentifier: id, options: nil) { item, _ in + defer { grp.leave() } + if let u = item as? URL { found = u; return } + if let s = item as? String, let u = URL(string: s) { found = u; return } + if let d = item as? Data, let s = String(data: d, encoding: .utf8), let u = URL(string: s) { found = u; return } } - } else if let title = pageTitle, !title.isEmpty { - // No URL found, just share the text - self?.saveToSharedDefaults(content: title, type: "text") + break } - - self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + } + grp.notify(queue: .main) { completion(found) } + } + + private func loadFirstPlainText(from providers: [NSItemProvider], completion: @escaping (String?) -> Void) { + let id = UTType.plainText.identifier + let grp = DispatchGroup() + var text: String? + for p in providers where p.hasItemConformingToTypeIdentifier(id) { + grp.enter() + p.loadItem(forTypeIdentifier: id, options: nil) { item, _ in + defer { grp.leave() } + if let s = item as? String { text = s } + else if let d = item as? Data, let s = String(data: d, encoding: .utf8) { text = s } + } + break + } + grp.notify(queue: .main) { completion(text) } + } + + // MARK: - Save + Finish + private func saveAndFinish(url: URL, title: String?) { + let payload: [String: String] = [ + "url": url.absoluteString, + "title": title ?? url.host ?? "Shared Link" + ] + if let json = try? JSONSerialization.data(withJSONObject: payload), + let s = String(data: json, encoding: .utf8) { + saveToSharedDefaults(content: s, type: "url") + finishWithMessage("✓ Shared link to bitchat") + } else { + finishWithMessage("Failed to encode link") } } - - override func configurationItems() -> [Any]! { - // No configuration items needed - return [] + + private func saveAndFinish(text: String) { + saveToSharedDefaults(content: text, type: "text") + finishWithMessage("✓ Shared text to bitchat") } - - // MARK: - Helper Methods - + private func saveToSharedDefaults(content: String, type: String) { - // Use app groups to share data between extension and main app - guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") else { - return - } - + guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") else { return } userDefaults.set(content, forKey: "sharedContent") userDefaults.set(type, forKey: "sharedContentType") userDefaults.set(Date(), forKey: "sharedContentDate") userDefaults.synchronize() - - - // Force open the main app - self.openMainApp() } - - private func openMainApp() { - // 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 + + private func finishWithMessage(_ msg: String) { + statusLabel.text = msg + // Complete shortly after showing status + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) } } -} \ No newline at end of file +} diff --git a/bitchatTests/NostrProtocolTests.swift b/bitchatTests/NostrProtocolTests.swift index ea49d96a..0cc92306 100644 --- a/bitchatTests/NostrProtocolTests.swift +++ b/bitchatTests/NostrProtocolTests.swift @@ -21,8 +21,10 @@ final class NostrProtocolTests: XCTestCase { let sender = try NostrIdentity.generate() let recipient = try NostrIdentity.generate() + #if DEBUG print("Sender pubkey: \(sender.publicKeyHex)") print("Recipient pubkey: \(recipient.publicKeyHex)") + #endif // Create a test message let originalContent = "Hello from NIP-17 test!" @@ -34,8 +36,10 @@ final class NostrProtocolTests: XCTestCase { senderIdentity: sender ) + #if DEBUG print("Gift wrap created with ID: \(giftWrap.id)") print("Gift wrap pubkey: \(giftWrap.pubkey)") + #endif // Decrypt the gift wrap let (decryptedContent, senderPubkey, timestamp) = try NostrProtocol.decryptPrivateMessage( @@ -52,7 +56,9 @@ final class NostrProtocolTests: XCTestCase { let timeDiff = abs(messageDate.timeIntervalSinceNow) XCTAssertLessThan(timeDiff, 60, "Message timestamp should be recent") + #if DEBUG print("✅ Successfully decrypted message: '\(decryptedContent)' from \(senderPubkey) at \(messageDate)") + #endif } func testGiftWrapUsesUniqueEphemeralKeys() throws { @@ -75,8 +81,10 @@ final class NostrProtocolTests: XCTestCase { // Gift wrap pubkeys should be different (unique ephemeral keys) XCTAssertNotEqual(message1.pubkey, message2.pubkey) + #if DEBUG print("Message 1 gift wrap pubkey: \(message1.pubkey)") print("Message 2 gift wrap pubkey: \(message2.pubkey)") + #endif // Both should decrypt successfully let (content1, _, _) = try NostrProtocol.decryptPrivateMessage( @@ -109,7 +117,9 @@ final class NostrProtocolTests: XCTestCase { giftWrap: giftWrap, recipientIdentity: wrongRecipient )) { error in + #if DEBUG print("Expected error when decrypting with wrong key: \(error)") + #endif } } diff --git a/bitchatTests/TestUtilities/LegacyTestProtocolTypes.swift b/bitchatTests/TestUtilities/LegacyTestProtocolTypes.swift deleted file mode 100644 index 118df3ce..00000000 --- a/bitchatTests/TestUtilities/LegacyTestProtocolTypes.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// LegacyTestProtocolTypes.swift -// bitchatTests -// -// Minimal legacy protocol types used only by tests to simulate old flows. -// These are not part of production code anymore. - -import Foundation - -struct ProtocolNack { - let originalPacketID: String - let nackID: String - let senderID: String - let receiverID: String - let packetType: UInt8 - let reason: String - let errorCode: UInt8 - - enum ErrorCode: UInt8 { - case unknown = 0 - case decryptionFailed = 2 - } - - init(originalPacketID: String, senderID: String, receiverID: String, packetType: UInt8, reason: String, errorCode: ErrorCode = .unknown) { - self.originalPacketID = originalPacketID - self.nackID = UUID().uuidString - self.senderID = senderID - self.receiverID = receiverID - self.packetType = packetType - self.reason = reason - self.errorCode = errorCode.rawValue - } - - func toBinaryData() -> Data { - // Tests don't parse the payload; return a compact encoding for completeness - var data = Data() - data.appendUUID(originalPacketID) - data.appendUUID(nackID) - data.append(UInt8(packetType)) - data.append(UInt8(errorCode)) - data.appendString(reason) - return data - } -} - diff --git a/build.log b/build.log deleted file mode 100644 index 415d6ce5..00000000 --- a/build.log +++ /dev/null @@ -1,13 +0,0 @@ -Command line invocation: - /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -project bitchat.xcodeproj -scheme bitchat -configuration Debug -sdk iphonesimulator CODE_SIGNING_ALLOWED=NO ONLY_ACTIVE_ARCH=YES - -Build settings from command line: - CODE_SIGNING_ALLOWED = NO - ONLY_ACTIVE_ARCH = YES - SDKROOT = iphonesimulator18.5 - -Resolve Package Graph -/Users/jack/Library/org.swift.swiftpm/configuration is not accessible or not writable, disabling user-level cache features./Users/jack/Library/org.swift.swiftpm/security is not accessible or not writable, disabling user-level cache features./Users/jack/Library/Caches/org.swift.swiftpm is not accessible or not writable, disabling user-level cache features. -Package: swift-secp256k1 - -fatalError \ No newline at end of file