Skip to content

Commit 50d2c0f

Browse files
committed
fix(notifications): drop the sender's name from their own chat message
A chat notification reads "Kevin Ricoy: whoa" under a line that already says Kevin Ricoy. The server composes the body that way because a plain notification has nowhere else to put the sender, but MessagingStyle does: it draws the Person above the message in a group, and in a DM the notification's own title carries the name. planMessageBody strips the prefix before the message is added. It matches against every name the sender is known by rather than the one being rendered — the line uses the address-book name when there is one, while the body was composed from the profile's display name — and only on an exact match, so a message that happens to start "Neil: " survives unless Neil sent it.
1 parent 12a4ffb commit 50d2c0f

3 files changed

Lines changed: 98 additions & 6 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.flipcash.app.notifications
2+
3+
/**
4+
* The text a chat push should show once `MessagingStyle` is drawing the sender itself.
5+
*
6+
* Pure for the same reason [planSenderLookup] is: the rule is what's worth testing, and stating it
7+
* needs neither a database nor a live service.
8+
*
9+
* The server composes a body that names its sender — "Kevin Ricoy: whoa" — because a plain
10+
* notification has nowhere else to put it. Under `MessagingStyle` there is: the platform renders
11+
* the `Person` above the line in a group, and the notification's own title carries it in a DM. The
12+
* name then appears twice.
13+
*
14+
* [senderNames] is every name this sender is known by rather than just the one being displayed,
15+
* because they can disagree — the line is rendered with the address-book name when there is one,
16+
* while the server composed the body from the profile's display name. Only an exact match is
17+
* stripped, so a message that happens to start "Neil: " stays intact unless Neil sent it.
18+
*
19+
* @param body the resolved push body, null when the push carries none
20+
* @param senderNames the sender's known names, in any order; blanks are ignored
21+
*/
22+
fun planMessageBody(body: String?, senderNames: List<String?>): String {
23+
val text = body.orEmpty()
24+
val prefix = senderNames
25+
.asSequence()
26+
.filterNot { it.isNullOrBlank() }
27+
.map { "$it:" }
28+
// Longest first, so a sender known by both a name and a fuller version of it loses the
29+
// whole thing rather than leaving the remainder behind.
30+
.sortedByDescending { it.length }
31+
.firstOrNull { text.startsWith(it, ignoreCase = true) }
32+
?: return text
33+
34+
// A body that is only the prefix isn't an attribution to strip — dropping it would post an
35+
// empty message.
36+
return text.substring(prefix.length).trimStart().ifBlank { text }
37+
}

‎apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -340,11 +340,16 @@ class NotificationService : FirebaseMessagingService(),
340340
}
341341
val e164 = contactE164 ?: sender?.profile?.verifiedPhoneNumber
342342

343-
val senderName = e164?.let { contactResolver.resolveName(it) }
344-
?: sender?.profile?.displayName?.takeIf { it.isNotBlank() }
345-
?: sender?.profile?.socialHandle()
346-
?: title
347-
?: ""
343+
// Every name this sender is known by, not just the one rendered: the server composed the
344+
// body from one of them and the line is drawn with another, so stripping the sender's name
345+
// out of their own message needs all of them. See [planMessageBody].
346+
val senderNames = listOf(
347+
e164?.let { contactResolver.resolveName(it) },
348+
sender?.profile?.displayName,
349+
sender?.profile?.socialHandle(),
350+
title,
351+
)
352+
val senderName = senderNames.firstOrNull { !it.isNullOrBlank() }.orEmpty()
348353

349354
// Device-contact photo (local, synchronous) first; otherwise the profile
350355
// picture URL loaded through the app's shared Coil loader (cache-first,
@@ -397,7 +402,7 @@ class NotificationService : FirebaseMessagingService(),
397402
?.let { NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(it) }
398403
?: NotificationCompat.MessagingStyle(selfPerson)
399404

400-
style.addMessage(body.orEmpty(), System.currentTimeMillis(), senderPerson)
405+
style.addMessage(planMessageBody(body, senderNames), System.currentTimeMillis(), senderPerson)
401406

402407
// After the extract above, not before: a re-post rebuilds the style from the notification
403408
// already on screen, which carries the old flag and title back with it.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.flipcash.app.notifications
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
6+
/**
7+
* `MessagingStyle` renders the sender itself, so a body that names its own sender shows the name
8+
* twice — which is what the server sends, since a plain notification has nowhere else to put it.
9+
*/
10+
class MessageBodyPlannerTest {
11+
12+
@Test
13+
fun `the sender's own name is dropped from the front of their message`() {
14+
assertEquals("whoa", planMessageBody("Kevin Ricoy: whoa", listOf("Kevin Ricoy")))
15+
}
16+
17+
@Test
18+
fun `a name the sender is known by elsewhere is dropped too`() {
19+
// Rendered under the address-book name; composed by the server from the profile's.
20+
assertEquals(
21+
"Howdy",
22+
planMessageBody("neilbryan: Howdy", listOf("Neil", "neilbryan")),
23+
)
24+
}
25+
26+
@Test
27+
fun `the longest matching name is what gets dropped`() {
28+
assertEquals("hi", planMessageBody("Ted Livingston: hi", listOf("Ted", "Ted Livingston")))
29+
}
30+
31+
@Test
32+
fun `someone else's name is left alone`() {
33+
assertEquals("Neil: Howdy", planMessageBody("Neil: Howdy", listOf("Kevin Ricoy")))
34+
}
35+
36+
@Test
37+
fun `a message that is only the sender's name survives`() {
38+
assertEquals("Kevin Ricoy:", planMessageBody("Kevin Ricoy:", listOf("Kevin Ricoy")))
39+
}
40+
41+
@Test
42+
fun `a body with no sender to match is unchanged`() {
43+
assertEquals("whoa", planMessageBody("whoa", listOf(null, "", " ")))
44+
}
45+
46+
@Test
47+
fun `a push with no body plans an empty message`() {
48+
assertEquals("", planMessageBody(null, listOf("Kevin Ricoy")))
49+
}
50+
}

0 commit comments

Comments
 (0)