From 50d2c0fa7ff2bdad0ca49a9279bf4fd8fced07cb Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 16 Sep 2026 22:37:34 -0400 Subject: [PATCH] fix(notifications): drop the sender's name from their own chat message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../app/notifications/MessageBodyPlanner.kt | 37 ++++++++++++++ .../app/notifications/NotificationService.kt | 17 ++++--- .../notifications/MessageBodyPlannerTest.kt | 50 +++++++++++++++++++ 3 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/MessageBodyPlanner.kt create mode 100644 apps/flipcash/shared/notifications/src/test/kotlin/com/flipcash/app/notifications/MessageBodyPlannerTest.kt diff --git a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/MessageBodyPlanner.kt b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/MessageBodyPlanner.kt new file mode 100644 index 0000000000..20781f0b10 --- /dev/null +++ b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/MessageBodyPlanner.kt @@ -0,0 +1,37 @@ +package com.flipcash.app.notifications + +/** + * The text a chat push should show once `MessagingStyle` is drawing the sender itself. + * + * Pure for the same reason [planSenderLookup] is: the rule is what's worth testing, and stating it + * needs neither a database nor a live service. + * + * The server composes a body that names its sender — "Kevin Ricoy: whoa" — because a plain + * notification has nowhere else to put it. Under `MessagingStyle` there is: the platform renders + * the `Person` above the line in a group, and the notification's own title carries it in a DM. The + * name then appears twice. + * + * [senderNames] is every name this sender is known by rather than just the one being displayed, + * because they can disagree — the line is rendered with the address-book name when there is one, + * while the server composed the body from the profile's display name. Only an exact match is + * stripped, so a message that happens to start "Neil: " stays intact unless Neil sent it. + * + * @param body the resolved push body, null when the push carries none + * @param senderNames the sender's known names, in any order; blanks are ignored + */ +fun planMessageBody(body: String?, senderNames: List): String { + val text = body.orEmpty() + val prefix = senderNames + .asSequence() + .filterNot { it.isNullOrBlank() } + .map { "$it:" } + // Longest first, so a sender known by both a name and a fuller version of it loses the + // whole thing rather than leaving the remainder behind. + .sortedByDescending { it.length } + .firstOrNull { text.startsWith(it, ignoreCase = true) } + ?: return text + + // A body that is only the prefix isn't an attribution to strip — dropping it would post an + // empty message. + return text.substring(prefix.length).trimStart().ifBlank { text } +} diff --git a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt index f128db3721..2ade7603d6 100644 --- a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt +++ b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt @@ -340,11 +340,16 @@ class NotificationService : FirebaseMessagingService(), } val e164 = contactE164 ?: sender?.profile?.verifiedPhoneNumber - val senderName = e164?.let { contactResolver.resolveName(it) } - ?: sender?.profile?.displayName?.takeIf { it.isNotBlank() } - ?: sender?.profile?.socialHandle() - ?: title - ?: "" + // Every name this sender is known by, not just the one rendered: the server composed the + // body from one of them and the line is drawn with another, so stripping the sender's name + // out of their own message needs all of them. See [planMessageBody]. + val senderNames = listOf( + e164?.let { contactResolver.resolveName(it) }, + sender?.profile?.displayName, + sender?.profile?.socialHandle(), + title, + ) + val senderName = senderNames.firstOrNull { !it.isNullOrBlank() }.orEmpty() // Device-contact photo (local, synchronous) first; otherwise the profile // picture URL loaded through the app's shared Coil loader (cache-first, @@ -397,7 +402,7 @@ class NotificationService : FirebaseMessagingService(), ?.let { NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(it) } ?: NotificationCompat.MessagingStyle(selfPerson) - style.addMessage(body.orEmpty(), System.currentTimeMillis(), senderPerson) + style.addMessage(planMessageBody(body, senderNames), System.currentTimeMillis(), senderPerson) // After the extract above, not before: a re-post rebuilds the style from the notification // already on screen, which carries the old flag and title back with it. diff --git a/apps/flipcash/shared/notifications/src/test/kotlin/com/flipcash/app/notifications/MessageBodyPlannerTest.kt b/apps/flipcash/shared/notifications/src/test/kotlin/com/flipcash/app/notifications/MessageBodyPlannerTest.kt new file mode 100644 index 0000000000..61486323ed --- /dev/null +++ b/apps/flipcash/shared/notifications/src/test/kotlin/com/flipcash/app/notifications/MessageBodyPlannerTest.kt @@ -0,0 +1,50 @@ +package com.flipcash.app.notifications + +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * `MessagingStyle` renders the sender itself, so a body that names its own sender shows the name + * twice — which is what the server sends, since a plain notification has nowhere else to put it. + */ +class MessageBodyPlannerTest { + + @Test + fun `the sender's own name is dropped from the front of their message`() { + assertEquals("whoa", planMessageBody("Kevin Ricoy: whoa", listOf("Kevin Ricoy"))) + } + + @Test + fun `a name the sender is known by elsewhere is dropped too`() { + // Rendered under the address-book name; composed by the server from the profile's. + assertEquals( + "Howdy", + planMessageBody("neilbryan: Howdy", listOf("Neil", "neilbryan")), + ) + } + + @Test + fun `the longest matching name is what gets dropped`() { + assertEquals("hi", planMessageBody("Ted Livingston: hi", listOf("Ted", "Ted Livingston"))) + } + + @Test + fun `someone else's name is left alone`() { + assertEquals("Neil: Howdy", planMessageBody("Neil: Howdy", listOf("Kevin Ricoy"))) + } + + @Test + fun `a message that is only the sender's name survives`() { + assertEquals("Kevin Ricoy:", planMessageBody("Kevin Ricoy:", listOf("Kevin Ricoy"))) + } + + @Test + fun `a body with no sender to match is unchanged`() { + assertEquals("whoa", planMessageBody("whoa", listOf(null, "", " "))) + } + + @Test + fun `a push with no body plans an empty message`() { + assertEquals("", planMessageBody(null, listOf("Kevin Ricoy"))) + } +}