Report a story on Android, which had no way to (ios#12, android#12)

Apple's Guideline 1.2 and Play's user-generated-content policy both
require a way to report. Android had the whole stack — `submitReport` on
the client, `Session.reportStory`, `MessengerViewModel.reportStory` — and
no screen called any of it. For those rules, a capability with no way in
is the same as no capability, and a story is the most public thing this
app carries, which is exactly the surface they are aimed at.

Message reporting was already wired and complete. I nearly rebuilt it
from scratch: a truncated grep showed no hits, I concluded Android had no
reporting at all, and got as far as a second `ReportSheet` before finding
the existing one. Discarded. The lesson is the boring one — `head -8` on
a grep is not evidence of absence.

Reuses that sheet rather than adding a second. Two would be two places
for the consent wording to drift apart, and the consent is the feature:
sending a copy hands the operator something readable, which is the one
thing this app otherwise never does. It was `private` to `ChatScreens`,
which is why the stories feed could not reach it; widened to `internal`.

⚠️ Not offered on your own story. Reporting yourself to the operator is
not something anybody means to do, and offering it invites a misfire.

⚠️ The author's name is passed in from `StoryCard` rather than resolved
again. It has the buddy list and has already done it, and two resolutions
of the same name are two chances to disagree about who is being reported.

1179 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-06 21:46:00 -04:00
co-authored by Claude Opus 5
parent 20d19e0de3
commit 900c91ca6f
2 changed files with 52 additions and 3 deletions
@@ -4096,7 +4096,13 @@ private fun EditMessageSheet(
* to somebody who may do nothing.
*/
@Composable
private fun ReportSheet(
/**
* ⚠️ Was `private` to this file, which is why the stories feed could not use
* it and Android had no way to report a story at all. Widened rather than
* copied: two report sheets would be two places for the consent wording to
* drift apart, and the consent is the part that matters.
*/
internal fun ReportSheet(
authorName: String,
preview: String,
onDismiss: () -> Unit,
@@ -238,7 +238,7 @@ private fun StoryCard(
}
Spacer(Modifier.height(Space.md))
StoryActions(story, model)
StoryActions(story, model, authorName = author)
}
}
@@ -253,10 +253,18 @@ private fun StoryCard(
* a row of buttons that do nothing is worse than no row.
*/
@Composable
private fun StoryActions(story: Vault.Story, model: MessengerViewModel) {
private fun StoryActions(
story: Vault.Story,
model: MessengerViewModel,
// ⚠️ Passed in rather than resolved here: `StoryCard` already has the
// buddy list and has already done this, and two resolutions of the same
// name are two chances to disagree about who is being reported.
authorName: String = "",
) {
val quick = listOf("❤️", "😂", "😮", "😢", "🔥")
var replying by remember { mutableStateOf(false) }
var muting by remember { mutableStateOf(false) }
var reporting by remember { mutableStateOf(false) }
if (story.mine) {
val tally = model.storyTally(story.id)
@@ -298,6 +306,41 @@ private fun StoryActions(story: Vault.Story, model: MessengerViewModel) {
Spacer(Modifier.size(12.dp))
Text("Mute", fontSize = 13.sp, color = Palette.Mist,
modifier = Modifier.clickable { muting = true }.testTag("story-mute"))
// 🔴 **Reporting a story, which Android could not do.** iOS has had it
// since stories shipped; here `MessengerViewModel.reportStory` was
// written, tested by nothing, and called by no screen. A capability
// with no way in is, for Apple's Guideline 1.2 and Play's UGC policy,
// the same as no capability — and a story is the most public thing
// this app carries, so it is the surface those rules are aimed at.
//
// ⚠️ Not on your own story. Reporting yourself to the operator is not
// something anybody means to do, and offering it invites a misfire.
if (!story.mine) {
Spacer(Modifier.size(12.dp))
Text("Report", fontSize = 13.sp, color = Palette.Mist,
modifier = Modifier.clickable { reporting = true }
.testTag("story-report"))
}
}
if (reporting) {
// ⚠️ The SAME sheet a message report uses, deliberately. Two sheets
// would be two places for the consent wording to drift apart, and the
// consent — that sending a copy hands the operator something readable —
// is the part that matters.
ReportSheet(
authorName = authorName.ifBlank { "this person" },
preview = story.caption,
onDismiss = { reporting = false },
onSend = { reason, includeContent, note ->
model.reportStory(story, reason, includeContent, note)
reporting = false
},
onBlock = {
model.setBlocked(story.authorId, true)
reporting = false
},
)
}
if (replying) {