From 900c91ca6f8d85bbd06bf803cc1bd3834027757c Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Sun, 6 Sep 2026 21:46:00 -0400 Subject: [PATCH] Report a story on Android, which had no way to (ios#12, android#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../io/flamenet/messenger/app/ChatScreens.kt | 8 +++- .../io/flamenet/messenger/app/StoriesFeed.kt | 47 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt index ee736d3..e58fffc 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt @@ -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, diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/StoriesFeed.kt b/app/src/main/kotlin/io/flamenet/messenger/app/StoriesFeed.kt index 7e7c067..97baa14 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/StoriesFeed.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/StoriesFeed.kt @@ -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) {