Skip to main content

Message List#

MessageListView is one of our core UI components, which displays a list of messages for a channel. It contains the following list of possible items:

  • Plain text message
  • Text and attachments (media or file) message
  • Deleted message (only for current user)
  • Error message (e.g. autoblocked message with inappropriate content)
  • System message (e.g. some user joined a channel)
  • Giphy preview
  • Date separator
  • Loading more indicator
  • Thread separator (for thread mode only)
  • Typing indicator

You're able to customize the appearance of this component using custom attributes as well as method calls at runtime. MessageListView also contains the set of overridable action/option handlers and event listeners. By default, this component has the following look:

Light ModeDark Mode
Message list overview in light modeMessage list overview in dark mode

Usage#

If you want to use all default features and default design of this component then getting started with it is easy:

  1. Add the component to your xml layout hierarchy.
  2. Bind it with a MessageListViewModel.

Adding MessageListView to your layout is easy as inserting following lines to your layout hierarchy (example for ConstraintLayout):

<io.getstream.chat.android.ui.message.list.MessageListView
android:id="@+id/message_list_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>

The UI components library includes a ViewModel for MessageListView and the bindView extension function makes it easy to use a default setup:

// 1. Init view model
val viewModel: MessageListViewModel by viewModels {
MessageListViewModelFactory(cid = "messaging:123")
}

// 2. Bind view and viewModel
viewModel.bindView(messageListView, lifecycleOwner)

Handling Actions#

