Custom Header for Message List

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. It also allows customization through its parameters.

In this example we will customize the MessageListHeader component and make it look similar to the WhatsApp 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,
            )
        }
    )
}
© Getstream.io, Inc. All Rights Reserved.