Add delivery confirmation and read receipts for private messages

- Implement full delivery status flow: sending → sent → delivered → read
- Add visual indicators (gray/green/blue checkmarks)
- Create DeliveryTracker service for managing confirmations
- Add end-to-end encrypted ACK and read receipt messages
- Handle ephemeral peer ID changes with message migration
- Fix message ID preservation in BinaryProtocol
- Ensure proper delivery status for incoming messages
- Add multiple triggers for sending read receipts
- Maintain full backwards compatibility with older clients

Visual indicators:
- ○ Gray circle = Sending
- ✓ Gray check = Sent
- ✓✓ Green double checks = Delivered
- ✓✓ Blue double checks = Read
- ⚠ Red triangle = Failed
This commit is contained in:
jack
2025-07-06 23:44:42 +02:00
parent b880c663c2
commit 784a809d07
8 changed files with 330 additions and 203 deletions
+18
View File
@@ -0,0 +1,18 @@
Add delivery confirmation and read receipts for private messages
- Implement full delivery status flow: sending → sent → delivered → read
- Add visual indicators (gray/green/blue checkmarks)
- Create DeliveryTracker service for managing confirmations
- Add end-to-end encrypted ACK and read receipt messages
- Handle ephemeral peer ID changes with message migration
- Fix message ID preservation in BinaryProtocol
- Ensure proper delivery status for incoming messages
- Add multiple triggers for sending read receipts
- Maintain full backwards compatibility with older clients
Visual indicators:
- ○ Gray circle = Sending
- ✓ Gray check = Sent
- ✓✓ Green double checks = Delivered
- ✓✓ Blue double checks = Read
- ⚠ Red triangle = Failed
+113
View File
@@ -0,0 +1,113 @@
# Delivery Confirmation Branch - Complete Summary
## Overview
Implemented a complete end-to-end delivery confirmation system with read receipts for private messages, similar to WhatsApp/Signal.
## Features Implemented
### Visual Status Indicators
- ○ Gray circle = Sending
- ✓ Gray single check = Sent (left device)
- ✓✓ Green double checks = Delivered (received by peer)
- ✓✓ Blue double checks (bold) = Read (viewed by peer)
- ⚠ Red triangle = Failed
### Core Components
1. **DeliveryStatus Enum** (`BitchatProtocol.swift`)
- States: sending, sent, delivered, read, failed, partiallyDelivered
- Includes recipient info and timestamps
2. **DeliveryTracker Service** (`DeliveryTracker.swift`)
- Manages pending deliveries
- Handles timeouts (30s default, 60s for favorites)
- Sends delivery status updates via Combine
- Thread-safe with proper locking
3. **Message Types** (`BitchatProtocol.swift`)
- DeliveryAck (0x0A): Confirms message received
- ReadReceipt (0x0C): Confirms message read
4. **UI Components** (`ContentView.swift`)
- DeliveryStatusView showing appropriate icons
- Real-time status updates
- Proper alignment with message text
## Key Technical Achievements
### 1. Message ID Consistency
- Fixed BinaryProtocol to preserve message IDs
- Ensures ACKs reference the correct message
### 2. Ephemeral Peer ID Handling
- Messages migrate between peer IDs based on nickname
- Read receipts sent to current peer ID, not old ones
- Chat history preserved across sessions
### 3. Read Receipt Triggers
- When opening a chat (via button or message send)
- When app becomes active with chat open
- When receiving message while chat is open
- Multiple retry attempts to ensure delivery
### 4. Thread Safety
- Fixed deadlock in DeliveryTracker
- Proper lock management
- Async message processing
### 5. Status Consistency
- Prevents status downgrades (read → delivered)
- Handles race conditions gracefully
- Proper status for incoming messages
## Privacy & Security
- All ACKs and receipts are end-to-end encrypted
- Only the original sender can decrypt confirmations
- No message content in confirmations
- Receipts only sent for private messages
## Backwards Compatibility ✅
- **Fully backwards compatible!**
- Old clients ignore new message types
- Mixed client scenarios work correctly
- No breaking changes to core messaging
## What's NOT Included
1. Read receipts for room/group messages
2. User preference to disable read receipts
3. Batch ACK optimization
4. Persistent storage of delivery status
5. "Last seen" functionality
## Testing Checklist
- [x] Single message delivery confirmation
- [x] Multiple messages in sequence
- [x] Read receipts when opening existing chat
- [x] Peer ID changes between sessions
- [x] Mixed old/new client communication
- [x] Favorite vs non-favorite timeouts
- [x] App backgrounding/foregrounding
## Known Limitations
1. Memory usage grows with pending deliveries (no cleanup)
2. Lots of debug logging (should be removed for production)
3. No UI indication of read timestamp
4. Status not shown in notifications/previews
## Code Quality
- Clean separation of concerns
- Follows existing app patterns
- Proper use of Combine for reactivity
- Well-structured and maintainable
## Files Modified
- `BitchatProtocol.swift` - Added delivery structures
- `DeliveryTracker.swift` - New service (created)
- `BluetoothMeshService.swift` - ACK/receipt handling
- `ChatViewModel.swift` - Status updates, read triggers
- `ContentView.swift` - UI indicators
- `BinaryProtocol.swift` - Message ID preservation
- `project.yml` - Added new service file
## Summary
The delivery confirmation system is feature-complete, secure, and backwards compatible. It provides users with real-time feedback about message delivery and read status while maintaining the app's privacy-focused design. The implementation is production-ready with minor cleanup needed (mainly removing debug logs).
-66
View File
@@ -1,66 +0,0 @@
# Delivery Confirmation - Complete Implementation
## The Journey
We implemented a full delivery confirmation system with read receipts. The feature went through several iterations to handle edge cases and the ephemeral nature of peer IDs.
## Key Challenges Solved
### 1. Deadlock Issues
- **Problem**: Nested lock acquisition causing macOS app to freeze
- **Solution**: Restructured DeliveryTracker to release locks before calling methods that acquire the same lock
### 2. Message ID Preservation
- **Problem**: ACKs referenced wrong message IDs
- **Solution**: Fixed BinaryProtocol to properly preserve message IDs during decoding
### 3. Race Condition with Status Updates
- **Problem**: Read receipts would briefly show blue checkmarks, then revert to green when ACK arrived
- **Solution**: Added logic to prevent downgrading from 'read' to 'delivered' status
### 4. Ephemeral Peer IDs
- **Problem**: Peer IDs change between sessions, breaking read receipts for existing messages
- **Solution**:
- Match messages by sender nickname in addition to peer ID
- Send read receipts to the CURRENT peer ID, not the old one from the message
- Removed requirement for senderPeerID to exist
## How It Works Now
### Sending a Message
1. Alice sends message to Bob
2. Message shows gray circle (○) - "sending"
3. Message is transmitted and shows gray checkmark (✓) - "sent"
4. Bob receives and sends ACK
5. Alice sees green double checkmarks (✓✓) - "delivered"
### Reading Messages
1. When Bob opens/views the chat with Alice
2. System checks all messages from Alice with "delivered" status
3. Sends read receipts for those messages
4. Alice sees blue double checkmarks (✓✓) - "read"
### Key Code Components
- `DeliveryTracker.swift` - Manages delivery confirmations and timeouts
- `BitchatProtocol.swift` - Defines delivery status enum and ACK/receipt structures
- `BluetoothMeshService.swift` - Handles sending/receiving ACKs and receipts
- `ChatViewModel.swift` - Updates UI and manages read receipt logic
- `ContentView.swift` - Displays delivery status indicators
## Visual Indicators
- ○ Gray circle = Sending
- ✓ Gray single check = Sent
- ✓✓ Green double checks = Delivered
- ✓✓ Blue double checks (bold) = Read
- ⚠ Red triangle = Failed
## Privacy Features
- All ACKs and read receipts are end-to-end encrypted
- Only the original sender can decrypt delivery confirmations
- No message content is included in confirmations
## Testing the Feature
1. Send a message between two devices
2. Watch status progression on sender's device
3. Open chat on receiver's device
4. Sender should see blue checkmarks
5. Works even if receiver was offline when message was sent
-74
View File
@@ -1,74 +0,0 @@
# Delivery Confirmation Feature Summary
## Overview
This branch implements a complete delivery confirmation system for bitchat, providing visual feedback for message delivery status and read receipts.
## Key Components Implemented
### 1. Delivery Status Tracking
- **DeliveryTracker.swift**: Core service managing delivery confirmations
- Tracks pending deliveries with timeouts (30s for private, 60s for rooms)
- Handles retries for favorite peers (up to 3 attempts)
- Thread-safe implementation with proper lock management
### 2. Protocol Updates
- **BitchatProtocol.swift**:
- Added `DeliveryStatus` enum with states: sending, sent, delivered, read, failed
- Added `DeliveryAck` structure for acknowledgments
- Added `ReadReceipt` structure for read confirmations
- New message types: `.deliveryAck` and `.readReceipt`
### 3. UI Implementation
- **ContentView.swift**:
- Added `DeliveryStatusView` component showing:
- Gray circle (○) for sending
- Gray checkmark (✓) for sent
- Green double checkmarks (✓✓) for delivered
- Blue double checkmarks (✓✓) for read
- Red triangle (⚠) for failed
- Proper vertical alignment with message text
### 4. Message Flow
- **BluetoothMeshService.swift**:
- Added ACK generation and sending
- Added read receipt generation when viewing chats
- Encrypted ACKs for privacy
- Proper message ID handling
### 5. Critical Bug Fixes
#### Deadlock Fix
- **Issue**: macOS app freeze when sending private messages
- **Cause**: Nested lock acquisition in DeliveryTracker
- **Fix**: Restructured code to release locks before calling methods that acquire the same lock
#### Message ID Preservation
- **Issue**: ACKs referenced wrong message IDs
- **Cause**: BinaryProtocol was reading but discarding ID with `let _ =`
- **Fix**: Changed to `let id =` and passed ID to BitchatMessage constructor
## Technical Details
### Privacy Considerations
- ACKs and read receipts are encrypted end-to-end
- Only the original sender can decrypt and process them
- No message content is included in confirmations
### Performance
- Debounced UI updates (100ms)
- Efficient lock usage to prevent deadlocks
- Automatic cleanup of old delivery data
### Error Handling
- Graceful timeout handling
- Retry logic for favorites
- Clear failure messages
## Testing
See `test_delivery_confirmation.md` for comprehensive test plan.
## Future Enhancements
- Group read receipts (show who read in rooms)
- Delivery timestamps in UI
- Persistent delivery status across app restarts
- Network quality indicators
-37
View File
@@ -1,37 +0,0 @@
# Test Checklist for Delivery Confirmation
## Quick Test
1. **Rick sends to Jack** (while Jack is not in the chat)
- ✓ Rick sees gray circle → gray check → green double checks
2. **Jack opens chat with Rick**
- ✓ Jack should see Rick's message
- ✓ Rick should see blue double checks (this is what we fixed!)
3. **Jack sends reply to Rick** (while Rick is viewing)
- ✓ Jack sees gray circle → gray check → green double checks → blue double checks
## Debug Logs to Watch For
**On Jack's device when opening chat:**
```
[UI] Selected private chat peer changed to [peer-id]
[UI] Triggering markPrivateMessagesAsRead for peer [peer-id]
[Delivery] Checking N messages in chat with peer [peer-id] (rick) for read receipts
[Delivery] Message [id] from rick, senderPeerID: [old-id], currentPeerID: [new-id], myNickname: jack, status: Delivered to jack
[Delivery] Sending read receipt for message [id] from rick to current peer [peer-id]
[DeliveryTracker] Broadcasting read receipt packet to [peer-id]
```
**On Rick's device when receiving read receipt:**
```
[Delivery] Received READ receipt for message [id] from jack
[Delivery] Updating message [id] to status: read(by: "jack", at: [timestamp])
[UI] Showing BLUE checkmarks for read status by jack
```
## What to Check
- Messages sent before opening chat should turn blue when chat is opened
- Peer ID changes between sessions shouldn't break read receipts
- No race conditions - blue checkmarks should stay blue
- Read receipts work for both real-time and delayed chat opening
+2 -1
View File
@@ -1581,7 +1581,8 @@ class BluetoothMeshService: NSObject {
recipientNickname: message.recipientNickname,
senderPeerID: senderID,
mentions: message.mentions,
room: message.room
room: message.room,
deliveryStatus: nil // Will be set to .delivered in ChatViewModel
)
// Track last message time from this peer
+179 -23
View File
@@ -865,6 +865,11 @@ class ChatViewModel: ObservableObject {
guard !content.isEmpty else { return }
guard let recipientNickname = meshService.getPeerNicknames()[peerID] else { return }
// IMPORTANT: When sending a message, it means we're viewing this chat
// Send read receipts for any delivered messages from this peer
print("[Delivery] Sending private message to \(peerID), checking for unread messages first")
markPrivateMessagesAsRead(from: peerID)
// Create the message locally
let message = BitchatMessage(
sender: nickname,
@@ -897,22 +902,85 @@ class ChatViewModel: ObservableObject {
func startPrivateChat(with peerID: String) {
print("[Delivery] Starting private chat with peer \(peerID)")
let peerNickname = meshService.getPeerNicknames()[peerID] ?? "unknown"
print("[Delivery] My nickname: \(nickname), peer nickname: \(peerNickname)")
selectedPrivateChatPeer = peerID
unreadPrivateMessages.remove(peerID)
// Initialize chat history if needed
if privateChats[peerID] == nil {
privateChats[peerID] = []
print("[Delivery] Initialized empty chat history for peer \(peerID)")
} else {
print("[Delivery] Found existing chat history with \(privateChats[peerID]?.count ?? 0) messages for peer \(peerID)")
// Check if we need to migrate messages from an old peer ID
// This happens when peer IDs change between sessions
if privateChats[peerID] == nil || privateChats[peerID]?.isEmpty == true {
print("[Delivery] No messages under current peer ID \(peerID), checking for messages from nickname \(peerNickname)")
// Look for messages from this nickname under other peer IDs
var migratedMessages: [BitchatMessage] = []
var oldPeerIDsToRemove: [String] = []
for (oldPeerID, messages) in privateChats {
if oldPeerID != peerID {
// Check if any messages in this chat are from the peer's nickname
// Check if this chat contains messages with this peer
let messagesWithPeer = messages.filter { msg in
// Message is FROM the peer to us
(msg.sender == peerNickname && msg.sender != nickname) ||
// OR message is FROM us TO the peer
(msg.sender == nickname && (msg.recipientNickname == peerNickname ||
// Also check if this was a private message in a chat that only has us and one other person
(msg.isPrivate && messages.allSatisfy { m in
m.sender == nickname || m.sender == peerNickname
})))
}
if !messagesWithPeer.isEmpty {
print("[Delivery] Found \(messagesWithPeer.count) messages with \(peerNickname) under old peer ID \(oldPeerID)")
// Check if ALL messages in this chat are between us and this peer
let allMessagesAreWithPeer = messages.allSatisfy { msg in
(msg.sender == peerNickname || msg.sender == nickname) &&
(msg.recipientNickname == nil || msg.recipientNickname == peerNickname || msg.recipientNickname == nickname)
}
if allMessagesAreWithPeer {
// This entire chat history belongs to this peer, migrate it all
print("[Delivery] Migrating entire chat history (\(messages.count) messages) from old peer ID \(oldPeerID) to new peer ID \(peerID)")
migratedMessages.append(contentsOf: messages)
oldPeerIDsToRemove.append(oldPeerID)
}
}
}
}
// Remove old peer ID entries that were fully migrated
for oldPeerID in oldPeerIDsToRemove {
privateChats.removeValue(forKey: oldPeerID)
unreadPrivateMessages.remove(oldPeerID)
}
// Initialize chat history with migrated messages if any
if !migratedMessages.isEmpty {
privateChats[peerID] = migratedMessages.sorted { $0.timestamp < $1.timestamp }
print("[Delivery] Migrated \(migratedMessages.count) messages to peer \(peerID)")
} else {
privateChats[peerID] = []
print("[Delivery] Initialized empty chat history for peer \(peerID)")
}
}
let messages = privateChats[peerID] ?? []
print("[Delivery] Chat with peer \(peerID) now has \(messages.count) messages")
for (index, msg) in messages.enumerated() {
print("[Delivery] Message \(index): from \(msg.sender), status: \(msg.deliveryStatus?.displayText ?? "none")")
}
// Send read receipts for unread messages from this peer
// Add a small delay to ensure UI has updated
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
print("[Delivery] Delayed call to markPrivateMessagesAsRead for peer \(peerID)")
self?.markPrivateMessagesAsRead(from: peerID)
}
// Also try immediately in case messages are already there
markPrivateMessagesAsRead(from: peerID)
}
func endPrivateChat() {
@@ -922,32 +990,69 @@ class ChatViewModel: ObservableObject {
@objc private func appDidBecomeActive() {
// When app becomes active, send read receipts for visible private chat
if let peerID = selectedPrivateChatPeer {
print("[Delivery] App became active with selectedPrivateChatPeer = \(peerID)")
// Try immediately
self.markPrivateMessagesAsRead(from: peerID)
// And again with a delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
print("[Delivery] App became active, checking for messages to mark as read in chat with \(peerID)")
print("[Delivery] App became active (delayed), checking for messages to mark as read in chat with \(peerID)")
self.markPrivateMessagesAsRead(from: peerID)
}
}
}
func markPrivateMessagesAsRead(from peerID: String) {
guard let messages = privateChats[peerID] else {
print("[Delivery] No messages found for peer \(peerID)")
// Get the nickname for this peer
let peerNickname = meshService.getPeerNicknames()[peerID] ?? ""
print("[Delivery] ========== markPrivateMessagesAsRead START ==========")
print("[Delivery] Called for peer \(peerID) (\(peerNickname))")
print("[Delivery] My nickname: \(nickname), My peerID: \(meshService.myPeerID)")
// First ensure we have the latest messages (in case of migration)
if let messages = privateChats[peerID], !messages.isEmpty {
print("[Delivery] Found \(messages.count) messages in privateChats[\(peerID)]")
} else {
print("[Delivery] No messages found in privateChats[\(peerID)], checking all chats for messages from \(peerNickname)")
// Look through ALL private chats to find messages from this nickname
for (chatPeerID, chatMessages) in privateChats {
let relevantMessages = chatMessages.filter { msg in
msg.sender == peerNickname && msg.sender != nickname
}
if !relevantMessages.isEmpty {
print("[Delivery] Found \(relevantMessages.count) messages from \(peerNickname) in privateChats[\(chatPeerID)]")
}
}
}
guard let messages = privateChats[peerID], !messages.isEmpty else {
print("[Delivery] No messages to process for peer \(peerID)")
print("[Delivery] ========== markPrivateMessagesAsRead END (no messages) ==========")
return
}
// Get the nickname for this peer
let peerNickname = meshService.getPeerNicknames()[peerID] ?? ""
print("[Delivery] Checking \(messages.count) messages in chat with peer \(peerID) (\(peerNickname)) for read receipts")
print("[Delivery] Processing \(messages.count) messages in chat with peer \(peerID) (\(peerNickname)) for read receipts")
// Find messages from the peer that haven't been read yet
for message in messages {
var readReceiptsSent = 0
for (index, message) in messages.enumerated() {
// Only send read receipts for messages from the other peer (not our own)
print("[Delivery] Message \(message.id) from \(message.sender), senderPeerID: \(message.senderPeerID ?? "nil"), currentPeerID: \(peerID), myNickname: \(nickname), status: \(message.deliveryStatus?.displayText ?? "none")")
// Check multiple conditions to ensure we catch all messages from the peer
let isOurMessage = message.sender == nickname
let isFromPeerByNickname = !peerNickname.isEmpty && message.sender == peerNickname
let isFromPeerByID = message.senderPeerID == peerID
let isPrivateToUs = message.isPrivate && message.recipientNickname == nickname
// Check if this is a message FROM the other person TO us
// Match by sender nickname since peer IDs change between sessions
let isFromPeer = message.sender == peerNickname || message.senderPeerID == peerID
if message.sender != nickname && isFromPeer {
// This is a message FROM the peer if it's not from us AND (matches nickname OR peer ID OR is private to us)
let isFromPeer = !isOurMessage && (isFromPeerByNickname || isFromPeerByID || isPrivateToUs)
print("[Delivery] Message \(index): id=\(message.id)")
print("[Delivery] from=\(message.sender), recipientNickname=\(message.recipientNickname ?? "nil")")
print("[Delivery] senderPeerID=\(message.senderPeerID ?? "nil"), currentPeerID=\(peerID)")
print("[Delivery] isPrivate=\(message.isPrivate), isOurMessage=\(isOurMessage), isFromPeer=\(isFromPeer)")
print("[Delivery] status=\(message.deliveryStatus?.displayText ?? "none")")
if isFromPeer {
if let status = message.deliveryStatus {
switch status {
case .sent, .delivered:
@@ -959,15 +1064,14 @@ class ChatViewModel: ObservableObject {
readerNickname: nickname
)
meshService.sendReadReceipt(receipt, to: peerID)
print("[Delivery] Sending read receipt for message \(message.id) from \(message.sender) to current peer \(peerID)")
print("[Delivery] Sending read receipt for message \(message.id) from \(message.sender) to current peer \(peerID)")
readReceiptsSent += 1
case .read:
// Already read, no need to send another receipt
print("[Delivery] Message \(message.id) already marked as read")
break
default:
// Message not yet delivered, can't mark as read
print("[Delivery] Message \(message.id) has status \(status.displayText), not sending read receipt")
break
}
} else {
// No delivery status - this might be an older message
@@ -979,10 +1083,16 @@ class ChatViewModel: ObservableObject {
readerNickname: nickname
)
meshService.sendReadReceipt(receipt, to: peerID)
print("[Delivery] Sending read receipt for old message \(message.id) to current peer \(peerID)")
print("[Delivery] Sending read receipt for old message \(message.id) to current peer \(peerID)")
readReceiptsSent += 1
}
} else {
print("[Delivery] Skipping message \(message.id) - it's from us (\(nickname))")
}
}
print("[Delivery] Sent \(readReceiptsSent) read receipts to peer \(peerID)")
print("[Delivery] ========== markPrivateMessagesAsRead END ==========")
}
func getPrivateChatMessages(for peerID: String) -> [BitchatMessage] {
@@ -1837,10 +1947,56 @@ extension ChatViewModel: BitchatDelegate {
if let peerID = senderPeerID {
// Message from someone else
// First check if we need to migrate existing messages from this sender
let senderNickname = message.sender
if privateChats[peerID] == nil || privateChats[peerID]?.isEmpty == true {
// Check if we have messages from this nickname under a different peer ID
var migratedMessages: [BitchatMessage] = []
var oldPeerIDsToRemove: [String] = []
for (oldPeerID, messages) in privateChats {
if oldPeerID != peerID {
// Check if this chat contains messages with this sender
let isRelevantChat = messages.contains { msg in
(msg.sender == senderNickname && msg.sender != nickname) ||
(msg.sender == nickname && msg.recipientNickname == senderNickname)
}
if isRelevantChat {
print("[Delivery] Found existing chat with \(senderNickname) under old peer ID \(oldPeerID), migrating to \(peerID)")
migratedMessages.append(contentsOf: messages)
oldPeerIDsToRemove.append(oldPeerID)
}
}
}
// Remove old peer ID entries
for oldPeerID in oldPeerIDsToRemove {
privateChats.removeValue(forKey: oldPeerID)
unreadPrivateMessages.remove(oldPeerID)
}
// Initialize with migrated messages
privateChats[peerID] = migratedMessages
}
if privateChats[peerID] == nil {
privateChats[peerID] = []
}
privateChats[peerID]?.append(message)
// Fix delivery status for incoming messages
var messageToStore = message
if message.sender != nickname {
// This is an incoming message - it should NOT have "sending" status
if messageToStore.deliveryStatus == nil || messageToStore.deliveryStatus == .sending {
// Mark it as delivered since we received it
messageToStore.deliveryStatus = .delivered(to: nickname, at: Date())
print("[Delivery] Setting delivery status for incoming message \(message.id) to delivered (was: \(message.deliveryStatus?.displayText ?? "nil"))")
}
}
privateChats[peerID]?.append(messageToStore)
// Sort messages by timestamp to ensure proper ordering
privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
+18 -2
View File
@@ -431,7 +431,15 @@ struct ContentView: View {
LazyVStack(alignment: .leading, spacing: 2) {
let messages: [BitchatMessage] = {
if let privatePeer = viewModel.selectedPrivateChatPeer {
return viewModel.getPrivateChatMessages(for: privatePeer)
let msgs = viewModel.getPrivateChatMessages(for: privatePeer)
// Log what we're showing
if !msgs.isEmpty {
print("[UI] Showing \(msgs.count) messages in private chat with \(privatePeer)")
for (idx, msg) in msgs.enumerated() {
print("[UI] Message \(idx): from \(msg.sender), status: \(msg.deliveryStatus?.displayText ?? "none")")
}
}
return msgs
} else if let currentRoom = viewModel.currentRoom {
return viewModel.getRoomMessages(currentRoom)
} else {
@@ -543,8 +551,16 @@ struct ContentView: View {
// Also check when view appears
if let peerID = viewModel.selectedPrivateChatPeer {
print("[UI] Messages view appeared with selected peer \(peerID)")
// Try multiple times to ensure read receipts are sent
viewModel.markPrivateMessagesAsRead(from: peerID)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
print("[UI] Triggering markPrivateMessagesAsRead on appear for peer \(peerID)")
print("[UI] Triggering markPrivateMessagesAsRead on appear (0.1s) for peer \(peerID)")
viewModel.markPrivateMessagesAsRead(from: peerID)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
print("[UI] Triggering markPrivateMessagesAsRead on appear (0.5s) for peer \(peerID)")
viewModel.markPrivateMessagesAsRead(from: peerID)
}
}