MessageListView comes with a set of actions available out-of-the-box by long pressing a message:

  • Adding reactions
  • Replies
  • Thread replies
  • Copying the message
  • Editing the message (if you are an owner)
  • Deleting the message (if you are an owner)
  • Flagging the message (if it doesn't belong to you)
Light ModeDark Mode
Message_options_in light modeMessage options in dark mode

Default action handlers are set up when binding the ViewModel with the View. You can customize the default behavior by overriding each of the following handlers:

messageListView.setLastMessageReadHandler {
// Handle when last message got read
}
messageListView.setEndRegionReachedHandler {
// Handle when end region reached
}
messageListView.setMessageDeleteHandler { message: Message ->
// Handle when message is going to be deleted
}
messageListView.setThreadStartHandler { message: Message ->
// Handle when new thread for message is started
}
messageListView.setMessageFlagHandler { message: Message ->
// Handle when message is going to be flagged
}
messageListView.setMessagePinHandler { message: Message ->
// Handle when message is going to be pinned
}
messageListView.setMessageUnpinHandler { message: Message ->
// Handle when message is going to be unpinned
}
messageListView.setGiphySendHandler { message: Message, giphyAction: GiphyAction ->
// Handle when some giphyAction is going to be performed
}
messageListView.setMessageRetryHandler { message: Message ->
// Handle when some failed message is going to be retried
}
messageListView.setMessageReactionHandler { message: Message, reactionType: String ->
// Handle when some reaction for message is going to be send
}
messageListView.setMessageReplyHandler { cid: String, message: Message ->
// Handle when message is going to be replied in the channel with cid
}
messageListView.setAttachmentDownloadHandler {
// Handle when attachment is going to be downloaded
}
note

Handlers must be set before passing any data to MessageListView. If you don't use the default binding that bindView provides, please make sure you're setting up all handlers.

Listeners#

In addition to the required handlers, MessageListView also provides some optional listeners. They are also set by default if you use bindView.

You can always override them to get events when something happens:

messageListView.setMessageClickListener { message: Message ->
// Listen to click on message events
}
messageListView.setEnterThreadListener { message: Message ->
// Listen to events when enter thread associated with a message
}
messageListView.setAttachmentDownloadClickListener { attachment: Attachment ->
// Listen to events when download click for an attachment happens
}
messageListView.setUserReactionClickListener { message: Message, user: User, reaction: Reaction ->
// Listen to clicks on user reactions on the message options overlay
}
messageListView.setMessageLongClickListener { message ->
// Handle long click on message
}
messageListView.setAttachmentClickListener { message, attachment ->
// Handle long click on attachment
}
messageListView.setUserClickListener { user ->
// Handle click on user avatar
}

Other available listeners for MessageListView can be found here.

Customization#

You can change the appearance of this component to fit your app's design requirements. There are two ways to change the style: using XML attributes and runtime changes.

Using XML Attributes#

MessageListView provides a large set of xml attributes available for customization. The full list of them is available here.

Let's consider an example when we want to change the style of messages sent by the current user.

Light ModeDark Mode
lightdark

In order to do that, we need to add additional attributes to MessageListView:

<io.getstream.chat.android.ui.message.list.MessageListView
android:id="@+id/messageListView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="0dp"
android:clipToPadding="false"
app:layout_constraintBottom_toTopOf="@+id/messageInputView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messagesHeaderView"
app:streamUiMessageBackgroundColorMine="#70AF74"
app:streamUiMessageBackgroundColorTheirs="#FFFFFF"
app:streamUiMessageTextColorMine="#FFFFFF"
app:streamUiMessageTextColorTheirs="#000000"
/>

Using Style Transformations#

Both MessageListView and its ViewHolders can be configured programmatically (a list of supported customizations can be found here and here).

As an example, let's apply the green style from the previous section, but this time programmatically:

BeforeAfter
message style beforemessage style after

We are going to use a custom TransformStyle.messageListItemStyleTransformer:

TransformStyle.messageListItemStyleTransformer = StyleTransformer { defaultViewStyle ->
defaultViewStyle.copy(
messageBackgroundColorMine = Color.parseColor("#70AF74"),
messageBackgroundColorTheirs = Color.WHITE,
textStyleMine = defaultViewStyle.textStyleMine.copy(color = Color.WHITE),
textStyleTheirs = defaultViewStyle.textStyleTheirs.copy(color = Color.BLACK),
)
}
note

The transformers should be set before the views are rendered to make sure that the new style was applied.

As another example, let's modify the default view which allows scrolling to the bottom when the new message arrives:

BeforeAfter
message style programmatically beforemessage style programmatically after

To achieve this effect we need to provide this custom TransformStyle.messageListStyleTransformer:

TransformStyle.messageListStyleTransformer = StyleTransformer { defaultViewStyle ->
defaultViewStyle.copy(
scrollButtonViewStyle = defaultViewStyle.scrollButtonViewStyle.copy(
scrollButtonColor = Color.RED,
scrollButtonUnreadEnabled = false,
scrollButtonIcon = ContextCompat.getDrawable(requireContext(), R.drawable.stream_ui_ic_clock)!!,
),
)
}

Channel Feature Flags#

Some xml attributes let you to enable/disable features in MessageListView.

  • streamUiScrollButtonEnabled - show/hide the scroll-to-bottom button
  • streamUiScrollButtonUnreadEnabled - show/hide the unread count badge on the scroll-to-bottom button
  • streamUiReactionsEnabled - whether users can react to messages
  • streamUiReplyEnabled - whether users can reply to messages
  • streamUiCopyMessageActionEnabled - whether users can copy messages
  • streamUiRetryMessageEnabled - whether users can retry failed messages
  • streamUiEditMessageEnabled - whether users can edit their messages
  • streamUiFlagMessageEnabled - whether users can flag messages
  • streamUiFlagMessageConfirmationEnabled - whether users will see the confirmation dialog when flag messages
  • streamUiDeleteMessageEnabled - whether users can delete their messages
  • streamUiDeleteConfirmationEnabled - whether users will see the confirmation dialog when deleting messages
  • streamUiThreadsEnabled - whether users can create thread replies

These attributes let you enable/disable configuration for channel features. E.g. if a channel's configuration supports message replies, but you disabled it via xml attributes, then members of this channel won't see such an option.

MessageListView provides you the possibility to enable/disable these channel features at runtime as well:

messageListView.setRepliesEnabled(false)
messageListView.setDeleteMessageEnabled(false)
messageListView.setEditMessageEnabled(false)
BeforeAfter
message list options beforemessage list options after

Messages Start Position#

You can configure the messages to start at the top or the bottom of the view (Default: bottom) by using streamUiMessagesStart and streamUiThreadMessagesStart attributes.

BottomTop
messages at the bottommessages at the top
note

Messages' position doesn't affect the stack. The default is from bottom to top. if you would like to change it, use the method setCustomLinearLayoutManager and set a LinearLayoutManager with your desired definitions.

Filtering Messages#

You can filter some messages if you don't want to show them in your MessageListView. Imagine you want to hide all messages that contain the word "secret". This can be done with following lines:

val forbiddenWord = "secret"
val predicate = MessageListView.MessageListItemPredicate { item ->
!(item is MessageListItem.MessageItem && item.message.text.contains(forbiddenWord))
}
messageListView.setMessageListItemPredicate(predicate)
note

The predicate has to return true for the items that you do want to display in the list.

Custom Message Views#

MessageListView provides an API for creating custom ViewHolders. To use your own ViewHolder:

  1. Extend MessageListItemViewHolderFactory.
  2. Write your own logic for creating ViewHolders.
  3. Create a new factory instance and set it on MessageListView.

Let's consider an example when we want to create custom ViewHolders for messages that came from other users less than 24 hours ago. The result should look like this:

  1. Add a new layout called today_message_list_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>

<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginStart="40dp"
android:layout_marginBottom="4dp"
app:cardBackgroundColor="@android:color/holo_green_dark"
app:cardCornerRadius="8dp"
app:cardElevation="0dp"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@id/marginEnd"
app:layout_constraintTop_toTopOf="parent"
>

<TextView
android:id="@+id/textLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@android:color/primary_text_light"
android:padding="16dp"
/>

</com.google.android.material.card.MaterialCardView>

<androidx.constraintlayout.widget.Guideline
android:id="@+id/marginEnd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.7"
/>

</androidx.constraintlayout.widget.ConstraintLayout>
  1. Add a new TodayViewHolder class that inflates this layout and populates it with data:
class TodayViewHolder(
parentView: ViewGroup,
private val binding: TodayMessageListItemBinding = TodayMessageListItemBinding.inflate(LayoutInflater.from(
parentView.context),
parentView,
false),
) : BaseMessageItemViewHolder<MessageListItem.MessageItem>(binding.root) {

override fun bindData(data: MessageListItem.MessageItem, diff: MessageListItemPayloadDiff?) {
binding.textLabel.text = data.message.text
}
}
  1. Add a new CustomMessageViewHolderFactory class that checks each message, and uses the custom ViewHolder when necessary:
class CustomMessageViewHolderFactory : MessageListItemViewHolderFactory() {
override fun getItemViewType(item: MessageListItem): Int {
return if (item is MessageListItem.MessageItem &&
item.isTheirs &&
item.message.attachments.isEmpty() &&
item.message.createdAt.isLessThenDayAgo()
) {
TODAY_VIEW_HOLDER_TYPE
} else {
super.getItemViewType(item)
}
}

private fun Date?.isLessThenDayAgo(): Boolean {
if (this == null) {
return false
}
val dayInMillis = TimeUnit.DAYS.toMillis(1)
return time >= System.currentTimeMillis() - dayInMillis
}

override fun createViewHolder(
parentView: ViewGroup,
viewType: Int,
): BaseMessageItemViewHolder<out MessageListItem> {
return if (viewType == TODAY_VIEW_HOLDER_TYPE) {
TodayViewHolder(parentView)
} else {
super.createViewHolder(parentView, viewType)
}
}

companion object {
private const val TODAY_VIEW_HOLDER_TYPE = 1
}
}
  1. Finally, set an instance of the custom factory on MessageListView
messageListView.setMessageViewHolderFactory(CustomMessageViewHolderFactory())

Additionally, you can also use ChatUI.markdown for Markdown support. If you do that, don't use android:autoLink attribute because it'll break the markdown Linkify implementation. This property is deprecated and ChatUI.messageTextTransformer should be used instead. See docs for more information.

Custom Empty State#

MessageListView handles loading and empty states out-of-box. If you want to customize these, you can do it at runtime.

Let's consider an example when you want to set a custom empty state:

val textView = TextView(context).apply {
text = "There are no messages yet"
setTextColor(Color.RED)
}
messageListView.setEmptyStateView(
view = textView,
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER
)
)

