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",