Skip to content

Commit 9bae337

Browse files
committed
fix(android): treat saved contacts as saved peers for notifications
When "Received Message" is disabled but "Message from Saved Peer" is enabled, no notification was posted for messages from saved peers saved via "Save to Contacts" (Chats long-press, Messaging star), QR import, or manual entry. MessageCollector resolved the saved-peer flag exclusively from the legacy announces.isFavorite column, but only the announce-stream star path sets that column. The other save paths write to the contacts table via ContactRepository and never touch the flag, so isFavorite reached NotificationHelper as false and the general-toggle-off gate silently dropped the notification. Resolve saved-peer status from both signals: the legacy announce favorite flag OR membership in the active identity's contacts table. Lookup failures keep the existing fail-safe default. Regression test: MessageCollectorTest covers a contacts-table-only saved peer (announce isFavorite = false, hasContact = true) and asserts notifyMessageReceived is called with isFavorite = true.
1 parent c179b0b commit 9bae337

2 files changed

Lines changed: 80 additions & 14 deletions

File tree

app/src/main/java/network/columba/app/service/MessageCollector.kt

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,7 @@ class MessageCollector
143143
}
144144
}
145145

146-
val isFavorite =
147-
try {
148-
announceRepository.getAnnounce(sourceHash)?.isFavorite ?: false
149-
} catch (e: Exception) {
150-
Log.w(TAG, "Could not check if peer is favorite", e)
151-
false
152-
}
146+
val isFavorite = isSavedPeer(sourceHash)
153147

154148
// Only notify if the message hasn't been read yet
155149
// This prevents duplicate notifications after service restart
@@ -274,13 +268,7 @@ class MessageCollector
274268
Log.d(TAG, "Message saved to database for peer ${sourceHash.take(16)} (hasPublicKey=${publicKey != null})")
275269

276270
// Check if sender is a saved peer (favorite)
277-
val isFavorite =
278-
try {
279-
announceRepository.getAnnounce(sourceHash)?.isFavorite ?: false
280-
} catch (e: Exception) {
281-
Log.w(TAG, "Could not check if peer is favorite", e)
282-
false
283-
}
271+
val isFavorite = isSavedPeer(sourceHash)
284272

285273
// Show notification for received message
286274
try {
@@ -458,6 +446,39 @@ class MessageCollector
458446
return PeerNameResolver.formatHashAsFallback(peerHash)
459447
}
460448

449+
/**
450+
* Determine whether [sourceHash] is a "saved peer" for notification purposes.
451+
*
452+
* A peer is considered saved if it is either:
453+
* - flagged as a favorite on its announce row (legacy "Save Peer" via the
454+
* announce stream star), OR
455+
* - present in the active identity's contacts table (written by the
456+
* "Save to Contacts" flows in Chats/Messaging, QR import, manual entry,
457+
* and propagation-node relay setup, which do not touch the announce flag).
458+
*
459+
* Both signals must be checked: the contacts table is the source of truth for
460+
* "saved" as the user experiences it, and the legacy flag is kept so peers saved
461+
* before the contacts table existed continue to notify. Either signal being true
462+
* is sufficient.
463+
*/
464+
private suspend fun isSavedPeer(sourceHash: String): Boolean {
465+
val favoriteFlag =
466+
try {
467+
announceRepository.getAnnounce(sourceHash)?.isFavorite ?: false
468+
} catch (e: Exception) {
469+
Log.w(TAG, "Could not check if peer is favorite", e)
470+
false
471+
}
472+
if (favoriteFlag) return true
473+
474+
return try {
475+
contactRepository.hasContact(sourceHash)
476+
} catch (e: Exception) {
477+
Log.w(TAG, "Could not check if peer is a saved contact", e)
478+
false
479+
}
480+
}
481+
461482
/**
462483
* Get peer name with fallback - uses PeerNameResolver for consistent lookup across the app
463484
*/

app/src/test/java/network/columba/app/service/MessageCollectorTest.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,51 @@ class MessageCollectorTest {
370370
}
371371
}
372372

373+
@Test
374+
fun `processMessage treats saved contact without favorite announce as favorite`() =
375+
runBlocking {
376+
// Given: A message from a peer saved via "Save to Contacts" (Chats/Messaging
377+
// screens). Those flows write to the contacts table and never touch the
378+
// legacy announce isFavorite flag, so the announce row reports
379+
// isFavorite = false even though the peer IS a saved contact.
380+
val testMessage =
381+
ReceivedMessage(
382+
messageHash = "contact_msg",
383+
content = "Message from saved contact",
384+
sourceHash = testSourceHash,
385+
destinationHash = testDestHash,
386+
timestamp = System.currentTimeMillis(),
387+
fieldsJson = null,
388+
publicKey = null,
389+
)
390+
391+
// Announce row exists but is NOT favorited (Save to Contacts never sets it)
392+
coEvery { announceRepository.getAnnounce(testSourceHashHex) } returns
393+
mockk {
394+
every { isFavorite } returns false
395+
}
396+
// But the peer IS in the contacts table
397+
coEvery { contactRepository.hasContact(testSourceHashHex) } returns true
398+
399+
// When: Start collecting and emit
400+
val startResult = runCatching { messageCollector.startCollecting() }
401+
assertTrue("startCollecting should complete without throwing", startResult.isSuccess)
402+
kotlinx.coroutines.delay(50)
403+
messageFlow.emit(testMessage)
404+
kotlinx.coroutines.delay(200)
405+
406+
// Then: Notification should be posted with isFavorite = true, so that
407+
// "Message from Saved Peer" (with "Received Message" off) still fires.
408+
coVerify(timeout = 2000) {
409+
notificationHelper.notifyMessageReceived(
410+
destinationHash = testSourceHashHex,
411+
peerName = any(),
412+
messagePreview = any(),
413+
isFavorite = true,
414+
)
415+
}
416+
}
417+
373418
@Test
374419
fun `processMessage handles announce lookup failure gracefully for notifications`() =
375420
runBlocking {

0 commit comments

Comments
 (0)