Merge 9c8db0873ee1c987e30123964c11331d0005e23c into 681c1800602e8bafd6b76b6b3dcb4538e531d577

This commit is contained in:
heyaim 2026-08-01 11:05:34 +00:00 committed by GitHub
commit 3871156e58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 96 additions and 4 deletions

View File

@ -548,8 +548,14 @@ final class ChatPrivateConversationCoordinator {
let isViewing = context.selectedPrivateChatPeer == conversationPeerID
let wasReadBefore = context.sentReadReceipts.contains(messageId)
// Recency gates only the notification. A DM sent while the app was
// closed reaches this path through the gift-wrap lookback long after
// it was sent, so gating the badge on recency left the conversation
// looking read and the message findable only by opening that chat.
// The mesh overload below marks unread whenever the chat is not on
// screen, with no recency test at all.
let isRecentMessage = Date().timeIntervalSince(messageTimestamp) < 30
let shouldMarkUnread = !wasReadBefore && !isViewing && isRecentMessage
let shouldMarkUnread = !wasReadBefore && !isViewing
if shouldMarkUnread {
context.markPrivateChatUnread(conversationPeerID)
}
@ -558,7 +564,7 @@ final class ChatPrivateConversationCoordinator {
sendReadReceiptIfNeeded(to: messageId, senderPubKey: senderPubkey, from: id)
}
if !isViewing && shouldMarkUnread {
if shouldMarkUnread && isRecentMessage {
context.notifyPrivateMessage(from: senderName, message: pm.content, peerID: conversationPeerID)
}

View File

@ -460,7 +460,8 @@ struct ChatPrivateConversationCoordinatorContextTests {
context.displayNamesByPubkey[senderPubkey] = "bob#5678"
let payloadData = PrivateMessagePacket(messageID: "geo-1", content: "hi there").encode()!
let payload = NoisePayload(type: .privateMessage, data: payloadData)
// Old timestamp: not "recent", so no unread marking (and no notification).
// An old timestamp suppresses the notification but not the unread
// badge: recency gates only the alert.
let oldTimestamp = Date().addingTimeInterval(-120)
coordinator.handlePrivateMessage(
@ -476,7 +477,7 @@ struct ChatPrivateConversationCoordinatorContextTests {
#expect(context.sentGeoDeliveryAcks == ["geo-1"])
#expect(context.privateChats[convKey]?.map(\.id) == ["geo-1"])
#expect(context.privateChats[convKey]?.first?.sender == "bob#5678")
#expect(context.unreadPrivateMessages.isEmpty)
#expect(context.unreadPrivateMessages == [convKey])
#expect(context.notifyUIChangedCount == 1)
// Redelivery: ack is deduplicated and the message is not appended twice.
@ -491,6 +492,91 @@ struct ChatPrivateConversationCoordinatorContextTests {
#expect(context.privateChats[convKey]?.count == 1)
}
/// A DM sent while the app was closed reaches this path through the
/// gift-wrap lookback, so it is minutes or hours old by the time it is
/// handled. The conversation still has to show as unread or the message
/// is unfindable; only the notification is gated on recency.
@Test @MainActor
func geoPrivateMessage_marksUnreadForAnOldMessageWithoutNotifying() async {
let context = MockChatPrivateConversationContext()
let coordinator = ChatPrivateConversationCoordinator(context: context)
let convKey = PeerID(str: "nostr_abcdef12")
let senderPubkey = "feedface00112233"
context.displayNamesByPubkey[senderPubkey] = "bob#5678"
let payloadData = PrivateMessagePacket(
messageID: "offline-1",
content: "sent while you were away"
).encode()!
let payload = NoisePayload(type: .privateMessage, data: payloadData)
coordinator.handlePrivateMessage(
payload,
senderPubkey: senderPubkey,
convKey: convKey,
id: MockChatPrivateConversationContext.dummyIdentity,
messageTimestamp: Date().addingTimeInterval(-3600)
)
#expect(context.privateChats[convKey]?.map(\.id) == ["offline-1"])
#expect(context.unreadPrivateMessages == [convKey])
#expect(context.privateMessageNotifications.isEmpty)
}
/// The recency window still does its job: a message that arrives while
/// the person is elsewhere in the app both badges and notifies.
@Test @MainActor
func geoPrivateMessage_notifiesForARecentMessage() async {
let context = MockChatPrivateConversationContext()
let coordinator = ChatPrivateConversationCoordinator(context: context)
let convKey = PeerID(str: "nostr_abcdef12")
let senderPubkey = "feedface00112233"
context.displayNamesByPubkey[senderPubkey] = "bob#5678"
let payloadData = PrivateMessagePacket(
messageID: "fresh-1",
content: "just sent"
).encode()!
let payload = NoisePayload(type: .privateMessage, data: payloadData)
coordinator.handlePrivateMessage(
payload,
senderPubkey: senderPubkey,
convKey: convKey,
id: MockChatPrivateConversationContext.dummyIdentity,
messageTimestamp: Date()
)
#expect(context.unreadPrivateMessages == [convKey])
#expect(context.privateMessageNotifications.map(\.peerID) == [convKey])
}
/// Opening the conversation is what clears it: a message that arrives
/// while the chat is on screen is neither badged nor notified.
@Test @MainActor
func geoPrivateMessage_doesNotMarkUnreadWhileTheChatIsOpen() async {
let context = MockChatPrivateConversationContext()
let coordinator = ChatPrivateConversationCoordinator(context: context)
let convKey = PeerID(str: "nostr_abcdef12")
let senderPubkey = "feedface00112233"
context.displayNamesByPubkey[senderPubkey] = "bob#5678"
context.selectedPrivateChatPeer = convKey
let payloadData = PrivateMessagePacket(
messageID: "viewing-1",
content: "you are looking at this"
).encode()!
let payload = NoisePayload(type: .privateMessage, data: payloadData)
coordinator.handlePrivateMessage(
payload,
senderPubkey: senderPubkey,
convKey: convKey,
id: MockChatPrivateConversationContext.dummyIdentity,
messageTimestamp: Date()
)
#expect(context.unreadPrivateMessages.isEmpty)
#expect(context.privateMessageNotifications.isEmpty)
}
@Test @MainActor
func accountDM_handsOpenShortIDConversationToStableWhenOffline() async {
let context = MockChatPrivateConversationContext()