mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 14:45:22 +00:00
DMs to unreachable peers use store-and-forward instead of failing instantly (#1415)
* DMs to unreachable peers route through store-and-forward; drop stale reachability gates
Field-found: sendPrivateMessage pre-judged reachability and marked the
message failed without ever calling the router, so the retained outbox,
courier deposits, and bridge drops never ran — a DM composed after a
peer's reachability window lapsed was dead on arrival while an identical
one sent a minute earlier delivered. Messages now always route; a live
path earns "sent", everything else stays "sending" until the router
reports carried/delivered or expires it as failed.
A sweep for sibling gates found and fixed:
- startPrivateChat refused offline non-mutual favorites ("mutual favorite
required for offline messaging") — store-and-forward only needs the
recipient's noise key, so the gate and its string are gone.
- markPrivateMessagesAsRead skipped the whole receipt pass for peers
without a stored Nostr key, starving mesh-connected non-favorites; the
router picks the transport now (sentReadReceipts dedups the parallel
PrivateChatManager path).
- DM/geoDM blocked notices and the unknown-group error posted to the
active public timeline; they now land in the thread they're about via
addLocalPrivateSystemMessage.
- Banned copy: literal "user" fallbacks in code become "anon" (the
default-nickname convention), and es/fr/it/pt translations of four keys
still saying usuario/utilisateur/utente/usuário now say person. Orphaned
keys removed (system.dm.unreachable, content.delivery.reason.unreachable,
system.chat.requires_favorite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Deflake BLEServiceCoreTests: longTimeout for positive waits
CI flaked on duplicatePacket_isDeduped: the first message landed after the
5s wait expired on a loaded runner (the test's later count==1 assertions
passed, proving late delivery, not lost delivery). All four positive waits
in the file now use TestConstants.longTimeout, which exists for exactly
this — waitUntil returns as soon as the condition holds, so passing runs
never pay the longer ceiling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix read-receipt test contamination; baseline Periphery noiseKey flake
Two CI failures, both environmental:
- markReadReceiptSent_returnsFalseOnSecondCall flaked because every test
ChatViewModel shared one per-process read-receipts scratch suite; the
lifecycle test persists the same "read-1" ID (a path the receipt-gate
removal now reaches), and test order decided who saw whose state. Each
instance now gets its own UUID-suffixed suite.
- Periphery intermittently misses the loadFromDisk read of
PrekeyBundleStore.StoredBundle.noiseKey and fails --strict with a false
"assign-only" finding (recurrence of a known flake). Its USR is
baselined; an in-source periphery:ignore can't work because strict mode
flags it as superfluous on runs where the indexer gets it right.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
jack
Claude Fable 5
parent
dc99d641c8
commit
55d51ba084
@@ -92,6 +92,9 @@ protocol ChatPrivateConversationContext: AnyObject {
|
||||
// MARK: System messages
|
||||
func addSystemMessage(_ content: String)
|
||||
func addMeshOnlySystemMessage(_ content: String)
|
||||
/// Appends a local-only system line into a specific private thread —
|
||||
/// errors about a DM belong in that DM, not on the active timeline.
|
||||
func addLocalPrivateSystemMessage(_ content: String, to peerID: PeerID)
|
||||
|
||||
// MARK: Favorites & notifications
|
||||
/// The persisted favorite relationship for the peer's Noise static key, if any.
|
||||
@@ -250,13 +253,14 @@ final class ChatPrivateConversationCoordinator {
|
||||
guard !content.isEmpty else { return }
|
||||
|
||||
if context.isPeerBlocked(peerID) {
|
||||
let nickname = context.peerNickname(for: peerID) ?? "user"
|
||||
context.addSystemMessage(
|
||||
let nickname = context.peerNickname(for: peerID) ?? "anon"
|
||||
context.addLocalPrivateSystemMessage(
|
||||
String(
|
||||
format: String(localized: "system.dm.blocked_recipient", comment: "System message when attempting to message a blocked user"),
|
||||
format: String(localized: "system.dm.blocked_recipient", comment: "System message when attempting to message a blocked person"),
|
||||
locale: .current,
|
||||
nickname
|
||||
)
|
||||
),
|
||||
to: peerID
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -278,11 +282,11 @@ final class ChatPrivateConversationCoordinator {
|
||||
let isMutualFavorite = favoriteStatus?.isMutual ?? false
|
||||
let hasNostrKey = favoriteStatus?.peerNostrPublicKey != nil
|
||||
|
||||
var recipientNickname = context.peerNickname(for: peerID)
|
||||
if recipientNickname == nil && favoriteStatus != nil {
|
||||
recipientNickname = favoriteStatus?.peerNickname
|
||||
}
|
||||
recipientNickname = recipientNickname ?? "user"
|
||||
// "anon" matches the app's default-nickname convention; "user" is
|
||||
// banned copy.
|
||||
let recipientNickname = context.peerNickname(for: peerID)
|
||||
?? favoriteStatus?.peerNickname
|
||||
?? "anon"
|
||||
|
||||
let messageID = UUID().uuidString
|
||||
let message = BitchatMessage(
|
||||
@@ -302,31 +306,25 @@ final class ChatPrivateConversationCoordinator {
|
||||
context.appendPrivateMessage(message, to: peerID)
|
||||
context.notifyUIChanged()
|
||||
|
||||
// Always hand the message to the router — it owns delivery. A live
|
||||
// link sends now; an unreachable peer gets the retained-outbox path
|
||||
// (resend on reconnect, courier deposits, bridge drops). Pre-judging
|
||||
// reachability here used to mark the message failed without ever
|
||||
// routing it, silently bypassing all of that (field-found: DMs
|
||||
// composed after a peer's reachability window lapsed were dead on
|
||||
// arrival while identical DMs sent a minute earlier delivered).
|
||||
context.routePrivateMessage(
|
||||
content,
|
||||
to: peerID,
|
||||
recipientNickname: recipientNickname,
|
||||
messageID: messageID
|
||||
)
|
||||
if isConnected || isReachable || (isMutualFavorite && hasNostrKey) {
|
||||
context.routePrivateMessage(
|
||||
content,
|
||||
to: peerID,
|
||||
recipientNickname: recipientNickname ?? "user",
|
||||
messageID: messageID
|
||||
)
|
||||
context.setPrivateDeliveryStatus(.sent, forMessageID: messageID, peerID: peerID)
|
||||
} else {
|
||||
context.setPrivateDeliveryStatus(
|
||||
.failed(
|
||||
reason: String(localized: "content.delivery.reason.unreachable", comment: "Failure reason when a peer is unreachable")
|
||||
),
|
||||
forMessageID: messageID,
|
||||
peerID: peerID
|
||||
)
|
||||
let name = recipientNickname ?? "user"
|
||||
context.addSystemMessage(
|
||||
String(
|
||||
format: String(localized: "system.dm.unreachable", comment: "System message when a recipient is unreachable"),
|
||||
locale: .current,
|
||||
name
|
||||
)
|
||||
)
|
||||
}
|
||||
// Otherwise the message stays "sending"; router callbacks move it to
|
||||
// carried (📦) when a courier/bridge copy ships, delivered/read on
|
||||
// acks, or failed when the outbox TTL expires.
|
||||
}
|
||||
|
||||
func sendGeohashDM(_ content: String, to peerID: PeerID) {
|
||||
@@ -372,8 +370,9 @@ final class ChatPrivateConversationCoordinator {
|
||||
forMessageID: messageID,
|
||||
peerID: peerID
|
||||
)
|
||||
context.addSystemMessage(
|
||||
String(localized: "system.dm.blocked_generic", comment: "System message when sending fails because user is blocked")
|
||||
context.addLocalPrivateSystemMessage(
|
||||
String(localized: "system.dm.blocked_generic", comment: "System message when sending fails because the person is blocked"),
|
||||
to: peerID
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user