mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 01:05:19 +00:00
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:
@@ -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
|
||||||
@@ -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).
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1581,7 +1581,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
recipientNickname: message.recipientNickname,
|
recipientNickname: message.recipientNickname,
|
||||||
senderPeerID: senderID,
|
senderPeerID: senderID,
|
||||||
mentions: message.mentions,
|
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
|
// Track last message time from this peer
|
||||||
|
|||||||
@@ -865,6 +865,11 @@ class ChatViewModel: ObservableObject {
|
|||||||
guard !content.isEmpty else { return }
|
guard !content.isEmpty else { return }
|
||||||
guard let recipientNickname = meshService.getPeerNicknames()[peerID] 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
|
// Create the message locally
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
sender: nickname,
|
sender: nickname,
|
||||||
@@ -897,22 +902,85 @@ class ChatViewModel: ObservableObject {
|
|||||||
|
|
||||||
func startPrivateChat(with peerID: String) {
|
func startPrivateChat(with peerID: String) {
|
||||||
print("[Delivery] Starting private chat with peer \(peerID)")
|
print("[Delivery] Starting private chat with peer \(peerID)")
|
||||||
|
let peerNickname = meshService.getPeerNicknames()[peerID] ?? "unknown"
|
||||||
|
print("[Delivery] My nickname: \(nickname), peer nickname: \(peerNickname)")
|
||||||
selectedPrivateChatPeer = peerID
|
selectedPrivateChatPeer = peerID
|
||||||
unreadPrivateMessages.remove(peerID)
|
unreadPrivateMessages.remove(peerID)
|
||||||
|
|
||||||
// Initialize chat history if needed
|
// Check if we need to migrate messages from an old peer ID
|
||||||
if privateChats[peerID] == nil {
|
// This happens when peer IDs change between sessions
|
||||||
privateChats[peerID] = []
|
if privateChats[peerID] == nil || privateChats[peerID]?.isEmpty == true {
|
||||||
print("[Delivery] Initialized empty chat history for peer \(peerID)")
|
print("[Delivery] No messages under current peer ID \(peerID), checking for messages from nickname \(peerNickname)")
|
||||||
} else {
|
|
||||||
print("[Delivery] Found existing chat history with \(privateChats[peerID]?.count ?? 0) messages for peer \(peerID)")
|
// 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
|
// Send read receipts for unread messages from this peer
|
||||||
// Add a small delay to ensure UI has updated
|
// Add a small delay to ensure UI has updated
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||||||
|
print("[Delivery] Delayed call to markPrivateMessagesAsRead for peer \(peerID)")
|
||||||
self?.markPrivateMessagesAsRead(from: peerID)
|
self?.markPrivateMessagesAsRead(from: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also try immediately in case messages are already there
|
||||||
|
markPrivateMessagesAsRead(from: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func endPrivateChat() {
|
func endPrivateChat() {
|
||||||
@@ -922,32 +990,69 @@ class ChatViewModel: ObservableObject {
|
|||||||
@objc private func appDidBecomeActive() {
|
@objc private func appDidBecomeActive() {
|
||||||
// When app becomes active, send read receipts for visible private chat
|
// When app becomes active, send read receipts for visible private chat
|
||||||
if let peerID = selectedPrivateChatPeer {
|
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) {
|
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)
|
self.markPrivateMessagesAsRead(from: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func markPrivateMessagesAsRead(from peerID: String) {
|
func markPrivateMessagesAsRead(from peerID: String) {
|
||||||
guard let messages = privateChats[peerID] else {
|
// Get the nickname for this peer
|
||||||
print("[Delivery] No messages found for peer \(peerID)")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the nickname for this peer
|
print("[Delivery] Processing \(messages.count) messages in chat with peer \(peerID) (\(peerNickname)) for read receipts")
|
||||||
let peerNickname = meshService.getPeerNicknames()[peerID] ?? ""
|
|
||||||
print("[Delivery] Checking \(messages.count) messages in chat with peer \(peerID) (\(peerNickname)) for read receipts")
|
|
||||||
|
|
||||||
// Find messages from the peer that haven't been read yet
|
// 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)
|
// 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
|
// This is a message FROM the peer if it's not from us AND (matches nickname OR peer ID OR is private to us)
|
||||||
// Match by sender nickname since peer IDs change between sessions
|
let isFromPeer = !isOurMessage && (isFromPeerByNickname || isFromPeerByID || isPrivateToUs)
|
||||||
let isFromPeer = message.sender == peerNickname || message.senderPeerID == peerID
|
|
||||||
if message.sender != nickname && isFromPeer {
|
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 {
|
if let status = message.deliveryStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case .sent, .delivered:
|
case .sent, .delivered:
|
||||||
@@ -959,15 +1064,14 @@ class ChatViewModel: ObservableObject {
|
|||||||
readerNickname: nickname
|
readerNickname: nickname
|
||||||
)
|
)
|
||||||
meshService.sendReadReceipt(receipt, to: peerID)
|
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:
|
case .read:
|
||||||
// Already read, no need to send another receipt
|
// Already read, no need to send another receipt
|
||||||
print("[Delivery] Message \(message.id) already marked as read")
|
print("[Delivery] Message \(message.id) already marked as read")
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
// Message not yet delivered, can't mark as read
|
// Message not yet delivered, can't mark as read
|
||||||
print("[Delivery] Message \(message.id) has status \(status.displayText), not sending read receipt")
|
print("[Delivery] Message \(message.id) has status \(status.displayText), not sending read receipt")
|
||||||
break
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No delivery status - this might be an older message
|
// No delivery status - this might be an older message
|
||||||
@@ -979,10 +1083,16 @@ class ChatViewModel: ObservableObject {
|
|||||||
readerNickname: nickname
|
readerNickname: nickname
|
||||||
)
|
)
|
||||||
meshService.sendReadReceipt(receipt, to: peerID)
|
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] {
|
func getPrivateChatMessages(for peerID: String) -> [BitchatMessage] {
|
||||||
@@ -1837,10 +1947,56 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
|
|
||||||
if let peerID = senderPeerID {
|
if let peerID = senderPeerID {
|
||||||
// Message from someone else
|
// 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 {
|
if privateChats[peerID] == nil {
|
||||||
privateChats[peerID] = []
|
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
|
// Sort messages by timestamp to ensure proper ordering
|
||||||
privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
|
privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
|
||||||
|
|
||||||
|
|||||||
@@ -431,7 +431,15 @@ struct ContentView: View {
|
|||||||
LazyVStack(alignment: .leading, spacing: 2) {
|
LazyVStack(alignment: .leading, spacing: 2) {
|
||||||
let messages: [BitchatMessage] = {
|
let messages: [BitchatMessage] = {
|
||||||
if let privatePeer = viewModel.selectedPrivateChatPeer {
|
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 {
|
} else if let currentRoom = viewModel.currentRoom {
|
||||||
return viewModel.getRoomMessages(currentRoom)
|
return viewModel.getRoomMessages(currentRoom)
|
||||||
} else {
|
} else {
|
||||||
@@ -543,8 +551,16 @@ struct ContentView: View {
|
|||||||
// Also check when view appears
|
// Also check when view appears
|
||||||
if let peerID = viewModel.selectedPrivateChatPeer {
|
if let peerID = viewModel.selectedPrivateChatPeer {
|
||||||
print("[UI] Messages view appeared with selected peer \(peerID)")
|
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) {
|
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)
|
viewModel.markPrivateMessagesAsRead(from: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user