add image render in chat message
This commit is contained in:
@@ -66,12 +66,15 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
import coop.composeapp.generated.resources.Res
|
import coop.composeapp.generated.resources.Res
|
||||||
import coop.composeapp.generated.resources.ic_add_circle
|
import coop.composeapp.generated.resources.ic_add_circle
|
||||||
import coop.composeapp.generated.resources.ic_arrow_back
|
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.LocalSnackbarHostState
|
||||||
import su.reya.coop.Room
|
import su.reya.coop.Room
|
||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
|
import su.reya.coop.extractUrls
|
||||||
import su.reya.coop.formatAsGroupHeader
|
import su.reya.coop.formatAsGroupHeader
|
||||||
import su.reya.coop.humanReadable
|
import su.reya.coop.humanReadable
|
||||||
|
import su.reya.coop.isImageUrl
|
||||||
import su.reya.coop.rememberUiState
|
import su.reya.coop.rememberUiState
|
||||||
|
import su.reya.coop.removeImageUrls
|
||||||
import su.reya.coop.roomId
|
import su.reya.coop.roomId
|
||||||
import su.reya.coop.shared.Avatar
|
import su.reya.coop.shared.Avatar
|
||||||
import su.reya.coop.shared.getExpressiveFontFamily
|
import su.reya.coop.shared.getExpressiveFontFamily
|
||||||
@@ -545,17 +551,21 @@ fun ChatMessage(
|
|||||||
rumor: UnsignedEvent,
|
rumor: UnsignedEvent,
|
||||||
isMine: Boolean = false,
|
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) {
|
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 {
|
} 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 =
|
val containerColor =
|
||||||
if (isMine) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.tertiaryContainer
|
if (!isMine) MaterialTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.primaryContainer
|
||||||
|
|
||||||
val contentColor =
|
val contentColor =
|
||||||
if (isMine) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onTertiaryContainer
|
if (!isMine) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -564,19 +574,38 @@ fun ChatMessage(
|
|||||||
contentAlignment = if (isMine) Alignment.CenterEnd else Alignment.CenterStart
|
contentAlignment = if (isMine) Alignment.CenterEnd else Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = if (isMine) Alignment.End else Alignment.Start
|
horizontalAlignment = if (isMine) Alignment.End else Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Surface(
|
if (cleanedContent.isNotBlank()) {
|
||||||
color = containerColor,
|
Surface(
|
||||||
contentColor = contentColor,
|
color = containerColor,
|
||||||
shape = bubbleShape,
|
contentColor = contentColor,
|
||||||
modifier = Modifier.widthIn(max = 280.dp)
|
shape = bubbleShape,
|
||||||
) {
|
modifier = Modifier.widthIn(max = 280.dp)
|
||||||
Text(
|
) {
|
||||||
text = rumor.content(),
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
text = rumor.content(),
|
||||||
style = MaterialTheme.typography.bodyMedium
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,21 @@ fun PublicKey.short(): String {
|
|||||||
val bech32 = toBech32()
|
val bech32 = toBech32()
|
||||||
return bech32.substring(0, 6) + "..." + bech32.substring(bech32.length - 4)
|
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<String> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user