From dcb3e999d190a800ca904735b38206fcb1fc597d Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Fri, 29 May 2026 11:58:37 +0200 Subject: [PATCH] Add ui-components composer autocomplete for enhanced mentions --- .../kotlin/ui/messages/MessageComposer.kt | 11 +- .../res/drawable/stream_design_ic_users.xml | 28 +++ .../api/stream-chat-android-ui-components.api | 21 +- .../messages/composer/MessageComposerView.kt | 33 ++- .../DefaultMessageComposerLeadingContent.kt | 2 +- ...essageComposerMentionSuggestionsContent.kt | 217 ++++++++++++++---- .../messages/MessageComposerViewModel.kt | 6 + .../MessageComposerViewModelBinder.kt | 12 + .../MessageComposerViewModelBinding.kt | 8 + .../stream_ui_shape_mention_icon_avatar.xml | 25 ++ .../layout/stream_ui_item_mention_special.xml | 79 +++++++ .../src/main/res/values-es/strings.xml | 3 + .../src/main/res/values-fr/strings.xml | 3 + .../src/main/res/values-hi/strings.xml | 3 + .../src/main/res/values-in/strings.xml | 3 + .../src/main/res/values-it/strings.xml | 3 + .../src/main/res/values-ja/strings.xml | 3 + .../src/main/res/values-ko/strings.xml | 3 + .../src/main/res/values/strings.xml | 3 + .../messages/MessageComposerViewModelTest.kt | 73 ++++++ 20 files changed, 478 insertions(+), 61 deletions(-) create mode 100644 stream-chat-android-ui-common/src/main/res/drawable/stream_design_ic_users.xml create mode 100644 stream-chat-android-ui-components/src/main/res/drawable/stream_ui_shape_mention_icon_avatar.xml create mode 100644 stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_mention_special.xml diff --git a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/ui/messages/MessageComposer.kt b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/ui/messages/MessageComposer.kt index ad05bc740a5..3a0f3e18e76 100644 --- a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/ui/messages/MessageComposer.kt +++ b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/ui/messages/MessageComposer.kt @@ -132,7 +132,7 @@ private object MessageComposer : Fragment() { messageComposerView.attachmentRemovalListener = { attachment -> // Handle attachment removal } - messageComposerView.mentionSelectionListener = { user -> + messageComposerView.suggestedMentionSelectionListener = { mention -> // Handle mention selection } messageComposerView.commandSelectionListener = { command -> @@ -198,8 +198,8 @@ private object MessageComposer : Fragment() { messageComposerView.attachmentRemovalListener = { attachment -> messageComposerViewModel.removeAttachment(attachment) } - messageComposerView.mentionSelectionListener = { user -> - messageComposerViewModel.selectMention(user) + messageComposerView.suggestedMentionSelectionListener = { mention -> + messageComposerViewModel.selectMention(mention) } messageComposerView.commandSelectionListener = { command -> messageComposerViewModel.selectCommand(command) @@ -279,7 +279,8 @@ private object MessageComposer : Fragment() { ) messageComposerView.setMentionSuggestionsContent( DefaultMessageComposerMentionSuggestionsContent(context).also { - it.mentionSelectionListener = { user -> messageComposerView.mentionSelectionListener(user) } + it.suggestedMentionSelectionListener = + { mention -> messageComposerView.suggestedMentionSelectionListener(mention) } } ) } @@ -323,7 +324,7 @@ private object MessageComposer : Fragment() { ) messageComposerView.setMentionSuggestionsContent( DefaultMessageComposerMentionSuggestionsContent(context).also { - it.mentionSelectionListener = { user -> messageComposerViewModel.selectMention(user) } + it.suggestedMentionSelectionListener = { mention -> messageComposerViewModel.selectMention(mention) } } ) } diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_design_ic_users.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_design_ic_users.xml new file mode 100644 index 00000000000..549ddcb7052 --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_design_ic_users.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api index 6c1acdef86e..dfb7e0045c7 100644 --- a/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api +++ b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api @@ -1034,6 +1034,7 @@ public final class io/getstream/chat/android/ui/feature/messages/composer/Messag public final fun getMentionSelectionListener ()Lkotlin/jvm/functions/Function1; public final fun getPollSubmissionListener ()Lkotlin/jvm/functions/Function1; public final fun getSendMessageButtonClickListener ()Lkotlin/jvm/functions/Function0; + public final fun getSuggestedMentionSelectionListener ()Lkotlin/jvm/functions/Function1; public final fun getTextInputChangeListener ()Lkotlin/jvm/functions/Function1; public final fun renderState (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;)V public final fun setAlsoSendToChannelSelectionListener (Lkotlin/jvm/functions/Function1;)V @@ -1076,6 +1077,7 @@ public final class io/getstream/chat/android/ui/feature/messages/composer/Messag public final fun setMentionSuggestionsContent (Landroid/view/View;)V public final fun setPollSubmissionListener (Lkotlin/jvm/functions/Function1;)V public final fun setSendMessageButtonClickListener (Lkotlin/jvm/functions/Function0;)V + public final fun setSuggestedMentionSelectionListener (Lkotlin/jvm/functions/Function1;)V public final fun setTextInputChangeListener (Lkotlin/jvm/functions/Function1;)V public final fun setTrailingContent (Landroid/view/View;)V public final fun setTrailingContent (Landroid/view/View;Landroid/widget/FrameLayout$LayoutParams;)V @@ -1789,11 +1791,13 @@ public class io/getstream/chat/android/ui/feature/messages/composer/content/Defa protected final fun getBinding ()Lio/getstream/chat/android/ui/databinding/StreamUiSuggestionListViewBinding; public fun getMentionSelectionListener ()Lkotlin/jvm/functions/Function1; protected final fun getStyle ()Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerViewStyle; + public fun getSuggestedMentionSelectionListener ()Lkotlin/jvm/functions/Function1; public fun renderState (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;)V protected final fun setAdapter (Lio/getstream/chat/android/ui/feature/messages/composer/content/MentionSuggestionsAdapter;)V protected final fun setBinding (Lio/getstream/chat/android/ui/databinding/StreamUiSuggestionListViewBinding;)V public fun setMentionSelectionListener (Lkotlin/jvm/functions/Function1;)V protected final fun setStyle (Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerViewStyle;)V + public fun setSuggestedMentionSelectionListener (Lkotlin/jvm/functions/Function1;)V } public class io/getstream/chat/android/ui/feature/messages/composer/content/DefaultMessageComposerOverlappingContent : androidx/constraintlayout/widget/ConstraintLayout, io/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerOverlappingContent { @@ -1863,6 +1867,11 @@ public class io/getstream/chat/android/ui/feature/messages/composer/content/Defa public abstract interface class io/getstream/chat/android/ui/feature/messages/composer/content/MentionSuggestionsAdapter { public abstract fun getItemCount ()I public abstract fun setItems (Ljava/util/List;)V + public fun setMentions (Ljava/util/List;)V +} + +public final class io/getstream/chat/android/ui/feature/messages/composer/content/MentionSuggestionsAdapter$DefaultImpls { + public static fun setMentions (Lio/getstream/chat/android/ui/feature/messages/composer/content/MentionSuggestionsAdapter;Ljava/util/List;)V } public abstract interface class io/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerCenterContent : io/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerContent { @@ -1951,11 +1960,15 @@ public final class io/getstream/chat/android/ui/feature/messages/composer/conten public abstract interface class io/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerMentionSuggestionsContent : io/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerContent { public abstract fun getMentionSelectionListener ()Lkotlin/jvm/functions/Function1; + public fun getSuggestedMentionSelectionListener ()Lkotlin/jvm/functions/Function1; public abstract fun setMentionSelectionListener (Lkotlin/jvm/functions/Function1;)V + public fun setSuggestedMentionSelectionListener (Lkotlin/jvm/functions/Function1;)V } public final class io/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerMentionSuggestionsContent$DefaultImpls { public static fun findViewByKey (Lio/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerMentionSuggestionsContent;Ljava/lang/String;)Landroid/view/View; + public static fun getSuggestedMentionSelectionListener (Lio/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerMentionSuggestionsContent;)Lkotlin/jvm/functions/Function1; + public static fun setSuggestedMentionSelectionListener (Lio/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerMentionSuggestionsContent;Lkotlin/jvm/functions/Function1;)V } public abstract interface class io/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerOverlappingContent : io/getstream/chat/android/ui/feature/messages/composer/content/MessageComposerContent { @@ -4597,6 +4610,7 @@ public final class io/getstream/chat/android/ui/viewmodel/messages/MessageCompos public final fun seekRecordingTo (F)V public final fun selectCommand (Lio/getstream/chat/android/models/Command;)V public final fun selectMention (Lio/getstream/chat/android/models/User;)V + public final fun selectMention (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;)V public final fun sendMessage ()V public final fun sendMessage (Lio/getstream/chat/android/models/Message;)V public final fun sendMessage (Lio/getstream/chat/android/models/Message;Lio/getstream/result/call/Call$Callback;)V @@ -4635,6 +4649,7 @@ public final class io/getstream/chat/android/ui/viewmodel/messages/MessageCompos public final fun onDismissSuggestions (Lkotlin/jvm/functions/Function0;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinder; public final fun onMentionSelection (Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinder; public final fun onSendMessageButtonClick (Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinder; + public final fun onSuggestedMentionSelection (Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinder; public final fun onTextInputChange (Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinder; public static final fun with (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinder; } @@ -4667,7 +4682,8 @@ public final class io/getstream/chat/android/ui/viewmodel/messages/MessageCompos public static final fun bind (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V public static final fun bind (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V public static final fun bind (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun bind$default (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun bind (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun bind$default (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static final fun bindDefaults (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;)V public static final fun bindDefaults (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;)V public static final fun bindDefaults (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V @@ -4691,7 +4707,8 @@ public final class io/getstream/chat/android/ui/viewmodel/messages/MessageCompos public static final fun bindDefaults (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V public static final fun bindDefaults (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V public static final fun bindDefaults (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun bindDefaults$default (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun bindDefaults (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun bindDefaults$default (Lio/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/ui/feature/messages/composer/MessageComposerView;Landroidx/lifecycle/LifecycleOwner;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/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V } public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel : androidx/lifecycle/ViewModel { diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/MessageComposerView.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/MessageComposerView.kt index 1246b869558..6596e3cac75 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/MessageComposerView.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/MessageComposerView.kt @@ -32,6 +32,7 @@ import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.models.Command import io.getstream.chat.android.models.CreatePollParams import io.getstream.chat.android.models.User +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import io.getstream.chat.android.ui.common.state.messages.MessageMode import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState import io.getstream.chat.android.ui.databinding.StreamUiMessageComposerBinding @@ -116,10 +117,26 @@ public class MessageComposerView : ConstraintLayout { public var attachmentRemovalListener: (Attachment) -> Unit = {} /** - * Selection listener invoked when a mention suggestion item is selected. - */ + * Selection listener invoked when a user mention suggestion item is selected. + * + * Kept for backward compatibility; only fires for [Mention.User] rows. New code should + * prefer [suggestedMentionSelectionListener], which fires for every mention type. Note both + * listeners fire on a user tap: a custom [mentionSelectionListener] runs in addition to + * [suggestedMentionSelectionListener], so the default selection still inserts the mention. To + * replace the default selection behavior, override [suggestedMentionSelectionListener]. + */ + @Deprecated( + message = "Use suggestedMentionSelectionListener; it also fires for other mention types.", + replaceWith = ReplaceWith("suggestedMentionSelectionListener"), + level = DeprecationLevel.WARNING, + ) public var mentionSelectionListener: (User) -> Unit = {} + /** + * Selection listener invoked when any mention suggestion item is selected. + */ + public var suggestedMentionSelectionListener: (Mention) -> Unit = {} + /** * Selection listener invoked when a command suggestion item is selected. */ @@ -256,7 +273,7 @@ public class MessageComposerView : ConstraintLayout { /** * The current list of mention suggestions. */ - private var mentionSuggestions: List? = null + private var mentionSuggestions: List? = null /** * Default implementation of [mentionSuggestionsContent]. @@ -264,6 +281,7 @@ public class MessageComposerView : ConstraintLayout { private val defaultMentionSuggestionsView: View by lazy { DefaultMessageComposerMentionSuggestionsContent(context).also { it.mentionSelectionListener = { user -> mentionSelectionListener(user) } + it.suggestedMentionSelectionListener = { mention -> suggestedMentionSelectionListener(mention) } }.attachContext() } @@ -606,6 +624,9 @@ public class MessageComposerView : ConstraintLayout { if (contentView.mentionSelectionListener == null) { contentView.mentionSelectionListener = { mentionSelectionListener(it) } } + if (contentView.suggestedMentionSelectionListener == null) { + contentView.suggestedMentionSelectionListener = { suggestedMentionSelectionListener(it) } + } } } @@ -643,12 +664,12 @@ public class MessageComposerView : ConstraintLayout { */ private fun renderSuggestion(state: MessageComposerState) { when { - state.mentionSuggestions.isNotEmpty() -> renderMentionSuggestions(state) + state.suggestedMentions.isNotEmpty() -> renderMentionSuggestions(state) state.commandSuggestions.isNotEmpty() -> renderCommandsSuggestions(state) else -> suggestionsPopup?.dismiss() } this.commandSuggestions = state.commandSuggestions - this.mentionSuggestions = state.mentionSuggestions + this.mentionSuggestions = state.suggestedMentions } /** @@ -683,7 +704,7 @@ public class MessageComposerView : ConstraintLayout { */ private fun renderMentionSuggestions(state: MessageComposerState) { // Do not do anything if the list hasn't changed - if (this.mentionSuggestions == state.mentionSuggestions) return + if (this.mentionSuggestions == state.suggestedMentions) return if (!messageComposerContext.style.messageInputMentionsHandlingEnabled) return (mentionSuggestionsContent as? MessageComposerContent)?.renderState(state) diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/content/DefaultMessageComposerLeadingContent.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/content/DefaultMessageComposerLeadingContent.kt index 1d4c503fcfb..5188cdd4cc6 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/content/DefaultMessageComposerLeadingContent.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/content/DefaultMessageComposerLeadingContent.kt @@ -130,7 +130,7 @@ public open class DefaultMessageComposerLeadingContent : FrameLayout, MessageCom val hasAttachments = state.attachments.isNotEmpty() val hasCommandInput = state.inputValue.startsWith("/") val hasCommandSuggestions = state.commandSuggestions.isNotEmpty() - val hasMentionSuggestions = state.mentionSuggestions.isNotEmpty() + val hasMentionSuggestions = state.suggestedMentions.isNotEmpty() val isInEditMode = state.action is Edit val hasCommands = state.hasCommands val noRecording = state.recording is RecordingState.Idle diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/content/DefaultMessageComposerMentionSuggestionsContent.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/content/DefaultMessageComposerMentionSuggestionsContent.kt index 19dfe448de3..d6056cd3cbb 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/content/DefaultMessageComposerMentionSuggestionsContent.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/content/DefaultMessageComposerMentionSuggestionsContent.kt @@ -16,6 +16,7 @@ package io.getstream.chat.android.ui.feature.messages.composer.content +import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet import android.view.ViewGroup @@ -24,8 +25,11 @@ import androidx.core.view.doOnLayout import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import io.getstream.chat.android.models.User +import io.getstream.chat.android.ui.R +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState import io.getstream.chat.android.ui.databinding.StreamUiItemMentionBinding +import io.getstream.chat.android.ui.databinding.StreamUiItemMentionSpecialBinding import io.getstream.chat.android.ui.databinding.StreamUiSuggestionListViewBinding import io.getstream.chat.android.ui.feature.messages.composer.MessageComposerContext import io.getstream.chat.android.ui.feature.messages.composer.MessageComposerView @@ -34,16 +38,36 @@ import io.getstream.chat.android.ui.font.setTextStyle import io.getstream.chat.android.ui.utils.extensions.applyTint import io.getstream.chat.android.ui.utils.extensions.createStreamThemeWrapper import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater -import io.getstream.chat.android.ui.widgets.internal.SimpleListAdapter +import io.getstream.chat.android.ui.common.R as UiCommonR /** * Represents the mention suggestion list popup shown above [MessageComposerView]. */ public interface MessageComposerMentionSuggestionsContent : MessageComposerContent { /** - * Selection listener invoked when a mention is selected. + * Selection listener invoked when a user mention is selected. + * + * Kept for backward compatibility; only fires for [Mention.User] rows. New code should + * prefer [suggestedMentionSelectionListener], which fires for every mention type. Note both + * listeners fire on a user tap: a custom [mentionSelectionListener] runs in addition to + * [suggestedMentionSelectionListener], so the default selection still inserts the mention. To + * replace the default selection behavior, override [suggestedMentionSelectionListener]. */ + @Deprecated( + message = "Use suggestedMentionSelectionListener; it also fires for other mention types.", + replaceWith = ReplaceWith("suggestedMentionSelectionListener"), + level = DeprecationLevel.WARNING, + ) public var mentionSelectionListener: ((User) -> Unit)? + + /** + * Selection listener invoked when any mention is selected. + * + * No-op default getter/setter for backward compatibility. + */ + public var suggestedMentionSelectionListener: ((Mention) -> Unit)? + get() = null + set(_) = Unit } /** @@ -67,11 +91,15 @@ public open class DefaultMessageComposerMentionSuggestionsContent : */ protected lateinit var adapter: MentionSuggestionsAdapter - /** - * Selection listener invoked when a mention is selected. - */ + @Deprecated( + message = "Use suggestedMentionSelectionListener; it also fires for other mention types.", + replaceWith = ReplaceWith("suggestedMentionSelectionListener"), + level = DeprecationLevel.WARNING, + ) public override var mentionSelectionListener: ((User) -> Unit)? = null + public override var suggestedMentionSelectionListener: ((Mention) -> Unit)? = null + public constructor(context: Context) : this(context, null) public constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) @@ -96,7 +124,18 @@ public open class DefaultMessageComposerMentionSuggestionsContent : protected open fun buildAdapter( style: MessageComposerViewStyle, ): T where T : RecyclerView.Adapter, T : MentionSuggestionsAdapter, VH : RecyclerView.ViewHolder { - return MentionsAdapter(style) { mentionSelectionListener?.invoke(it) } as T + return MentionsAdapter(style = style, onMentionSelected = ::onMentionSelected) as T + } + + /** + * Fires the (Mention)-typed listener for every row, and the deprecated user-only listener + * when the picked mention is a [Mention.User], so existing callers keep working. + */ + private fun onMentionSelected(mention: Mention) { + suggestedMentionSelectionListener?.invoke(mention) + if (mention is Mention.User) { + mentionSelectionListener?.invoke(mention.user) + } } /** @@ -118,7 +157,7 @@ public open class DefaultMessageComposerMentionSuggestionsContent : * @param state The state that will be used to render the updated UI. */ override fun renderState(state: MessageComposerState) { - adapter.setItems(state.mentionSuggestions) + adapter.setMentions(state.suggestedMentions) } } @@ -128,10 +167,23 @@ public open class DefaultMessageComposerMentionSuggestionsContent : public interface MentionSuggestionsAdapter { /** - * Sets the list of mention suggestions to be displayed. + * Sets the list of user mention suggestions to be displayed. + * + * Kept for backward compatibility; only sees [Mention.User] entries. Override [setMentions] + * to also handle other mention types. */ public fun setItems(items: List) + /** + * Sets the heterogeneous mention suggestion list. + * + * The default implementation filters [Mention.User] entries and delegates to [setItems], so + * adapters that only override [setItems] keep working — they simply ignore non-user mentions. + */ + public fun setMentions(items: List) { + setItems(items.mapNotNull { (it as? Mention.User)?.user }) + } + /** * Returns the number of items in the adapter. */ @@ -139,52 +191,75 @@ public interface MentionSuggestionsAdapter { } /** - * [RecyclerView.Adapter] responsible for displaying mention suggestions in a RecyclerView. + * [RecyclerView.Adapter] responsible for displaying heterogeneous mention suggestions. * - * @param style The style for [MessageComposerView]. - * @param mentionSelectionListener The listener invoked when a mention is selected from the list. + * Renders [Mention.User] rows with the existing user-mention layout and every other [Mention] + * variant with a simpler icon-plus-name row. */ private class MentionsAdapter( private val style: MessageComposerViewStyle, - private val mentionSelectionListener: (User) -> Unit, -) : SimpleListAdapter(), MentionSuggestionsAdapter { + private val onMentionSelected: (Mention) -> Unit, +) : RecyclerView.Adapter(), MentionSuggestionsAdapter { - /** - * Creates and instantiates a new instance of [MentionsViewHolder]. - * - * @param parent The ViewGroup into which the new View will be added. - * @param viewType The view type of the new View. - * @return A new [MentionsViewHolder] instance. - */ - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MentionsViewHolder { - return StreamUiItemMentionBinding - .inflate(parent.streamThemeInflater, parent, false) - .let { MentionsViewHolder(it, style, mentionSelectionListener) } + private val items: MutableList = mutableListOf() + + override fun setItems(items: List): Unit = setMentions(items.map(Mention::User)) + + @SuppressLint("NotifyDataSetChanged") + override fun setMentions(items: List) { + this.items.clear() + this.items.addAll(items) + notifyDataSetChanged() + } + + override fun getItemCount(): Int = items.size + + override fun getItemViewType(position: Int): Int = when (items[position]) { + is Mention.User -> VIEW_TYPE_USER + else -> VIEW_TYPE_SPECIAL + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = when (viewType) { + VIEW_TYPE_USER -> + StreamUiItemMentionBinding + .inflate(parent.streamThemeInflater, parent, false) + .let { UserMentionsViewHolder(it, style, onMentionSelected) } + + else -> + StreamUiItemMentionSpecialBinding + .inflate(parent.streamThemeInflater, parent, false) + .let { SpecialMentionsViewHolder(it, style, onMentionSelected) } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = items[position] + when (holder) { + is UserMentionsViewHolder -> holder.bind(item as Mention.User) + is SpecialMentionsViewHolder -> holder.bind(item) + } + } + + private companion object { + const val VIEW_TYPE_USER = 0 + const val VIEW_TYPE_SPECIAL = 1 } } /** - * [RecyclerView.ViewHolder] used for rendering mention items. - * - * @param binding Generated binding class for the XML layout. - * @param style The style for [MessageComposerView]. - * @param mentionSelectionListener The listener invoked when a mention is selected. + * [RecyclerView.ViewHolder] used for rendering user mention items. */ -private class MentionsViewHolder( +private class UserMentionsViewHolder( val binding: StreamUiItemMentionBinding, style: MessageComposerViewStyle, - val mentionSelectionListener: (User) -> Unit, -) : SimpleListAdapter.ViewHolder(binding.root) { + val onMentionSelected: (Mention) -> Unit, +) : RecyclerView.ViewHolder(binding.root) { - private lateinit var item: User + private lateinit var item: Mention.User - /** - * The template string for the mention item with user name placeholder. - */ private val mentionTemplateText = style.mentionSuggestionItemMentionText init { - binding.root.setOnClickListener { mentionSelectionListener(item) } + binding.root.setOnClickListener { onMentionSelected(item) } binding.usernameTextView.setTextStyle(style.mentionSuggestionItemUsernameTextStyle) binding.mentionNameTextView.setTextStyle(style.mentionSuggestionItemMentionTextStyle) binding.mentionsIcon.setImageDrawable( @@ -194,21 +269,69 @@ private class MentionsViewHolder( ) } - /** - * Updates [itemView] elements for a given [User] object. - * - * @param item Single mention suggestion represented by [User] class. - */ - override fun bind(item: User) { + fun bind(item: Mention.User) { this.item = item + val user = item.user // Workaround for race condition caused by Coil trying to load stale avatar on layout. binding.userAvatarView.doOnLayout { - binding.userAvatarView.setUser(item) + binding.userAvatarView.setUser(user) } - val username = String.format(mentionTemplateText, item.id.lowercase()) - binding.usernameTextView.text = item.name.ifEmpty { username } - binding.mentionNameTextView.isVisible = item.name.isNotEmpty() + val username = String.format(mentionTemplateText, user.id.lowercase()) + binding.usernameTextView.text = user.name.ifEmpty { username } + binding.mentionNameTextView.isVisible = user.name.isNotEmpty() binding.mentionNameTextView.text = username } } + +/** + * [RecyclerView.ViewHolder] used for rendering non-user mention items. + * + * Until the design system covers custom mentions, an unknown [Mention] type falls back to a neutral + * icon and the [Mention.display] string as the row label. + */ +private class SpecialMentionsViewHolder( + val binding: StreamUiItemMentionSpecialBinding, + style: MessageComposerViewStyle, + val onMentionSelected: (Mention) -> Unit, +) : RecyclerView.ViewHolder(binding.root) { + + private lateinit var item: Mention + + init { + binding.root.setOnClickListener { onMentionSelected(item) } + binding.nameTextView.setTextStyle(style.mentionSuggestionItemUsernameTextStyle) + binding.subtitleTextView.setTextStyle(style.mentionSuggestionItemMentionTextStyle) + } + + fun bind(item: Mention) { + this.item = item + binding.iconImageView.setImageResource(item.iconRes()) + binding.nameTextView.text = "@${item.display}" + val subtitle = item.subtitle(binding.root.context) + binding.subtitleTextView.text = subtitle.orEmpty() + binding.subtitleTextView.isVisible = subtitle != null + } + + private fun Mention.iconRes(): Int = when (this) { + Mention.Channel, Mention.Here -> UiCommonR.drawable.stream_design_ic_megaphone + is Mention.Role -> UiCommonR.drawable.stream_design_ic_role + is Mention.Group -> UiCommonR.drawable.stream_design_ic_users + else -> UiCommonR.drawable.stream_design_ic_users + } + + private fun Mention.subtitle(context: Context): String? = when (this) { + Mention.Channel -> context.getString( + R.string.stream_ui_message_composer_mention_suggestion_channel_subtitle, + ) + Mention.Here -> context.getString( + R.string.stream_ui_message_composer_mention_suggestion_here_subtitle, + ) + is Mention.Role -> context.getString( + R.string.stream_ui_message_composer_mention_suggestion_role_subtitle, + role, + ) + is Mention.Group -> group.description?.takeIf { it.isNotBlank() } + else -> null + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel.kt index c7cdbfbb1ff..65d49fcedac 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel.kt @@ -24,6 +24,7 @@ import io.getstream.chat.android.models.CreatePollParams import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User import io.getstream.chat.android.ui.common.feature.messages.composer.MessageComposerController +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import io.getstream.chat.android.ui.common.state.messages.Edit import io.getstream.chat.android.ui.common.state.messages.MessageAction import io.getstream.chat.android.ui.common.state.messages.MessageInput @@ -173,6 +174,11 @@ public class MessageComposerViewModel( */ public fun selectMention(user: User): Unit = messageComposerController.selectMention(user) + /** + * Autocompletes the current text input with the selected mention. + */ + public fun selectMention(mention: Mention): Unit = messageComposerController.selectMention(mention) + /** * Switches the message composer to the command input mode. * diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinder.kt index 90c6a3e7794..7bd5eace1a9 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinder.kt @@ -21,6 +21,7 @@ import io.getstream.chat.android.models.Attachment import io.getstream.chat.android.models.Command import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import io.getstream.chat.android.ui.feature.messages.composer.MessageComposerView import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.alsoSendToChannelSelectionListener import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.attachmentRemovalListener @@ -41,6 +42,7 @@ import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelD import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.dismissSuggestionsListener import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.mentionSelectionListener import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.sendMessageButtonClickListener +import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.suggestedMentionSelectionListener import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.textInputChangeListener /** @@ -70,6 +72,7 @@ public class MessageComposerViewModelBinder private constructor( private var attachmentSelectionListener: (List) -> Unit = vm.attachmentSelectionListener private var attachmentRemovalListener: (Attachment) -> Unit = vm.attachmentRemovalListener private var mentionSelectionListener: (User) -> Unit = vm.mentionSelectionListener + private var suggestedMentionSelectionListener: (Mention) -> Unit = vm.suggestedMentionSelectionListener private var commandSelectionListener: (Command) -> Unit = vm.commandSelectionListener private var alsoSendToChannelSelectionListener: (Boolean) -> Unit = vm.alsoSendToChannelSelectionListener private var dismissActionClickListener: () -> Unit = vm.dismissActionClickListener @@ -144,6 +147,14 @@ public class MessageComposerViewModelBinder private constructor( return this } + /** + * Sets the selection listener invoked when any mention suggestion item is selected. + */ + public fun onSuggestedMentionSelection(listener: (Mention) -> Unit): MessageComposerViewModelBinder { + suggestedMentionSelectionListener = listener + return this + } + /** * Sets the selection listener invoked when a command suggestion item is selected. * @@ -322,6 +333,7 @@ public class MessageComposerViewModelBinder private constructor( ?: vm.audioCompleteButtonClickListener(view.composerStyle.audioRecordingSendOnComplete), audioSliderDragStartListener = audioSliderDragStartListener, audioSliderDragStopListener = audioSliderDragStopListener, + suggestedMentionSelectionListener = suggestedMentionSelectionListener, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinding.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinding.kt index 7e1f1c16ff5..e8b2536fc10 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinding.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModelBinding.kt @@ -25,6 +25,7 @@ import io.getstream.chat.android.models.Command import io.getstream.chat.android.models.CreatePollParams import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import io.getstream.chat.android.ui.feature.messages.composer.MessageComposerView import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.alsoSendToChannelSelectionListener import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.attachmentRemovalListener @@ -46,6 +47,7 @@ import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelD import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.mentionSelectionListener import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.pollSubmissionListener import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.sendMessageButtonClickListener +import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.suggestedMentionSelectionListener import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelDefaults.textInputChangeListener import kotlinx.coroutines.launch @@ -109,6 +111,7 @@ public fun MessageComposerViewModel.bindView( ), audioSliderDragStartListener: (Float) -> Unit = this.audioSliderDragStartListener, audioSliderDragStopListener: (Float) -> Unit = this.audioSliderDragStopListener, + suggestedMentionSelectionListener: (Mention) -> Unit = this.suggestedMentionSelectionListener, ) { view.sendMessageButtonClickListener = { sendMessageButtonClickListener(messageBuilder()) } view.textInputChangeListener = textInputChangeListener @@ -116,6 +119,7 @@ public fun MessageComposerViewModel.bindView( view.attachmentRemovalListener = attachmentRemovalListener view.pollSubmissionListener = pollSubmissionListener view.mentionSelectionListener = mentionSelectionListener + view.suggestedMentionSelectionListener = suggestedMentionSelectionListener view.commandSelectionListener = commandSelectionListener view.alsoSendToChannelSelectionListener = alsoSendToChannelSelectionListener view.dismissActionClickListener = dismissActionClickListener @@ -194,6 +198,7 @@ public fun MessageComposerViewModel.bindViewDefaults( audioCompleteButtonClickListener: (() -> Unit)? = null, audioSliderDragStartListener: ((Float) -> Unit)? = null, audioSliderDragStopListener: ((Float) -> Unit)? = null, + suggestedMentionSelectionListener: ((Mention) -> Unit)? = null, ) { val sendOnComplete = view.composerStyle.audioRecordingSendOnComplete bindView( @@ -223,6 +228,8 @@ public fun MessageComposerViewModel.bindViewDefaults( audioCompleteButtonClickListener, audioSliderDragStartListener = this.audioSliderDragStartListener and audioSliderDragStartListener, audioSliderDragStopListener = this.audioSliderDragStopListener and audioSliderDragStopListener, + suggestedMentionSelectionListener = + this.suggestedMentionSelectionListener and suggestedMentionSelectionListener, ) } @@ -253,6 +260,7 @@ internal object MessageComposerViewModelDefaults { val MessageComposerViewModel.pollSubmissionListener: (CreatePollParams) -> Unit get() = { createPoll(it) } val MessageComposerViewModel.attachmentRemovalListener: (Attachment) -> Unit get() = ::removeAttachment val MessageComposerViewModel.mentionSelectionListener: (User) -> Unit get() = { selectMention(it) } + val MessageComposerViewModel.suggestedMentionSelectionListener: (Mention) -> Unit get() = { selectMention(it) } val MessageComposerViewModel.commandSelectionListener: (Command) -> Unit get() = { selectCommand(it) } val MessageComposerViewModel.alsoSendToChannelSelectionListener: (Boolean) -> Unit get() = { setAlsoSendToChannel(it) } val MessageComposerViewModel.dismissActionClickListener: () -> Unit get() = { dismissMessageActions() } diff --git a/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_shape_mention_icon_avatar.xml b/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_shape_mention_icon_avatar.xml new file mode 100644 index 00000000000..716b646db77 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_shape_mention_icon_avatar.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_mention_special.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_mention_special.xml new file mode 100644 index 00000000000..4b00d48964f --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_mention_special.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/values-es/strings.xml b/stream-chat-android-ui-components/src/main/res/values-es/strings.xml index 6e926ffe0b3..e24297acb82 100644 --- a/stream-chat-android-ui-components/src/main/res/values-es/strings.xml +++ b/stream-chat-android-ui-components/src/main/res/values-es/strings.xml @@ -74,6 +74,9 @@ "Permitir acceso a archivos de audio" "Permitir acceso a más contenido multimedia" "Permitir acceso a contenido multimedia" + "Notificar a todos en esta conversación" + "Notificar a todos los miembros conectados en esta conversación" + "Notificar a todos los miembros %1$s" "Permitir acceso a tu galería" "No puedes enviar mensajes en este canal" "Escribe algo aquí" diff --git a/stream-chat-android-ui-components/src/main/res/values-fr/strings.xml b/stream-chat-android-ui-components/src/main/res/values-fr/strings.xml index 32dbd43bb2d..f25071a9910 100644 --- a/stream-chat-android-ui-components/src/main/res/values-fr/strings.xml +++ b/stream-chat-android-ui-components/src/main/res/values-fr/strings.xml @@ -74,6 +74,9 @@ "Autoriser l\'accès aux fichiers audio" "Autoriser l\'accès à plus de médias visuels" "Autoriser l\'accès aux médias visuels" + "Notifier tout le monde dans cette conversation" + "Notifier tous les membres en ligne dans cette conversation" + "Notifier tous les membres %1$s" "Autoriser l\'accès à votre galerie" "Vous ne pouvez pas envoyer de messages dans ce canal" "Écrivez quelque chose ici" diff --git a/stream-chat-android-ui-components/src/main/res/values-hi/strings.xml b/stream-chat-android-ui-components/src/main/res/values-hi/strings.xml index f6286b3434e..f9a309ca159 100644 --- a/stream-chat-android-ui-components/src/main/res/values-hi/strings.xml +++ b/stream-chat-android-ui-components/src/main/res/values-hi/strings.xml @@ -74,6 +74,9 @@ "ऑडियो फ़ाइलों तक पहुँच की अनुमति दें" "अधिक विज़ुअल मीडिया तक पहुँच की अनुमति दें" "विज़ुअल मीडिया तक पहुँच की अनुमति दें" + "इस बातचीत में सभी को सूचित करें" + "इस बातचीत के सभी ऑनलाइन सदस्यों को सूचित करें" + "सभी %1$s सदस्यों को सूचित करें" "गैलरी एक्सेस करने की अनुमति दें" "आप इस चैनल में मैसेज नहीं भेज सकते" "यहाँ कुछ लिखें" diff --git a/stream-chat-android-ui-components/src/main/res/values-in/strings.xml b/stream-chat-android-ui-components/src/main/res/values-in/strings.xml index d8c28d62355..72c1a97be5d 100644 --- a/stream-chat-android-ui-components/src/main/res/values-in/strings.xml +++ b/stream-chat-android-ui-components/src/main/res/values-in/strings.xml @@ -71,6 +71,9 @@ "Izinkan akses ke berkas audio" "Izinkan akses ke lebih banyak media visual" "Izinkan akses ke media visual" + "Beri tahu semua orang di percakapan ini" + "Beri tahu semua anggota online di percakapan ini" + "Beri tahu semua anggota %1$s" "Izinkan akses ke Galeri" "Anda tidak dapat mengirim pesan di grup ini" "Masukkan pesan" diff --git a/stream-chat-android-ui-components/src/main/res/values-it/strings.xml b/stream-chat-android-ui-components/src/main/res/values-it/strings.xml index 441de586d89..0d7814f0c56 100644 --- a/stream-chat-android-ui-components/src/main/res/values-it/strings.xml +++ b/stream-chat-android-ui-components/src/main/res/values-it/strings.xml @@ -74,6 +74,9 @@ "Consenti l\'accesso ai file audio" "Consenti l\'accesso a più contenuti multimediali" "Consenti l\'accesso ai contenuti multimediali" + "Notifica tutti in questa conversazione" + "Notifica tutti i membri online in questa conversazione" + "Notifica tutti i membri %1$s" "Consenti l\'accesso alla tua Galleria" "Non puoi inviare messaggi in questo canale" "Scrivi qualcosa" diff --git a/stream-chat-android-ui-components/src/main/res/values-ja/strings.xml b/stream-chat-android-ui-components/src/main/res/values-ja/strings.xml index e238558628d..f8e5c2ab5a6 100644 --- a/stream-chat-android-ui-components/src/main/res/values-ja/strings.xml +++ b/stream-chat-android-ui-components/src/main/res/values-ja/strings.xml @@ -62,6 +62,9 @@ "オーディオファイルへのアクセスを許可する" "より多くのビジュアルメディアへのアクセスを許可する" "ビジュアルメディアへのアクセスを許可する" + "この会話の全員に通知します" + "この会話のオンラインメンバー全員に通知します" + "%1$s メンバー全員に通知します" "ギャラリーへのアクセスを許可する" "このチャンネルではメッセージを送信できません" "ここに何か書く" diff --git a/stream-chat-android-ui-components/src/main/res/values-ko/strings.xml b/stream-chat-android-ui-components/src/main/res/values-ko/strings.xml index 9cde0c559c6..9349e68153e 100644 --- a/stream-chat-android-ui-components/src/main/res/values-ko/strings.xml +++ b/stream-chat-android-ui-components/src/main/res/values-ko/strings.xml @@ -62,6 +62,9 @@ "오디오 파일 액세스 허용" "더 많은 시각적 미디어에 대한 액세스 허용" "시각적 미디어에 대한 액세스 허용" + "이 대화의 모든 사람에게 알립니다" + "이 대화의 모든 온라인 멤버에게 알립니다" + "모든 %1$s 멤버에게 알립니다" "갤러리 액세스를 허용" "이 채널에서는 메시지를 보낼 수 없습니다" "메시지 입력" diff --git a/stream-chat-android-ui-components/src/main/res/values/strings.xml b/stream-chat-android-ui-components/src/main/res/values/strings.xml index 2922a1f7b39..95c05ad57b8 100644 --- a/stream-chat-android-ui-components/src/main/res/values/strings.xml +++ b/stream-chat-android-ui-components/src/main/res/values/strings.xml @@ -83,6 +83,9 @@ \@%1$s + Notify everyone in this channel + Notify every online member in this channel + Notify all %1$s members Instant Commands diff --git a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt index eb9c3f95db5..dd663ab6b43 100644 --- a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt +++ b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt @@ -36,13 +36,16 @@ import io.getstream.chat.android.models.FileUploadConfig import io.getstream.chat.android.models.InitializationState import io.getstream.chat.android.models.Member import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Role import io.getstream.chat.android.models.User +import io.getstream.chat.android.models.UserGroup import io.getstream.chat.android.positiveRandomLong import io.getstream.chat.android.randomString import io.getstream.chat.android.test.TestCoroutineExtension import io.getstream.chat.android.test.asCall import io.getstream.chat.android.ui.common.feature.messages.composer.MessageComposerController import io.getstream.chat.android.ui.common.feature.messages.composer.mention.DefaultUserLookupHandler +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import io.getstream.chat.android.ui.common.state.messages.Edit import io.getstream.chat.android.ui.common.state.messages.MessageMode import io.getstream.chat.android.ui.common.state.messages.Reply @@ -57,7 +60,11 @@ import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be instance of` import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq @@ -356,6 +363,31 @@ internal class MessageComposerViewModelTest { viewModel.messageInput.value.text `should be equal to` "@Jc Miñarro " } + @ParameterizedTest + @MethodSource("nonUserMentionCases") + fun `Given message composer When typing mention query Should surface the matching non-user Mention`( + capability: String, + query: String, + expectedMention: Mention, + roles: List, + groups: List, + ) = runTest { + val viewModel = Fixture() + .givenCurrentUser() + .givenChannelQuery() + .givenChannelState(channelData = channelDataWith(capability)) + .givenNoMemberQueryResult() + .givenRoleSearchResult(roles) + .givenGroupSearchResult(groups) + .get() + + viewModel.setMessageInput(query) + advanceUntilIdle() + + viewModel.messageComposerState.value.suggestedMentions `should be equal to` listOf(expectedMention) + viewModel.messageComposerState.value.mentionSuggestions.size `should be equal to` 0 + } + private class Fixture( private val chatClient: ChatClient = mock(), private val channelId: String = "messaging:123", @@ -431,6 +463,21 @@ internal class MessageComposerViewModelTest { whenever(chatClient.markMessageRead(any(), any(), any())) doReturn Unit.asCall() } + fun givenRoleSearchResult(roles: List) = apply { + whenever(chatClient.searchRoles(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())) + .doReturn(roles.asCall()) + } + + fun givenGroupSearchResult(groups: List) = apply { + whenever(chatClient.searchUserGroups(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())) + .doReturn(groups.asCall()) + } + + fun givenNoMemberQueryResult() = apply { + whenever(chatClient.queryMembers(any(), any(), any(), any(), any(), any(), any())) + .doReturn(emptyList().asCall()) + } + fun get(): MessageComposerViewModel { return MessageComposerViewModel( MessageComposerController( @@ -457,6 +504,32 @@ internal class MessageComposerViewModelTest { ownCapabilities = capabilities.toSet(), ) + @JvmStatic + fun nonUserMentionCases(): List { + val role = Role(name = "admin") + val group = UserGroup(id = "g1", name = "platform") + val noRoles = emptyList() + val noGroups = emptyList() + return listOf( + Arguments.of(ChannelCapabilities.NOTIFY_CHANNEL, "@", Mention.Channel, noRoles, noGroups), + Arguments.of(ChannelCapabilities.NOTIFY_HERE, "@", Mention.Here, noRoles, noGroups), + Arguments.of( + ChannelCapabilities.NOTIFY_ROLE, + "@admin", + Mention.Role(role.name), + listOf(role), + noGroups, + ), + Arguments.of( + ChannelCapabilities.NOTIFY_GROUP, + "@plat", + Mention.Group(group), + noRoles, + listOf(group), + ), + ) + } + val user1 = User( id = "Jc", name = "Jc Miñarro",