This code will display the following empty state:

The custom empty state in action

Configure When Avatar Appears#

Is it possible to configure when the avatar for messages appears. You can use MessageListView.setShowAvatarPredicate and pass a predicate to define when the avatar is going to be shown. This example implements the default behaviour:

messageListView.setShowAvatarPredicate(
ShowAvatarPredicate { messageItem ->
messageItem.positions.contains(MessageListItem.Position.BOTTOM) && messageItem.isTheirs
}
)
note

To avoid overlap between the avatar and the messages of the chat, remember to use streamUiMessageStartMargin and streamUiMessageEndMargin to create space for the avatar of the messages.

If you set a predicate that shows avatars for your own messages as well, use this value:

streamUiMessageEndMargin=">@dimen/stream_ui_message_viewholder_avatar_missing_margin"

If your predicate doesn't show avatars for your own messages (this is the default behavior), remove the end margin:

streamUiMessageEndMargin="0dp"

Configure giphy#

It is possible to configure the gifs size. Prior of version 4.23.0 gifs were auto-sizable and will resize themselves accordingly with the size of the image they load.

As of v5.0.0, our Giphy holders are much more customizable. You can use three Giphy load modes:

  • original: The original Giphy file size, as given by the API.
  • fixedHeight: Smaller file size Giphy, that uses fixed height in order to improve scrolling performance and image loading speed.
  • fixedHeightDownsampled: Same as fixedHeight, but downsampled to only a few frames, making it roughly 6x smaller and faster to load, but less visually appealing.

Alongside these Giphy types, you can also choose the scaleType when loading the Giphy:

  • fitCenter: The recommended setting, fits the GIF to the center of the container and keeps the aspect ratio.
  • centerCrop: Useful if you want larger size giphies and items, but don't care if the image is cropped.
  • center: Doesn't scale the image and centers it in the parent. Not recommended for most Giphy images as it will cause a lot of whitespace and smaller images.
  • ...

We recommend always using fitCenter as the go-to way of loading Giphy images that doesn't cause images to crop. For the best performance, we recommend either fixedHeight, or fixedHeightDownsampled if you don't mind the low frame rate.

Did you find this page helpful?