mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-3096 Chats | Enhancement | Render markup in chat bubbles (#1836)
This commit is contained in:
parent
cd714a5856
commit
69a6819814
8 changed files with 342 additions and 78 deletions
|
@ -78,6 +78,9 @@ class DiscussionFragment : BaseComposeFragment() {
|
|||
},
|
||||
onBackButtonClicked = {
|
||||
// TODO
|
||||
},
|
||||
onMarkupLinkClicked = {
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -37,6 +37,8 @@ import com.anytypeio.anytype.core_ui.extensions.throttledClick
|
|||
import com.anytypeio.anytype.core_utils.ext.arg
|
||||
import com.anytypeio.anytype.core_utils.ext.argOrNull
|
||||
import com.anytypeio.anytype.core_utils.ext.toast
|
||||
import com.anytypeio.anytype.core_utils.intents.SystemAction
|
||||
import com.anytypeio.anytype.core_utils.intents.proceedWithAction
|
||||
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
|
||||
import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
|
@ -194,6 +196,11 @@ class HomeScreenFragment : BaseComposeFragment(),
|
|||
},
|
||||
onBackButtonClicked = {
|
||||
findNavController().popBackStack()
|
||||
},
|
||||
onMarkupLinkClicked = {
|
||||
proceedWithAction(
|
||||
SystemAction.OpenUrl(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package com.anytypeio.anytype.core_ui.text
|
||||
|
||||
import com.anytypeio.anytype.core_models.Block
|
||||
|
||||
fun String.splitByMarks(
|
||||
marks: List<Block.Content.Text.Mark>
|
||||
) : List<Pair<String, List<Block.Content.Text.Mark>>> {
|
||||
|
||||
if (this.isEmpty()) return emptyList()
|
||||
|
||||
// Create a map to track styles applied to each character index
|
||||
val styleMap = mutableMapOf<Int, MutableList<Block.Content.Text.Mark>>()
|
||||
|
||||
// Populate the style map with styles for each index in the ranges
|
||||
for (styledRange in marks) {
|
||||
for (index in styledRange.range) {
|
||||
if (index in indices) {
|
||||
// Add the style to the current index, initializing the list if needed
|
||||
styleMap.computeIfAbsent(index) { mutableListOf() }.add(styledRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result = mutableListOf<Pair<String, List<Block.Content.Text.Mark>>>()
|
||||
var currentIndex = 0
|
||||
|
||||
while (currentIndex < length) {
|
||||
// Get the styles at the current index (or an empty list if none)
|
||||
val stylesAtCurrentIndex = styleMap[currentIndex] ?: emptyList()
|
||||
|
||||
|
||||
// Find the extent of this style group (i.e., where styles change)
|
||||
var endIndex = currentIndex
|
||||
while (endIndex < length && (styleMap[endIndex] ?: emptyList()) == stylesAtCurrentIndex) {
|
||||
endIndex++
|
||||
}
|
||||
|
||||
// Add the substring and its associated styles to the result
|
||||
result.add(substring(currentIndex until endIndex) to stylesAtCurrentIndex)
|
||||
|
||||
// Move to the next group
|
||||
currentIndex = endIndex
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package com.anytypeio.anytype.core_ui.text
|
||||
|
||||
import com.anytypeio.anytype.core_models.Block.Content.Text.Mark
|
||||
import kotlin.test.Test
|
||||
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SplitByMarkTests {
|
||||
|
||||
@Test
|
||||
fun testNoMarks() {
|
||||
val input = "Hello, world!"
|
||||
val marks = emptyList<Mark>()
|
||||
val result = input.splitByMarks(marks)
|
||||
val expected = listOf(input to emptyList<Mark>())
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSingleMark() {
|
||||
val input = "Hello, world!"
|
||||
val marks = listOf(
|
||||
Mark(0..4, Mark.Type.BOLD) // "Hello" styled as bold
|
||||
)
|
||||
val result = input.splitByMarks(marks)
|
||||
val expected = listOf(
|
||||
"Hello" to listOf(marks[0]),
|
||||
", world!" to emptyList()
|
||||
)
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOverlappingMarks() {
|
||||
val input = "Hello, world!"
|
||||
val marks = listOf(
|
||||
Mark(0..4, Mark.Type.BOLD), // "Hello" styled as bold
|
||||
Mark(3..7, Mark.Type.ITALIC) // Overlaps "lo, w" with italic
|
||||
)
|
||||
val result = input.splitByMarks(marks)
|
||||
val expected = listOf(
|
||||
"Hel" to listOf(marks[0]),
|
||||
"lo" to listOf(marks[0], marks[1]),
|
||||
", w" to listOf(marks[1]),
|
||||
"orld!" to emptyList()
|
||||
)
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleAdjacentMarks() {
|
||||
val input = "Hello, world!"
|
||||
val marks = listOf(
|
||||
Mark(0..4, Mark.Type.BOLD), // "Hello" styled as bold
|
||||
Mark(7..11, Mark.Type.ITALIC) // "world" styled as italic
|
||||
)
|
||||
val result = input.splitByMarks(marks)
|
||||
val expected = listOf(
|
||||
"Hello" to listOf(marks[0]),
|
||||
", " to emptyList(),
|
||||
"world" to listOf(marks[1]),
|
||||
"!" to emptyList()
|
||||
)
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOutOfBoundsMarks() {
|
||||
val input = "Short text"
|
||||
val marks = listOf(
|
||||
Mark(0..4, Mark.Type.BOLD), // Valid range
|
||||
Mark(10..15, Mark.Type.ITALIC) // Out-of-bounds, should be ignored
|
||||
)
|
||||
val result = input.splitByMarks(marks)
|
||||
val expected = listOf(
|
||||
"Short" to listOf(marks[0]),
|
||||
" text" to emptyList()
|
||||
)
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmptyString() {
|
||||
val input = ""
|
||||
val marks = listOf(
|
||||
Mark(0..4, Mark.Type.BOLD) // Should be ignored since the input is empty
|
||||
)
|
||||
val result = input.splitByMarks(marks)
|
||||
val expected = emptyList<Pair<String, List<Mark>>>()
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFullyOverlappingMarks() {
|
||||
val input = "Overlap test"
|
||||
val marks = listOf(
|
||||
Mark(0..11, Mark.Type.BOLD), // Full range
|
||||
Mark(0..11, Mark.Type.ITALIC) // Fully overlaps with another style
|
||||
)
|
||||
val result = input.splitByMarks(marks)
|
||||
val expected = listOf(
|
||||
"Overlap test" to marks
|
||||
)
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNestedMarks() {
|
||||
val input = "Nested styles"
|
||||
val marks = listOf(
|
||||
Mark(0..5, Mark.Type.BOLD), // "Nested" styled as bold
|
||||
Mark(4..13, Mark.Type.ITALIC) // Overlaps "ed styles" with italic
|
||||
)
|
||||
val result = input.splitByMarks(marks)
|
||||
val expected = listOf(
|
||||
"Nest" to listOf(marks[0]),
|
||||
"ed" to listOf(marks[0], marks[1]),
|
||||
" styles" to listOf(marks[1])
|
||||
)
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
package com.anytypeio.anytype.feature_discussions.presentation
|
||||
|
||||
import com.anytypeio.anytype.core_models.Block
|
||||
import com.anytypeio.anytype.core_models.Hash
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
|
||||
sealed interface DiscussionView {
|
||||
data class Message(
|
||||
val id: String,
|
||||
val content: String,
|
||||
val content: List<Content.Part>,
|
||||
val author: String,
|
||||
val timestamp: Long,
|
||||
val attachments: List<Chat.Message.Attachment> = emptyList(),
|
||||
|
@ -15,6 +16,20 @@ sealed interface DiscussionView {
|
|||
val isEdited: Boolean = false,
|
||||
val avatar: Avatar = Avatar.Initials()
|
||||
) : DiscussionView {
|
||||
|
||||
interface Content {
|
||||
data class Part(
|
||||
val part: String,
|
||||
val styles: List<Block.Content.Text.Mark> = emptyList()
|
||||
) : Content {
|
||||
val isBold: Boolean = styles.any { it.type == Block.Content.Text.Mark.Type.BOLD }
|
||||
val isItalic: Boolean = styles.any { it.type == Block.Content.Text.Mark.Type.ITALIC }
|
||||
val isStrike = styles.any { it.type == Block.Content.Text.Mark.Type.STRIKETHROUGH }
|
||||
val underline = styles.any { it.type == Block.Content.Text.Mark.Type.UNDERLINE }
|
||||
val link = styles.find { it.type == Block.Content.Text.Mark.Type.LINK }
|
||||
}
|
||||
}
|
||||
|
||||
data class Reaction(
|
||||
val emoji: String,
|
||||
val count: Int,
|
||||
|
|
|
@ -7,7 +7,7 @@ import com.anytypeio.anytype.core_models.ObjectWrapper
|
|||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.primitives.Space
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_ui.text.splitByMarks
|
||||
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
|
||||
import com.anytypeio.anytype.domain.base.fold
|
||||
import com.anytypeio.anytype.domain.base.onFailure
|
||||
|
@ -114,14 +114,24 @@ class DiscussionViewModel @Inject constructor(
|
|||
is Store.Data -> type.members.find { member ->
|
||||
member.identity == msg.creator
|
||||
}
|
||||
|
||||
is Store.Empty -> null
|
||||
}
|
||||
}
|
||||
|
||||
val content = msg.content
|
||||
|
||||
DiscussionView.Message(
|
||||
id = msg.id,
|
||||
timestamp = msg.createdAt * 1000,
|
||||
content = msg.content?.text.orEmpty(),
|
||||
content = content?.text
|
||||
.orEmpty()
|
||||
.splitByMarks(marks = content?.marks.orEmpty())
|
||||
.map { (part, styles) ->
|
||||
DiscussionView.Message.Content.Part(
|
||||
part = part,
|
||||
styles = styles
|
||||
)
|
||||
},
|
||||
author = member?.name ?: msg.creator.takeLast(5),
|
||||
isUserAuthor = msg.creator == account,
|
||||
isEdited = msg.modifiedAt > msg.createdAt,
|
||||
|
|
|
@ -19,19 +19,31 @@ fun DiscussionPreview() {
|
|||
messages = listOf(
|
||||
DiscussionView.Message(
|
||||
id = "1",
|
||||
content = stringResource(id = R.string.default_text_placeholder),
|
||||
content = listOf(
|
||||
DiscussionView.Message.Content.Part(
|
||||
part = stringResource(id = R.string.default_text_placeholder)
|
||||
)
|
||||
),
|
||||
author = "Walter",
|
||||
timestamp = System.currentTimeMillis()
|
||||
),
|
||||
DiscussionView.Message(
|
||||
id = "2",
|
||||
content = stringResource(id = R.string.default_text_placeholder),
|
||||
content = listOf(
|
||||
DiscussionView.Message.Content.Part(
|
||||
part = stringResource(id = R.string.default_text_placeholder)
|
||||
)
|
||||
),
|
||||
author = "Leo",
|
||||
timestamp = System.currentTimeMillis()
|
||||
),
|
||||
DiscussionView.Message(
|
||||
id = "3",
|
||||
content = stringResource(id = R.string.default_text_placeholder),
|
||||
content = listOf(
|
||||
DiscussionView.Message.Content.Part(
|
||||
part = stringResource(id = R.string.default_text_placeholder)
|
||||
)
|
||||
),
|
||||
author = "Gilbert",
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
|
@ -44,7 +56,10 @@ fun DiscussionPreview() {
|
|||
onDeleteMessage = {},
|
||||
onCopyMessage = {},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {}
|
||||
onEditMessage = {},
|
||||
onMarkupLinkClicked = {
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -59,7 +74,11 @@ fun DiscussionScreenPreview() {
|
|||
add(
|
||||
DiscussionView.Message(
|
||||
id = idx.toString(),
|
||||
content = stringResource(id = R.string.default_text_placeholder),
|
||||
content = listOf(
|
||||
DiscussionView.Message.Content.Part(
|
||||
part = stringResource(id = R.string.default_text_placeholder)
|
||||
)
|
||||
),
|
||||
author = "User ${idx.inc()}",
|
||||
timestamp =
|
||||
System.currentTimeMillis()
|
||||
|
@ -82,7 +101,8 @@ fun DiscussionScreenPreview() {
|
|||
onEditMessage = {},
|
||||
onExitEditMessageMode = {},
|
||||
isSpaceLevelChat = true,
|
||||
onBackButtonClicked = {}
|
||||
onBackButtonClicked = {},
|
||||
onMarkupLinkClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -92,13 +112,18 @@ fun DiscussionScreenPreview() {
|
|||
fun BubblePreview() {
|
||||
Bubble(
|
||||
name = "Leo Marx",
|
||||
msg = stringResource(id = R.string.default_text_placeholder),
|
||||
msg = listOf(
|
||||
DiscussionView.Message.Content.Part(
|
||||
part = stringResource(id = R.string.default_text_placeholder)
|
||||
)
|
||||
),
|
||||
timestamp = System.currentTimeMillis(),
|
||||
onReacted = {},
|
||||
onDeleteMessage = {},
|
||||
onCopyMessage = {},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {}
|
||||
onEditMessage = {},
|
||||
onMarkupLinkClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -108,14 +133,19 @@ fun BubblePreview() {
|
|||
fun BubbleEditedPreview() {
|
||||
Bubble(
|
||||
name = "Leo Marx",
|
||||
msg = stringResource(id = R.string.default_text_placeholder),
|
||||
msg = listOf(
|
||||
DiscussionView.Message.Content.Part(
|
||||
part = stringResource(id = R.string.default_text_placeholder)
|
||||
)
|
||||
),
|
||||
isEdited = true,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
onReacted = {},
|
||||
onDeleteMessage = {},
|
||||
onCopyMessage = {},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {}
|
||||
onEditMessage = {},
|
||||
onMarkupLinkClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -125,7 +155,11 @@ fun BubbleEditedPreview() {
|
|||
fun BubbleWithAttachmentPreview() {
|
||||
Bubble(
|
||||
name = "Leo Marx",
|
||||
msg = stringResource(id = R.string.default_text_placeholder),
|
||||
msg = listOf(
|
||||
DiscussionView.Message.Content.Part(
|
||||
part = stringResource(id = R.string.default_text_placeholder)
|
||||
)
|
||||
),
|
||||
timestamp = System.currentTimeMillis(),
|
||||
onReacted = {},
|
||||
onDeleteMessage = {},
|
||||
|
@ -139,6 +173,7 @@ fun BubbleWithAttachmentPreview() {
|
|||
)
|
||||
},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {}
|
||||
onEditMessage = {},
|
||||
onMarkupLinkClicked = {}
|
||||
)
|
||||
}
|
|
@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.FlowRow
|
|||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
@ -27,9 +26,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
|||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
|
@ -72,16 +69,21 @@ import androidx.compose.ui.res.colorResource
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
|
@ -127,7 +129,8 @@ fun DiscussionScreenWrapper(
|
|||
vm: DiscussionViewModel,
|
||||
// TODO move to view model
|
||||
onAttachClicked: () -> Unit,
|
||||
onBackButtonClicked: () -> Unit
|
||||
onBackButtonClicked: () -> Unit,
|
||||
onMarkupLinkClicked: (String) -> Unit
|
||||
) {
|
||||
NavHost(
|
||||
navController = rememberNavController(),
|
||||
|
@ -164,7 +167,7 @@ fun DiscussionScreenWrapper(
|
|||
onReacted = vm::onReacted,
|
||||
onCopyMessage = { msg ->
|
||||
clipboard.setText(
|
||||
AnnotatedString(text = msg.content)
|
||||
AnnotatedString(text = msg.content.joinToString())
|
||||
)
|
||||
},
|
||||
onDeleteMessage = vm::onDeleteMessage,
|
||||
|
@ -172,7 +175,8 @@ fun DiscussionScreenWrapper(
|
|||
onAttachmentClicked = vm::onAttachmentClicked,
|
||||
isInEditMessageMode = vm.chatBoxMode.collectAsState().value is ChatBoxMode.EditMessage,
|
||||
onExitEditMessageMode = vm::onExitEditMessageMode,
|
||||
onBackButtonClicked = onBackButtonClicked
|
||||
onBackButtonClicked = onBackButtonClicked,
|
||||
onMarkupLinkClicked = onMarkupLinkClicked
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
vm.commands.collect { command ->
|
||||
|
@ -212,7 +216,8 @@ fun DiscussionScreen(
|
|||
onCopyMessage: (DiscussionView.Message) -> Unit,
|
||||
onEditMessage: (DiscussionView.Message) -> Unit,
|
||||
onAttachmentClicked: (Chat.Message.Attachment) -> Unit,
|
||||
onExitEditMessageMode: () -> Unit
|
||||
onExitEditMessageMode: () -> Unit,
|
||||
onMarkupLinkClicked: (String) -> Unit
|
||||
) {
|
||||
var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(""))
|
||||
|
@ -258,12 +263,13 @@ fun DiscussionScreen(
|
|||
onEditMessage = { msg ->
|
||||
onEditMessage(msg).also {
|
||||
textState = TextFieldValue(
|
||||
msg.content,
|
||||
selection = TextRange(msg.content.length)
|
||||
msg.content.joinToString(),
|
||||
selection = TextRange(msg.content.joinToString().length)
|
||||
)
|
||||
chatBoxFocusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
},
|
||||
onMarkupLinkClicked = onMarkupLinkClicked
|
||||
)
|
||||
// Jump to bottom button shows up when user scrolls past a threshold.
|
||||
// Convert to pixels:
|
||||
|
@ -282,7 +288,7 @@ fun DiscussionScreen(
|
|||
|
||||
GoToBottomButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(end = 12.dp),
|
||||
onGoToBottomClicked = {
|
||||
scope.launch {
|
||||
|
@ -790,7 +796,8 @@ fun Messages(
|
|||
onDeleteMessage: (DiscussionView.Message) -> Unit,
|
||||
onCopyMessage: (DiscussionView.Message) -> Unit,
|
||||
onAttachmentClicked: (Chat.Message.Attachment) -> Unit,
|
||||
onEditMessage: (DiscussionView.Message) -> Unit
|
||||
onEditMessage: (DiscussionView.Message) -> Unit,
|
||||
onMarkupLinkClicked: (String) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
|
@ -839,7 +846,8 @@ fun Messages(
|
|||
onAttachmentClicked = onAttachmentClicked,
|
||||
onEditMessage = {
|
||||
onEditMessage(msg)
|
||||
}
|
||||
},
|
||||
onMarkupLinkClicked = onMarkupLinkClicked
|
||||
)
|
||||
if (msg.isUserAuthor) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
@ -957,7 +965,7 @@ val userMessageBubbleColor = Color(0x66000000)
|
|||
fun Bubble(
|
||||
modifier: Modifier = Modifier,
|
||||
name: String,
|
||||
msg: String,
|
||||
msg: List<DiscussionView.Message.Content.Part>,
|
||||
timestamp: Long,
|
||||
attachments: List<Chat.Message.Attachment> = emptyList(),
|
||||
isUserAuthor: Boolean = false,
|
||||
|
@ -967,7 +975,8 @@ fun Bubble(
|
|||
onDeleteMessage: () -> Unit,
|
||||
onCopyMessage: () -> Unit,
|
||||
onEditMessage: () -> Unit,
|
||||
onAttachmentClicked: (Chat.Message.Attachment) -> Unit
|
||||
onAttachmentClicked: (Chat.Message.Attachment) -> Unit,
|
||||
onMarkupLinkClicked: (String) -> Unit
|
||||
) {
|
||||
var showDropdownMenu by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
|
@ -1014,48 +1023,70 @@ fun Bubble(
|
|||
maxLines = 1
|
||||
)
|
||||
}
|
||||
if (isEdited) {
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
top = 0.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 0.dp
|
||||
),
|
||||
text = buildAnnotatedString {
|
||||
append(msg)
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = if (isUserAuthor)
|
||||
colorResource(id = R.color.text_white)
|
||||
else
|
||||
colorResource(id = R.color.text_primary),
|
||||
)
|
||||
) {
|
||||
append(
|
||||
" (${stringResource(R.string.chats_message_edited)})"
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
top = 0.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 0.dp
|
||||
),
|
||||
text = buildAnnotatedString {
|
||||
msg.forEach { part ->
|
||||
if (part.link != null && part.link.param != null) {
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = "link",
|
||||
styles = TextLinkStyles(
|
||||
style = SpanStyle(
|
||||
fontWeight = if (part.isBold) FontWeight.Bold else null,
|
||||
fontStyle = if (part.isItalic) FontStyle.Italic else null,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
)
|
||||
) {
|
||||
onMarkupLinkClicked(part.link.param.orEmpty())
|
||||
}
|
||||
) {
|
||||
append(part.part)
|
||||
}
|
||||
} else {
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
fontWeight = if (part.isBold) FontWeight.Bold else null,
|
||||
fontStyle = if (part.isItalic) FontStyle.Italic else null,
|
||||
textDecoration = if (part.underline)
|
||||
TextDecoration.Underline
|
||||
else if (part.isStrike)
|
||||
TextDecoration.LineThrough
|
||||
else null,
|
||||
)
|
||||
) {
|
||||
append(part.part)
|
||||
}
|
||||
}
|
||||
},
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
top = 0.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 0.dp
|
||||
),
|
||||
text = msg,
|
||||
style = BodyRegular,
|
||||
color = if (isUserAuthor)
|
||||
colorResource(id = R.color.text_white)
|
||||
else
|
||||
colorResource(id = R.color.text_primary),
|
||||
)
|
||||
}
|
||||
|
||||
if (isEdited) {
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = if (isUserAuthor)
|
||||
colorResource(id = R.color.text_white)
|
||||
else
|
||||
colorResource(id = R.color.text_primary),
|
||||
)
|
||||
) {
|
||||
append(
|
||||
" (${stringResource(R.string.chats_message_edited)})"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
style = BodyRegular,
|
||||
color = if (isUserAuthor)
|
||||
colorResource(id = R.color.text_white)
|
||||
else
|
||||
colorResource(id = R.color.text_primary),
|
||||
)
|
||||
attachments.forEach { attachment ->
|
||||
Attachment(
|
||||
modifier = Modifier.padding(
|
||||
|
@ -1329,12 +1360,7 @@ fun GoToBottomButton(
|
|||
.offset(x = 0.dp, y = -bottomOffset)
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = colorResource(id = R.color.shape_primary),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.background(color = colorResource(id = R.color.background_primary))
|
||||
.background(color = colorResource(id = R.color.navigation_panel))
|
||||
.clickable {
|
||||
onGoToBottomClicked()
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue