From 9af611faf1fc53cf4aab78a26da16c2a86199b38 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 5 Jul 2026 09:11:03 +0200 Subject: [PATCH] fix: fetch uncached addressable thread roots; render blank PS1 blocks as empty slots --- .../amethyst/ui/note/types/Ps1Save.kt | 13 ++++++++- .../FilterMissingEventsForThread.kt | 6 +++- amethyst/src/main/res/values/strings.xml | 1 + .../experimental/ps1saves/Ps1SaveEvent.kt | 7 +++++ .../experimental/ps1saves/Ps1SaveEventTest.kt | 28 +++++++++++++++++++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt index eaa1e88c1c..639c26947c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -86,6 +87,7 @@ fun RenderPs1Save(baseNote: Note) { region = noteEvent.region(), blockNumber = noteEvent.blockNumber(), icon = noteEvent.icon(), + isBlank = noteEvent.isBlankBlock(), hexPreview = noteEvent.content .take(HEX_PREVIEW_CHARS) @@ -127,7 +129,15 @@ fun RenderPs1Save(baseNote: Note) { ) } - if (save.hexPreview != null) { + if (save.isBlank) { + Spacer(Modifier.height(6.dp)) + Text( + text = stringRes(R.string.ps1_save_empty_slot), + style = MaterialTheme.typography.bodyMedium, + fontStyle = FontStyle.Italic, + color = MaterialTheme.colorScheme.placeholderText, + ) + } else if (save.hexPreview != null) { Spacer(Modifier.height(6.dp)) Text( text = save.hexPreview, @@ -187,5 +197,6 @@ private class Ps1SaveInfo( val region: String?, val blockNumber: Int?, val icon: Ps1SaveIcon?, + val isBlank: Boolean, val hexPreview: String?, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt index 4d27497ca7..c2a7d7ba40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt @@ -55,7 +55,11 @@ fun filterMissingEventsForThread( mapOfSet { val rootNote = threadInfo.root if (rootNote.event == null && rootNote is AddressableNote) { - potentialRelaysToFindEvent(rootNote).ifEmpty { defaultRelays }.forEach { relayUrl -> + // Must be the address-based resolver: the event-based one feeds the + // note's aTag idHex into the hex-keyed event-hint index, which throws + // on the non-hex string and kills the whole filter build — leaving a + // thread opened on an uncached naddr permanently unfetched. + potentialRelaysToFindAddress(rootNote).ifEmpty { defaultRelays }.forEach { relayUrl -> add(relayUrl, rootNote.address) } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index b8810a8640..bd4ffe9016 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -3498,6 +3498,7 @@ PS1 memory card save block %1$d + Empty slot Police Speed camera diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEvent.kt index 6e1612fd41..39d1974122 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEvent.kt @@ -83,6 +83,13 @@ class Ps1SaveEvent( */ fun icon() = Ps1SaveIcon.parse(content) + /** + * True when the block holds no save data: erased flash sectors (all `ff`) + * or zero-filled blocks, whatever the publisher's hex casing. UIs show + * these as empty slots instead of a hex dump. + */ + fun isBlankBlock() = content.isNotEmpty() && content.all { it.equals(content[0], ignoreCase = true) } + companion object { const val KIND = 38192 } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEventTest.kt index 03a65563ae..0029d6724c 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEventTest.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.utils.EventFactory import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertTrue @@ -77,6 +78,33 @@ class Ps1SaveEventTest { assertEquals("synctest-f1fc95d1-1", event.dTag()) } + @Test + fun blankBlockDetection() { + fun eventWith(content: String): Ps1SaveEvent { + val event: Event = + EventFactory.create( + id = "00".repeat(32), + pubKey = "00".repeat(32), + createdAt = 1_783_151_940L, + kind = Ps1SaveEvent.KIND, + tags = arrayOf(arrayOf("d", "card-1")), + content = content, + sig = "00".repeat(64), + ) + assertIs(event) + return event + } + + // Erased flash sectors (all ff) and zero-filled blocks are both blank, + // regardless of the publisher's hex casing. + assertTrue(eventWith("ffffffffffff").isBlankBlock()) + assertTrue(eventWith("ffFFffFFffFF").isBlankBlock()) + assertTrue(eventWith("000000000000").isBlankBlock()) + // Real save data and empty content are not. + assertFalse(eventWith("5343110153594e43").isBlankBlock()) + assertFalse(eventWith("").isBlankBlock()) + } + @Test fun blockNumberIsNullWhenTagIsMissingOrNotANumber() { val event: Event =