Skip to content
Open
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 @@ -21,10 +21,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
Expand All @@ -44,9 +40,6 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.ui.theme.MessageFooterStatusIndicatorParams
import io.getstream.chat.android.compose.ui.theme.MessageStyling
import io.getstream.chat.android.compose.ui.theme.StreamTokens
import io.getstream.chat.android.compose.ui.util.clickable
import io.getstream.chat.android.core.utils.date.truncateFuture
import io.getstream.chat.android.models.Message
import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState
import io.getstream.chat.android.ui.common.utils.extensions.shouldShowMessageStatusIndicator

Expand Down Expand Up @@ -81,11 +74,10 @@ public fun MessageFooter(
)
}

if (messageItem.showMessageFooter) {
val showEditLabel = message.messageTextUpdatedAt != null && !message.isDeleted()
var showEditInfo by remember(message.id, showEditLabel) { mutableStateOf(false) }
val timestampStyle = MessageStyling.timestampStyle()
val showEditLabel = message.messageTextUpdatedAt != null && !message.isDeleted()
val timestampStyle = MessageStyling.timestampStyle()

if (messageItem.showMessageFooter) {
Row(
modifier = Modifier.padding(top = StreamTokens.spacingXs, bottom = StreamTokens.spacing2xs),
verticalAlignment = Alignment.CenterVertically,
Expand Down Expand Up @@ -121,61 +113,27 @@ public fun MessageFooter(
textStyle = timestampStyle,
)
}
if (showEditLabel && !showEditInfo) {
if (showEditLabel) {
Text(
modifier = Modifier
.padding(start = StreamTokens.spacingXs)
.clickable { showEditInfo = !showEditInfo }
.testTag("Stream_MessageEditedLabel"),
text = LocalContext.current.getString(R.string.stream_compose_message_list_footnote_edited),
style = timestampStyle,
)
}
}
if (showEditLabel && showEditInfo) {
Row(
modifier = Modifier
.padding(bottom = StreamTokens.spacing2xs)
.clickable { showEditInfo = !showEditInfo },
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.padding(end = StreamTokens.spacing2xs)
.weight(1f, fill = false),
text = LocalContext.current.getString(R.string.stream_compose_message_list_footnote_edited),
style = timestampStyle,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
MessageEditedTimestamp(message = message)
}
} else if (showEditLabel) {
Row(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Edited" should be shown even when the rest of the footer is not shown

modifier = Modifier.padding(top = StreamTokens.spacingXs, bottom = StreamTokens.spacing2xs),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.testTag("Stream_MessageEditedLabel"),
text = LocalContext.current.getString(R.string.stream_compose_message_list_footnote_edited),
style = timestampStyle,
)
}
}
}
}

