mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 21:45:20 +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
@@ -1 +1 @@
|
||||
{"v1":{"usrs":["param-buf-arti_bootstrap_summary(_:_:)-s:3Tor22arti_bootstrap_summary33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSpys4Int8VG_AEtF","param-dataDir-arti_start(_:_:)-s:3Tor10arti_start33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSPys4Int8VG_s6UInt16VtF","param-len-arti_bootstrap_summary(_:_:)-s:3Tor22arti_bootstrap_summary33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSpys4Int8VG_AEtF","param-socksPort-arti_start(_:_:)-s:3Tor10arti_start33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSPys4Int8VG_s6UInt16VtF","s:13BitFoundation16PeerCapabilitiesV8wifiBulkACvpZ","s:13BitFoundation18KeychainReadResultO18isRecoverableErrorSbvp","s:13BitFoundation23KeychainManagerProtocolP11secureClearyySSzF","s:18bitchatTests_macOS12MockKeychainC11secureClearyySSzF","s:18bitchatTests_macOS20TrackingMockKeychainC11resetCountsyyF","s:18bitchatTests_macOS20TrackingMockKeychainC11secureClearyySSzF","s:18bitchatTests_macOS20TrackingMockKeychainC25totalSecureClearCallCountSivp","s:18bitchatTests_macOS20TrackingMockKeychainC26secureClearStringCallCountSivp","s:18bitchatTests_macOS20TrackingMockKeychainC27_secureClearStringCallCount06_AB6D1M24FD239F2969C82F4108818260LLSivp","s:18bitchatTests_macOS24FailingCacheSaveKeychain33_22380C7A11A569A0B83FA83F34C498A7LLC11secureClearyySSzF","s:18bitchatTests_macOS24MockGeohashPresenceTimer33_483587EFB96650EE130EFB09BBA2A1AALLC7handleryycvp","s:3Tor0A7ManagerC21goDormantOnBackgroundyyF","s:7bitchat10AppRuntimeC24handleScreenshotCaptured33_C8B369AD8BC1D9963A50CEDA77A4332ALLyyF","s:7bitchat10AppRuntimeC33handleDidBecomeActiveNotificationyyF","s:7bitchat10BLEServiceC18logBluetoothStatus33_69191C53E68500C17D98DBCF2BDA7100LLyySSF","s:7bitchat10BLEServiceC20centralRestorationID33_69191C53E68500C17D98DBCF2BDA7100LLSSvpZ","s:7bitchat10BLEServiceC22captureBluetoothStatus33_69191C53E68500C17D98DBCF2BDA7100LL7contextySS_tF","s:7bitchat10BLEServiceC23peripheralRestorationID33_69191C53E68500C17D98DBCF2BDA7100LLSSvpZ","s:7bitchat10BLEServiceC29scheduleBluetoothStatusSample33_69191C53E68500C17D98DBCF2BDA7100LL5after7contextySd_SStF","s:7bitchat10QRScanViewV8isActiveSbvp","s:7bitchat15BLEPeerRegistryV5countSivp","s:7bitchat15KeychainManagerC11secureClearyySSzF","s:7bitchat15PaymentChipViewV7openURL33_10AC50641B1EBCD52E5092A2E521D236LL7SwiftUI13OpenURLActionVvp","s:7bitchat15TransportConfigO29uiBatchDispatchStaggerSecondsSdvpZ","s:7bitchat15TransportConfigO35uiShareExtensionDismissDelaySecondsSdvpZ","s:7bitchat15TransportConfigO38bleBackgroundPendingConnectSlotReserveSivpZ","s:7bitchat17GossipSyncManagerC10persistNowyyF","s:7bitchat17NostrRelayManagerC15InboundEventKey33_E4160FE8A9A2C9D6308EAAD5A8B5CB07LLV7eventIDSSvp","s:7bitchat25LocationNotesDependenciesV3now10Foundation4DateVycvp","s:7bitchat25NWPathReachabilityMonitorC7monitor33_84633C9DBCAF57538179C1E04DB8E015LL7Network0bD0CSgvp"]}}
|
||||
{"v1":{"usrs":["param-buf-arti_bootstrap_summary(_:_:)-s:3Tor22arti_bootstrap_summary33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSpys4Int8VG_AEtF","param-dataDir-arti_start(_:_:)-s:3Tor10arti_start33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSPys4Int8VG_s6UInt16VtF","param-len-arti_bootstrap_summary(_:_:)-s:3Tor22arti_bootstrap_summary33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSpys4Int8VG_AEtF","param-socksPort-arti_start(_:_:)-s:3Tor10arti_start33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSPys4Int8VG_s6UInt16VtF","s:13BitFoundation16PeerCapabilitiesV8wifiBulkACvpZ","s:13BitFoundation18KeychainReadResultO18isRecoverableErrorSbvp","s:13BitFoundation23KeychainManagerProtocolP11secureClearyySSzF","s:18bitchatTests_macOS12MockKeychainC11secureClearyySSzF","s:18bitchatTests_macOS20TrackingMockKeychainC11resetCountsyyF","s:18bitchatTests_macOS20TrackingMockKeychainC11secureClearyySSzF","s:18bitchatTests_macOS20TrackingMockKeychainC25totalSecureClearCallCountSivp","s:18bitchatTests_macOS20TrackingMockKeychainC26secureClearStringCallCountSivp","s:18bitchatTests_macOS20TrackingMockKeychainC27_secureClearStringCallCount06_AB6D1M24FD239F2969C82F4108818260LLSivp","s:18bitchatTests_macOS24FailingCacheSaveKeychain33_22380C7A11A569A0B83FA83F34C498A7LLC11secureClearyySSzF","s:18bitchatTests_macOS24MockGeohashPresenceTimer33_483587EFB96650EE130EFB09BBA2A1AALLC7handleryycvp","s:3Tor0A7ManagerC21goDormantOnBackgroundyyF","s:7bitchat10AppRuntimeC24handleScreenshotCaptured33_C8B369AD8BC1D9963A50CEDA77A4332ALLyyF","s:7bitchat10AppRuntimeC33handleDidBecomeActiveNotificationyyF","s:7bitchat10BLEServiceC18logBluetoothStatus33_69191C53E68500C17D98DBCF2BDA7100LLyySSF","s:7bitchat10BLEServiceC20centralRestorationID33_69191C53E68500C17D98DBCF2BDA7100LLSSvpZ","s:7bitchat10BLEServiceC22captureBluetoothStatus33_69191C53E68500C17D98DBCF2BDA7100LL7contextySS_tF","s:7bitchat10BLEServiceC23peripheralRestorationID33_69191C53E68500C17D98DBCF2BDA7100LLSSvpZ","s:7bitchat10BLEServiceC29scheduleBluetoothStatusSample33_69191C53E68500C17D98DBCF2BDA7100LL5after7contextySd_SStF","s:7bitchat10QRScanViewV8isActiveSbvp","s:7bitchat15BLEPeerRegistryV5countSivp","s:7bitchat15KeychainManagerC11secureClearyySSzF","s:7bitchat15PaymentChipViewV7openURL33_10AC50641B1EBCD52E5092A2E521D236LL7SwiftUI13OpenURLActionVvp","s:7bitchat15TransportConfigO29uiBatchDispatchStaggerSecondsSdvpZ","s:7bitchat15TransportConfigO35uiShareExtensionDismissDelaySecondsSdvpZ","s:7bitchat15TransportConfigO38bleBackgroundPendingConnectSlotReserveSivpZ","s:7bitchat17GossipSyncManagerC10persistNowyyF","s:7bitchat17NostrRelayManagerC15InboundEventKey33_E4160FE8A9A2C9D6308EAAD5A8B5CB07LLV7eventIDSSvp","s:7bitchat17PrekeyBundleStoreC12StoredBundleV8noiseKey10Foundation4DataVvp","s:7bitchat25LocationNotesDependenciesV3now10Foundation4DateVycvp","s:7bitchat25NWPathReachabilityMonitorC7monitor33_84633C9DBCAF57538179C1E04DB8E015LL7Network0bD0CSgvp"]}}
|
||||
+13
-550
@@ -27498,7 +27498,7 @@
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "desbloquear a un usuario"
|
||||
"value" : "desbloquear a una persona"
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
@@ -30155,185 +30155,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.delivery.reason.unreachable" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "القرين غير متاح"
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "পিয়ার পৌঁছানো যাচ্ছে না"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "peer nicht erreichbar"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "peer not reachable"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "el destinatario no es alcanzable"
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "hindi maabot ang peer"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "pair injoignable"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "עמית לא זמין"
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "पीयर पहुंच योग्य नहीं"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "peer tidak dapat dijangkau"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "peer irraggiungibile"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ピアに到達できません"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "피어에 연결할 수 없습니다"
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "peer tidak dapat dijangkau"
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "पीयर पहुँचयोग्य छैन"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "peer onbereikbaar"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "peer nieosiągalny"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "par inacessível"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "par inalcançável"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "пир недостижим"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "peer ej nåbar"
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "peer அணுக முடியவில்லை"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ติดต่อเพียร์ไม่ได้"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "eşe ulaşılamıyor"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "пір недосяжний"
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "peer تک رسائی نہیں"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "không liên lạc được với nút"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "同伴不可达"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "同伴不可達"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.delivery.reason.voice_send_failed" : {
|
||||
"comment" : "Failure reason shown when a voice note could not be sent",
|
||||
"extractionState" : "manual",
|
||||
@@ -58664,7 +58485,7 @@
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "no se puede iniciar un chat con %@: el usuario está bloqueado."
|
||||
"value" : "no se puede iniciar un chat con %@: esta persona está bloqueada."
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
@@ -58676,7 +58497,7 @@
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "impossible de démarrer un chat avec %@ : utilisateur bloqué."
|
||||
"value" : "impossible de démarrer un chat avec %@ : cette personne est bloquée."
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
@@ -58700,7 +58521,7 @@
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "impossibile avviare una chat con %@: utente bloccato."
|
||||
"value" : "impossibile avviare una chat con %@: questa persona è bloccata."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
@@ -58748,7 +58569,7 @@
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "não é possível iniciar chat com %@: usuário bloqueado."
|
||||
"value" : "não é possível iniciar chat com %@: a pessoa está bloqueada."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
@@ -58813,185 +58634,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"system.chat.requires_favorite" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "لا يمكن بدء دردشة مع %@: يجب أن تكونا مفضلين متبادلين للتشغيل بدون اتصال."
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@-এর সঙ্গে চ্যাট শুরু করা যায় না: অফলাইন মেসেজিংয়ের জন্য পারস্পরিক প্রিয় দরকার।"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "chat mit %@ kann nicht gestartet werden: gegenseitige favoriten für offline nötig."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "cannot start chat with %@: mutual favorite required for offline messaging."
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "no se puede iniciar un chat con %@: necesitas ser favoritos mutuos para mensajería sin conexión."
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "hindi makapagsimula ng chat kay %@: kailangan ang mutual na paborito para sa offline na mensahe."
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "impossible de démarrer un chat avec %@ : favoris mutuels requis pour le hors ligne."
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "לא ניתן להתחיל צ'אט עם %@: נדרשים מועדפים הדדיים לאופליין."
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ के साथ चैट शुरू नहीं कर सकते: ऑफ़लाइन मैसेजिंग के लिए आपसी पसंदीदा आवश्यक है।"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "tidak bisa mulai chat dengan %@: butuh favorit bersama untuk offline."
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "impossibile avviare una chat con %@: servono preferiti reciproci per l'offline."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@とはチャットできません: オフラインには相互のお気に入りが必要です。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@와 채팅을 시작할 수 없습니다: 오프라인 메시지를 보내려면 서로 즐겨찾기에 추가해야 합니다."
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "tidak bisa mulai chat dengan %@: butuh favorit bersama untuk offline."
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ सँग च्याट सुरु गर्न मिलेन: अफलाइनका लागि दुवै मनपर्ने हुनुपर्छ"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "kan chat met %@ niet starten: wederzijds favoriet vereist voor offline berichten."
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "nie można rozpocząć czatu z %@: potrzebne wzajemne ulubione dla wiadomości offline."
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "não é possível iniciar o chat com %@: precisam de ser favoritos mútuos para mensagens offline."
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "não é possível iniciar chat com %@: vocês precisam ser favoritos mútuos para mensagens offline."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "нельзя начать чат с %@: нужны взаимные избранные для офлайна."
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "kan inte starta chatt med %@: ömsesidig favorit krävs för offline-meddelanden."
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ உடன் உரையாடல் தொடங்க முடியாது: ஆஃப்லைன் செய்திகளுக்கு இருபுறமும் பிரியப்பட்டவர்கள் ஆக வேண்டும்."
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ไม่สามารถเริ่มแชทกับ %@: ต้องเป็นรายการโปรดทั้งสองฝ่ายเพื่อใช้งานออฟไลน์"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ ile sohbet başlatılamıyor: çevrimdışı mesajlaşma için karşılıklı favori gerekiyor."
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "не можна почати чат з %@: потрібне взаємне вибране для офлайна."
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ کے ساتھ چیٹ شروع نہیں کر سکتے: آفلائن پیغامات کیلئے باہمی پسندیدہ ہونا ضروری ہے۔"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "không thể bắt đầu chat với %@: cần yêu thích lẫn nhau để nhắn ngoại tuyến."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "无法与 %@ 开始聊天:离线消息需要互相关注。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "無法與 %@ 開始聊天:離線訊息需要互相關注。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"system.dm.blocked_generic" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -59022,7 +58664,7 @@
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "no se puede enviar el mensaje: el usuario está bloqueado."
|
||||
"value" : "no se puede enviar el mensaje: esta persona está bloqueada."
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
@@ -59034,7 +58676,7 @@
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "envoi impossible : utilisateur bloqué."
|
||||
"value" : "envoi impossible : cette personne est bloquée."
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
@@ -59058,7 +58700,7 @@
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "invio non riuscito: utente bloccato."
|
||||
"value" : "invio non riuscito: questa persona è bloccata."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
@@ -59106,7 +58748,7 @@
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "não foi possível enviar: usuário bloqueado."
|
||||
"value" : "não foi possível enviar: a pessoa está bloqueada."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
@@ -59201,7 +58843,7 @@
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "no se puede enviar un mensaje a %@: el usuario está bloqueado."
|
||||
"value" : "no se puede enviar un mensaje a %@: esta persona está bloqueada."
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
@@ -59213,7 +58855,7 @@
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "impossible d'envoyer à %@ : utilisateur bloqué."
|
||||
"value" : "impossible d'envoyer à %@ : cette personne est bloquée."
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
@@ -59237,7 +58879,7 @@
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "impossibile inviare a %@: utente bloccato."
|
||||
"value" : "impossibile inviare a %@: questa persona è bloccata."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
@@ -59285,7 +58927,7 @@
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "não é possível enviar mensagem para %@: usuário bloqueado."
|
||||
"value" : "não é possível enviar mensagem para %@: a pessoa está bloqueada."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
@@ -59350,185 +58992,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"system.dm.unreachable" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "لا يمكن الإرسال إلى %@: المستلم غير متاح عبر mesh أو nostr."
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@-কে বার্তা পাঠানো যায় না - পিয়ার মেশ বা নোস্টরে পৌঁছানো যাচ্ছে না।"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "senden an %@ nicht möglich: empfänger über mesh oder nostr nicht erreichbar."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "cannot send message to %@ - peer is not reachable via mesh or nostr."
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "no se puede enviar un mensaje a %@: el destinatario no es alcanzable por mesh ni Nostr."
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "hindi maipadala kay %@ - hindi maabot ang peer sa mesh o nostr."
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "impossible d'envoyer à %@ : destinataire injoignable via mesh ou nostr."
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "אי אפשר לשלוח ל-%@: הנמען אינו נגיש דרך mesh או nostr."
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ को संदेश नहीं भेज सकते - पीयर मेश या नोस्ट्र पर पहुँच योग्य नहीं।"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "tidak bisa mengirim ke %@: penerima tidak dapat dijangkau lewat mesh atau nostr."
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "impossibile inviare a %@: destinatario irraggiungibile via mesh o nostr."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@に送れません: 受信者はmeshやnostrで到達できません。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@에게 메시지를 보낼 수 없습니다 - mesh 또는 nostr를 통해 피어에 연결할 수 없습니다."
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "tidak bisa menghantar ke %@: penerima tidak dapat dijangkau lewat mesh atau nostr."
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ लाई पठाउन मिलेन: प्राप्तकर्ता mesh वा nostr बाट उपलब्ध छैन"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "kan geen bericht sturen naar %@ – peer niet bereikbaar via mesh of nostr."
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "nie można wysłać do %@ – peer nieosiągalny przez mesh ani nostr."
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "não é possível enviar mensagem a %@ - o par não está acessível por mesh ou Nostr."
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "não é possível enviar mensagem para %@: destinatário inalcançável por mesh ou nostr."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "нельзя отправить %@: адресат недоступен через mesh или nostr."
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "kan inte skicka till %@ – peer nås inte via mesh eller nostr."
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ க்கு செய்தி அனுப்ப முடியவில்லை - peer mesh அல்லது nostr மூலமாக அணுக முடியவில்லை."
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ไม่สามารถส่งข้อความถึง %@ - ติดต่อเพียร์ผ่าน mesh หรือ nostr ไม่ได้"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@'a mesaj gönderilemiyor - eş mesh veya Nostr üzerinden ulaşılamıyor."
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "неможливо надіслати %@: одержувач недосяжний через mesh або nostr."
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ کو پیغام نہیں بھیج سکتے - peer mesh یا nostr کے ذریعے دستیاب نہیں۔"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "không thể gửi tin cho %@ - nút không thể liên lạc qua mesh hoặc nostr."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "无法向 %@ 发送:对方无法通过 mesh 或 Nostr 到达。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "無法向 %@ 發送:對方無法透過 mesh 或 Nostr 到達。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"system.gateway.sent_via_mesh" : {
|
||||
"comment" : "System message when a geohash message was handed to a mesh internet gateway because no relay is reachable",
|
||||
"extractionState" : "manual",
|
||||
|
||||
@@ -22,6 +22,11 @@ import Foundation
|
||||
/// prekey-seal for recipients met long ago. Included in the panic wipe.
|
||||
final class PrekeyBundleStore {
|
||||
struct StoredBundle: Codable {
|
||||
// noiseKey is read in loadFromDisk (dictionary keying), but the
|
||||
// Periphery indexer intermittently misses that read and flakes CI
|
||||
// with "assign-only" — its USR is baselined (an in-source ignore
|
||||
// can't work: strict mode flags it as superfluous on the runs where
|
||||
// the indexer gets it right).
|
||||
let noiseKey: Data
|
||||
var generatedAt: UInt64
|
||||
var prekeyIDs: [UInt32]
|
||||
|
||||
@@ -336,7 +336,12 @@ final class ChatGroupCoordinator {
|
||||
guard !content.isEmpty, content.count <= InputValidator.Limits.maxMessageLength else { return }
|
||||
guard let group = context.groupStore.group(for: groupPeerID),
|
||||
let key = context.groupStore.key(forGroupID: group.groupID) else {
|
||||
context.addSystemMessage(String(localized: "system.group.unknown", comment: "System message when sending into an unknown group"))
|
||||
// The person is inside the group thread; the error belongs there,
|
||||
// not on the active public timeline.
|
||||
context.addLocalPrivateSystemMessage(
|
||||
String(localized: "system.group.unknown", comment: "System message when sending into an unknown group"),
|
||||
to: groupPeerID
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -231,7 +231,10 @@ final class ChatLifecycleCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
guard peerNostrPubkey != nil else { return }
|
||||
// No Nostr-key gate here: the router picks whatever transport can
|
||||
// reach the peer (mesh included), so read receipts must flow for
|
||||
// non-favorite mesh peers too. `sentReadReceipts` dedups against the
|
||||
// PrivateChatManager path; the router drops receipts it can't route.
|
||||
|
||||
for message in getPrivateChatMessages(for: peerID) {
|
||||
guard (message.senderPeerID == peerID || message.senderPeerID == noiseKeyHex) && !message.isRelay else {
|
||||
|
||||
@@ -347,20 +347,10 @@ final class ChatPeerIdentityCoordinator {
|
||||
return
|
||||
}
|
||||
|
||||
if let peer = context.unifiedPeer(for: peerID),
|
||||
peer.isFavorite && !peer.theyFavoritedUs && !peer.isConnected {
|
||||
context.addSystemMessage(
|
||||
String(
|
||||
format: String(
|
||||
localized: "system.chat.requires_favorite",
|
||||
comment: "System message when mutual favorite requirement blocks chat"
|
||||
),
|
||||
locale: .current,
|
||||
peerNickname
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
// No mutual-favorite gate: store-and-forward (couriers, bridge drops,
|
||||
// retained outbox) only needs the recipient's noise key, so an
|
||||
// offline non-mutual favorite is still worth writing to — the router
|
||||
// decides what delivery looks like, not chat entry.
|
||||
|
||||
_ = context.consolidatePrivateMessages(for: peerID, peerNickname: peerNickname)
|
||||
|
||||
@@ -553,7 +543,8 @@ final class ChatPeerIdentityCoordinator {
|
||||
!favorite.peerNickname.isEmpty {
|
||||
return favorite.peerNickname
|
||||
}
|
||||
return "user"
|
||||
// "anon" matches the default-nickname convention; "user" is banned copy.
|
||||
return "anon"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -398,16 +398,18 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
let readReceiptsDefaults: UserDefaults
|
||||
|
||||
/// Default read-receipt persistence store. Production uses `.standard`.
|
||||
/// Under test, a dedicated scratch suite is used instead — wiped at first
|
||||
/// use per process — so back-to-back local test runs never see each
|
||||
/// other's persisted receipts (and tests never pollute `.standard`).
|
||||
static let defaultReadReceiptsDefaults: UserDefaults = {
|
||||
/// Under test, every instance gets its own scratch suite: a per-process
|
||||
/// shared suite let one test's persisted receipts leak into another
|
||||
/// test's freshly constructed view model (surfaced as an order-dependent
|
||||
/// CI flake on a duplicated message ID), and tests never pollute
|
||||
/// `.standard`.
|
||||
static func defaultReadReceiptsDefaults() -> UserDefaults {
|
||||
guard TestEnvironment.isRunningTests else { return .standard }
|
||||
let suiteName = "chat.bitchat.tests.readReceipts"
|
||||
let suiteName = "chat.bitchat.tests.readReceipts.\(UUID().uuidString)"
|
||||
guard let scratch = UserDefaults(suiteName: suiteName) else { return .standard }
|
||||
scratch.removePersistentDomain(forName: suiteName)
|
||||
return scratch
|
||||
}()
|
||||
}
|
||||
|
||||
// Track sent read receipts to avoid duplicates (persisted across launches)
|
||||
// Note: Persistence happens automatically in didSet, no lifecycle observers needed
|
||||
@@ -809,7 +811,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
self.autocompleteService = services.autocompleteService
|
||||
self.deduplicationService = services.deduplicationService
|
||||
self.publicMessagePipeline = services.publicMessagePipeline
|
||||
let readReceiptsDefaults = readReceiptsDefaults ?? Self.defaultReadReceiptsDefaults
|
||||
let readReceiptsDefaults = readReceiptsDefaults ?? Self.defaultReadReceiptsDefaults()
|
||||
self.readReceiptsDefaults = readReceiptsDefaults
|
||||
self.sentReadReceipts = ChatViewModelBootstrapper.loadPersistedReadReceipts(userDefaults: readReceiptsDefaults)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ struct BLEServiceCoreTests {
|
||||
ble._test_handlePacket(packet, fromPeerID: sender, signingPublicKey: signingKey)
|
||||
let receivedFirst = await TestHelpers.waitUntil(
|
||||
{ delegate.publicMessagesSnapshot().count == 1 },
|
||||
timeout: TestConstants.defaultTimeout
|
||||
timeout: TestConstants.longTimeout
|
||||
)
|
||||
#expect(receivedFirst)
|
||||
|
||||
@@ -188,7 +188,7 @@ struct BLEServiceCoreTests {
|
||||
|
||||
let rebound = await TestHelpers.waitUntil(
|
||||
{ ble._test_centralBinding(centralUUID) == newPeerID },
|
||||
timeout: TestConstants.defaultTimeout
|
||||
timeout: TestConstants.longTimeout
|
||||
)
|
||||
#expect(rebound)
|
||||
|
||||
@@ -197,7 +197,7 @@ struct BLEServiceCoreTests {
|
||||
let peerIDs = ble.currentPeerSnapshots().map(\.peerID)
|
||||
return peerIDs.contains(newPeerID) && !peerIDs.contains(oldPeerID)
|
||||
},
|
||||
timeout: TestConstants.defaultTimeout
|
||||
timeout: TestConstants.longTimeout
|
||||
)
|
||||
#expect(retired)
|
||||
}
|
||||
@@ -374,7 +374,7 @@ struct BLEServiceCoreTests {
|
||||
|
||||
let reachedBudget = await TestHelpers.waitUntil(
|
||||
{ outbound.count(ofType: .pong) >= budget },
|
||||
timeout: TestConstants.defaultTimeout
|
||||
timeout: TestConstants.longTimeout
|
||||
)
|
||||
#expect(reachedBudget)
|
||||
// Give any over-budget pong a chance to surface, then confirm the
|
||||
|
||||
@@ -328,7 +328,7 @@ struct ChatLifecycleCoordinatorContextTests {
|
||||
#expect(context.ownerLevelReadPasses == [peerID])
|
||||
}
|
||||
@Test @MainActor
|
||||
func markPrivateMessagesAsRead_routesReceiptsOnlyForNostrReachableFavorites() {
|
||||
func markPrivateMessagesAsRead_routesReceiptsForFavoritesAndNonFavorites() {
|
||||
let context = MockChatLifecycleContext()
|
||||
let coordinator = ChatLifecycleCoordinator(context: context)
|
||||
let noiseKey = Data(repeating: 0xAB, count: 32)
|
||||
@@ -351,12 +351,15 @@ struct ChatLifecycleCoordinatorContextTests {
|
||||
#expect(context.routedReadReceipts.map(\.peerID) == [peerID])
|
||||
#expect(context.sentReadReceipts.contains("in-1"))
|
||||
|
||||
// No favorite relationship (no Nostr key): the receipt pass is skipped.
|
||||
// No favorite relationship: receipts still route — the router picks
|
||||
// whatever transport can reach the peer (mesh included). Gating on a
|
||||
// stored Nostr key silently starved mesh-connected non-favorites.
|
||||
let otherKey = Data(repeating: 0xCD, count: 32)
|
||||
let otherPeer = PeerID(hexData: otherKey)
|
||||
context.privateChats[otherPeer] = [makePrivateMessage(id: "in-2", senderPeerID: otherPeer)]
|
||||
coordinator.markPrivateMessagesAsRead(from: otherPeer)
|
||||
#expect(context.routedReadReceipts.map(\.messageID) == ["in-1"])
|
||||
#expect(context.routedReadReceipts.map(\.messageID) == ["in-1", "in-2"])
|
||||
#expect(context.sentReadReceipts.contains("in-2"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -234,6 +234,12 @@ private final class MockChatPrivateConversationContext: ChatPrivateConversationC
|
||||
meshOnlySystemMessages.append(content)
|
||||
}
|
||||
|
||||
private(set) var privateSystemMessages: [(content: String, peerID: PeerID)] = []
|
||||
|
||||
func addLocalPrivateSystemMessage(_ content: String, to peerID: PeerID) {
|
||||
privateSystemMessages.append((content, peerID))
|
||||
}
|
||||
|
||||
static let dummyIdentity = NostrIdentity(
|
||||
privateKey: Data(repeating: 0x11, count: 32),
|
||||
publicKey: Data(repeating: 0x22, count: 32),
|
||||
@@ -680,19 +686,21 @@ struct ChatPrivateConversationCoordinatorContextTests {
|
||||
#expect(context.systemMessages.isEmpty)
|
||||
}
|
||||
|
||||
/// Field-found: pre-judging reachability here marked the message failed
|
||||
/// without ever routing it, so the router's retained outbox, courier
|
||||
/// deposits, and bridge drops never got a chance. A fully unreachable
|
||||
/// non-favorite must still be routed and stay "sending" (the router's
|
||||
/// callbacks later move it to carried/delivered or expire it as failed).
|
||||
@Test @MainActor
|
||||
func sendPrivateMessage_failsWhenOfflineWithoutMutualFavorite() async {
|
||||
func sendPrivateMessage_routesAndStaysSendingWhenOfflineWithoutMutualFavorite() async {
|
||||
let context = MockChatPrivateConversationContext()
|
||||
let coordinator = ChatPrivateConversationCoordinator(context: context)
|
||||
let peerID = PeerID(hexData: Data(repeating: 0xCD, count: 32))
|
||||
|
||||
coordinator.sendPrivateMessage("hello?", to: peerID)
|
||||
|
||||
#expect(context.routedPrivateMessages.isEmpty)
|
||||
#expect(context.systemMessages.count == 1)
|
||||
guard case .failed = context.privateChats[peerID]?.first?.deliveryStatus else {
|
||||
Issue.record("expected .failed delivery status")
|
||||
return
|
||||
}
|
||||
#expect(context.routedPrivateMessages.map(\.content) == ["hello?"])
|
||||
#expect(context.privateChats[peerID]?.first?.deliveryStatus == .sending)
|
||||
#expect(context.systemMessages.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,11 @@ struct ChatViewModelPrivateChatExtensionTests {
|
||||
// Check MockTransport implementation... it might need update or verification
|
||||
}
|
||||
|
||||
/// An unreachable recipient no longer means instant failure: the message
|
||||
/// is routed anyway so the router's outbox/courier/bridge machinery can
|
||||
/// deliver it, and it stays "sending" until a router callback resolves it.
|
||||
@Test @MainActor
|
||||
func sendPrivateMessage_unreachable_setsFailedStatus() async {
|
||||
func sendPrivateMessage_unreachable_staysSendingForStoreAndForward() async {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
let validHex = "0102030405060708090a0b0c0d0e0f100102030405060708090a0b0c0d0e0f10"
|
||||
let peerID = PeerID(str: validHex)
|
||||
@@ -80,11 +83,7 @@ struct ChatViewModelPrivateChatExtensionTests {
|
||||
viewModel.sendPrivateMessage("Hello", to: peerID)
|
||||
|
||||
#expect(viewModel.privateChats[peerID]?.count == 1)
|
||||
let status = viewModel.privateChats[peerID]?.last?.deliveryStatus
|
||||
#expect({
|
||||
if case .failed = status { return true }
|
||||
return false
|
||||
}())
|
||||
#expect(viewModel.privateChats[peerID]?.last?.deliveryStatus == .sending)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
@@ -780,8 +779,10 @@ struct ChatViewModelGeoDMTests {
|
||||
#expect(isFailed(status: viewModel.privateChats[convKey]?.last?.deliveryStatus))
|
||||
}
|
||||
|
||||
/// The blocked notice belongs in the DM thread the person is typing in,
|
||||
/// not on the active location-channel timeline.
|
||||
@Test @MainActor
|
||||
func sendGeohashDM_blockedRecipient_marksFailedAndAddsSystemMessage() async {
|
||||
func sendGeohashDM_blockedRecipient_marksFailedAndAddsSystemMessageInThread() async {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
let geohash = "u4pruydq"
|
||||
let recipientHex = "0000000000000000000000000000000000000000000000000000000000000003"
|
||||
@@ -793,9 +794,11 @@ struct ChatViewModelGeoDMTests {
|
||||
|
||||
viewModel.sendGeohashDM("hello", to: convKey)
|
||||
|
||||
#expect(viewModel.privateChats[convKey]?.count == 1)
|
||||
#expect(isFailed(status: viewModel.privateChats[convKey]?.last?.deliveryStatus))
|
||||
#expect(viewModel.messages.contains(where: { $0.sender == "system" }))
|
||||
let thread = viewModel.privateChats[convKey] ?? []
|
||||
#expect(thread.count == 2)
|
||||
#expect(isFailed(status: thread.first?.deliveryStatus))
|
||||
#expect(thread.last?.sender == "system")
|
||||
#expect(!viewModel.messages.contains(where: { $0.sender == "system" }))
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
|
||||
Reference in New Issue
Block a user