Mute one contact's stories, without blocking them
Android half of ios#33. Same shape as iOS: the muted set lives in the vault beside the audience list, because who somebody has muted is a statement about their social graph and shared preferences are readable by anything that reaches the file. The filter runs on the way out of `stories()`, after `saveStories` has already written the live set back. A muted contact's stories are still received and still stored — dropping them on receipt would be cheaper, but unmuting would then show an empty strip until their next post, and muting would quietly mean something other than what it says. `Vault.visibleStories` never drops the signed-in user, even if their own id is in the set. `setStoryMuted` refuses to put it there, but a restored backup from a build without that guard could carry one, and hiding somebody's own posts from them reads as the posts having failed to send. Long press on a story bubble to hide or restore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bf44c15266
commit
0c1af3e45f
@@ -0,0 +1,96 @@
|
||||
package io.flamenet.e2e
|
||||
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Muting one contact's stories (ios#33).
|
||||
*
|
||||
* The ordinary middle between seeing everything and blocking somebody.
|
||||
* Entirely local: no envelope, no relay call, and the author is never told.
|
||||
*/
|
||||
class StoryMuteTest {
|
||||
|
||||
private class MapStorage : Vault.Storage {
|
||||
val records = LinkedHashMap<String, ByteArray>()
|
||||
override fun read(name: String) = records[name]
|
||||
override fun write(name: String, bytes: ByteArray) { records[name] = bytes }
|
||||
override fun delete(name: String) { records.remove(name) }
|
||||
override fun names() = records.keys.toList()
|
||||
}
|
||||
|
||||
private fun key(): SecretKey =
|
||||
KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
|
||||
|
||||
private fun story(author: Long, at: Long) = Vault.Story(
|
||||
id = "s$author-$at",
|
||||
authorId = author,
|
||||
mine = false,
|
||||
ref = Attachment.Ref(
|
||||
kind = Attachment.STORY, id = "blob$at", key = ByteArray(32),
|
||||
digest = ByteArray(32), bytes = 1, mime = "image/jpeg",
|
||||
width = 1, height = 1, duration = 0.0, expires = 0.0,
|
||||
),
|
||||
caption = "",
|
||||
postedAt = at,
|
||||
seen = false,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `a muted author is dropped from the strip`() {
|
||||
val visible = Vault.visibleStories(
|
||||
listOf(story(7, 3), story(8, 2), story(9, 1)), setOf(8), me = 1)
|
||||
assertEquals(listOf(7L, 9L), visible.map { it.authorId })
|
||||
}
|
||||
|
||||
/** ⚠️ Order is the caller's — newest first. Filtering must not re-sort. */
|
||||
@Test
|
||||
fun `filtering preserves the order it was given`() {
|
||||
val visible = Vault.visibleStories(
|
||||
listOf(story(9, 3), story(7, 2), story(8, 1)), emptySet(), me = 1)
|
||||
assertEquals(listOf(9L, 7L, 8L), visible.map { it.authorId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 Your own posts survive your own id being in the set. `setStoryMuted`
|
||||
* refuses to add it, but a restored backup could carry one, and hiding
|
||||
* somebody's own stories from them reads as the posts having failed.
|
||||
*/
|
||||
@Test
|
||||
fun `your own stories are never hidden even if your id is muted`() {
|
||||
val visible = Vault.visibleStories(
|
||||
listOf(story(1, 2), story(7, 1)), setOf(1, 7), me = 1)
|
||||
assertEquals(listOf(1L), visible.map { it.authorId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `muting nobody changes nothing`() {
|
||||
val all = listOf(story(7, 2), story(8, 1))
|
||||
assertEquals(2, Vault.visibleStories(all, emptySet(), me = 1).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a muted author with no stories is simply absent`() {
|
||||
val visible = Vault.visibleStories(listOf(story(7, 1)), setOf(99), me = 1)
|
||||
assertEquals(listOf(7L), visible.map { it.authorId })
|
||||
}
|
||||
|
||||
// -- Persistence ---------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun `the muted set round-trips through the vault`() {
|
||||
val vault = Vault(MapStorage(), key())
|
||||
assertTrue(vault.mutedStoryAuthors().isEmpty())
|
||||
vault.saveMutedStoryAuthors(setOf(8L, 12L))
|
||||
assertEquals(setOf(8L, 12L), vault.mutedStoryAuthors())
|
||||
}
|
||||
|
||||
/** An absent record is an empty set, not a crash. */
|
||||
@Test
|
||||
fun `a vault written before this feature reads as nothing muted`() {
|
||||
assertTrue(Vault(MapStorage(), key()).mutedStoryAuthors().isEmpty())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user