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 4558c82793f..b3ad9a1f970 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1838,33 +1838,33 @@ public final class io/getstream/chat/android/compose/ui/components/moderatedmess public final class io/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollAnswersKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollAnswersKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-1 Lkotlin/jvm/functions/Function2; public static field lambda-2 Lkotlin/jvm/functions/Function3; public static field lambda-3 Lkotlin/jvm/functions/Function3; public static field lambda-4 Lkotlin/jvm/functions/Function3; + public static field lambda-5 Lkotlin/jvm/functions/Function2; public fun ()V - public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-4$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-5$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollDialogHeaderKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollDialogHeaderKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollMoreOptionsDialogKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollMoreOptionsDialogKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public static field lambda-2 Lkotlin/jvm/functions/Function3; - public static field lambda-3 Lkotlin/jvm/functions/Function2; + public static field lambda-1 Lkotlin/jvm/functions/Function2; public fun ()V - public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; - public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; - public final fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollOptionInputKt { @@ -1905,7 +1905,7 @@ public final class io/getstream/chat/android/compose/ui/components/poll/PollAnsw } public final class io/getstream/chat/android/compose/ui/components/poll/PollDialogHeaderKt { - public static final fun PollDialogHeader (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V + public static final fun PollDialogHeader (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialogKt { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/AvatarStack.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/AvatarStack.kt index 0391d23dde4..c6f43619357 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/AvatarStack.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/AvatarStack.kt @@ -39,7 +39,6 @@ import io.getstream.chat.android.previewdata.PreviewUserData * @param users The list of users to display avatars for. * @param avatarSize The size of each avatar. * @param modifier Modifier for styling. - * @param showBorder Whether to show a border around the first avatar. * @param trailingContent Optional composable rendered after the avatars. */ @Composable @@ -48,7 +47,6 @@ internal fun UserAvatarStack( users: List, avatarSize: Dp, modifier: Modifier = Modifier, - showBorder: Boolean = false, trailingContent: @Composable (() -> Unit)? = null, ) { val componentFactory = ChatTheme.componentFactory @@ -60,14 +58,14 @@ internal fun UserAvatarStack( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(-overlap), ) { - users.forEachIndexed { index, user -> + users.forEach { user -> componentFactory.UserAvatar( modifier = Modifier .size(avatarSize + borderSize) .border(borderSize, colors.borderCoreInverse, CircleShape) .padding(borderSize), user = user, - showBorder = showBorder && index == 0, + showBorder = false, showIndicator = false, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageThreadFooter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageThreadFooter.kt index 7ac9977b0d3..c0ea041b6f9 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageThreadFooter.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageThreadFooter.kt @@ -140,7 +140,6 @@ private fun ThreadParticipants(participants: List) { overlap = StreamTokens.spacingXs, users = participants.take(MaxThreadParticipants), avatarSize = AvatarSize.Small, - showBorder = true, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt index f2bff2bf12c..a7f2ac5ed44 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt @@ -232,7 +232,7 @@ private fun PollMessageContent( poll = poll, option = option, voteCount = voteCount, - users = poll.getVotes(option).mapNotNull(Vote::user), + users = remember(poll.votes, option) { poll.getVotes(option).mapNotNull(Vote::user) }, totalVoteCount = poll.voteCountsByOption.values.sum(), checkedCount = poll.ownVotes.size, checked = poll.ownVotes.any { it.optionId == option.id }, @@ -438,7 +438,6 @@ private fun PollOptionItem( overlap = StreamTokens.spacingXs, users = users.take(MaxStackedAvatars), avatarSize = AvatarSize.ExtraSmall, - showBorder = true, modifier = Modifier.padding(start = StreamTokens.spacingXs, end = StreamTokens.spacing2xs), ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt index 62df4ba7da6..b7b089aadc8 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt @@ -25,20 +25,20 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -47,26 +47,33 @@ import androidx.compose.runtime.collectAsState 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.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize +import io.getstream.chat.android.compose.ui.components.button.StreamButton +import io.getstream.chat.android.compose.ui.components.button.StreamButtonSize +import io.getstream.chat.android.compose.ui.components.button.StreamButtonStyleDefaults +import io.getstream.chat.android.compose.ui.components.button.StreamTextButton import io.getstream.chat.android.compose.ui.components.composer.InputField import io.getstream.chat.android.compose.ui.theme.ChatTheme -import io.getstream.chat.android.compose.ui.util.clickable +import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel import io.getstream.chat.android.models.Answer +import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.VotingVisibility +import io.getstream.chat.android.previewdata.PreviewPollData +import io.getstream.chat.android.previewdata.PreviewUserData import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll +import java.util.Date @Suppress("LongMethod", "MagicNumber") @Composable @@ -85,11 +92,11 @@ public fun PollAnswersDialog( targetState = true } } - val showAddAnswerDialog = remember { mutableStateOf(false) } - if (showAddAnswerDialog.value) { + var showAddAnswerDialog by remember { mutableStateOf(false) } + if (showAddAnswerDialog) { AddAnswerDialog( initMessage = currentUserAnswer?.text ?: "", - onDismiss = { showAddAnswerDialog.value = false }, + onDismiss = { showAddAnswerDialog = false }, onNewAnswer = { newAnswer -> listViewModel.castAnswer(selectedPoll.message, selectedPoll.poll, newAnswer) }, @@ -99,6 +106,8 @@ public fun PollAnswersDialog( alignment = Alignment.BottomCenter, onDismissRequest = onDismissRequest, ) { + BackHandler(onBack = onBackPressed) + @Suppress("MagicNumber") AnimatedVisibility( visibleState = state, @@ -110,102 +119,103 @@ public fun PollAnswersDialog( slideOutVertically(animationSpec = tween(400)), label = "poll answers dialog", ) { - val poll = selectedPoll.poll - - BackHandler { onBackPressed.invoke() } + Content( + poll = selectedPoll.poll, + currentUserAnswer = currentUserAnswer, + showAnonymousAvatar = showAnonymousAvatar, + onBackPressed = onBackPressed, + onAddOrEditClick = { showAddAnswerDialog = true }, + ) + } + } +} - LazyColumn( - modifier = Modifier - .fillMaxSize() - .background(ChatTheme.colors.backgroundCoreApp), - ) { - item { - PollDialogHeader( - title = stringResource(id = R.string.stream_compose_poll_answers), - onBackPressed = onBackPressed, - ) +@Composable +private fun Content( + poll: Poll, + currentUserAnswer: Answer? = null, + showAnonymousAvatar: Boolean = false, + onBackPressed: () -> Unit = {}, + onAddOrEditClick: () -> Unit = {}, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.backgroundCoreApp), + ) { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_answers), + onBackPressed = onBackPressed, + trailingContent = { + if (!poll.closed && currentUserAnswer == null) { + StreamButton( + onClick = onAddOrEditClick, + style = StreamButtonStyleDefaults.primarySolid, + ) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_edit), + contentDescription = stringResource(id = R.string.stream_compose_add_answer), + modifier = Modifier.size(20.dp), + ) + } } + }, + ) - items( - items = poll.answers, - key = { answer -> answer.id }, - ) { answer -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = StreamTokens.spacingMd) + .padding(top = StreamTokens.spacing2xl), + verticalArrangement = Arrangement.spacedBy(StreamTokens.spacingMd), + ) { + val showAvatar = (poll.votingVisibility == VotingVisibility.PUBLIC) || showAnonymousAvatar + + if (currentUserAnswer != null) { + PollAnswersItem( + answer = currentUserAnswer, + showAvatar = showAvatar, + showUpdateButton = !poll.closed, + onUpdateClick = onAddOrEditClick, + ) + } + poll.answers.forEach { answer -> + if (answer.id != currentUserAnswer?.id) { PollAnswersItem( answer = answer, - showAvatar = (poll.votingVisibility == VotingVisibility.PUBLIC) || showAnonymousAvatar, + showAvatar = showAvatar, ) - Spacer(modifier = Modifier.height(8.dp)) - } - - item { Spacer(modifier = Modifier.height(16.dp)) } - - if (!poll.closed) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(shape = ButtonDefaults.shape) - .clickable { showAddAnswerDialog.value = true } - .background(color = ChatTheme.colors.backgroundCoreSurface), - contentAlignment = Alignment.Center, - ) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 11.dp), - text = stringResource( - id = when (currentUserAnswer == null) { - true -> R.string.stream_compose_add_answer - false -> R.string.stream_compose_edit_answer - }, - ), - textAlign = TextAlign.Center, - color = ChatTheme.colors.accentPrimary, - fontSize = 16.sp, - ) - } - } } } } } } -@Suppress("LongMethod") @Composable internal fun PollAnswersItem( answer: Answer, showAvatar: Boolean, + showUpdateButton: Boolean = false, + onUpdateClick: () -> Unit = {}, ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .background( - color = ChatTheme.colors.backgroundCoreSurface, - shape = RoundedCornerShape( - topStart = 12.dp, - topEnd = 12.dp, - bottomStart = 12.dp, - bottomEnd = 12.dp, - ), - ) - .padding(horizontal = 16.dp, vertical = 16.dp), - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - modifier = Modifier.weight(1f), - text = answer.text, - color = ChatTheme.colors.textPrimary, - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - ) - } + val colors = ChatTheme.colors + val typography = ChatTheme.typography - Spacer(modifier = Modifier.height(16.dp)) + PollSection( + contentPadding = PaddingValues(StreamTokens.spacingMd), + verticalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), + ) { + Text( + text = answer.text, + color = colors.textPrimary, + style = typography.bodyDefault, + ) - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), + ) { val user = answer.user?.takeIf { showAvatar } if (user != null) { ChatTheme.componentFactory.UserAvatar( @@ -216,21 +226,32 @@ internal fun PollAnswersItem( ) Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 4.dp) - .weight(1f), + modifier = Modifier.weight(1f), text = user.name, - color = ChatTheme.colors.textPrimary, - fontSize = 14.sp, + color = colors.chatTextUsername, + style = typography.captionDefault, ) } Text( - modifier = Modifier.padding(bottom = 2.dp), text = ChatTheme.dateFormatter.formatDate(answer.createdAt), - color = ChatTheme.colors.textSecondary, - fontSize = 14.sp, + color = colors.textTertiary, + style = typography.captionDefault, + ) + } + + if (showUpdateButton) { + HorizontalDivider( + modifier = Modifier.padding(top = StreamTokens.spacingXs), + color = colors.borderCoreDefault, + ) + + StreamTextButton( + onClick = onUpdateClick, + text = stringResource(id = R.string.stream_compose_edit_answer), + style = StreamButtonStyleDefaults.secondaryGhost, + size = StreamButtonSize.Small, + modifier = Modifier.fillMaxWidth(), ) } } @@ -300,3 +321,40 @@ internal fun AddAnswerDialog( containerColor = ChatTheme.colors.backgroundElevationElevation1, ) } + +@Preview +@Composable +private fun PollAnswersDialogPreview() { + ChatTheme { + val now = Date() + val pollWithAnswers = PreviewPollData.poll1.copy( + answers = listOf( + Answer( + id = "preview1", + pollId = "", + text = "I think we should go with option A, it makes the most sense.", + createdAt = now, + updatedAt = now, + user = PreviewUserData.user1, + ), + Answer( + id = "preview2", + pollId = "", + text = "This is my own comment on the poll.", + createdAt = now, + updatedAt = now, + user = PreviewUserData.user2, + ), + Answer( + id = "preview3", + pollId = "", + text = "Option B is clearly better!", + createdAt = now, + updatedAt = now, + user = PreviewUserData.user3, + ), + ), + ) + Content(poll = pollWithAnswers) + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollDialogHeader.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollDialogHeader.kt index 68f3c3d4e6f..9b1b1f91cd7 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollDialogHeader.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollDialogHeader.kt @@ -38,6 +38,7 @@ import io.getstream.chat.android.compose.ui.theme.StreamTokens public fun PollDialogHeader( title: String, onBackPressed: () -> Unit, + trailingContent: @Composable () -> Unit = {}, ) { Row( modifier = Modifier @@ -51,12 +52,15 @@ public fun PollDialogHeader( ) Text( + modifier = Modifier.weight(1f), text = title, style = ChatTheme.typography.headingMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, color = ChatTheme.colors.textPrimary, ) + + trailingContent() } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialog.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialog.kt index 79e65c5265b..a86846337ea 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialog.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialog.kt @@ -19,45 +19,53 @@ package io.getstream.chat.android.compose.ui.components.poll import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize +import io.getstream.chat.android.compose.ui.components.avatar.UserAvatarStack import io.getstream.chat.android.compose.ui.components.common.RadioCheck import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel import io.getstream.chat.android.models.Option import io.getstream.chat.android.models.Poll +import io.getstream.chat.android.models.User import io.getstream.chat.android.models.Vote +import io.getstream.chat.android.models.VotingVisibility import io.getstream.chat.android.previewdata.PreviewMessageData import io.getstream.chat.android.previewdata.PreviewPollData import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType @@ -132,114 +140,135 @@ private fun Content( BackHandler { onBackPressed.invoke() } - LazyColumn( + Column( modifier = Modifier .fillMaxSize() .background(ChatTheme.colors.backgroundCoreApp), ) { - item { - PollDialogHeader( - title = stringResource(id = R.string.stream_compose_poll_options), - onBackPressed = onBackPressed, - ) - } + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_options), + onBackPressed = onBackPressed, + ) - item { PollMoreOptionsTitle(title = poll.name) } + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = StreamTokens.spacingMd), + ) { + PollMoreOptionsTitle(title = poll.name) - item { Spacer(modifier = Modifier.height(16.dp)) } + val totalVoteCount = remember(poll.voteCountsByOption) { poll.voteCountsByOption.values.sum() } - pollMoreOptionsContent( - poll = poll, - onCastVote = onCastVote, - onRemoveVote = onRemoveVote, - ) + PollMoreOptionsItemList( + poll = poll, + totalVoteCount = totalVoteCount, + onCastVote = onCastVote, + onRemoveVote = onRemoveVote, + ) - item { Spacer(modifier = Modifier.height(16.dp)) } + val context = LocalContext.current + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = StreamTokens.spacing2xl), + text = remember(totalVoteCount) { + context.resources.getQuantityString( + R.plurals.stream_compose_poll_total_vote_counts, + totalVoteCount, + totalVoteCount, + ) + }, + style = ChatTheme.typography.bodyDefault, + color = ChatTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + } } } @Composable internal fun PollMoreOptionsTitle(title: String) { - Box( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - .heightIn(min = 56.dp) - .clip(shape = RoundedCornerShape(StreamTokens.radiusXl)) - .background(ChatTheme.colors.backgroundCoreSurface) - .padding(16.dp), + PollSection( + modifier = Modifier.padding(vertical = StreamTokens.spacing2xl), + verticalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), + contentPadding = PaddingValues(StreamTokens.spacingMd), ) { Text( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterStart), + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.stream_compose_poll_question_label), + color = ChatTheme.colors.textTertiary, + style = ChatTheme.typography.headingExtraSmall, + ) + + Text( + modifier = Modifier.fillMaxWidth(), text = title, + style = ChatTheme.typography.headingMedium, color = ChatTheme.colors.textPrimary, - fontWeight = FontWeight.Medium, - fontSize = 16.sp, ) } } -private fun LazyListScope.pollMoreOptionsContent( +@Composable +private fun PollMoreOptionsItemList( poll: Poll, + totalVoteCount: Int, onCastVote: (Option) -> Unit, onRemoveVote: (Vote) -> Unit, ) { - val options = poll.options - itemsIndexed( - items = options, - key = { _, option -> option.id }, - ) { index, option -> - val voteCount = poll.voteCountsByOption[option.id] ?: 0 - val isVotedByMine = poll.ownVotes.any { it.optionId == option.id } + PollSection( + contentPadding = PaddingValues( + horizontal = StreamTokens.spacingXs, + vertical = StreamTokens.spacingMd, + ), + verticalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), + ) { + poll.options.forEach { option -> + val voteCount = poll.voteCountsByOption[option.id] ?: 0 + val isVotedByMine = poll.ownVotes.any { it.optionId == option.id } + val users = remember(poll.votes, option) { poll.getVotes(option).mapNotNull(Vote::user) } - PollMoreOptionItem( - index = index, - poll = poll, - option = option, - voteCount = voteCount, - checkedCount = poll.ownVotes.size, - checked = isVotedByMine, - onCastVote = { onCastVote.invoke(option) }, - onRemoveVote = { - val vote = poll.votes.firstOrNull { it.optionId == option.id } ?: return@PollMoreOptionItem - onRemoveVote.invoke(vote) - }, - ) + PollMoreOptionItem( + poll = poll, + option = option, + voteCount = voteCount, + totalVoteCount = totalVoteCount, + users = users, + checkedCount = poll.ownVotes.size, + checked = isVotedByMine, + onCastVote = { onCastVote(option) }, + onRemoveVote = { poll.votes.find { it.optionId == option.id }?.let(onRemoveVote) }, + ) + } } } +@Suppress("LongParameterList", "LongMethod") @Composable private fun PollMoreOptionItem( - index: Int, poll: Poll, option: Option, voteCount: Int, + totalVoteCount: Int, + users: List, checkedCount: Int, checked: Boolean, onCastVote: () -> Unit, onRemoveVote: () -> Unit, ) { - val shape = if (index == 0) { - RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp) - } else if (index == poll.options.size - 1) { - RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp) - } else { - RoundedCornerShape(0.dp) - } + val colors = ChatTheme.colors + val typography = ChatTheme.typography Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) - .background(color = ChatTheme.colors.backgroundCoreSurface, shape = shape) - .padding(horizontal = 16.dp, vertical = 16.dp), + .padding(StreamTokens.spacingXs), + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingSm), verticalAlignment = Alignment.CenterVertically, ) { if (!poll.closed) { RadioCheck( - modifier = Modifier.padding(end = 8.dp), checked = checked, onCheckedChange = { enabled -> val canVote = poll.maxVotesAllowed?.let { checkedCount < it } ?: true @@ -249,27 +278,64 @@ private fun PollMoreOptionItem( onRemoveVote.invoke() } }, + borderColor = colors.chatBorderOnChatIncoming, ) } - Text( - modifier = Modifier.weight(1f), - text = option.text, - color = ChatTheme.colors.textPrimary, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - fontSize = 16.sp, - ) + Column(verticalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs)) { + Row(Modifier.heightIn(min = AvatarSize.ExtraSmall)) { + Text( + modifier = Modifier.weight(1f), + text = option.text, + style = typography.captionDefault, + color = colors.chatTextIncoming, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) - Text( - modifier = Modifier.padding(bottom = 2.dp), - text = voteCount.toString(), - color = ChatTheme.colors.textPrimary, - fontSize = 16.sp, - ) + if (users.isNotEmpty() && poll.votingVisibility != VotingVisibility.ANONYMOUS) { + UserAvatarStack( + overlap = StreamTokens.spacingXs, + users = users.take(MaxStackedAvatars), + avatarSize = AvatarSize.ExtraSmall, + modifier = Modifier.padding(start = StreamTokens.spacingXs, end = StreamTokens.spacing2xs), + ) + } + + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = voteCount.toString(), + style = typography.metadataDefault, + color = colors.chatTextIncoming, + ) + } + + val progress by animateFloatAsState( + targetValue = if (voteCount == 0 || totalVoteCount == 0) { + 0f + } else { + voteCount / totalVoteCount.toFloat() + }, + ) + + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .clip(RoundedCornerShape(4.dp)), + progress = { progress }, + color = colors.chatPollProgressFillIncoming, + trackColor = colors.chatPollProgressTrackIncoming, + gapSize = 0.dp, + strokeCap = StrokeCap.Square, + drawStopIndicator = { /* Don't draw the stop indicator */ }, + ) + } } } +private const val MaxStackedAvatars = 3 + @Preview @Composable private fun PollMoreOptionsDialogPreview() { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollViewResultDialog.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollViewResultDialog.kt index 512bf72fe8c..af35908b644 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollViewResultDialog.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollViewResultDialog.kt @@ -200,7 +200,7 @@ private fun PollViewResultItem( index: Int, onShowAllClick: (option: Option) -> Unit, ) { - PollResultSection(modifier = Modifier.padding(top = StreamTokens.spacingMd)) { + PollSection(modifier = Modifier.padding(top = StreamTokens.spacingMd)) { Text( modifier = Modifier .fillMaxWidth() @@ -311,7 +311,7 @@ private fun PollViewResultTitle( title: String, modifier: Modifier, ) { - PollResultSection( + PollSection( modifier = modifier, verticalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), contentPadding = PaddingValues(StreamTokens.spacingMd), @@ -333,7 +333,7 @@ private fun PollViewResultTitle( } @Composable -private fun PollResultSection( +internal fun PollSection( modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), verticalArrangement: Arrangement.Vertical = Arrangement.Top, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt index 44e3d7c899c..b137bd391c2 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt @@ -319,7 +319,6 @@ internal fun DefaultMessageTypingIndicatorContent(state: TypingItemState) { overlap = StreamTokens.spacingXs, users = state.typingUsers.take(MaxTypingUsersAvatars), avatarSize = AvatarSize.Medium, - showBorder = true, trailingContent = if (overflowCount > 0) { { CountBadge(text = "+$overflowCount", size = CountBadgeSize.Medium) } } else { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt index f51f93440b8..b7f5a5744c9 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt @@ -176,7 +176,6 @@ internal fun ThreadItemParticipants(participants: List) { overlap = StreamTokens.spacingXs, users = participants, avatarSize = AvatarSize.Small, - showBorder = true, ) } diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index c5f7c5c2d66..d67a400516b 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -337,7 +337,7 @@ Poll Results Poll Comments Add a Comment - Update Comment + Update Your Comment %d vote %d votes diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_footer.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_footer.png index fb1817536f8..1542bff6c70 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_footer.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_footer.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_no_replies.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_no_replies.png index 4d7cd93cbf8..87270fea6bf 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_no_replies.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_no_replies.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_replies.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_replies.png index 53324e0c7b2..acd29a04573 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_replies.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_MessageFooterTest_thread_start_with_replies.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_PollMessageContentTest_poll_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_PollMessageContentTest_poll_content.png index 5db03c01a4e..43df277c758 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_PollMessageContentTest_poll_content.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_PollMessageContentTest_poll_content.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollMoreOptionsDialogTest_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollMoreOptionsDialogTest_dark_mode.png index 41906193196..a2ce6ab951c 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollMoreOptionsDialogTest_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollMoreOptionsDialogTest_dark_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollMoreOptionsDialogTest_light_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollMoreOptionsDialogTest_light_mode.png index 1c54900939a..511219f66ac 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollMoreOptionsDialogTest_light_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollMoreOptionsDialogTest_light_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_TypingIndicatorContentTest_multiple_users_typing.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_TypingIndicatorContentTest_multiple_users_typing.png index 4700e6f5fee..e2f20fd1071 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_TypingIndicatorContentTest_multiple_users_typing.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_TypingIndicatorContentTest_multiple_users_typing.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png index 0d227199bbb..8cf4687e731 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png index bb9e97ec2b6..5ba6d8f5bd7 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.png index 4016a5d4690..294d02fecd6 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.png index 239c47c5b83..29adfff961e 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.png index 4d2394f9e7a..caad27fd416 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.png index c994643ea79..c6b2178079d 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.png differ diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/PollsConstants.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/PollsConstants.kt index 480a6941035..23e7f9c32c4 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/PollsConstants.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/PollsConstants.kt @@ -41,5 +41,5 @@ public object PollsConstants { /** * The maximum number of visible options in a poll message. */ - public const val MAX_NUMBER_OF_VISIBLE_OPTIONS: Int = 10 + public const val MAX_NUMBER_OF_VISIBLE_OPTIONS: Int = 5 }