From 6652d1f42965c0f690d95378fb526f7449cec7a0 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 3 Jul 2025 23:14:42 +0200 Subject: [PATCH] Fix voice recording and notification issues - Fix mic button gesture handling by removing Button wrapper - Use direct gesture on ZStack for more reliable touch detection - Add debug logging for recording start/stop - Fix notification delivery on iOS - Change from checking .background to checking != .active - Notifications now work when app is inactive or background - Add NSUserNotificationsUsageDescription to Info.plist - Improve voice note fragment transmission - Reduce fragment size from 400 to 200 bytes for better reliability - Simplify fragment timing from batch-based to linear 100ms delays - Lower packet size threshold from 512 to 400 bytes for fragmentation - Add peripheral connection state checking before writes - Better error logging throughout transmission pipeline - Add comprehensive debug logging for troubleshooting - Log audio file size and duration - Log peripheral connection states during broadcast - Log fragment send progress with timing info --- bitchat.xcodeproj/project.pbxproj | 15 +++-- bitchat/Info.plist | 2 + bitchat/Services/BluetoothMeshService.swift | 41 ++++++++------ bitchat/Services/NotificationService.swift | 7 ++- bitchat/Views/ContentView.swift | 63 ++++++++++++--------- 5 files changed, 77 insertions(+), 51 deletions(-) diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index b3b1b933..e5a930fe 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -3,17 +3,19 @@ archiveVersion = 1; classes = { }; - objectVersion = 63; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; }; 1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; }; 4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; }; + 61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; }; 6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; }; 6E628AC6B7E1754F3BD5090D /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90B7FFCA6DA409F3CB78704 /* AudioPlaybackService.swift */; }; 6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; }; 739429DFDE5C5829CF70DA7D /* EncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC1563390A15C042D059CF9 /* EncryptionService.swift */; }; + 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 */; }; 7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; }; @@ -31,11 +33,12 @@ /* Begin PBXFileReference section */ 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = ""; }; + 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6C8801D22A8F987A7696BB90 /* AudioRecordingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordingService.swift; sourceTree = ""; }; 6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = ""; }; 7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 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; }; A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = ""; }; B90B7FFCA6DA409F3CB78704 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = ""; }; @@ -109,6 +112,7 @@ 6C8801D22A8F987A7696BB90 /* AudioRecordingService.swift */, D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */, 6DC1563390A15C042D059CF9 /* EncryptionService.swift */, + 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */, ); path = Services; sourceTree = ""; @@ -181,6 +185,7 @@ ); mainGroup = 18198ED912AAF495D8AF7763; minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 54; projectDirPath = ""; projectRoot = ""; targets = ( @@ -223,6 +228,7 @@ D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */, 92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */, 739429DFDE5C5829CF70DA7D /* EncryptionService.swift in Sources */, + 749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -239,6 +245,7 @@ 7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */, 1D9674FA5F998503831DC281 /* ContentView.swift in Sources */, DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */, + 61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -262,7 +269,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.app; + PRODUCT_BUNDLE_IDENTIFIER = com.bitchat.app; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; @@ -288,7 +295,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.app; + PRODUCT_BUNDLE_IDENTIFIER = com.bitchat.app; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; diff --git a/bitchat/Info.plist b/bitchat/Info.plist index 78b57dd9..f4bbfc29 100644 --- a/bitchat/Info.plist +++ b/bitchat/Info.plist @@ -28,6 +28,8 @@ bitchat uses Bluetooth to discover and connect with other bitchat users nearby. NSMicrophoneUsageDescription bitchat needs access to your microphone to record voice notes. + NSUserNotificationsUsageDescription + bitchat needs permission to send notifications for mentions and private messages. UIBackgroundModes bluetooth-central diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 899842b2..68913ecd 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -44,7 +44,7 @@ class BluetoothMeshService: NSObject { // Fragment handling private var incomingFragments: [String: [Int: Data]] = [:] // fragmentID -> [index: data] private var fragmentMetadata: [String: (originalType: UInt8, totalFragments: Int, timestamp: Date)] = [:] - private let maxFragmentSize = 400 // Leave room for protocol overhead + private let maxFragmentSize = 200 // Smaller fragments for better reliability let myPeerID: String @@ -224,15 +224,17 @@ class BluetoothMeshService: NSObject { if let packetData = packet.toBinaryData() { print("[VOICE] Final packet size: \(packetData.count) bytes") - if packetData.count > 512 { - print("[VOICE] WARNING: Packet size exceeds typical BLE MTU of 512 bytes!") + // Check if packet exceeds safe BLE size (leave margin for overhead) + if packetData.count > 400 { + print("[VOICE] Packet size \(packetData.count) exceeds safe BLE limit") print("[VOICE] Fragmenting voice note into smaller packets...") self.sendFragmentedPacket(packet) } else { + print("[VOICE] Sending voice note as single packet") self.broadcastPacket(packet) } } else { - self.broadcastPacket(packet) + print("[VOICE] ERROR: Failed to convert packet to binary data") } } } @@ -348,11 +350,18 @@ class BluetoothMeshService: NSObject { // Send to connected peripherals (as central) var sentToPeripherals = 0 - for (_, peripheral) in connectedPeripherals { + for (peerID, peripheral) in connectedPeripherals { if let characteristic = peripheralCharacteristics[peripheral] { - // Always use withResponse for reliability, especially for background - peripheral.writeValue(data, for: characteristic, type: .withResponse) - sentToPeripherals += 1 + // Check if peripheral is connected before writing + if peripheral.state == .connected { + // Always use withResponse for reliability, especially for background + peripheral.writeValue(data, for: characteristic, type: .withResponse) + sentToPeripherals += 1 + } else { + print("[BROADCAST] Peripheral \(peerID) not connected (state: \(peripheral.state.rawValue))") + } + } else { + print("[BROADCAST] No characteristic found for peripheral \(peerID)") } } print("[BROADCAST] Sent to \(sentToPeripherals) connected peripherals") @@ -643,10 +652,8 @@ class BluetoothMeshService: NSObject { print("[FRAGMENT] Fragment ID: \(fragmentID.hexEncodedString())") print("[FRAGMENT] Original packet size: \(fullData.count) bytes") - // Send fragments in batches to avoid congestion - let batchSize = 5 - let delayBetweenFragments: TimeInterval = 0.05 // 50ms between fragments - let delayBetweenBatches: TimeInterval = 0.2 // 200ms between batches + // Send fragments with delays to avoid congestion + let delayBetweenFragments: TimeInterval = 0.1 // 100ms between fragments for (index, fragmentData) in fragments.enumerated() { var fragmentPayload = Data() @@ -676,19 +683,17 @@ class BluetoothMeshService: NSObject { payload: fragmentPayload ) - // Calculate delay based on batch - let batchNumber = index / batchSize - let indexInBatch = index % batchSize - let totalDelay = (Double(batchNumber) * delayBetweenBatches) + (Double(indexInBatch) * delayBetweenFragments) + // Send fragments with linear delay + let totalDelay = Double(index) * delayBetweenFragments // Send fragments on background queue with calculated delay messageQueue.asyncAfter(deadline: .now() + totalDelay) { [weak self] in self?.broadcastPacket(fragmentPacket) - print("[FRAGMENT] Sent fragment \(index + 1)/\(fragments.count) type: \(fragmentType) (batch \(batchNumber + 1))") + print("[FRAGMENT] Sent fragment \(index + 1)/\(fragments.count) type: \(fragmentType) at +\(totalDelay)s") } } - let totalTime = Double((fragments.count - 1) / batchSize) * delayBetweenBatches + Double((fragments.count - 1) % batchSize) * delayBetweenFragments + let totalTime = Double(fragments.count - 1) * delayBetweenFragments print("[FRAGMENT] Total send time: \(totalTime)s for \(fragments.count) fragments") } diff --git a/bitchat/Services/NotificationService.swift b/bitchat/Services/NotificationService.swift index 2c0bfaaa..cd2559da 100644 --- a/bitchat/Services/NotificationService.swift +++ b/bitchat/Services/NotificationService.swift @@ -25,11 +25,14 @@ class NotificationService { func sendLocalNotification(title: String, body: String, identifier: String) { #if os(iOS) - guard UIApplication.shared.applicationState == .background else { - print("[NOTIFICATIONS] App is in foreground, skipping notification") + // Send notification if app is not active (background or inactive) + guard UIApplication.shared.applicationState != .active else { + print("[NOTIFICATIONS] App is active/foreground, skipping notification") return } + print("[NOTIFICATIONS] App state: \(UIApplication.shared.applicationState.rawValue), sending notification") + let content = UNMutableNotificationContent() content.title = title content.body = body diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 6d22b9f2..14fe2693 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -431,41 +431,41 @@ struct ContentView: View { } // Push to talk button - Button(action: {}) { - ZStack { - // Mic icon - Image(systemName: "mic.circle.fill") - .font(.system(size: 20)) - .foregroundColor(isRecordingVoice ? Color.red.opacity(0.8) : textColor) - - // Local ripples that start from the button itself - if isRecordingVoice { - ForEach(0..<4) { index in - Circle() - .stroke(Color.red.opacity(0.3), lineWidth: 1) - .frame(width: 20, height: 20) - .scaleEffect(1 + Double(index) * 0.5) - .opacity(isRecordingVoice ? 0.5 - Double(index) * 0.1 : 0) - .animation( - Animation.easeOut(duration: 1.5) - .repeatForever(autoreverses: false) - .delay(Double(index) * 0.2), - value: isRecordingVoice - ) - } + ZStack { + // Mic icon + Image(systemName: "mic.circle.fill") + .font(.system(size: 20)) + .foregroundColor(isRecordingVoice ? Color.red.opacity(0.8) : textColor) + + // Local ripples that start from the button itself + if isRecordingVoice { + ForEach(0..<4) { index in + Circle() + .stroke(Color.red.opacity(0.3), lineWidth: 1) + .frame(width: 20, height: 20) + .scaleEffect(1 + Double(index) * 0.5) + .opacity(isRecordingVoice ? 0.5 - Double(index) * 0.1 : 0) + .animation( + Animation.easeOut(duration: 1.5) + .repeatForever(autoreverses: false) + .delay(Double(index) * 0.2), + value: isRecordingVoice + ) } } } - .buttonStyle(.plain) - .simultaneousGesture( + .contentShape(Rectangle()) // Make entire area tappable + .gesture( DragGesture(minimumDistance: 0) .onChanged { _ in if !isRecordingVoice { + print("[UI] Starting voice recording") startVoiceRecording() } } .onEnded { _ in if isRecordingVoice { + print("[UI] Stopping voice recording") stopVoiceRecording() } } @@ -503,7 +503,11 @@ struct ContentView: View { #endif viewModel.audioRecorder.startRecording { result in - // Will handle result in stopVoiceRecording + if case .failure(let error) = result { + print("[UI] Failed to start recording: \(error)") + isRecordingVoice = false + recordingScale = 1.0 + } } } @@ -517,12 +521,17 @@ struct ContentView: View { switch result { case .success(let audioURL): // Read audio file and send as voice note - if let audioData = try? Data(contentsOf: audioURL) { + do { + let audioData = try Data(contentsOf: audioURL) + print("[UI] Read audio file: \(audioData.count) bytes, duration: \(duration)s") + // Use the captured duration, ensure it's at least 0.1s viewModel.sendVoiceNote(audioData, duration: max(0.1, duration)) // Clean up temporary file - try? FileManager.default.removeItem(at: audioURL) + try FileManager.default.removeItem(at: audioURL) + } catch { + print("[UI] Failed to read audio file: \(error)") } case .failure(let error): print("[AUDIO] Recording failed: \(error)")