Skip to content

Commit 4c9ec24

Browse files
Highlight mentions in MessageComposer (Compose) (#5984)
* Introduce Mention contract to allow defining custom mentions. * Introduce MentionStyleFactory for customizing the mentions in the MessageComposer. * Add example for customizing composer mentions. * Add MessageComposerViewModel.selectMention(Mention) method. * Add MessageComposerViewModel.selectMention(Mention) method. * Remove redundant click listener. * Update CHANGELOG.md. * Fix Kdoc. --------- Co-authored-by: Aleksandar Apostolov <[email protected]>
1 parent f1320c2 commit 4c9ec24

File tree

18 files changed

+735
-40
lines changed

18 files changed

+735
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676

7777
### ✅ Added
7878
- Add `MessageListViewModel.hideUnreadSeparator()` method to manually hide the unread separator. [#6001](https://github.com/GetStream/stream-chat-android/pull/6001)
79+
- Add `MentionStyleFactory` for customizing mentions in the message composer. [#5984](https://github.com/GetStream/stream-chat-android/pull/5984)
7980

8081
### ⚠️ Changed
8182

stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import io.getstream.chat.android.compose.sample.feature.channel.isGroupChannel
6464
import io.getstream.chat.android.compose.sample.ui.channel.DirectChannelInfoActivity
6565
import io.getstream.chat.android.compose.sample.ui.channel.GroupChannelInfoActivity
6666
import io.getstream.chat.android.compose.sample.ui.component.CustomChatComponentFactory
67+
import io.getstream.chat.android.compose.sample.ui.component.CustomMentionStyleFactory
6768
import io.getstream.chat.android.compose.sample.ui.location.LocationPickerTabFactory
6869
import io.getstream.chat.android.compose.sample.vm.SharedLocationViewModelFactory
6970
import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResultType
@@ -80,6 +81,7 @@ import io.getstream.chat.android.compose.ui.messages.attachments.factory.Attachm
8081
import io.getstream.chat.android.compose.ui.messages.composer.MessageComposer
8182
import io.getstream.chat.android.compose.ui.messages.list.MessageList
8283
import io.getstream.chat.android.compose.ui.theme.ChatTheme
84+
import io.getstream.chat.android.compose.ui.theme.ComposerInputFieldTheme
8385
import io.getstream.chat.android.compose.ui.theme.MessageComposerTheme
8486
import io.getstream.chat.android.compose.ui.theme.MessageOptionsTheme
8587
import io.getstream.chat.android.compose.ui.theme.MessageTheme
@@ -145,7 +147,13 @@ class MessagesActivity : ComponentActivity() {
145147
val colors = if (isInDarkMode) StreamColors.defaultDarkColors() else StreamColors.defaultColors()
146148
val typography = StreamTypography.defaultTypography()
147149
val shapes = StreamShapes.defaultShapes()
148-
val messageComposerTheme = MessageComposerTheme.defaultTheme(isInDarkMode, typography, shapes, colors)
150+
val messageComposerTheme = MessageComposerTheme
151+
.defaultTheme(isInDarkMode, typography, shapes, colors)
152+
.copy(
153+
inputField = ComposerInputFieldTheme.defaultTheme(
154+
mentionStyleFactory = CustomMentionStyleFactory(colors.primaryAccent),
155+
),
156+
)
149157
val ownMessageTheme = MessageTheme.defaultOwnTheme(isInDarkMode, typography, shapes, colors)
150158
val attachmentsPickerTabFactories = AttachmentsPickerTabFactories.defaultFactories() +
151159
LocationPickerTabFactory(viewModelFactory = SharedLocationViewModelFactory(cid))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.compose.sample.ui.component
18+
19+
import androidx.compose.ui.graphics.Color
20+
import androidx.compose.ui.text.SpanStyle
21+
import io.getstream.chat.android.compose.ui.theme.MentionStyleFactory
22+
import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention
23+
24+
/**
25+
* A custom implementation of [MentionStyleFactory] that applies a specific color to user mentions.
26+
*
27+
* @param color The color to apply to user mentions.
28+
*/
29+
class CustomMentionStyleFactory(private val color: Color) : MentionStyleFactory {
30+
31+
override fun styleFor(mention: Mention): SpanStyle? = when (mention) {
32+
is Mention.User -> SpanStyle(color = color)
33+
else -> null
34+
}
35+
}

stream-chat-android-compose/api/stream-chat-android-compose.api

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,7 +1669,7 @@ public final class io/getstream/chat/android/compose/ui/components/composer/Cool
16691669
}
16701670

16711671
public final class io/getstream/chat/android/compose/ui/components/composer/InputFieldKt {
1672-
public static final fun InputField (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZILandroidx/compose/foundation/BorderStroke;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/text/KeyboardOptions;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
1672+
public static final fun InputField (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZILandroidx/compose/foundation/BorderStroke;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/ui/text/input/VisualTransformation;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
16731673
}
16741674

16751675
public final class io/getstream/chat/android/compose/ui/components/composer/MessageInputKt {
@@ -3367,24 +3367,27 @@ public final class io/getstream/chat/android/compose/ui/theme/ComposerCancelIcon
33673367
public final class io/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme {
33683368
public static final field $stable I
33693369
public static final field Companion Lio/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme$Companion;
3370-
public synthetic fun <init> (Landroidx/compose/ui/graphics/Shape;JLandroidx/compose/ui/text/TextStyle;JLkotlin/jvm/internal/DefaultConstructorMarker;)V
3370+
public synthetic fun <init> (Landroidx/compose/ui/graphics/Shape;JLandroidx/compose/ui/text/TextStyle;JLio/getstream/chat/android/compose/ui/theme/MentionStyleFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
3371+
public synthetic fun <init> (Landroidx/compose/ui/graphics/Shape;JLandroidx/compose/ui/text/TextStyle;JLio/getstream/chat/android/compose/ui/theme/MentionStyleFactory;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
33713372
public final fun component1 ()Landroidx/compose/ui/graphics/Shape;
33723373
public final fun component2-0d7_KjU ()J
33733374
public final fun component3 ()Landroidx/compose/ui/text/TextStyle;
33743375
public final fun component4-0d7_KjU ()J
3375-
public final fun copy-3bbok98 (Landroidx/compose/ui/graphics/Shape;JLandroidx/compose/ui/text/TextStyle;J)Lio/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme;
3376-
public static synthetic fun copy-3bbok98$default (Lio/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme;Landroidx/compose/ui/graphics/Shape;JLandroidx/compose/ui/text/TextStyle;JILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme;
3376+
public final fun component5 ()Lio/getstream/chat/android/compose/ui/theme/MentionStyleFactory;
3377+
public final fun copy-wffgcV4 (Landroidx/compose/ui/graphics/Shape;JLandroidx/compose/ui/text/TextStyle;JLio/getstream/chat/android/compose/ui/theme/MentionStyleFactory;)Lio/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme;
3378+
public static synthetic fun copy-wffgcV4$default (Lio/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme;Landroidx/compose/ui/graphics/Shape;JLandroidx/compose/ui/text/TextStyle;JLio/getstream/chat/android/compose/ui/theme/MentionStyleFactory;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme;
33773379
public fun equals (Ljava/lang/Object;)Z
33783380
public final fun getBackgroundColor-0d7_KjU ()J
33793381
public final fun getBorderShape ()Landroidx/compose/ui/graphics/Shape;
33803382
public final fun getCursorBrushColor-0d7_KjU ()J
3383+
public final fun getMentionStyleFactory ()Lio/getstream/chat/android/compose/ui/theme/MentionStyleFactory;
33813384
public final fun getTextStyle ()Landroidx/compose/ui/text/TextStyle;
33823385
public fun hashCode ()I
33833386
public fun toString ()Ljava/lang/String;
33843387
}
33853388

33863389
public final class io/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme$Companion {
3387-
public final fun defaultTheme (Lio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Lio/getstream/chat/android/compose/ui/theme/StreamColors;Landroidx/compose/runtime/Composer;II)Lio/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme;
3390+
public final fun defaultTheme (Lio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Lio/getstream/chat/android/compose/ui/theme/StreamColors;Lio/getstream/chat/android/compose/ui/theme/MentionStyleFactory;Landroidx/compose/runtime/Composer;II)Lio/getstream/chat/android/compose/ui/theme/ComposerInputFieldTheme;
33883391
}
33893392

33903393
public final class io/getstream/chat/android/compose/ui/theme/ComposerLinkPreviewTheme {
@@ -3458,6 +3461,15 @@ public final class io/getstream/chat/android/compose/ui/theme/IconStyle {
34583461
public fun toString ()Ljava/lang/String;
34593462
}
34603463

3464+
public abstract interface class io/getstream/chat/android/compose/ui/theme/MentionStyleFactory {
3465+
public static final field Companion Lio/getstream/chat/android/compose/ui/theme/MentionStyleFactory$Companion;
3466+
public abstract fun styleFor (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;)Landroidx/compose/ui/text/SpanStyle;
3467+
}
3468+
3469+
public final class io/getstream/chat/android/compose/ui/theme/MentionStyleFactory$Companion {
3470+
public final fun getNoStyle ()Lio/getstream/chat/android/compose/ui/theme/MentionStyleFactory;
3471+
}
3472+
34613473
public final class io/getstream/chat/android/compose/ui/theme/MessageBackgroundShapes {
34623474
public static final field $stable I
34633475
public fun <init> (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)V
@@ -4992,6 +5004,7 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageC
49925004
public final fun seekRecordingTo (F)V
49935005
public final fun selectCommand (Lio/getstream/chat/android/models/Command;)V
49945006
public final fun selectMention (Lio/getstream/chat/android/models/User;)V
5007+
public final fun selectMention (Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/Mention;)V
49955008
public final fun sendMessage (Lio/getstream/chat/android/models/Message;Lio/getstream/result/call/Call$Callback;)V
49965009
public static synthetic fun sendMessage$default (Lio/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel;Lio/getstream/chat/android/models/Message;Lio/getstream/result/call/Call$Callback;ILjava/lang/Object;)V
49975010
public final fun sendRecording ()V

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/InputField.kt

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,23 @@ import androidx.compose.ui.platform.testTag
3737
import androidx.compose.ui.res.stringResource
3838
import androidx.compose.ui.semantics.contentDescription
3939
import androidx.compose.ui.semantics.semantics
40+
import androidx.compose.ui.text.AnnotatedString
4041
import androidx.compose.ui.text.TextRange
4142
import androidx.compose.ui.text.TextStyle
4243
import androidx.compose.ui.text.input.KeyboardCapitalization
4344
import androidx.compose.ui.text.input.OffsetMapping
4445
import androidx.compose.ui.text.input.TextFieldValue
4546
import androidx.compose.ui.text.input.TransformedText
47+
import androidx.compose.ui.text.input.VisualTransformation
4648
import androidx.compose.ui.text.style.TextDecoration
4749
import androidx.compose.ui.tooling.preview.Preview
4850
import androidx.compose.ui.unit.dp
4951
import io.getstream.chat.android.compose.R
5052
import io.getstream.chat.android.compose.ui.theme.ChatTheme
51-
import io.getstream.chat.android.compose.ui.util.buildAnnotatedMessageText
53+
import io.getstream.chat.android.compose.ui.theme.ComposerInputFieldTheme
54+
import io.getstream.chat.android.compose.ui.theme.StreamColors
55+
import io.getstream.chat.android.compose.ui.theme.StreamTypography
56+
import io.getstream.chat.android.compose.ui.util.buildAnnotatedInputText
5257

5358
/**
5459
* Custom input field that we use for our UI. It's fairly simple - shows a basic input with clipped
@@ -65,6 +70,8 @@ import io.getstream.chat.android.compose.ui.util.buildAnnotatedMessageText
6570
* @param border The [BorderStroke] that will appear around the input field.
6671
* @param innerPadding The padding inside the input field, around the label or input.
6772
* @param keyboardOptions The [KeyboardOptions] to be applied to the input.
73+
* @param visualTransformation The [VisualTransformation] to be applied to the input. By default, it applies text
74+
* styling and link styling.
6875
* @param decorationBox Composable function that represents the input field decoration as it's filled with content.
6976
*/
7077
@Composable
@@ -77,6 +84,11 @@ public fun InputField(
7784
border: BorderStroke = BorderStroke(1.dp, ChatTheme.colors.borders),
7885
innerPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
7986
keyboardOptions: KeyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
87+
visualTransformation: VisualTransformation = DefaultInputFieldVisualTransformation(
88+
inputFieldTheme = ChatTheme.messageComposerTheme.inputField,
89+
typography = ChatTheme.typography,
90+
colors = ChatTheme.colors,
91+
),
8092
decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit,
8193
) {
8294
var textState by remember { mutableStateOf(TextFieldValue(text = value)) }
@@ -94,8 +106,6 @@ public fun InputField(
94106
}
95107

96108
val theme = ChatTheme.messageComposerTheme.inputField
97-
val typography = ChatTheme.typography
98-
val colors = ChatTheme.colors
99109
val description = stringResource(id = R.string.stream_compose_cd_message_input)
100110

101111
BasicTextField(
@@ -113,19 +123,7 @@ public fun InputField(
113123
onValueChange(it.text)
114124
}
115125
},
116-
visualTransformation = {
117-
val styledText = buildAnnotatedMessageText(
118-
text = it.text,
119-
textColor = theme.textStyle.color,
120-
textFontStyle = typography.body.fontStyle,
121-
linkStyle = TextStyle(
122-
color = colors.primaryAccent,
123-
textDecoration = TextDecoration.Underline,
124-
),
125-
mentionsColor = colors.primaryAccent,
126-
)
127-
TransformedText(styledText, OffsetMapping.Identity)
128-
},
126+
visualTransformation = visualTransformation,
129127
textStyle = theme.textStyle,
130128
cursorBrush = SolidColor(theme.cursorBrushColor),
131129
decorationBox = { innerTextField -> decorationBox(innerTextField) },
@@ -136,6 +134,36 @@ public fun InputField(
136134
)
137135
}
138136

137+
/**
138+
* Default visual transformation for the [InputField] composable.
139+
* Applies text styling and link styling to the input text.
140+
*
141+
* @param inputFieldTheme The theme for the input field.
142+
* @param typography The typography styles to be used.
143+
* @param colors The color palette to be used.
144+
*/
145+
private class DefaultInputFieldVisualTransformation(
146+
val inputFieldTheme: ComposerInputFieldTheme,
147+
val typography: StreamTypography,
148+
val colors: StreamColors,
149+
) : VisualTransformation {
150+
override fun filter(text: AnnotatedString): TransformedText {
151+
val textColor = inputFieldTheme.textStyle.color
152+
val fontStyle = typography.body.fontStyle
153+
val linkStyle = TextStyle(
154+
color = colors.primaryAccent,
155+
textDecoration = TextDecoration.Underline,
156+
)
157+
val transformed = buildAnnotatedInputText(
158+
text = text.text,
159+
textColor = textColor,
160+
textFontStyle = fontStyle,
161+
linkStyle = linkStyle,
162+
)
163+
return TransformedText(transformed, OffsetMapping.Identity)
164+
}
165+
}
166+
139167
@Preview
140168
@Composable
141169
private fun InputFieldPreview() {

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/MessageInput.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,22 @@ import androidx.compose.foundation.text.KeyboardOptions
3030
import androidx.compose.runtime.Composable
3131
import androidx.compose.ui.Alignment
3232
import androidx.compose.ui.Modifier
33+
import androidx.compose.ui.text.AnnotatedString
34+
import androidx.compose.ui.text.TextStyle
3335
import androidx.compose.ui.text.input.KeyboardCapitalization
36+
import androidx.compose.ui.text.input.OffsetMapping
37+
import androidx.compose.ui.text.input.TransformedText
38+
import androidx.compose.ui.text.input.VisualTransformation
39+
import androidx.compose.ui.text.style.TextDecoration
3440
import androidx.compose.ui.unit.dp
3541
import io.getstream.chat.android.compose.ui.theme.ChatTheme
42+
import io.getstream.chat.android.compose.ui.theme.ComposerInputFieldTheme
43+
import io.getstream.chat.android.compose.ui.theme.StreamColors
44+
import io.getstream.chat.android.compose.ui.theme.StreamTypography
45+
import io.getstream.chat.android.compose.ui.util.buildAnnotatedInputText
3646
import io.getstream.chat.android.models.Attachment
3747
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canSendMessage
48+
import io.getstream.chat.android.ui.common.feature.messages.composer.mention.Mention
3849
import io.getstream.chat.android.ui.common.state.messages.Edit
3950
import io.getstream.chat.android.ui.common.state.messages.Reply
4051
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
@@ -70,6 +81,13 @@ public fun MessageInput(
7081
val (value, attachments, activeAction) = messageComposerState
7182
val canSendMessage = messageComposerState.canSendMessage()
7283

84+
val visualTransformation = MessageInputVisualTransformation(
85+
inputFieldTheme = ChatTheme.messageComposerTheme.inputField,
86+
typography = ChatTheme.typography,
87+
colors = ChatTheme.colors,
88+
mentions = messageComposerState.selectedMentions,
89+
)
90+
7391
InputField(
7492
modifier = modifier,
7593
value = value,
@@ -78,6 +96,7 @@ public fun MessageInput(
7896
enabled = canSendMessage,
7997
innerPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
8098
keyboardOptions = keyboardOptions,
99+
visualTransformation = visualTransformation,
81100
decorationBox = { innerTextField ->
82101
Column {
83102
if (activeAction is Reply) {
@@ -125,6 +144,40 @@ public fun MessageInput(
125144
)
126145
}
127146

147+
/**
148+
* Visual transformation applied to the message input field.
149+
* Applies text styling, link styling, and mention styling to the input text.
150+
*
151+
* @param inputFieldTheme The theme for the input field.
152+
* @param typography The typography styles to be used.
153+
* @param colors The color palette to be used.
154+
* @param mentions The set of mentions to be styled in the input text.
155+
*/
156+
private class MessageInputVisualTransformation(
157+
val inputFieldTheme: ComposerInputFieldTheme,
158+
val typography: StreamTypography,
159+
val colors: StreamColors,
160+
val mentions: Set<Mention>,
161+
) : VisualTransformation {
162+
override fun filter(text: AnnotatedString): TransformedText {
163+
val textColor = inputFieldTheme.textStyle.color
164+
val fontStyle = typography.body.fontStyle
165+
val linkStyle = TextStyle(
166+
color = colors.primaryAccent,
167+
textDecoration = TextDecoration.Underline,
168+
)
169+
val transformed = buildAnnotatedInputText(
170+
text = text.text,
171+
textColor = textColor,
172+
textFontStyle = fontStyle,
173+
linkStyle = linkStyle,
174+
mentions = mentions,
175+
mentionStyleFactory = inputFieldTheme.mentionStyleFactory,
176+
)
177+
return TransformedText(transformed, OffsetMapping.Identity)
178+
}
179+
}
180+
128181
/**
129182
* The default number of lines allowed in the input. The message input will become scrollable after
130183
* this threshold is exceeded.

0 commit comments

Comments
 (0)