Skip to content

Commit 1a8bf8b

Browse files
authored
Merge pull request #5482 from element-hq/feature/bma/improveAnnouncementService
Improve AnnouncementService.
2 parents 4c358ae + 8c8f142 commit 1a8bf8b

File tree

21 files changed

+223
-86
lines changed

21 files changed

+223
-86
lines changed

features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ package io.element.android.features.announcement.api
99

1010
enum class Announcement {
1111
Space,
12+
NewNotificationSound,
1213
}

features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@ package io.element.android.features.announcement.api
99

1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.ui.Modifier
12+
import kotlinx.coroutines.flow.Flow
1213

1314
interface AnnouncementService {
1415
suspend fun showAnnouncement(announcement: Announcement)
1516

17+
suspend fun onAnnouncementDismissed(announcement: Announcement)
18+
19+
fun announcementsToShowFlow(): Flow<List<Announcement>>
20+
21+
/**
22+
* Use this composable to render the announcement UI in Fullscreen.
23+
*/
1624
@Composable
1725
fun Render(
1826
modifier: Modifier,

features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import androidx.compose.runtime.collectAsState
1212
import androidx.compose.runtime.getValue
1313
import androidx.compose.runtime.remember
1414
import dev.zacsweers.metro.Inject
15+
import io.element.android.features.announcement.api.Announcement
16+
import io.element.android.features.announcement.impl.store.AnnouncementStatus
1517
import io.element.android.features.announcement.impl.store.AnnouncementStore
1618
import io.element.android.libraries.architecture.Presenter
1719
import kotlinx.coroutines.flow.map
@@ -23,8 +25,8 @@ class AnnouncementPresenter(
2325
@Composable
2426
override fun present(): AnnouncementState {
2527
val showSpaceAnnouncement by remember {
26-
announcementStore.spaceAnnouncementFlow().map {
27-
it == AnnouncementStore.SpaceAnnouncement.Show
28+
announcementStore.announcementStatusFlow(Announcement.Space).map {
29+
it == AnnouncementStatus.Show
2830
}
2931
}.collectAsState(false)
3032
return AnnouncementState(

features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ import io.element.android.features.announcement.api.Announcement
2121
import io.element.android.features.announcement.api.AnnouncementService
2222
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
2323
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView
24+
import io.element.android.features.announcement.impl.store.AnnouncementStatus
2425
import io.element.android.features.announcement.impl.store.AnnouncementStore
2526
import io.element.android.libraries.architecture.Presenter
27+
import kotlinx.coroutines.flow.Flow
28+
import kotlinx.coroutines.flow.combine
2629
import kotlinx.coroutines.flow.first
2730

2831
@ContributesBinding(AppScope::class)
@@ -35,13 +38,36 @@ class DefaultAnnouncementService(
3538
override suspend fun showAnnouncement(announcement: Announcement) {
3639
when (announcement) {
3740
Announcement.Space -> showSpaceAnnouncement()
41+
Announcement.NewNotificationSound -> {
42+
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
43+
}
44+
}
45+
}
46+
47+
override suspend fun onAnnouncementDismissed(announcement: Announcement) {
48+
announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Shown)
49+
}
50+
51+
override fun announcementsToShowFlow(): Flow<List<Announcement>> {
52+
return combine(
53+
announcementStore.announcementStatusFlow(Announcement.Space),
54+
announcementStore.announcementStatusFlow(Announcement.NewNotificationSound),
55+
) { spaceAnnouncementStatus, newNotificationSoundStatus ->
56+
buildList {
57+
if (spaceAnnouncementStatus == AnnouncementStatus.Show) {
58+
add(Announcement.Space)
59+
}
60+
if (newNotificationSoundStatus == AnnouncementStatus.Show) {
61+
add(Announcement.NewNotificationSound)
62+
}
63+
}
3864
}
3965
}
4066

4167
private suspend fun showSpaceAnnouncement() {
42-
val currentValue = announcementStore.spaceAnnouncementFlow().first()
43-
if (currentValue == AnnouncementStore.SpaceAnnouncement.NeverShown) {
44-
announcementStore.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Show)
68+
val currentValue = announcementStore.announcementStatusFlow(Announcement.Space).first()
69+
if (currentValue == AnnouncementStatus.NeverShown) {
70+
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
4571
}
4672
}
4773

features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ package io.element.android.features.announcement.impl.spaces
1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.runtime.rememberCoroutineScope
1212
import dev.zacsweers.metro.Inject
13+
import io.element.android.features.announcement.api.Announcement
14+
import io.element.android.features.announcement.impl.store.AnnouncementStatus
1315
import io.element.android.features.announcement.impl.store.AnnouncementStore
14-
import io.element.android.features.announcement.impl.store.AnnouncementStore.SpaceAnnouncement
1516
import io.element.android.libraries.architecture.Presenter
1617
import kotlinx.coroutines.launch
1718

@@ -26,7 +27,7 @@ class SpaceAnnouncementPresenter(
2627
fun handleEvents(event: SpaceAnnouncementEvents) {
2728
when (event) {
2829
SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch {
29-
announcementStore.setSpaceAnnouncementValue(SpaceAnnouncement.Shown)
30+
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
3031
}
3132
}
3233
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.announcement.impl.store
9+
10+
enum class AnnouncementStatus {
11+
NeverShown,
12+
Show,
13+
Shown,
14+
}

features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@
77

88
package io.element.android.features.announcement.impl.store
99

10+
import io.element.android.features.announcement.api.Announcement
1011
import kotlinx.coroutines.flow.Flow
1112

1213
interface AnnouncementStore {
13-
suspend fun setSpaceAnnouncementValue(value: SpaceAnnouncement)
14-
fun spaceAnnouncementFlow(): Flow<SpaceAnnouncement>
14+
suspend fun setAnnouncementStatus(
15+
announcement: Announcement,
16+
status: AnnouncementStatus,
17+
)
1518

16-
suspend fun reset()
19+
fun announcementStatusFlow(
20+
announcement: Announcement,
21+
): Flow<AnnouncementStatus>
1722

18-
enum class SpaceAnnouncement {
19-
NeverShown,
20-
Show,
21-
Shown,
22-
}
23+
suspend fun reset()
2324
}

features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import androidx.datastore.preferences.core.intPreferencesKey
1212
import dev.zacsweers.metro.AppScope
1313
import dev.zacsweers.metro.ContributesBinding
1414
import dev.zacsweers.metro.Inject
15+
import io.element.android.features.announcement.api.Announcement
1516
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
1617
import kotlinx.coroutines.flow.Flow
1718
import kotlinx.coroutines.flow.map
1819

1920
private val spaceAnnouncementKey = intPreferencesKey("spaceAnnouncement")
21+
private val newNotificationSoundKey = intPreferencesKey("newNotificationSound")
2022

2123
@ContributesBinding(AppScope::class)
2224
@Inject
@@ -25,20 +27,32 @@ class DefaultAnnouncementStore(
2527
) : AnnouncementStore {
2628
private val store = preferenceDataStoreFactory.create("elementx_announcement")
2729

28-
override suspend fun setSpaceAnnouncementValue(value: AnnouncementStore.SpaceAnnouncement) {
29-
store.edit {
30-
it[spaceAnnouncementKey] = value.ordinal
30+
override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
31+
val key = announcement.toKey()
32+
store.edit { prefs ->
33+
prefs[key] = status.ordinal
3134
}
3235
}
3336

34-
override fun spaceAnnouncementFlow(): Flow<AnnouncementStore.SpaceAnnouncement> {
37+
override fun announcementStatusFlow(announcement: Announcement): Flow<AnnouncementStatus> {
38+
val key = announcement.toKey()
39+
// For NewNotificationSound, a migration will set it to Show on application upgrade (see AppMigration08)
40+
val defaultStatus = when (announcement) {
41+
Announcement.Space -> AnnouncementStatus.NeverShown
42+
Announcement.NewNotificationSound -> AnnouncementStatus.Shown
43+
}
3544
return store.data.map { prefs ->
36-
val ordinal = prefs[spaceAnnouncementKey] ?: AnnouncementStore.SpaceAnnouncement.NeverShown.ordinal
37-
AnnouncementStore.SpaceAnnouncement.entries.getOrElse(ordinal) { AnnouncementStore.SpaceAnnouncement.NeverShown }
45+
val ordinal = prefs[key] ?: defaultStatus.ordinal
46+
AnnouncementStatus.entries.getOrElse(ordinal) { defaultStatus }
3847
}
3948
}
4049

4150
override suspend fun reset() {
4251
store.edit { it.clear() }
4352
}
4453
}
54+
55+
private fun Announcement.toKey() = when (this) {
56+
Announcement.Space -> spaceAnnouncementKey
57+
Announcement.NewNotificationSound -> newNotificationSoundKey
58+
}

features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
package io.element.android.features.announcement.impl
99

1010
import com.google.common.truth.Truth.assertThat
11+
import io.element.android.features.announcement.api.Announcement
12+
import io.element.android.features.announcement.impl.store.AnnouncementStatus
1113
import io.element.android.features.announcement.impl.store.AnnouncementStore
1214
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
1315
import io.element.android.tests.testutils.test
@@ -33,10 +35,10 @@ class AnnouncementPresenterTest {
3335
presenter.test {
3436
val state = awaitItem()
3537
assertThat(state.showSpaceAnnouncement).isFalse()
36-
store.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Show)
38+
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
3739
val updatedState = awaitItem()
3840
assertThat(updatedState.showSpaceAnnouncement).isTrue()
39-
store.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Shown)
41+
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
4042
val finalState = awaitItem()
4143
assertThat(finalState.showSpaceAnnouncement).isFalse()
4244
}

features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77

88
package io.element.android.features.announcement.impl
99

10+
import app.cash.turbine.test
1011
import com.google.common.truth.Truth.assertThat
1112
import io.element.android.features.announcement.api.Announcement
1213
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
1314
import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState
15+
import io.element.android.features.announcement.impl.store.AnnouncementStatus
1416
import io.element.android.features.announcement.impl.store.AnnouncementStore
1517
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
1618
import io.element.android.libraries.architecture.Presenter
@@ -25,14 +27,49 @@ class DefaultAnnouncementServiceTest {
2527
val sut = createDefaultAnnouncementService(
2628
announcementStore = announcementStore,
2729
)
28-
assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.NeverShown)
30+
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
2931
sut.showAnnouncement(Announcement.Space)
30-
assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Show)
32+
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Show)
3133
// Simulate user close the announcement
32-
announcementStore.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Shown)
34+
sut.onAnnouncementDismissed(Announcement.Space)
3335
// Entering again the space tab should not change the value
3436
sut.showAnnouncement(Announcement.Space)
35-
assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Shown)
37+
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
38+
}
39+
40+
@Test
41+
fun `when showing NewNotificationSound announcement, announcement is set to show even if it was already shown`() = runTest {
42+
val announcementStore = InMemoryAnnouncementStore()
43+
val sut = createDefaultAnnouncementService(
44+
announcementStore = announcementStore,
45+
)
46+
assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.NeverShown)
47+
sut.showAnnouncement(Announcement.NewNotificationSound)
48+
assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
49+
// Simulate user close the announcement
50+
sut.onAnnouncementDismissed(Announcement.NewNotificationSound)
51+
// Calling again showAnnouncement should set it back to Show
52+
sut.showAnnouncement(Announcement.NewNotificationSound)
53+
assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
54+
}
55+
56+
@Test
57+
fun `test announcementsToShowFlow`() = runTest {
58+
val announcementStore = InMemoryAnnouncementStore()
59+
val sut = createDefaultAnnouncementService(
60+
announcementStore = announcementStore,
61+
)
62+
sut.announcementsToShowFlow().test {
63+
assertThat(awaitItem()).isEmpty()
64+
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
65+
assertThat(awaitItem()).containsExactly(Announcement.Space)
66+
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
67+
assertThat(awaitItem()).containsExactly(Announcement.Space, Announcement.NewNotificationSound)
68+
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
69+
assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound)
70+
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown)
71+
assertThat(awaitItem()).isEmpty()
72+
}
3673
}
3774

3875
private fun createDefaultAnnouncementService(

0 commit comments

Comments
 (0)