feat: render image in chat message #36
@@ -7,12 +7,16 @@ import android.speech.RecognizerIntent
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -97,6 +101,7 @@ import su.reya.coop.Room
|
|||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
import su.reya.coop.extractUrls
|
import su.reya.coop.extractUrls
|
||||||
import su.reya.coop.formatAsGroupHeader
|
import su.reya.coop.formatAsGroupHeader
|
||||||
|
import su.reya.coop.formatAsTime
|
||||||
import su.reya.coop.humanReadable
|
import su.reya.coop.humanReadable
|
||||||
import su.reya.coop.isImageUrl
|
import su.reya.coop.isImageUrl
|
||||||
import su.reya.coop.rememberUiState
|
import su.reya.coop.rememberUiState
|
||||||
@@ -554,6 +559,9 @@ fun ChatMessage(
|
|||||||
val content = rumor.content()
|
val content = rumor.content()
|
||||||
val images = remember(content) { content.extractUrls().filter { it.isImageUrl() } }
|
val images = remember(content) { content.extractUrls().filter { it.isImageUrl() } }
|
||||||
val cleanedContent = remember(content) { content.removeImageUrls() }
|
val cleanedContent = remember(content) { content.removeImageUrls() }
|
||||||
|
val timestamp = remember(rumor) { rumor.createdAt().formatAsTime() }
|
||||||
|
|
||||||
|
var isMessageClicked by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val bubbleShape = if (isMine) {
|
val bubbleShape = if (isMine) {
|
||||||
RoundedCornerShape(topStart = 20.dp, topEnd = 4.dp, bottomStart = 20.dp, bottomEnd = 20.dp)
|
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
|
contentAlignment = if (isMine) Alignment.CenterEnd else Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null
|
||||||
|
) {
|
||||||
|
isMessageClicked = !isMessageClicked
|
||||||
|
},
|
||||||
horizontalAlignment = if (isMine) Alignment.End else Alignment.Start,
|
horizontalAlignment = if (isMine) Alignment.End else Alignment.Start,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,21 +153,25 @@ fun UnsignedEvent.roomId(): Long {
|
|||||||
return sortedUniqueKeys.hashCode().toLong()
|
return sortedUniqueKeys.hashCode().toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Timestamp.ago(): String {
|
fun Timestamp.formatAsTime(): String {
|
||||||
val SECONDS_IN_MINUTE = 60L
|
val timeZone = TimeZone.currentSystemDefault()
|
||||||
val MINUTES_IN_HOUR = 60L
|
val inputInstant = Instant.fromEpochSeconds(this.asSecs().toLong())
|
||||||
val HOURS_IN_DAY = 24L
|
val inputDateTime = inputInstant.toLocalDateTime(timeZone)
|
||||||
val DAYS_IN_MONTH = 30L
|
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 inputInstant = Instant.fromEpochSeconds(this.asSecs().toLong())
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
val duration = now - inputInstant
|
val duration = now - inputInstant
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
duration.inWholeSeconds < SECONDS_IN_MINUTE -> "Now"
|
duration.inWholeSeconds < 60L -> "Now"
|
||||||
duration.inWholeMinutes < MINUTES_IN_HOUR -> "${duration.inWholeMinutes}m"
|
duration.inWholeMinutes < 60L -> "${duration.inWholeMinutes}m"
|
||||||
duration.inWholeHours < HOURS_IN_DAY -> "${duration.inWholeHours}h"
|
duration.inWholeHours < 24L -> "${duration.inWholeHours}h"
|
||||||
duration.inWholeDays < DAYS_IN_MONTH -> "${duration.inWholeDays}d"
|
duration.inWholeDays < 30L -> "${duration.inWholeDays}d"
|
||||||
else -> {
|
else -> {
|
||||||
val localDateTime = inputInstant.toLocalDateTime(TimeZone.currentSystemDefault())
|
val localDateTime = inputInstant.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
val month =
|
val month =
|
||||||
|
|||||||
Reference in New Issue
Block a user