1717package io.getstream.chat.android.compose.ui.components.selectedmessage
1818
1919import androidx.compose.foundation.background
20- import androidx.compose.foundation.layout.Column
21- import androidx.compose.foundation.layout.Spacer
20+ import androidx.compose.foundation.layout.Arrangement
21+ import androidx.compose.foundation.layout.Row
2222import androidx.compose.foundation.layout.fillMaxHeight
2323import androidx.compose.foundation.layout.fillMaxWidth
2424import androidx.compose.foundation.layout.height
25- import androidx.compose.foundation.rememberScrollState
25+ import androidx.compose.foundation.layout.padding
26+ import androidx.compose.foundation.layout.size
27+ import androidx.compose.foundation.layout.width
28+ import androidx.compose.foundation.lazy.LazyColumn
29+ import androidx.compose.foundation.lazy.items
30+ import androidx.compose.foundation.lazy.rememberLazyListState
31+ import androidx.compose.foundation.shape.CircleShape
2632import androidx.compose.foundation.shape.RoundedCornerShape
27- import androidx.compose.foundation.verticalScroll
2833import androidx.compose.material3.BottomSheetDefaults
2934import androidx.compose.material3.ExperimentalMaterial3Api
3035import androidx.compose.material3.ModalBottomSheet
3136import androidx.compose.material3.Text
3237import androidx.compose.material3.rememberModalBottomSheetState
3338import androidx.compose.runtime.Composable
39+ import androidx.compose.runtime.collectAsState
3440import androidx.compose.runtime.getValue
35- import androidx.compose.runtime.mutableStateOf
3641import androidx.compose.runtime.remember
37- import androidx.compose.runtime.setValue
3842import androidx.compose.ui.Alignment
3943import androidx.compose.ui.Modifier
40- import androidx.compose.ui.platform.LocalResources
44+ import androidx.compose.ui.draw.clip
45+ import androidx.compose.ui.res.pluralStringResource
4146import androidx.compose.ui.text.style.TextOverflow
4247import androidx.compose.ui.tooling.preview.Preview
4348import androidx.compose.ui.unit.dp
49+ import androidx.lifecycle.viewmodel.compose.viewModel
4450import io.getstream.chat.android.compose.R
51+ import io.getstream.chat.android.compose.handlers.LoadMoreHandler
4552import io.getstream.chat.android.compose.state.messages.MessageReactionItemState
4653import io.getstream.chat.android.compose.state.userreactions.UserReactionItemState
54+ import io.getstream.chat.android.compose.ui.components.LoadingIndicator
55+ import io.getstream.chat.android.compose.ui.components.ShimmerProgressIndicator
56+ import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize
4757import io.getstream.chat.android.compose.ui.theme.ChatTheme
48- import io.getstream.chat.android.compose.util.extensions.toSet
58+ import io.getstream.chat.android.compose.ui.theme.StreamTokens
59+ import io.getstream.chat.android.compose.ui.util.ViewModelStore
60+ import io.getstream.chat.android.compose.viewmodel.messages.ReactionsMenuViewModel
4961import io.getstream.chat.android.models.ChannelCapabilities
5062import io.getstream.chat.android.models.Message
5163import io.getstream.chat.android.models.Reaction
@@ -103,7 +115,7 @@ public fun SelectedReactionsMenu(
103115 * Default content for the reactions menu bottom sheet.
104116 *
105117 * Composes the reaction count title, the reaction count row (chips), and the user reactions list
106- * inside a scrollable column.
118+ * inside a scrollable column with pagination support .
107119 *
108120 * @param message The selected message.
109121 * @param currentUser The currently logged in user.
@@ -122,87 +134,152 @@ internal fun ReactionsMenuContent(
122134 onShowMoreReactionsSelected : () -> Unit ,
123135 modifier : Modifier = Modifier ,
124136) {
125- val reactionGroups = buildReactionGroups(message)
126- val userReactions = buildUserReactionItems(
127- message = message,
128- currentUser = currentUser,
129- )
130- val resolver = ChatTheme .reactionResolver
131- val onAddReactionClick = onShowMoreReactionsSelected
132- .takeIf { ChannelCapabilities .SEND_REACTION in ownCapabilities }
133- val onReactionOptionSelected: (String ) -> Unit = { type ->
134- onMessageAction(
135- React (
136- reaction = Reaction (
137- messageId = message.id,
138- type = type,
139- emojiCode = resolver.emojiCode(type),
137+ ViewModelStore (message.id) {
138+ val viewModel = viewModel {
139+ ReactionsMenuViewModel (
140+ messageId = message.id,
141+ initialReactions = message.latestReactions,
142+ )
143+ }
144+
145+ val state by viewModel.state.collectAsState()
146+
147+ val reactionGroups = buildReactionGroups(message)
148+ val resolver = ChatTheme .reactionResolver
149+ val onAddReactionClick = onShowMoreReactionsSelected
150+ .takeIf { ChannelCapabilities .SEND_REACTION in ownCapabilities }
151+ val onReactionOptionSelected: (String ) -> Unit = { type ->
152+ onMessageAction(
153+ React (
154+ reaction = Reaction (
155+ messageId = message.id,
156+ type = type,
157+ emojiCode = resolver.emojiCode(type),
158+ ),
159+ message = message,
140160 ),
141- message = message,
142- ),
161+ )
162+ }
163+
164+ val userReactions = buildUserReactionItems(
165+ reactions = state.reactions,
166+ currentUser = currentUser,
143167 )
144- }
145168
146- var selectedReactionType by remember { mutableStateOf<String ?>(null ) }
147- val filteredUserReactions = remember(userReactions, selectedReactionType) {
148- selectedReactionType?.let { type -> userReactions.filter { it.type == type } } ? : userReactions
169+ ReactionsMenuList (
170+ reactionGroups = reactionGroups,
171+ items = userReactions,
172+ selectedReactionType = state.selectedReactionType,
173+ isLoading = state.isLoading,
174+ isLoadingMore = state.isLoadingMore,
175+ onReactionSelected = viewModel::selectReaction,
176+ onReactionOptionSelected = onReactionOptionSelected,
177+ onAddReactionClick = onAddReactionClick,
178+ onLoadMore = viewModel::loadMore,
179+ modifier = modifier,
180+ )
149181 }
182+ }
183+
184+ @Suppress(" LongParameterList" )
185+ @Composable
186+ private fun ReactionsMenuList (
187+ reactionGroups : List <MessageReactionItemState >,
188+ items : List <UserReactionItemState >,
189+ selectedReactionType : String? ,
190+ isLoading : Boolean ,
191+ isLoadingMore : Boolean ,
192+ onReactionSelected : (String ) -> Unit ,
193+ onReactionOptionSelected : (String ) -> Unit ,
194+ onAddReactionClick : (() -> Unit )? ,
195+ onLoadMore : () -> Unit ,
196+ modifier : Modifier = Modifier ,
197+ ) {
198+ val listState = rememberLazyListState()
150199
151- val reactionCountText = LocalResources .current.getQuantityString(
152- R .plurals.stream_compose_message_reactions,
153- filteredUserReactions.size,
154- filteredUserReactions.size,
200+ LoadMoreHandler (
201+ lazyListState = listState,
202+ loadMore = onLoadMore,
155203 )
156204
157- Column (
205+ LazyColumn (
206+ state = listState,
158207 horizontalAlignment = Alignment .CenterHorizontally ,
159208 modifier = modifier
160209 .fillMaxWidth()
161- .background(ChatTheme .colors.backgroundElevationElevation1)
162- .verticalScroll(rememberScrollState()),
210+ .background(ChatTheme .colors.backgroundElevationElevation1),
163211 ) {
164- Text (
165- text = reactionCountText,
166- style = ChatTheme .typography.headingMedium,
167- maxLines = 1 ,
168- overflow = TextOverflow .Ellipsis ,
169- color = ChatTheme .colors.textPrimary,
170- )
212+ item(key = " Stream_header" ) {
213+ val totalCount = reactionGroups.sumOf(MessageReactionItemState ::count)
214+ Text (
215+ text = pluralStringResource(R .plurals.stream_compose_message_reactions, totalCount, totalCount),
216+ style = ChatTheme .typography.headingMedium,
217+ maxLines = 1 ,
218+ overflow = TextOverflow .Ellipsis ,
219+ color = ChatTheme .colors.textPrimary,
220+ )
221+ ReactionCountRow (
222+ reactionGroups = reactionGroups,
223+ selectedReactionType = selectedReactionType,
224+ onReactionSelected = onReactionSelected,
225+ onAddReactionClick = onAddReactionClick,
226+ )
227+ }
171228
172- ReactionCountRow (
173- reactionGroups = reactionGroups,
174- selectedReactionType = selectedReactionType,
175- onReactionSelected = { type ->
176- selectedReactionType = if (selectedReactionType == type) null else type
177- },
178- onAddReactionClick = onAddReactionClick,
179- )
229+ if (isLoading) {
230+ items(ShimmerItemCount , key = { " Stream_shimmer_$it " }) {
231+ UserReactionShimmerItem ()
232+ }
233+ } else {
234+ items(
235+ items = items,
236+ key = { item -> " ${item.user.id} _${item.type} " },
237+ ) { item ->
238+ UserReactionRow (
239+ modifier = Modifier .padding(bottom = StreamTokens .spacingXs),
240+ item = item,
241+ onClick = if (item.isMine) {
242+ { onReactionOptionSelected(item.type) }
243+ } else {
244+ null
245+ },
246+ )
247+ }
180248
181- UserReactionsList (
182- userReactions = filteredUserReactions,
183- onReactionOptionSelected = onReactionOptionSelected,
184- )
249+ if (isLoadingMore) {
250+ item(key = " Stream_loading_more" ) {
251+ LoadingIndicator (modifier = Modifier .size(24 .dp))
252+ }
253+ }
254+ }
185255 }
186256}
187257
188258@Composable
189- private fun UserReactionsList (
190- userReactions : List <UserReactionItemState >,
191- onReactionOptionSelected : (String ) -> Unit ,
192- ) {
193- userReactions.forEach { item ->
194- UserReactionRow (
195- item = item,
196- onClick = if (item.isMine) {
197- { onReactionOptionSelected(item.type) }
198- } else {
199- null
200- },
259+ private fun UserReactionShimmerItem (modifier : Modifier = Modifier ) {
260+ Row (
261+ modifier = modifier
262+ .fillMaxWidth()
263+ .padding(horizontal = StreamTokens .spacingSm, vertical = StreamTokens .spacingXs),
264+ verticalAlignment = Alignment .CenterVertically ,
265+ horizontalArrangement = Arrangement .spacedBy(StreamTokens .spacingXs),
266+ ) {
267+ ShimmerProgressIndicator (
268+ modifier = Modifier
269+ .size(AvatarSize .Medium )
270+ .clip(CircleShape ),
271+ )
272+ ShimmerProgressIndicator (
273+ modifier = Modifier
274+ .width(200 .dp)
275+ .height(16 .dp)
276+ .clip(CircleShape ),
201277 )
202- Spacer (modifier = Modifier .height(8 .dp))
203278 }
204279}
205280
281+ private const val ShimmerItemCount = 8
282+
206283/* *
207284 * Builds a list of [MessageReactionItemState] from the message's reaction groups.
208285 *
@@ -227,46 +304,52 @@ private fun buildReactionGroups(message: Message): List<MessageReactionItemState
227304}
228305
229306/* *
230- * Builds a list of user reactions, based on the current user and the selected message .
307+ * Builds a list of user reactions from the loaded reactions list .
231308 *
232- * @param message The message the reactions were left for .
309+ * @param reactions The list of reactions loaded from the API .
233310 * @param currentUser The currently logged in user.
234311 */
235312@Composable
236313private fun buildUserReactionItems (
237- message : Message ,
314+ reactions : List < Reaction > ,
238315 currentUser : User ? ,
239316): List <UserReactionItemState > {
240317 val resolver = ChatTheme .reactionResolver
241- return message.latestReactions
242- .mapNotNull {
318+ return remember(reactions, currentUser, resolver) {
319+ reactions .mapNotNull {
243320 val user = it.user ? : return @mapNotNull null
244- val type = it.type
245-
246321 UserReactionItemState (
247322 user = user,
248- type = type,
323+ type = it. type,
249324 isMine = currentUser?.id == user.id,
250- emojiCode = resolver.emojiCode(type),
325+ emojiCode = resolver.emojiCode(it. type),
251326 )
252327 }
328+ }
253329}
254330
255331@Composable
256- private fun ReactionsMenuContentPreview (selectedMessage : Message ) {
257- ReactionsMenuContent (
258- message = selectedMessage,
259- currentUser = PreviewUserData .user1,
260- onMessageAction = {},
261- onShowMoreReactionsSelected = {},
262- ownCapabilities = ChannelCapabilities .toSet(),
332+ private fun ReactionsMenuListPreview (message : Message ) {
333+ ReactionsMenuList (
334+ reactionGroups = buildReactionGroups(message),
335+ items = buildUserReactionItems(
336+ reactions = message.latestReactions,
337+ currentUser = PreviewUserData .user1,
338+ ),
339+ selectedReactionType = null ,
340+ isLoading = false ,
341+ isLoadingMore = false ,
342+ onReactionSelected = {},
343+ onReactionOptionSelected = {},
344+ onAddReactionClick = {},
345+ onLoadMore = {},
263346 )
264347}
265348
266349@Composable
267350internal fun ReactionsMenuContentOneReaction () {
268- ReactionsMenuContentPreview (
269- selectedMessage = PreviewMessageData .message1.copy(
351+ ReactionsMenuListPreview (
352+ message = PreviewMessageData .message1.copy(
270353 latestReactions = PreviewReactionData .oneReaction,
271354 reactionGroups = PreviewReactionData .oneReactionGroup,
272355 ),
@@ -275,8 +358,8 @@ internal fun ReactionsMenuContentOneReaction() {
275358
276359@Composable
277360internal fun ReactionsMenuContentManyReactions () {
278- ReactionsMenuContentPreview (
279- PreviewMessageData .message1.copy(
361+ ReactionsMenuListPreview (
362+ message = PreviewMessageData .message1.copy(
280363 latestReactions = PreviewReactionData .manyReaction,
281364 reactionGroups = PreviewReactionData .manyReactionGroups,
282365 ),
0 commit comments