diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 5ec32f15442..73f8fc9f012 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1516,6 +1516,12 @@ public final class io/getstream/chat/android/compose/ui/components/messages/Comp public final fun getLambda$-388063089$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$ScrollToFirstUnreadButtonKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$ScrollToFirstUnreadButtonKt; + public fun ()V + public final fun getLambda$-1405339857$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; +} + public final class io/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$SwipeToReplyIconKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$SwipeToReplyIconKt; public fun ()V @@ -2091,7 +2097,7 @@ public final class io/getstream/chat/android/compose/ui/messages/list/Composable public final class io/getstream/chat/android/compose/ui/messages/list/ComposableSingletons$MessagesKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/messages/list/ComposableSingletons$MessagesKt; public fun ()V - public final fun getLambda$1954099387$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda$203042131$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; } public final class io/getstream/chat/android/compose/ui/messages/list/MessageContainerKt { @@ -2107,8 +2113,8 @@ public final class io/getstream/chat/android/compose/ui/messages/list/MessageIte } public final class io/getstream/chat/android/compose/ui/messages/list/MessageListKt { - public static final fun MessageList (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/foundation/layout/Arrangement$Vertical;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V - public static final fun MessageList (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V + public static final fun MessageList (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/foundation/layout/Arrangement$Vertical;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V + public static final fun MessageList (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState { @@ -3456,6 +3462,7 @@ public abstract interface class io/getstream/chat/android/compose/ui/theme/ChatC public fun ReactionsMenu (Lio/getstream/chat/android/compose/ui/theme/ReactionsMenuParams;Landroidx/compose/runtime/Composer;I)V public fun ReactionsMenuContent (Lio/getstream/chat/android/compose/ui/theme/ReactionsMenuContentParams;Landroidx/compose/runtime/Composer;I)V public fun ScrollToBottomButton (Lio/getstream/chat/android/compose/ui/theme/ScrollToBottomButtonParams;Landroidx/compose/runtime/Composer;I)V + public fun ScrollToFirstUnreadButton (Lio/getstream/chat/android/compose/ui/theme/ScrollToFirstUnreadButtonParams;Landroidx/compose/runtime/Composer;I)V public fun SearchInputClearButton (Lio/getstream/chat/android/compose/ui/theme/SearchInputClearButtonParams;Landroidx/compose/runtime/Composer;I)V public fun SearchInputLabel (Lio/getstream/chat/android/compose/ui/theme/SearchInputLabelParams;Landroidx/compose/runtime/Composer;I)V public fun SearchInputLeadingIcon (Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/compose/ui/theme/SearchInputLeadingIconParams;Landroidx/compose/runtime/Composer;I)V @@ -3645,6 +3652,7 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatComponentFacto public static fun ReactionsMenu (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/ReactionsMenuParams;Landroidx/compose/runtime/Composer;I)V public static fun ReactionsMenuContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/ReactionsMenuContentParams;Landroidx/compose/runtime/Composer;I)V public static fun ScrollToBottomButton (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/ScrollToBottomButtonParams;Landroidx/compose/runtime/Composer;I)V + public static fun ScrollToFirstUnreadButton (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/ScrollToFirstUnreadButtonParams;Landroidx/compose/runtime/Composer;I)V public static fun SearchInputClearButton (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/SearchInputClearButtonParams;Landroidx/compose/runtime/Composer;I)V public static fun SearchInputLabel (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/SearchInputLabelParams;Landroidx/compose/runtime/Composer;I)V public static fun SearchInputLeadingIcon (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/compose/ui/theme/SearchInputLeadingIconParams;Landroidx/compose/runtime/Composer;I)V @@ -5155,18 +5163,23 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageListEmptyTh public final class io/getstream/chat/android/compose/ui/theme/MessageListHelperContentParams { public static final field $stable I - public fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function1;)V + public fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V + public synthetic fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState; public final fun component2 ()Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState; public final fun component3 ()Landroidx/compose/foundation/layout/PaddingValues; public final fun component4 ()Lkotlin/jvm/functions/Function1; - public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/ui/theme/MessageListHelperContentParams; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageListHelperContentParams;Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageListHelperContentParams; + public final fun component5 ()Lkotlin/jvm/functions/Function0; + public final fun component6 ()Lkotlin/jvm/functions/Function0; + public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lio/getstream/chat/android/compose/ui/theme/MessageListHelperContentParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageListHelperContentParams;Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageListHelperContentParams; public fun equals (Ljava/lang/Object;)Z public final fun getContentPadding ()Landroidx/compose/foundation/layout/PaddingValues; public final fun getMessageListState ()Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState; public final fun getMessagesLazyListState ()Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState; + public final fun getOnDismissUnreadLabel ()Lkotlin/jvm/functions/Function0; public final fun getOnScrollToBottomClick ()Lkotlin/jvm/functions/Function1; + public final fun getOnScrollToFirstUnreadClick ()Lkotlin/jvm/functions/Function0; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -5683,6 +5696,27 @@ public final class io/getstream/chat/android/compose/ui/theme/ScrollToBottomButt public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/compose/ui/theme/ScrollToFirstUnreadButtonParams { + public static final field $stable I + public fun (ZILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;)V + public synthetic fun (ZILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Z + public final fun component2 ()I + public final fun component3 ()Lkotlin/jvm/functions/Function0; + public final fun component4 ()Lkotlin/jvm/functions/Function0; + public final fun component5 ()Landroidx/compose/ui/Modifier; + public final fun copy (ZILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;)Lio/getstream/chat/android/compose/ui/theme/ScrollToFirstUnreadButtonParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/ScrollToFirstUnreadButtonParams;ZILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ScrollToFirstUnreadButtonParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getModifier ()Landroidx/compose/ui/Modifier; + public final fun getOnClick ()Lkotlin/jvm/functions/Function0; + public final fun getOnDismiss ()Lkotlin/jvm/functions/Function0; + public final fun getUnreadCount ()I + public final fun getVisible ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/compose/ui/theme/SearchInputClearButtonParams { public static final field $stable I public fun (Lkotlin/jvm/functions/Function0;)V @@ -6776,6 +6810,7 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageL public final fun deleteMessage (Lio/getstream/chat/android/models/Message;)V public final fun deleteMessage (Lio/getstream/chat/android/models/Message;Z)V public static synthetic fun deleteMessage$default (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Lio/getstream/chat/android/models/Message;ZILjava/lang/Object;)V + public final fun disableUnreadLabelButton ()V public final fun dismissAllMessageActions ()V public final fun dismissMessageAction (Lio/getstream/chat/android/ui/common/state/messages/MessageAction;)V public final fun displayPollMoreOptions (Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;)V diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPage.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPage.kt index 74dadb012c6..7c74707a6cc 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPage.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPage.kt @@ -340,7 +340,7 @@ internal fun MediaGalleryVideoPage( MediaThumbnail( modifier = Modifier .matchParentSize() - .clickable { + .clickable(bounded = true) { showThumbnail = false player.play() }, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPhotosMenu.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPhotosMenu.kt index 60fb010f0c0..87b15085741 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPhotosMenu.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPhotosMenu.kt @@ -188,7 +188,7 @@ private fun MediaGalleryPhotosMenuItem( modifier = Modifier .fillMaxWidth() .aspectRatio(1f) - .clickable { onClick() }, + .clickable(bounded = true) { onClick() }, contentAlignment = Alignment.Center, ) { val data = attachment.imagePreviewData diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/ScrollToFirstUnreadButton.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/ScrollToFirstUnreadButton.kt new file mode 100644 index 00000000000..330299eb46c --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/ScrollToFirstUnreadButton.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.components.messages + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens +import io.getstream.chat.android.compose.ui.util.clickable + +private val IconSize = 16.dp + +/** + * A floating pill anchored at the top of the message list that lets the user jump to the first + * unread message when it sits outside the viewport. + * + * The pill exposes two interactions: tapping the label area scrolls to the first unread message, + * and tapping the trailing close (X) icon dismisses the pill without scrolling. + * + * @param unreadCount The number of unread messages to display in the label. + * @param modifier The modifier used for styling. + * @param onClick The handler triggered when the user taps the label area. + * @param onDismiss The handler triggered when the user taps the close (X) icon. + */ +@Composable +internal fun ScrollToFirstUnreadButton( + unreadCount: Int, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + onDismiss: () -> Unit = {}, +) { + Surface( + modifier = modifier + .shadow(StreamTokens.spacing2xs, shape = CircleShape), + shape = CircleShape, + color = ChatTheme.colors.backgroundCoreElevation1, + contentColor = ChatTheme.colors.textPrimary, + border = BorderStroke(1.dp, ChatTheme.colors.borderCoreSubtle), + ) { + Row( + modifier = Modifier + .height(IntrinsicSize.Min) + .heightIn(min = 40.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier + .testTag("Stream_ScrollToFirstUnreadButton") + .clickable(role = Role.Button, onClick = onClick) + .fillMaxHeight() + .padding(start = StreamTokens.spacingSm, end = StreamTokens.spacingXs), + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(IconSize), + painter = painterResource(R.drawable.stream_design_ic_arrow_up), + contentDescription = stringResource(R.string.stream_compose_scroll_to_first_unread), + ) + Text( + text = pluralStringResource( + id = R.plurals.stream_compose_scroll_to_first_unread_count, + count = unreadCount, + unreadCount, + ), + style = ChatTheme.typography.metadataEmphasis, + ) + } + VerticalDivider( + modifier = Modifier.padding(vertical = StreamTokens.spacing2xs), + thickness = StreamTokens.borderStrokeSubtle, + color = ChatTheme.colors.borderCoreSubtle, + ) + Icon( + modifier = Modifier + .testTag("Stream_ScrollToFirstUnreadButton_Dismiss") + .clickable(role = Role.Button, onClick = onDismiss) + .fillMaxHeight() + .padding(start = StreamTokens.spacingXs, end = StreamTokens.spacingSm) + .size(IconSize), + painter = painterResource(R.drawable.stream_design_ic_xmark), + contentDescription = stringResource(R.string.stream_compose_scroll_to_first_unread_dismiss), + ) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun ScrollToFirstUnreadButtonPreview() { + ChatTheme { + ScrollToFirstUnreadButton() + } +} + +@Composable +internal fun ScrollToFirstUnreadButton() { + ScrollToFirstUnreadButton( + unreadCount = 9, + ) +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt index 0e2f16ae01c..6c9c1245d02 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt @@ -75,6 +75,9 @@ import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll * @param onMediaGalleryPreviewResult Handler when the user selects an option in the Media Gallery Preview screen. * @param onMessagesPageEndReached Handler for pagination when the end of newest messages have been reached. * @param onScrollToBottomClicked Handler when the user requests to scroll to the bottom of the messages list. + * @param onScrollToFirstUnreadClicked Handler when the user taps the scroll-to-first-unread pill. + * @param onDismissUnreadLabel Handler when the user dismisses the scroll-to-first-unread pill via + * its close affordance. * @param onPauseAudioRecordingAttachments Handler for lifecycle events. */ @Composable @@ -108,6 +111,8 @@ public fun MessageList( }, onMessagesPageEndReached: (String) -> Unit = { viewModel.onBottomEndRegionReached(it) }, onScrollToBottomClicked: (() -> Unit) -> Unit = { viewModel.scrollToBottom(scrollToBottom = it) }, + onScrollToFirstUnreadClicked: () -> Unit = { viewModel.scrollToFirstUnreadMessage() }, + onDismissUnreadLabel: () -> Unit = { viewModel.disableUnreadLabelButton() }, onPauseAudioRecordingAttachments: () -> Unit = { viewModel.pauseAudioRecordingAttachments() }, ) { MessageList( @@ -122,6 +127,8 @@ public fun MessageList( onScrolledToBottom = onScrollToBottom, onMessagesPageEndReached = onMessagesPageEndReached, onScrollToBottom = onScrollToBottomClicked, + onScrollToFirstUnread = onScrollToFirstUnreadClicked, + onDismissUnreadLabel = onDismissUnreadLabel, onPauseAudioRecordingAttachments = onPauseAudioRecordingAttachments, messageItemParams = { messageListItem -> MessageItemParams( @@ -270,6 +277,9 @@ internal fun DefaultMessageListEmptyContent(modifier: Modifier) { * @param onScrolledToBottom Handler when the user scrolls to the bottom. * @param onMessagesPageEndReached Handler for pagination when the end of newest messages have been reached. * @param onScrollToBottom Handler when the user requests to scroll to the bottom of the messages list. + * @param onScrollToFirstUnread Handler when the user taps the scroll-to-first-unread pill. + * @param onDismissUnreadLabel Handler when the user dismisses the scroll-to-first-unread pill via + * its close affordance. * @param onPauseAudioRecordingAttachments Handler for lifecycle events. * @param messageItemParams Factory that builds [MessageItemParams] for each message list item. */ @@ -287,6 +297,8 @@ public fun MessageList( onScrolledToBottom: () -> Unit = {}, onMessagesPageEndReached: (String) -> Unit = {}, onScrollToBottom: (() -> Unit) -> Unit = {}, + onScrollToFirstUnread: () -> Unit = {}, + onDismissUnreadLabel: () -> Unit = {}, onPauseAudioRecordingAttachments: () -> Unit = {}, messageItemParams: (MessageListItemState) -> MessageItemParams = ::MessageItemParams, ) { @@ -313,6 +325,8 @@ public fun MessageList( onScrolledToBottom = onScrolledToBottom, onMessagesEndReached = onMessagesPageEndReached, onScrollToBottom = onScrollToBottom, + onScrollToFirstUnread = onScrollToFirstUnread, + onDismissUnreadLabel = onDismissUnreadLabel, itemContent = { messageListItem -> with(ChatTheme.componentFactory) { MessageItem(params = messageItemParams(messageListItem)) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/Messages.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/Messages.kt index b3269a61fad..332ba88c2bd 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/Messages.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/Messages.kt @@ -55,6 +55,7 @@ import io.getstream.chat.android.compose.ui.theme.MessageListHelperContentParams import io.getstream.chat.android.compose.ui.theme.MessageListItemModifierParams import io.getstream.chat.android.compose.ui.theme.MessageListLoadingMoreItemContentParams import io.getstream.chat.android.compose.ui.theme.ScrollToBottomButtonParams +import io.getstream.chat.android.compose.ui.theme.ScrollToFirstUnreadButtonParams import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.compose.util.isAppInForegroundAsState import io.getstream.chat.android.models.Message @@ -67,6 +68,7 @@ import io.getstream.chat.android.ui.common.state.messages.list.MyOwn import io.getstream.chat.android.ui.common.state.messages.list.NewMessageState import io.getstream.chat.android.ui.common.state.messages.list.Other import io.getstream.chat.android.ui.common.state.messages.list.Typing +import io.getstream.chat.android.ui.common.state.messages.list.UnreadSeparatorItemState import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch @@ -93,6 +95,9 @@ import kotlin.math.abs * @param onScrolledToBottom Handler when the user reaches the bottom of the list. * @param onMessagesEndReached Handler for pagination, when the user reaches chronologically the end of messages. * @param onScrollToBottom Handler when the user requests to scroll to the bottom of the messages list. + * @param onScrollToFirstUnread Handler when the user taps the scroll-to-first-unread pill. + * @param onDismissUnreadLabel Handler when the user dismisses the scroll-to-first-unread pill via + * its close affordance. * @param modifier Modifier for styling. * @param contentPadding Padding values to be applied to the message list surrounding the content inside. * @param helperContent Composable that, by default, represents the helper content featuring scrolling behavior based @@ -115,6 +120,8 @@ internal fun Messages( onScrolledToBottom: () -> Unit, onMessagesEndReached: (String) -> Unit, onScrollToBottom: (() -> Unit) -> Unit, + onScrollToFirstUnread: () -> Unit = {}, + onDismissUnreadLabel: () -> Unit = {}, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(vertical = 16.dp), helperContent: @Composable BoxScope.() -> Unit = { @@ -125,6 +132,8 @@ internal fun Messages( messagesLazyListState = messagesLazyListState, contentPadding = contentPadding, onScrollToBottomClick = onScrollToBottom, + onScrollToFirstUnreadClick = onScrollToFirstUnread, + onDismissUnreadLabel = onDismissUnreadLabel, ), ) } @@ -301,7 +310,11 @@ private fun MessageListState.getVerticalArrangement( * * @param messagesState The state of messages, current message list, thread, user and more. * @param messagesLazyListState The scrolling state of the list, used to manipulate and trigger scroll events. + * @param contentPadding Padding values applied around the message list content. * @param scrollToBottom Handler when the user requests to scroll to the bottom of the messages list. + * @param scrollToFirstUnread Handler when the user taps the scroll-to-first-unread pill. + * @param dismissUnreadLabel Handler when the user dismisses the scroll-to-first-unread pill via + * its close affordance. */ @Suppress("LongMethod") @Composable @@ -310,6 +323,8 @@ internal fun BoxScope.DefaultMessagesHelperContent( messagesLazyListState: MessagesLazyListState, contentPadding: PaddingValues, scrollToBottom: (() -> Unit) -> Unit, + scrollToFirstUnread: () -> Unit = {}, + dismissUnreadLabel: () -> Unit = {}, ) { val lazyListState = messagesLazyListState.lazyListState @@ -370,7 +385,35 @@ internal fun BoxScope.DefaultMessagesHelperContent( areNewestMessagesLoaded, ) + val unreadLabel = messagesState.unreadLabel + val unreadSeparatorId = remember(messages) { + messages.firstOrNull { it is UnreadSeparatorItemState }?.id + } + val isUnreadSeparatorVisible by remember(unreadSeparatorId, lazyListState) { + derivedStateOf { + unreadSeparatorId != null && + lazyListState.layoutInfo.visibleItemsInfo.any { it.key == unreadSeparatorId } + } + } + val scrollToFirstUnreadVisible = isScrollToFirstUnreadVisible( + buttonVisibility = unreadLabel?.buttonVisibility == true, + isUnreadSeparatorVisible = isUnreadSeparatorVisible, + ) + with(ChatTheme.componentFactory) { + ScrollToFirstUnreadButton( + params = ScrollToFirstUnreadButtonParams( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(contentPadding) + .padding(top = StreamTokens.spacingMd), + visible = scrollToFirstUnreadVisible, + unreadCount = unreadLabel?.unreadCount ?: 0, + onClick = scrollToFirstUnread, + onDismiss = dismissUnreadLabel, + ), + ) + ScrollToBottomButton( params = ScrollToBottomButtonParams( modifier = Modifier @@ -514,6 +557,24 @@ private fun shouldScrollToBottomButtonBeVisibleAtIndex(firstVisibleItemIndex: In return abs(firstVisibleItemIndex) >= 3 } +/** + * Determines whether the scroll-to-first-unread pill should be visible. + * + * The pill shows when the controller has produced an unread label with [buttonVisibility] enabled + * and the inline unread separator is not present in the visible viewport — either because the + * boundary lies above the loaded window, or because it has not been loaded into the list yet. + * + * @param buttonVisibility Whether the controller currently allows the button to be shown. + * @param isUnreadSeparatorVisible Whether the inline unread separator is currently within the + * visible viewport. + * + * @return Whether the scroll-to-first-unread pill should be shown. + */ +private fun isScrollToFirstUnreadVisible( + buttonVisibility: Boolean, + isUnreadSeparatorVisible: Boolean, +): Boolean = buttonVisibility && !isUnreadSeparatorVisible + /** * The default loading more indicator. */ diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt index 32b08940aca..c5de9a9b4e0 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt @@ -114,6 +114,7 @@ import io.getstream.chat.android.compose.ui.components.messages.MessageText import io.getstream.chat.android.compose.ui.components.messages.MessageThreadFooter import io.getstream.chat.android.compose.ui.components.messages.QuotedMessage import io.getstream.chat.android.compose.ui.components.messages.ScrollToBottomButton +import io.getstream.chat.android.compose.ui.components.messages.ScrollToFirstUnreadButton import io.getstream.chat.android.compose.ui.components.messages.SegmentedMessageReactions import io.getstream.chat.android.compose.ui.components.messages.SwipeToReplyIcon import io.getstream.chat.android.compose.ui.components.reactionpicker.ReactionsPicker @@ -732,6 +733,8 @@ public interface ChatComponentFactory { messagesLazyListState = params.messagesLazyListState, contentPadding = params.contentPadding, scrollToBottom = params.onScrollToBottomClick, + scrollToFirstUnread = params.onScrollToFirstUnreadClick, + dismissUnreadLabel = params.onDismissUnreadLabel, ) } @@ -765,6 +768,37 @@ public interface ChatComponentFactory { } } + /** + * The default scroll-to-first-unread pill shown at the top of the message list when there are + * unread messages and the first unread is outside the viewport. + * + * @param params Parameters for this component. + */ + @Composable + public fun ScrollToFirstUnreadButton(params: ScrollToFirstUnreadButtonParams) { + if (LocalInspectionMode.current) { + if (params.visible) { + ScrollToFirstUnreadButton( + modifier = params.modifier, + unreadCount = params.unreadCount, + onClick = params.onClick, + onDismiss = params.onDismiss, + ) + } + } else { + FadingVisibility( + modifier = params.modifier, + visible = params.visible, + ) { + ScrollToFirstUnreadButton( + unreadCount = params.unreadCount, + onClick = params.onClick, + onDismiss = params.onDismiss, + ) + } + } + } + /** * The default message item component, which renders each [MessageListItemState]'s subtype. * This includes date separators, system messages, and regular messages. diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt index ad566fa54dd..5f675cc71aa 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt @@ -481,12 +481,18 @@ public data class MessageListEmptyContentParams( * @param messagesLazyListState The lazy list state for scrolling. * @param contentPadding The content padding of the message list. * @param onScrollToBottomClick Action invoked when the scroll to bottom button is clicked. + * @param onScrollToFirstUnreadClick Action invoked when the scroll-to-first-unread pill body is + * clicked. Implementations should scroll the list to the unread boundary. + * @param onDismissUnreadLabel Action invoked when the close affordance on the + * scroll-to-first-unread pill is clicked. Implementations should hide the pill without scrolling. */ public data class MessageListHelperContentParams( val messageListState: MessageListState, val messagesLazyListState: MessagesLazyListState, val contentPadding: PaddingValues, val onScrollToBottomClick: (() -> Unit) -> Unit, + val onScrollToFirstUnreadClick: () -> Unit = {}, + val onDismissUnreadLabel: () -> Unit = {}, ) /** @@ -504,6 +510,25 @@ public data class ScrollToBottomButtonParams( val modifier: Modifier = Modifier, ) +/** + * Parameters for [ChatComponentFactory.ScrollToFirstUnreadButton]. + * + * @param visible Whether the pill is visible. + * @param unreadCount The number of unread messages to display in the pill label. + * @param onClick Action invoked when the pill body is clicked. Implementations should scroll the + * list to the first unread message. + * @param onDismiss Action invoked when the close (X) affordance is clicked. Implementations + * should hide the pill without scrolling. + * @param modifier Modifier for styling. + */ +public data class ScrollToFirstUnreadButtonParams( + val visible: Boolean, + val unreadCount: Int, + val onClick: () -> Unit, + val onDismiss: () -> Unit, + val modifier: Modifier = Modifier, +) + /** * Parameters for [ChatComponentFactory.MessageItem]. * diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ModifierUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ModifierUtils.kt index 246a8b62f31..a73bd0e718a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ModifierUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ModifierUtils.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -71,12 +72,15 @@ internal fun Modifier.dragPointerInput( * When `false`, [onClick], and this modifier will appear disabled for accessibility service. * @param onClickLabel Semantic / accessibility label for the click action. Announced by screen readers * (e.g. "double tap to