Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: grouping message dates (WPB-1733) 🍒 #2972

Merged
merged 2 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.wire.android.ui.home.conversations

import android.annotation.SuppressLint
import android.net.Uri
import android.text.format.DateUtils
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn
Expand All @@ -28,6 +29,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
Expand All @@ -46,9 +48,11 @@ import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -142,8 +146,11 @@ import com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder
import com.wire.android.ui.home.messagecomposer.state.rememberMessageComposerStateHolder
import com.wire.android.ui.legalhold.dialog.subject.LegalHoldSubjectMessageDialog
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.MessageDateTimeGroup
import com.wire.android.util.normalizeLink
import com.wire.android.util.permission.PermissionDenialType
import com.wire.android.util.serverDate
import com.wire.android.util.ui.UIText
import com.wire.android.util.ui.openDownloadFolder
import com.wire.kalium.logic.NetworkFailure
Expand All @@ -164,6 +171,8 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import java.util.Date
import java.util.Locale
import kotlin.time.Duration.Companion.milliseconds

/**
Expand Down Expand Up @@ -977,6 +986,7 @@ fun MessageList(
) {
val prevItemCount = remember { mutableStateOf(lazyPagingMessages.itemCount) }
val readLastMessageAtStartTriggered = remember { mutableStateOf(false) }
val currentTime by currentTimeInMillisFlow.collectAsState(initial = System.currentTimeMillis())

LaunchedEffect(lazyPagingMessages.itemCount) {
if (lazyPagingMessages.itemCount > prevItemCount.value && selectedMessageId == null) {
Expand Down Expand Up @@ -1048,6 +1058,23 @@ fun MessageList(
val showAuthor = rememberShouldShowHeader(index, message, lazyPagingMessages)
val useSmallBottomPadding = rememberShouldHaveSmallBottomPadding(index, message, lazyPagingMessages)

if (index > 0) {
val previousMessage = lazyPagingMessages[index - 1] ?: message

val currentGroup = message.header.messageTime.getFormattedDateGroup(now = currentTime)
val previousGroup = previousMessage.header.messageTime.getFormattedDateGroup(now = currentTime)

if (currentGroup != previousGroup) {
previousMessage.header.messageTime.utcISO.serverDate()?.let { serverDate ->
MessageGroupDateTime(
messageDateTime = serverDate,
messageDateTimeGroup = previousGroup,
now = currentTime
)
}
}
}

MessageContainerItem(
message = message,
conversationDetailsData = conversationDetailsData,
Expand Down Expand Up @@ -1075,8 +1102,7 @@ fun MessageList(
}
),
isSelectedMessage = (message.header.messageId == selectedMessageId),
isInteractionAvailable = interactionAvailability == InteractionAvailability.ENABLED,
currentTimeInMillisFlow = currentTimeInMillisFlow
isInteractionAvailable = interactionAvailability == InteractionAvailability.ENABLED
)
}
}
Expand All @@ -1085,6 +1111,76 @@ fun MessageList(
)
}

@Composable
private fun MessageGroupDateTime(
now: Long,
messageDateTime: Date,
messageDateTimeGroup: MessageDateTimeGroup?
) {
val context = LocalContext.current

val timeString = when (messageDateTimeGroup) {
is MessageDateTimeGroup.Now -> context.resources.getString(R.string.message_datetime_now)
is MessageDateTimeGroup.Within30Minutes -> DateUtils.getRelativeTimeSpanString(
messageDateTime.time,
now,
DateUtils.MINUTE_IN_MILLIS
).toString()
is MessageDateTimeGroup.Daily -> {
when (messageDateTimeGroup.type) {
MessageDateTimeGroup.Daily.Type.Today -> DateUtils.getRelativeDateTimeString(
context,
messageDateTime.time,
DateUtils.DAY_IN_MILLIS,
DateUtils.DAY_IN_MILLIS,
0
).toString()
MessageDateTimeGroup.Daily.Type.Yesterday ->
DateUtils.getRelativeDateTimeString(
context,
messageDateTime.time,
DateUtils.DAY_IN_MILLIS,
DateUtils.DAY_IN_MILLIS * 2,
0
).toString()
MessageDateTimeGroup.Daily.Type.WithinWeek -> DateUtils.formatDateTime(
context,
messageDateTime.time,
DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME
)
MessageDateTimeGroup.Daily.Type.NotWithinWeekButSameYear -> DateUtils.formatDateTime(
context,
messageDateTime.time,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME
)
MessageDateTimeGroup.Daily.Type.Other -> DateUtils.formatDateTime(
context,
messageDateTime.time,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_TIME
)
}
}
null -> ""
}

Row(
Modifier
.fillMaxWidth()
.background(color = colorsScheme().divider)
.padding(
top = dimensions().spacing6x,
bottom = dimensions().spacing6x,
start = dimensions().spacing56x
)
) {
Text(
text = timeString.uppercase(Locale.getDefault()),
color = colorsScheme().secondaryText,
style = MaterialTheme.wireTypography.title03,
)
}
}

private fun updateLastReadMessage(
lastVisibleMessage: UIMessage,
lastUnreadMessageInstant: Instant?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ import com.wire.kalium.logic.data.asset.AssetTransferStatus
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.user.UserId
import kotlinx.collections.immutable.PersistentMap
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

@OptIn(ExperimentalFoundationApi::class)
@Suppress("ComplexMethod")
Expand Down Expand Up @@ -79,8 +77,7 @@ fun MessageContainerItem(
shouldDisplayFooter: Boolean = true,
onReplyClickable: Clickable? = null,
isSelectedMessage: Boolean = false,
isInteractionAvailable: Boolean = true,
currentTimeInMillisFlow: Flow<Long> = flow { },
isInteractionAvailable: Boolean = true
) {
val selfDeletionTimerState = rememberSelfDeletionTimer(message.header.messageStatus.expirationStatus)
if (
Expand Down Expand Up @@ -158,8 +155,7 @@ fun MessageContainerItem(
shouldDisplayMessageStatus = shouldDisplayMessageStatus,
shouldDisplayFooter = shouldDisplayFooter,
selfDeletionTimerState = selfDeletionTimerState,
useSmallBottomPadding = useSmallBottomPadding,
currentTimeInMillisFlow = currentTimeInMillisFlow
useSmallBottomPadding = useSmallBottomPadding
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import androidx.compose.material3.SwipeToDismissBoxState
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -84,7 +83,6 @@ import com.wire.android.ui.home.conversations.model.MessageHeader
import com.wire.android.ui.home.conversations.model.MessageImage
import com.wire.android.ui.home.conversations.model.MessageSource
import com.wire.android.ui.home.conversations.model.MessageStatus
import com.wire.android.ui.home.conversations.model.MessageTime
import com.wire.android.ui.home.conversations.model.UIMessage
import com.wire.android.ui.home.conversations.model.UIMessageContent
import com.wire.android.ui.home.conversations.model.UIQuotedMessage
Expand All @@ -96,15 +94,12 @@ import com.wire.android.ui.home.conversations.model.messagetypes.location.Locati
import com.wire.android.ui.theme.Accent
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.MessageDateTime
import com.wire.android.util.launchGeoIntent
import com.wire.kalium.logic.data.asset.AssetTransferStatus
import com.wire.kalium.logic.data.asset.isSaved
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.user.UserId
import kotlinx.collections.immutable.PersistentMap
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlin.math.absoluteValue
import kotlin.math.min

Expand Down Expand Up @@ -136,7 +131,6 @@ fun RegularMessageItem(
onReplyClickable: Clickable? = null,
isInteractionAvailable: Boolean = true,
useSmallBottomPadding: Boolean = false,
currentTimeInMillisFlow: Flow<Long> = flow { },
selfDeletionTimerState: SelfDeletionTimerHelper.SelfDeletionTimerState = SelfDeletionTimerHelper.SelfDeletionTimerState.NotExpirable
): Unit = with(message) {
val onSwipe = remember(message) { { onSwipedToReply(message) } }
Expand All @@ -161,7 +155,7 @@ fun RegularMessageItem(
Column {
if (showAuthor) {
Spacer(modifier = Modifier.height(dimensions().avatarClickablePadding))
MessageAuthorRow(messageHeader = message.header, currentTimeInMillisFlow)
MessageAuthorRow(messageHeader = message.header)
Spacer(modifier = Modifier.height(dimensions().spacing4x))
}
if (selfDeletionTimerState is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) {
Expand Down Expand Up @@ -447,7 +441,7 @@ fun MessageExpireLabel(messageContent: UIMessageContent?, assetTransferStatus: A
}

@Composable
private fun MessageAuthorRow(messageHeader: MessageHeader, currentTimeInMillisFlow: Flow<Long>) {
private fun MessageAuthorRow(messageHeader: MessageHeader) {
with(messageHeader) {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(
Expand All @@ -470,8 +464,7 @@ private fun MessageAuthorRow(messageHeader: MessageHeader, currentTimeInMillisFl
}
}
MessageTimeLabel(
messageTime = messageHeader.messageTime,
currentTimeInMillisFlow = currentTimeInMillisFlow,
messageTime = messageHeader.messageTime.formattedDate,
modifier = Modifier.padding(start = dimensions().spacing6x)
)
}
Expand Down Expand Up @@ -510,35 +503,11 @@ private fun MessageFooter(

@Composable
private fun MessageTimeLabel(
messageTime: MessageTime,
currentTimeInMillisFlow: Flow<Long>,
messageTime: String,
modifier: Modifier = Modifier
) {

val currentTime by currentTimeInMillisFlow.collectAsState(initial = System.currentTimeMillis())

val messageDateTime = messageTime.formattedDate(now = currentTime)

val context = LocalContext.current

val timeString = when (messageDateTime) {
is MessageDateTime.Now -> context.resources.getString(R.string.message_datetime_now)
is MessageDateTime.Within30Minutes -> context.resources.getQuantityString(
R.plurals.message_datetime_minutes_ago,
messageDateTime.minutes,
messageDateTime.minutes
)

is MessageDateTime.Today -> context.resources.getString(R.string.message_datetime_today, messageDateTime.time)
is MessageDateTime.Yesterday -> context.resources.getString(R.string.message_datetime_yesterday, messageDateTime.time)
is MessageDateTime.WithinWeek -> context.resources.getString(R.string.message_datetime_other, messageDateTime.date)
is MessageDateTime.NotWithinWeekButSameYear -> context.resources.getString(R.string.message_datetime_other, messageDateTime.date)
is MessageDateTime.Other -> context.resources.getString(R.string.message_datetime_other, messageDateTime.date)
null -> ""
}

Text(
text = timeString,
text = messageTime,
style = MaterialTheme.typography.labelSmall.copy(color = MaterialTheme.wireColorScheme.secondaryText),
maxLines = 1,
modifier = modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration
import com.wire.android.ui.markdown.MarkdownConstants
import com.wire.android.ui.theme.Accent
import com.wire.android.util.Copyable
import com.wire.android.util.MessageDateTime
import com.wire.android.util.MessageDateTimeGroup
import com.wire.android.util.groupedUIMessageDateTime
import com.wire.android.util.ui.LocalizedStringResource
import com.wire.android.util.ui.UIText
import com.wire.android.util.uiMessageDateTime
Expand Down Expand Up @@ -622,7 +623,8 @@ enum class MessageSource {
}

data class MessageTime(val utcISO: String) {
fun formattedDate(now: Long): MessageDateTime? = utcISO.uiMessageDateTime(now = now)
val formattedDate: String = utcISO.uiMessageDateTime() ?: ""
fun getFormattedDateGroup(now: Long): MessageDateTimeGroup? = utcISO.groupedUIMessageDateTime(now = now)
}

@Stable
Expand Down
Loading
Loading