Skip to content

Commit 42e993a

Browse files
Merge pull request #1122 from torlando-tech/fix/saved-peer-notifications
fix(android): treat saved contacts as saved peers for notifications
2 parents 7a81861 + 9bae337 commit 42e993a

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)