Improve AnnouncementService.

This commit is contained in:
Benoit Marty 2025-10-08 09:45:44 +02:00
parent 59ef782b3e
commit 752e846b1c
20 changed files with 169 additions and 81 deletions

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.map
@ -23,8 +24,8 @@ class AnnouncementPresenter(
@Composable
override fun present(): AnnouncementState {
val showSpaceAnnouncement by remember {
announcementStore.spaceAnnouncementFlow().map {
it == AnnouncementStore.SpaceAnnouncement.Show
announcementStore.announcementStateFlow(Announcement.Space).map {
it == AnnouncementStore.AnnouncementStatus.Show
}
}.collectAsState(false)
return AnnouncementState(

View file

@ -23,6 +23,8 @@ import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementSta
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
@ContributesBinding(AppScope::class)
@ -35,13 +37,36 @@ class DefaultAnnouncementService(
override suspend fun showAnnouncement(announcement: Announcement) {
when (announcement) {
Announcement.Space -> showSpaceAnnouncement()
Announcement.NewNotificationSound -> {
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStore.AnnouncementStatus.Show)
}
}
}
override suspend fun onAnnouncementDismissed(announcement: Announcement) {
announcementStore.setAnnouncementStatus(announcement, AnnouncementStore.AnnouncementStatus.Shown)
}
override fun announcementsToShowFlow(): Flow<List<Announcement>> {
return combine(
announcementStore.announcementStateFlow(Announcement.Space),
announcementStore.announcementStateFlow(Announcement.NewNotificationSound),
) { spaceAnnouncementStatus, newNotificationSoundStatus ->
buildList {
if (spaceAnnouncementStatus == AnnouncementStore.AnnouncementStatus.Show) {
add(Announcement.Space)
}
if (newNotificationSoundStatus == AnnouncementStore.AnnouncementStatus.Show) {
add(Announcement.NewNotificationSound)
}
}
}
}
private suspend fun showSpaceAnnouncement() {
val currentValue = announcementStore.spaceAnnouncementFlow().first()
if (currentValue == AnnouncementStore.SpaceAnnouncement.NeverShown) {
announcementStore.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Show)
val currentValue = announcementStore.announcementStateFlow(Announcement.Space).first()
if (currentValue == AnnouncementStore.AnnouncementStatus.NeverShown) {
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStore.AnnouncementStatus.Show)
}
}

View file

@ -10,8 +10,9 @@ package io.element.android.features.announcement.impl.spaces
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.features.announcement.impl.store.AnnouncementStore.SpaceAnnouncement
import io.element.android.features.announcement.impl.store.AnnouncementStore.AnnouncementStatus
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch
@ -26,7 +27,7 @@ class SpaceAnnouncementPresenter(
fun handleEvents(event: SpaceAnnouncementEvents) {
when (event) {
SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch {
announcementStore.setSpaceAnnouncementValue(SpaceAnnouncement.Shown)
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
}
}
}

View file

@ -7,15 +7,22 @@
package io.element.android.features.announcement.impl.store
import io.element.android.features.announcement.api.Announcement
import kotlinx.coroutines.flow.Flow
interface AnnouncementStore {
suspend fun setSpaceAnnouncementValue(value: SpaceAnnouncement)
fun spaceAnnouncementFlow(): Flow<SpaceAnnouncement>
suspend fun setAnnouncementStatus(
announcement: Announcement,
status: AnnouncementStatus,
)
fun announcementStateFlow(
announcement: Announcement,
): Flow<AnnouncementStatus>
suspend fun reset()
enum class SpaceAnnouncement {
enum class AnnouncementStatus {
NeverShown,
Show,
Shown,

View file

@ -12,11 +12,13 @@ import androidx.datastore.preferences.core.intPreferencesKey
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val spaceAnnouncementKey = intPreferencesKey("spaceAnnouncement")
private val showNewNotificationSoundBannerKey = intPreferencesKey("showNewNotificationSoundBanner")
@ContributesBinding(AppScope::class)
@Inject
@ -25,16 +27,23 @@ class DefaultAnnouncementStore(
) : AnnouncementStore {
private val store = preferenceDataStoreFactory.create("elementx_announcement")
override suspend fun setSpaceAnnouncementValue(value: AnnouncementStore.SpaceAnnouncement) {
store.edit {
it[spaceAnnouncementKey] = value.ordinal
override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStore.AnnouncementStatus) {
val key = announcement.toKey()
store.edit { prefs ->
prefs[key] = status.ordinal
}
}
override fun spaceAnnouncementFlow(): Flow<AnnouncementStore.SpaceAnnouncement> {
override fun announcementStateFlow(announcement: Announcement): Flow<AnnouncementStore.AnnouncementStatus> {
val key = announcement.toKey()
// For NewNotificationSound, a migration will set it to Show on application upgrade (see AppMigration08)
val defaultStatus = when (announcement) {
Announcement.Space -> AnnouncementStore.AnnouncementStatus.NeverShown
Announcement.NewNotificationSound -> AnnouncementStore.AnnouncementStatus.Shown
}
return store.data.map { prefs ->
val ordinal = prefs[spaceAnnouncementKey] ?: AnnouncementStore.SpaceAnnouncement.NeverShown.ordinal
AnnouncementStore.SpaceAnnouncement.entries.getOrElse(ordinal) { AnnouncementStore.SpaceAnnouncement.NeverShown }
val ordinal = prefs[key] ?: defaultStatus.ordinal
AnnouncementStore.AnnouncementStatus.entries.getOrElse(ordinal) { defaultStatus }
}
}
@ -42,3 +51,8 @@ class DefaultAnnouncementStore(
store.edit { it.clear() }
}
}
private fun Announcement.toKey() = when (this) {
Announcement.Space -> spaceAnnouncementKey
Announcement.NewNotificationSound -> showNewNotificationSoundBannerKey
}

View file

@ -8,6 +8,7 @@
package io.element.android.features.announcement.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
import io.element.android.tests.testutils.test
@ -33,10 +34,10 @@ class AnnouncementPresenterTest {
presenter.test {
val state = awaitItem()
assertThat(state.showSpaceAnnouncement).isFalse()
store.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Show)
store.setAnnouncementStatus(Announcement.Space, AnnouncementStore.AnnouncementStatus.Show)
val updatedState = awaitItem()
assertThat(updatedState.showSpaceAnnouncement).isTrue()
store.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Shown)
store.setAnnouncementStatus(Announcement.Space, AnnouncementStore.AnnouncementStatus.Shown)
val finalState = awaitItem()
assertThat(finalState.showSpaceAnnouncement).isFalse()
}

View file

@ -25,14 +25,14 @@ class DefaultAnnouncementServiceTest {
val sut = createDefaultAnnouncementService(
announcementStore = announcementStore,
)
assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.NeverShown)
assertThat(announcementStore.announcementStateFlow(Announcement.Space).first()).isEqualTo(AnnouncementStore.AnnouncementStatus.NeverShown)
sut.showAnnouncement(Announcement.Space)
assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Show)
assertThat(announcementStore.announcementStateFlow(Announcement.Space).first()).isEqualTo(AnnouncementStore.AnnouncementStatus.Show)
// Simulate user close the announcement
announcementStore.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Shown)
sut.onAnnouncementDismissed(Announcement.Space)
// Entering again the space tab should not change the value
sut.showAnnouncement(Announcement.Space)
assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Shown)
assertThat(announcementStore.announcementStateFlow(Announcement.Space).first()).isEqualTo(AnnouncementStore.AnnouncementStatus.Shown)
}
private fun createDefaultAnnouncementService(

View file

@ -8,6 +8,7 @@
package io.element.android.features.announcement.impl.spaces
import com.google.common.truth.Truth.assertThat
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
import io.element.android.tests.testutils.test
@ -23,10 +24,10 @@ class SpaceAnnouncementPresenterTest {
announcementStore = store,
)
presenter.test {
assertThat(store.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.NeverShown)
assertThat(store.announcementStateFlow(Announcement.Space).first()).isEqualTo(AnnouncementStore.AnnouncementStatus.NeverShown)
val state = awaitItem()
state.eventSink(SpaceAnnouncementEvents.Continue)
assertThat(store.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Shown)
assertThat(store.announcementStateFlow(Announcement.Space).first()).isEqualTo(AnnouncementStore.AnnouncementStatus.Shown)
}
}
}

View file

@ -7,23 +7,33 @@
package io.element.android.features.announcement.impl.store
import io.element.android.features.announcement.api.Announcement
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class InMemoryAnnouncementStore(
initialSpaceAnnouncement: AnnouncementStore.SpaceAnnouncement = AnnouncementStore.SpaceAnnouncement.NeverShown,
initialSpaceAnnouncementStatus: AnnouncementStore.AnnouncementStatus = AnnouncementStore.AnnouncementStatus.NeverShown,
initialNewNotificationSoundAnnouncementStatus: AnnouncementStore.AnnouncementStatus = AnnouncementStore.AnnouncementStatus.NeverShown,
) : AnnouncementStore {
private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncement)
override suspend fun setSpaceAnnouncementValue(value: AnnouncementStore.SpaceAnnouncement) {
spaceAnnouncement.value = value
private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncementStatus)
private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus)
override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStore.AnnouncementStatus) {
when (announcement) {
Announcement.Space -> spaceAnnouncement.value = status
Announcement.NewNotificationSound -> newNotificationSoundAnnouncement.value = status
}
}
override fun spaceAnnouncementFlow(): Flow<AnnouncementStore.SpaceAnnouncement> {
return spaceAnnouncement.asStateFlow()
override fun announcementStateFlow(announcement: Announcement): Flow<AnnouncementStore.AnnouncementStatus> {
return when (announcement) {
Announcement.Space -> spaceAnnouncement.asStateFlow()
Announcement.NewNotificationSound -> newNotificationSoundAnnouncement.asStateFlow()
}
}
override suspend fun reset() {
spaceAnnouncement.value = AnnouncementStore.SpaceAnnouncement.NeverShown
spaceAnnouncement.value = AnnouncementStore.AnnouncementStatus.NeverShown
}
}