Custom Header for Message List

In this cookbook recipe, you'll learn how to customize the message list header to match your app's design. The header is a key part of the chat experience, displaying channel information and navigation controls.

Overview

The message list header sits above the list of messages and shows the channel name and avatar, members and online members count, current connection status, and a back button. While the SDK provides a default header that works well for most cases, you may want to customize it to:

  • Match your app's visual design and branding
  • Display additional channel information
  • Add custom actions or buttons

In this example, we'll customize the MessageListHeader component to look similar to WhatsApp's conversation title bar.

Custom Message List Header

State Handling

Let's define a new composable, CustomMessageListHeader, that takes the channel cid and a back handler as parameters.

@Composable
fun CustomMessageListHeader(cid: String?, onBackClick: () -> Unit = {})

Inside it we'll use the built-in MessageListViewModel to acquire the state that we need.

val viewModel = viewModel(
    modelClass = MessageListViewModel::class.java,
    factory = MessagesViewModelFactory(LocalContext.current, channelId = cid)
)

val channel = viewModel.channel
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
val currentUser by viewModel.user.collectAsStateWithLifecycle()

We pass channel, connectionState and currentUser to the MessageListHeader component, alongside other state that we get from the view model:

MessageListHeader(
    channel = channel,
    currentUser = currentUser,
    connectionState = connectionState,
    typingUsers = viewModel.typingUsers,
    messageMode = viewModel.messageMode,
    onBackPressed = onBackClick,
)

Leading Content

Let's customize the leading content, which represents the start slot of the header. This is a very simple customization: we just replace the default back arrow with our custom one.

Note that we'll also use CustomHeaderButton for other buttons that we'll add later to the header.

MessageListHeader(
    // State
    leadingContent = { CustomHeaderLeadingContent(onClick = onBackClick) },
)

@Composable
private fun CustomHeaderLeadingContent(onClick: () -> Unit) {
    CustomHeaderButton(
        iconRes = R.drawable.ic_back,
        contentDescription = "Back",
        onClick = onClick
    )
}

@Composable
private fun CustomHeaderButton(
    @DrawableRes iconRes: Int,
    contentDescription: String,
    onClick: () -> Unit,
) {
    IconButton(
        onClick = onClick,
        content = {
            Icon(
                painter = painterResource(id = iconRes),
                contentDescription = contentDescription,
                tint = Color.White,
            )
        }
    )
}

Center Content

Now, let's use the middle slot of the header, named centerContent, and pass our CustomHeaderCenterContent to it.

We use several composables and utility methods in order to display the channel avatar, name, member and online member count:

  • The ChannelAvatar component from our SDK to show the avatar. Based on the state of the channel and the number of members, it shows different types of images.
  • The ChatTheme.ChannelNameFormatter.formatChannelName method to show the name of the channel, based on a set of rules. Search for the DefaultChannelNameFormatter component for more info.
  • The channel.getMembersStatusText extension method to show either a member count for a group channel or the last seen text for a direct one-to-one conversation.
MessageListHeader(
    // State
    leadingContent = { CustomHeaderLeadingContent(onClick = onBackClick) },
    centerContent = { CustomHeaderCenterContent(channel = channel, currentUser = currentUser) },
)

@Composable
private fun CustomHeaderCenterContent(channel: Channel, currentUser: User?) {
    Row {
        ChannelAvatar(
            modifier = Modifier.size(40.dp),
            channel = channel,
            currentUser = currentUser,
        )
        Spacer(modifier = Modifier.size(10.dp))
        Column {
            Text(
                text = ChatTheme.channelNameFormatter.formatChannelName(channel, currentUser),
                color = Color.White,
                fontWeight = FontWeight.SemiBold,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
            )
            Text(
                text = channel.getMembersStatusText(LocalContext.current, currentUser),
                color = Color.LightGray,
                style = ChatTheme.typography.footnote
            )
        }
    }
}

Trailing Content

Next, we'll use the last slot, trailingContent, to add video and audio call buttons and a menu button, like WhatsApp has. As this is only an example, these buttons don't do anything. You can check out our Video SDK to implement audio & video calling.

MessageListHeader(
    // State
    leadingContent = { CustomHeaderLeadingContent(onClick = onBackClick) },
    centerContent = { CustomHeaderCenterContent(channel = channel, currentUser = currentUser) },
    trailingContent = { CustomHeaderTrailingContent() },
)

@Composable
private fun CustomHeaderTrailingContent() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .offset(x = 10.dp),
        horizontalArrangement = Arrangement.End
    ) {
        CustomHeaderButton(
            iconRes = R.drawable.ic_videocam,
            contentDescription = "Video Call",
            onClick = {}
        )
        CustomHeaderButton(
            iconRes = R.drawable.ic_phone,
            contentDescription = "Audio Call",
            onClick = {}
        )
        CustomHeaderButton(
            iconRes = R.drawable.ic_menu,
            contentDescription = "Menu",
            onClick = {}
        )
    }
}

Final Touches

As final touches, we change the height and the background color of the header:

MessageListHeader(
    // State
    modifier = Modifier.height(55.dp),
    color = Color(0xFF0F7B6F),
    // Content slots
)

Make sure to use ChatTheme as the root of all the composables that use our Compose UI Components. It's used to provide default style properties and utility methods.

Full code

Below you can find the full code for our implementation.

@Composable
fun CustomMessageListHeader(cid: String?, onBackClick: () -> Unit = {}) {
    cid?.let {
        val viewModel = viewModel(
            modelClass = MessageListViewModel::class.java,
            factory = MessagesViewModelFactory(LocalContext.current, channelId = cid)
        )

        val channel = viewModel.channel
        val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
        val currentUser by viewModel.user.collectAsStateWithLifecycle()

        MessageListHeader(
            channel = channel,
            currentUser = currentUser,
            connectionState = connectionState,
            modifier = Modifier.height(55.dp),
            typingUsers = viewModel.typingUsers,
            messageMode = viewModel.messageMode,
            onBackPressed = onBackClick,
            color = Color(0xFF0F7B6F),
            leadingContent = { CustomHeaderLeadingContent(onClick = onBackClick) },
            centerContent = { CustomHeaderCenterContent(channel = channel, currentUser = currentUser) },
            trailingContent = { CustomHeaderTrailingContent() },
        )
    }
}

@Composable
private fun CustomHeaderLeadingContent(onClick: () -> Unit) {
    CustomHeaderButton(
        iconRes = R.drawable.ic_back,
        contentDescription = "Back",
        onClick = onClick
    )
}

@Composable
private fun CustomHeaderCenterContent(channel: Channel, currentUser: User?) {
    Row {
        ChannelAvatar(
            modifier = Modifier.size(40.dp),
            channel = channel,
            currentUser = currentUser,
        )
        Spacer(modifier = Modifier.size(10.dp))
        Column {
            Text(
                text = ChatTheme.channelNameFormatter.formatChannelName(channel, currentUser),
                color = Color.White,
                fontWeight = FontWeight.SemiBold,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
            )
            Text(
                text = channel.getMembersStatusText(LocalContext.current, currentUser),
                color = Color.LightGray,
                style = ChatTheme.typography.footnote
            )
        }
    }
}

@Composable
private fun CustomHeaderTrailingContent() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .offset(x = 10.dp),
        horizontalArrangement = Arrangement.End
    ) {
        CustomHeaderButton(
            iconRes = R.drawable.ic_videocam,
            contentDescription = "Video Call",
            onClick = {}
        )
        CustomHeaderButton(
            iconRes = R.drawable.ic_phone,
            contentDescription = "Audio Call",
            onClick = {}
        )
        CustomHeaderButton(
            iconRes = R.drawable.ic_menu,
            contentDescription = "Menu",
            onClick = {}
        )
    }
}

@Composable
private fun CustomHeaderButton(@DrawableRes iconRes: Int, contentDescription: String, onClick: () -> Unit) {
    IconButton(
        onClick = onClick,
        content = {
            Icon(
                painter = painterResource(id = iconRes),
                contentDescription = contentDescription,
                tint = Color.White,
            )
        }
    )
}

Integration with MessagesScreen

If you want to use MessagesScreen with all its built-in functionality (message list, composer, attachments picker, etc.) but replace only the header, you can do so using the topBarContent parameter:

MessagesScreen(
    viewModelFactory = MessagesViewModelFactory(
        context = this,
        channelId = channelId
    ),
    topBarContent = { backAction ->
        CustomMessageListHeader(
            cid = channelId,
            onBackClick = backAction.onBackPressed
        )
    }
)