Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ sealed interface ChatView {
private val resolvedTitle: String
get() {
val layout = wrapper?.layout
return when {
layout == ObjectType.Layout.NOTE -> {
return when (layout) {
ObjectType.Layout.NOTE -> {
wrapper.snippet.orEmpty()
}
layout in SupportedLayouts.fileLayouts -> {
in SupportedLayouts.fileLayouts -> {
val fileName = wrapper?.name.orEmpty()
val fileExt = wrapper?.fileExt
when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.IntOffset
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.feature_chats.R
Expand Down Expand Up @@ -141,9 +142,48 @@ class AnnotatedTextTransformation(
) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val annotatedString = AnnotatedString.Builder(text).apply {
spans.forEach { span ->
if (span.start in text.indices && span.end <= text.length) {
addStyle(span.style, span.start, span.end)
val processedRanges = mutableSetOf<Int>()

spans.forEachIndexed { index, span ->
if (index in processedRanges) return@forEachIndexed
if (span.start !in text.indices || span.end > text.length) return@forEachIndexed

// Find all spans with the same range
val sameRangeSpans = mutableListOf(span)
for (i in (index + 1) until spans.size) {
val other = spans[i]
if (other.start == span.start && other.end == span.end) {
sameRangeSpans.add(other)
processedRanges.add(i)
}
}

// Collect text decorations
val decorations = sameRangeSpans.mapNotNull { it.style.textDecoration }

if (decorations.size > 1) {
// Multiple text decorations - combine them
val combinedDecoration = TextDecoration.combine(decorations)

// Apply non-decoration styles
sameRangeSpans.forEach { s ->
if (s.style.textDecoration == null) {
addStyle(s.style, span.start, span.end)
} else {
val nonDecorationStyle = s.style.copy(textDecoration = null)
if (nonDecorationStyle != SpanStyle()) {
addStyle(nonDecorationStyle, span.start, span.end)
}
}
}

// Apply combined decoration once
addStyle(SpanStyle(textDecoration = combinedDecoration), span.start, span.end)
} else {
// No conflict - apply normally
sameRangeSpans.forEach { s ->
addStyle(s.style, span.start, span.end)
}
}
}
}.toAnnotatedString()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.anytypeio.anytype.feature_chats.ui

import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.style.TextDecoration
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

class AnnotatedTextTransformationTest {

@Test
fun `should combine decorations for same range`() {
// Given: two decorations on the same range [0, 5]
val spans = listOf(
ChatBoxSpan.Markup(
style = SpanStyle(textDecoration = TextDecoration.LineThrough),
start = 0,
end = 5,
type = ChatBoxSpan.Markup.STRIKETHROUGH
),
ChatBoxSpan.Markup(
style = SpanStyle(textDecoration = TextDecoration.Underline),
start = 0,
end = 5,
type = ChatBoxSpan.Markup.UNDERLINE
)
)

val transformation = AnnotatedTextTransformation(spans)
val input = AnnotatedString("Hello world")

// When: transformation is applied
val result = transformation.filter(input)

// Then: both decorations should be present on the range
val spanStyles = result.text.spanStyles
assertEquals(1, spanStyles.size, "Should have exactly one combined span style")

val appliedStyle = spanStyles.first()
assertEquals(0, appliedStyle.start)
assertEquals(5, appliedStyle.end)

// Check that both decorations are present
assertNotNull(appliedStyle.item.textDecoration)
val decoration = appliedStyle.item.textDecoration!!

// Both decorations should be combined
assertEquals(
TextDecoration.combine(listOf(TextDecoration.LineThrough, TextDecoration.Underline)),
decoration
)
}

@Test
fun `should handle non-overlapping decorations independently`() {
// Given: two decorations on different ranges
val spans = listOf(
ChatBoxSpan.Markup(
style = SpanStyle(textDecoration = TextDecoration.LineThrough),
start = 0,
end = 5,
type = ChatBoxSpan.Markup.STRIKETHROUGH
),
ChatBoxSpan.Markup(
style = SpanStyle(textDecoration = TextDecoration.Underline),
start = 6,
end = 11,
type = ChatBoxSpan.Markup.UNDERLINE
)
)

val transformation = AnnotatedTextTransformation(spans)
val input = AnnotatedString("Hello world")

// When: transformation is applied
val result = transformation.filter(input)

// Then: should have two separate span styles
val spanStyles = result.text.spanStyles
assertEquals(2, spanStyles.size, "Should have two separate span styles")

// Verify first span (strikethrough on "Hello")
val first = spanStyles.find { it.start == 0 && it.end == 5 }
assertNotNull(first)
assertEquals(TextDecoration.LineThrough, first.item.textDecoration)

// Verify second span (underline on "world")
val second = spanStyles.find { it.start == 6 && it.end == 11 }
assertNotNull(second)
assertEquals(TextDecoration.Underline, second.item.textDecoration)
}

@Test
fun `should handle mixed decoration and non-decoration on same range`() {
// Given: bold (no decoration) + strikethrough (decoration) on same range
val spans = listOf(
ChatBoxSpan.Markup(
style = SpanStyle(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold),
start = 0,
end = 5,
type = ChatBoxSpan.Markup.BOLD
),
ChatBoxSpan.Markup(
style = SpanStyle(textDecoration = TextDecoration.LineThrough),
start = 0,
end = 5,
type = ChatBoxSpan.Markup.STRIKETHROUGH
)
)

val transformation = AnnotatedTextTransformation(spans)
val input = AnnotatedString("Hello world")

// When: transformation is applied
val result = transformation.filter(input)

// Then: should have one span with bold + one with decoration
val spanStyles = result.text.spanStyles
assertEquals(2, spanStyles.size, "Should have two span styles")

// Verify bold is applied
val boldSpan = spanStyles.find { it.item.fontWeight == androidx.compose.ui.text.font.FontWeight.Bold }
assertNotNull(boldSpan)

// Verify strikethrough is applied
val decorationSpan = spanStyles.find { it.item.textDecoration == TextDecoration.LineThrough }
assertNotNull(decorationSpan)
}
}
Loading