diff --git a/docusaurus/docs/Android/assets/poll_attachments.png b/docusaurus/docs/Android/assets/poll_attachments.png new file mode 100644 index 00000000000..0bc20ba8420 Binary files /dev/null and b/docusaurus/docs/Android/assets/poll_attachments.png differ diff --git a/docusaurus/docs/Android/assets/polls_configurations.png b/docusaurus/docs/Android/assets/polls_configurations.png new file mode 100644 index 00000000000..f5b72901928 Binary files /dev/null and b/docusaurus/docs/Android/assets/polls_configurations.png differ diff --git a/docusaurus/docs/Android/assets/polls_creation_dark.png b/docusaurus/docs/Android/assets/polls_creation_dark.png new file mode 100644 index 00000000000..7f8ed6c85a2 Binary files /dev/null and b/docusaurus/docs/Android/assets/polls_creation_dark.png differ diff --git a/docusaurus/docs/Android/assets/polls_creation_dialog.png b/docusaurus/docs/Android/assets/polls_creation_dialog.png new file mode 100644 index 00000000000..a0e6d688df2 Binary files /dev/null and b/docusaurus/docs/Android/assets/polls_creation_dialog.png differ diff --git a/docusaurus/docs/Android/assets/polls_creation_light.png b/docusaurus/docs/Android/assets/polls_creation_light.png new file mode 100644 index 00000000000..bec928454b5 Binary files /dev/null and b/docusaurus/docs/Android/assets/polls_creation_light.png differ diff --git a/docusaurus/docs/Android/assets/polls_dashboard.png b/docusaurus/docs/Android/assets/polls_dashboard.png new file mode 100644 index 00000000000..ec13957fd79 Binary files /dev/null and b/docusaurus/docs/Android/assets/polls_dashboard.png differ diff --git a/docusaurus/docs/Android/assets/polls_header.png b/docusaurus/docs/Android/assets/polls_header.png new file mode 100644 index 00000000000..848987e7105 Binary files /dev/null and b/docusaurus/docs/Android/assets/polls_header.png differ diff --git a/docusaurus/docs/Android/assets/polls_message_content.png b/docusaurus/docs/Android/assets/polls_message_content.png new file mode 100644 index 00000000000..17fa263b3a4 Binary files /dev/null and b/docusaurus/docs/Android/assets/polls_message_content.png differ diff --git a/docusaurus/docs/Android/assets/polls_option_list.png b/docusaurus/docs/Android/assets/polls_option_list.png new file mode 100644 index 00000000000..5f675eee77d Binary files /dev/null and b/docusaurus/docs/Android/assets/polls_option_list.png differ diff --git a/docusaurus/docs/Android/assets/polls_question_input.png b/docusaurus/docs/Android/assets/polls_question_input.png new file mode 100644 index 00000000000..1f90a7b8f71 Binary files /dev/null and b/docusaurus/docs/Android/assets/polls_question_input.png differ diff --git a/docusaurus/docs/Android/assets/polls_switches.png b/docusaurus/docs/Android/assets/polls_switches.png new file mode 100644 index 00000000000..f52b011dfa1 Binary files /dev/null and b/docusaurus/docs/Android/assets/polls_switches.png differ diff --git a/docusaurus/docs/Android/compose/polls.mdx b/docusaurus/docs/Android/compose/polls.mdx new file mode 100644 index 00000000000..a6cd2da2862 --- /dev/null +++ b/docusaurus/docs/Android/compose/polls.mdx @@ -0,0 +1,426 @@ +# Polls + +Stream Chat's Compose SDK offers the capability to create polls within your chat application. Polls are an effective tool for enhancing user interaction and engagement, providing a dynamic way to gather opinions and feedback from users. This feature allows you to seamlessly integrate interactive polls, making your chat application more engaging and interactive. + +:::note +Polls on Compose are available since version **6.5.0**. +::: + +Polls are disabled by default. To enable this feature, navigate to the [Stream dashboard](https://getstream.io/dashboard/) for your app and enable the **Polls** flag for your channel type. + +![Polls Dashboard](../assets/polls_dashboard.png) + +Once you enable that feature, an additional "Polls" icon will appear in the attachment picker within the default composer of the SDK. + +![Poll attachment](../assets/poll_attachments.png) + +## Poll Configurations + +When you tap the "Polls" icon, a new screen for creating polls will be displayed. Here, you can set up the poll title, the options, and various settings such as the maximum number of votes, whether the poll is anonymous, and if it allows comments. This allows for a customized polling experience within the app. + +![Poll Configurations](../assets/polls_configurations.png) + +You can determine which options users can configure when creating a poll by providing your own `PollsConfig`. For instance, you can create a configuration that hides the "Comments" option and allows multiple votes by default. This customization ensures that the polling experience aligns with the desired usage and interaction patterns within your application. + +Now it's time to create the poll using `PollConfig` and `AttachmentsPicker`. The `AttachmentsPicker` allows you to define user behavior for poll creation by utilizing the `onAttachmentPickerAction` parameter. Here's an example: + +```kotlin +var isFullScreenContent by rememberSaveable { mutableStateOf(false) } +val screenHeight = LocalConfiguration.current.screenHeightDp +val pickerHeight by animateDpAsState( + targetValue = if (isFullScreenContent) screenHeight.dp else ChatTheme.dimens.attachmentsPickerHeight, + label = "full sized picker animation", +) + +AttachmentsPicker( + attachmentsPickerViewModel = attachmentsPickerViewModel, + modifier = Modifier + .align(Alignment.BottomCenter) + .height(pickerHeight), + onTabClick = { _, tab -> isFullScreenContent = tab.isFullContent }, + onAttachmentPickerAction = { action -> + if (action is AttachmentPickerPollCreation) { + listViewModel.createPoll( + pollConfig = PollConfig( + name = action.question, + options = action.options.filter { it.title.isNotEmpty() }.map { it.title }, + description = action.question, + allowUserSuggestedOptions = action.switches.any { it.key == "allowUserSuggestedOptions" && it.enabled }, + votingVisibility = if (action.switches.any { it.key == "votingVisibility" && it.enabled }) { + VotingVisibility.ANONYMOUS + } else { + VotingVisibility.PUBLIC + }, + maxVotesAllowed = if (action.switches.any { it.key == "maxVotesAllowed" && it.enabled }) { + action.switches.first { it.key == "maxVotesAllowed" }.pollSwitchInput?.value.toString() + .toInt() + } else { + 1 + }, + ), + ) + } + }, + .. +) +``` + +## Customize Poll Creation Screens + +You can customize the poll creation screen by implementing your own [AttachmentsPicker](https://getstream.io/chat/docs/sdk/android/compose/message-components/attachments-picker/) and `AttachmentsPickerTabFactory`. The default implementation is provided within `ChatTheme` using `StreamAttachmentFactories.defaultFactories()`. This allows you to tailor the appearance and behavior of the poll creation interface to better fit your application's needs and user experience expectations. + +```kotlin +/** + * Holds the information required to add support for "poll" tab in the attachment picker. + */ +public class AttachmentsPickerPollTabFactory : AttachmentsPickerTabFactory { + + /** + * The attachment picker mode that this factory handles. + */ + override val attachmentsPickerMode: AttachmentsPickerMode + get() = Poll + + /** + * Emits a file icon for this tab. + * + * @param isEnabled If the tab is enabled. + * @param isSelected If the tab is selected. + */ + @Composable + override fun PickerTabIcon(isEnabled: Boolean, isSelected: Boolean) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_poll), + contentDescription = stringResource(id = R.string.stream_compose_poll_option), + tint = when { + isEnabled -> ChatTheme.colors.textLowEmphasis + else -> ChatTheme.colors.disabled + }, + ) + } + + /** + * Emits content that allows users to create a poll in this tab. + * + * @param onAttachmentPickerAction A lambda that will be invoked when an action is happened. + * @param attachments The list of attachments to display. + * @param onAttachmentsChanged Handler to set the loaded list of attachments to display. + * @param onAttachmentItemSelected Handler when the item selection state changes. + * @param onAttachmentsSubmitted Handler to submit the selected attachments to the message composer. + */ + @Composable + override fun PickerTabContent( + onAttachmentPickerAction: (AttachmentPickerAction) -> Unit, + attachments: List, + onAttachmentsChanged: (List) -> Unit, + onAttachmentItemSelected: (AttachmentPickerItemState) -> Unit, + onAttachmentsSubmitted: (List) -> Unit, + ) { + val coroutineScope = rememberCoroutineScope() + val questionListLazyState = rememberLazyListState() + val pollSwitchItemFactory = ChatTheme.pollSwitchitemFactory + var optionItemList by remember { mutableStateOf(emptyList()) } + var switchItemList: List by remember { mutableStateOf(pollSwitchItemFactory.providePollSwitchItemList()) } + var hasErrorOnOptions by remember { mutableStateOf(false) } + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = -available.y + coroutineScope.launch { + questionListLazyState.scrollBy(delta) + } + return Offset.Zero + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .nestedScroll(nestedScrollConnection) + .verticalScroll(rememberScrollState()) + .background(ChatTheme.colors.appBackground), + ) { + val (question, onQuestionChanged) = rememberSaveable { mutableStateOf("") } + val isEnabled = question.isNotBlank() && optionItemList.any { it.title.isNotBlank() } && !hasErrorOnOptions + val hasChanges = question.isNotBlank() || optionItemList.any { it.title.isNotBlank() } + var isShowingDiscardDialog by remember { mutableStateOf(false) } + + PollCreationHeader( + modifier = Modifier.fillMaxWidth(), + enabledCreation = isEnabled, + onPollCreateClicked = { + onAttachmentPickerAction.invoke( + AttachmentPickerPollCreation( + question = question, + options = optionItemList, + switches = switchItemList, + ), + ) + onAttachmentPickerAction.invoke(AttachmentPickerBack) + }, + onBackPressed = { + if (!hasChanges) { + onAttachmentPickerAction.invoke(AttachmentPickerBack) + } else { + isShowingDiscardDialog = true + } + }, + ) + + PollQuestionInput( + question = question, + onQuestionChanged = onQuestionChanged, + ) + + PollOptionList( + lazyListState = questionListLazyState, + onQuestionsChanged = { + optionItemList = it + hasErrorOnOptions = it.fastAny { item -> item.pollOptionError != null } + }, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + PollSwitchList( + pollSwitchItems = switchItemList, + onSwitchesChanged = { + switchItemList = it + hasErrorOnOptions = it.fastAny { item -> item.pollOptionError != null } + }, + ) + + if (isShowingDiscardDialog) { + PollCreationDiscardDialog( + onCancelClicked = { isShowingDiscardDialog = false }, + onDiscardClicked = { + isShowingDiscardDialog = false + onAttachmentPickerAction.invoke(AttachmentPickerBack) + }, + ) + } + } + } +} +``` + +The `PickerTabContent` composable function in the code above includes several key components: `PollCreationHeader`, `PollQuestionInput`, `PollOptionList`, `PollSwitchList`, and `PollCreationDiscardDialog`. Let's break down each UI component one by one to understand their roles and how they contribute to the overall poll creation screen. + +### PollCreationHeader + +![Poll Header](../assets/polls_header.png) + +This is the header of the poll creation screen. It consists of three parts: leading, center, and trailing content, each with its dedicated roles such as navigating back, displaying the title, and submitting the poll creation. You can fully customize the header using the provided parameters as shown in the code below: + +```kotlin +public fun PollCreationHeader( + modifier: Modifier = Modifier, + color: Color = ChatTheme.colors.appBackground, + shape: Shape = ChatTheme.shapes.header, + elevation: Dp = 0.dp, + onBackPressed: () -> Unit = {}, + enabledCreation: Boolean, + onPollCreateClicked: () -> Unit, + leadingContent: @Composable (RowScope.() -> Unit)? = null, + centerContent: @Composable (RowScope.() -> Unit)? = null, + trailingContent: @Composable (RowScope.() -> Unit)? = null, +) +``` + +### PollQuestionInput + +![Poll Header](../assets/polls_question_input.png) + +The poll input component allows the poll creator to describe the question of the poll. You can use this component to receive the poll title during poll creation. This provides a user-friendly interface for entering and displaying the main question of the poll, ensuring clarity and engagement. + +```kotlin +val (question, onQuestionChanged) = rememberSaveable { mutableStateOf("") } + +PollQuestionInput( + question = question, + onQuestionChanged = onQuestionChanged, +) +``` + +### PollOptionList + +![Poll Option List](../assets/polls_option_list.png) + +The re-orderable list of options consists of input fields where each entry must be unique. If a duplicate entry is detected, an error message will be displayed. This ensures that all poll options are distinct, maintaining the integrity and clarity of the poll. + +```kotlin +val questionListLazyState = rememberLazyListState() +var optionItemList by remember { mutableStateOf(emptyList()) } +var hasErrorOnOptions by remember { mutableStateOf(false) } + +PollOptionList( + lazyListState = questionListLazyState, + onQuestionsChanged = { + optionItemList = it + hasErrorOnOptions = it.fastAny { item -> item.pollOptionError != null } + }, +) +``` +The `onQuestionsChanged` lambda provides a list of `PollOptionItem`, which contains information about each option item whenever the input list is modified, reordered, or added to. This ensures that any changes to the poll options are tracked and updated accordingly. + +### PollSwitchList + +![Poll Switch List](../assets/polls_switches.png) + +The list of switches configures the poll properties, such as allowing multiple answers, enabling anonymous polling, or suggesting an option. These switches provide flexibility in setting up the poll to match the desired level of engagement and privacy. + +```kotlin +val pollSwitchItemFactory = ChatTheme.pollSwitchitemFactory +var switchItemList: List by remember { mutableStateOf(pollSwitchItemFactory.providePollSwitchItemList()) } +var hasErrorOnOptions by remember { mutableStateOf(false) } + +PollSwitchList( + pollSwitchItems = switchItemList, + onSwitchesChanged = { + switchItemList = it + hasErrorOnOptions = it.fastAny { item -> item.pollOptionError != null } + }, +) +``` + +The `onSwitchesChanged` lambda provides a list of `PollSwitchItem`, detailing each switch configuration whenever changes occur in the switch list. You can customize the default switch options by implementing your own `PollSwitchItemFactory`, as shown in the example below: + +```kotlin +public class MyPollSwitchItemFactory( + private val context: Context, +) : PollSwitchItemFactory { + + /** + * Provides a default list of [PollSwitchItem] to create the poll switch item list. + */ + override fun providePollSwitchItemList(): List = + listOf( + PollSwitchItem( + title = context.getString(R.string.stream_compose_poll_option_switch_multiple_answers), + pollSwitchInput = PollSwitchInput(keyboardType = KeyboardType.Decimal, maxValue = 2, value = 0), + key = "maxVotesAllowed", + enabled = false, + ), + PollSwitchItem( + title = context.getString(R.string.stream_compose_poll_option_switch_anonymous_poll), + key = "votingVisibility", + enabled = false, + ), + PollSwitchItem( + title = context.getString(R.string.stream_compose_poll_option_switch_suggest_option), + key = "allowUserSuggestedOptions", + enabled = false, + ), + PollSwitchItem( + title = context.getString(R.string.stream_compose_poll_option_switch_add_comment), + enabled = false, + ), + ) +} +``` + +You can apply your custom `PollSwitchItemFactory` to the `ChatTheme` as shown in the example below: + +```kotlin +ChatTheme( + pollSwitchItemFactory = MyPollSwitchItemFactory(context) +) { + .. +} +``` + +### PollCreationDiscardDialog + +![Poll Switch List](../assets/polls_creation_dialog.png) + +The dialog that appears when a user attempts to navigate back with unsaved changes on the poll creation screen helps prevent accidental loss of poll information. This safeguard ensures users are prompted to confirm their intent to leave, thereby protecting any data they have entered from being inadvertently discarded. + +```kotlin +var isShowingDiscardDialog by remember { mutableStateOf(false) } + +PollCreationDiscardDialog( + onCancelClicked = { isShowingDiscardDialog = false }, + onDiscardClicked = { + isShowingDiscardDialog = false + onAttachmentPickerAction.invoke(AttachmentPickerBack) + }, +) +``` + +In most case, you should use it with the `PollCreationHeader` like the example below: + +```kotlin +var isShowingDiscardDialog by remember { mutableStateOf(false) } + +PollCreationHeader( + modifier = Modifier.fillMaxWidth(), + enabledCreation = isEnabled, + onPollCreateClicked = { + onAttachmentPickerAction.invoke( + AttachmentPickerPollCreation( + question = question, + options = optionItemList, + switches = switchItemList, + ), + ) + onAttachmentPickerAction.invoke(AttachmentPickerBack) + }, + onBackPressed = { + if (!hasChanges) { + onAttachmentPickerAction.invoke(AttachmentPickerBack) + } else { + isShowingDiscardDialog = true + } + }, +) + +if (isShowingDiscardDialog) { + PollCreationDiscardDialog( + onCancelClicked = { isShowingDiscardDialog = false }, + onDiscardClicked = { + isShowingDiscardDialog = false + onAttachmentPickerAction.invoke(AttachmentPickerBack) + }, + ) +} +``` + +By combining all the components mentioned above, you will see the resulting poll creation screen below: + +| Light Mode - Poll Creation | Dark Mode - Poll Creation +|---------------------------------------------------------------------------|---------------------------------------------------------------------------| +| ![Poll Creation Light](../assets/polls_creation_light.png) | ![Poll Creation Dark](../assets/polls_creation_dark.png) | + +This setup includes a customizable header, input fields for the poll question, a list of options with unique validation, configurable switches, and a back navigation confirmation dialog. + +## PollMessageContent + +![Poll Message Content](../assets/polls_message_content.png) + +The [PollMessageContent](https://github.com/GetStream/stream-chat-android/tree/main/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt) displays the poll question and answer options, distinguishing between the poll owner and other users. It allows user interaction, showing real-time vote counts and participants, along with the question title, answer items, and additional option buttons. + +It is implemented inside the [MessageItem](https://github.com/GetStream/stream-chat-android/blob/b4edd566fce3a009fe326f508f025692e09acc5f/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt#L414), so you can customize the poll content message by implementing your own `MessageItem` composable. + +```kotlin +if (messageItem.message.isPoll()) { + val poll = messageItem.message.poll + LaunchedEffect(key1 = poll) { + if (poll != null) { + onPollUpdated.invoke(messageItem.message, poll) + } + } + + PollMessageContent( + modifier = modifier, + messageItem = messageItem, + onCastVote = onCastVote, + onRemoveVote = onRemoveVote, + selectPoll = selectPoll, + onClosePoll = onClosePoll, + onLongItemClick = onLongItemClick, + ) +} + +.. +``` \ No newline at end of file diff --git a/stream-chat-android-client/api/stream-chat-android-client.api b/stream-chat-android-client/api/stream-chat-android-client.api index 802db9c13cc..d7cf5cd7629 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -2860,6 +2860,8 @@ public final class io/getstream/chat/android/client/utils/message/MessageUtils { public static final fun isGiphyEphemeral (Lio/getstream/chat/android/models/Message;)Z public static final fun isModerationError (Lio/getstream/chat/android/models/Message;Ljava/lang/String;)Z public static final fun isPinnedAndNotDeleted (Lio/getstream/chat/android/models/Message;)Z + public static final fun isPoll (Lio/getstream/chat/android/models/Message;)Z + public static final fun isPollClosed (Lio/getstream/chat/android/models/Message;)Z public static final fun isRegular (Lio/getstream/chat/android/models/Message;)Z public static final fun isReply (Lio/getstream/chat/android/models/Message;)Z public static final fun isSystem (Lio/getstream/chat/android/models/Message;)Z diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/message/MessageUtils.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/message/MessageUtils.kt index 8ec85d21583..8d09444679b 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/message/MessageUtils.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/message/MessageUtils.kt @@ -125,6 +125,16 @@ public fun Message.isError(): Boolean = type == MessageType.ERROR */ public fun Message.isGiphy(): Boolean = command == AttachmentType.GIPHY +/** + * @return If the message is related to the poll. + */ +public fun Message.isPoll(): Boolean = poll != null + +/** + * @return If the message is related to the poll. + */ +public fun Message.isPollClosed(): Boolean = poll?.closed == true + /** * @return If the message is a temporary message to select a gif. */ 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 68b14604ba0..c7c669c94cc 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -880,6 +880,13 @@ public final class io/getstream/chat/android/compose/ui/components/avatar/Compos public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/chat/android/compose/ui/components/avatar/ComposableSingletons$UserAvatarRowKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/avatar/ComposableSingletons$UserAvatarRowKt; + 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/Function2; +} + public final class io/getstream/chat/android/compose/ui/components/avatar/GroupAvatarKt { public static final fun GroupAvatar (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V } @@ -893,7 +900,11 @@ public final class io/getstream/chat/android/compose/ui/components/avatar/Initia } public final class io/getstream/chat/android/compose/ui/components/avatar/UserAvatarKt { - public static final fun UserAvatar-fzeukOM (Lio/getstream/chat/android/models/User;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/text/TextStyle;Ljava/lang/String;ZLio/getstream/chat/android/compose/state/OnlineIndicatorAlignment;JLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V + public static final fun UserAvatar-L6JiHuU (Lio/getstream/chat/android/models/User;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/text/TextStyle;Ljava/lang/String;ZLandroidx/compose/ui/graphics/painter/Painter;Lio/getstream/chat/android/compose/state/OnlineIndicatorAlignment;JLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V +} + +public final class io/getstream/chat/android/compose/ui/components/avatar/UserAvatarRowKt { + public static final fun UserAvatarRow-DG5NWxM (Ljava/util/List;Landroidx/compose/ui/Modifier;IFILandroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/text/TextStyle;Ljava/lang/String;JLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/components/channels/ChannelMembersKt { @@ -1046,6 +1057,13 @@ public final class io/getstream/chat/android/compose/ui/components/messageoption public static final fun defaultMessageOptionsState (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;ZLjava/util/Set;Landroidx/compose/runtime/Composer;I)Ljava/util/List; } +public final class io/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$GiphyMessageContentKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$GiphyMessageContentKt; + 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/Function2; +} + public final class io/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$MessageReactionItemKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$MessageReactionItemKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; @@ -1080,6 +1098,17 @@ public final class io/getstream/chat/android/compose/ui/components/messages/Comp public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$PollMessageContentKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$PollMessageContentKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function2; + public static field lambda-3 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 fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; +} + public final class io/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$QuotedMessageContentKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$QuotedMessageContentKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; @@ -1133,6 +1162,10 @@ public final class io/getstream/chat/android/compose/ui/components/messages/Owne public static final fun OwnedMessageVisibilityContent (Lio/getstream/chat/android/models/Message;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } +public final class io/getstream/chat/android/compose/ui/components/messages/PollMessageContentKt { + public static final fun PollMessageContent (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/chat/android/compose/ui/components/messages/QuotedMessageContentKt { public static final fun QuotedMessageContent (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/models/Message;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } @@ -1175,6 +1208,28 @@ public final class io/getstream/chat/android/compose/ui/components/moderatedmess public static final fun ModeratedMessageOptionItem (Lio/getstream/chat/android/ui/common/state/messages/list/ModeratedMessageOption;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } +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 fun ()V + 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$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/Function3; + public static field lambda-4 Lkotlin/jvm/functions/Function3; + public static field lambda-5 Lkotlin/jvm/functions/Function3; + 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/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/Function3; +} + public final class io/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollOptionInputKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollOptionInputKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; @@ -1186,11 +1241,38 @@ public final class io/getstream/chat/android/compose/ui/components/poll/Composab public final fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollViewResultDialogKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollViewResultDialogKt; + 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/Function3; + public static field lambda-4 Lkotlin/jvm/functions/Function3; + public static field lambda-5 Lkotlin/jvm/functions/Function3; + 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/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/Function3; +} + +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 final class io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialogKt { + public static final fun PollMoreOptionsDialog (Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V +} + public final class io/getstream/chat/android/compose/ui/components/poll/PollOptionInputKt { public static final fun PollOptionInput (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Ljava/lang/String;ZIILandroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/text/KeyboardOptions;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V public static final fun PollOptionInputPreview (Landroidx/compose/runtime/Composer;I)V } +public final class io/getstream/chat/android/compose/ui/components/poll/PollViewResultDialogKt { + public static final fun PollViewResultDialog (Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V +} + public final class io/getstream/chat/android/compose/ui/components/reactionoptions/ComposableSingletons$ExtendedReactionsOptionsKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/reactionoptions/ComposableSingletons$ExtendedReactionsOptionsKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; @@ -1691,17 +1773,17 @@ public final class io/getstream/chat/android/compose/ui/messages/list/Composable } public final class io/getstream/chat/android/compose/ui/messages/list/MessageContainerKt { - public static final fun MessageContainer (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lio/getstream/chat/android/models/ReactionSorting;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun MessageContainer (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lio/getstream/chat/android/models/ReactionSorting;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V } public final class io/getstream/chat/android/compose/ui/messages/list/MessageItemKt { public static final field HighlightFadeOutDurationMillis I - public static final fun MessageItem (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lio/getstream/chat/android/models/ReactionSorting;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V + public static final fun MessageItem (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lio/getstream/chat/android/models/ReactionSorting;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V } 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;Lio/getstream/chat/android/models/ReactionSorting;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Lio/getstream/chat/android/compose/ui/messages/list/ThreadMessagesStart;Lkotlin/jvm/functions/Function1;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/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V - public static final fun MessageList (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/ThreadMessagesStart;Lio/getstream/chat/android/models/ReactionSorting;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/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V + public static final fun MessageList (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Lio/getstream/chat/android/models/ReactionSorting;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Lio/getstream/chat/android/compose/ui/messages/list/ThreadMessagesStart;Lkotlin/jvm/functions/Function1;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/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V + public static final fun MessageList (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/ThreadMessagesStart;Lio/getstream/chat/android/models/ReactionSorting;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/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V } public final class io/getstream/chat/android/compose/ui/messages/list/MessagesKt { @@ -2652,13 +2734,16 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageL public fun (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController;)V public final fun banUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V public static synthetic fun banUser$default (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)V + public final fun castVote (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Option;)V public final fun clearNewMessageState ()V + public final fun closePoll (Ljava/lang/String;)V public final fun createPoll (Lio/getstream/chat/android/models/PollConfig;)V 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 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 public final fun flagMessage (Lio/getstream/chat/android/models/Message;Ljava/lang/String;Ljava/util/Map;)V public final fun getChannel ()Lio/getstream/chat/android/models/Channel; public final fun getConnectionState ()Lkotlinx/coroutines/flow/StateFlow; @@ -2668,12 +2753,14 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageL public final fun getMessageById (Ljava/lang/String;)Lio/getstream/chat/android/models/Message; public final fun getMessageFooterVisibilityState ()Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility; public final fun getMessageMode ()Lio/getstream/chat/android/ui/common/state/messages/MessageMode; + public final fun getPollState ()Lio/getstream/chat/android/ui/common/state/messages/poll/PollState; public final fun getShowSystemMessagesState ()Z public final fun getTypingUsers ()Ljava/util/List; public final fun getUser ()Lkotlinx/coroutines/flow/StateFlow; public final fun isInThread ()Z public final fun isOnline ()Lkotlinx/coroutines/flow/Flow; public final fun isShowingOverlay ()Z + public final fun isShowingPollOptionDetails ()Z public final fun leaveThread ()V public final fun loadNewerMessages (Ljava/lang/String;I)V public static synthetic fun loadNewerMessages$default (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Ljava/lang/String;IILjava/lang/Object;)V @@ -2686,6 +2773,7 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageL public final fun performMessageAction (Lio/getstream/chat/android/ui/common/state/messages/MessageAction;)V public final fun removeOverlay ()V public final fun removeShadowBanFromUser (Ljava/lang/String;)V + public final fun removeVote (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)V public final fun scrollToBottom (ILkotlin/jvm/functions/Function0;)V public static synthetic fun scrollToBottom$default (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;ILkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public final fun scrollToMessage (Ljava/lang/String;Ljava/lang/String;)V @@ -2703,6 +2791,7 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageL public final fun unbanUser (Ljava/lang/String;)V public final fun unmuteUser (Ljava/lang/String;)V public final fun updateLastSeenMessage (Lio/getstream/chat/android/models/Message;)V + public final fun updatePollState (Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType;)V } public final class io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory : androidx/lifecycle/ViewModelProvider$Factory { diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index 5ec6b13c773..7f7595026b5 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -2,7 +2,7 @@ - ComplexCondition:MessageItem.kt$!messageItem.isMine && ( messageItem.showMessageFooter || messageItem.groupPosition.contains(MessagePosition.BOTTOM) || messageItem.groupPosition.contains(MessagePosition.NONE) ) + ComplexCondition:MessageItem.kt$!messageItem.isMine && ( messageItem.showMessageFooter || messageItem.groupPosition.contains(MessagePosition.BOTTOM) || messageItem.groupPosition.contains( MessagePosition.NONE, ) ) ComplexCondition:MessageTranslatedLabel.kt$!isGiphy && !isDeleted && userLanguage != i18nLanguage && translatedText != messageItem.message.text ComplexCondition:Messages.kt$!endOfMessages && index == messages.lastIndex && messages.isNotEmpty() && lazyListState.isScrollInProgress ForbiddenComment:MessageText.kt$// TODO: Fix emoji font padding once this is resolved and exposed: https://issuetracker.google.com/issues/171394808 @@ -24,6 +24,10 @@ LongMethod:MessageOptions.kt$@Composable public fun defaultMessageOptionsState( selectedMessage: Message, currentUser: User?, isInThread: Boolean, ownCapabilities: Set<String>, ): List<MessageOptionItemState> LongMethod:MessagesScreen.kt$@OptIn(ExperimentalAnimationApi::class) @Composable private fun BoxScope.AttachmentsPickerMenu( listViewModel: MessageListViewModel, attachmentsPickerViewModel: AttachmentsPickerViewModel, composerViewModel: MessageComposerViewModel, ) LongMethod:PollCreationDiscardDialog.kt$@Composable public fun PollCreationDiscardDialog( usePlatformDefaultWidth: Boolean = false, onCancelClicked: () -> Unit, onDiscardClicked: () -> Unit, ) + LongMethod:PollMessageContent.kt$@Composable private fun PollMessageContent( message: Message, poll: Poll, isMine: Boolean, onClosePoll: (String) -> Unit, onCastVote: (Option) -> Unit, onRemoveVote: (Vote) -> Unit, selectPoll: (Message, Poll, PollSelectionType) -> Unit, ) + LongMethod:PollMessageContent.kt$@Composable private fun PollOptionItem( modifier: Modifier = Modifier, poll: Poll, option: Option, voteCount: Int, totalVoteCount: Int, users: List<User>, checkedCount: Int, checked: Boolean, onCastVote: () -> Unit, onRemoveVote: () -> Unit, ) + LongMethod:PollMessageContent.kt$@Composable public fun PollMessageContent( modifier: Modifier, messageItem: MessageItemState, onCastVote: (Message, Poll, Option) -> Unit, onRemoveVote: (Message, Poll, Vote) -> Unit, selectPoll: (Message, Poll, PollSelectionType) -> Unit, onClosePoll: (String) -> Unit, onLongItemClick: (Message) -> Unit = {}, ) + LongMethod:PollMoreOptionsDialog.kt$@Composable public fun PollMoreOptionsDialog( selectedPoll: SelectedPoll?, listViewModel: MessageListViewModel, onDismissRequest: () -> Unit, onBackPressed: () -> Unit, ) LongMethod:PollOptionList.kt$@Composable public fun PollOptionList( modifier: Modifier = Modifier, lazyListState: LazyListState = rememberLazyListState(), title: String = stringResource(id = R.string.stream_compose_poll_option_title), optionItems: List<PollOptionItem> = emptyList(), onQuestionsChanged: (List<PollOptionItem>) -> Unit, itemHeightSize: Dp = ChatTheme.dimens.pollOptionInputHeight, itemInnerPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 4.dp), ) LongMethod:PollSwitchList.kt$@Composable public fun PollSwitchList( modifier: Modifier = Modifier, pollSwitchItems: List<PollSwitchItem>, onSwitchesChanged: (List<PollSwitchItem>) -> Unit, itemHeightSize: Dp = ChatTheme.dimens.pollOptionInputHeight, itemInnerPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 16.dp), ) LongMethod:StreamTypography.kt$StreamTypography.Companion$public fun defaultTypography(fontFamily: FontFamily? = null): StreamTypography @@ -32,7 +36,12 @@ LongParameterList:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$( context: Context, mediaGalleryPreviewAction: MediaGalleryPreviewAction, currentPage: Int, attachments: List<Attachment>, writePermissionState: PermissionState, downloadPayload: MutableState<Attachment?>, ) LongParameterList:MediaGalleryPreviewActivityAttachmentState.kt$MediaGalleryPreviewActivityAttachmentState$( val name: String?, val thumbUrl: String?, val imageUrl: String?, val assetUrl: String?, val originalWidth: Int?, val originalHeight: Int?, val type: String?, ) LongParameterList:MessageComposer.kt$( value: String, coolDownTime: Int, attachments: List<Attachment>, validationErrors: List<ValidationError>, ownCapabilities: Set<String>, isInEditMode: Boolean, onSendMessage: (String, List<Attachment>) -> Unit, onRecordingSaved: (Attachment) -> Unit, statefulStreamMediaRecorder: StatefulStreamMediaRecorder?, ) + LongParameterList:MessageItem.kt$( messageItem: MessageItemState, onLongItemClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onPollUpdated: (Message, Poll) -> Unit, onCastVote: (Message, Poll, Option) -> Unit, onRemoveVote: (Message, Poll, Vote) -> Unit, selectPoll: (Message, Poll, PollSelectionType) -> Unit, onClosePoll: (String) -> Unit, ) LongParameterList:MessagesScreen.kt$( listViewModel: MessageListViewModel, composerViewModel: MessageComposerViewModel, selectedMessageState: SelectedMessageState?, selectedMessage: Message, skipPushNotification: Boolean, skipEnrichUrl: Boolean, ) + LongParameterList:PollMessageContent.kt$( message: Message, poll: Poll, isMine: Boolean, onClosePoll: (String) -> Unit, onCastVote: (Option) -> Unit, onRemoveVote: (Vote) -> Unit, selectPoll: (Message, Poll, PollSelectionType) -> Unit, ) + LongParameterList:PollMessageContent.kt$( modifier: Modifier = Modifier, poll: Poll, option: Option, voteCount: Int, totalVoteCount: Int, users: List<User>, checkedCount: Int, checked: Boolean, onCastVote: () -> Unit, onRemoveVote: () -> Unit, ) + LongParameterList:PollMessageContent.kt$( modifier: Modifier, messageItem: MessageItemState, onCastVote: (Message, Poll, Option) -> Unit, onRemoveVote: (Message, Poll, Vote) -> Unit, selectPoll: (Message, Poll, PollSelectionType) -> Unit, onClosePoll: (String) -> Unit, onLongItemClick: (Message) -> Unit = {}, ) + LongParameterList:PollMoreOptionsDialog.kt$( index: Int, poll: Poll, option: Option, voteCount: Int, checkedCount: Int, checked: Boolean, onCastVote: () -> Unit, onRemoveVote: () -> Unit, ) MagicNumber:AudioRecording.kt$2.5f MagicNumber:AudioWaveSeekbar.kt$0.4 MagicNumber:AudioWaveSeekbar.kt$100F @@ -51,9 +60,16 @@ MagicNumber:MessageComposer.kt$60_000 MagicNumber:Messages.kt$3 MagicNumber:Messages.kt$5 + MagicNumber:PollMessageContent.kt$0.5f + MagicNumber:PollMessageContent.kt$10 + MagicNumber:PollMoreOptionsDialog.kt$200 + MagicNumber:PollMoreOptionsDialog.kt$400 MagicNumber:PollOptionList.kt$5 MagicNumber:PollOptionList.kt$8 MagicNumber:PollSwitchList.kt$8 + MagicNumber:PollViewResultDialog.kt$200 + MagicNumber:PollViewResultDialog.kt$400 + MagicNumber:PreviewPollData.kt$PreviewPollData$3 MagicNumber:SearchInput.kt$8f MagicNumber:TypingIndicatorAnimatedDot.kt$0.5f MaxLineLength:AttachmentsPickerPollTabFactory.kt$AttachmentsPickerPollTabFactory$var switchItemList: List<PollSwitchItem> by remember { mutableStateOf(pollSwitchItemFactory.providePollSwitchItemList()) } @@ -61,6 +77,8 @@ MaxLineLength:ChatTheme.kt$error("No attachments picker tab factories provided! Make sure to wrap all usages of Stream components in a ChatTheme.") MaxLineLength:MediaAttachmentContent.kt$mediaGalleryPreviewLauncher: ManagedActivityResultLauncher<MediaGalleryPreviewContract.Input, MediaGalleryPreviewResult?> MaxLineLength:MediaAttachmentFactory.kt$mediaGalleryPreviewLauncher: ManagedActivityResultLauncher<MediaGalleryPreviewContract.Input, MediaGalleryPreviewResult?> + MaxLineLength:MessageItem.kt$message.isDeleted() && messageItem.deletedMessageVisibility == DeletedMessageVisibility.VISIBLE_FOR_CURRENT_USER + MaxLineLength:MessageItem.kt$messageItem.showMessageFooter || messageItem.groupPosition.contains(MessagePosition.BOTTOM) MaxLineLength:MessageOptions.kt$iconPainter = painterResource(id = if (selectedMessage.pinned) R.drawable.stream_compose_ic_unpin_message else R.drawable.stream_compose_ic_pin_message) MaxLineLength:MessageOptions.kt$title = if (selectedMessage.pinned) R.string.stream_compose_unpin_message else R.string.stream_compose_pin_message MaxLineLength:MessagesScreen.kt$* diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/previewdata/PreviewMessageData.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/previewdata/PreviewMessageData.kt index 2d5d85d8be6..cfe0f590772 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/previewdata/PreviewMessageData.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/previewdata/PreviewMessageData.kt @@ -47,4 +47,18 @@ internal object PreviewMessageData { type = MessageType.REGULAR, ownReactions = mutableListOf(Reaction(messageId = "message-id-3", type = "haha")), ) + + val messageWithError: Message = Message( + id = "message-id-4", + text = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + createdAt = Date(), + type = MessageType.ERROR, + ) + + val messageWithPoll: Message = Message( + id = "message-id-5", + createdAt = Date(), + type = MessageType.REGULAR, + poll = PreviewPollData.poll1, + ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/previewdata/PreviewPollData.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/previewdata/PreviewPollData.kt new file mode 100644 index 00000000000..688cce80b14 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/previewdata/PreviewPollData.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2014-2024 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.previewdata + +import io.getstream.chat.android.models.Option +import io.getstream.chat.android.models.Poll +import io.getstream.chat.android.models.Vote +import io.getstream.chat.android.models.VotingVisibility +import java.util.Date +import java.util.UUID + +/** + * Provides sample poll that will be used to render previews. + */ +internal object PreviewPollData { + + private val option1 = Option( + id = UUID.randomUUID().toString(), + text = "option1", + ) + + private val option2 = Option( + id = UUID.randomUUID().toString(), + text = "option2", + ) + + private val option3 = Option( + id = UUID.randomUUID().toString(), + text = "option3", + ) + + val poll1 = Poll( + id = UUID.randomUUID().toString(), + name = "Vote an option!", + description = "This is a poll", + options = listOf(option1, option2, option3), + votingVisibility = VotingVisibility.PUBLIC, + enforceUniqueVote = true, + maxVotesAllowed = 1, + allowUserSuggestedOptions = false, + allowAnswers = true, + voteCountsByOption = mapOf( + option1.id to 3, + option2.id to 1, + option3.id to 1, + ), + votes = listOf( + Vote( + id = UUID.randomUUID().toString(), + pollId = UUID.randomUUID().toString(), + optionId = option1.id, + createdAt = Date(), + updatedAt = Date(), + user = PreviewUserData.user1, + ), + Vote( + id = UUID.randomUUID().toString(), + pollId = UUID.randomUUID().toString(), + optionId = option1.id, + createdAt = Date(), + updatedAt = Date(), + user = PreviewUserData.user2, + ), + Vote( + id = UUID.randomUUID().toString(), + pollId = UUID.randomUUID().toString(), + optionId = option1.id, + createdAt = Date(), + updatedAt = Date(), + user = PreviewUserData.user3, + ), + Vote( + id = UUID.randomUUID().toString(), + pollId = UUID.randomUUID().toString(), + optionId = option2.id, + createdAt = Date(), + updatedAt = Date(), + user = PreviewUserData.user4, + ), + Vote( + id = UUID.randomUUID().toString(), + pollId = UUID.randomUUID().toString(), + optionId = option3.id, + createdAt = Date(), + updatedAt = Date(), + user = PreviewUserData.user5, + ), + ), + ownVotes = listOf(), + createdAt = Date(), + updatedAt = Date(), + closed = false, + ) +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/UserAvatar.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/UserAvatar.kt index 7f054c93148..6c632234448 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/UserAvatar.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/UserAvatar.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset @@ -58,6 +59,7 @@ public fun UserAvatar( textStyle: TextStyle = ChatTheme.typography.title3Bold, contentDescription: String? = null, showOnlineIndicator: Boolean = true, + placeholderPainter: Painter? = null, onlineIndicatorAlignment: OnlineIndicatorAlignment = OnlineIndicatorAlignment.TopEnd, initialsAvatarOffset: DpOffset = DpOffset(0.dp, 0.dp), onlineIndicator: @Composable BoxScope.() -> Unit = { @@ -74,6 +76,7 @@ public fun UserAvatar( shape = shape, contentDescription = contentDescription, onClick = onClick, + placeholderPainter = placeholderPainter, initialsAvatarOffset = initialsAvatarOffset, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/UserAvatarRow.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/UserAvatarRow.kt new file mode 100644 index 00000000000..eedd476d918 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/UserAvatarRow.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2014-2024 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.avatar + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import io.getstream.chat.android.compose.previewdata.PreviewUserData +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.models.User + +/** + * Represents the [User] avatar that's shown on the Messages screen or in headers of DMs. + * + * Based on the state within the [User], we either show an image or their initials. + * + * @param users The list of users whose avatar we want to show. + * @param modifier Modifier for styling. + * @param maxAvatarCount The maximum count for displaying the avatar row. + * @param size The size of each user avatar. + * @param offset The offset of the + * @param shape The shape of the avatar. + * @param textStyle The [TextStyle] that will be used for the initials. + * @param contentDescription The content description of the avatar. + * @param initialsAvatarOffset The initials offset to apply to the avatar. + * @param onClick The handler when the user clicks on the avatar. + */ +@Composable +public fun UserAvatarRow( + users: List, + modifier: Modifier = Modifier, + maxAvatarCount: Int = 3, + size: Dp = 20.dp, + offset: Int = 7, + shape: Shape = ChatTheme.shapes.avatar, + textStyle: TextStyle = ChatTheme.typography.title3Bold, + contentDescription: String? = null, + initialsAvatarOffset: DpOffset = DpOffset(0.dp, 0.dp), + onClick: (() -> Unit)? = null, +) { + if (users.isEmpty()) return + + Row( + modifier = modifier.width(IntrinsicSize.Min), + horizontalArrangement = Arrangement.spacedBy((-offset).dp), + ) { + users.take(maxAvatarCount).forEachIndexed { index, user -> + UserAvatar( + modifier = Modifier + .size(size) + .zIndex((users.size - index).toFloat()), + user = user, + shape = shape, + textStyle = textStyle, + showOnlineIndicator = false, + contentDescription = contentDescription, + initialsAvatarOffset = initialsAvatarOffset, + onClick = onClick, + ) + } + } +} + +@Preview +@Composable +private fun UserAvatarRowPreview() { + ChatTheme { + Column( + horizontalAlignment = Alignment.End, + ) { + UserAvatarRow( + users = listOf( + PreviewUserData.user1, + ), + ) + UserAvatarRow( + users = listOf( + PreviewUserData.user1, + PreviewUserData.user2, + ), + ) + UserAvatarRow( + users = listOf( + PreviewUserData.user1, + PreviewUserData.user2, + PreviewUserData.user3, + ), + ) + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt index 3b2906ad662..2d684519092 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt @@ -41,8 +41,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource 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 io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.previewdata.PreviewMessageData import io.getstream.chat.android.compose.ui.attachments.content.MessageAttachmentsContent import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.models.Message @@ -199,3 +201,15 @@ internal fun GiphyButton( ) } } + +@Preview +@Composable +private fun GiphyMessageContentPreview() { + ChatTheme { + GiphyMessageContent( + modifier = Modifier.size(600.dp), + message = PreviewMessageData.message1, + onGiphyActionClick = {}, + ) + } +} 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 new file mode 100644 index 00000000000..aabddb48f0a --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt @@ -0,0 +1,470 @@ +/* + * Copyright (c) 2014-2024 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.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +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.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.getstream.chat.android.client.utils.message.isDeleted +import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.previewdata.PreviewMessageData +import io.getstream.chat.android.compose.ui.components.avatar.UserAvatarRow +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.util.isErrorOrFailed +import io.getstream.chat.android.models.Message +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.ui.common.state.messages.list.MessageItemState +import io.getstream.chat.android.ui.common.state.messages.list.MessagePosition +import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType + +/** + * Message content for the poll, which distinguishes the owner and users and allows them to interact. + * + * @param messageItem The message item to show the content for. + * @param modifier Modifier for styling. + * @param onCastVote Callback when a user cast a vote on an option. + * @param onMoreOption Callback when a user clicked seeing more options. + * @param onRemoveVote Callback when a user remove a vote on an option. + * @param onLongItemClick Handler when the user selects a message, on long tap. + */ +@Composable +public fun PollMessageContent( + modifier: Modifier, + messageItem: MessageItemState, + onCastVote: (Message, Poll, Option) -> Unit, + onRemoveVote: (Message, Poll, Vote) -> Unit, + selectPoll: (Message, Poll, PollSelectionType) -> Unit, + onClosePoll: (String) -> Unit, + onLongItemClick: (Message) -> Unit = {}, +) { + val message = messageItem.message + val position = messageItem.groupPosition + val ownsMessage = messageItem.isMine + + val messageBubbleShape = when { + position.contains(MessagePosition.TOP) || position.contains(MessagePosition.MIDDLE) -> RoundedCornerShape(16.dp) + else -> { + if (ownsMessage) ChatTheme.shapes.myMessageBubble else ChatTheme.shapes.otherMessageBubble + } + } + + val messageBubbleColor = when { + message.isDeleted() -> when (ownsMessage) { + true -> ChatTheme.ownMessageTheme.deletedBackgroundColor + else -> ChatTheme.otherMessageTheme.deletedBackgroundColor + } + + else -> when (ownsMessage) { + true -> ChatTheme.colors.linkBackground + else -> ChatTheme.otherMessageTheme.backgroundColor + } + } + + val poll = message.poll + if (!messageItem.isErrorOrFailed() && poll != null) { + MessageBubble( + modifier = modifier, + shape = messageBubbleShape, + color = messageBubbleColor, + border = if (messageItem.isMine) null else BorderStroke(1.dp, ChatTheme.colors.borders), + content = { + PollMessageContent( + message = message, + poll = poll, + isMine = ownsMessage, + onCastVote = { option -> + onCastVote.invoke(message, poll, option) + }, + onRemoveVote = { vote -> + onRemoveVote.invoke(message, poll, vote) + }, + selectPoll = selectPoll, + onClosePoll = onClosePoll, + ) + }, + ) + } else { + Box(modifier = modifier) { + MessageBubble( + modifier = Modifier.padding(end = 12.dp), + shape = messageBubbleShape, + color = messageBubbleColor, + content = { + MessageContent( + message = message, + currentUser = messageItem.currentUser, + onLongItemClick = onLongItemClick, + onGiphyActionClick = {}, + onMediaGalleryPreviewResult = {}, + onQuotedMessageClick = {}, + ) + }, + ) + + Icon( + modifier = Modifier + .size(24.dp) + .align(Alignment.BottomEnd), + painter = painterResource(id = R.drawable.stream_compose_ic_error), + contentDescription = null, + tint = ChatTheme.colors.errorAccent, + ) + } + } +} + +@Composable +private fun PollMessageContent( + message: Message, + poll: Poll, + isMine: Boolean, + onClosePoll: (String) -> Unit, + onCastVote: (Option) -> Unit, + onRemoveVote: (Vote) -> Unit, + selectPoll: (Message, Poll, PollSelectionType) -> Unit, +) { + val heightMax = LocalConfiguration.current.screenHeightDp + val isClosed = poll.closed + + LazyColumn( + modifier = Modifier + .padding( + horizontal = 10.dp, + vertical = 12.dp, + ) + .heightIn(max = heightMax.dp), + userScrollEnabled = false, + ) { + item { + Text( + modifier = Modifier.padding(bottom = 4.dp), + text = poll.name, + color = ChatTheme.colors.textHighEmphasis, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + ) + } + + item { + Text( + modifier = Modifier.padding(bottom = 8.dp), + text = poll.name, + color = ChatTheme.colors.textLowEmphasis, + fontSize = 12.sp, + ) + } + + items( + items = poll.options.take(10), + key = { it.id }, + ) { option -> + val voteCount = poll.voteCountsByOption[option.id] ?: 0 + val isVotedByMine = poll.ownVotes.any { it.optionId == option.id } + + PollOptionItem( + poll = poll, + option = option, + voteCount = voteCount, + users = poll.votes.filter { it.optionId == option.id }.mapNotNull { it.user }, + totalVoteCount = poll.votes.size, + checkedCount = poll.ownVotes.count { it.optionId == option.id }, + checked = isVotedByMine, + onCastVote = { onCastVote.invoke(option) }, + onRemoveVote = { + val vote = poll.votes.firstOrNull { it.optionId == option.id } ?: return@PollOptionItem + onRemoveVote.invoke(vote) + }, + ) + } + + if (poll.options.size > 10) { + item { + PollOptionButton( + text = stringResource(id = R.string.stream_compose_poll_see_more_options, poll.options.size), + onButtonClicked = { selectPoll.invoke(message, poll, PollSelectionType.MoreOption) }, + ) + } + } + + item { + PollOptionButton( + text = stringResource(id = R.string.stream_compose_poll_view_result), + onButtonClicked = { selectPoll.invoke(message, poll, PollSelectionType.ViewResult) }, + ) + } + + if (isMine && !isClosed) { + item { + PollOptionButton( + text = stringResource(id = R.string.stream_compose_poll_end_vote), + onButtonClicked = { onClosePoll.invoke(poll.id) }, + ) + } + } + } +} + +@Composable +private fun PollOptionItem( + modifier: Modifier = Modifier, + poll: Poll, + option: Option, + voteCount: Int, + totalVoteCount: Int, + users: List, + checkedCount: Int, + checked: Boolean, + onCastVote: () -> Unit, + onRemoveVote: () -> Unit, +) { + val isVotedByMine = poll.ownVotes.any { it.optionId == option.id } + + Column( + modifier = modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (!poll.closed) { + PollItemCheckBox( + enabled = checked, + onCheckChanged = { enabled -> + if (enabled && checkedCount < poll.maxVotesAllowed && !checked) { + onCastVote.invoke() + } else if (!enabled) { + onRemoveVote.invoke() + } + }, + ) + } + + Text( + modifier = Modifier + .weight(0.5f) + .padding(start = 4.dp, bottom = 2.dp), + text = option.text, + color = ChatTheme.colors.textHighEmphasis, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + fontSize = 16.sp, + ) + + Row { + if (voteCount > 0) { + UserAvatarRow( + modifier = Modifier.padding(end = 2.dp), + users = users, + ) + } + + Text( + modifier = Modifier.padding(bottom = 2.dp), + text = voteCount.toString(), + color = ChatTheme.colors.textHighEmphasis, + fontSize = 16.sp, + ) + } + } + + val progress = if (voteCount == 0 || totalVoteCount == 0) { + 0f + } else { + voteCount / totalVoteCount.toFloat() + } + + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding( + start = if (poll.closed) { + 0.dp + } else { + 22.dp + }, + ) + .clip(RoundedCornerShape(4.dp)) + .height(4.dp), + progress = progress, + color = if (isVotedByMine) { + ChatTheme.colors.infoAccent + } else { + ChatTheme.colors.primaryAccent + }, + backgroundColor = ChatTheme.colors.inputBackground, + ) + } +} + +@Composable +internal fun PollItemCheckBox( + modifier: Modifier = Modifier, + enabled: Boolean, + onCheckChanged: (Boolean) -> Unit, +) { + Box( + modifier = modifier + .size(18.dp) + .background( + if (enabled) { + ChatTheme.colors.primaryAccent + } else { + ChatTheme.colors.disabled + }, + CircleShape, + ) + .padding(1.dp) + .background( + if (enabled) { + ChatTheme.colors.primaryAccent + } else { + ChatTheme.colors.inputBackground + }, + CircleShape, + ) + .clickable { onCheckChanged.invoke(!enabled) }, + ) { + if (enabled) { + Icon( + modifier = Modifier + .align(Alignment.Center) + .padding(3.dp), + painter = painterResource(id = R.drawable.stream_compose_ic_checkmark), + tint = Color.White, + contentDescription = null, + ) + } + } +} + +@Composable +private fun PollOptionButton( + text: String, + onButtonClicked: () -> Unit, +) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 11.dp) + .clickable { onButtonClicked.invoke() }, + textAlign = TextAlign.Center, + text = text, + color = ChatTheme.colors.primaryAccent, + fontSize = 16.sp, + ) +} + +@Preview +@Composable +private fun PollMessageContentPreview() { + ChatTheme { + Column(modifier = Modifier.background(ChatTheme.colors.appBackground)) { + PollMessageContent( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + onCastVote = { _, _, _ -> }, + onRemoveVote = { _, _, _ -> }, + selectPoll = { _, _, _ -> }, + onClosePoll = {}, + messageItem = MessageItemState( + message = PreviewMessageData.messageWithPoll, + isMine = true, + ), + ) + + PollMessageContent( + modifier = Modifier + .fillMaxWidth() + .padding(6.dp), + onCastVote = { _, _, _ -> }, + onRemoveVote = { _, _, _ -> }, + selectPoll = { _, _, _ -> }, + onClosePoll = {}, + messageItem = MessageItemState( + message = PreviewMessageData.messageWithError, + isMine = true, + ), + ) + } + } +} + +@Preview +@Composable +private fun PollItemCheckBoxPreview() { + ChatTheme { + Row { + PollItemCheckBox( + enabled = false, + onCheckChanged = {}, + ) + + PollItemCheckBox( + enabled = true, + onCheckChanged = {}, + ) + } + } +} + +@Preview +@Composable +private fun PollOptionButtonPreview() { + ChatTheme { + Column { + PollOptionButton("End Vote") {} + PollOptionButton("View Result") {} + } + } +} 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 new file mode 100644 index 00000000000..ae1e17bf786 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollDialogHeader.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2014-2024 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.poll + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +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.components.BackButton +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.util.mirrorRtl + +@Composable +public fun PollDialogHeader( + title: String, + onBackPressed: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val layoutDirection = LocalLayoutDirection.current + + BackButton( + modifier = Modifier + .mirrorRtl(layoutDirection = layoutDirection) + .padding(end = 32.dp), + painter = painterResource(id = R.drawable.stream_compose_ic_arrow_back), + onBackPressed = onBackPressed, + ) + + Text( + text = title, + style = ChatTheme.typography.title3Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = ChatTheme.colors.textHighEmphasis, + ) + } +} + +@Preview +@Composable +private fun PollDialogHeaderPreview() { + ChatTheme { + Box( + modifier = Modifier + .fillMaxWidth() + .background(ChatTheme.colors.appBackground), + ) { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_results), + onBackPressed = {}, + ) + } + } +} 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 new file mode 100644 index 00000000000..95d59fa1a55 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialog.kt @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2014-2024 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. + */ + +@file:OptIn(ExperimentalAnimationApi::class) + +package io.getstream.chat.android.compose.ui.components.poll + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.MutableTransitionState +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.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.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +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.res.stringResource +import androidx.compose.ui.text.font.FontWeight +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.previewdata.PreviewPollData +import io.getstream.chat.android.compose.ui.components.messages.PollItemCheckBox +import io.getstream.chat.android.compose.ui.theme.ChatTheme +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.Vote +import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll + +/** + * A dialog that should be shown if a user taps the seeing more options on the poll message. + * + * @param selectedPoll The current poll that contains all the states. + * @param listViewModel The [MessageListViewModel] used to read state from. + * @param onDismissRequest Handler for dismissing the dialog. + * @param onBackPressed Handler for pressing a back button. + */ +@Composable +public fun PollMoreOptionsDialog( + selectedPoll: SelectedPoll?, + listViewModel: MessageListViewModel, + onDismissRequest: () -> Unit, + onBackPressed: () -> Unit, +) { + val state = remember { + MutableTransitionState(false).apply { + // Start the animation immediately. + targetState = true + } + } + Popup( + alignment = Alignment.BottomCenter, + onDismissRequest = onDismissRequest, + ) { + AnimatedVisibility( + visibleState = state, + enter = fadeIn() + slideInVertically( + animationSpec = tween(400), + initialOffsetY = { fullHeight -> fullHeight / 2 }, + ), + exit = fadeOut(animationSpec = tween(200)) + + slideOutVertically(animationSpec = tween(400)), + label = "poll more options dialog", + ) { + if (selectedPoll != null) { + val poll = selectedPoll.poll + val message = selectedPoll.message + + BackHandler { onBackPressed.invoke() } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground), + ) { + item { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_options), + onBackPressed = onBackPressed, + ) + } + + item { PollMoreOptionsTitle(title = poll.name) } + + item { Spacer(modifier = Modifier.height(16.dp)) } + + pollMoreOptionsContent( + poll = poll, + onCastVote = { option -> + listViewModel.castVote( + message = message, + poll = poll, + option = option, + ) + }, + onRemoveVote = { vote -> + listViewModel.removeVote( + message = message, + poll = poll, + vote = vote, + ) + }, + ) + + item { Spacer(modifier = Modifier.height(16.dp)) } + } + } + } + } +} + +@Composable +internal fun PollMoreOptionsTitle(title: String) { + Box( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .heightIn(min = 56.dp) + .clip(shape = ChatTheme.shapes.pollOptionInput) + .background(ChatTheme.colors.inputBackground) + .padding(16.dp), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterStart), + text = title, + color = ChatTheme.colors.textHighEmphasis, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + ) + } +} + +internal fun LazyListScope.pollMoreOptionsContent( + poll: Poll, + 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 } + + PollMoreOptionItem( + index = index, + poll = poll, + option = option, + voteCount = voteCount, + checkedCount = poll.ownVotes.count { it.optionId == option.id }, + checked = isVotedByMine, + onCastVote = { onCastVote.invoke(option) }, + onRemoveVote = { + val vote = poll.votes.firstOrNull { it.optionId == option.id } ?: return@PollMoreOptionItem + onRemoveVote.invoke(vote) + }, + ) + } +} + +@Composable +internal fun PollMoreOptionItem( + index: Int, + poll: Poll, + option: Option, + voteCount: Int, + 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) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .background(color = ChatTheme.colors.inputBackground, shape = shape) + .padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (!poll.closed) { + PollItemCheckBox( + enabled = checked, + onCheckChanged = { enabled -> + if (enabled && checkedCount < poll.maxVotesAllowed && !checked) { + onCastVote.invoke() + } else if (!enabled) { + onRemoveVote.invoke() + } + }, + ) + } + + Text( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp), + text = option.text, + color = ChatTheme.colors.textHighEmphasis, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + fontSize = 16.sp, + ) + + Text( + modifier = Modifier.padding(bottom = 2.dp), + text = voteCount.toString(), + color = ChatTheme.colors.textHighEmphasis, + fontSize = 16.sp, + ) + } +} + +@Preview +@Composable +private fun PollMoreOptionsDialogPreview() { + val poll = PreviewPollData.poll1 + + ChatTheme { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground), + ) { + item { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_options), + onBackPressed = {}, + ) + } + + item { PollMoreOptionsTitle(title = poll.name) } + + item { Spacer(modifier = Modifier.height(16.dp)) } + + pollMoreOptionsContent( + poll = poll, + onCastVote = {}, + onRemoveVote = {}, + ) + + item { Spacer(modifier = Modifier.height(16.dp)) } + } + } +} 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 new file mode 100644 index 00000000000..168f7355ab4 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollViewResultDialog.kt @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2014-2024 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. + */ + +@file:OptIn(ExperimentalAnimationApi::class) + +package io.getstream.chat.android.compose.ui.components.poll + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.MutableTransitionState +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.Column +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.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +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.previewdata.PreviewPollData +import io.getstream.chat.android.compose.ui.components.avatar.UserAvatar +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.models.Option +import io.getstream.chat.android.models.Poll +import io.getstream.chat.android.models.Vote +import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll + +/** + * A dialog that should be shown if a user taps the seeing result of the votes. + * + * @param selectedPoll The current poll that contains all the states. + * @param onDismissRequest Handler for dismissing the dialog. + * @param onBackPressed Handler for pressing a back button. + */ +@Composable +public fun PollViewResultDialog( + selectedPoll: SelectedPoll?, + onDismissRequest: () -> Unit, + onBackPressed: () -> Unit, +) { + val state = remember { + MutableTransitionState(false).apply { + // Start the animation immediately. + targetState = true + } + } + Popup( + alignment = Alignment.BottomCenter, + onDismissRequest = onDismissRequest, + ) { + AnimatedVisibility( + visibleState = state, + enter = fadeIn() + slideInVertically( + animationSpec = tween(400), + initialOffsetY = { fullHeight -> fullHeight / 2 }, + ), + exit = fadeOut(animationSpec = tween(200)) + + slideOutVertically(animationSpec = tween(400)), + label = "poll view result dialog", + ) { + if (selectedPoll != null) { + val poll = selectedPoll.poll + + BackHandler { onBackPressed.invoke() } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground), + ) { + item { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_results), + onBackPressed = onBackPressed, + ) + } + + item { PollViewResultTitle(title = poll.name) } + + item { Spacer(modifier = Modifier.height(16.dp)) } + + pollViewResultContent(poll = poll) + + item { Spacer(modifier = Modifier.height(16.dp)) } + } + } + } + } +} + +internal fun LazyListScope.pollViewResultContent( + poll: Poll, +) { + val votes = poll.votes + val options = poll.options.sortedByDescending { option -> votes.count { it.optionId == option.id } } + + itemsIndexed( + items = options, + key = { _, option -> option.id }, + ) { index, option -> + PollViewResultItem( + index = index, + option = option, + votes = votes.filter { it.optionId == option.id }, + ) + } +} + +@Composable +private fun PollViewResultItem( + index: Int, + option: Option, + votes: List, +) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 4.dp) + .fillMaxWidth() + .clip(shape = ChatTheme.shapes.pollOptionInput) + .background(ChatTheme.colors.inputBackground) + .padding(16.dp), + ) { + Row(modifier = Modifier.fillMaxWidth()) { + Text( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = option.text, + color = ChatTheme.colors.textHighEmphasis, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + ) + + if (index == 0) { + Icon( + modifier = Modifier.padding(end = 8.dp), + painter = painterResource(id = R.drawable.stream_compose_ic_award), + tint = ChatTheme.colors.textHighEmphasis, + contentDescription = null, + ) + } + + Text( + text = stringResource(id = R.string.stream_compose_poll_vote_counts, votes.size), + color = ChatTheme.colors.textHighEmphasis, + fontSize = 16.sp, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + votes.forEach { vote -> + PollVoteItem(vote = vote) + } + } +} + +@Composable +private fun PollVoteItem(vote: Vote) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val user = vote.user + if (user != null) { + UserAvatar( + modifier = Modifier.size(20.dp), + user = user, + showOnlineIndicator = false, + ) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp) + .weight(1f), + text = user.name, + color = ChatTheme.colors.textHighEmphasis, + fontSize = 14.sp, + ) + } + } +} + +@Composable +internal fun PollViewResultTitle(title: String) { + Box( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .heightIn(min = 56.dp) + .clip(shape = ChatTheme.shapes.pollOptionInput) + .background(ChatTheme.colors.inputBackground) + .padding(16.dp), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterStart), + text = title, + color = ChatTheme.colors.textHighEmphasis, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + ) + } +} + +@Preview +@Composable +internal fun PollViewResultDialogPreview() { + val poll = PreviewPollData.poll1 + + ChatTheme { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground), + ) { + item { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_results), + onBackPressed = {}, + ) + } + + item { PollViewResultTitle(title = poll.name) } + + item { Spacer(modifier = Modifier.height(16.dp)) } + + pollViewResultContent(poll = poll) + + item { Spacer(modifier = Modifier.height(16.dp)) } + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt index 5f0a0291969..a8e5754bd56 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt @@ -62,6 +62,8 @@ import io.getstream.chat.android.compose.state.messages.attachments.StatefulStre import io.getstream.chat.android.compose.ui.components.SimpleDialog import io.getstream.chat.android.compose.ui.components.messageoptions.defaultMessageOptionsState import io.getstream.chat.android.compose.ui.components.moderatedmessage.ModeratedMessageDialog +import io.getstream.chat.android.compose.ui.components.poll.PollMoreOptionsDialog +import io.getstream.chat.android.compose.ui.components.poll.PollViewResultDialog import io.getstream.chat.android.compose.ui.components.reactionpicker.ReactionsPicker import io.getstream.chat.android.compose.ui.components.selectedmessage.SelectedMessageMenu import io.getstream.chat.android.compose.ui.components.selectedmessage.SelectedReactionsMenu @@ -99,6 +101,7 @@ import io.getstream.chat.android.ui.common.state.messages.list.SelectedMessageRe import io.getstream.chat.android.ui.common.state.messages.list.SelectedMessageReactionsState import io.getstream.chat.android.ui.common.state.messages.list.SelectedMessageState import io.getstream.chat.android.ui.common.state.messages.list.SendAnyway +import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType import io.getstream.chat.android.ui.common.state.messages.updateMessage /** @@ -310,6 +313,7 @@ public fun MessagesScreen( skipEnrichUrl = skipEnrichUrl, ) MessageDialogs(listViewModel = listViewModel) + PollDialogs(listViewModel = listViewModel) } } @@ -741,3 +745,26 @@ private fun MessageDialogs(listViewModel: MessageListViewModel) { ) } } + +@Composable +private fun PollDialogs(listViewModel: MessageListViewModel) { + val dismiss = { listViewModel.displayPollMoreOptions(null) } + val selectedPoll = listViewModel.pollState.selectedPoll + + if (selectedPoll?.pollSelectionType == PollSelectionType.MoreOption) { + PollMoreOptionsDialog( + selectedPoll = selectedPoll, + onDismissRequest = { dismiss.invoke() }, + onBackPressed = { dismiss.invoke() }, + listViewModel = listViewModel, + ) + } + + if (selectedPoll?.pollSelectionType == PollSelectionType.ViewResult) { + PollViewResultDialog( + selectedPoll = selectedPoll, + onDismissRequest = { dismiss.invoke() }, + onBackPressed = { dismiss.invoke() }, + ) + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt index f55791f15a4..284696b1f5a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt @@ -36,8 +36,11 @@ import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryP import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.viewmodel.messages.MessagesViewModelFactory import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Option +import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.ReactionSorting import io.getstream.chat.android.models.User +import io.getstream.chat.android.models.Vote import io.getstream.chat.android.ui.common.feature.messages.list.MessageListController import io.getstream.chat.android.ui.common.state.messages.list.DateSeparatorItemState import io.getstream.chat.android.ui.common.state.messages.list.EmptyThreadPlaceholderItemState @@ -49,6 +52,7 @@ import io.getstream.chat.android.ui.common.state.messages.list.SystemMessageItem import io.getstream.chat.android.ui.common.state.messages.list.ThreadDateSeparatorItemState import io.getstream.chat.android.ui.common.state.messages.list.TypingItemState import io.getstream.chat.android.ui.common.state.messages.list.UnreadSeparatorItemState +import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType /** * Represents the message item container that allows us to customize each type of item in the MessageList. @@ -59,6 +63,8 @@ import io.getstream.chat.android.ui.common.state.messages.list.UnreadSeparatorIt * @param onReactionsClick Handler when the user taps on message reactions. * @param onThreadClick Handler when the user taps on a thread within a message item. * @param onGiphyActionClick Handler when the user taps on Giphy message actions. + * @param onCastVote Handler for casting a vote on an option. + * @param onClosePoll Handler for closing a poll. * @param onQuotedMessageClick Handler for quoted message click action. * @param onUserAvatarClick Handler when users avatar is clicked. * @param onMediaGalleryPreviewResult Handler when the user receives a result from the Media Gallery Preview. @@ -78,6 +84,11 @@ public fun MessageContainer( onLongItemClick: (Message) -> Unit = {}, onReactionsClick: (Message) -> Unit = {}, onThreadClick: (Message) -> Unit = {}, + onPollUpdated: (Message, Poll) -> Unit = { _, _ -> }, + onCastVote: (Message, Poll, Option) -> Unit = { _, _, _ -> }, + onRemoveVote: (Message, Poll, Vote) -> Unit = { _, _, _ -> }, + selectPoll: (Message, Poll, PollSelectionType) -> Unit = { _, _, _ -> }, + onClosePoll: (String) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, onUserAvatarClick: ((User) -> Unit)? = null, @@ -101,6 +112,11 @@ public fun MessageContainer( onLongItemClick = onLongItemClick, onReactionsClick = onReactionsClick, onThreadClick = onThreadClick, + onPollUpdated = onPollUpdated, + onCastVote = onCastVote, + onRemoveVote = onRemoveVote, + selectPoll = selectPoll, + onClosePoll = onClosePoll, onGiphyActionClick = onGiphyActionClick, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick, @@ -239,6 +255,10 @@ internal fun DefaultSystemMessageContent(systemMessageState: SystemMessageItemSt * @param onLongItemClick Handler when the user long taps on an item. * @param onReactionsClick Handler when the user taps on message reactions. * @param onThreadClick Handler when the user clicks on the message thread. + * @param onCastVote Handler for casting a vote on an option. + * @param onRemoveVote Handler for removing a vote on an option. + * @param onMoreOption Handler for seeing more options. + * @param onClosePoll Handler for closing a poll. * @param onGiphyActionClick Handler when the user selects a Giphy action. * @param onQuotedMessageClick Handler for quoted message click action. * @param onMediaGalleryPreviewResult Handler when the user receives a result from the Media Gallery Preview. @@ -252,6 +272,11 @@ internal fun DefaultMessageItem( onReactionsClick: (Message) -> Unit = {}, onThreadClick: (Message) -> Unit, onGiphyActionClick: (GiphyAction) -> Unit, + onPollUpdated: (Message, Poll) -> Unit = { _, _ -> }, + onCastVote: (Message, Poll, Option) -> Unit, + onRemoveVote: (Message, Poll, Vote) -> Unit, + selectPoll: (Message, Poll, PollSelectionType) -> Unit, + onClosePoll: (String) -> Unit, onQuotedMessageClick: (Message) -> Unit, onUserAvatarClick: () -> Unit, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, @@ -262,6 +287,11 @@ internal fun DefaultMessageItem( onLongItemClick = onLongItemClick, onReactionsClick = onReactionsClick, onThreadClick = onThreadClick, + onPollUpdated = onPollUpdated, + onCastVote = onCastVote, + onRemoveVote = onRemoveVote, + selectPoll = selectPoll, + onClosePoll = onClosePoll, onGiphyActionClick = onGiphyActionClick, onQuotedMessageClick = onQuotedMessageClick, onUserAvatarClick = onUserAvatarClick, 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 6d2b5817de6..0f1c6a739a8 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 @@ -41,6 +41,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.BottomEnd @@ -56,6 +57,7 @@ import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.client.utils.message.isGiphyEphemeral import io.getstream.chat.android.client.utils.message.isPinned +import io.getstream.chat.android.client.utils.message.isPoll import io.getstream.chat.android.client.utils.message.isThreadStart import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult @@ -68,6 +70,7 @@ import io.getstream.chat.android.compose.ui.components.messages.MessageHeaderLab import io.getstream.chat.android.compose.ui.components.messages.MessageReactions import io.getstream.chat.android.compose.ui.components.messages.MessageText import io.getstream.chat.android.compose.ui.components.messages.OwnedMessageVisibilityContent +import io.getstream.chat.android.compose.ui.components.messages.PollMessageContent import io.getstream.chat.android.compose.ui.components.messages.QuotedMessage import io.getstream.chat.android.compose.ui.components.messages.UploadingFooter import io.getstream.chat.android.compose.ui.theme.ChatTheme @@ -75,13 +78,17 @@ import io.getstream.chat.android.compose.ui.util.isEmojiOnlyWithoutBubble import io.getstream.chat.android.compose.ui.util.isErrorOrFailed import io.getstream.chat.android.compose.ui.util.isUploading import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Option +import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.ReactionSorting import io.getstream.chat.android.models.User +import io.getstream.chat.android.models.Vote import io.getstream.chat.android.ui.common.state.messages.list.DeletedMessageVisibility import io.getstream.chat.android.ui.common.state.messages.list.GiphyAction import io.getstream.chat.android.ui.common.state.messages.list.MessageFocused import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState import io.getstream.chat.android.ui.common.state.messages.list.MessagePosition +import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType /** * The default message container for all messages in the Conversation/Messages screen. @@ -100,6 +107,9 @@ import io.getstream.chat.android.ui.common.state.messages.list.MessagePosition * @param modifier Modifier for styling. * @param onReactionsClick Handler when the user taps on message reactions. * @param onThreadClick Handler for thread clicks, if this message has a thread going. + * @param onCastVote Handler for casting a vote on an option. + * @param onMoreOption Handler for seeing more options. + * @param onClosePoll Handler for closing a poll. * @param onGiphyActionClick Handler when the user taps on an action button in a giphy message item. * @param onQuotedMessageClick Handler for quoted message click action. * @param onUserAvatarClick Handler when users avatar is clicked. @@ -125,6 +135,11 @@ public fun MessageItem( modifier: Modifier = Modifier, onReactionsClick: (Message) -> Unit = {}, onThreadClick: (Message) -> Unit = {}, + onPollUpdated: (Message, Poll) -> Unit = { _, _ -> }, + onCastVote: (Message, Poll, Option) -> Unit = { _, _, _ -> }, + onRemoveVote: (Message, Poll, Vote) -> Unit = { _, _, _ -> }, + selectPoll: (Message, Poll, PollSelectionType) -> Unit = { _, _, _ -> }, + onClosePoll: (String) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, onUserAvatarClick: (() -> Unit)? = null, @@ -149,6 +164,11 @@ public fun MessageItem( onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onGiphyActionClick = onGiphyActionClick, onQuotedMessageClick = onQuotedMessageClick, + onPollUpdated = onPollUpdated, + onCastVote = onCastVote, + onRemoveVote = onRemoveVote, + selectPoll = selectPoll, + onClosePoll = onClosePoll, ) }, footerContent: @Composable ColumnScope.(MessageItemState) -> Unit = { @@ -242,11 +262,10 @@ internal fun RowScope.DefaultMessageItemLeadingContent( .size(24.dp) .align(Alignment.Bottom) - if (!messageItem.isMine && - ( - messageItem.showMessageFooter || - messageItem.groupPosition.contains(MessagePosition.BOTTOM) || - messageItem.groupPosition.contains(MessagePosition.NONE) + if (!messageItem.isMine && ( + messageItem.showMessageFooter || messageItem.groupPosition.contains(MessagePosition.BOTTOM) || messageItem.groupPosition.contains( + MessagePosition.NONE, + ) ) ) { UserAvatar( @@ -316,20 +335,15 @@ internal fun DefaultMessageItemHeaderContent( val ownReactions = message.ownReactions val reactionGroups = message.reactionGroups.ifEmpty { return } val iconFactory = ChatTheme.reactionIconFactory - reactionGroups - .filter { iconFactory.isReactionSupported(it.key) } - .takeIf { it.isNotEmpty() } - ?.toList() - ?.sortedWith { o1, o2 -> reactionSorting.compare(o1.second, o2.second) } - ?.map { (type, _) -> + reactionGroups.filter { iconFactory.isReactionSupported(it.key) }.takeIf { it.isNotEmpty() }?.toList() + ?.sortedWith { o1, o2 -> reactionSorting.compare(o1.second, o2.second) }?.map { (type, _) -> val isSelected = ownReactions.any { it.type == type } val reactionIcon = iconFactory.createReactionIcon(type) ReactionOptionItemState( painter = reactionIcon.getPainter(isSelected), type = type, ) - } - ?.let { options -> + }?.let { options -> MessageReactions( modifier = Modifier .clickable( @@ -367,10 +381,11 @@ internal fun ColumnScope.DefaultMessageItemFooterContent( message = message, ) } - message.isDeleted() && - messageItem.deletedMessageVisibility == DeletedMessageVisibility.VISIBLE_FOR_CURRENT_USER -> { + + message.isDeleted() && messageItem.deletedMessageVisibility == DeletedMessageVisibility.VISIBLE_FOR_CURRENT_USER -> { OwnedMessageVisibilityContent(message = message) } + else -> { MessageFooter(messageItem = messageItem) } @@ -409,6 +424,9 @@ internal fun DefaultMessageItemTrailingContent( * @param onGiphyActionClick Handler when the user taps on an action button in a giphy message item. * @param onQuotedMessageClick Handler for quoted message click action. * @param onMediaGalleryPreviewResult Handler when the user selects an option in the Media Gallery Preview screen. + * @param onCastVote Handler when a user cast a vote on an option. + * @param onRemoveVote Handler when a user cast a remove on an option. + * @param onClosePoll Handler when a user close a poll. */ @Composable internal fun DefaultMessageItemCenterContent( @@ -417,9 +435,32 @@ internal fun DefaultMessageItemCenterContent( onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, + onPollUpdated: (Message, Poll) -> Unit, + onCastVote: (Message, Poll, Option) -> Unit, + onRemoveVote: (Message, Poll, Vote) -> Unit, + selectPoll: (Message, Poll, PollSelectionType) -> Unit, + onClosePoll: (String) -> Unit, + ) { val modifier = Modifier.widthIn(max = ChatTheme.dimens.messageItemMaxWidth) - if (messageItem.message.isEmojiOnlyWithoutBubble()) { + if (messageItem.message.isPoll()) { + val poll = messageItem.message.poll + LaunchedEffect(key1 = poll) { + if (poll != null) { + onPollUpdated.invoke(messageItem.message, poll) + } + } + + PollMessageContent( + modifier = modifier, + messageItem = messageItem, + onCastVote = onCastVote, + onRemoveVote = onRemoveVote, + selectPoll = selectPoll, + onClosePoll = onClosePoll, + onLongItemClick = onLongItemClick, + ) + } else if (messageItem.message.isEmojiOnlyWithoutBubble()) { EmojiMessageContent( modifier = modifier, messageItem = messageItem, 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 a0294e61a59..7fe4819f2c7 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 @@ -35,12 +35,17 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.rememberMessageListState import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Option +import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.ReactionSorting import io.getstream.chat.android.models.ReactionSortingByFirstReactionAt import io.getstream.chat.android.models.User +import io.getstream.chat.android.models.Vote import io.getstream.chat.android.ui.common.state.messages.list.GiphyAction import io.getstream.chat.android.ui.common.state.messages.list.MessageListItemState import io.getstream.chat.android.ui.common.state.messages.list.MessageListState +import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType +import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll /** * Default MessageList component, that relies on [MessageListViewModel] to connect all the data @@ -95,6 +100,34 @@ public fun MessageList( onLastVisibleMessageChanged: (Message) -> Unit = { viewModel.updateLastSeenMessage(it) }, onScrollToBottom: () -> Unit = { viewModel.clearNewMessageState() }, onGiphyActionClick: (GiphyAction) -> Unit = { viewModel.performGiphyAction(it) }, + onPollUpdated: (Message, Poll) -> Unit = { message, poll -> + val selectedPoll = viewModel.pollState.selectedPoll + if (viewModel.isShowingPollOptionDetails && + selectedPoll != null && selectedPoll.poll.id == poll.id + ) { + viewModel.updatePollState(poll, message, selectedPoll.pollSelectionType) + } + }, + onCastVote: (Message, Poll, Option) -> Unit = { message, poll, option -> + viewModel.castVote( + message = message, + poll = poll, + option = option, + ) + }, + onRemoveVote: (Message, Poll, Vote) -> Unit = { message, poll, vote -> + viewModel.removeVote( + message = message, + poll = poll, + vote = vote, + ) + }, + selectPoll: (Message, Poll, PollSelectionType) -> Unit = { message, poll, selectionType -> + viewModel.displayPollMoreOptions(selectedPoll = SelectedPoll(poll, message, selectionType)) + }, + onClosePoll: (String) -> Unit = { pollId -> + viewModel.closePoll(pollId = pollId) + }, onQuotedMessageClick: (Message) -> Unit = { message -> viewModel.scrollToMessage( messageId = message.id, @@ -127,6 +160,11 @@ public fun MessageList( messageListItemState = messageListItem, reactionSorting = reactionSorting, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + onCastVote = onCastVote, + onRemoveVote = onRemoveVote, + selectPoll = selectPoll, + onPollUpdated = onPollUpdated, + onClosePoll = onClosePoll, onThreadClick = onThreadClick, onLongItemClick = onLongItemClick, onReactionsClick = onReactionsClick, @@ -171,6 +209,8 @@ public fun MessageList( * @param onReactionsClick Handler when the user taps on message reactions. * @param onGiphyActionClick Handler when the user taps on Giphy message actions. * @param onQuotedMessageClick Handler for quoted message click action. + * @param onCastVote Handler for casting a vote on an option. + * @param onClosePoll Handler for closing a poll. */ @Suppress("LongParameterList") @Composable @@ -182,6 +222,11 @@ internal fun DefaultMessageContainer( onLongItemClick: (Message) -> Unit, onReactionsClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit, + onPollUpdated: (Message, Poll) -> Unit, + onCastVote: (Message, Poll, Option) -> Unit, + onRemoveVote: (Message, Poll, Vote) -> Unit, + selectPoll: (Message, Poll, PollSelectionType) -> Unit, + onClosePoll: (String) -> Unit = { _ -> }, onQuotedMessageClick: (Message) -> Unit, onUserAvatarClick: ((User) -> Unit)? = null, ) { @@ -192,6 +237,11 @@ internal fun DefaultMessageContainer( onReactionsClick = onReactionsClick, onThreadClick = onThreadClick, onGiphyActionClick = onGiphyActionClick, + onPollUpdated = onPollUpdated, + onCastVote = onCastVote, + onRemoveVote = onRemoveVote, + selectPoll = selectPoll, + onClosePoll = onClosePoll, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick, onUserAvatarClick = onUserAvatarClick, @@ -273,6 +323,11 @@ public fun MessageList( onMessagesPageStartReached: () -> Unit = {}, onLastVisibleMessageChanged: (Message) -> Unit = {}, onScrolledToBottom: () -> Unit = {}, + onPollUpdated: (Message, Poll) -> Unit = { _, _ -> }, + onCastVote: (Message, Poll, Option) -> Unit = { _, _, _ -> }, + onRemoveVote: (Message, Poll, Vote) -> Unit = { _, _, _ -> }, + selectPoll: (Message, Poll, PollSelectionType) -> Unit = { _, _, _ -> }, + onClosePoll: (String) -> Unit = { _ -> }, onThreadClick: (Message) -> Unit = {}, onLongItemClick: (Message) -> Unit = {}, onReactionsClick: (Message) -> Unit = {}, @@ -299,6 +354,11 @@ public fun MessageList( DefaultMessageContainer( messageListItemState = it, reactionSorting = reactionSorting, + onPollUpdated = onPollUpdated, + onCastVote = onCastVote, + onRemoveVote = onRemoveVote, + selectPoll = selectPoll, + onClosePoll = onClosePoll, onLongItemClick = onLongItemClick, onThreadClick = onThreadClick, onReactionsClick = onReactionsClick, @@ -330,6 +390,7 @@ public fun MessageList( onMessagesEndReached = onMessagesPageEndReached, onScrollToBottom = onScrollToBottom, ) + else -> emptyContent() } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt index 996b5f25426..e7a7a1d052a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt @@ -21,7 +21,10 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString +import io.getstream.chat.android.client.utils.message.isPoll +import io.getstream.chat.android.client.utils.message.isPollClosed import io.getstream.chat.android.client.utils.message.isSystem +import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory import io.getstream.chat.android.compose.ui.theme.StreamTypography import io.getstream.chat.android.models.Attachment @@ -116,6 +119,22 @@ private class DefaultMessagePreviewFormatter( if (message.isSystem()) { append(displayedText) + } else if (message.isPoll()) { + if (message.isPollClosed()) { + append( + context.getString( + R.string.stream_compose_poll_closed_preview, + message.poll?.name.orEmpty(), + ), + ) + } else { + append( + context.getString( + R.string.stream_compose_poll_created_preview, + message.poll?.name.orEmpty(), + ), + ) + } } else { appendSenderName( message = message, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt index aac1cd155ab..b8247f3ffc1 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt @@ -23,8 +23,11 @@ import io.getstream.chat.android.compose.util.extensions.asState import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ConnectionState import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Option +import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.PollConfig import io.getstream.chat.android.models.User +import io.getstream.chat.android.models.Vote import io.getstream.chat.android.state.plugin.state.channel.thread.ThreadState import io.getstream.chat.android.ui.common.feature.messages.list.DateSeparatorHandler import io.getstream.chat.android.ui.common.feature.messages.list.MessageListController @@ -36,7 +39,11 @@ import io.getstream.chat.android.ui.common.state.messages.list.GiphyAction import io.getstream.chat.android.ui.common.state.messages.list.MessageFooterVisibility import io.getstream.chat.android.ui.common.state.messages.list.MessageListState import io.getstream.chat.android.ui.common.state.messages.list.NewMessageState +import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType +import io.getstream.chat.android.ui.common.state.messages.poll.PollState +import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll import io.getstream.log.taggedLogger +import io.getstream.result.call.Call import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @@ -81,6 +88,11 @@ public class MessageListViewModel( */ public val messageMode: MessageMode by messageListController.mode.asState(viewModelScope) + /** + * Holds the current [PollState] that's used for the messages list. + */ + public val pollState: PollState by messageListController.pollState.asState(viewModelScope) + /** * The information for the current [Channel]. */ @@ -108,6 +120,12 @@ public class MessageListViewModel( public val isShowingOverlay: Boolean get() = currentMessagesState.selectedMessageState != null + /** + * Whether is the poll option details should be shown or not. + */ + public val isShowingPollOptionDetails: Boolean + get() = pollState.selectedPoll != null + /** * Gives us information about the online state of the device. */ @@ -188,6 +206,25 @@ public class MessageListViewModel( messageListController.selectMessage(message) } + /** + * Triggered when the user taps the show more options button on the poll message. + * + * @param selectedPoll The poll that holds the details to be drawn on the more options screen. + */ + public fun displayPollMoreOptions(selectedPoll: SelectedPoll?) { + messageListController.displayPollMoreOptions(selectedPoll) + } + + /** + * Triggered when the poll information has been changed and need to sync on the poll states. + * + * @param poll The poll that holds the details to be drawn on the more options screen. + * @param message The message that contains the poll information. + */ + public fun updatePollState(poll: Poll, message: Message, pollSelectionType: PollSelectionType) { + messageListController.updatePollState(poll, message, pollSelectionType) + } + /** * Triggered when the user taps on and selects message reactions. * @@ -405,6 +442,47 @@ public class MessageListViewModel( messageListController.createPoll(pollConfig = pollConfig) } + /** + * Cast a vote for a poll in a message. + * + * @param message The message where the poll is. + * @param poll The poll that want to be casted a vote. + * @param option The option to vote for. + * + * @return Executable async [Call] responsible for casting a vote. + */ + public fun castVote(message: Message, poll: Poll, option: Option) { + messageListController.castVote( + messageId = message.id, + pollId = poll.id, + option = option, + ) + } + + /** + * Remove a vote for a poll in a message. + * + * @param message The message where the poll is. + * @param poll The poll that want to be casted a vote. + * @param vote The vote that should be removed. + */ + public fun removeVote(message: Message, poll: Poll, vote: Vote) { + messageListController.removeVote( + messageId = message.id, + pollId = poll.id, + vote = vote, + ) + } + + /** + * Close a poll in a message. + * + * @param pollId The poll id. + */ + public fun closePoll(pollId: String) { + messageListController.closePoll(pollId = pollId) + } + /** * Scrolls to message if in list otherwise get the message from backend. Does not work for threads. * diff --git a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_award.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_award.xml new file mode 100644 index 00000000000..e33ac724580 --- /dev/null +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_award.xml @@ -0,0 +1,25 @@ + + + + + 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 c6423b769c6..8ff32cfa1b2 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -203,4 +203,12 @@ Anonymous poll Suggest an option Add a comment + View Results + End Vote + See All %d Options + Poll Options + Poll Results + %d votes + 📊 Poll created: %s + 📊 Poll closed: %s diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/poll/PollUITest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/poll/PollUITest.kt index 28e0b22b146..5352f1e0fb7 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/poll/PollUITest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/poll/PollUITest.kt @@ -16,15 +16,27 @@ package io.getstream.chat.android.compose.ui.poll +import androidx.compose.foundation.background +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.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.previewdata.PreviewPollData import io.getstream.chat.android.compose.ui.BaseComposeTest +import io.getstream.chat.android.compose.ui.components.poll.PollDialogHeader +import io.getstream.chat.android.compose.ui.components.poll.PollMoreOptionsTitle +import io.getstream.chat.android.compose.ui.components.poll.PollViewResultTitle +import io.getstream.chat.android.compose.ui.components.poll.pollMoreOptionsContent +import io.getstream.chat.android.compose.ui.components.poll.pollViewResultContent import io.getstream.chat.android.compose.ui.messages.attachments.factory.AttachmentsPickerPollTabFactory import io.getstream.chat.android.compose.ui.messages.attachments.poll.PollCreationDiscardDialog import io.getstream.chat.android.compose.ui.messages.attachments.poll.PollCreationHeader @@ -35,6 +47,7 @@ import io.getstream.chat.android.compose.ui.messages.attachments.poll.PollOption import io.getstream.chat.android.compose.ui.messages.attachments.poll.PollSwitchInput import io.getstream.chat.android.compose.ui.messages.attachments.poll.PollSwitchItem import io.getstream.chat.android.compose.ui.messages.attachments.poll.PollSwitchList +import io.getstream.chat.android.compose.ui.theme.ChatTheme import org.junit.Rule import org.junit.Test @@ -154,4 +167,120 @@ internal class PollUITest : BaseComposeTest() { } } } + + @Test + fun `snapshot PollMoreOptionsDialog composable`() { + snapshot { + val poll = PreviewPollData.poll1 + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground), + ) { + item { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_options), + onBackPressed = {}, + ) + } + + item { PollMoreOptionsTitle(title = poll.name) } + + item { Spacer(modifier = Modifier.height(16.dp)) } + + pollMoreOptionsContent( + poll = poll, + onCastVote = {}, + onRemoveVote = {}, + ) + + item { Spacer(modifier = Modifier.height(16.dp)) } + } + } + } + + @Test + fun `snapshot PollMoreOptionsDialog composable in dark mode`() { + snapshotWithDarkMode { + val poll = PreviewPollData.poll1 + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground), + ) { + item { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_options), + onBackPressed = {}, + ) + } + + item { PollMoreOptionsTitle(title = poll.name) } + + item { Spacer(modifier = Modifier.height(16.dp)) } + + pollMoreOptionsContent( + poll = poll, + onCastVote = {}, + onRemoveVote = {}, + ) + + item { Spacer(modifier = Modifier.height(16.dp)) } + } + } + } + + @Test + fun `snapshot PollViewResultDialog composable`() { + val poll = PreviewPollData.poll1 + snapshot { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground), + ) { + item { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_results), + onBackPressed = {}, + ) + } + + item { PollViewResultTitle(title = poll.name) } + + item { Spacer(modifier = Modifier.height(16.dp)) } + + pollViewResultContent(poll = poll) + + item { Spacer(modifier = Modifier.height(16.dp)) } + } + } + } + + @Test + fun `snapshot PollViewResultDialog composable in dark mode`() { + val poll = PreviewPollData.poll1 + snapshotWithDarkMode { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground), + ) { + item { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_results), + onBackPressed = {}, + ) + } + + item { PollViewResultTitle(title = poll.name) } + + item { Spacer(modifier = Modifier.height(16.dp)) } + + pollViewResultContent(poll = poll) + + item { Spacer(modifier = Modifier.height(16.dp)) } + } + } + } } diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollMoreOptionsDialog composable in dark mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollMoreOptionsDialog composable in dark mode.png new file mode 100644 index 00000000000..4103cc783e1 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollMoreOptionsDialog composable in dark mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollMoreOptionsDialog composable.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollMoreOptionsDialog composable.png new file mode 100644 index 00000000000..4bdd6dcc248 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollMoreOptionsDialog composable.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollViewResultDialog composable in dark mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollViewResultDialog composable in dark mode.png new file mode 100644 index 00000000000..08f6fb82eb5 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollViewResultDialog composable in dark mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollViewResultDialog composable.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollViewResultDialog composable.png new file mode 100644 index 00000000000..cef30c5ec49 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot PollViewResultDialog composable.png differ diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index 96e7faa98d5..7a69e5d5994 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -133,7 +133,9 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes public final fun banUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V public static synthetic fun banUser$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)V public final fun blockUser (Ljava/lang/String;)V + public final fun castVote (Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Option;)V public final fun clearNewMessageState ()V + public final fun closePoll (Ljava/lang/String;)V public final fun createPoll (Lio/getstream/chat/android/models/PollConfig;Lkotlin/jvm/functions/Function1;)V public static synthetic fun createPoll$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController;Lio/getstream/chat/android/models/PollConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public final fun deleteMessage (Lio/getstream/chat/android/models/Message;Z)V @@ -141,6 +143,7 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes 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 public final fun enterNormalMode ()V public final fun enterThreadMode (Lio/getstream/chat/android/models/Message;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun enterThreadMode$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController;Lio/getstream/chat/android/models/Message;ILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; @@ -159,6 +162,7 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes public final fun getMessageListState ()Lkotlinx/coroutines/flow/StateFlow; public final fun getMode ()Lkotlinx/coroutines/flow/StateFlow; public final fun getOwnCapabilities ()Lkotlinx/coroutines/flow/StateFlow; + public final fun getPollState ()Lkotlinx/coroutines/flow/StateFlow; public final fun getShowSystemMessagesState ()Lkotlinx/coroutines/flow/StateFlow; public final fun getThreadListState ()Lkotlinx/coroutines/flow/StateFlow; public final fun getThreadLoadOrderOlderToNewer ()Z @@ -190,6 +194,7 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes public final fun removeAttachment (Ljava/lang/String;Lio/getstream/chat/android/models/Attachment;)V public final fun removeOverlay ()V public final fun removeShadowBanFromUser (Ljava/lang/String;)V + public final fun removeVote (Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Vote;)V public final fun resendMessage (Lio/getstream/chat/android/models/Message;)V public final fun scrollToBottom (ILkotlin/jvm/functions/Function0;)V public static synthetic fun scrollToBottom$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController;ILkotlin/jvm/functions/Function0;ILjava/lang/Object;)V @@ -213,6 +218,7 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes public final fun unpinMessage (Lio/getstream/chat/android/models/Message;)V public final fun updateLastSeenMessage (Lio/getstream/chat/android/models/Message;)V public final fun updateMessagePin (Lio/getstream/chat/android/models/Message;)V + public final fun updatePollState (Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType;)V public final fun updateUserMute (Lio/getstream/chat/android/models/User;)V } @@ -285,6 +291,30 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollCastingVoteError : io/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent { + public static final field $stable I + public fun (Lio/getstream/result/Error;)V + public final fun component1 ()Lio/getstream/result/Error; + public final fun copy (Lio/getstream/result/Error;)Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollCastingVoteError; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollCastingVoteError;Lio/getstream/result/Error;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollCastingVoteError; + public fun equals (Ljava/lang/Object;)Z + public fun getStreamError ()Lio/getstream/result/Error; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollClosingError : io/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent { + public static final field $stable I + public fun (Lio/getstream/result/Error;)V + public final fun component1 ()Lio/getstream/result/Error; + public final fun copy (Lio/getstream/result/Error;)Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollClosingError; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollClosingError;Lio/getstream/result/Error;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollClosingError; + public fun equals (Ljava/lang/Object;)Z + public fun getStreamError ()Lio/getstream/result/Error; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollCreationError : io/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent { public static final field $stable I public fun (Lio/getstream/result/Error;)V @@ -297,6 +327,18 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollRemovingVoteError : io/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent { + public static final field $stable I + public fun (Lio/getstream/result/Error;)V + public final fun component1 ()Lio/getstream/result/Error; + public final fun copy (Lio/getstream/result/Error;)Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollRemovingVoteError; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollRemovingVoteError;Lio/getstream/result/Error;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$PollRemovingVoteError; + public fun equals (Ljava/lang/Object;)Z + public fun getStreamError ()Lio/getstream/result/Error; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent$UnmuteUserError : io/getstream/chat/android/ui/common/feature/messages/list/MessageListController$ErrorEvent { public static final field $stable I public fun (Lio/getstream/result/Error;)V @@ -1434,6 +1476,56 @@ public final class io/getstream/chat/android/ui/common/state/messages/list/Unrea public fun toString ()Ljava/lang/String; } +public abstract class io/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType { + public static final field $stable I +} + +public final class io/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType$MoreOption : io/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType { + public static final field $stable I + public static final field INSTANCE Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType$MoreOption; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType$ViewResult : io/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType { + public static final field $stable I + public static final field INSTANCE Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType$ViewResult; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/chat/android/ui/common/state/messages/poll/PollState { + public static final field $stable I + public fun ()V + public fun (Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;)V + public synthetic fun (Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll; + public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;)Lio/getstream/chat/android/ui/common/state/messages/poll/PollState; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/poll/PollState;Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/poll/PollState; + public fun equals (Ljava/lang/Object;)Z + public final fun getSelectedPoll ()Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll { + public static final field $stable I + public fun (Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType;)V + public final fun component1 ()Lio/getstream/chat/android/models/Poll; + public final fun component2 ()Lio/getstream/chat/android/models/Message; + public final fun component3 ()Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType; + public final fun copy (Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType;)Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll; + public fun equals (Ljava/lang/Object;)Z + public final fun getMessage ()Lio/getstream/chat/android/models/Message; + public final fun getPoll ()Lio/getstream/chat/android/models/Poll; + public final fun getPollSelectionType ()Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract interface class io/getstream/chat/android/ui/common/utils/ChannelNameFormatter { public static final field Companion Lio/getstream/chat/android/ui/common/utils/ChannelNameFormatter$Companion; public abstract fun formatChannelName (Lio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/User;)Ljava/lang/String; diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt index 088f570a8fa..548e0544e9f 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt @@ -45,9 +45,12 @@ import io.getstream.chat.android.models.Flag import io.getstream.chat.android.models.Member import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.MessagesState +import io.getstream.chat.android.models.Option +import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.PollConfig import io.getstream.chat.android.models.Reaction import io.getstream.chat.android.models.User +import io.getstream.chat.android.models.Vote import io.getstream.chat.android.state.extensions.awaitRepliesAsState import io.getstream.chat.android.state.extensions.cancelEphemeralMessage import io.getstream.chat.android.state.extensions.getMessageUsingCache @@ -102,6 +105,9 @@ import io.getstream.chat.android.ui.common.state.messages.list.TypingItemState import io.getstream.chat.android.ui.common.state.messages.list.UnreadSeparatorItemState import io.getstream.chat.android.ui.common.state.messages.list.lastItemOrNull import io.getstream.chat.android.ui.common.state.messages.list.stringify +import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType +import io.getstream.chat.android.ui.common.state.messages.poll.PollState +import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll import io.getstream.chat.android.ui.common.utils.extensions.onFirst import io.getstream.chat.android.ui.common.utils.extensions.shouldShowMessageFooter import io.getstream.log.TaggedLogger @@ -288,6 +294,13 @@ public class MessageListController( MutableStateFlow(MessageListState(isLoading = true)) public val messageListState: StateFlow = _messageListState + /** + * Current state of the poll. + */ + private val _pollState: MutableStateFlow = + MutableStateFlow(PollState()) + public val pollState: StateFlow = _pollState + /** * Current state of the thread message list. */ @@ -1420,6 +1433,31 @@ public class MessageListController( } } + /** + * Triggered when the user taps the show more options button on the poll message. + * + * @param selectedPoll The poll that holds the details to be drawn on the more options screen. + */ + public fun displayPollMoreOptions(selectedPoll: SelectedPoll?) { + _pollState.value = _pollState.value.copy(selectedPoll = selectedPoll) + } + + /** + * Triggered when the poll information has been changed and need to sync on the poll states. + * + * @param poll The poll that holds the details to be drawn on the more options screen. + * @param message The message that contains the poll information. + */ + public fun updatePollState(poll: Poll, message: Message, pollSelectionType: PollSelectionType) { + _pollState.value = _pollState.value.copy( + selectedPoll = SelectedPoll( + poll = poll, + message = message, + pollSelectionType = pollSelectionType, + ), + ) + } + /** * Triggered when the user selects a new message action, in the message overlay. * @@ -1746,6 +1784,66 @@ public class MessageListController( } } + /** + * Cast a vote for a poll in a message. + * + * @param messageId The message id where the poll is. + * @param pollId The poll id. + * @param option The option to vote for. + */ + public fun castVote( + messageId: String, + pollId: String, + option: Option, + ) { + chatClient.castPollVote( + messageId = messageId, + pollId = pollId, + option = option, + ).enqueue(onError = { error -> + onActionResult(error) { + ErrorEvent.PollCastingVoteError(it) + } + }) + } + + /** + * Remove a vote for a poll in a message. + * + * @param messageId The message id where the poll is. + * @param pollId The poll id. + * @param vote The vote that should be removed. + */ + public fun removeVote( + messageId: String, + pollId: String, + vote: Vote, + ) { + chatClient.removePollVote( + messageId = messageId, + pollId = pollId, + vote = vote, + ).enqueue(onError = { error -> + onActionResult(error) { + ErrorEvent.PollCastingVoteError(it) + } + }) + } + + /** + * Close a poll in a message. + * + * @param pollId The poll id. + */ + public fun closePoll(pollId: String) { + chatClient.closePoll(pollId = pollId) + .enqueue(onError = { error -> + onActionResult(error) { + ErrorEvent.PollCastingVoteError(it) + } + }) + } + /** * Triggered when the user selects a reaction for the currently selected message. If the message already has that * reaction, from the current user, we remove it. Otherwise we add a new reaction. @@ -2138,6 +2236,27 @@ public class MessageListController( */ public data class PollCreationError(override val streamError: Error) : ErrorEvent(streamError) + /** + * When an error occurs while casting a vote. + * + * @param streamError Contains the original [Throwable] along with a message. + */ + public data class PollCastingVoteError(override val streamError: Error) : ErrorEvent(streamError) + + /** + * When an error occurs while removing a vote. + * + * @param streamError Contains the original [Throwable] along with a message. + */ + public data class PollRemovingVoteError(override val streamError: Error) : ErrorEvent(streamError) + + /** + * When an error occurs while closing a vote. + * + * @param streamError Contains the original [Throwable] along with a message. + */ + public data class PollClosingError(override val streamError: Error) : ErrorEvent(streamError) + /** * When an error occurs while blocking a user. * diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/poll/PollState.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/poll/PollState.kt new file mode 100644 index 00000000000..3e9526c0706 --- /dev/null +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/poll/PollState.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2014-2024 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.ui.common.state.messages.poll + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Poll + +/** + * Holds the state of the poll. + * + * @property selectedPoll The more options that should be displayed. + */ +@Immutable +public data class PollState( + public val selectedPoll: SelectedPoll? = null, +) + +@Immutable +public data class SelectedPoll( + val poll: Poll, + val message: Message, + val pollSelectionType: PollSelectionType, +) + +@Stable +public sealed class PollSelectionType { + public data object MoreOption : PollSelectionType() + + public data object ViewResult : PollSelectionType() +} + +internal fun PollState.stringify(): String { + return "PollState(" + + "selectedPoll: $selectedPoll)" +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt index e900bfa85ed..0df4a93a0ab 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt @@ -307,6 +307,9 @@ public class MessageListView : ConstraintLayout { is MessageListController.ErrorEvent.MarkUnreadError -> R.string.stream_ui_message_list_error_mark_as_unread_message is MessageListController.ErrorEvent.PollCreationError -> R.string.stream_ui_message_list_error_create_poll + is MessageListController.ErrorEvent.PollCastingVoteError -> R.string.stream_ui_message_list_error_cast_vote + is MessageListController.ErrorEvent.PollRemovingVoteError -> R.string.stream_ui_message_list_error_cast_vote + is MessageListController.ErrorEvent.PollClosingError -> R.string.stream_ui_message_list_error_close_poll }.let(::showToast) } diff --git a/stream-chat-android-ui-components/src/main/res/values/strings_message_list.xml b/stream-chat-android-ui-components/src/main/res/values/strings_message_list.xml index 61cfcc8d015..6c14e26d051 100644 --- a/stream-chat-android-ui-components/src/main/res/values/strings_message_list.xml +++ b/stream-chat-android-ui-components/src/main/res/values/strings_message_list.xml @@ -92,4 +92,7 @@ Failed to unpin message Failed to delete message Failed to create a poll + Failed to close a poll + Failed to cast a vote + Failed to remove a vote