Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import net.thunderbird.core.ui.compose.common.modifier.testTagAsResourceId
import net.thunderbird.feature.notification.api.NotificationSeverity
import net.thunderbird.feature.notification.api.ui.action.NotificationAction
import net.thunderbird.feature.notification.api.ui.host.rememberInAppNotificationHostStateHolder
import net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyles
import net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyle
import net.thunderbird.feature.notification.api.ui.util.printSemanticTree
import net.thunderbird.feature.notification.testing.fake.FakeInAppOnlyNotification
import net.thunderbird.feature.notification.testing.fake.ui.action.createFakeNotificationAction
Expand Down Expand Up @@ -380,7 +380,7 @@ class BannerGlobalNotificationHostTest : ComposeTest() {
title = title,
contentText = contentText,
severity = severity,
inAppNotificationStyles = inAppNotificationStyles { bannerGlobal() },
inAppNotificationStyle = inAppNotificationStyle { bannerGlobal() },
actions = action?.let { setOf(it) }.orEmpty(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import kotlinx.collections.immutable.toPersistentList
import net.thunderbird.core.ui.compose.common.modifier.testTagAsResourceId
import net.thunderbird.feature.notification.api.ui.action.NotificationAction
import net.thunderbird.feature.notification.api.ui.host.rememberInAppNotificationHostStateHolder
import net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyles
import net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyle
import net.thunderbird.feature.notification.api.ui.util.assertBannerInline
import net.thunderbird.feature.notification.api.ui.util.assertBannerInlineList
import net.thunderbird.feature.notification.api.ui.util.printSemanticTree
Expand Down Expand Up @@ -328,7 +328,7 @@ class BannerInlineNotificationListHostTest : ComposeTest() {
title = title,
contentText = supportingText,
actions = actions,
inAppNotificationStyles = inAppNotificationStyles { bannerInline() },
inAppNotificationStyle = inAppNotificationStyle { bannerInline() },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import kotlinx.coroutines.flow.SharedFlow
import net.thunderbird.feature.notification.api.receiver.InAppNotificationEvent
import net.thunderbird.feature.notification.api.receiver.InAppNotificationReceiver
import net.thunderbird.feature.notification.api.ui.action.NotificationAction
import net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyles
import net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyle
import net.thunderbird.feature.notification.api.ui.util.assertBannerInline
import net.thunderbird.feature.notification.api.ui.util.assertBannerInlineList
import net.thunderbird.feature.notification.api.ui.util.printSemanticTree
Expand Down Expand Up @@ -174,7 +174,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
// Arrange
val event = InAppNotificationEvent.Show(
notification = FakeInAppOnlyNotification(
inAppNotificationStyles = inAppNotificationStyles { bannerGlobal() },
inAppNotificationStyle = inAppNotificationStyle { bannerGlobal() },
),
)
val receiver = FakeInAppNotificationReceiver()
Expand Down Expand Up @@ -207,7 +207,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
val action = createFakeNotificationAction(actionTitle)
val event = InAppNotificationEvent.Show(
notification = FakeInAppOnlyNotification(
inAppNotificationStyles = inAppNotificationStyles { bannerGlobal() },
inAppNotificationStyle = inAppNotificationStyle { bannerGlobal() },
actions = setOf(action),
),
)
Expand Down Expand Up @@ -254,7 +254,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
// Arrange
val event = InAppNotificationEvent.Show(
notification = FakeInAppOnlyNotification(
inAppNotificationStyles = inAppNotificationStyles { bannerGlobal() },
inAppNotificationStyle = inAppNotificationStyle { bannerGlobal() },
),
)
val receiver = FakeInAppNotificationReceiver()
Expand All @@ -281,7 +281,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
// Arrange
val event = InAppNotificationEvent.Show(
notification = FakeInAppOnlyNotification(
inAppNotificationStyles = inAppNotificationStyles { bannerInline() },
inAppNotificationStyle = inAppNotificationStyle { bannerInline() },
actions = setOf(createFakeNotificationAction("Action")),
),
)
Expand Down Expand Up @@ -314,7 +314,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
val notification = FakeInAppOnlyNotification(
title = "The notification",
contentText = "The content",
inAppNotificationStyles = inAppNotificationStyles { bannerInline() },
inAppNotificationStyle = inAppNotificationStyle { bannerInline() },
actions = setOf(createFakeNotificationAction("Action")),
)
val event = InAppNotificationEvent.Show(notification = notification)
Expand Down Expand Up @@ -355,7 +355,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
val notification = FakeInAppOnlyNotification(
title = "The notification",
contentText = "The content",
inAppNotificationStyles = inAppNotificationStyles { bannerInline() },
inAppNotificationStyle = inAppNotificationStyle { bannerInline() },
actions = setOf(createFakeNotificationAction("Action")),
)
val event = InAppNotificationEvent.Show(notification = notification)
Expand All @@ -376,7 +376,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
InAppNotificationEvent.Show(
notification = FakeInAppOnlyNotification(
title = "Notification $it",
inAppNotificationStyles = inAppNotificationStyles { bannerInline() },
inAppNotificationStyle = inAppNotificationStyle { bannerInline() },
actions = setOf(createFakeNotificationAction("Action")),
),
),
Expand Down Expand Up @@ -426,7 +426,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
val action = createFakeNotificationAction(actionTitle)
val event = InAppNotificationEvent.Show(
notification = FakeInAppOnlyNotification(
inAppNotificationStyles = inAppNotificationStyles { bannerInline() },
inAppNotificationStyle = inAppNotificationStyle { bannerInline() },
actions = setOf(action),
),
)
Expand Down Expand Up @@ -494,7 +494,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
InAppNotificationEvent.Show(
notification = FakeInAppOnlyNotification(
title = "Notification $it",
inAppNotificationStyles = inAppNotificationStyles { bannerInline() },
inAppNotificationStyle = inAppNotificationStyle { bannerInline() },
actions = setOf(createFakeNotificationAction("Action")),
),
),
Expand Down Expand Up @@ -527,7 +527,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
// Arrange
val event = InAppNotificationEvent.Show(
notification = FakeInAppOnlyNotification(
inAppNotificationStyles = inAppNotificationStyles { bannerGlobal() },
inAppNotificationStyle = inAppNotificationStyle { bannerGlobal() },
),
)
val receiver = FakeInAppNotificationReceiver()
Expand Down Expand Up @@ -562,7 +562,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
// Arrange
val event = InAppNotificationEvent.Show(
notification = FakeInAppOnlyNotification(
inAppNotificationStyles = inAppNotificationStyles { bannerInline() },
inAppNotificationStyle = inAppNotificationStyle { bannerInline() },
actions = setOf(createFakeNotificationAction("Action")),
),
)
Expand Down Expand Up @@ -595,7 +595,7 @@ class InAppNotificationScaffoldTest : ComposeTest() {
// Arrange
val event = InAppNotificationEvent.Show(
notification = FakeInAppOnlyNotification(
inAppNotificationStyles = inAppNotificationStyles { snackbar() },
inAppNotificationStyle = inAppNotificationStyle { snackbar() },
actions = setOf(createFakeNotificationAction("Action")),
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ interface SystemNotification : Notification {
/**
* Represents a notification displayed within the application.
*
* @property inAppNotificationStyles The styles of the in-app notification.
* @property inAppNotificationStyle The style of the in-app notification.
* Defaults to [InAppNotificationStyle.Undefined].
* @see InAppNotificationStyle
* @see net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyles
* @see net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyle
*/
interface InAppNotification : Notification {
val inAppNotificationStyles: List<InAppNotificationStyle> get() = InAppNotificationStyle.Undefined
val inAppNotificationStyle: InAppNotificationStyle get() = InAppNotificationStyle.Undefined
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ class InAppNotificationHostStateHolder(private val enabled: ImmutableSet<Display
private fun InAppNotification.toInAppNotificationData(): InAppNotificationHostState =
InAppNotificationHostStateImpl(
bannerGlobalVisual = BannerGlobalVisual.from(notification = this),
bannerInlineVisuals = BannerInlineVisual.from(notification = this).toPersistentSet(),
bannerInlineVisuals = BannerInlineVisual.from(notification = this)
?.let { persistentSetOf(it) }
?: persistentSetOf(),
snackbarVisual = SnackbarVisual.from(notification = this),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package net.thunderbird.feature.notification.api.ui.host.visual

import androidx.compose.runtime.Stable
import androidx.compose.ui.util.fastFilter
import kotlin.time.Duration
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
Expand Down Expand Up @@ -56,7 +55,7 @@ data class BannerGlobalVisual(
* @throws IllegalStateException fails the check validations.
*/
fun from(notification: InAppNotification): BannerGlobalVisual? =
notification.toVisuals<InAppNotificationStyle.BannerGlobalNotification, BannerGlobalVisual> { style ->
notification.toVisual<InAppNotificationStyle.BannerGlobalNotification, BannerGlobalVisual> { style ->
BannerGlobalVisual(
message = checkNotNull(notification.contentText) {
"A notification with a BannerGlobalNotification style must have a contentText not null"
Expand All @@ -73,7 +72,7 @@ data class BannerGlobalVisual(
.firstOrNull(),
priority = style.priority,
)
}.singleOrNull()
}
}
}

Expand Down Expand Up @@ -119,8 +118,8 @@ data class BannerInlineVisual(
* @return A list containing a [BannerInlineVisual] if the conversion is successful, an empty list otherwise.
* @throws IllegalStateException if any of the validation checks fail.
*/
fun from(notification: InAppNotification): List<BannerInlineVisual> =
notification.toVisuals<InAppNotificationStyle.BannerInlineNotification, BannerInlineVisual> { style ->
fun from(notification: InAppNotification): BannerInlineVisual? =
notification.toVisual<InAppNotificationStyle.BannerInlineNotification, BannerInlineVisual> { style ->
BannerInlineVisual(
title = checkTitle(notification.title),
supportingText = checkContentText(notification.contentText),
Expand Down Expand Up @@ -193,7 +192,7 @@ data class SnackbarVisual(
* when the style is [InAppNotificationStyle.SnackbarNotification].
*/
fun from(notification: InAppNotification): SnackbarVisual? =
notification.toVisuals<InAppNotificationStyle.SnackbarNotification, SnackbarVisual> { style ->
notification.toVisual<InAppNotificationStyle.SnackbarNotification, SnackbarVisual> { style ->
SnackbarVisual(
message = checkNotNull(notification.contentText) {
"A notification with a SnackbarNotification style must have a contentText not null"
Expand All @@ -203,19 +202,19 @@ data class SnackbarVisual(
},
duration = style.duration,
)
}.singleOrNull()
}
}
}

private inline fun <
reified TStyle : InAppNotificationStyle,
reified TVisual : InAppNotificationVisual,
> InAppNotification.toVisuals(
> InAppNotification.toVisual(
transform: (TStyle) -> TVisual,
): List<TVisual> {
return inAppNotificationStyles
.fastFilter { style -> style is TStyle }
.map { style ->
): TVisual? {
return inAppNotificationStyle
.takeIf { style -> style is TStyle }
?.let { style ->
check(style is TStyle)
transform(style)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import net.thunderbird.feature.notification.api.ui.style.builder.InAppNotificati
* feedback or information.
*/
sealed interface InAppNotificationStyle {
companion object {
/**
* Represents an undefined in-app notification style.
* This can be used as a default or placeholder when no specific style is applicable.
*/
val Undefined: List<InAppNotificationStyle> = emptyList()
}
/**
* Represents an undefined in-app notification style.
* This can be used as a default or placeholder when no specific style is applicable.
*/
data object Undefined : InAppNotificationStyle

/**
* @see InAppNotificationStyleBuilder.bannerInline
Expand Down Expand Up @@ -49,9 +47,12 @@ enum class SnackbarDuration { Short, Long, Indefinite }
*
* Example:
* ```
* inAppNotificationStyles {
* inAppNotificationStyle {
* bannerInline()
* }
*
* inAppNotificationStyle {
* snackbar(duration = 30.seconds)
* bottomSheet()
* }
* ```
*
Expand All @@ -60,8 +61,8 @@ enum class SnackbarDuration { Short, Long, Indefinite }
* @return a list of [InAppNotificationStyle]
*/
@NotificationStyleMarker
fun inAppNotificationStyles(
fun inAppNotificationStyle(
builder: @NotificationStyleMarker InAppNotificationStyleBuilder.() -> Unit,
): List<InAppNotificationStyle> {
): InAppNotificationStyle {
return InAppNotificationStyleBuilder().apply(builder).build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import net.thunderbird.feature.notification.api.ui.style.SnackbarDuration
* This interface defines the methods available for configuring the style of an in-app notification.
*/
class InAppNotificationStyleBuilder internal constructor() {
private var styles = mutableListOf<InAppNotificationStyle>()
private var style: InAppNotificationStyle = InAppNotificationStyle.Undefined

/**
* Use inline error banners to surface issues that must be resolved before the user can continue
Expand All @@ -39,7 +39,7 @@ class InAppNotificationStyleBuilder internal constructor() {
@NotificationStyleMarker
fun bannerInline() {
checkSingleStyleEntry<BannerInlineNotification>()
styles += BannerInlineNotification
style = BannerInlineNotification
}

/**
Expand Down Expand Up @@ -69,7 +69,7 @@ class InAppNotificationStyleBuilder internal constructor() {
@NotificationStyleMarker
fun bannerGlobal(priority: Int = 0) {
checkSingleStyleEntry<BannerGlobalNotification>()
styles += BannerGlobalNotification(priority = priority)
style = BannerGlobalNotification(priority = priority)
}

/**
Expand All @@ -90,7 +90,7 @@ class InAppNotificationStyleBuilder internal constructor() {
@NotificationStyleMarker
fun snackbar(duration: SnackbarDuration = SnackbarDuration.Short) {
checkSingleStyleEntry<SnackbarNotification>()
styles += SnackbarNotification(duration)
style = SnackbarNotification(duration)
}

/**
Expand All @@ -112,24 +112,25 @@ class InAppNotificationStyleBuilder internal constructor() {
@NotificationStyleMarker
fun dialog() {
checkSingleStyleEntry<DialogNotification>()
styles += DialogNotification
style = DialogNotification
}

/**
* Builds the [InAppNotificationStyle] based on the provided parameters.
*
* @return The constructed [InAppNotificationStyle].
*/
fun build(): List<InAppNotificationStyle> {
check(styles.isNotEmpty()) {
"You must add at least one in-app notification style."
fun build(): InAppNotificationStyle {
check(style != InAppNotificationStyle.Undefined) {
"You must pick one in-app notification style."
}
return styles.takeUnless { it.isEmpty() } ?: InAppNotificationStyle.Undefined
return style
}

private inline fun <reified T> checkSingleStyleEntry() {
check(styles.none { it is T }) {
"An in-app notification can only have at most one type of ${T::class.simpleName} style"
check(style == InAppNotificationStyle.Undefined) {
"An in-app notification can only have one type of style. " +
"Current style is ${style::class.simpleName}, trying to set ${T::class.simpleName}."
}
}
}
Loading