Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions app/src/main/java/com/lxmf/messenger/nomadnet/PartialManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class PartialManager(
val status: Status,
val document: MicronDocument?,
val refreshInterval: Int?,
val fieldNames: List<String> = emptyList(),
) {
enum class Status { LOADING, LOADED, ERROR }
}
Expand Down Expand Up @@ -138,6 +139,7 @@ class PartialManager(
status = PartialState.Status.LOADING,
document = null,
refreshInterval = partial.refreshInterval,
fieldNames = partial.fieldNames,
)
)
}
Expand Down Expand Up @@ -174,7 +176,7 @@ class PartialManager(

jobs[key] =
scope.launch(Dispatchers.IO) {
fetchAndUpdate(key, existing.url, existing.refreshInterval)
fetchAndUpdate(key, existing.url, existing.refreshInterval, existing.fieldNames)
}
}

Expand All @@ -191,21 +193,22 @@ class PartialManager(
key: String,
partial: MicronElement.Partial,
) {
fetchAndUpdate(key, partial.url, partial.refreshInterval)
fetchAndUpdate(key, partial.url, partial.refreshInterval, partial.fieldNames)
}

private suspend fun fetchAndUpdate(
key: String,
url: String,
refreshInterval: Int?,
allowedFields: List<String> = emptyList(),
) {
var consecutiveErrors = 0
try {
while (true) {
fetchSemaphore.withPermit {
val (nodeHash, path) = resolveNomadNetUrl(url, currentNodeHash())

val formDataJson = buildFormDataJson()
val formDataJson = buildFormDataJson(allowedFields)

val result =
protocol.requestNomadnetPage(
Comment thread
sentry[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -273,17 +276,30 @@ class PartialManager(
status = PartialState.Status.ERROR,
document = null,
refreshInterval = refreshInterval,
fieldNames = allowedFields,
)
)
}
}
}

private fun buildFormDataJson(): String? {
private fun buildFormDataJson(allowedFields: List<String>): String? {
val fields = formFields()
if (fields.isEmpty()) return null
// Match NomadNet TUI: only forward fields declared by the partial.
// "*" means all fields; empty list means no fields.
val allFields = "*" in allowedFields
val filtered =
if (allFields) {
fields
} else if (allowedFields.isEmpty()) {
return null
} else {
fields.filterKeys { it in allowedFields }
}
if (filtered.isEmpty()) return null
val json = JSONObject()
for ((k, v) in fields) {
for ((k, v) in filtered) {
json.put(k, v)
}
return json.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,20 +207,21 @@ private fun MicronLineComposable(

val indentPadding = (line.indentLevel * INDENT_DP).dp

// In scroll mode (MONOSPACE_SCROLL), use min-width for ALL alignments so wide
// lines (e.g. ASCII art) can extend beyond the viewport and scroll horizontally.
// In other modes, centered/right-aligned lines use exact viewport width so text
// wraps within the viewport (matching NomadNet terminal behavior).
// In scroll mode, left-aligned lines use min-width so wide content (ASCII art)
// can extend beyond the viewport and scroll horizontally. Centered/right-aligned
// lines use exact viewport width so text wraps and TextAlign actually centers.
val isScrollMode = renderingMode == RenderingMode.MONOSPACE_SCROLL
val scrollAligned = isScrollMode && line.alignment != MicronAlignment.LEFT
val widthModifier =
if (minLineWidth != Dp.Unspecified) {
if (isScrollMode) {
Modifier.widthIn(min = minLineWidth)
} else {
when (line.alignment) {
MicronAlignment.CENTER, MicronAlignment.RIGHT -> Modifier.width(minLineWidth)
MicronAlignment.LEFT -> Modifier.widthIn(min = minLineWidth)
}
when {
scrollAligned -> Modifier.width(minLineWidth)
isScrollMode -> Modifier.widthIn(min = minLineWidth)
else ->
when (line.alignment) {
MicronAlignment.CENTER, MicronAlignment.RIGHT -> Modifier.width(minLineWidth)
MicronAlignment.LEFT -> Modifier.widthIn(min = minLineWidth)
}
}
} else {
Modifier
Expand Down Expand Up @@ -347,11 +348,11 @@ private fun MicronLineComposable(

val lineModifier =
widthModifier
.then(if (isScrollMode) Modifier else Modifier.fillMaxWidth())
.then(if (isScrollMode && !scrollAligned) Modifier else Modifier.fillMaxWidth())
.padding(start = indentPadding)
.then(if (headingBg != null) Modifier.background(headingBg) else Modifier)
.then(
if (hasLinks && !isScrollMode) {
if (hasLinks && (!isScrollMode || scrollAligned)) {
Modifier.defaultMinSize(minHeight = MIN_LINK_HEIGHT_DP.dp)
} else {
Modifier
Expand Down Expand Up @@ -386,11 +387,13 @@ private fun MicronLineComposable(
)
}

val softWrap = !isScrollMode || scrollAligned

if (hasLinks) {
ClickableText(
text = annotatedString,
modifier = lineModifier,
softWrap = !isScrollMode,
softWrap = softWrap,
style = textStyle,
onClick = { offset ->
annotatedString
Expand All @@ -409,7 +412,7 @@ private fun MicronLineComposable(
Text(
text = annotatedString,
modifier = lineModifier,
softWrap = !isScrollMode,
softWrap = softWrap,
style = textStyle,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,18 +726,20 @@ class MicronParserTest {

@Test
fun `backtick before link with formatting`() {
// Real-world pattern: `c`F0ad`_`[label`dest]`_`f
val doc = MicronParser.parse("`c`F0ad`_`[\"Hoard's Heart\"`/page/hoardsheart.mu]`_`f")
// Real-world line from a NomadNet node index page
val doc = MicronParser.parse("`c`F0ad`_`[\"Hoard's Heart\" by CupsofJade`:/page/hoardsheart.mu]`_`f - Reticulumified!")
assertEquals(MicronAlignment.CENTER, doc.lines[0].alignment)
val link =
doc.lines[0]
.elements
.filterIsInstance<MicronElement.Link>()
.first()
assertEquals("\"Hoard's Heart\"", link.label)
assertEquals("/page/hoardsheart.mu", link.destination)
// No stray backtick in any text element
assertEquals("\"Hoard's Heart\" by CupsofJade", link.label)
assertEquals(":/page/hoardsheart.mu", link.destination)
// Trailing text after the link
val texts = doc.lines[0].elements.filterIsInstance<MicronElement.Text>()
assertTrue(texts.any { it.content.contains("Reticulumified!") })
// No stray backtick in any text element
assertTrue(texts.none { it.content.contains("`") })
}

Expand Down
Loading