diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt index 90736da..0d7afeb 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -66,12 +66,15 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.compose.AsyncImage import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.ic_add_circle import coop.composeapp.generated.resources.ic_arrow_back @@ -92,9 +95,12 @@ import su.reya.coop.LocalNostrViewModel import su.reya.coop.LocalSnackbarHostState import su.reya.coop.Room import su.reya.coop.Screen +import su.reya.coop.extractUrls import su.reya.coop.formatAsGroupHeader import su.reya.coop.humanReadable +import su.reya.coop.isImageUrl import su.reya.coop.rememberUiState +import su.reya.coop.removeImageUrls import su.reya.coop.roomId import su.reya.coop.shared.Avatar import su.reya.coop.shared.getExpressiveFontFamily @@ -545,17 +551,21 @@ fun ChatMessage( rumor: UnsignedEvent, isMine: Boolean = false, ) { + val content = rumor.content() + val images = remember(content) { content.extractUrls().filter { it.isImageUrl() } } + val cleanedContent = remember(content) { content.removeImageUrls() } + val bubbleShape = if (isMine) { - RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp, bottomStart = 20.dp, bottomEnd = 4.dp) + RoundedCornerShape(topStart = 20.dp, topEnd = 4.dp, bottomStart = 20.dp, bottomEnd = 20.dp) } else { - RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp, bottomStart = 4.dp, bottomEnd = 20.dp) + RoundedCornerShape(topStart = 4.dp, topEnd = 20.dp, bottomStart = 20.dp, bottomEnd = 20.dp) } val containerColor = - if (isMine) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.tertiaryContainer + if (!isMine) MaterialTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.primaryContainer val contentColor = - if (isMine) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onTertiaryContainer + if (!isMine) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onPrimaryContainer Box( modifier = Modifier @@ -564,19 +574,38 @@ fun ChatMessage( contentAlignment = if (isMine) Alignment.CenterEnd else Alignment.CenterStart ) { Column( - horizontalAlignment = if (isMine) Alignment.End else Alignment.Start + horizontalAlignment = if (isMine) Alignment.End else Alignment.Start, + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Surface( - color = containerColor, - contentColor = contentColor, - shape = bubbleShape, - modifier = Modifier.widthIn(max = 280.dp) - ) { - Text( - text = rumor.content(), - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - style = MaterialTheme.typography.bodyMedium - ) + if (cleanedContent.isNotBlank()) { + Surface( + color = containerColor, + contentColor = contentColor, + shape = bubbleShape, + modifier = Modifier.widthIn(max = 280.dp) + ) { + Text( + text = rumor.content(), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = MaterialTheme.typography.bodyLarge + ) + } + } + images.forEach { imageUrl -> + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier.widthIn(max = 280.dp) + ) { + AsyncImage( + model = imageUrl, + contentDescription = "Image from chat", + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)), + contentScale = ContentScale.FillWidth + ) + } } } } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Extensions.kt b/shared/src/commonMain/kotlin/su/reya/coop/Extensions.kt index 3c5d624..7952b10 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Extensions.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Extensions.kt @@ -6,3 +6,21 @@ fun PublicKey.short(): String { val bech32 = toBech32() return bech32.substring(0, 6) + "..." + bech32.substring(bech32.length - 4) } + +private val urlRegex = Regex("(https?://\\S+)", RegexOption.IGNORE_CASE) +private val imageExtensions = setOf("jpg", "jpeg", "png", "gif", "webp", "bmp") + +fun String.extractUrls(): List { + return urlRegex.findAll(this).map { it.value }.toList() +} + +fun String.removeImageUrls(): String { + return urlRegex.replace(this) { result -> + if (result.value.isImageUrl()) "" else result.value + }.replace(Regex("\\s+"), " ").trim() +} + +fun String.isImageUrl(): Boolean { + val extension = this.substringAfterLast('.', "").lowercase() + return extension in imageExtensions +}