mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
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
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
6C8801D22A8F987A7696BB90 /* AudioRecordingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordingService.swift; sourceTree = "<group>"; };
|
||||
6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
|
||||
B90B7FFCA6DA409F3CB78704 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = "<group>"; };
|
||||
@@ -109,6 +112,7 @@
|
||||
6C8801D22A8F987A7696BB90 /* AudioRecordingService.swift */,
|
||||
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */,
|
||||
6DC1563390A15C042D059CF9 /* EncryptionService.swift */,
|
||||
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
@@ -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;
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
<string>bitchat uses Bluetooth to discover and connect with other bitchat users nearby.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>bitchat needs access to your microphone to record voice notes.</string>
|
||||
<key>NSUserNotificationsUsageDescription</key>
|
||||
<string>bitchat needs permission to send notifications for mentions and private messages.</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>bluetooth-central</string>
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)")
|
||||
|
||||
Reference in New Issue
Block a user