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 0d7afeb..8d16d95 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -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 + ) + ) + } } } } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Room.kt b/shared/src/commonMain/kotlin/su/reya/coop/Room.kt index b1c6c3e..5b7d8eb 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Room.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Room.kt @@ -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 =