feat: render image in chat message #36

Merged
reya merged 5 commits from feat/render-image into master 2026-07-04 02:25:10 +00:00
2 changed files with 41 additions and 9 deletions
Showing only changes of commit 77d422f20e - Show all commits

View File

@@ -7,12 +7,16 @@ import android.speech.RecognizerIntent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -97,6 +101,7 @@ import su.reya.coop.Room
import su.reya.coop.Screen
import su.reya.coop.extractUrls
import su.reya.coop.formatAsGroupHeader
import su.reya.coop.formatAsTime
import su.reya.coop.humanReadable
import su.reya.coop.isImageUrl
import su.reya.coop.rememberUiState
@@ -554,6 +559,9 @@ fun ChatMessage(
val content = rumor.content()
val images = remember(content) { content.extractUrls().filter { it.isImageUrl() } }
val cleanedContent = remember(content) { content.removeImageUrls() }
val timestamp = remember(rumor) { rumor.createdAt().formatAsTime() }
var isMessageClicked by remember { mutableStateOf(false) }
val bubbleShape = if (isMine) {
RoundedCornerShape(topStart = 20.dp, topEnd = 4.dp, bottomStart = 20.dp, bottomEnd = 20.dp)
@@ -574,6 +582,12 @@ fun ChatMessage(
contentAlignment = if (isMine) Alignment.CenterEnd else Alignment.CenterStart
) {
Column(
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
isMessageClicked = !isMessageClicked
},
horizontalAlignment = if (isMine) Alignment.End else Alignment.Start,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -607,6 +621,20 @@ fun ChatMessage(
)
}
}
AnimatedVisibility(
visible = isMessageClicked,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Text(
text = timestamp,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.align(
if (isMine) Alignment.End else Alignment.Start
)
)
}
}
}
}

View File

@@ -153,21 +153,25 @@ fun UnsignedEvent.roomId(): Long {
return sortedUniqueKeys.hashCode().toLong()
}
fun Timestamp.ago(): String {
val SECONDS_IN_MINUTE = 60L
val MINUTES_IN_HOUR = 60L
val HOURS_IN_DAY = 24L
val DAYS_IN_MONTH = 30L
fun Timestamp.formatAsTime(): String {
val timeZone = TimeZone.currentSystemDefault()
val inputInstant = Instant.fromEpochSeconds(this.asSecs().toLong())
val inputDateTime = inputInstant.toLocalDateTime(timeZone)
val hour = inputDateTime.hour.toString().padStart(2, '0')
val minute = inputDateTime.minute.toString().padStart(2, '0')
return "$hour:$minute"
}
fun Timestamp.ago(): String {
val inputInstant = Instant.fromEpochSeconds(this.asSecs().toLong())
val now = Clock.System.now()
val duration = now - inputInstant
return when {
duration.inWholeSeconds < SECONDS_IN_MINUTE -> "Now"
duration.inWholeMinutes < MINUTES_IN_HOUR -> "${duration.inWholeMinutes}m"
duration.inWholeHours < HOURS_IN_DAY -> "${duration.inWholeHours}h"
duration.inWholeDays < DAYS_IN_MONTH -> "${duration.inWholeDays}d"
duration.inWholeSeconds < 60L -> "Now"
duration.inWholeMinutes < 60L -> "${duration.inWholeMinutes}m"
duration.inWholeHours < 24L -> "${duration.inWholeHours}h"
duration.inWholeDays < 30L -> "${duration.inWholeDays}d"
else -> {
val localDateTime = inputInstant.toLocalDateTime(TimeZone.currentSystemDefault())
val month =