Custom Message Items
By default, the embedded messaging UI uses a built-in message item component to display each message in the list. You can replace this with your own custom component to match your application's design or to add additional functionality.
Custom message items are rendered within the same list infrastructure as built-in items. Therefore, pagination, loading states, and scrolling work identically.
When to Use Custom Message Items
Custom message items let you:
- Match your app's visual design language.
- Display additional message properties.
- Add custom interactions such as swiping actions and long-press menus.
- Integrate with your existing component library.
Available Message Properties
Your custom component receives a view model called CustomMessageItemViewModelApi with the following properties:
| Property | Type | Description |
|---|---|---|
title | String | The message headline |
lead | String | Preview text for the message |
imageUrl | String | URL for the thumbnail image |
imageAltText | String | Alternative text for the thumbnail image |
receivedAt | Long | Timestamp when the message was received |
isNotOpened | Boolean | Indicates if the user has not opened the message |
isPinned | Boolean | Indicates if the message is pinned |
categories | List<Category> | Categories assigned to the message |
An additional isSelected Boolean argument determines whether the message is currently selected.
Implementing Custom Message Items
To implement custom message items, pass a custom composable function to EmbeddedMessagingView or EmbeddedMessagingCompactView.
- Android
- Kotlin Multiplatform
- iOS
- Web
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.sap.ec.mobileengage.embeddedmessaging.ui.item.CustomMessageItemViewModelApi
import com.sap.ec.mobileengage.embeddedmessaging.ui.EmbeddedMessagingView
@Composable
fun MessagingScreen() {
EmbeddedMessagingView(
showFilters = true,
customMessageItem = { viewModel, isSelected ->
CustomMessageItem(viewModel, isSelected)
}
)
}
@Composable
fun CompactMessagingScreen() {
EmbeddedMessagingCompactView(
onNavigate = {
// onNavigate logic
},
customMessageItem = { viewModel, isSelected ->
CustomMessageItem(viewModel, isSelected)
}
)
}
@Composable
fun CustomMessageItem(
viewModel: CustomMessageItemViewModelApi,
isSelected: Boolean
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surface
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = viewModel.title,
style = MaterialTheme.typography.titleMedium,
fontWeight = if (viewModel.isNotOpened) FontWeight.Bold else FontWeight.Normal
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = viewModel.lead,
style = MaterialTheme.typography.bodyMedium,
maxLines = 2
)
if (viewModel.isPinned) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "📌 Pinned",
style = MaterialTheme.typography.labelSmall
)
}
}
}
}
Parameters:
viewModel: CustomMessageItemViewModelApi- The message data.isSelected: Boolean- Whether the message is currently selected.
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.sap.ec.mobileengage.embeddedmessaging.ui.item.CustomMessageItemViewModelApi
import com.sap.ec.mobileengage.embeddedmessaging.ui.EmbeddedMessagingView
@Composable
fun MessagingScreen() {
EmbeddedMessagingView(
showFilters = true,
customMessageItem = { viewModel, isSelected ->
CustomMessageItem(viewModel, isSelected)
}
)
}
@Composable
fun CompactMessagingScreen() {
EmbeddedMessagingCompactView(
onNavigate = {
// onNavigate logic
},
customMessageItem = { viewModel, isSelected ->
CustomMessageItem(viewModel, isSelected)
}
)
}
@Composable
fun CustomMessageItem(
viewModel: CustomMessageItemViewModelApi,
isSelected: Boolean
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surface
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = viewModel.title,
style = MaterialTheme.typography.titleMedium,
fontWeight = if (viewModel.isNotOpened) FontWeight.Bold else FontWeight.Normal
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = viewModel.lead,
style = MaterialTheme.typography.bodyMedium,
maxLines = 2
)
if (viewModel.isPinned) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "📌 Pinned",
style = MaterialTheme.typography.labelSmall
)
}
}
}
}
Parameters:
viewModel: CustomMessageItemViewModelApi- The message data.isSelected: Boolean- Whether the message is currently selected.
EngagementCloud.shared.embeddedMessaging.ViewController(showFilters: true) { viewModel, isSelected in
// return custom messageItem viewController
}
EngagementCloud.shared.embeddedMessaging.CompactViewController {
//onNavigate logic
} customMessageItem: { viewModel, isSelected in
//customMessageItem viewController implementation
}
Create a custom HTML element and register it with the browser:
Step 1: Define Your Custom Element
class MyCustomMessageItem extends HTMLElement {
static get observedAttributes() {
return [
"title",
"lead",
"image",
"image-alt-text",
"received-at",
"is-not-opened",
"is-selected",
"is-pinned",
"categories"
];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
render() {
const title = this.getAttribute("title") ?? "";
const lead = this.getAttribute("lead") ?? "";
const image = this.getAttribute("image") ?? "";
const isSelected = this.getAttribute("is-selected") === "true";
const isNotOpened = this.getAttribute("is-not-opened") === "true";
const isPinned = this.getAttribute("is-pinned") === "true";
const categoriesAttr = this.getAttribute("categories") ?? "[]";
const categories = JSON.parse(categoriesAttr);
this.shadowRoot.innerHTML = `
<style>
.card {
border: 1px solid ${isSelected ? '#007bff' : '#ddd'};
border-radius: 8px;
padding: 12px;
background: ${isSelected ? '#e7f1ff' : 'white'};
}
.title {
font-weight: ${isNotOpened ? 'bold' : 'normal'};
margin: 0 0 4px 0;
}
</style>
<div class="card">
${image ? `<img src="${image}" alt="" style="max-width: 100%; border-radius: 4px;">` : ""}
<h3 class="title">${title}</h3>
<p class="lead">${lead}</p>
${isPinned ? '<div class="pinned">📌 Pinned</div>' : ''}
${categories.length > 0 ? `
<div class="categories">
${categories.map(c => `<span class="category">${c.text}</span>`).join('')}
</div>
` : ''}
</div>
`;
}
}
// Register the custom element
window.customElements.define("my-custom-message-item", MyCustomMessageItem);
Step 2: Use the Custom Element
Pass the element name to the embedded messaging component:
<ec-embedded-messaging
custom-message-item-element-name="my-custom-message-item">
</ec-embedded-messaging>
Alternatively, pass it to the compact list:
<ec-embedded-messaging-compact
custom-message-item-element-name="my-custom-message-item">
</ec-embedded-messaging-compact>
Category Object Structure
The categories attribute is a JSON-encoded array with the following structure:
[
{ "id": "61f0c404-5cb3-11e7-907b-a6006ad3dba0", "text": "Promotions" },
{ "id": "72f0c404-5cb3-11e7-907b-a6006ad3dba4", "text": "Updates" }
]
Best Practices
- Handle the selected state by visually indicating when a message is selected to maintain clear user feedback.
- Show unread indicator by using bold text or a visual indicator for
isNotOpened = truemessages. - Optimize image loading by considering lazy loading for better performance.
- Maintain accessibility by ensuring your custom component includes proper ARIA labels and keyboard navigation.
- Keep it clickable by ensuring your component does not block pointer events.