/**
* Composable function to display the timestamp for when a message was last edited.
*
* This function adjusts the `message.messageTextUpdatedAt` time to ensure it does not exceed the current system time.
* If the `messageTextUpdatedAt` time is slightly in the future, it resets the time to the current system time.
*
* @param message The message object containing the `messageTextUpdatedAt` timestamp.
* @param modifier Modifier for styling.
* @param formatType The type of formatting to provide. By default, it's [DateFormatType.RELATIVE].
*/
@Composable
internal fun MessageEditedTimestamp(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Showing the edited timestamp is a feature that doesn't exist anymore in the new designs, so I'm also removing that

message: Message,
modifier: Modifier = Modifier,
formatType: DateFormatType = DateFormatType.RELATIVE,
) {
val editedAt = message.messageTextUpdatedAt?.truncateFuture()
Timestamp(
date = editedAt,
modifier = modifier,
formatType = formatType,
textStyle = MessageStyling.timestampStyle(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public class ChannelViewModelFactory(
private val maxAttachmentCount: Int = AttachmentConstants.MAX_ATTACHMENTS_COUNT,
private val showSystemMessages: Boolean = true,
private val deletedMessageVisibility: DeletedMessageVisibility = DeletedMessageVisibility.ALWAYS_VISIBLE,
private val messageFooterVisibility: MessageFooterVisibility = MessageFooterVisibility.WithTimeDifference(),
private val messageFooterVisibility: MessageFooterVisibility = MessageFooterVisibility.LastInGroup,
private val dateSeparatorHandler: DateSeparatorHandler = DateSeparatorHandler.getDefaultDateSeparatorHandler(),
private val threadDateSeparatorHandler: DateSeparatorHandler =
DateSeparatorHandler.getDefaultThreadDateSeparatorHandler(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2769,20 +2769,6 @@ public final class io/getstream/chat/android/ui/common/state/messages/list/Messa
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility$WithTimeDifference : io/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility {
public static final field $stable I
public fun <init> ()V
public fun <init> (J)V
public synthetic fun <init> (JILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()J
public final fun copy (J)Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility$WithTimeDifference;
public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility$WithTimeDifference;JILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility$WithTimeDifference;
public fun equals (Ljava/lang/Object;)Z
public final fun getTimeDifferenceMillis ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/chat/android/ui/common/state/messages/list/MessageItemState : io/getstream/chat/android/ui/common/state/messages/list/HasMessageListItemState {
public static final field $stable I
public fun <init> (Lio/getstream/chat/android/models/Message;Ljava/lang/String;ZZZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/ui/common/state/messages/list/MessagePosition;ZZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFocusState;Ljava/util/List;ZLjava/util/Set;Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ public class MessageListController(
public val channelState: StateFlow<ChannelState?>,
private val deletedMessageVisibility: DeletedMessageVisibility = DeletedMessageVisibility.ALWAYS_VISIBLE,
private val showSystemMessages: Boolean = true,
private val messageFooterVisibility: MessageFooterVisibility = MessageFooterVisibility.WithTimeDifference(),
private val messageFooterVisibility: MessageFooterVisibility = MessageFooterVisibility.LastInGroup,
private val enforceUniqueReactions: Boolean = false,
private val dateSeparatorHandler: DateSeparatorHandler = DateSeparatorHandler.getDefaultDateSeparatorHandler(),
private val threadDateSeparatorHandler: DateSeparatorHandler =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@

package io.getstream.chat.android.ui.common.feature.messages.list

import io.getstream.chat.android.client.extensions.getCreatedAtOrDefault
import io.getstream.chat.android.client.extensions.internal.NEVER
import io.getstream.chat.android.client.utils.message.isError
import io.getstream.chat.android.client.utils.message.isSystem
import io.getstream.chat.android.core.internal.InternalStreamChatApi
import io.getstream.chat.android.models.Message
import io.getstream.chat.android.models.User
import io.getstream.chat.android.ui.common.feature.messages.list.MessagePositionHandler.Companion.DEFAULT_MAX_GROUP_TIME_MILLIS
import io.getstream.chat.android.ui.common.state.messages.list.MessagePosition
import kotlin.math.abs

/**
* A handler to determine the position of a message inside a group.
Expand Down Expand Up @@ -49,14 +53,20 @@ public fun interface MessagePositionHandler {
): MessagePosition

public companion object {
private const val DEFAULT_MAX_GROUP_TIME_MILLIS: Long = 60 * 1000L

/**
* The default implementation of the [MessagePositionHandler] interface which can be taken
* as a reference when implementing a custom one.
*
* @param maxGroupTimeMillis The maximum time difference (in milliseconds) between
* consecutive messages from the same user before they are split into separate groups.
* Defaults to [DEFAULT_MAX_GROUP_TIME_MILLIS] (60 seconds).
*
* @return The default implementation of [MessagePositionHandler].
*/
@InternalStreamChatApi
public fun defaultHandler(): MessagePositionHandler {
public fun defaultHandler(maxGroupTimeMillis: Long = DEFAULT_MAX_GROUP_TIME_MILLIS): MessagePositionHandler {
return MessagePositionHandler {
previous: Message?,
message: Message,
Expand All @@ -65,8 +75,12 @@ public fun interface MessagePositionHandler {
isBeforeDateSeparator: Boolean,
_: Boolean,
->
val isGroupedWithPrevious = !isAfterDateSeparator && previous.isValidMessageFromUser(message.user)
val isGroupedWithNext = !isBeforeDateSeparator && next.isValidMessageFromUser(message.user)
val isGroupedWithPrevious = !isAfterDateSeparator &&
previous.isValidMessageFromUser(message.user) &&
!message.exceedsMaxTimeDifference(previous, maxGroupTimeMillis)
val isGroupedWithNext = !isBeforeDateSeparator &&
next.isValidMessageFromUser(message.user) &&
!next.exceedsMaxTimeDifference(message, maxGroupTimeMillis)

when {
isGroupedWithNext && !isGroupedWithPrevious -> MessagePosition.TOP
Expand All @@ -80,5 +94,12 @@ public fun interface MessagePositionHandler {
private fun Message?.isValidMessageFromUser(user: User): Boolean {
return this != null && !this.isSystem() && !this.isError() && this.user.id == user.id
}

private fun Message?.exceedsMaxTimeDifference(other: Message?, maxMillis: Long): Boolean {
if (this == null || other == null) return false
val thisTime = this.getCreatedAtOrDefault(NEVER).time
val otherTime = other.getCreatedAtOrDefault(NEVER).time
return abs(thisTime - otherTime) > maxMillis
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,10 @@ public sealed class MessageFooterVisibility {
override fun toString(): String = "LastInGroup"
}

/**
* The message footer will be visible to items that are sent inside a specified time duration.
*
* @param timeDifferenceMillis Time duration after which we show the message footer.
*/
public data class WithTimeDifference(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in Slack, this now conflicted with the logic in MessagePositionHandler so I removed it. Note: we still have the option to remove the whole MessageFooterVisibility before releasing v7. It seems kind of an outdated concept now that:

  • MessagePositionHandler handles time-based grouping
  • the footer can be replaced through the component factory

val timeDifferenceMillis: Long = DEFAULT_FOOTER_TIME_DIFF_MILLIS,
) : MessageFooterVisibility()

/**
* The message footer will be visible for every message.
*/
public object Always : MessageFooterVisibility() {
override fun toString(): String = "Always"
}
}

/**
* The default time difference after which the footer is shown.
*/
private const val DEFAULT_FOOTER_TIME_DIFF_MILLIS: Long = 60 * 1000L
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

package io.getstream.chat.android.ui.common.utils.extensions

import io.getstream.chat.android.client.extensions.getCreatedAtOrDefault
import io.getstream.chat.android.client.extensions.internal.NEVER
import io.getstream.chat.android.client.utils.message.isDeleted
import io.getstream.chat.android.core.internal.InternalStreamChatApi
import io.getstream.chat.android.models.Message
import io.getstream.chat.android.ui.common.state.messages.list.MessageFooterVisibility
Expand All @@ -41,42 +38,9 @@ public fun MessageFooterVisibility.shouldShowMessageFooter(
nextMessage: Message?,
): Boolean {
if (nextMessage == null && this != MessageFooterVisibility.Never) return true
if (message.messageTextUpdatedAt != null && this != MessageFooterVisibility.Never) return true
return when (this) {
MessageFooterVisibility.Always -> true
MessageFooterVisibility.LastInGroup -> isLastMessageInGroup
MessageFooterVisibility.Never -> false
is MessageFooterVisibility.WithTimeDifference -> isFooterVisibleWithTimeDifference(
message = message,
nextMessage = nextMessage,
isLastMessageInGroup = isLastMessageInGroup,
timeDifferenceMillis = timeDifferenceMillis,
)
}
}

/**
* @param message The current [Message].
* @param nextMessage The next [Message] in the list if there is one.
* @param isLastMessageInGroup If the message is the last in group of messages.
* @param timeDifferenceMillis The time difference between next and current message.
*
* @return Whether the footer should be visible or not.
*/
private fun isFooterVisibleWithTimeDifference(
message: Message,
nextMessage: Message?,
isLastMessageInGroup: Boolean,
timeDifferenceMillis: Long,
): Boolean {
return when {
isLastMessageInGroup -> true
message.isDeleted() -> false
message.user.id != nextMessage?.user?.id ||
nextMessage.isDeleted() ||
(nextMessage.getCreatedAtOrDefault(NEVER).time) -
(message.getCreatedAtOrDefault(NEVER).time) >
timeDifferenceMillis -> true
else -> false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,10 @@ internal class MessageListControllerTests {
@Test
fun `Given regular message followed and preceded by current user message When grouping messages Should add middle position to message`() =
runTest {
val messages = randomMessageList(3) { randomMessage(user = user1) }
val now = java.util.Date()
val messages = randomMessageList(3) {
randomMessage(user = user1, createdAt = now, createdLocallyAt = now)
}
val messagesState = MutableStateFlow(messages)
val controller = Fixture()
.givenCurrentUser()
Expand Down
Loading
Loading