From 7ad2f3ba9826dc98e4ce400723fb76cba5e37206 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Fri, 29 May 2026 11:49:32 +0200 Subject: [PATCH] Render enhanced mentions in incoming messages Introduces the `Mention` sealed type and `MentionRegex` in `ui-common` as shared primitives, then wires both SDKs to highlight `@channel`, `@here`, role, and group tokens alongside user mentions when a message arrives with the matching fields populated. Compose: `TextUtils` adds per-type annotation tags and `StreamDesign` per-type color tokens (`chatTextMention*`/ `chatBgMention*`); `MessageText`/`MessageContent`/ `MessageContainer`/`MessageItem`/`MessageList` plumb a typed `onMentionClick: (Mention) -> Unit` callback. ui-components: `Linkify`/`MentionSpan`/`TextViewLinkHandler`/ `LongClickFriendlyLinkMovementMethod` are rewritten to span every mention type and dispatch through a new `MessageListView.OnMentionTokenClickListener` (the legacy user-only `OnMentionClickListener` stays in place, deprecated). --- .../api/stream-chat-android-compose.api | 131 ++++++++++++---- .../ui/components/messages/MessageContent.kt | 5 + .../ui/components/messages/MessageText.kt | 46 ++++-- .../ui/messages/list/MessageContainer.kt | 7 + .../compose/ui/messages/list/MessageItem.kt | 3 + .../compose/ui/messages/list/MessageList.kt | 3 + .../ui/theme/ChatComponentFactoryParams.kt | 101 ++++++++++++- .../android/compose/ui/theme/StreamDesign.kt | 27 ++++ .../compose/ui/util/MessageTextFormatter.kt | 11 +- .../chat/android/compose/ui/util/TextUtils.kt | 136 ++++++++++++----- .../messages/MessageTextHelpersTest.kt | 93 ++++++++++-- .../compose/ui/util/TextUtilsKtTest.kt | 112 +++++++++++++- .../markdown/MarkdownTextTransformer.kt | 2 +- .../api/stream-chat-android-ui-common.api | 46 ++++++ .../messages/composer/mention/Mention.kt | 60 +++++++- .../messages/composer/mention/MentionRegex.kt | 32 ++++ .../composer/mention/MentionRegexTest.kt | 87 +++++++++++ .../api/stream-chat-android-ui-components.api | 6 + .../feature/messages/list/MessageListView.kt | 44 +++++- .../list/adapter/MessageListListeners.kt | 8 + .../list/adapter/MessageListListenersImpl.kt | 15 ++ .../impl/CustomAttachmentsViewHolder.kt | 5 +- .../impl/FileAttachmentsViewHolder.kt | 3 +- .../impl/LinkAttachmentsViewHolder.kt | 3 +- .../impl/MediaAttachmentsViewHolder.kt | 3 +- .../impl/MessagePlainTextViewHolder.kt | 3 +- .../LongClickFriendlyLinkMovementMethod.kt | 25 +++- .../AutoLinkableTextTransformer.kt | 2 +- .../chat/android/ui/utils/Linkify.kt | 140 ++++++++++-------- .../ui/utils/{UserSpan.kt => MentionSpan.kt} | 10 +- .../android/ui/utils/TextViewLinkHandler.kt | 6 +- 31 files changed, 989 insertions(+), 186 deletions(-) create mode 100644 stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionRegex.kt create mode 100644 stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionRegexTest.kt rename stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/{UserSpan.kt => MentionSpan.kt} (71%) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 3e8235e73af..a95b4b8b80d 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1610,7 +1610,7 @@ public final class io/getstream/chat/android/compose/ui/components/messages/Mess } public final class io/getstream/chat/android/compose/ui/components/messages/MessageContentKt { - public static final fun MessageContent (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lio/getstream/chat/android/compose/state/messages/MessageAlignment;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun MessageContent (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lio/getstream/chat/android/compose/state/messages/MessageAlignment;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/chat/android/compose/ui/components/messages/MessageFooterKt { @@ -1623,7 +1623,7 @@ public final class io/getstream/chat/android/compose/ui/components/messages/Mess } public final class io/getstream/chat/android/compose/ui/components/messages/MessageTextKt { - public static final fun MessageText (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun MessageText (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/components/messages/MessageThreadFooterKt { @@ -2190,14 +2190,14 @@ public final class io/getstream/chat/android/compose/ui/messages/list/Composable public final class io/getstream/chat/android/compose/ui/messages/list/MessageContainerKt { public static final field HighlightFadeOutDurationMillis I - public static final fun DefaultMessageContent (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V + public static final fun DefaultMessageContent (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V public static final fun EmojiMessageContent (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V - public static final fun MessageContainer (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V - public static final fun RegularMessageContent (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun MessageContainer (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V + public static final fun RegularMessageContent (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/messages/list/MessageItemKt { - public static final fun MessageItem (Landroidx/compose/foundation/lazy/LazyItemScope;Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V + public static final fun MessageItem (Landroidx/compose/foundation/lazy/LazyItemScope;Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/chat/android/compose/ui/messages/list/MessageListKt { @@ -4847,8 +4847,8 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageComposerLin public final class io/getstream/chat/android/compose/ui/theme/MessageComposerParams { public static final field $stable I - public fun (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;Lkotlin/jvm/functions/Function4;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;Lkotlin/jvm/functions/Function4;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;Lkotlin/jvm/functions/Function4;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;Lkotlin/jvm/functions/Function4;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState; public final fun component10 ()Lkotlin/jvm/functions/Function1; public final fun component11 ()Lkotlin/jvm/functions/Function1; @@ -4861,6 +4861,7 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageComposerPar public final fun component18 ()Lkotlin/jvm/functions/Function1; public final fun component19 ()Lkotlin/jvm/functions/Function0; public final fun component2 ()Lkotlin/jvm/functions/Function4; + public final fun component20 ()Lkotlin/jvm/functions/Function1; public final fun component3 ()Landroidx/compose/ui/Modifier; public final fun component4 ()Z public final fun component5 ()Lkotlin/jvm/functions/Function2; @@ -4868,8 +4869,8 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageComposerPar public final fun component7 ()Ljava/lang/String; public final fun component8 ()Lkotlin/jvm/functions/Function0; public final fun component9 ()Ljava/lang/String; - public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;Lkotlin/jvm/functions/Function4;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerParams; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageComposerParams;Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;Lkotlin/jvm/functions/Function4;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerParams; + public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;Lkotlin/jvm/functions/Function4;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageComposerParams;Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;Lkotlin/jvm/functions/Function4;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerParams; public fun equals (Ljava/lang/Object;)Z public final fun getAttachmentsActionLabel ()Ljava/lang/String; public final fun getInput ()Lkotlin/jvm/functions/Function4; @@ -4883,6 +4884,7 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageComposerPar public final fun getOnCancelLinkPreviewClick ()Lkotlin/jvm/functions/Function0; public final fun getOnCommandSelected ()Lkotlin/jvm/functions/Function1; public final fun getOnLinkPreviewClick ()Lkotlin/jvm/functions/Function1; + public final fun getOnMentionSelected ()Lkotlin/jvm/functions/Function1; public final fun getOnSendMessage ()Lkotlin/jvm/functions/Function2; public final fun getOnUserSelected ()Lkotlin/jvm/functions/Function1; public final fun getOnValueChange ()Lkotlin/jvm/functions/Function1; @@ -4957,6 +4959,65 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageComposerSna public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemCenterContentParams { + public static final field $stable I + public fun (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;)V + public synthetic fun (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention; + public final fun component2 ()Landroidx/compose/ui/Modifier; + public final fun copy (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemCenterContentParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemCenterContentParams;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemCenterContentParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getMention ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention; + public final fun getModifier ()Landroidx/compose/ui/Modifier; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemLeadingContentParams { + public static final field $stable I + public fun (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;)V + public synthetic fun (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention; + public final fun component2 ()Landroidx/compose/ui/Modifier; + public final fun copy (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemLeadingContentParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemLeadingContentParams;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemLeadingContentParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getMention ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention; + public final fun getModifier ()Landroidx/compose/ui/Modifier; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemParams { + public static final field $stable I + public fun (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Lkotlin/jvm/functions/Function1;)V + public final fun component1 ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention; + public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemParams;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getMention ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention; + public final fun getOnMentionSelected ()Lkotlin/jvm/functions/Function1; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemTrailingContentParams { + public static final field $stable I + public fun (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;)V + public synthetic fun (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention; + public final fun component2 ()Landroidx/compose/ui/Modifier; + public final fun copy (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemTrailingContentParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemTrailingContentParams;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;Landroidx/compose/ui/Modifier;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageComposerSuggestionItemTrailingContentParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getMention ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention; + public final fun getModifier ()Landroidx/compose/ui/Modifier; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/compose/ui/theme/MessageComposerTrailingContentParams { public static final field $stable I public fun (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;)V @@ -5034,8 +5095,8 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageComposerUse public final class io/getstream/chat/android/compose/ui/theme/MessageContainerParams { public static final field $stable I - public fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState; public final fun component10 ()Lkotlin/jvm/functions/Function1; public final fun component11 ()Lkotlin/jvm/functions/Function1; @@ -5048,6 +5109,7 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageContainerPa public final fun component18 ()Lkotlin/jvm/functions/Function1; public final fun component19 ()Lkotlin/jvm/functions/Function2; public final fun component2 ()Landroidx/compose/ui/Modifier; + public final fun component20 ()Lkotlin/jvm/functions/Function1; public final fun component3 ()Lkotlin/jvm/functions/Function2; public final fun component4 ()Lkotlin/jvm/functions/Function3; public final fun component5 ()Lkotlin/jvm/functions/Function3; @@ -5055,8 +5117,8 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageContainerPa public final fun component7 ()Lkotlin/jvm/functions/Function1; public final fun component8 ()Lkotlin/jvm/functions/Function2; public final fun component9 ()Lkotlin/jvm/functions/Function1; - public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/theme/MessageContainerParams; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageContainerParams;Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageContainerParams; + public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/ui/theme/MessageContainerParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageContainerParams;Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageContainerParams; public fun equals (Ljava/lang/Object;)Z public final fun getMessageItem ()Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState; public final fun getModifier ()Landroidx/compose/ui/Modifier; @@ -5067,6 +5129,7 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageContainerPa public final fun getOnGiphyActionClick ()Lkotlin/jvm/functions/Function1; public final fun getOnLongItemClick ()Lkotlin/jvm/functions/Function1; public final fun getOnMediaGalleryPreviewResult ()Lkotlin/jvm/functions/Function1; + public final fun getOnMentionClick ()Lkotlin/jvm/functions/Function1; public final fun getOnMessageLinkClick ()Lkotlin/jvm/functions/Function2; public final fun getOnPollUpdated ()Lkotlin/jvm/functions/Function2; public final fun getOnQuotedMessageClick ()Lkotlin/jvm/functions/Function1; @@ -5083,14 +5146,15 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageContainerPa public final class io/getstream/chat/android/compose/ui/theme/MessageContentParams { public static final field $stable I - public fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState; public final fun component10 ()Lkotlin/jvm/functions/Function1; public final fun component11 ()Lkotlin/jvm/functions/Function1; public final fun component12 ()Lkotlin/jvm/functions/Function1; public final fun component13 ()Lkotlin/jvm/functions/Function1; public final fun component14 ()Lkotlin/jvm/functions/Function2; + public final fun component15 ()Lkotlin/jvm/functions/Function1; public final fun component2 ()Lkotlin/jvm/functions/Function1; public final fun component3 ()Lkotlin/jvm/functions/Function2; public final fun component4 ()Lkotlin/jvm/functions/Function3; @@ -5099,8 +5163,8 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageContentPara public final fun component7 ()Lkotlin/jvm/functions/Function3; public final fun component8 ()Lkotlin/jvm/functions/Function1; public final fun component9 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/theme/MessageContentParams; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageContentParams;Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageContentParams; + public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/ui/theme/MessageContentParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageContentParams;Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageContentParams; public fun equals (Ljava/lang/Object;)Z public final fun getMessageItem ()Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState; public final fun getOnAddAnswer ()Lkotlin/jvm/functions/Function3; @@ -5111,6 +5175,7 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageContentPara public final fun getOnLinkClick ()Lkotlin/jvm/functions/Function2; public final fun getOnLongItemClick ()Lkotlin/jvm/functions/Function1; public final fun getOnMediaGalleryPreviewResult ()Lkotlin/jvm/functions/Function1; + public final fun getOnMentionClick ()Lkotlin/jvm/functions/Function1; public final fun getOnPollUpdated ()Lkotlin/jvm/functions/Function2; public final fun getOnQuotedMessageClick ()Lkotlin/jvm/functions/Function1; public final fun getOnRemoveVote ()Lkotlin/jvm/functions/Function3; @@ -5212,8 +5277,8 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageGiphyConten public final class io/getstream/chat/android/compose/ui/theme/MessageItemParams { public static final field $stable I - public fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState; public final fun component10 ()Lkotlin/jvm/functions/Function1; public final fun component11 ()Lkotlin/jvm/functions/Function1; @@ -5224,6 +5289,7 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageItemParams public final fun component16 ()Lkotlin/jvm/functions/Function1; public final fun component17 ()Lkotlin/jvm/functions/Function1; public final fun component18 ()Lkotlin/jvm/functions/Function2; + public final fun component19 ()Lkotlin/jvm/functions/Function1; public final fun component2 ()Lkotlin/jvm/functions/Function2; public final fun component3 ()Lkotlin/jvm/functions/Function3; public final fun component4 ()Lkotlin/jvm/functions/Function3; @@ -5232,8 +5298,8 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageItemParams public final fun component7 ()Lkotlin/jvm/functions/Function2; public final fun component8 ()Lkotlin/jvm/functions/Function1; public final fun component9 ()Lkotlin/jvm/functions/Function1; - public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/theme/MessageItemParams; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageItemParams;Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageItemParams; + public final fun copy (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/ui/theme/MessageItemParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageItemParams;Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageItemParams; public fun equals (Ljava/lang/Object;)Z public final fun getMessageListItem ()Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState; public final fun getOnAddAnswer ()Lkotlin/jvm/functions/Function3; @@ -5243,6 +5309,7 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageItemParams public final fun getOnGiphyActionClick ()Lkotlin/jvm/functions/Function1; public final fun getOnLongItemClick ()Lkotlin/jvm/functions/Function1; public final fun getOnMediaGalleryPreviewResult ()Lkotlin/jvm/functions/Function1; + public final fun getOnMentionClick ()Lkotlin/jvm/functions/Function1; public final fun getOnMessageLinkClick ()Lkotlin/jvm/functions/Function2; public final fun getOnPollUpdated ()Lkotlin/jvm/functions/Function2; public final fun getOnQuotedMessageClick ()Lkotlin/jvm/functions/Function1; @@ -5511,8 +5578,8 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageReactionsPi public final class io/getstream/chat/android/compose/ui/theme/MessageRegularContentParams { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/compose/state/messages/MessageAlignment;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/compose/state/messages/MessageAlignment;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/compose/state/messages/MessageAlignment;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/compose/state/messages/MessageAlignment;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/models/Message; public final fun component2 ()Lio/getstream/chat/android/models/User; public final fun component3 ()Lio/getstream/chat/android/compose/state/messages/MessageAlignment; @@ -5521,8 +5588,9 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageRegularCont public final fun component6 ()Lkotlin/jvm/functions/Function1; public final fun component7 ()Lkotlin/jvm/functions/Function1; public final fun component8 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/compose/state/messages/MessageAlignment;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/theme/MessageRegularContentParams; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageRegularContentParams;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/compose/state/messages/MessageAlignment;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageRegularContentParams; + public final fun component9 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/compose/state/messages/MessageAlignment;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/ui/theme/MessageRegularContentParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageRegularContentParams;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/compose/state/messages/MessageAlignment;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageRegularContentParams; public fun equals (Ljava/lang/Object;)Z public final fun getCurrentUser ()Lio/getstream/chat/android/models/User; public final fun getMessage ()Lio/getstream/chat/android/models/Message; @@ -5530,6 +5598,7 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageRegularCont public final fun getOnLinkClick ()Lkotlin/jvm/functions/Function2; public final fun getOnLongItemClick ()Lkotlin/jvm/functions/Function1; public final fun getOnMediaGalleryPreviewResult ()Lkotlin/jvm/functions/Function1; + public final fun getOnMentionClick ()Lkotlin/jvm/functions/Function1; public final fun getOnQuotedMessageClick ()Lkotlin/jvm/functions/Function1; public final fun getOnUserMentionClick ()Lkotlin/jvm/functions/Function1; public fun hashCode ()I @@ -5550,22 +5619,24 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageSpacerParam public final class io/getstream/chat/android/compose/ui/theme/MessageTextContentParams { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/models/Message; public final fun component2 ()Lio/getstream/chat/android/models/User; public final fun component3 ()Lkotlin/jvm/functions/Function1; public final fun component4 ()Lkotlin/jvm/functions/Function1; public final fun component5 ()Landroidx/compose/ui/Modifier; public final fun component6 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/theme/MessageTextContentParams; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageTextContentParams;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageTextContentParams; + public final fun component7 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/ui/theme/MessageTextContentParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/MessageTextContentParams;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/MessageTextContentParams; public fun equals (Ljava/lang/Object;)Z public final fun getCurrentUser ()Lio/getstream/chat/android/models/User; public final fun getMessage ()Lio/getstream/chat/android/models/Message; public final fun getModifier ()Landroidx/compose/ui/Modifier; public final fun getOnLinkClick ()Lkotlin/jvm/functions/Function2; public final fun getOnLongItemClick ()Lkotlin/jvm/functions/Function1; + public final fun getOnMentionClick ()Lkotlin/jvm/functions/Function1; public final fun getOnUserMentionClick ()Lkotlin/jvm/functions/Function1; public fun hashCode ()I public fun toString ()Ljava/lang/String; diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt index 56f6ba9c79c..15e5a79d84f 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt @@ -68,6 +68,7 @@ import io.getstream.chat.android.compose.ui.util.passiveRipple import io.getstream.chat.android.compose.ui.util.shouldBeDisplayedAsFullSizeAttachment 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.common.state.messages.list.GiphyAction import io.getstream.chat.android.ui.common.utils.extensions.hasLink @@ -95,6 +96,7 @@ public fun MessageContent( messageAlignment: MessageAlignment = MessageAlignment.Start, onLinkClick: ((Message, String) -> Unit)? = null, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, + onMentionClick: (Mention) -> Unit = {}, ) { when { message.isGiphyEphemeral() -> ChatTheme.componentFactory.MessageGiphyContent( @@ -123,6 +125,7 @@ public fun MessageContent( onQuotedMessageClick = onQuotedMessageClick, onLinkClick = onLinkClick, onUserMentionClick = onUserMentionClick, + onMentionClick = onMentionClick, ), ) } @@ -180,6 +183,7 @@ internal fun DefaultMessageRegularContent( onQuotedMessageClick: (Message) -> Unit, onUserMentionClick: (User) -> Unit = {}, onLinkClick: ((Message, String) -> Unit)? = null, + onMentionClick: (Mention) -> Unit = {}, ) { val componentFactory = ChatTheme.componentFactory @@ -270,6 +274,7 @@ internal fun DefaultMessageRegularContent( onLongItemClick = onLongItemClick, onLinkClick = onLinkClick, onUserMentionClick = onUserMentionClick, + onMentionClick = onMentionClick, ), ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt index 147312a99ee..b8234fd7dba 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt @@ -47,14 +47,19 @@ import androidx.compose.ui.util.fastForEach import androidx.core.net.toUri import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.theme.MessageStyling +import io.getstream.chat.android.compose.ui.util.AnnotationTagChannelMention import io.getstream.chat.android.compose.ui.util.AnnotationTagEmail -import io.getstream.chat.android.compose.ui.util.AnnotationTagMention +import io.getstream.chat.android.compose.ui.util.AnnotationTagGroupMention +import io.getstream.chat.android.compose.ui.util.AnnotationTagHereMention +import io.getstream.chat.android.compose.ui.util.AnnotationTagRoleMention import io.getstream.chat.android.compose.ui.util.AnnotationTagUrl +import io.getstream.chat.android.compose.ui.util.AnnotationTagUserMention import io.getstream.chat.android.compose.ui.util.isFewEmoji import io.getstream.chat.android.compose.ui.util.isSingleEmoji import io.getstream.chat.android.compose.ui.util.showOriginalTextAsState 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.common.utils.extensions.getUserByNameOrId import io.getstream.chat.android.ui.common.utils.extensions.isMine @@ -81,6 +86,7 @@ public fun MessageText( onLongItemClick: (Message) -> Unit, onLinkClick: ((Message, String) -> Unit)? = null, onUserMentionClick: (User) -> Unit = {}, + onMentionClick: (Mention) -> Unit = {}, ) { val context = LocalContext.current @@ -105,7 +111,7 @@ public fun MessageText( } val annotations = remember(styledText) { - styledText.getStringAnnotations(0, styledText.lastIndex) + styledText.getStringAnnotations(0, styledText.length) } if (annotations.fastAny(AnnotatedString.Range::isInteractiveTag)) { ClickableText( @@ -122,6 +128,7 @@ public fun MessageText( position = position, message = message, onLinkClick = onLinkClick, + onMentionClick = onMentionClick, onUserMentionClick = onUserMentionClick, fallback = { url -> context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) @@ -228,8 +235,17 @@ private suspend fun AwaitPointerEventScope.consumeUntilUp() { * Whether this annotation range carries one of the interactive tags handled by [MessageText]: * URL, email, or mention. */ -internal fun AnnotatedString.Range.isInteractiveTag(): Boolean = - tag == AnnotationTagUrl || tag == AnnotationTagEmail || tag == AnnotationTagMention +internal fun AnnotatedString.Range.isInteractiveTag(): Boolean = when (tag) { + AnnotationTagUrl, + AnnotationTagEmail, + AnnotationTagUserMention, + AnnotationTagChannelMention, + AnnotationTagHereMention, + AnnotationTagRoleMention, + AnnotationTagGroupMention, + -> true + else -> false +} /** * Whether any annotation in the list both has an interactive tag and covers [offset]. Uses @@ -240,10 +256,10 @@ internal fun List>.hasInteractiveAt(offset: Int): /** * Resolves the interactive annotation at the given character [position] and dispatches the right - * handler. Mention annotations route to [onUserMentionClick] after resolving the username against - * [Message.mentionedUsers]; URL/email annotations route to [onLinkClick] when set, otherwise to - * [fallback]. Annotations with empty items, non-interactive tags, or no match at [position] are - * ignored. + * handler. Mention annotations route to [onMentionClick]; user mentions additionally fire + * [onUserMentionClick] for backward compatibility. URL/email annotations route to [onLinkClick] + * when set, otherwise to [fallback]. Annotations with empty items, non-interactive + * tags, or no match at [position] are ignored. */ @Suppress("LongParameterList") internal fun handleAnnotationClick( @@ -253,13 +269,23 @@ internal fun handleAnnotationClick( onLinkClick: ((Message, String) -> Unit)?, onUserMentionClick: (User) -> Unit, fallback: (String) -> Unit, + onMentionClick: (Mention) -> Unit = {}, ) { val annotation = annotations.firstOrNull { it.isInteractiveTag() && position in it.start until it.end } ?: return when (annotation.tag) { - AnnotationTagMention -> { - message.mentionedUsers.getUserByNameOrId(annotation.item)?.let(onUserMentionClick) + AnnotationTagUserMention -> { + val user = message.mentionedUsers.getUserByNameOrId(annotation.item) ?: return + onMentionClick(Mention.User(user)) + onUserMentionClick(user) + } + AnnotationTagChannelMention -> onMentionClick(Mention.Channel) + AnnotationTagHereMention -> onMentionClick(Mention.Here) + AnnotationTagRoleMention -> onMentionClick(Mention.Role(annotation.item)) + AnnotationTagGroupMention -> { + message.mentionedGroups.find { it.name == annotation.item } + ?.let { onMentionClick(Mention.Group(it)) } } AnnotationTagUrl, AnnotationTagEmail -> { val url = annotation.item diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt index a0c6d964e27..fc25f8db3e0 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt @@ -110,6 +110,7 @@ import io.getstream.chat.android.models.Option import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.User import io.getstream.chat.android.models.Vote +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import io.getstream.chat.android.ui.common.feature.messages.translations.MessageOriginalTranslationsStore import io.getstream.chat.android.ui.common.state.messages.list.GiphyAction import io.getstream.chat.android.ui.common.state.messages.list.MessageFocused @@ -170,6 +171,7 @@ public fun MessageContainer( onUserMentionClick: (User) -> Unit = {}, onReply: (Message) -> Unit = {}, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, + onMentionClick: (Mention) -> Unit = {}, ) { val message = messageItem.message val focusState = messageItem.focusState @@ -301,6 +303,7 @@ public fun MessageContainer( onQuotedMessageClick = onQuotedMessageClick, onLinkClick = onLinkClick, onUserMentionClick = onUserMentionClick, + onMentionClick = onMentionClick, onPollUpdated = onPollUpdated, onCastVote = onCastVote, onRemoveVote = onRemoveVote, @@ -641,6 +644,7 @@ public fun DefaultMessageContent( onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit, onClosePoll: (String) -> Unit, onAddPollOption: (poll: Poll, option: String) -> Unit, + onMentionClick: (Mention) -> Unit = {}, ) { val finalModifier = modifier.widthIn(max = 264.dp) if (messageItem.message.isPoll() && !messageItem.message.isDeleted()) { @@ -681,6 +685,7 @@ public fun DefaultMessageContent( onQuotedMessageClick = onQuotedMessageClick, onLinkClick = onLinkClick, onUserMentionClick = onUserMentionClick, + onMentionClick = onMentionClick, ) } } @@ -759,6 +764,7 @@ public fun RegularMessageContent( onLinkClick: ((Message, String) -> Unit)? = null, onUserMentionClick: (User) -> Unit = {}, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, + onMentionClick: (Mention) -> Unit = {}, ) { val message = messageItem.message val ownsMessage = messageItem.isMine @@ -778,6 +784,7 @@ public fun RegularMessageContent( onQuotedMessageClick = onQuotedMessageClick, onLinkClick = onLinkClick, onUserMentionClick = onUserMentionClick, + onMentionClick = onMentionClick, ) } if (!messageItem.isErrorOrFailed()) { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt index cf666f527ea..2d7f79265d3 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt @@ -78,6 +78,7 @@ import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.User import io.getstream.chat.android.models.Vote import io.getstream.chat.android.previewdata.PreviewUserData +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import io.getstream.chat.android.ui.common.state.messages.list.DateSeparatorItemState import io.getstream.chat.android.ui.common.state.messages.list.EmptyThreadPlaceholderItemState import io.getstream.chat.android.ui.common.state.messages.list.GiphyAction @@ -133,6 +134,7 @@ public fun LazyItemScope.MessageItem( onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onUserMentionClick: (User) -> Unit = {}, onReply: (Message) -> Unit = {}, + onMentionClick: (Mention) -> Unit = {}, ) { with(ChatTheme.componentFactory) { when (messageListItemState) { @@ -188,6 +190,7 @@ public fun LazyItemScope.MessageItem( onUserAvatarClick = onUserAvatarClick, onMessageLinkClick = onLinkClick, onUserMentionClick = onUserMentionClick, + onMentionClick = onMentionClick, onAddAnswer = onAddAnswer, onReply = onReply, ), diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt index 6c9c1245d02..e3fb9cfaabf 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt @@ -44,6 +44,7 @@ import io.getstream.chat.android.models.Option import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.User import io.getstream.chat.android.models.Vote +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import io.getstream.chat.android.ui.common.state.messages.list.GiphyAction import io.getstream.chat.android.ui.common.state.messages.list.MessageListItemState import io.getstream.chat.android.ui.common.state.messages.list.MessageListState @@ -205,6 +206,7 @@ internal fun LazyItemScope.DefaultMessageItem( onLinkClick: ((Message, String) -> Unit)? = null, onUserMentionClick: (User) -> Unit = {}, onReply: (Message) -> Unit = {}, + onMentionClick: (Mention) -> Unit = {}, ) { MessageItem( messageListItemState = messageListItemState, @@ -223,6 +225,7 @@ internal fun LazyItemScope.DefaultMessageItem( onUserAvatarClick = onUserAvatarClick, onLinkClick = onLinkClick, onUserMentionClick = onUserMentionClick, + onMentionClick = onMentionClick, onAddAnswer = onAddAnswer, onReply = onReply, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt index dfbda3ad809..80e11062594 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt @@ -73,6 +73,7 @@ import io.getstream.chat.android.models.Vote import io.getstream.chat.android.ui.common.feature.channel.info.ChannelInfoMemberViewEvent import io.getstream.chat.android.ui.common.feature.channel.info.ChannelInfoViewAction import io.getstream.chat.android.ui.common.feature.channel.info.ChannelInfoViewEvent +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import io.getstream.chat.android.ui.common.model.MessageResult import io.getstream.chat.android.ui.common.state.channel.attachments.ChannelAttachmentsViewState import io.getstream.chat.android.ui.common.state.channel.info.ChannelInfoViewState @@ -564,7 +565,9 @@ public data class ScrollToFirstUnreadButtonParams( * @param onGiphyActionClick Action invoked when a Giphy action is clicked. * @param onMediaGalleryPreviewResult Action invoked with the media gallery preview result. * @param onQuotedMessageClick Action invoked when a quoted message is clicked. - * @param onUserMentionClick Action invoked when a user mention is clicked. + * @param onUserMentionClick Action invoked when a user mention is clicked. Prefer [onMentionClick] + * for new code: it also fires for other mention types. + * @param onMentionClick Action invoked when any mention is clicked. * @param onAddAnswer Action invoked when an answer is added to a poll. * @param onReply Action invoked when the reply button is clicked. * @param onUserAvatarClick Action invoked when a user avatar is clicked. @@ -584,11 +587,17 @@ public data class MessageItemParams( val onGiphyActionClick: (GiphyAction) -> Unit = {}, val onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, val onQuotedMessageClick: (Message) -> Unit = {}, + @Deprecated( + message = "Use onMentionClick; it also fires for other mention types.", + replaceWith = ReplaceWith("onMentionClick"), + level = DeprecationLevel.WARNING, + ) val onUserMentionClick: (User) -> Unit = {}, val onAddAnswer: (Message, Poll, String) -> Unit = { _, _, _ -> }, val onReply: (Message) -> Unit = {}, val onUserAvatarClick: ((User) -> Unit)? = null, val onMessageLinkClick: ((Message, String) -> Unit)? = null, + val onMentionClick: (Mention) -> Unit = {}, ) /** @@ -690,7 +699,9 @@ public data class MessageListStartOfTheChannelItemContentParams( * @param onGiphyActionClick Action invoked when a Giphy action is clicked. * @param onMediaGalleryPreviewResult Action invoked with the media gallery preview result. * @param onQuotedMessageClick Action invoked when a quoted message is clicked. - * @param onUserMentionClick Action invoked when a user mention is clicked. + * @param onUserMentionClick Action invoked when a user mention is clicked. Prefer [onMentionClick] + * for new code: it also fires for other mention types. + * @param onMentionClick Action invoked when any mention is clicked. * @param onAddAnswer Action invoked when an answer is added to a poll. * @param onReply Action invoked when the reply button is clicked. * @param onUserAvatarClick Action invoked when a user avatar is clicked. @@ -711,11 +722,17 @@ public data class MessageContainerParams( val onGiphyActionClick: (GiphyAction) -> Unit = {}, val onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, val onQuotedMessageClick: (Message) -> Unit = {}, + @Deprecated( + message = "Use onMentionClick; it also fires for other mention types.", + replaceWith = ReplaceWith("onMentionClick"), + level = DeprecationLevel.WARNING, + ) val onUserMentionClick: (User) -> Unit = {}, val onAddAnswer: (Message, Poll, String) -> Unit = { _, _, _ -> }, val onReply: (Message) -> Unit = {}, val onUserAvatarClick: ((User) -> Unit)? = null, val onMessageLinkClick: ((Message, String) -> Unit)? = null, + val onMentionClick: (Mention) -> Unit = {}, ) /** @@ -796,6 +813,7 @@ public data class MessageAuthorParams( * @param onUserMentionClick Action invoked when a user mention is clicked. * @param onMediaGalleryPreviewResult Action invoked with the media gallery preview result. * @param onLinkClick Action invoked when a link in a message is clicked. + * @param onMentionClick Action invoked when any mention is clicked. */ public data class MessageContentParams( val messageItem: MessageItemState, @@ -809,9 +827,15 @@ public data class MessageContentParams( val onAddPollOption: (Poll, String) -> Unit = { _, _ -> }, val onGiphyActionClick: (GiphyAction) -> Unit = {}, val onQuotedMessageClick: (Message) -> Unit = {}, + @Deprecated( + message = "Use onMentionClick; it also fires for other mention types.", + replaceWith = ReplaceWith("onMentionClick"), + level = DeprecationLevel.WARNING, + ) val onUserMentionClick: (User) -> Unit = {}, val onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, val onLinkClick: ((Message, String) -> Unit)? = null, + val onMentionClick: (Mention) -> Unit = {}, ) /** @@ -858,8 +882,10 @@ public data class MessageDeletedContentParams( * @param onLongItemClick Action invoked when a message is long-clicked. * @param onMediaGalleryPreviewResult Action invoked with the media gallery preview result. * @param onQuotedMessageClick Action invoked when a quoted message is clicked. - * @param onUserMentionClick Action invoked when a user mention is clicked. + * @param onUserMentionClick Action invoked when a user mention is clicked. Prefer [onMentionClick] + * for new code: it also fires for other mention types. * @param onLinkClick Action invoked when a link in a message is clicked. + * @param onMentionClick Action invoked when any mention is clicked. */ public data class MessageRegularContentParams( val message: Message, @@ -868,8 +894,14 @@ public data class MessageRegularContentParams( val onLongItemClick: (Message) -> Unit, val onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit, val onQuotedMessageClick: (Message) -> Unit, + @Deprecated( + message = "Use onMentionClick; it also fires for other mention types.", + replaceWith = ReplaceWith("onMentionClick"), + level = DeprecationLevel.WARNING, + ) val onUserMentionClick: (User) -> Unit, val onLinkClick: ((Message, String) -> Unit)? = null, + val onMentionClick: (Mention) -> Unit = {}, ) /** @@ -878,17 +910,25 @@ public data class MessageRegularContentParams( * @param message The message containing the text. * @param currentUser The currently logged in user. * @param onLongItemClick Action invoked when a message is long-clicked. - * @param onUserMentionClick Action invoked when a user mention is clicked. + * @param onUserMentionClick Action invoked when a user mention is clicked. Prefer [onMentionClick] + * for new code: it also fires for other mention types. * @param modifier Modifier for styling. * @param onLinkClick Action invoked when a link in a message is clicked. + * @param onMentionClick Action invoked when any mention is clicked. */ public data class MessageTextContentParams( val message: Message, val currentUser: User?, val onLongItemClick: (Message) -> Unit, + @Deprecated( + message = "Use onMentionClick; it also fires for other mention types.", + replaceWith = ReplaceWith("onMentionClick"), + level = DeprecationLevel.WARNING, + ) val onUserMentionClick: (User) -> Unit, val modifier: Modifier = Modifier, val onLinkClick: ((Message, String) -> Unit)? = null, + val onMentionClick: (Mention) -> Unit = {}, ) /** @@ -946,13 +986,14 @@ public class SwipeToReplyContentParams * @param onValueChange Action invoked when the input value changes. * @param onAttachmentRemoved Action invoked when an attachment is removed. * @param onCancelAction Action invoked when the cancel button is clicked. - * @param onUserSelected Action invoked when a user is selected. + * @param onUserSelected Action invoked when a user mention is selected. * @param onCommandSelected Action invoked when a command is selected. * @param onAlsoSendToChannelSelected Action invoked when also-send-to-channel is changed. * @param onActiveCommandDismiss Action invoked when the active command is dismissed. * @param recordingActions The actions to control the audio recording. * @param onLinkPreviewClick Action invoked when a link preview is clicked. * @param onCancelLinkPreviewClick Action invoked when the cancel link preview button is clicked. + * @param onMentionSelected Action invoked when a non-user [Mention] is selected. */ public data class MessageComposerParams( val messageComposerState: MessageComposerState, @@ -967,6 +1008,11 @@ public data class MessageComposerParams( val onValueChange: (String) -> Unit = {}, val onAttachmentRemoved: (Attachment) -> Unit = {}, val onCancelAction: () -> Unit = {}, + @Deprecated( + message = "Use onMentionSelected, which receives every mention type.", + replaceWith = ReplaceWith("onMentionSelected"), + level = DeprecationLevel.WARNING, + ) val onUserSelected: (User) -> Unit = {}, val onCommandSelected: (Command) -> Unit = {}, val onAlsoSendToChannelSelected: (Boolean) -> Unit = {}, @@ -974,6 +1020,7 @@ public data class MessageComposerParams( val recordingActions: AudioRecordingActions = AudioRecordingActions.None, val onLinkPreviewClick: ((LinkPreview) -> Unit)? = null, val onCancelLinkPreviewClick: (() -> Unit)? = null, + val onMentionSelected: (Mention) -> Unit = {}, ) /** @@ -1004,6 +1051,50 @@ public data class MessageComposerUserSuggestionItemParams( val onUserSelected: (User) -> Unit, ) +/** + * Parameters for [ChatComponentFactory.MessageComposerSuggestionItem]. + * + * @param mention The non-user [Mention] for which the suggestion is rendered. + * @param onMentionSelected Action invoked when the mention is selected. + */ +public data class MessageComposerSuggestionItemParams( + val mention: Mention, + val onMentionSelected: (Mention) -> Unit, +) + +/** + * Parameters for [ChatComponentFactory.MessageComposerSuggestionItemLeadingContent]. + * + * @param mention The mention for which the leading content is rendered. + * @param modifier Modifier for styling. + */ +public data class MessageComposerSuggestionItemLeadingContentParams( + val mention: Mention, + val modifier: Modifier = Modifier, +) + +/** + * Parameters for [ChatComponentFactory.MessageComposerSuggestionItemCenterContent]. + * + * @param mention The mention for which the center content is rendered. + * @param modifier Modifier for styling. + */ +public data class MessageComposerSuggestionItemCenterContentParams( + val mention: Mention, + val modifier: Modifier = Modifier, +) + +/** + * Parameters for [ChatComponentFactory.MessageComposerSuggestionItemTrailingContent]. + * + * @param mention The mention for which the trailing content is rendered. + * @param modifier Modifier for styling. + */ +public data class MessageComposerSuggestionItemTrailingContentParams( + val mention: Mention, + val modifier: Modifier = Modifier, +) + /** * Parameters for [ChatComponentFactory.MessageComposerUserSuggestionItemLeadingContent]. * diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDesign.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDesign.kt index c592290770c..0bd354e551b 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDesign.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDesign.kt @@ -304,6 +304,33 @@ public object StreamDesign { /** Mention styling in chat messages. */ internal val chatTextMention: Color = chatTextLink + /** User-mention text color (`@`). */ + internal val chatTextMentionUser: Color = chatTextMention + + /** Broadcast-mention text color (`@channel`, `@here`). */ + internal val chatTextMentionBroadcast: Color = chatTextMention + + /** Role-mention text color (`@`). */ + internal val chatTextMentionRole: Color = chatTextMention + + /** Group-mention text color (`@`). */ + internal val chatTextMentionGroup: Color = chatTextMention + + /** Mention background fill. */ + internal val chatBgMention: Color = Color.Transparent + + /** User-mention background fill. */ + internal val chatBgMentionUser: Color = chatBgMention + + /** Broadcast-mention background fill. */ + internal val chatBgMentionBroadcast: Color = chatBgMention + + /** Role-mention background fill. */ + internal val chatBgMentionRole: Color = chatBgMention + + /** Group-mention background fill. */ + internal val chatBgMentionGroup: Color = chatBgMention + /** Reaction count text in chat. */ internal val chatTextReaction: Color = textSecondary diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageTextFormatter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageTextFormatter.kt index 27986a4c332..5ed9e38d334 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageTextFormatter.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageTextFormatter.kt @@ -54,6 +54,8 @@ public fun interface MessageTextFormatter { * @param typography The typography to use for styling. * @param colors The colors to use for styling. * @param textStyle The text style to use for styling. + * @param mentionColor Return [Color.Unspecified] (the default) to use per-type tokens from + * [colors]; return a specified color to apply it to every mention. * @param builder The builder to use for customizing the text. * @return The default implementation of [MessageTextFormatter]. * @@ -71,11 +73,12 @@ public fun interface MessageTextFormatter { textStyle: (isMine: Boolean, message: Message) -> TextStyle = { isMine, _ -> MessageStyling.textStyle(outgoing = isMine, typography, colors) }, linkStyle: (isMine: Boolean) -> TextStyle = { MessageStyling.linkStyle(typography, colors) }, - mentionColor: (isMine: Boolean) -> Color = { colors.chatTextMention }, + mentionColor: (isMine: Boolean) -> Color = { Color.Unspecified }, builder: AnnotatedMessageTextBuilder? = null, ): MessageTextFormatter { return DefaultMessageTextFormatter( autoTranslationEnabled = autoTranslationEnabled, + colors = colors, typography = typography, textStyle = textStyle, linkStyle = linkStyle, @@ -124,6 +127,7 @@ private class CompositeMessageTextFormatter( */ private class DefaultMessageTextFormatter( private val autoTranslationEnabled: Boolean, + private val colors: StreamDesign.Colors, private val typography: StreamDesign.Typography, private val textStyle: (isMine: Boolean, message: Message) -> TextStyle, private val linkStyle: (isMine: Boolean) -> TextStyle, @@ -149,18 +153,15 @@ private class DefaultMessageTextFormatter( else -> message.text } - val mentionedUserNames = message.mentionedUsers.map { it.name.ifEmpty { it.id } } val isMine = message.isMine(currentUser) val textColor = textStyle(isMine, message).color val linkStyle = linkStyle(isMine) - val mentionColor = mentionColor(isMine) return buildAnnotatedMessageText( text = displayedText, textColor = textColor, textFontStyle = typography.bodyDefault.fontStyle, linkStyle = linkStyle, - mentionsColor = mentionColor, - mentionedUserNames = mentionedUserNames, + mentions = message.collectTextMentions(colors = colors, textColorOverride = mentionColor(isMine)), builder = { builder?.invoke(this, message, currentUser) }, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.kt index 7bf1f90bd21..7e2d448d6bb 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.kt @@ -19,6 +19,7 @@ package io.getstream.chat.android.compose.ui.util import android.annotation.SuppressLint import android.text.util.Linkify import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle @@ -27,6 +28,9 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.core.util.PatternsCompat +import io.getstream.chat.android.compose.ui.theme.StreamDesign +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.mentionRegex import java.util.regex.Pattern internal typealias AnnotationTag = String @@ -42,9 +46,85 @@ internal const val AnnotationTagUrl: AnnotationTag = "URL" internal const val AnnotationTagEmail: AnnotationTag = "EMAIL" /** - * The tag used to annotate mentions in the message text. + * The tag used to annotate user mentions (`@`) in the message text. */ -internal const val AnnotationTagMention: AnnotationTag = "MENTION" +internal const val AnnotationTagUserMention: AnnotationTag = "MENTION" + +/** + * The tag used to annotate `@channel` mentions in the message text. + */ +internal const val AnnotationTagChannelMention: AnnotationTag = "CHANNEL_MENTION" + +/** + * The tag used to annotate `@here` mentions in the message text. + */ +internal const val AnnotationTagHereMention: AnnotationTag = "HERE_MENTION" + +/** + * The tag used to annotate role mentions (e.g. `@admin`) in the message text. The annotation + * value is the role name. + */ +internal const val AnnotationTagRoleMention: AnnotationTag = "ROLE_MENTION" + +/** + * The tag used to annotate user-group mentions (e.g. `@backendsupport`) in the message text. The + * annotation value is the group name. + */ +internal const val AnnotationTagGroupMention: AnnotationTag = "GROUP_MENTION" + +/** + * A single mention to highlight in a message's text. + * + * @property token The literal that follows the `@` in the text. + * @property annotationTag The annotation tag attached to every match. + */ +internal data class TextMention( + val token: String, + val annotationTag: AnnotationTag, + val color: Color, + val background: Color, +) + +/** + * Builds the list of [TextMention]s to highlight for this message, each pre-populated with its + * per-type color and background from [colors] (overridden by [textColorOverride] when specified). + */ +internal fun Message.collectTextMentions( + colors: StreamDesign.Colors, + textColorOverride: Color = Color.Unspecified, +): List = buildList { + fun add(token: String, tag: AnnotationTag) { + add( + TextMention( + token = token, + annotationTag = tag, + color = textColorOverride.takeOrElse { colors.mentionTextColorFor(tag) }, + background = colors.mentionBackgroundFor(tag), + ), + ) + } + mentionedUsers.forEach { user -> add(user.name.ifEmpty(user::id), AnnotationTagUserMention) } + if (mentionedChannel) add("channel", AnnotationTagChannelMention) + if (mentionedHere) add("here", AnnotationTagHereMention) + mentionedRoles.forEach { role -> add(role, AnnotationTagRoleMention) } + mentionedGroups.forEach { group -> add(group.name, AnnotationTagGroupMention) } +} + +internal fun StreamDesign.Colors.mentionTextColorFor(tag: AnnotationTag): Color = when (tag) { + AnnotationTagUserMention -> chatTextMentionUser + AnnotationTagChannelMention, AnnotationTagHereMention -> chatTextMentionBroadcast + AnnotationTagRoleMention -> chatTextMentionRole + AnnotationTagGroupMention -> chatTextMentionGroup + else -> chatTextMention +} + +internal fun StreamDesign.Colors.mentionBackgroundFor(tag: AnnotationTag): Color = when (tag) { + AnnotationTagUserMention -> chatBgMentionUser + AnnotationTagChannelMention, AnnotationTagHereMention -> chatBgMentionBroadcast + AnnotationTagRoleMention -> chatBgMentionRole + AnnotationTagGroupMention -> chatBgMentionGroup + else -> Color.Transparent +} /** * Builds an [AnnotatedString] from a given text, applying styles and annotations for links and mentions. @@ -54,8 +134,7 @@ internal const val AnnotationTagMention: AnnotationTag = "MENTION" * @param textColor The color to be applied to the regular text. * @param textFontStyle The font style to be applied to the regular text. * @param linkStyle The text style to be applied to links within the text. - * @param mentionsColor The color to be applied to mentions within the text. - * @param mentionedUserNames A list of usernames that are mentioned in the text. + * @param mentions The list of mentions to highlight in the text. * @param builder An optional lambda to apply additional styles or annotations. */ @SuppressLint("RestrictedApi") @@ -64,8 +143,7 @@ internal fun buildAnnotatedMessageText( textColor: Color, textFontStyle: FontStyle?, linkStyle: TextStyle, - mentionsColor: Color, - mentionedUserNames: List = emptyList(), + mentions: List = emptyList(), builder: (AnnotatedString.Builder).() -> Unit = {}, ): AnnotatedString { return buildAnnotatedString { @@ -97,11 +175,9 @@ internal fun buildAnnotatedMessageText( schemes = EMAIL_SCHEMES, textStyle = linkStyle, ) - tagUser( - text = text, - mentionsColor = mentionsColor, - mentionedUserNames = mentionedUserNames, - ) + mentions.forEach { mention -> + tagMention(text = text, mention = mention) + } // Finally, we apply any additional styling that was passed in. builder(this) @@ -215,33 +291,27 @@ private fun AnnotatedString.Builder.linkify( } } -private fun AnnotatedString.Builder.tagUser( +/** + * Tags every word-bounded `@` occurrence in [text] with [annotationTag] and styles it + * with [mentionsColor]. + */ +private fun AnnotatedString.Builder.tagMention( text: String, - mentionsColor: Color, - mentionedUserNames: List, + mention: TextMention, ) { - mentionedUserNames.forEach { userName -> - val start = text.indexOf(userName) - val end = start + userName.length - - if (start < 0) return@forEach - - // Backtrack one position to include the leading `@`. Clamp to 0 when the mention is at the - // start of the text and no `@` precedes it — otherwise the resulting -1 span start crashes - // TalkBack's spannable conversion with `setSpan (-1 ...) starts before 0`. - val styledStart = (start - 1).coerceAtLeast(0) - + if (mention.token.isEmpty()) return + val pattern = mentionRegex(mention.token) + pattern.findAll(text).forEach { match -> addStyle( - style = SpanStyle(color = mentionsColor), - start = styledStart, - end = end, + style = SpanStyle(color = mention.color, background = mention.background), + start = match.range.first, + end = match.range.last + 1, ) - addStringAnnotation( - tag = AnnotationTagMention, - annotation = userName, - start = styledStart, - end = end, + tag = mention.annotationTag, + annotation = mention.token, + start = match.range.first, + end = match.range.last + 1, ) } } diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/messages/MessageTextHelpersTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/messages/MessageTextHelpersTest.kt index 515e43078cb..c8c63d9989a 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/messages/MessageTextHelpersTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/messages/MessageTextHelpersTest.kt @@ -17,13 +17,19 @@ package io.getstream.chat.android.compose.ui.components.messages import androidx.compose.ui.text.AnnotatedString +import io.getstream.chat.android.compose.ui.util.AnnotationTagChannelMention import io.getstream.chat.android.compose.ui.util.AnnotationTagEmail -import io.getstream.chat.android.compose.ui.util.AnnotationTagMention +import io.getstream.chat.android.compose.ui.util.AnnotationTagGroupMention +import io.getstream.chat.android.compose.ui.util.AnnotationTagHereMention +import io.getstream.chat.android.compose.ui.util.AnnotationTagRoleMention import io.getstream.chat.android.compose.ui.util.AnnotationTagUrl +import io.getstream.chat.android.compose.ui.util.AnnotationTagUserMention import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User import io.getstream.chat.android.randomMessage import io.getstream.chat.android.randomUser +import io.getstream.chat.android.randomUserGroup +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention import org.amshove.kluent.`should be equal to` import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -82,7 +88,7 @@ internal class MessageTextHelpersTest { @Test fun `hasInteractiveAt returns true for a mention annotation`() { val annotations = listOf( - AnnotatedString.Range(item = "user", start = 0, end = 5, tag = AnnotationTagMention), + AnnotatedString.Range(item = "user", start = 0, end = 5, tag = AnnotationTagUserMention), ) annotations.hasInteractiveAt(offset = 2) `should be equal to` true @@ -190,7 +196,7 @@ internal class MessageTextHelpersTest { val onLinkClick = mock<(Message, String) -> Unit>() val message = randomMessage(text = "@alice", mentionedUsers = listOf(mentioned)) val annotations = listOf( - AnnotatedString.Range(item = "alice", start = 0, end = 6, tag = AnnotationTagMention), + AnnotatedString.Range(item = "alice", start = 0, end = 6, tag = AnnotationTagUserMention), ) handleAnnotationClick( @@ -206,12 +212,22 @@ internal class MessageTextHelpersTest { verify(onLinkClick, never()).invoke(any(), any()) } - @Test - fun `handleAnnotationClick is a no-op when the mentioned user is not in the message`() { + @ParameterizedTest + @MethodSource("missingEntityCases") + fun `handleAnnotationClick is a no-op when the mentioned entity is missing from the message`( + message: Message, + annotationTag: String, + annotationItem: String, + ) { + val onMentionClick = mock<(Mention) -> Unit>() val onUserMentionClick = mock<(User) -> Unit>() - val message = randomMessage(text = "@bob", mentionedUsers = emptyList()) val annotations = listOf( - AnnotatedString.Range(item = "bob", start = 0, end = 4, tag = AnnotationTagMention), + AnnotatedString.Range( + item = annotationItem, + start = 0, + end = annotationItem.length + 1, + tag = annotationTag, + ), ) handleAnnotationClick( @@ -221,9 +237,42 @@ internal class MessageTextHelpersTest { onLinkClick = null, onUserMentionClick = onUserMentionClick, fallback = {}, + onMentionClick = onMentionClick, ) - verify(onUserMentionClick, never()).invoke(any()) + verify(onMentionClick, never())(any()) + verify(onUserMentionClick, never())(any()) + } + + @ParameterizedTest + @MethodSource("mentionDispatchCases") + fun `handleAnnotationClick fires onMentionClick with the right Mention for each kind`( + message: Message, + annotationTag: String, + annotationItem: String, + expectedMention: Mention, + ) { + val onMentionClick = mock<(Mention) -> Unit>() + val annotations = listOf( + AnnotatedString.Range( + item = annotationItem, + start = 0, + end = annotationItem.length + 1, + tag = annotationTag, + ), + ) + + handleAnnotationClick( + annotations = annotations, + position = 1, + message = message, + onLinkClick = null, + onUserMentionClick = {}, + fallback = {}, + onMentionClick = onMentionClick, + ) + + verify(onMentionClick)(expectedMention) } @Test @@ -319,11 +368,37 @@ internal class MessageTextHelpersTest { companion object { + @JvmStatic + fun mentionDispatchCases(): List { + val group = randomUserGroup(name = "backend") + return listOf( + Arguments.of(randomMessage(), AnnotationTagChannelMention, "channel", Mention.Channel), + Arguments.of(randomMessage(), AnnotationTagHereMention, "here", Mention.Here), + Arguments.of(randomMessage(), AnnotationTagRoleMention, "admin", Mention.Role("admin")), + Arguments.of( + randomMessage(mentionedGroups = listOf(group)), + AnnotationTagGroupMention, + "backend", + Mention.Group(group), + ), + ) + } + + @JvmStatic + fun missingEntityCases(): List = listOf( + Arguments.of(randomMessage(mentionedUsers = emptyList()), AnnotationTagUserMention, "bob"), + Arguments.of(randomMessage(mentionedGroups = emptyList()), AnnotationTagGroupMention, "ghost"), + ) + @JvmStatic fun interactiveTagCases(): List = listOf( Arguments.of(AnnotationTagUrl, true), Arguments.of(AnnotationTagEmail, true), - Arguments.of(AnnotationTagMention, true), + Arguments.of(AnnotationTagUserMention, true), + Arguments.of(AnnotationTagChannelMention, true), + Arguments.of(AnnotationTagHereMention, true), + Arguments.of(AnnotationTagRoleMention, true), + Arguments.of(AnnotationTagGroupMention, true), Arguments.of("STYLE", false), Arguments.of("UNKNOWN", false), Arguments.of("", false), diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/TextUtilsKtTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/TextUtilsKtTest.kt index 220e60b5599..5ba4df31482 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/TextUtilsKtTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/TextUtilsKtTest.kt @@ -20,6 +20,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import io.getstream.chat.android.compose.ui.theme.StreamDesign +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.randomMessage +import io.getstream.chat.android.randomUser +import io.getstream.chat.android.randomUserGroup import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -46,7 +51,14 @@ internal class TextUtilsKtTest { val textFontStyle = FontStyle.Normal val linkStyle = TextStyle(color = Color.Blue) val mentionsColor = Color.Red - val mentionedUserNames = listOf("John") + val mentions = listOf( + TextMention( + token = "John", + annotationTag = AnnotationTagUserMention, + color = mentionsColor, + background = Color.Unspecified, + ), + ) // When val result = buildAnnotatedMessageText( @@ -54,8 +66,7 @@ internal class TextUtilsKtTest { textColor = textColor, textFontStyle = textFontStyle, linkStyle = linkStyle, - mentionsColor = mentionsColor, - mentionedUserNames = mentionedUserNames, + mentions = mentions, ) // Then @@ -76,13 +87,85 @@ internal class TextUtilsKtTest { assertEquals(63, emailAnnotations[0].end) // Verify mention annotation - val mentionAnnotations = result.getStringAnnotations(AnnotationTagMention, 0, text.length) + val mentionAnnotations = result.getStringAnnotations(AnnotationTagUserMention, 0, text.length) assertEquals(1, mentionAnnotations.size) assertEquals("John", mentionAnnotations[0].item) assertEquals(71, mentionAnnotations[0].start) // Position of "@John" (includes @) assertEquals(76, mentionAnnotations[0].end) } + @Test + fun `collectTextMentions emits no entries for a message without mentions`() { + val message = randomMessage( + mentionedUsers = emptyList(), + mentionedChannel = false, + mentionedHere = false, + mentionedRoles = emptyList(), + mentionedGroups = emptyList(), + ) + + val result = message.collectTextMentions(colors = StreamDesign.Colors.default()) + + assertEquals(emptyList(), result) + } + + @Test + fun `collectTextMentions emits a user mention token from name`() { + val message = randomMessage( + mentionedUsers = listOf(randomUser(id = "u1", name = "John")), + mentionedChannel = false, + mentionedHere = false, + ) + + val result = message.collectTextMentions(colors = StreamDesign.Colors.default()) + + assertEquals(1, result.size) + assertEquals("John", result[0].token) + assertEquals(AnnotationTagUserMention, result[0].annotationTag) + } + + @Test + fun `collectTextMentions falls back to user id when name is empty`() { + val message = randomMessage( + mentionedUsers = listOf(randomUser(id = "u1", name = "")), + mentionedChannel = false, + mentionedHere = false, + ) + + val result = message.collectTextMentions(colors = StreamDesign.Colors.default()) + + assertEquals(1, result.size) + assertEquals("u1", result[0].token) + assertEquals(AnnotationTagUserMention, result[0].annotationTag) + } + + @ParameterizedTest + @MethodSource("nonUserMentionCases") + fun `collectTextMentions maps each non-user mention kind to its token and tag`( + message: Message, + expectedToken: String, + expectedTag: AnnotationTag, + ) { + val result = message.collectTextMentions(colors = StreamDesign.Colors.default()) + + assertEquals(1, result.size) + assertEquals(expectedToken, result[0].token) + assertEquals(expectedTag, result[0].annotationTag) + } + + @Test + fun `collectTextMentions applies textColorOverride when specified`() { + val message = randomMessage(mentionedChannel = true, mentionedHere = false, mentionedUsers = emptyList()) + val override = Color.Red + + val result = message.collectTextMentions( + colors = StreamDesign.Colors.default(), + textColorOverride = override, + ) + + assertEquals(override, result[0].color) + } + @Test fun `buildAnnotatedInputText should annotate URLs and emails correctly`() { // Given @@ -119,6 +202,27 @@ internal class TextUtilsKtTest { companion object { + @JvmStatic + fun nonUserMentionCases(): List { + val empty = randomMessage( + mentionedUsers = emptyList(), + mentionedChannel = false, + mentionedHere = false, + mentionedRoles = emptyList(), + mentionedGroups = emptyList(), + ) + return listOf( + Arguments.of(empty.copy(mentionedChannel = true), "channel", AnnotationTagChannelMention), + Arguments.of(empty.copy(mentionedHere = true), "here", AnnotationTagHereMention), + Arguments.of(empty.copy(mentionedRoles = listOf("admin")), "admin", AnnotationTagRoleMention), + Arguments.of( + empty.copy(mentionedGroups = listOf(randomUserGroup(name = "backend"))), + "backend", + AnnotationTagGroupMention, + ), + ) + } + @JvmStatic fun urlArguments() = listOf( Arguments.of( diff --git a/stream-chat-android-markdown-transformer/src/main/kotlin/io/getstream/chat/android/markdown/MarkdownTextTransformer.kt b/stream-chat-android-markdown-transformer/src/main/kotlin/io/getstream/chat/android/markdown/MarkdownTextTransformer.kt index 0a39ee85ac1..bf9d27584d9 100644 --- a/stream-chat-android-markdown-transformer/src/main/kotlin/io/getstream/chat/android/markdown/MarkdownTextTransformer.kt +++ b/stream-chat-android-markdown-transformer/src/main/kotlin/io/getstream/chat/android/markdown/MarkdownTextTransformer.kt @@ -47,6 +47,6 @@ public class MarkdownTextTransformer @JvmOverloads constructor( override fun transformAndApply(textView: TextView, messageItem: MessageListItem.MessageItem) { val displayedText = getDisplayedText(messageItem) markwon.setMarkdown(textView, displayedText.fixItalicAtEnd()) - Linkify.addLinks(textView, messageItem.message.mentionedUsers) + Linkify.addLinks(textView, messageItem.message) } } diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index d99c0b19ca0..78a7c04bfad 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -758,6 +758,48 @@ public abstract interface class io/getstream/chat/android/ui/common/feature/mess public abstract fun getType ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType; } +public final class io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Channel : io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention { + public static final field $stable I + public static final field INSTANCE Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Channel; + public fun getDisplay ()Ljava/lang/String; + public fun getType ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType; +} + +public final class io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Group : io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention { + public static final field $stable I + public fun (Lio/getstream/chat/android/models/UserGroup;)V + public final fun component1 ()Lio/getstream/chat/android/models/UserGroup; + public final fun copy (Lio/getstream/chat/android/models/UserGroup;)Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Group; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Group;Lio/getstream/chat/android/models/UserGroup;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Group; + public fun equals (Ljava/lang/Object;)Z + public fun getDisplay ()Ljava/lang/String; + public final fun getGroup ()Lio/getstream/chat/android/models/UserGroup; + public fun getType ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Here : io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention { + public static final field $stable I + public static final field INSTANCE Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Here; + public fun getDisplay ()Ljava/lang/String; + public fun getType ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType; +} + +public final class io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Role : io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention { + public static final field $stable I + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Role; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Role;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$Role; + public fun equals (Ljava/lang/Object;)Z + public fun getDisplay ()Ljava/lang/String; + public final fun getRole ()Ljava/lang/String; + public fun getType ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention$User : io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention { public static final field $stable I public fun (Lio/getstream/chat/android/models/User;)V @@ -786,6 +828,10 @@ public final class io/getstream/chat/android/ui/common/feature/messages/composer } public final class io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType$Companion { + public final fun getChannel ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType; + public final fun getGroup ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType; + public final fun getHere ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType; + public final fun getRole ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType; public final fun getUser ()Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionType; } diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention.kt index 86b1e4b3ab0..d109939754b 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention.kt @@ -32,13 +32,33 @@ public data class MentionType(public val value: String) { * Predefined mention type for user mentions (ex. "@John Doe"). */ public val user: MentionType = MentionType("user") + + /** + * Predefined mention type for `@channel` mentions, notifying every channel member. + */ + public val channel: MentionType = MentionType("channel") + + /** + * Predefined mention type for `@here` mentions, notifying online channel members. + */ + public val here: MentionType = MentionType("here") + + /** + * Predefined mention type for role mentions (e.g. `@admin`, `@moderator`). + */ + public val role: MentionType = MentionType("role") + + /** + * Predefined mention type for user-group mentions (e.g. `@backendsupport`). + */ + public val group: MentionType = MentionType("group") } } /** * Represents a mention token inside the message composer. * - * By default, only user mentions are supported. + * Built-in types are [User], [Channel], [Here], [Role], and [Group]. * You can extend this interface to define custom mentions if needed. */ public interface Mention { @@ -58,7 +78,7 @@ public interface Mention { public val display: String /** - * Represents a user mention inside the message composer. + * A user mention. * * @param user The user being mentioned. */ @@ -66,4 +86,40 @@ public interface Mention { override val type: MentionType = MentionType.user override val display: String = user.name.ifEmpty { user.id } } + + /** + * An `@channel` mention: notifies every channel member. + */ + public object Channel : Mention { + override val type: MentionType = MentionType.channel + override val display: String = "channel" + } + + /** + * An `@here` mention: notifies online channel members. + */ + public object Here : Mention { + override val type: MentionType = MentionType.here + override val display: String = "here" + } + + /** + * A role mention (e.g. `@admin`). + * + * @param role The role name. + */ + public data class Role(public val role: String) : Mention { + override val type: MentionType = MentionType.role + override val display: String = role + } + + /** + * A user-group mention. + * + * @param group The group being mentioned. + */ + public data class Group(public val group: io.getstream.chat.android.models.UserGroup) : Mention { + override val type: MentionType = MentionType.group + override val display: String = group.name + } } diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionRegex.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionRegex.kt new file mode 100644 index 00000000000..57edf683084 --- /dev/null +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/mention/MentionRegex.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.ui.common.feature.messages.composer.mention + +import io.getstream.chat.android.core.internal.InternalStreamChatApi + +/** + * Builds the regex used to locate a mention token (`@`) inside message text. + * + * The boundaries use Unicode-aware lookaround, because Java's default word-boundary classes only + * recognize ASCII `[A-Za-z0-9_]`. The lookbehind `(? ranges={2}") + @MethodSource("provideMatchCases") + fun `mentionRegex finds the expected ranges`( + display: String, + text: String, + expectedRanges: List, + ) { + val matches = mentionRegex(display).findAll(text).map { it.range }.toList() + + matches `should be equal to` expectedRanges + } + + companion object { + + @JvmStatic + @Suppress("LongMethod") + fun provideMatchCases(): List = listOf( + // Basic match preceded by space. + Arguments.of("alice", "hello @alice!", listOf(6..11)), + // Match at the start of the string. + Arguments.of("alice", "@alice hi", listOf(0..5)), + // Match at the end of the string. + Arguments.of("alice", "hello @alice", listOf(6..11)), + // Match as the entire string. + Arguments.of("alice", "@alice", listOf(0..5)), + // Two consecutive mentions of the same display separated by space. + Arguments.of("alice", "@alice @alice", listOf(0..5, 7..12)), + // ASCII suffix prevents a false match (regression for "@Chewbacca" in "@Chewbaccaa"). + Arguments.of("Chewbacca", "W @Chewbacca @Chewbaccaa", listOf(2..11)), + // ASCII prefix prevents a false match (regression for Compose missing leading boundary). + Arguments.of("alice", "foo@alice bar", emptyList()), + // Non-ASCII letter after the display name must NOT be treated as a word boundary. + Arguments.of("alice", "@aliceé", emptyList()), + Arguments.of("Chewbacca", "@Chewbaccaé", emptyList()), + // Non-ASCII letter before the display name must NOT allow a mid-word match. + Arguments.of("alice", "José@alice", emptyList()), + // Non-ASCII letter as separator is treated like any other non-word boundary char would be: + // a letter on either side means "still inside a word", so no match. + Arguments.of("alice", "Я@alice", emptyList()), + // Punctuation neighbours are fine on both sides. + Arguments.of("alice", "(@alice)", listOf(1..6)), + Arguments.of("alice", "@alice.", listOf(0..5)), + Arguments.of("alice", "@alice,next", listOf(0..5)), + // Display name containing a space (e.g. "@John Doe"). + Arguments.of("John Doe", "hi @John Doe!", listOf(3..11)), + // Display name ending in a non-word character (regression for `\b` at the tail). + Arguments.of("admins!", "ping @admins! now", listOf(5..12)), + // Underscore is treated as a word character — adjacent underscore blocks the match. + Arguments.of("alice", "@alice_smith", emptyList()), + // Digit suffix is a word character — blocks the match. + Arguments.of("alice", "@alice1", emptyList()), + // The match must be case-sensitive. + Arguments.of("Alice", "@alice", emptyList()), + // Special regex characters in the display name must be matched literally. + Arguments.of("a.b", "hi @a.b!", listOf(3..6)), + Arguments.of("a.b", "hi @aXb!", emptyList()), + // @channel-style mentions. + Arguments.of("channel", "hey @channel", listOf(4..11)), + Arguments.of("here", "@here and @herein", listOf(0..4)), + ) + } +} 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 59f5acb4b30..6c1acdef86e 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 @@ -2315,6 +2315,7 @@ public final class io/getstream/chat/android/ui/feature/messages/list/MessageLis public final fun setOnEnterThreadListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnEnterThreadListener;)V public final fun setOnLinkClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnLinkClickListener;)V public final fun setOnMentionClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMentionClickListener;)V + public final fun setOnMentionTokenClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMentionTokenClickListener;)V public final fun setOnMessageClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageClickListener;)V public final fun setOnMessageLongClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageLongClickListener;)V public final fun setOnMessageRetryListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageRetryListener;)V @@ -2493,6 +2494,10 @@ public abstract interface class io/getstream/chat/android/ui/feature/messages/li public abstract fun onMentionClick (Lio/getstream/chat/android/models/User;)Z } +public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMentionTokenClickListener { + public abstract fun onMentionClick (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;)Z +} + public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageClickListener { public abstract fun onMessageClick (Lio/getstream/chat/android/models/Message;)Z } @@ -3058,6 +3063,7 @@ public abstract interface class io/getstream/chat/android/ui/feature/messages/li public abstract fun getGiphySendListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnGiphySendListener; public abstract fun getLinkClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnLinkClickListener; public abstract fun getMentionClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMentionClickListener; + public abstract fun getMentionTokenClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMentionTokenClickListener; public abstract fun getMessageClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageClickListener; public abstract fun getMessageLongClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageLongClickListener; public abstract fun getMessageRetryListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageRetryListener; diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt index 71b46e60cea..57b51087694 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt @@ -56,6 +56,7 @@ import io.getstream.chat.android.models.Reaction import io.getstream.chat.android.models.User import io.getstream.chat.android.ui.ChatUI 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.feature.messages.list.MessageListController import io.getstream.chat.android.ui.common.helper.DateFormatter import io.getstream.chat.android.ui.common.state.messages.BlockUser @@ -579,6 +580,9 @@ public class MessageListView : ConstraintLayout { private val defaultMentionClickListener = OnMentionClickListener { false } + private val defaultMentionTokenClickListener = OnMentionTokenClickListener { + false + } private val defaultGiphySendListener = OnGiphySendListener { action -> giphySendHandler.onSendGiphy(action) @@ -1579,10 +1583,18 @@ public class MessageListView : ConstraintLayout { } /** - * Sets the mention click listener to be used by MessageListView. + * Sets the user-mention click listener used by [MessageListView]. + * + * Kept for backward compatibility; only fires for user mention taps. New code should prefer + * [setOnMentionTokenClickListener], which also fires for other mention types. * * @param listener The listener to use. If null, the default will be used instead. */ + @Deprecated( + message = "Use setOnMentionTokenClickListener; it also fires for other mention types.", + replaceWith = ReplaceWith("setOnMentionTokenClickListener"), + level = DeprecationLevel.WARNING, + ) public fun setOnMentionClickListener(listener: OnMentionClickListener?) { if (listener == null) { listenerContainer.mentionClickListener = defaultMentionClickListener @@ -1593,6 +1605,24 @@ public class MessageListView : ConstraintLayout { } } + /** + * Sets the mention-token click listener used by [MessageListView]. + * + * Unlike [setOnMentionClickListener], this listener fires for every [Mention] subtype and is + * the canonical hook for handling mention taps in rendered message text. + * + * @param listener The listener to use. If null, the default will be used instead. + */ + public fun setOnMentionTokenClickListener(listener: OnMentionTokenClickListener?) { + if (listener == null) { + listenerContainer.mentionTokenClickListener = defaultMentionTokenClickListener + } else { + listenerContainer.mentionTokenClickListener = OnMentionTokenClickListener { mention -> + listener.onMentionClick(mention) || defaultMentionTokenClickListener.onMentionClick(mention) + } + } + } + /** * Sets the link click listener to be used by MessageListView. * @@ -2153,10 +2183,22 @@ public class MessageListView : ConstraintLayout { public fun onUserClick(user: User): Boolean } + @Deprecated( + message = "Use OnMentionTokenClickListener; it also fires for other mention types.", + replaceWith = ReplaceWith("OnMentionTokenClickListener"), + level = DeprecationLevel.WARNING, + ) public fun interface OnMentionClickListener { public fun onMentionClick(user: User): Boolean } + /** + * Click listener invoked when any mention is tapped inside rendered message text. + */ + public fun interface OnMentionTokenClickListener { + public fun onMentionClick(mention: Mention): Boolean + } + public fun interface OnReactionViewClickListener { public fun onReactionViewClick(message: Message): Boolean } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListeners.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListeners.kt index 0e5b8ff9650..3a6b83247d7 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListeners.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListeners.kt @@ -22,6 +22,7 @@ import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAtta import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnGiphySendListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnLinkClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMentionClickListener +import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMentionTokenClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageLongClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageRetryListener @@ -47,7 +48,14 @@ public sealed interface MessageListListeners { public val attachmentDownloadClickListener: OnAttachmentDownloadClickListener public val reactionViewClickListener: OnReactionViewClickListener public val userClickListener: OnUserClickListener + + @Deprecated( + message = "Use mentionTokenClickListener; it also fires for other mention types.", + replaceWith = ReplaceWith("mentionTokenClickListener"), + level = DeprecationLevel.WARNING, + ) public val mentionClickListener: OnMentionClickListener + public val mentionTokenClickListener: OnMentionTokenClickListener public val giphySendListener: OnGiphySendListener public val linkClickListener: OnLinkClickListener public val unreadLabelReachedListener: OnUnreadLabelReachedListener diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenersImpl.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenersImpl.kt index fa8d37cd778..25332d61c1e 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenersImpl.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenersImpl.kt @@ -22,6 +22,7 @@ import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAtta import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnGiphySendListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnLinkClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMentionClickListener +import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMentionTokenClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageLongClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageRetryListener @@ -54,6 +55,7 @@ internal class MessageListListenersImpl( reactionViewClickListener: OnReactionViewClickListener = OnReactionViewClickListener(EmptyFunctions.ONE_PARAM), userClickListener: OnUserClickListener = OnUserClickListener(EmptyFunctions.ONE_PARAM), mentionClickListener: OnMentionClickListener = OnMentionClickListener(EmptyFunctions.ONE_PARAM), + mentionTokenClickListener: OnMentionTokenClickListener = OnMentionTokenClickListener(EmptyFunctions.ONE_PARAM), giphySendListener: OnGiphySendListener = OnGiphySendListener(EmptyFunctions.ONE_PARAM), linkClickListener: OnLinkClickListener = OnLinkClickListener(EmptyFunctions.ONE_PARAM), onUnreadLabelReachedListener: OnUnreadLabelReachedListener = OnUnreadLabelReachedListener { }, @@ -142,6 +144,11 @@ internal class MessageListListenersImpl( } } + @Deprecated( + message = "Use mentionTokenClickListener; it also fires for other mention types.", + replaceWith = ReplaceWith("mentionTokenClickListener"), + level = DeprecationLevel.WARNING, + ) override var mentionClickListener: OnMentionClickListener by ListenerDelegate( mentionClickListener, ) { realListener -> @@ -150,6 +157,14 @@ internal class MessageListListenersImpl( } } + override var mentionTokenClickListener: OnMentionTokenClickListener by ListenerDelegate( + mentionTokenClickListener, + ) { realListener -> + OnMentionTokenClickListener { mention -> + realListener().onMentionClick(mention) + } + } + override var giphySendListener: OnGiphySendListener by ListenerDelegate( giphySendListener, ) { realListener -> diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/CustomAttachmentsViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/CustomAttachmentsViewHolder.kt index 274548cdf7a..ae539d7ccbc 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/CustomAttachmentsViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/CustomAttachmentsViewHolder.kt @@ -30,6 +30,7 @@ import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.att import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.InnerAttachmentViewHolder import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.Decorator import io.getstream.chat.android.ui.feature.messages.list.internal.LongClickFriendlyLinkMovementMethod +import io.getstream.chat.android.ui.feature.messages.list.internal.dispatchMentionClick import io.getstream.chat.android.ui.helper.transformer.ChatMessageTextTransformer import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater @@ -141,7 +142,7 @@ public class CustomAttachmentsViewHolder internal constructor( textView = messageText, longClickTarget = messageContainer, onLinkClicked = container.linkClickListener::onLinkClick, - onMentionClicked = container.mentionClickListener::onMentionClick, + onMentionClicked = container::dispatchMentionClick, ) } } @@ -156,7 +157,7 @@ public class CustomAttachmentsViewHolder internal constructor( textView = binding.messageText, longClickTarget = binding.messageContainer, onLinkClicked = container.linkClickListener::onLinkClick, - onMentionClicked = container.mentionClickListener::onMentionClick, + onMentionClicked = container::dispatchMentionClick, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/FileAttachmentsViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/FileAttachmentsViewHolder.kt index bda57051962..3aa25bb64f5 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/FileAttachmentsViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/FileAttachmentsViewHolder.kt @@ -31,6 +31,7 @@ import io.getstream.chat.android.ui.feature.messages.list.adapter.view.internal. import io.getstream.chat.android.ui.feature.messages.list.adapter.view.internal.AttachmentLongClickListener import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.Decorator import io.getstream.chat.android.ui.feature.messages.list.internal.LongClickFriendlyLinkMovementMethod +import io.getstream.chat.android.ui.feature.messages.list.internal.dispatchMentionClick import io.getstream.chat.android.ui.helper.transformer.ChatMessageTextTransformer import io.getstream.chat.android.ui.utils.extensions.dpToPx import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater @@ -149,7 +150,7 @@ public class FileAttachmentsViewHolder internal constructor( textView = binding.messageText, longClickTarget = binding.messageContainer, onLinkClicked = listenerContainer.linkClickListener::onLinkClick, - onMentionClicked = listenerContainer.mentionClickListener::onMentionClick, + onMentionClicked = listenerContainer::dispatchMentionClick, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/LinkAttachmentsViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/LinkAttachmentsViewHolder.kt index 55e91c2297d..c361e28e210 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/LinkAttachmentsViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/LinkAttachmentsViewHolder.kt @@ -29,6 +29,7 @@ import io.getstream.chat.android.ui.feature.messages.list.adapter.MessageListLis import io.getstream.chat.android.ui.feature.messages.list.adapter.internal.DecoratedBaseMessageItemViewHolder import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.Decorator import io.getstream.chat.android.ui.feature.messages.list.internal.LongClickFriendlyLinkMovementMethod +import io.getstream.chat.android.ui.feature.messages.list.internal.dispatchMentionClick import io.getstream.chat.android.ui.helper.transformer.ChatMessageTextTransformer import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater @@ -135,7 +136,7 @@ public class LinkAttachmentsViewHolder internal constructor( textView = binding.messageText, longClickTarget = binding.messageContainer, onLinkClicked = container.linkClickListener::onLinkClick, - onMentionClicked = container.mentionClickListener::onMentionClick, + onMentionClicked = container::dispatchMentionClick, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MediaAttachmentsViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MediaAttachmentsViewHolder.kt index f68d4d3af64..eb92d7b5b04 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MediaAttachmentsViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MediaAttachmentsViewHolder.kt @@ -36,6 +36,7 @@ import io.getstream.chat.android.ui.feature.messages.list.adapter.view.internal. import io.getstream.chat.android.ui.feature.messages.list.adapter.view.internal.AttachmentLongClickListener import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.Decorator import io.getstream.chat.android.ui.feature.messages.list.internal.LongClickFriendlyLinkMovementMethod +import io.getstream.chat.android.ui.feature.messages.list.internal.dispatchMentionClick import io.getstream.chat.android.ui.helper.transformer.ChatMessageTextTransformer import io.getstream.chat.android.ui.utils.extensions.dpToPx import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater @@ -215,7 +216,7 @@ public class MediaAttachmentsViewHolder internal constructor( textView = binding.messageText, longClickTarget = binding.messageContainer, onLinkClicked = container.linkClickListener::onLinkClick, - onMentionClicked = container.mentionClickListener::onMentionClick, + onMentionClicked = container::dispatchMentionClick, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MessagePlainTextViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MessagePlainTextViewHolder.kt index dc063ccad57..d72503ee6b3 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MessagePlainTextViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MessagePlainTextViewHolder.kt @@ -27,6 +27,7 @@ import io.getstream.chat.android.ui.feature.messages.list.adapter.MessageListLis import io.getstream.chat.android.ui.feature.messages.list.adapter.internal.DecoratedBaseMessageItemViewHolder import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.Decorator import io.getstream.chat.android.ui.feature.messages.list.internal.LongClickFriendlyLinkMovementMethod +import io.getstream.chat.android.ui.feature.messages.list.internal.dispatchMentionClick import io.getstream.chat.android.ui.helper.transformer.ChatMessageTextTransformer import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater @@ -75,7 +76,7 @@ public class MessagePlainTextViewHolder internal constructor( textView = messageText, longClickTarget = messageContainer, onLinkClicked = container.linkClickListener::onLinkClick, - onMentionClicked = container.mentionClickListener::onMentionClick, + onMentionClicked = container::dispatchMentionClick, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt index 8b4627887de..1e4b1484b8f 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt @@ -20,7 +20,8 @@ import android.text.method.LinkMovementMethod import android.view.View import android.widget.TextView import androidx.core.widget.doAfterTextChanged -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.list.adapter.MessageListListeners import io.getstream.chat.android.ui.feature.messages.list.internal.LongClickFriendlyLinkMovementMethod.Companion.set import io.getstream.chat.android.ui.utils.TextViewLinkHandler import io.getstream.chat.android.ui.utils.shouldConsumeLongTap @@ -37,7 +38,7 @@ internal class LongClickFriendlyLinkMovementMethod private constructor( private val textView: TextView, private val longClickTarget: View, private val onLinkClicked: (url: String) -> Unit, - private val onUserClicked: (user: User) -> Unit, + private val onMentionClicked: (mention: Mention) -> Unit, ) : TextViewLinkHandler() { private var isLongClick = false @@ -68,9 +69,9 @@ internal class LongClickFriendlyLinkMovementMethod private constructor( return false } - override fun onUserClick(user: User) { + override fun onMentionClick(mention: Mention) { if (checkLongClick()) return - onUserClicked(user) + onMentionClicked(mention) } companion object { @@ -78,9 +79,23 @@ internal class LongClickFriendlyLinkMovementMethod private constructor( textView: TextView, longClickTarget: View, onLinkClicked: (url: String) -> Unit, - onMentionClicked: (user: User) -> Unit, + onMentionClicked: (mention: Mention) -> Unit, ) { LongClickFriendlyLinkMovementMethod(textView, longClickTarget, onLinkClicked, onMentionClicked) } } } + +/** + * Dispatches a mention-text click to the canonical [MessageListListeners.mentionTokenClickListener] + * and, for user mentions, additionally to the deprecated user-only + * [MessageListListeners.mentionClickListener] for backward compatibility. Mirrors the Compose + * counterpart in `MessageText` which always fires both callbacks. + */ +internal fun MessageListListeners.dispatchMentionClick(mention: Mention) { + mentionTokenClickListener.onMentionClick(mention) + if (mention is Mention.User) { + @Suppress("DEPRECATION") + mentionClickListener.onMentionClick(mention.user) + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/transformer/AutoLinkableTextTransformer.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/transformer/AutoLinkableTextTransformer.kt index b8293dd433f..d58b7c193c7 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/transformer/AutoLinkableTextTransformer.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/transformer/AutoLinkableTextTransformer.kt @@ -31,6 +31,6 @@ public class AutoLinkableTextTransformer( override fun transformAndApply(textView: TextView, messageItem: MessageListItem.MessageItem) { transformer(textView, messageItem) - Linkify.addLinks(textView, messageItem.message.mentionedUsers) + Linkify.addLinks(textView, messageItem.message) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt index 45999f570ab..20304369f28 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt @@ -27,12 +27,16 @@ import android.text.util.Linkify import android.widget.TextView import androidx.core.util.PatternsCompat import io.getstream.chat.android.core.internal.InternalStreamChatApi -import io.getstream.chat.android.models.User +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.mentionRegex import java.util.Locale import java.util.regex.Pattern /** - * Linkify links the part of the text based on matching pattern. + * Utility for linkifying message text: scans a [TextView] for URLs, email addresses, and + * mention tokens (`@user`, `@channel`, `@here`, role mentions) referenced by a [Message], and + * applies clickable spans for each. * * This class is a simplified version of [Linkify] and differs only in one following way * It doesn't remove any existing URLSpan from the Spannable. @@ -41,68 +45,61 @@ import java.util.regex.Pattern public object Linkify { /** - * Scans the provided TextView and turns all occurrences - * of the link types into clickable links. - * If matches are found the movement method for the TextView is set to - * LinkMovementMethod. + * Scans the provided TextView and turns URLs, email addresses and every mention token + * present in [message] into clickable links. * * NOTE: Because this implementation doesn't remove existing URLSpan, * make sure it is not repeatedly called on same text. * - * @param textView TextView whose text is to be marked-up with links. - * @param mentionableUsers List of users to be marked-up with links. + * @param textView TextView whose text will be scanned and marked up with clickable spans. + * @param message Message providing the mention tokens to linkify alongside URLs and email addresses. */ - public fun addLinks( - textView: TextView, - mentionableUsers: List, - ) { - val t: CharSequence = textView.text + public fun addLinks(textView: TextView, message: Message) { + val original = textView.text + val spannable = original as? Spannable ?: SpannableString.valueOf(original) + if (!applySpans(spannable, message)) return + addLinkMovementMethod(textView) + if (spannable !== original) textView.text = spannable + } - if (t is Spannable) { - if (addLinks(t, mentionableUsers)) { - addLinkMovementMethod(textView) - } - } else { - val s = SpannableString.valueOf(t) - if (addLinks(s, mentionableUsers)) { - addLinkMovementMethod(textView) - textView.text = s - } - } + private fun applySpans(spannable: Spannable, message: Message): Boolean = + gatherSpecs(spannable, message) + .pruneOverlaps(spannable) + .onEach { spannable.setSpan(it.span, it.start, it.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } + .isNotEmpty() + + private fun gatherSpecs(spannable: Spannable, message: Message): List = buildList { + addAll(urlSpecs(spannable)) + addAll(emailSpecs(spannable)) + message.mentionedUsers.forEach { addAll(mentionSpecs(spannable, Mention.User(it))) } + if (message.mentionedChannel) addAll(mentionSpecs(spannable, Mention.Channel)) + if (message.mentionedHere) addAll(mentionSpecs(spannable, Mention.Here)) + message.mentionedRoles.forEach { addAll(mentionSpecs(spannable, Mention.Role(it))) } + message.mentionedGroups.forEach { addAll(mentionSpecs(spannable, Mention.Group(it))) } } - /** - * Scans the provided spannable text and turns all occurrences - * of the link types into clickable links (Currently only support web urls). - * - * @param spannable Spannable whose text is to be marked-up with links. - * @return True if at least one link is found and applied. - */ @SuppressLint("RestrictedApi") - private fun addLinks( - spannable: Spannable, - mentionableUsers: List, - ): Boolean = - ( - gatherSpanSpecs( - spannable, - PatternsCompat.AUTOLINK_WEB_URL, - Linkify.sUrlMatchFilter, - ) { it.makeUrlSpan(listOf("http://", "https://", "rtsp://")) } + gatherSpanSpecs( - spannable, - PatternsCompat.AUTOLINK_EMAIL_ADDRESS, + private fun urlSpecs(spannable: Spannable): List = gatherSpanSpecs( + spannable, + PatternsCompat.AUTOLINK_WEB_URL, + Linkify.sUrlMatchFilter, + ) { it.makeUrlSpan(listOf("http://", "https://", "rtsp://")) } - null, - ) { it.makeUrlSpan(listOf("mailto:")) } + mentionableUsers.flatMap { user -> - gatherSpanSpecs( - spannable, - Pattern.compile("((?:\\B|^)(@${user.name})(?:\\b|\$))"), - null, - ) { UserSpan(user) } - } - ).pruneOverlaps(spannable) - .map { spannable.setSpan(it.span, it.start, it.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } - .isNotEmpty() + @SuppressLint("RestrictedApi") + private fun emailSpecs(spannable: Spannable): List = gatherSpanSpecs( + spannable, + PatternsCompat.AUTOLINK_EMAIL_ADDRESS, + null, + ) { it.makeUrlSpan(listOf("mailto:")) } + + private fun mentionSpecs(spannable: Spannable, mention: Mention): List { + if (mention.display.isEmpty()) return emptyList() + return gatherSpanSpecs( + spannable, + mentionRegex(mention.display).toPattern(), + null, + ) { MentionSpan(mention) } + } private fun addLinkMovementMethod(t: TextView) { val m = t.movementMethod @@ -165,17 +162,30 @@ public object Linkify { return specs } - private fun List.pruneOverlaps(text: Spannable): List = - this - text.getSpans(0, text.length, URLSpan::class.java).map { - SpanSpec( - span = it, - start = text.getSpanStart(it), - end = text.getSpanEnd(it), - ) - }.flatMap { link -> - this.filter { it.start <= link.start && it.end >= link.end } + - this.filter { link.start <= it.start && link.end >= it.end } - }.toSet() + /** + * Drops any spec fully contained by, or fully containing, a URL span: either one already + * on the buffer or one gathered in this pass. + * + * E.g. prevents `@user` in `https://example.com/@user` from getting its own span. + */ + private fun List.pruneOverlaps(text: Spannable): List { + val existingUrlSpans = text.getSpans(0, text.length, URLSpan::class.java).map { + SpanSpec(span = it, start = text.getSpanStart(it), end = text.getSpanEnd(it)) + } + val newUrlSpecs = filter { it.span is URLSpan } + val dropped = mutableSetOf() + existingUrlSpans.forEach { link -> + filterTo(dropped) { it.contains(link) || link.contains(it) } + } + newUrlSpecs.forEach { link -> + // only drop non-URL specs, otherwise a URL spec would drop itself (self-containment) + filterTo(dropped) { it.span !is URLSpan && (it.contains(link) || link.contains(it)) } + } + return this - dropped + } + + private fun SpanSpec.contains(other: SpanSpec): Boolean = + start <= other.start && end >= other.end private data class SpanSpec( val span: ClickableSpan, diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/UserSpan.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/MentionSpan.kt similarity index 71% rename from stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/UserSpan.kt rename to stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/MentionSpan.kt index 35381c867eb..34e680b1b2c 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/UserSpan.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/MentionSpan.kt @@ -18,14 +18,12 @@ package io.getstream.chat.android.ui.utils import android.text.style.ClickableSpan import android.view.View -import io.getstream.chat.android.models.User +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention /** - * A [ClickableSpan] that represents a [User]. - * - * This class is used to display a user's name in a [android.widget.TextView] and make it clickable. - * @property user The user that this span represents. + * Marker [ClickableSpan] that annotates a stretch of message text as a [Mention]. Carries no + * behaviour: actual click handling is wired by [TextViewLinkHandler]. */ -internal class UserSpan(val user: User) : ClickableSpan() { +internal class MentionSpan(val mention: Mention) : ClickableSpan() { override fun onClick(widget: View) { /* no-op */ } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt index f95aadebaaf..02d3f74772b 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt @@ -21,7 +21,7 @@ import android.text.method.LinkMovementMethod import android.text.style.URLSpan import android.view.MotionEvent import android.widget.TextView -import io.getstream.chat.android.models.User +import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention internal abstract class TextViewLinkHandler : LinkMovementMethod() { override fun onTouchEvent( @@ -45,10 +45,10 @@ internal abstract class TextViewLinkHandler : LinkMovementMethod() { val off = layout.getOffsetForHorizontal(line, x.toFloat()) buffer.getSpans(off, off, URLSpan::class.java).firstOrNull()?.let { onLinkClick(it.url) } - buffer.getSpans(off, off, UserSpan::class.java).firstOrNull()?.let { onUserClick(it.user) } + buffer.getSpans(off, off, MentionSpan::class.java).firstOrNull()?.let { onMentionClick(it.mention) } return true } abstract fun onLinkClick(url: String) - abstract fun onUserClick(user: User) + abstract fun onMentionClick(mention: Mention) }