diff --git a/bitchat/Protocols/BitchatProtocol.swift b/bitchat/Protocols/BitchatProtocol.swift index 58e3d0bb..ccfc9050 100644 --- a/bitchat/Protocols/BitchatProtocol.swift +++ b/bitchat/Protocols/BitchatProtocol.swift @@ -155,6 +155,7 @@ protocol BitchatDelegate: AnyObject { func didConnectToPeer(_ peerID: String) func didDisconnectFromPeer(_ peerID: String) func didUpdatePeerList(_ peers: [String]) + func didReceiveRoomLeave(_ room: String, from peerID: String) // Optional method to check if a fingerprint belongs to a favorite peer func isFavorite(fingerprint: String) -> Bool @@ -165,4 +166,8 @@ extension BitchatDelegate { func isFavorite(fingerprint: String) -> Bool { return false } + + func didReceiveRoomLeave(_ room: String, from peerID: String) { + // Default empty implementation + } } \ No newline at end of file diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 8a8cc53a..cc7f643c 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -665,6 +665,26 @@ class BluetoothMeshService: NSObject { } } + func sendRoomLeaveNotification(_ room: String) { + messageQueue.async { [weak self] in + guard let self = self else { return } + + // Create a leave packet with room hashtag as payload + let packet = BitchatPacket( + type: MessageType.leave.rawValue, + senderID: Data(self.myPeerID.utf8), + recipientID: SpecialRecipients.broadcast, // Broadcast to all + timestamp: UInt64(Date().timeIntervalSince1970 * 1000), + payload: Data(room.utf8), // Room hashtag as payload + signature: nil, + ttl: 3 // Short TTL for leave notifications + ) + + bitchatLog("Sending room leave notification for \(room)", category: "room") + self.broadcastPacket(packet) + } + } + private func sendAnnouncementToPeer(_ peerID: String) { guard let vm = delegate as? ChatViewModel else { return } @@ -1585,28 +1605,45 @@ class BluetoothMeshService: NSObject { } case .leave: - if let nickname = String(data: packet.payload, encoding: .utf8), - let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) { - - - // Remove from active peers with proper locking - activePeersLock.lock() - activePeers.remove(senderID) - activePeersLock.unlock() - - announcedPeers.remove(senderID) - - // Show leave message - DispatchQueue.main.async { - self.delegate?.didDisconnectFromPeer(nickname) + if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) { + // Check if payload contains a room hashtag + if let room = String(data: packet.payload, encoding: .utf8), + room.hasPrefix("#") { + // Room leave notification + bitchatLog("Received room leave notification from \(senderID) for room \(room)", category: "room") + + DispatchQueue.main.async { + self.delegate?.didReceiveRoomLeave(room, from: senderID) + } + + // Relay if TTL > 0 + if packet.ttl > 1 { + var relayPacket = packet + relayPacket.ttl -= 1 + self.broadcastPacket(relayPacket) + } + } else { + // Legacy peer disconnect (keeping for backwards compatibility) + if let nickname = String(data: packet.payload, encoding: .utf8) { + // Remove from active peers with proper locking + activePeersLock.lock() + activePeers.remove(senderID) + activePeersLock.unlock() + + announcedPeers.remove(senderID) + + // Show leave message + DispatchQueue.main.async { + self.delegate?.didDisconnectFromPeer(nickname) + } + self.notifyPeerListUpdate() + + // Clean up peer data + peerNicknamesLock.lock() + peerNicknames.removeValue(forKey: senderID) + peerNicknamesLock.unlock() + } } - self.notifyPeerListUpdate() - - // Clean up peer data - peerNicknamesLock.lock() - peerNicknames.removeValue(forKey: senderID) - peerNicknamesLock.unlock() - } else { } case .fragmentStart, .fragmentContinue, .fragmentEnd: diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 09858cb2..6a944fce 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -132,6 +132,9 @@ class ChatViewModel: ObservableObject { joinedRooms.remove(room) saveJoinedRooms() + // Send leave notification to other peers + meshService.sendRoomLeaveNotification(room) + // If we're currently in this room, exit to main chat if currentRoom == room { currentRoom = nil @@ -630,6 +633,17 @@ class ChatViewModel: ObservableObject { } extension ChatViewModel: BitchatDelegate { + func didReceiveRoomLeave(_ room: String, from peerID: String) { + // Remove peer from room members + if roomMembers[room] != nil { + roomMembers[room]?.remove(peerID) + bitchatLog("Removed peer \(peerID) from room \(room) members", category: "room") + + // Force UI update + objectWillChange.send() + } + } + func didReceiveMessage(_ message: BitchatMessage) { bitchatLog("Received message from \(message.sender), room: \(message.room ?? "nil"), senderPeerID: \(message.senderPeerID ?? "nil")", category: "message") diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index d24d7c39..a1b7f588 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -644,8 +644,8 @@ struct ContentView: View { HStack(spacing: 8) { // Signal strength indicator or unread message icon if isMe { - Text("•") - .font(.system(size: 12)) + Image(systemName: "person.fill") + .font(.system(size: 10)) .foregroundColor(textColor) } else if viewModel.unreadPrivateMessages.contains(peerID) { Image(systemName: "envelope.fill")