diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/AepUIConstants.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/AepUIConstants.kt index dad0b836..800c3865 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/AepUIConstants.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/AepUIConstants.kt @@ -20,7 +20,7 @@ object AepUIConstants { const val CARD_CLICKED = "Card clicked" } - internal object DefaultStyle { + internal object DefaultAepUIStyle { const val IMAGE_WIDTH = 100 const val IMAGE_PROGRESS_SPINNER_SIZE = 48 const val TITLE_TEXT_SIZE = 15 @@ -30,7 +30,10 @@ object AepUIConstants { const val BUTTON_TEXT_SIZE = 13 val BUTTON_FONT_WEIGHT = FontWeight.Normal const val SPACING = 8 + const val DISMISS_BUTTON_SIZE = 13 } - const val DISMISS_BUTTON_SIZE = 13 + internal object DefaultAepContainerStyle { + const val CIRCULAR_PROGRESS_WIDTH = 30 + } } diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/BaseContainerUI.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/BaseContainerUI.kt new file mode 100644 index 00000000..41854142 --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/BaseContainerUI.kt @@ -0,0 +1,35 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui + +import androidx.compose.runtime.mutableStateOf +import com.adobe.marketing.mobile.aepcomposeui.state.AepContainerUIState +import com.adobe.marketing.mobile.aepcomposeui.uimodels.AepContainerUITemplate + +sealed class BaseContainerUI( + private val template: T, + state: S +) : AepContainerUI { + private val _state = mutableStateOf(state) + + override fun getTemplate(): T { + return template + } + + override fun getState(): S { + return _state.value + } + + override fun updateState(newState: S) { + _state.value = newState + } +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/InboxContainerUI.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/InboxContainerUI.kt new file mode 100644 index 00000000..f0251fbd --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/InboxContainerUI.kt @@ -0,0 +1,26 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui + +import com.adobe.marketing.mobile.aepcomposeui.state.InboxContainerUIState +import com.adobe.marketing.mobile.aepcomposeui.uimodels.InboxContainerUITemplate + +/** + * Implementation of the [AepContainerUI] interface used in rendering a UI for an [InboxContainerUITemplate]. + * + * @param template The template associated with the inbox container UI. + * @param state The current state of the inbox container UI. + */ +class InboxContainerUI( + private val template: InboxContainerUITemplate, + state: InboxContainerUIState +) : BaseContainerUI (template, state) diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepAsyncImage.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepAsyncImage.kt index 51a2ef56..2f04e4d4 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepAsyncImage.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepAsyncImage.kt @@ -44,13 +44,13 @@ import com.adobe.marketing.mobile.messaging.ContentCardImageManager */ @Composable internal fun AepAsyncImage( - image: AepImage?, + image: AepImage, imageStyle: AepImageStyle = AepImageStyle(), onSuccess: (Bitmap) -> Unit = {}, onError: (Throwable) -> Unit = {} ) { - val imageUrl = if (isSystemInDarkTheme() && image?.darkUrl != null) - image.darkUrl else image?.url + val imageUrl = if (isSystemInDarkTheme() && image.darkUrl != null) + image.darkUrl else image.url var imageBitmap by remember { mutableStateOf(null) } var isLoading by remember { mutableStateOf(true) } @@ -76,11 +76,11 @@ internal fun AepAsyncImage( if (isLoading) { Box( modifier = imageStyle.modifier ?: Modifier - .size(AepUIConstants.DefaultStyle.IMAGE_WIDTH.dp), + .size(AepUIConstants.DefaultAepUIStyle.IMAGE_WIDTH.dp), contentAlignment = Alignment.Center ) { CircularProgressIndicator( - modifier = Modifier.size(AepUIConstants.DefaultStyle.IMAGE_PROGRESS_SPINNER_SIZE.dp), + modifier = Modifier.size(AepUIConstants.DefaultAepUIStyle.IMAGE_PROGRESS_SPINNER_SIZE.dp), strokeWidth = 4.dp ) } diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepCircularProgressIndicator.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepCircularProgressIndicator.kt new file mode 100644 index 00000000..0bd9b8d8 --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepCircularProgressIndicator.kt @@ -0,0 +1,37 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.adobe.marketing.mobile.aepcomposeui.AepUIConstants + +/** + * A composable function that displays a centered circular progress indicator. + **/ +@Composable +fun AepCircularProgressIndicator() { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(AepUIConstants.DefaultAepContainerStyle.CIRCULAR_PROGRESS_WIDTH.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepContainer.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepContainer.kt new file mode 100644 index 00000000..ea8de1dc --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepContainer.kt @@ -0,0 +1,46 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.components + +import androidx.compose.runtime.Composable +import com.adobe.marketing.mobile.aepcomposeui.AepContainerUI +import com.adobe.marketing.mobile.aepcomposeui.InboxContainerUI +import com.adobe.marketing.mobile.aepcomposeui.observers.AepUIEventObserver +import com.adobe.marketing.mobile.aepcomposeui.style.AepUIStyle +import com.adobe.marketing.mobile.aepcomposeui.style.ContainerStyle + +/** + * AEP Container Composable that renders the appropriate container based on the type of [AepContainerUI] provided. + * + * @param containerUi The AEP container UI model to be rendered. + * @param containerStyle The style to be applied to the container. + * @param itemsStyle The style to be applied to the cards within the container. + * @param cardUIEventListener An optional event listener for content card UI events. + */ +@Composable +fun AepContainer( + containerUi: AepContainerUI<*, *>, + containerStyle: ContainerStyle = ContainerStyle(), + itemsStyle: AepUIStyle = AepUIStyle(), + observer: AepUIEventObserver? = null +) { + when (containerUi) { + is InboxContainerUI -> { + InboxContainer( + ui = containerUi, + inboxContainerStyle = containerStyle.inboxContainerUIStyle, + itemsStyle = itemsStyle, + observer = observer + ) + } + } +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepLazyColumn.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepLazyColumn.kt new file mode 100644 index 00000000..08889896 --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepLazyColumn.kt @@ -0,0 +1,52 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.components + +import androidx.compose.foundation.gestures.ScrollableDefaults +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.adobe.marketing.mobile.aepcomposeui.style.AepLazyColumnStyle + +/** + * A composable function that displays a lazy column element with customizable properties. + * + * @param lazyColumnStyle The [AepLazyColumnStyle] to be applied to the lazy column element. + * @param content The content of the lazy column. + */ +@Composable +internal fun AepLazyColumn( + lazyColumnStyle: AepLazyColumnStyle = AepLazyColumnStyle(), + content: LazyListScope.() -> Unit +) { + LazyColumn( + modifier = lazyColumnStyle.modifier ?: Modifier, + contentPadding = lazyColumnStyle.contentPadding ?: PaddingValues(0.dp), + reverseLayout = lazyColumnStyle.reverseLayout ?: false, + verticalArrangement = lazyColumnStyle.verticalArrangement ?: getDefaultVerticalArrangement(lazyColumnStyle.reverseLayout ?: false), + horizontalAlignment = lazyColumnStyle.horizontalAlignment ?: Alignment.Start, + flingBehavior = lazyColumnStyle.flingBehavior ?: ScrollableDefaults.flingBehavior(), + userScrollEnabled = lazyColumnStyle.userScrollEnabled ?: true, + content = content + ) +} + +/** + * Helper function to get the default vertical arrangement based on reverse layout. + */ +private fun getDefaultVerticalArrangement(reverseLayout: Boolean): Arrangement.Vertical = + if (reverseLayout) Arrangement.Bottom else Arrangement.Top diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepListItems.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepListItems.kt new file mode 100644 index 00000000..fe4492c8 --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/AepListItems.kt @@ -0,0 +1,97 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.adobe.marketing.mobile.aepcomposeui.AepUI +import com.adobe.marketing.mobile.aepcomposeui.ImageOnlyUI +import com.adobe.marketing.mobile.aepcomposeui.LargeImageUI +import com.adobe.marketing.mobile.aepcomposeui.SmallImageUI +import com.adobe.marketing.mobile.aepcomposeui.observers.AepUIEventObserver +import com.adobe.marketing.mobile.aepcomposeui.style.AepImageStyle +import com.adobe.marketing.mobile.aepcomposeui.style.AepUIStyle +import com.adobe.marketing.mobile.aepcomposeui.uimodels.AepImage + +/** + * Renders a list of AEP UI items within a LazyListScope. + * + * @param items The list of [AepUI] items to be rendered. + * @param itemsStyle The style to be applied to the items. + * @param unreadItemsStyle An optional style to be applied to unread items. + * @param unreadIcon An optional Triple containing the unread icon [AepImage], its [AepImageStyle], and its [Alignment]. + * @param observer An optional observer that listens to UI events. + */ +fun LazyListScope.renderListItems( + items: List>, + itemsStyle: AepUIStyle, + unreadItemsStyle: AepUIStyle = itemsStyle, + unreadIcon: Triple? = null, + observer: AepUIEventObserver? +) { + items(items = items) { aepUI -> + val state = aepUI.getState() + if (!state.dismissed) { + Box(modifier = Modifier.padding(0.dp)) { + val read = aepUI.getState().read + when (aepUI) { + is SmallImageUI -> { + // Use read or unread style based on UI state + val style = + if (read == false) unreadItemsStyle.smallImageUIStyle else itemsStyle.smallImageUIStyle + SmallImageCard( + ui = aepUI, + style = style, + observer = observer + ) + } + + is LargeImageUI -> { + // Use read or unread style based on UI state + val style = + if (read == false) unreadItemsStyle.largeImageUIStyle else itemsStyle.largeImageUIStyle + LargeImageCard( + ui = aepUI, + style = style, + observer = observer + ) + } + + is ImageOnlyUI -> { + // Use read or unread style based on UI state + val style = + if (read == false) unreadItemsStyle.imageOnlyUIStyle else itemsStyle.imageOnlyUIStyle + ImageOnlyCard( + ui = aepUI, + style = style, + observer = observer + ) + } + } + // Display unread icon one is provided and if the item is unread + if (read != null && !read && unreadIcon != null) { + Box(modifier = Modifier.align(unreadIcon.third)) { + AepAsyncImage( + image = unreadIcon.first, + imageStyle = unreadIcon.second + ) + } + } + } + } + } +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/EmptyContainer.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/EmptyContainer.kt new file mode 100644 index 00000000..ec8dedd5 --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/EmptyContainer.kt @@ -0,0 +1,68 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.adobe.marketing.mobile.aepcomposeui.style.AepImageStyle +import com.adobe.marketing.mobile.aepcomposeui.style.AepTextStyle +import com.adobe.marketing.mobile.aepcomposeui.uimodels.AepImage +import com.adobe.marketing.mobile.aepcomposeui.uimodels.AepText + +/** + * Composable that renders an empty container if the empty message or image is provided. + */ +@Composable +fun EmptyInboxContainer( + emptyMessage: AepText? = null, + emptyMessageStyle: AepTextStyle = AepTextStyle(), + emptyImage: AepImage? = null, + emptyImageStyle: AepImageStyle = AepImageStyle() +) { + if (emptyMessage != null || emptyImage != null) { + // Wrap AepText in an invisible Surface to provide Material Theme context + Surface( + color = Color.Transparent, + modifier = Modifier.fillMaxWidth(), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + emptyMessage?.let { + AepText( + model = it, + textStyle = emptyMessageStyle + ) + } + Spacer(modifier = Modifier.height(8.dp)) + emptyImage?.let { + AepAsyncImage( + image = it, + imageStyle = emptyImageStyle + ) + } + } + } + } +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/InboxContainer.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/InboxContainer.kt new file mode 100644 index 00000000..c64550a9 --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/InboxContainer.kt @@ -0,0 +1,212 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.components + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import com.adobe.marketing.mobile.aepcomposeui.InboxContainerUI +import com.adobe.marketing.mobile.aepcomposeui.observers.AepUIEventObserver +import com.adobe.marketing.mobile.aepcomposeui.state.InboxContainerUIState +import com.adobe.marketing.mobile.aepcomposeui.style.AepCardStyle +import com.adobe.marketing.mobile.aepcomposeui.style.AepUIStyle +import com.adobe.marketing.mobile.aepcomposeui.style.ImageOnlyUIStyle +import com.adobe.marketing.mobile.aepcomposeui.style.InboxContainerUIStyle +import com.adobe.marketing.mobile.aepcomposeui.style.LargeImageUIStyle +import com.adobe.marketing.mobile.aepcomposeui.style.SmallImageUIStyle +import com.adobe.marketing.mobile.aepcomposeui.uimodels.AepText + +/** + * Composable that renders the inbox container UI. + * + * @param ui The [InboxContainerUI] to be rendered. + * @param inboxContainerStyle The [InboxContainerUIStyle] to be applied to the inbox container. + * @param itemsStyle The [AepUIStyle] to be applied to the items within the inbox container. + * @param observer An optional [AepUIEventObserver] to handle UI events. + */ +@Composable +internal fun InboxContainer( + ui: InboxContainerUI, + inboxContainerStyle: InboxContainerUIStyle, + itemsStyle: AepUIStyle, + observer: AepUIEventObserver? +) { + val inboxContainerSettings = ui.getTemplate() + val inboxContainerState = ui.getState() + when (inboxContainerState) { + is InboxContainerUIState.Loading -> { + inboxContainerStyle.loadingIndicator() + } + + is InboxContainerUIState.Error -> { + EmptyInboxContainer( + // todo: Replace these with server provided error message and style if we add it to the container settings UI + AepText("Error fetching messages"), + inboxContainerStyle.emptyMessageStyle + ) + } + + is InboxContainerUIState.Success -> { + // Limit the number of items to the specified capacity + val uiList = inboxContainerState.items.take(inboxContainerSettings.capacity) + + // Determine unread card background color based on theme, server settings, and style overrides + val isDarkTheme = isSystemInDarkTheme() + val unreadCardColor = remember( + isDarkTheme, + inboxContainerStyle.unreadBgColor, + inboxContainerSettings.unreadBgColor + ) { + if (isDarkTheme) { + inboxContainerStyle.unreadBgColor?.darkColor + ?: inboxContainerSettings.unreadBgColor?.darkColor + } else { + inboxContainerStyle.unreadBgColor?.lightColor + ?: inboxContainerSettings.unreadBgColor?.lightColor + } + } + + Column { + // Wrap AepText in an invisible Surface to provide Material Theme context + Surface( + color = Color.Transparent + ) { + AepText( + model = inboxContainerSettings.heading, + textStyle = inboxContainerStyle.headingStyle + ) + } + + // Create unread style variant if unread color is specified + val unreadCardsStyle = createUnreadCardsStyle(itemsStyle, unreadCardColor) + + if (uiList.isEmpty()) { + EmptyInboxContainer( + inboxContainerSettings.emptyMessage, + inboxContainerStyle.emptyMessageStyle, + inboxContainerSettings.emptyImage, + inboxContainerStyle.emptyImageStyle + ) + } else { + AepLazyColumn( + lazyColumnStyle = inboxContainerStyle.lazyColumnStyle + ) { + renderListItems( + items = uiList, + itemsStyle = itemsStyle, + unreadItemsStyle = unreadCardsStyle, + unreadIcon = if (inboxContainerSettings.unreadIcon != null) Triple( + inboxContainerSettings.unreadIcon, + inboxContainerStyle.unreadIconStyle, + inboxContainerStyle.unreadIconAlignment + ?: inboxContainerSettings.unreadIconAlignment ?: Alignment.TopStart + ) + else null, + observer = observer + ) + } + } + } + } + } +} + +/** + * Creates unread card styles with the specified unread color, or returns the original styles if no unread color is provided. + */ +@Composable +private fun createUnreadCardsStyle(cardsStyle: AepUIStyle, unreadCardColor: Color?): AepUIStyle { + if (unreadCardColor == null) { + return cardsStyle + } + + val unreadSmallImageStyle = createUnreadSmallImageStyle(cardsStyle.smallImageUIStyle, unreadCardColor) + val unreadLargeImageStyle = createUnreadLargeImageStyle(cardsStyle.largeImageUIStyle, unreadCardColor) + val unreadImageOnlyStyle = createUnreadImageOnlyStyle(cardsStyle.imageOnlyUIStyle, unreadCardColor) + + return AepUIStyle( + smallImageUIStyle = unreadSmallImageStyle, + largeImageUIStyle = unreadLargeImageStyle, + imageOnlyUIStyle = unreadImageOnlyStyle + ) +} + +@Composable +private fun createUnreadSmallImageStyle(originalStyle: SmallImageUIStyle, unreadCardColor: Color): SmallImageUIStyle { + val unreadCardStyle = AepCardStyle( + modifier = originalStyle.cardStyle.modifier, + enabled = originalStyle.cardStyle.enabled, + shape = originalStyle.cardStyle.shape, + colors = CardDefaults.cardColors(unreadCardColor), + elevation = originalStyle.cardStyle.elevation, + border = originalStyle.cardStyle.border + ) + return SmallImageUIStyle.Builder() + .cardStyle(unreadCardStyle) + .imageStyle(originalStyle.imageStyle) + .rootRowStyle(originalStyle.rootRowStyle) + .textColumnStyle(originalStyle.textColumnStyle) + .titleAepTextStyle(originalStyle.titleTextStyle) + .bodyAepTextStyle(originalStyle.bodyTextStyle) + .buttonRowStyle(originalStyle.buttonRowStyle) + .buttonStyle(originalStyle.buttonStyle.map { it }.toTypedArray()) + .dismissButtonStyle(originalStyle.dismissButtonStyle) + .dismissButtonAlignment(originalStyle.dismissButtonAlignment) + .build() +} + +@Composable +private fun createUnreadLargeImageStyle(originalStyle: LargeImageUIStyle, unreadCardColor: Color): LargeImageUIStyle { + val unreadCardStyle = AepCardStyle( + modifier = originalStyle.cardStyle.modifier, + enabled = originalStyle.cardStyle.enabled, + shape = originalStyle.cardStyle.shape, + colors = CardDefaults.cardColors(unreadCardColor), + elevation = originalStyle.cardStyle.elevation, + border = originalStyle.cardStyle.border + ) + return LargeImageUIStyle.Builder() + .cardStyle(unreadCardStyle) + .imageStyle(originalStyle.imageStyle) + .rootColumnStyle(originalStyle.rootColumnStyle) + .textColumnStyle(originalStyle.textColumnStyle) + .titleAepTextStyle(originalStyle.titleTextStyle) + .bodyAepTextStyle(originalStyle.bodyTextStyle) + .buttonRowStyle(originalStyle.buttonRowStyle) + .buttonStyle(originalStyle.buttonStyle.map { it }.toTypedArray()) + .dismissButtonStyle(originalStyle.dismissButtonStyle) + .dismissButtonAlignment(originalStyle.dismissButtonAlignment) + .build() +} + +@Composable +private fun createUnreadImageOnlyStyle(originalStyle: ImageOnlyUIStyle, unreadCardColor: Color): ImageOnlyUIStyle { + val unreadCardStyle = AepCardStyle( + modifier = originalStyle.cardStyle.modifier, + enabled = originalStyle.cardStyle.enabled, + shape = originalStyle.cardStyle.shape, + colors = CardDefaults.cardColors(unreadCardColor), + elevation = originalStyle.cardStyle.elevation, + border = originalStyle.cardStyle.border + ) + return ImageOnlyUIStyle.Builder() + .cardStyle(unreadCardStyle) + .imageStyle(originalStyle.imageStyle) + .dismissButtonStyle(originalStyle.dismissButtonStyle) + .dismissButtonAlignment(originalStyle.dismissButtonAlignment) + .build() +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/LargeImageCard.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/LargeImageCard.kt index 822d9599..157d67c2 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/LargeImageCard.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/LargeImageCard.kt @@ -57,10 +57,12 @@ fun LargeImageCard( AepColumn( columnStyle = style.rootColumnStyle ) { - AepAsyncImage( - image = ui.getTemplate().image, - imageStyle = style.imageStyle - ) + ui.getTemplate().image?.let { + AepAsyncImage( + image = it, + imageStyle = style.imageStyle + ) + } AepColumn( columnStyle = style.textColumnStyle ) { diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCard.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCard.kt index 9a6c90f7..57d9ee8a 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCard.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCard.kt @@ -57,10 +57,12 @@ fun SmallImageCard( AepRow( rowStyle = style.rootRowStyle ) { - AepAsyncImage( - image = ui.getTemplate().image, - imageStyle = style.imageStyle - ) + ui.getTemplate().image?.let { + AepAsyncImage( + image = it, + imageStyle = style.imageStyle + ) + } AepColumn( columnStyle = style.textColumnStyle diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/AepCardUIState.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/AepCardUIState.kt index 29095e76..99114c6d 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/AepCardUIState.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/AepCardUIState.kt @@ -18,9 +18,10 @@ package com.adobe.marketing.mobile.aepcomposeui.state * * @property dismissed Indicates whether the card has been dismissed. * @property displayed Indicates whether the card is currently displayed. + * @property read Indicates whether the card has been read. */ open class AepCardUIState( open val dismissed: Boolean, open val displayed: Boolean, - open val read: Boolean + open val read: Boolean? ) diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/AepContainerUIState.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/AepContainerUIState.kt index 099e895e..13ef90ca 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/AepContainerUIState.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/AepContainerUIState.kt @@ -24,12 +24,6 @@ import com.adobe.marketing.mobile.aepcomposeui.AepUI */ sealed interface AepContainerUIState { - /** - * Represents the loading state of the container UI. - * Can be extended by specific container types to include loading-specific information. - */ - interface Loading : AepContainerUIState - /** * Represents the successful state of the container UI. * Can be extended by specific container types to include their own success data. diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/ImageOnlyCardUIState.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/ImageOnlyCardUIState.kt index c489b324..4caedaed 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/ImageOnlyCardUIState.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/ImageOnlyCardUIState.kt @@ -20,5 +20,5 @@ package com.adobe.marketing.mobile.aepcomposeui.state data class ImageOnlyCardUIState( override val dismissed: Boolean = false, override val displayed: Boolean = false, - override val read: Boolean = false + override val read: Boolean? = null ) : AepCardUIState(dismissed, displayed, read) diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/InboxContainerUIState.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/InboxContainerUIState.kt new file mode 100644 index 00000000..3ddae9bd --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/InboxContainerUIState.kt @@ -0,0 +1,44 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.state + +import com.adobe.marketing.mobile.aepcomposeui.AepUI + +/** + * Sealed interface representing different states for Inbox Container UI. + * Extends the base AepContainerUIState to provide inbox-specific state management. + */ +sealed interface InboxContainerUIState : AepContainerUIState { + + /** + * Loading state for the inbox container. + */ + object Loading : InboxContainerUIState + + /** + * Success state for the inbox container. + * + * @param items List of AEP UI elements to display in the inbox + */ + data class Success( + override val items: List> = emptyList() + ) : AepContainerUIState.Success, InboxContainerUIState + + /** + * Error state for the inbox container. + * + * @param error The throwable that caused the error + */ + data class Error( + override val error: Throwable + ) : AepContainerUIState.Error, InboxContainerUIState +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/LargeImageCardUIState.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/LargeImageCardUIState.kt index a3da17e5..1df5bef9 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/LargeImageCardUIState.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/LargeImageCardUIState.kt @@ -20,5 +20,5 @@ package com.adobe.marketing.mobile.aepcomposeui.state data class LargeImageCardUIState( override val dismissed: Boolean = false, override val displayed: Boolean = false, - override val read: Boolean = false + override val read: Boolean? = null ) : AepCardUIState(dismissed, displayed, read) diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/SmallImageCardUIState.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/SmallImageCardUIState.kt index f009cebd..c1fd4c4a 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/SmallImageCardUIState.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/state/SmallImageCardUIState.kt @@ -20,5 +20,5 @@ package com.adobe.marketing.mobile.aepcomposeui.state data class SmallImageCardUIState( override val dismissed: Boolean = false, override val displayed: Boolean = false, - override val read: Boolean = false + override val read: Boolean? = null ) : AepCardUIState(dismissed, displayed, read) diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/AepLazyColumnStyle.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/AepLazyColumnStyle.kt new file mode 100644 index 00000000..a7e12179 --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/AepLazyColumnStyle.kt @@ -0,0 +1,56 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.style + +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +/** + * Class representing the style for an AEP LazyColumn component. + * + * @param modifier The modifier for the LazyColumn. + * @param contentPadding The padding values for the content inside the LazyColumn. + * @param reverseLayout Whether the layout should be reversed. + * @param verticalArrangement The vertical arrangement of the items in the LazyColumn. + * @param horizontalAlignment The horizontal alignment of the items in the LazyColumn. + * @param flingBehavior The fling behavior for the LazyColumn. + * @param userScrollEnabled Whether user scrolling is enabled for the LazyColumn. + */ +class AepLazyColumnStyle( + var modifier: Modifier? = null, + var contentPadding: PaddingValues? = null, + var reverseLayout: Boolean? = null, + var verticalArrangement: Arrangement.Vertical? = null, + var horizontalAlignment: Alignment.Horizontal? = null, + var flingBehavior: FlingBehavior? = null, + var userScrollEnabled: Boolean? = null +) { + companion object { + internal fun merge(defaultStyle: AepLazyColumnStyle, overridingStyle: AepLazyColumnStyle?): AepLazyColumnStyle { + if (overridingStyle == null) { + return defaultStyle + } + return AepLazyColumnStyle( + modifier = overridingStyle.modifier ?: defaultStyle.modifier, + contentPadding = overridingStyle.contentPadding ?: defaultStyle.contentPadding, + reverseLayout = overridingStyle.reverseLayout ?: defaultStyle.reverseLayout, + verticalArrangement = overridingStyle.verticalArrangement ?: defaultStyle.verticalArrangement, + horizontalAlignment = overridingStyle.horizontalAlignment ?: defaultStyle.horizontalAlignment, + flingBehavior = overridingStyle.flingBehavior ?: defaultStyle.flingBehavior, + userScrollEnabled = overridingStyle.userScrollEnabled ?: defaultStyle.userScrollEnabled + ) + } + } +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/AepUIStyle.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/AepUIStyle.kt index 9647d007..7370b7f5 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/AepUIStyle.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/AepUIStyle.kt @@ -11,13 +11,19 @@ package com.adobe.marketing.mobile.aepcomposeui.style +import com.adobe.marketing.mobile.aepcomposeui.components.ImageOnlyCard +import com.adobe.marketing.mobile.aepcomposeui.components.LargeImageCard +import com.adobe.marketing.mobile.aepcomposeui.components.SmallImageCard + /** * Enumerates the style configuration for all supported types of AEP UI components. * - * This class provides the style configurations for different AEP UIs, such as small image components. - * - * @property smallImageUiStyle The style configuration for small image AEP UIs. + * @param smallImageUIStyle The [SmallImageUIStyle] with configuration for [SmallImageCard]. + * @param smallImageUIStyle The [LargeImageUIStyle] with configuration for [LargeImageCard]. + * @param imageOnlyUIStyle Thr [ImageOnlyUIStyle] with configuration for [ImageOnlyCard]. */ class AepUIStyle( - val smallImageUiStyle: SmallImageUIStyle = SmallImageUIStyle.Builder().build(), + val smallImageUIStyle: SmallImageUIStyle = SmallImageUIStyle.Builder().build(), + val largeImageUIStyle: LargeImageUIStyle = LargeImageUIStyle.Builder().build(), + val imageOnlyUIStyle: ImageOnlyUIStyle = ImageOnlyUIStyle.Builder().build() ) diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/ContainerStyle.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/ContainerStyle.kt new file mode 100644 index 00000000..9b844c49 --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/ContainerStyle.kt @@ -0,0 +1,21 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.style + +/** + * Enumerates the style configuration for all supported types of AEP container UI components. + * + * @param inboxContainerUIStyle The [InboxContainerUIStyle] with configuration for Inbox Container UI. + */ +class ContainerStyle( + val inboxContainerUIStyle: InboxContainerUIStyle = InboxContainerUIStyle.Builder().build(), +) diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/ImageOnlyUIStyle.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/ImageOnlyUIStyle.kt index 06f74c30..a90af29a 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/ImageOnlyUIStyle.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/ImageOnlyUIStyle.kt @@ -24,8 +24,8 @@ import com.adobe.marketing.mobile.aepcomposeui.AepUIConstants /** * Class representing the style for a image only AEP UI. * - * @param cardStyle The style for the card. - * @param imageStyle The style for the image. + * @property cardStyle The style for the card. + * @property imageStyle The style for the image. * @property dismissButtonStyle The style for the dismiss button. * @property dismissButtonAlignment The alignment for the dismiss button. */ @@ -37,18 +37,18 @@ class ImageOnlyUIStyle private constructor( ) { companion object { private val defaultCardStyle = AepCardStyle( - modifier = Modifier.padding(AepUIConstants.DefaultStyle.SPACING.dp), + modifier = Modifier.padding(AepUIConstants.DefaultAepUIStyle.SPACING.dp), shape = RoundedCornerShape(0.dp) ) private val defaultImageStyle = AepImageStyle( - modifier = Modifier.width(AepUIConstants.DefaultStyle.IMAGE_WIDTH.dp), + modifier = Modifier.width(AepUIConstants.DefaultAepUIStyle.IMAGE_WIDTH.dp), contentScale = ContentScale.Fit, alignment = Alignment.Center ) private val defaultDismissButtonStyle = AepIconStyle( modifier = Modifier - .padding(AepUIConstants.DefaultStyle.SPACING.dp) - .size(AepUIConstants.DISMISS_BUTTON_SIZE.dp) + .padding(AepUIConstants.DefaultAepUIStyle.SPACING.dp) + .size(AepUIConstants.DefaultAepUIStyle.DISMISS_BUTTON_SIZE.dp) ) private val defaultDismissButtonAlignment = Alignment.TopEnd } diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/InboxContainerUIStyle.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/InboxContainerUIStyle.kt new file mode 100644 index 00000000..9d61d75d --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/InboxContainerUIStyle.kt @@ -0,0 +1,110 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.style + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.adobe.marketing.mobile.aepcomposeui.AepUIConstants +import com.adobe.marketing.mobile.aepcomposeui.components.AepCircularProgressIndicator +import com.adobe.marketing.mobile.aepcomposeui.uimodels.AepColor + +/** + * Class representing the style for the Inbox Container. + * + * @property loadingIndicator The composable function representing the loading indicator. + * @property headingStyle The style for the heading text. + * @property lazyColumnStyle The style for the lazy column displaying messages. + * @property emptyMessageStyle The style for the empty message text. + * @property emptyImageStyle The style for the empty state image. + * @property unreadIconStyle The style for the unread icon. + * @property unreadIconAlignment The alignment for the unread icon. + * @property unreadBgColor The background color for unread messages. + */ +class InboxContainerUIStyle private constructor( + val loadingIndicator: @Composable () -> Unit, + val headingStyle: AepTextStyle, + val lazyColumnStyle: AepLazyColumnStyle, + val emptyMessageStyle: AepTextStyle, + val emptyImageStyle: AepImageStyle, + val unreadIconStyle: AepImageStyle, + val unreadIconAlignment: Alignment?, + val unreadBgColor: AepColor? +) { + companion object { + private val defaultHeadingStyle = AepTextStyle( + textStyle = TextStyle( + fontSize = AepUIConstants.DefaultAepUIStyle.TITLE_TEXT_SIZE.sp, + fontWeight = AepUIConstants.DefaultAepUIStyle.TITLE_FONT_WEIGHT + ) + ) + private val defaultListStyle = AepLazyColumnStyle() + private val defaultEmptyMessageStyle = AepTextStyle() + private val defaultEmptyImageStyle = AepImageStyle() + private val defaultUnreadIconStyle = AepImageStyle( + modifier = Modifier + .padding(AepUIConstants.DefaultAepUIStyle.SPACING.dp + 5.dp) + .size(20.dp) + ) + private val defaultLoadingIndicator: @Composable () -> Unit = { + AepCircularProgressIndicator() + } + } + + class Builder { + private var headingStyle: AepTextStyle? = null + private var lazyColumnStyle: AepLazyColumnStyle? = null + private var emptyMessageStyle: AepTextStyle? = null + private var emptyImageStyle: AepImageStyle? = null + private var unreadIconStyle: AepImageStyle? = null + private var unreadIconAlignment: Alignment? = null + private var unreadBgColor: AepColor? = null + private var loadingIndicator: (@Composable () -> Unit)? = null + + fun headingStyle(headingStyle: AepTextStyle) = apply { this.headingStyle = headingStyle } + + fun lazyColumnStyle(listStyle: AepLazyColumnStyle) = apply { this.lazyColumnStyle = listStyle } + + fun emptyMessageStyle(emptyMessageStyle: AepTextStyle) = + apply { this.emptyMessageStyle = emptyMessageStyle } + + fun emptyImageStyle(emptyImageStyle: AepImageStyle) = + apply { this.emptyImageStyle = emptyImageStyle } + + fun unreadIconStyle(unreadIconStyle: AepImageStyle) = + apply { this.unreadIconStyle = unreadIconStyle } + + fun unreadIconAlignment(unreadIconAlignment: Alignment) = + apply { this.unreadIconAlignment = unreadIconAlignment } + + fun unreadBgColor(unreadBgColor: AepColor) = apply { this.unreadBgColor = unreadBgColor } + + fun loadingIndicator(loadingIndicator: @Composable () -> Unit) = + apply { this.loadingIndicator = loadingIndicator } + + fun build() = InboxContainerUIStyle( + headingStyle = AepTextStyle.merge(defaultHeadingStyle, headingStyle), + lazyColumnStyle = AepLazyColumnStyle.merge(defaultListStyle, lazyColumnStyle), + emptyMessageStyle = AepTextStyle.merge(defaultEmptyMessageStyle, emptyMessageStyle), + emptyImageStyle = AepImageStyle.merge(defaultEmptyImageStyle, emptyImageStyle), + unreadIconStyle = AepImageStyle.merge(defaultUnreadIconStyle, unreadIconStyle), + unreadIconAlignment = unreadIconAlignment, + unreadBgColor = unreadBgColor, + loadingIndicator = loadingIndicator ?: defaultLoadingIndicator + ) + } +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/LargeImageUIStyle.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/LargeImageUIStyle.kt index ffa89622..45b4c7e0 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/LargeImageUIStyle.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/LargeImageUIStyle.kt @@ -28,10 +28,10 @@ import com.adobe.marketing.mobile.aepcomposeui.AepUIConstants /** * Class representing the style for a large image AEP UI. * - * @param cardStyle The style for the card. - * @param rootColumnStyle The style for the root row. - * @param imageStyle The style for the image. - * @param textColumnStyle The style for the column containing the title, body and buttons. + * @property cardStyle The style for the card. + * @property rootColumnStyle The style for the root column. + * @property imageStyle The style for the image. + * @property textColumnStyle The style for the column containing the title, body and buttons. * @property titleTextStyle The text style for the title. * @property bodyTextStyle The text style for the body. * @property buttonRowStyle The style for the row containing the buttons. @@ -53,11 +53,11 @@ class LargeImageUIStyle private constructor( ) { companion object { private val defaultCardStyle = AepCardStyle( - modifier = Modifier.padding(AepUIConstants.DefaultStyle.SPACING.dp) + modifier = Modifier.padding(AepUIConstants.DefaultAepUIStyle.SPACING.dp) ) private val defaultRootColumnStyle = AepColumnStyle( verticalArrangement = Arrangement.spacedBy( - AepUIConstants.DefaultStyle.SPACING.dp, + AepUIConstants.DefaultAepUIStyle.SPACING.dp, Alignment.CenterVertically ), horizontalAlignment = Alignment.CenterHorizontally @@ -68,35 +68,35 @@ class LargeImageUIStyle private constructor( private val defaultTextColumnStyle = AepColumnStyle( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy( - AepUIConstants.DefaultStyle.SPACING.dp, + AepUIConstants.DefaultAepUIStyle.SPACING.dp, Alignment.CenterVertically ), horizontalAlignment = Alignment.Start ) private val defaultTitleAepTextStyle = AepTextStyle( textStyle = TextStyle( - fontSize = AepUIConstants.DefaultStyle.TITLE_TEXT_SIZE.sp, - fontWeight = AepUIConstants.DefaultStyle.TITLE_FONT_WEIGHT + fontSize = AepUIConstants.DefaultAepUIStyle.TITLE_TEXT_SIZE.sp, + fontWeight = AepUIConstants.DefaultAepUIStyle.TITLE_FONT_WEIGHT ) ) private val defaultBodyAepTextStyle = AepTextStyle( textStyle = TextStyle( - fontSize = AepUIConstants.DefaultStyle.BODY_TEXT_SIZE.sp, - fontWeight = AepUIConstants.DefaultStyle.BODY_FONT_WEIGHT, + fontSize = AepUIConstants.DefaultAepUIStyle.BODY_TEXT_SIZE.sp, + fontWeight = AepUIConstants.DefaultAepUIStyle.BODY_FONT_WEIGHT, ) ) private val defaultButtonRowStyle = AepRowStyle( modifier = Modifier.width(IntrinsicSize.Min), horizontalArrangement = Arrangement.spacedBy( - AepUIConstants.DefaultStyle.SPACING.dp, + AepUIConstants.DefaultAepUIStyle.SPACING.dp, Alignment.CenterHorizontally ), verticalAlignment = Alignment.CenterVertically ) private val defaultButtonTextStyle = AepTextStyle( textStyle = TextStyle( - fontSize = AepUIConstants.DefaultStyle.BUTTON_TEXT_SIZE.sp, - fontWeight = AepUIConstants.DefaultStyle.BUTTON_FONT_WEIGHT, + fontSize = AepUIConstants.DefaultAepUIStyle.BUTTON_TEXT_SIZE.sp, + fontWeight = AepUIConstants.DefaultAepUIStyle.BUTTON_FONT_WEIGHT, ) ) private val defaultButtonStyle = AepButtonStyle( @@ -104,8 +104,8 @@ class LargeImageUIStyle private constructor( ) private val defaultDismissButtonStyle = AepIconStyle( modifier = Modifier - .padding(AepUIConstants.DefaultStyle.SPACING.dp) - .size(AepUIConstants.DISMISS_BUTTON_SIZE.dp) + .padding(AepUIConstants.DefaultAepUIStyle.SPACING.dp) + .size(AepUIConstants.DefaultAepUIStyle.DISMISS_BUTTON_SIZE.dp) ) private val defaultDismissButtonAlignment = Alignment.TopEnd } diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/SmallImageUIStyle.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/SmallImageUIStyle.kt index 84485eab..b812b32a 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/SmallImageUIStyle.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/style/SmallImageUIStyle.kt @@ -27,10 +27,10 @@ import com.adobe.marketing.mobile.aepcomposeui.AepUIConstants /** * Class representing the style for a small image AEP UI. * - * @param cardStyle The style for the card. - * @param rootRowStyle The style for the root row. - * @param imageStyle The style for the image. - * @param textColumnStyle The style for the column containing the title, body and buttons. + * @property cardStyle The style for the card. + * @property rootRowStyle The style for the root row. + * @property imageStyle The style for the image. + * @property textColumnStyle The style for the column containing the title, body and buttons. * @property titleTextStyle The text style for the title. * @property bodyTextStyle The text style for the body. * @property buttonRowStyle The style for the row containing the buttons. @@ -52,51 +52,51 @@ class SmallImageUIStyle private constructor( ) { companion object { private val defaultCardStyle = AepCardStyle( - modifier = Modifier.padding(AepUIConstants.DefaultStyle.SPACING.dp) + modifier = Modifier.padding(AepUIConstants.DefaultAepUIStyle.SPACING.dp) ) private val defaultRootRowStyle = AepRowStyle( horizontalArrangement = Arrangement.spacedBy( - AepUIConstants.DefaultStyle.SPACING.dp, + AepUIConstants.DefaultAepUIStyle.SPACING.dp, Alignment.CenterHorizontally ), verticalAlignment = Alignment.CenterVertically ) private val defaultImageStyle = AepImageStyle( - modifier = Modifier.width(AepUIConstants.DefaultStyle.IMAGE_WIDTH.dp), + modifier = Modifier.width(AepUIConstants.DefaultAepUIStyle.IMAGE_WIDTH.dp), contentScale = ContentScale.Fit, alignment = Alignment.Center ) private val defaultTextColumnStyle = AepColumnStyle( verticalArrangement = Arrangement.spacedBy( - AepUIConstants.DefaultStyle.SPACING.dp, + AepUIConstants.DefaultAepUIStyle.SPACING.dp, Alignment.CenterVertically ), horizontalAlignment = Alignment.Start, ) private val defaultTitleAepTextStyle = AepTextStyle( textStyle = TextStyle( - fontSize = AepUIConstants.DefaultStyle.TITLE_TEXT_SIZE.sp, - fontWeight = AepUIConstants.DefaultStyle.TITLE_FONT_WEIGHT + fontSize = AepUIConstants.DefaultAepUIStyle.TITLE_TEXT_SIZE.sp, + fontWeight = AepUIConstants.DefaultAepUIStyle.TITLE_FONT_WEIGHT ) ) private val defaultBodyAepTextStyle = AepTextStyle( textStyle = TextStyle( - fontSize = AepUIConstants.DefaultStyle.BODY_TEXT_SIZE.sp, - fontWeight = AepUIConstants.DefaultStyle.BODY_FONT_WEIGHT, + fontSize = AepUIConstants.DefaultAepUIStyle.BODY_TEXT_SIZE.sp, + fontWeight = AepUIConstants.DefaultAepUIStyle.BODY_FONT_WEIGHT, ) ) private val defaultButtonRowStyle = AepRowStyle( modifier = Modifier.width(IntrinsicSize.Min), horizontalArrangement = Arrangement.spacedBy( - AepUIConstants.DefaultStyle.SPACING.dp, + AepUIConstants.DefaultAepUIStyle.SPACING.dp, Alignment.CenterHorizontally ), verticalAlignment = Alignment.CenterVertically ) private val defaultButtonTextStyle = AepTextStyle( textStyle = TextStyle( - fontSize = AepUIConstants.DefaultStyle.BUTTON_TEXT_SIZE.sp, - fontWeight = AepUIConstants.DefaultStyle.BUTTON_FONT_WEIGHT, + fontSize = AepUIConstants.DefaultAepUIStyle.BUTTON_TEXT_SIZE.sp, + fontWeight = AepUIConstants.DefaultAepUIStyle.BUTTON_FONT_WEIGHT, ) ) private val defaultButtonStyle = AepButtonStyle( @@ -104,8 +104,8 @@ class SmallImageUIStyle private constructor( ) private val defaultDismissButtonStyle = AepIconStyle( modifier = Modifier - .padding(AepUIConstants.DefaultStyle.SPACING.dp) - .size(AepUIConstants.DISMISS_BUTTON_SIZE.dp) + .padding(AepUIConstants.DefaultAepUIStyle.SPACING.dp) + .size(AepUIConstants.DefaultAepUIStyle.DISMISS_BUTTON_SIZE.dp) ) private val defaultDismissButtonAlignment = Alignment.TopEnd } diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/AepColor.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/AepColor.kt index 2adfe505..b62f1ddd 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/AepColor.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/AepColor.kt @@ -1,5 +1,5 @@ /* - Copyright 2024 Adobe. All rights reserved. + Copyright 2025 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +13,12 @@ package com.adobe.marketing.mobile.aepcomposeui.uimodels import androidx.compose.ui.graphics.Color +/** + * Data class representing colors for both light and dark themes in AEP UI components. + * + * @param lightColor The color to use in light theme mode. + * @param darkColor The color to use in dark theme mode. + */ data class AepColor( val lightColor: Color, val darkColor: Color, diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/ImageOnlyTemplate.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/ImageOnlyTemplate.kt index 647969d2..fb884200 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/ImageOnlyTemplate.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/ImageOnlyTemplate.kt @@ -15,15 +15,17 @@ package com.adobe.marketing.mobile.aepcomposeui.uimodels * Class representing an image only template, which implements the [AepUITemplate] interface. * * @param id The unique identifier for this template. - * @property image The details of the image to be displayed. - * @property actionUrl If provided, interacting with this card will result in the opening of the actionUrl. - * @property dismissBtn The details for the image only template dismiss button. + * @param image The details of the image to be displayed. + * @param actionUrl If provided, interacting with this card will result in the opening of the actionUrl. + * @param dismissBtn The details for the image only template dismiss button. + * @param isRead Indicates whether this template has been read. */ data class ImageOnlyTemplate( val id: String, val image: AepImage, val actionUrl: String? = null, - val dismissBtn: AepIcon? = null + val dismissBtn: AepIcon? = null, + val isRead: Boolean = false ) : AepUITemplate { /** diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/InboxContainerUITemplate.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/InboxContainerUITemplate.kt new file mode 100644 index 00000000..20636c57 --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/InboxContainerUITemplate.kt @@ -0,0 +1,36 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.aepcomposeui.uimodels + +import androidx.compose.ui.Alignment + +data class InboxContainerUITemplate( + override val heading: AepText, + override val capacity: Int, + override val emptyMessage: AepText? = null, + override val emptyImage: AepImage? = null, + override val unreadBgColor: AepColor? = null, + override val unreadIcon: AepImage? = null, + override val unreadIconAlignment: Alignment? = null +) : AepContainerUITemplate( + heading = heading, + layout = "vertical", + capacity = capacity, + emptyMessage = emptyMessage, + emptyImage = emptyImage, + isUnreadEnabled = true, + unreadBgColor = unreadBgColor, + unreadIcon = unreadIcon, + unreadIconAlignment = unreadIconAlignment +) { + override fun getType() = AepContainerUIType.INBOX +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/LargeImageTemplate.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/LargeImageTemplate.kt index f77047d0..d63618ab 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/LargeImageTemplate.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/LargeImageTemplate.kt @@ -15,12 +15,13 @@ package com.adobe.marketing.mobile.aepcomposeui.uimodels * Class representing a large image template, which implements the [AepUITemplate] interface. * * @param id The unique identifier for this template. - * @property title The title text and display settings. - * @property body The body text and display settings. - * @property image The details of the image to be displayed. - * @property actionUrl If provided, interacting with this card will result in the opening of the actionUrl. - * @property buttons The details for the large image template buttons. - * @property dismissBtn The details for the large image template dismiss button. + * @param title The title text and display settings. + * @param body The body text and display settings. + * @param image The details of the image to be displayed. + * @param actionUrl If provided, interacting with this card will result in the opening of the actionUrl. + * @param buttons The details for the large image template buttons. + * @param dismissBtn The details for the large image template dismiss button. + * @param isRead Indicates whether this template has been read. */ data class LargeImageTemplate( val id: String, @@ -29,7 +30,8 @@ data class LargeImageTemplate( val image: AepImage? = null, val actionUrl: String? = null, val buttons: List? = null, - val dismissBtn: AepIcon? = null + val dismissBtn: AepIcon? = null, + val isRead: Boolean = false ) : AepUITemplate { /** diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/SmallImageTemplate.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/SmallImageTemplate.kt index ad771615..51e1f79d 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/SmallImageTemplate.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/uimodels/SmallImageTemplate.kt @@ -15,12 +15,13 @@ package com.adobe.marketing.mobile.aepcomposeui.uimodels * Class representing a small image template, which implements the [AepUITemplate] interface. * * @param id The unique identifier for this template. - * @property title The title text and display settings. - * @property body The body text and display settings. - * @property image The details of the image to be displayed. - * @property actionUrl If provided, interacting with this card will result in the opening of the actionUrl. - * @property buttons The details for the small image template buttons. - * @property dismissBtn The details for the small image template dismiss button. + * @param title The title text and display settings. + * @param body The body text and display settings. + * @param image The details of the image to be displayed. + * @param actionUrl If provided, interacting with this card will result in the opening of the actionUrl. + * @param buttons The details for the small image template buttons. + * @param dismissBtn The details for the small image template dismiss button. + * @param isRead Indicates whether this template has been read. */ data class SmallImageTemplate( val id: String, @@ -29,7 +30,8 @@ data class SmallImageTemplate( val image: AepImage? = null, val actionUrl: String? = null, val buttons: List? = null, - val dismissBtn: AepIcon? = null + val dismissBtn: AepIcon? = null, + val isRead: Boolean = false ) : AepUITemplate { /** diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardContainerUIProvider.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardContainerUIProvider.kt new file mode 100644 index 00000000..4476e9df --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardContainerUIProvider.kt @@ -0,0 +1,160 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.messaging + +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import com.adobe.marketing.mobile.aepcomposeui.AepContainerUI +import com.adobe.marketing.mobile.aepcomposeui.InboxContainerUI +import com.adobe.marketing.mobile.aepcomposeui.contentprovider.AepContainerUIContentProvider +import com.adobe.marketing.mobile.aepcomposeui.state.InboxContainerUIState +import com.adobe.marketing.mobile.aepcomposeui.uimodels.AepColor +import com.adobe.marketing.mobile.aepcomposeui.uimodels.AepContainerUITemplate +import com.adobe.marketing.mobile.aepcomposeui.uimodels.AepImage +import com.adobe.marketing.mobile.aepcomposeui.uimodels.AepText +import com.adobe.marketing.mobile.aepcomposeui.uimodels.InboxContainerUITemplate +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +/** + * ContentCardContainerUIProvider is responsible for fetching and managing container UI templates + * for a given surface. It manages the container configuration and provides reactive + * updates when the container needs to be refreshed. + * + * @property contentCardUIProvider The provider for the content cards. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class ContentCardContainerUIProvider(val surface: Surface) : AepContainerUIContentProvider { + + private val contentCardUIProvider = ContentCardUIProvider(surface) + + // Reactive state flow that holds the current container UI template (null = not loaded yet) + private val _containerUIFlow = MutableStateFlow?>(null) + private val containerUIFlow = _containerUIFlow.asStateFlow() + + // Transformed flow that starts with container and switches to content cards with flatMapLatest + private fun toAepContainerUI(): Flow>> = + containerUIFlow.filterNotNull().flatMapLatest { containerResult -> + val containerUI = containerResult.getOrNull() + when (containerUI) { + is InboxContainerUITemplate -> { + contentCardUIProvider.getUIContent().map { contentCardResult -> + // Convert AepUITemplate list to AepUI list + val aepUIList = contentCardResult.getOrNull()?.mapNotNull { template -> + ContentCardSchemaDataUtils.getAepUI(template) + } + + if (aepUIList != null) { + Result.success( + InboxContainerUI( + containerUI, + InboxContainerUIState.Success(aepUIList) + ) + ) + } else { + Result.success( + InboxContainerUI( + containerUI, + InboxContainerUIState.Error( + contentCardResult.exceptionOrNull() + ?: Throwable("Unknown error loading container content cards") + ) + ) + ) + } + }.onStart { + emit( + Result.success( + InboxContainerUI(containerUI, InboxContainerUIState.Loading) + ) + ) + } + } + null -> { + flow>> { + emit( + Result.failure( + containerResult.exceptionOrNull() ?: Throwable("Container not loaded yet") + ) + ) + } + } + } + } + + fun getContentCardContainerUI(): Flow>> = + toAepContainerUI().onStart { + refreshContainer() + } + + /** + * Retrieves a reactive flow of container UI templates. + * + * The flow automatically loads the initial container template when first collected, + * then continues to emit updates whenever [refreshContainer] is called. + * + * All collectors will automatically receive the loaded content and any future updates. + * + * @return A [Flow] that emits a [Result] containing the [AepContainerUITemplate]. + */ + override fun getContainerUIContent(): Flow> = + containerUIFlow + .onStart { + // Only fetch if not already loaded (lazy loading) + if (_containerUIFlow.value == null) { + refreshContainer() + } + } + // Only emit actual results, filter out null states + .filterNotNull() + + /** + * Refreshes the container UI by fetching new container configuration and updating + * the flow returned by [getContainerUIContent]. This will cause all collectors of the flow + * to receive the updated container. + * + * Note: [getContainerUIContent] automatically loads initial content when first collected, + * so this method is only needed for manual refresh operations. + */ + override suspend fun refreshContainer() { + _containerUIFlow.value = fetchContainer() + } + + /** + * Fetches the container configuration. This method will be updated in the future + * to call an API similar to Messaging.getPropositionsForSurface. + * + * @return A [Result] containing the [AepContainerUITemplate] or an error if fetching fails. + */ + private fun fetchContainer(): Result { + return Result.success( + InboxContainerUITemplate( + heading = AepText("Message Inbox"), + capacity = 15, + emptyMessage = AepText("No messages right now"), + unreadIcon = AepImage( + url = "https://icons.veryicon.com/png/o/leisure/crisp-app-icon-library-v3/notification-5.png", + darkUrl = "https://icons.veryicon.com/png/o/leisure/crisp-app-icon-library-v3/notification-5.png", + ), + unreadBgColor = AepColor(Color.DarkGray, Color.LightGray), + unreadIconAlignment = Alignment.TopStart + ) + ) + } +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardSchemaDataUtils.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardSchemaDataUtils.kt index 95122bb0..4b32d0a1 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardSchemaDataUtils.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardSchemaDataUtils.kt @@ -75,11 +75,8 @@ internal object ContentCardSchemaDataUtils { * @return The built [AepUITemplate] or null if the proposition is not a content card or parsing fails. */ internal fun buildTemplate(proposition: Proposition): AepUITemplate? { - if (!isContentCard(proposition)) return null - if (proposition.items.size <= 0) return null - val propositionItem = proposition.items[0] val baseTemplateModel: AepUITemplate? = propositionItem.contentCardSchemaData?.let { diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ImageOnlyTemplateEventHandler.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ImageOnlyTemplateEventHandler.kt index e733fe1e..aeaf49bf 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ImageOnlyTemplateEventHandler.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ImageOnlyTemplateEventHandler.kt @@ -31,7 +31,7 @@ internal class ImageOnlyTemplateEventHandler( return when (event) { is UIEvent.Dismiss -> currentState.copy(dismissed = true) is UIEvent.Display -> currentState.copy(displayed = true) - else -> currentState + is UIEvent.Interact -> currentState.copy(read = true) } } } diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/LargeImageTemplateEventHandler.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/LargeImageTemplateEventHandler.kt index 302acac2..bcdfded1 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/LargeImageTemplateEventHandler.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/LargeImageTemplateEventHandler.kt @@ -29,7 +29,7 @@ internal class LargeImageTemplateEventHandler(private val callback: ContentCardU return when (event) { is UIEvent.Dismiss -> currentState.copy(dismissed = true) is UIEvent.Display -> currentState.copy(displayed = true) - else -> currentState + is UIEvent.Interact -> currentState.copy(read = true) } } } diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/MessagingEventHandler.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/MessagingEventHandler.kt index 8270d000..65f96c69 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/MessagingEventHandler.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/MessagingEventHandler.kt @@ -136,8 +136,10 @@ internal abstract class MessagingEventHandler { + val ui = event.aepUi + ui.updateState(getNewState(event)) val urlHandled = - callback?.onInteract(event.aepUi, event.action.id, event.action.actionUrl) + callback?.onInteract(ui, event.action.id, event.action.actionUrl) // Open the URL if available and not handled by the listener if (urlHandled != true && !event.action.actionUrl.isNullOrEmpty()) { diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/SmallImageTemplateEventHandler.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/SmallImageTemplateEventHandler.kt index 1e89cd7e..1efe5a65 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/SmallImageTemplateEventHandler.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/SmallImageTemplateEventHandler.kt @@ -29,7 +29,7 @@ internal class SmallImageTemplateEventHandler(private val callback: ContentCardU return when (event) { is UIEvent.Dismiss -> currentState.copy(dismissed = true) is UIEvent.Display -> currentState.copy(displayed = true) - else -> currentState + is UIEvent.Interact -> currentState.copy(read = true) } } } diff --git a/code/messaging/src/test/java/com/adobe/marketing/mobile/aepcomposeui/components/AepAsyncImageTests.kt b/code/messaging/src/test/java/com/adobe/marketing/mobile/aepcomposeui/components/AepAsyncImageTests.kt index 837d8b13..ec476615 100644 --- a/code/messaging/src/test/java/com/adobe/marketing/mobile/aepcomposeui/components/AepAsyncImageTests.kt +++ b/code/messaging/src/test/java/com/adobe/marketing/mobile/aepcomposeui/components/AepAsyncImageTests.kt @@ -219,31 +219,4 @@ class AepAsyncImageTests( .onAllNodes(hasTestTag("AepImageComposable")) .assertCountEquals(0) } - - @Test - fun `Test AepAsyncImage handles null image`() { - // setup - mockkObject(ContentCardImageManager) - every { - ContentCardImageManager.getContentCardImageBitmap(any(), any(), any()) - } just Runs - - // test - composeTestRule.setContent { - AepAsyncImage( - image = null, - imageStyle = AepImageStyle( - modifier = Modifier.testTag("AepImageComposable") - ) - ) - } - - // verify - verify(exactly = 0) { - ContentCardImageManager.getContentCardImageBitmap(any(), any(), any()) - } - composeTestRule - .onAllNodes(hasTestTag("AepImageComposable")) - .assertCountEquals(0) - } } diff --git a/code/testapp/src/main/java/com/adobe/marketing/mobile/messagingsample/MessagingApplication.kt b/code/testapp/src/main/java/com/adobe/marketing/mobile/messagingsample/MessagingApplication.kt index 62018e35..d9615bbe 100644 --- a/code/testapp/src/main/java/com/adobe/marketing/mobile/messagingsample/MessagingApplication.kt +++ b/code/testapp/src/main/java/com/adobe/marketing/mobile/messagingsample/MessagingApplication.kt @@ -24,7 +24,7 @@ class MessagingApplication : Application() { private val ENVIRONMENT_FILE_ID = "3149c49c3910/4f6b2fbf2986/launch-7d78a5fd1de3-development" private val ASSURANCE_SESSION_ID = "" private val STAGING_APP_ID = "staging/1b50a869c4a2/bcd1a623883f/launch-e44d085fc760-development" - private val STAGING = false + private val STAGING = true override fun onCreate() { super.onCreate() diff --git a/code/testapp/src/main/java/com/adobe/marketing/mobile/messagingsample/ScrollingFeedActivity.kt b/code/testapp/src/main/java/com/adobe/marketing/mobile/messagingsample/ScrollingFeedActivity.kt index 6074b812..95f3d89f 100644 --- a/code/testapp/src/main/java/com/adobe/marketing/mobile/messagingsample/ScrollingFeedActivity.kt +++ b/code/testapp/src/main/java/com/adobe/marketing/mobile/messagingsample/ScrollingFeedActivity.kt @@ -15,62 +15,54 @@ import android.os.Bundle import android.util.Log import android.widget.ImageButton import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.material3.MaterialTheme import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import com.adobe.marketing.mobile.Messaging import com.adobe.marketing.mobile.aepcomposeui.AepUI -import com.adobe.marketing.mobile.aepcomposeui.AepUIConstants -import com.adobe.marketing.mobile.aepcomposeui.ImageOnlyUI -import com.adobe.marketing.mobile.aepcomposeui.LargeImageUI -import com.adobe.marketing.mobile.aepcomposeui.SmallImageUI -import com.adobe.marketing.mobile.aepcomposeui.components.ImageOnlyCard -import com.adobe.marketing.mobile.aepcomposeui.components.LargeImageCard -import com.adobe.marketing.mobile.aepcomposeui.components.SmallImageCard -import com.adobe.marketing.mobile.aepcomposeui.style.AepCardStyle +import com.adobe.marketing.mobile.aepcomposeui.components.AepContainer import com.adobe.marketing.mobile.aepcomposeui.style.AepColumnStyle -import com.adobe.marketing.mobile.aepcomposeui.style.AepIconStyle import com.adobe.marketing.mobile.aepcomposeui.style.AepImageStyle +import com.adobe.marketing.mobile.aepcomposeui.style.AepLazyColumnStyle import com.adobe.marketing.mobile.aepcomposeui.style.AepRowStyle import com.adobe.marketing.mobile.aepcomposeui.style.AepTextStyle +import com.adobe.marketing.mobile.aepcomposeui.style.AepUIStyle +import com.adobe.marketing.mobile.aepcomposeui.style.ContainerStyle import com.adobe.marketing.mobile.aepcomposeui.style.ImageOnlyUIStyle +import com.adobe.marketing.mobile.aepcomposeui.style.InboxContainerUIStyle import com.adobe.marketing.mobile.aepcomposeui.style.LargeImageUIStyle import com.adobe.marketing.mobile.aepcomposeui.style.SmallImageUIStyle +import com.adobe.marketing.mobile.messaging.ContentCardContainerUIProvider import com.adobe.marketing.mobile.messaging.ContentCardEventObserver -import com.adobe.marketing.mobile.messaging.ContentCardMapper import com.adobe.marketing.mobile.messaging.ContentCardUIEventListener -import com.adobe.marketing.mobile.messaging.ContentCardUIProvider import com.adobe.marketing.mobile.messaging.Surface import com.adobe.marketing.mobile.messagingsample.databinding.ActivityScrollingBinding -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlin.jvm.java class ScrollingFeedActivity : AppCompatActivity() { private lateinit var binding: ActivityScrollingBinding - private lateinit var contentCardUIProvider: ContentCardUIProvider - private lateinit var contentCardViewModel: AepContentCardViewModel private lateinit var contentCardCallback: ContentCardCallback override fun onCreate(savedInstanceState: Bundle?) { @@ -86,182 +78,84 @@ class ScrollingFeedActivity : AppCompatActivity() { val surface = Surface("card/ms") surfaces.add(surface) - // Initialize the ContentCardUIProvider - contentCardUIProvider = ContentCardUIProvider(surface) - - // Initialize the ViewModel - contentCardViewModel = - ViewModelProvider(this, AepContentCardViewModelFactory(contentCardUIProvider)).get( - AepContentCardViewModel::class.java - ) + Messaging.updatePropositionsForSurfaces(surfaces) + val viewModel: ExistingViewModel = ViewModelProvider(this)[ExistingViewModel::class.java] contentCardCallback = ContentCardCallback() - // Set a click listener for refresh button which calls the API for fetch content cards from Edge val refreshButton: ImageButton = findViewById(R.id.refreshButton) refreshButton.setOnClickListener { Messaging.updatePropositionsForSurfaces(surfaces) - contentCardViewModel.refreshContent() + viewModel.refresh() } binding.composeView.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { AppTheme { - AepContentCardList(contentCardViewModel) - } - } - } - } - - - @Composable - private fun AepContentCardList(viewModel: AepContentCardViewModel) { - // Collect the state from ViewModel - val aepUiList by viewModel.aepUIList.collectAsStateWithLifecycle() - - // Get the ContentCardSchemaData for the AepUI list if needed - val contentCardSchemaDataList = aepUiList.map { - when (it) { - is SmallImageUI -> - ContentCardMapper.Companion.instance.getContentCardSchemaData(it.getTemplate().id) - - else -> null - } - } - - // Reorder the AepUI list based on the ContentCardSchemaData fields if needed - val reorderedAepUIList = aepUiList.sortedWith(compareByDescending { - val rank = - contentCardSchemaDataList[aepUiList.indexOf(it)]?.meta?.get("priority") as String? - ?: "0" - rank.toInt() - }) - - // Displaying content cards in a Column - // create a custom style for the small image card in column - val smallImageCardStyleColumn = SmallImageUIStyle.Builder() - .rootRowStyle( - AepRowStyle( - modifier = Modifier.fillMaxWidth().padding(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), - ) - ) - .build() - - val largeImageCardStyleColumn = LargeImageUIStyle.Builder() - .imageStyle(AepImageStyle(modifier = Modifier.fillMaxWidth(), - contentScale = ContentScale.FillWidth)) - .textColumnStyle(AepColumnStyle(modifier = Modifier.padding(8.dp))) - .buttonRowStyle( - AepRowStyle( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), - verticalAlignment = Alignment.CenterVertically - ) - ) - .build() - - val imageOnlyCardStyleColumn = ImageOnlyUIStyle.Builder() - .imageStyle( - AepImageStyle( - modifier = Modifier.fillMaxWidth(), - contentScale = ContentScale.FillWidth - ) - ) - .build() - - // Create column with composables from AepUI instances -// LazyColumn { -// items(reorderedAepUIList) { aepUI -> -// when (aepUI) { -// is SmallImageUI -> { -// val state = aepUI.getState() -// if (!state.dismissed) { -// SmallImageCard( -// ui = aepUI, -// style = SmallImageUIStyle.Builder().build(), -// observer = ContentCardEventObserver(contentCardCallback) -// ) -// } -// } -// is LargeImageUI -> { -// val state = aepUI.getState() -// if (!state.dismissed) { -// LargeImageCard( -// ui = aepUI, -// style = LargeImageUIStyle.Builder().build(), -// observer = ContentCardEventObserver(contentCardCallback) -// ) -// } -// } -// is ImageOnlyUI -> { -// val state = aepUI.getState() -// if (!state.dismissed) { -// ImageOnlyCard( -// ui = aepUI, -// style = ImageOnlyUIStyle.Builder().build(), -// observer = ContentCardEventObserver(contentCardCallback) -// ) -// } -// } -// } -// } -// } - - // Displaying content cards in a Row - // create a custom style for the small image card in row - val smallImageCardStyleRow = SmallImageUIStyle.Builder() - .cardStyle(AepCardStyle(modifier = Modifier.width(400.dp).height(200.dp).padding(8.dp))) - .rootRowStyle( - AepRowStyle( - modifier = Modifier.fillMaxSize().padding(8.dp) - ) - ) - .bodyAepTextStyle(AepTextStyle(maxLines = 3)) - .build() - - val largeImageCardStyleRow = LargeImageUIStyle.Builder() - .cardStyle(AepCardStyle(modifier = Modifier.width(400.dp).height(200.dp).padding(8.dp))) - .build() - - val imageOnlyCardStyleRow = ImageOnlyUIStyle.Builder() - .imageStyle(AepImageStyle(modifier = Modifier.width(400.dp).height(200.dp), contentScale = ContentScale.FillWidth)) - .build() - - // Create row with composables from AepUI instances - LazyRow { - items(reorderedAepUIList) { aepUI -> - when (aepUI) { - is SmallImageUI -> { - val state = aepUI.getState() - if (!state.dismissed) { - SmallImageCard( - ui = aepUI, - style = SmallImageUIStyle.Builder().build(), - observer = ContentCardEventObserver(contentCardCallback) + val smallImageCardStyleColumn = SmallImageUIStyle.Builder() + .rootRowStyle( + AepRowStyle( + modifier = Modifier.fillMaxWidth().padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), ) - } - } - is LargeImageUI -> { - val state = aepUI.getState() - if (!state.dismissed) { - LargeImageCard( - ui = aepUI, - style = LargeImageUIStyle.Builder().build(), - observer = ContentCardEventObserver(contentCardCallback) + ) + .build() + + val largeImageCardStyleColumn = LargeImageUIStyle.Builder() + .imageStyle( + AepImageStyle( + modifier = Modifier.fillMaxWidth().height(150.dp), + contentScale = ContentScale.FillWidth ) - } - } - is ImageOnlyUI -> { - val state = aepUI.getState() - if (!state.dismissed) { - ImageOnlyCard( - ui = aepUI, - style = ImageOnlyUIStyle.Builder().build(), - observer = ContentCardEventObserver(contentCardCallback) + ) + .textColumnStyle(AepColumnStyle(modifier = Modifier.padding(8.dp))) + .build() + + val imageOnlyCardStyleColumn = ImageOnlyUIStyle.Builder() + .imageStyle( + AepImageStyle( + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.FillWidth + ) + ) + .build() + + val containerUi = viewModel.containerUIFlow.collectAsStateWithLifecycle().value + + containerUi?.let { ui -> + val headingStyle = AepTextStyle( + modifier = Modifier.fillMaxWidth().padding(10.dp), + textStyle = TextStyle( + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + textAlign = TextAlign.Center + ) + ) + + val inboxContainerStyle = InboxContainerUIStyle.Builder() + .headingStyle(headingStyle) + .lazyColumnStyle( + AepLazyColumnStyle( + modifier = Modifier.background(Color.Gray), + contentPadding = PaddingValues(10.dp) + ) ) - } + .build() + + AepContainer( + containerUi = ui, + containerStyle = ContainerStyle( + inboxContainerUIStyle = inboxContainerStyle + ), + itemsStyle = AepUIStyle( + smallImageUIStyle = smallImageCardStyleColumn, + largeImageUIStyle = largeImageCardStyleColumn, + imageOnlyUIStyle = imageOnlyCardStyleColumn, + ), + observer = ContentCardEventObserver(ContentCardCallback()) + ) } } } @@ -288,48 +182,23 @@ class ContentCardCallback: ContentCardUIEventListener { return false } } - // create new view model or reuse existing one to hold the aepUIList -class AepContentCardViewModel(private val contentCardUIProvider: ContentCardUIProvider) : ViewModel() { - // State to hold AepUI list - private val _aepUIList = MutableStateFlow>>(emptyList()) - val aepUIList: StateFlow>> = _aepUIList.asStateFlow() +class ExistingViewModel: ViewModel() { + private val containerUIProvider = ContentCardContainerUIProvider(Surface("card/ms")) - init { - // Launch a coroutine to fetch the aepUIList from the ContentCardUIProvider - // when the ViewModel is created - viewModelScope.launch { - contentCardUIProvider.getContentCardUI().collect { aepUiResult -> - aepUiResult.onSuccess { aepUi -> - _aepUIList.value = aepUi - } - aepUiResult.onFailure { throwable -> - Log.d("ContentCardUIProvider", "Error fetching AepUI list: ${throwable}") - } - } + val containerUIFlow = containerUIProvider.getContentCardContainerUI() + .mapNotNull { result -> + result.getOrNull() } - } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = null + ) - // Function to refresh the aepUIList from the ContentCardUIProvider - fun refreshContent() { + fun refresh() { viewModelScope.launch { - contentCardUIProvider.refreshContent() - } - } -} - -class AepContentCardViewModelFactory( - private val contentCardUIProvider: ContentCardUIProvider -) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return when { - modelClass.isAssignableFrom(AepContentCardViewModel::class.java) -> { - AepContentCardViewModel(contentCardUIProvider) as T - } - - else -> throw IllegalArgumentException("Unknown ViewModel class") + containerUIProvider.refreshContainer() } } } \ No newline at end of file