Skip to content

Commit 8fddebc

Browse files
committed
feat(notification): add InAppNotificationCommand implementation
1 parent 19cf477 commit 8fddebc

File tree

9 files changed

+303
-27
lines changed

9 files changed

+303
-27
lines changed

feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/AppNotification.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ abstract class AppNotification : Notification {
7070
* @see SystemNotificationStyle
7171
* @see net.thunderbird.feature.notification.api.ui.style.systemNotificationStyle
7272
*/
73-
sealed interface SystemNotification : Notification {
73+
interface SystemNotification : Notification {
7474
val subText: String? get() = null
7575
val channel: NotificationChannel
7676
val group: NotificationGroup? get() = null
@@ -112,6 +112,6 @@ sealed interface SystemNotification : Notification {
112112
* @see InAppNotificationStyle
113113
* @see net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyle
114114
*/
115-
sealed interface InAppNotification : Notification {
115+
interface InAppNotification : Notification {
116116
val inAppNotificationStyle: InAppNotificationStyle get() = InAppNotificationStyle.Undefined
117117
}

feature/notification/impl/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
plugins {
22
id(ThunderbirdPlugins.Library.kmpCompose)
3+
alias(libs.plugins.dev.mokkery)
34
}
45

56
kotlin {
67
sourceSets {
78
commonMain.dependencies {
89
implementation(projects.core.common)
10+
implementation(projects.core.featureflag)
911
implementation(projects.core.outcome)
1012
implementation(projects.core.logging.api)
1113
implementation(projects.feature.notification.api)
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package net.thunderbird.feature.notification.impl.command
22

3+
import net.thunderbird.core.featureflag.FeatureFlagKey
4+
import net.thunderbird.core.featureflag.FeatureFlagProvider
5+
import net.thunderbird.core.featureflag.FeatureFlagResult
36
import net.thunderbird.core.logging.Logger
47
import net.thunderbird.core.outcome.Outcome
8+
import net.thunderbird.feature.notification.api.NotificationRegistry
59
import net.thunderbird.feature.notification.api.command.NotificationCommand
610
import net.thunderbird.feature.notification.api.content.InAppNotification
711
import net.thunderbird.feature.notification.api.receiver.NotificationNotifier
812

13+
private const val TAG = "InAppNotificationCommand"
14+
915
/**
1016
* A command that handles in-app notifications.
1117
*
@@ -16,13 +22,42 @@ import net.thunderbird.feature.notification.api.receiver.NotificationNotifier
1622
*/
1723
internal class InAppNotificationCommand(
1824
private val logger: Logger,
25+
private val featureFlagProvider: FeatureFlagProvider,
26+
private val notificationRegistry: NotificationRegistry,
1927
notification: InAppNotification,
2028
notifier: NotificationNotifier<InAppNotification>,
2129
) : NotificationCommand<InAppNotification>(notification, notifier) {
30+
private val isFeatureFlagEnabled: Boolean
31+
get() = featureFlagProvider
32+
.provide(FeatureFlagKey.DisplayInAppNotifications) == FeatureFlagResult.Enabled
33+
2234
override suspend fun execute(): Outcome<Success<InAppNotification>, Failure<InAppNotification>> {
23-
logger.debug {
24-
"TODO: Implementation on GitHub Issue #9245. Notification = $notification."
35+
logger.debug(TAG) { "execute() called with: notification = $notification" }
36+
return when {
37+
isFeatureFlagEnabled.not() ->
38+
Outcome.failure(
39+
error = Failure(
40+
command = this,
41+
throwable = NotificationCommandException(
42+
message = "${FeatureFlagKey.DisplayInAppNotifications.key} feature flag is not enabled",
43+
),
44+
),
45+
)
46+
47+
canExecuteCommand() -> {
48+
notifier.show(id = notificationRegistry.register(notification), notification = notification)
49+
Outcome.success(Success(command = this))
50+
}
51+
52+
else -> {
53+
Outcome.failure(Failure(command = this, throwable = Exception("Can't execute command.")))
54+
}
2555
}
26-
return Outcome.success(data = Success(command = this))
2756
}
57+
58+
// TODO(#9392): Verify if the app is on foreground. IF it isn't, then should fail
59+
// executing the command
60+
// TODO(#9420): If the app is on background and the severity is Fatal or Critical, we should
61+
// let the command execute, but store it in a database instead of triggering the show notification logic.
62+
private fun canExecuteCommand(): Boolean = true
2863
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package net.thunderbird.feature.notification.impl.command
2+
3+
class NotificationCommandException @JvmOverloads constructor(
4+
override val message: String?,
5+
override val cause: Throwable? = null,
6+
) : Exception(message, cause)

feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/NotificationCommandFactory.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package net.thunderbird.feature.notification.impl.command
22

3+
import net.thunderbird.core.featureflag.FeatureFlagProvider
34
import net.thunderbird.core.logging.Logger
5+
import net.thunderbird.feature.notification.api.NotificationRegistry
46
import net.thunderbird.feature.notification.api.command.NotificationCommand
57
import net.thunderbird.feature.notification.api.content.InAppNotification
68
import net.thunderbird.feature.notification.api.content.Notification
79
import net.thunderbird.feature.notification.api.content.SystemNotification
10+
import net.thunderbird.feature.notification.api.receiver.NotificationNotifier
811
import net.thunderbird.feature.notification.impl.receiver.InAppNotificationNotifier
912
import net.thunderbird.feature.notification.impl.receiver.SystemNotificationNotifier
1013

@@ -13,8 +16,10 @@ import net.thunderbird.feature.notification.impl.receiver.SystemNotificationNoti
1316
*/
1417
internal class NotificationCommandFactory(
1518
private val logger: Logger,
19+
private val featureFlagProvider: FeatureFlagProvider,
20+
private val notificationRegistry: NotificationRegistry,
1621
private val systemNotificationNotifier: SystemNotificationNotifier,
17-
private val inAppNotificationNotifier: InAppNotificationNotifier,
22+
private val inAppNotificationNotifier: NotificationNotifier<InAppNotification>,
1823
) {
1924
/**
2025
* Creates a set of [NotificationCommand]s for the given [notification].
@@ -41,6 +46,8 @@ internal class NotificationCommandFactory(
4146
commands.add(
4247
InAppNotificationCommand(
4348
logger = logger,
49+
featureFlagProvider = featureFlagProvider,
50+
notificationRegistry = notificationRegistry,
4451
notification = notification,
4552
notifier = inAppNotificationNotifier,
4653
),

feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package net.thunderbird.feature.notification.impl.inject
22

33
import net.thunderbird.feature.notification.api.NotificationRegistry
4+
import net.thunderbird.feature.notification.api.content.InAppNotification
5+
import net.thunderbird.feature.notification.api.receiver.NotificationNotifier
46
import net.thunderbird.feature.notification.api.sender.NotificationSender
57
import net.thunderbird.feature.notification.impl.DefaultNotificationRegistry
68
import net.thunderbird.feature.notification.impl.command.NotificationCommandFactory
79
import net.thunderbird.feature.notification.impl.receiver.InAppNotificationNotifier
810
import net.thunderbird.feature.notification.impl.receiver.SystemNotificationNotifier
911
import net.thunderbird.feature.notification.impl.sender.DefaultNotificationSender
1012
import org.koin.core.module.Module
13+
import org.koin.core.qualifier.named
1114
import org.koin.dsl.module
1215

1316
internal expect val platformFeatureNotificationModule: Module
@@ -16,13 +19,17 @@ val featureNotificationModule = module {
1619
includes(platformFeatureNotificationModule)
1720

1821
factory { SystemNotificationNotifier() }
19-
factory { InAppNotificationNotifier() }
22+
factory<NotificationNotifier<InAppNotification>>(named<InAppNotificationNotifier>()) {
23+
InAppNotificationNotifier()
24+
}
2025

2126
factory<NotificationCommandFactory> {
2227
NotificationCommandFactory(
2328
logger = get(),
29+
featureFlagProvider = get(),
30+
notificationRegistry = get(),
2431
systemNotificationNotifier = get(),
25-
inAppNotificationNotifier = get(),
32+
inAppNotificationNotifier = get(named<InAppNotificationNotifier>()),
2633
)
2734
}
2835

feature/notification/impl/src/commonTest/kotlin/net/thunderbird/feature/notification/impl/DefaultNotificationRegistryTest.kt

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package net.thunderbird.feature.notification.impl
22

3-
import androidx.compose.ui.graphics.vector.ImageVector
4-
import androidx.compose.ui.unit.dp
53
import assertk.assertThat
64
import assertk.assertions.containsAtLeast
75
import assertk.assertions.hasSize
@@ -13,9 +11,7 @@ import kotlin.test.Test
1311
import kotlinx.coroutines.runBlocking
1412
import kotlinx.coroutines.test.runTest
1513
import net.thunderbird.feature.notification.api.NotificationId
16-
import net.thunderbird.feature.notification.api.NotificationSeverity
17-
import net.thunderbird.feature.notification.api.content.AppNotification
18-
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
14+
import net.thunderbird.feature.notification.impl.fake.FakeNotification
1915

2016
@Suppress("MaxLineLength")
2117
class DefaultNotificationRegistryTest {
@@ -176,18 +172,4 @@ class DefaultNotificationRegistryTest {
176172
// Assert
177173
assertThat(registry[notification]).isNull()
178174
}
179-
180-
data class FakeNotification(
181-
override val title: String = "fake title",
182-
override val contentText: String? = "fake content",
183-
override val severity: NotificationSeverity = NotificationSeverity.Information,
184-
override val icon: NotificationIcon = NotificationIcon(
185-
inAppNotificationIcon = ImageVector.Builder(
186-
defaultWidth = 0.dp,
187-
defaultHeight = 0.dp,
188-
viewportWidth = 0f,
189-
viewportHeight = 0f,
190-
).build(),
191-
),
192-
) : AppNotification()
193175
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package net.thunderbird.feature.notification.impl.command
2+
3+
import assertk.all
4+
import assertk.assertThat
5+
import assertk.assertions.hasMessage
6+
import assertk.assertions.isEqualTo
7+
import assertk.assertions.isInstanceOf
8+
import assertk.assertions.prop
9+
import dev.mokkery.matcher.any
10+
import dev.mokkery.spy
11+
import dev.mokkery.verify.VerifyMode.Companion.exactly
12+
import dev.mokkery.verifySuspend
13+
import kotlin.random.Random
14+
import kotlin.test.Test
15+
import kotlinx.coroutines.flow.Flow
16+
import kotlinx.coroutines.test.runTest
17+
import net.thunderbird.core.featureflag.FeatureFlagKey
18+
import net.thunderbird.core.featureflag.FeatureFlagProvider
19+
import net.thunderbird.core.featureflag.FeatureFlagResult
20+
import net.thunderbird.core.logging.testing.TestLogger
21+
import net.thunderbird.core.outcome.Outcome
22+
import net.thunderbird.feature.notification.api.NotificationId
23+
import net.thunderbird.feature.notification.api.NotificationRegistry
24+
import net.thunderbird.feature.notification.api.NotificationSeverity
25+
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
26+
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
27+
import net.thunderbird.feature.notification.api.content.InAppNotification
28+
import net.thunderbird.feature.notification.api.content.Notification
29+
import net.thunderbird.feature.notification.api.receiver.NotificationNotifier
30+
import net.thunderbird.feature.notification.impl.fake.FakeNotification
31+
import net.thunderbird.feature.notification.impl.fake.FakeSystemOnlyNotification
32+
33+
@Suppress("MaxLineLength")
34+
class InAppNotificationCommandTest {
35+
@Test
36+
fun `execute should return Failure when display_in_app_notifications feature flag is Disabled`() =
37+
runTest {
38+
// Arrange
39+
val testSubject = createTestSubject(
40+
featureFlagProvider = { key ->
41+
when (key) {
42+
FeatureFlagKey.DisplayInAppNotifications -> FeatureFlagResult.Disabled
43+
else -> FeatureFlagResult.Enabled
44+
}
45+
},
46+
)
47+
48+
// Act
49+
val outcome = testSubject.execute()
50+
51+
// Assert
52+
53+
assertThat(outcome)
54+
.isInstanceOf<Outcome.Failure<Failure<InAppNotification>>>()
55+
.prop("error") { it.error }
56+
.all {
57+
prop(Failure<InAppNotification>::command)
58+
.isEqualTo(testSubject)
59+
prop(Failure<InAppNotification>::throwable)
60+
.isInstanceOf<NotificationCommandException>()
61+
.hasMessage(
62+
"${FeatureFlagKey.DisplayInAppNotifications.key} feature flag is not enabled",
63+
)
64+
}
65+
}
66+
67+
@Test
68+
fun `execute should return Failure when display_in_app_notifications feature flag is Unavailable`() =
69+
runTest {
70+
// Arrange
71+
val testSubject = createTestSubject(
72+
featureFlagProvider = { key ->
73+
when (key) {
74+
FeatureFlagKey.DisplayInAppNotifications -> FeatureFlagResult.Unavailable
75+
else -> FeatureFlagResult.Enabled
76+
}
77+
},
78+
)
79+
80+
// Act
81+
val outcome = testSubject.execute()
82+
83+
// Assert
84+
assertThat(outcome)
85+
.isInstanceOf<Outcome.Failure<Failure<InAppNotification>>>()
86+
.prop("error") { it.error }
87+
.all {
88+
prop(Failure<InAppNotification>::command)
89+
.isEqualTo(testSubject)
90+
prop(Failure<InAppNotification>::throwable)
91+
.isInstanceOf<NotificationCommandException>()
92+
.hasMessage(
93+
"${FeatureFlagKey.DisplayInAppNotifications.key} feature flag is not enabled",
94+
)
95+
}
96+
}
97+
98+
@Test
99+
fun `execute should return Success when display_in_app_notifications feature flag is Enabled`() =
100+
runTest {
101+
// Arrange
102+
val notification = FakeNotification(
103+
severity = NotificationSeverity.Information,
104+
)
105+
val notifier = spy(FakeNotifier())
106+
val testSubject = createTestSubject(
107+
notification = notification,
108+
notifier = notifier,
109+
)
110+
111+
// Act
112+
val outcome = testSubject.execute()
113+
114+
// Assert
115+
assertThat(outcome)
116+
.isInstanceOf<Outcome.Success<Success<InAppNotification>>>()
117+
.prop("data") { it.data }
118+
.all {
119+
prop(Success<InAppNotification>::command)
120+
.isEqualTo(testSubject)
121+
}
122+
123+
verifySuspend(exactly(1)) {
124+
notifier.show(id = any(), notification)
125+
}
126+
}
127+
128+
private fun createTestSubject(
129+
notification: InAppNotification = FakeNotification(),
130+
featureFlagProvider: FeatureFlagProvider = FeatureFlagProvider { FeatureFlagResult.Enabled },
131+
notifier: NotificationNotifier<InAppNotification> = FakeNotifier(),
132+
notificationRegistry: NotificationRegistry = FakeNotificationRegistry(),
133+
): InAppNotificationCommand {
134+
val logger = TestLogger()
135+
return InAppNotificationCommand(
136+
logger = logger,
137+
featureFlagProvider = featureFlagProvider,
138+
notificationRegistry = notificationRegistry,
139+
notification = notification,
140+
notifier = notifier,
141+
)
142+
}
143+
}
144+
145+
private open class FakeNotificationRegistry : NotificationRegistry {
146+
override val registrar: Map<NotificationId, Notification>
147+
get() = TODO("Not yet implemented")
148+
149+
override fun get(notificationId: NotificationId): Notification? {
150+
TODO("Not yet implemented")
151+
}
152+
153+
override fun get(notification: Notification): NotificationId? {
154+
TODO("Not yet implemented")
155+
}
156+
157+
override suspend fun register(notification: Notification): NotificationId {
158+
return NotificationId(value = Random.nextInt())
159+
}
160+
161+
override fun unregister(notificationId: NotificationId) {
162+
TODO("Not yet implemented")
163+
}
164+
165+
override fun unregister(notification: Notification) {
166+
TODO("Not yet implemented")
167+
}
168+
}
169+
170+
private open class FakeNotifier : NotificationNotifier<InAppNotification> {
171+
override suspend fun show(
172+
id: NotificationId,
173+
notification: InAppNotification,
174+
) = Unit
175+
176+
override fun dispose() = Unit
177+
}

0 commit comments

Comments
 (0)