From a19a0252743dc6f1d538f1d5bf654d74c9ff09ae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 20:44:14 +0000 Subject: [PATCH] Revert "fix(richtext): don't truncate single-atom content mid-token" This reverts commit 3dbf24710809d6931f8cac073f12f035d28e437e. --- .../ExpandableTextCutOffCalculator.kt | 26 ++---- .../commons/richtext/CashuTokenParserTest.kt | 79 ------------------- .../ExpandableTextCutOffCalculatorTest.kt | 33 +------- 3 files changed, 6 insertions(+), 132 deletions(-) delete mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/CashuTokenParserTest.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt index b1b43d523f..f335889e4d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt @@ -45,28 +45,12 @@ class ExpandableTextCutOffCalculator { // if it is still too big, finds the first space or new line BEFORE the cut off. val newString = content.take(SHORT_TEXT_LENGTH) - val firstSpaceBeforeCut = newString.lastIndexOf(' ') - val firstNewLineBeforeCut = newString.lastIndexOf('\n') + val firstSpaceBeforeCut = + newString.lastIndexOf(' ').let { if (it < 0) newString.length else it } + val firstNewLineBeforeCut = + newString.lastIndexOf('\n').let { if (it < 0) newString.length else it } - if (firstSpaceBeforeCut < 0 && firstNewLineBeforeCut < 0) { - // No whitespace boundary anywhere in the first - // SHORT_TEXT_LENGTH characters either. The - // entire content is one indivisible atom — a - // long cashuA/cashuB token, a base64 data: URI, - // a Lightning invoice, a single huge URL. Cutting - // mid-atom corrupts the segment so the inline - // renderer can't decode it (the cashuB prefix - // survives so it gets matched, but the truncated - // body fails to base64-decode and falls back to - // rendering the raw text + a useless Show More - // button). Render the whole atom. - return content.length - } - - maxOf( - firstSpaceBeforeCut.coerceAtLeast(0), - firstNewLineBeforeCut.coerceAtLeast(0), - ) + maxOf(firstSpaceBeforeCut, firstNewLineBeforeCut) } else { min } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/CashuTokenParserTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/CashuTokenParserTest.kt deleted file mode 100644 index a6ddc8ecea..0000000000 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/CashuTokenParserTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.commons.richtext - -import com.vitorpamplona.amethyst.commons.model.EmptyTagList -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class CashuTokenParserTest { - private val realCashuBToken = - "cashuBv2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YWRkVEVTVGF0n79haUgAEHk32wzI" + - "ZWFwn79hYQJhc3hAZWM1YWI3Yjc1NjViYjBjZTZhNzg2NzBkMDA0OGExMjVlZGQzMjJhYmVjMTEzYWMwZTBmZGVkZmE3NTQ4Mzg3OWFj" + - "WCED0-ops8Ta6NjKChNJPe_jgIbXLlyxg2KSy2WaSTADo5D_v2FhCGFzeEBkNDZlODU5MDExNjU0NmNjZjAwNTE3ZTQ1NmU0MTY0N2Fm" + - "ZWUxOWNlMzY2N2IzYTcxODZkMzEwZDY1MjM3OTM4YWNYIQMTDGTY943O4ojhKopoYdemsUE2rSLfzwNBODL8WgOX0v______" - - @Test - fun detectsCashuBAlone() { - val state = RichTextParser().parseText(realCashuBToken, EmptyTagList, null) - val segment = state.paragraphs[0].words[0] - assertTrue( - segment is CashuSegment, - "Expected CashuSegment for cashuB token alone, got ${segment::class.simpleName} (\"${segment.segmentText.take(20)}…\")", - ) - assertEquals(realCashuBToken, segment.segmentText) - } - - @Test - fun detectsCashuBSurroundedByText() { - val text = "Here is a token: $realCashuBToken — enjoy" - val state = RichTextParser().parseText(text, EmptyTagList, null) - val tokenSegment = - state.paragraphs[0] - .words - .firstOrNull { it is CashuSegment } - assertTrue( - tokenSegment != null, - "Expected a CashuSegment in the parsed words; got " + - state.paragraphs[0] - .words - .joinToString { (it::class.simpleName ?: "?") + "(\"${it.segmentText.take(12)}…\")" }, - ) - assertEquals(realCashuBToken, tokenSegment.segmentText) - } - - @Test - fun detectsCashuBWithLeadingNewline() { - val text = "Here is a token:\n$realCashuBToken" - val state = RichTextParser().parseText(text, EmptyTagList, null) - val tokenSegment = - state.paragraphs - .flatMap { it.words } - .firstOrNull { it is CashuSegment } - assertTrue( - tokenSegment != null, - "Expected a CashuSegment after a newline; got " + - state.paragraphs.flatMap { it.words }.joinToString { it::class.simpleName ?: "?" }, - ) - assertEquals(realCashuBToken, tokenSegment.segmentText) - } -} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculatorTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculatorTest.kt index 26cd3fffe0..3c5733176b 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculatorTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculatorTest.kt @@ -56,37 +56,6 @@ class ExpandableTextCutOffCalculatorTest { @Test fun testImage() { - // The data: URI has no whitespace anywhere — cutting it mid-base64 would - // give RichTextViewer a malformed token that fails the BASE64_IMAGE regex - // and falls back to a wall of raw text. Render the whole atom instead. - assertEquals(image.length, ExpandableTextCutOffCalculator.indexToCutOff(image)) - } - - @Test - fun testStandaloneCashuBTokenIsNotTruncated() { - // Real 480-char cashuB token reported by a user — no whitespace, no - // newlines. The pre-fix calculator cut at 350, leaving the renderer - // with a corrupt token that failed to decode (wall of base64 + a - // useless "Show more" button). With the fix, the whole token is - // rendered so CashuPreview can decode and show the redeem card. - val token = - "cashuBv2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YWRkVEVTVGF0n79haUgAEHk32wzI" + - "ZWFwn79hYQJhc3hAZWM1YWI3Yjc1NjViYjBjZTZhNzg2NzBkMDA0OGExMjVlZGQzMjJhYmVjMTEzYWMwZTBmZGVkZmE3NTQ4Mzg3OWFj" + - "WCED0-ops8Ta6NjKChNJPe_jgIbXLlyxg2KSy2WaSTADo5D_v2FhCGFzeEBkNDZlODU5MDExNjU0NmNjZjAwNTE3ZTQ1NmU0MTY0N2Fm" + - "ZWUxOWNlMzY2N2IzYTcxODZkMzEwZDY1MjM3OTM4YWNYIQMTDGTY943O4ojhKopoYdemsUE2rSLfzwNBODL8WgOX0v______" - assertEquals(token.length, ExpandableTextCutOffCalculator.indexToCutOff(token)) - } - - @Test - fun testTextBeforeLongTokenCutsAtTheBoundary() { - // Preamble of breathable text followed by a single very long atomic - // token. The cut should land at the boundary BEFORE the token starts, - // not inside it. - val preamble = "Here is a token for you: " - val token = "cashuBv2F" + "x".repeat(800) - val content = preamble + token - - val cut = ExpandableTextCutOffCalculator.indexToCutOff(content) - assertEquals(preamble.length - 1, cut) // index of the space right before the token + assertEquals(350, ExpandableTextCutOffCalculator.indexToCutOff(image)) } }