Merge pull request #5482 from element-hq/feature/bma/improveAnnouncementService
Improve AnnouncementService.
This commit is contained in:
@@ -9,4 +9,5 @@ package io.element.android.features.announcement.api
|
||||
|
||||
enum class Announcement {
|
||||
Space,
|
||||
NewNotificationSound,
|
||||
}
|
||||
|
||||
@@ -9,10 +9,18 @@ package io.element.android.features.announcement.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AnnouncementService {
|
||||
suspend fun showAnnouncement(announcement: Announcement)
|
||||
|
||||
suspend fun onAnnouncementDismissed(announcement: Announcement)
|
||||
|
||||
fun announcementsToShowFlow(): Flow<List<Announcement>>
|
||||
|
||||
/**
|
||||
* Use this composable to render the announcement UI in Fullscreen.
|
||||
*/
|
||||
@Composable
|
||||
fun Render(
|
||||
modifier: Modifier,
|
||||
|
||||
@@ -12,6 +12,8 @@ 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.AnnouncementStatus
|
||||
import io.element.android.features.announcement.impl.store.AnnouncementStore
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -23,8 +25,8 @@ class AnnouncementPresenter(
|
||||
@Composable
|
||||
override fun present(): AnnouncementState {
|
||||
val showSpaceAnnouncement by remember {
|
||||
announcementStore.spaceAnnouncementFlow().map {
|
||||
it == AnnouncementStore.SpaceAnnouncement.Show
|
||||
announcementStore.announcementStatusFlow(Announcement.Space).map {
|
||||
it == AnnouncementStatus.Show
|
||||
}
|
||||
}.collectAsState(false)
|
||||
return AnnouncementState(
|
||||
|
||||
@@ -21,8 +21,11 @@ import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
|
||||
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView
|
||||
import io.element.android.features.announcement.impl.store.AnnouncementStatus
|
||||
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 +38,36 @@ class DefaultAnnouncementService(
|
||||
override suspend fun showAnnouncement(announcement: Announcement) {
|
||||
when (announcement) {
|
||||
Announcement.Space -> showSpaceAnnouncement()
|
||||
Announcement.NewNotificationSound -> {
|
||||
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onAnnouncementDismissed(announcement: Announcement) {
|
||||
announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Shown)
|
||||
}
|
||||
|
||||
override fun announcementsToShowFlow(): Flow<List<Announcement>> {
|
||||
return combine(
|
||||
announcementStore.announcementStatusFlow(Announcement.Space),
|
||||
announcementStore.announcementStatusFlow(Announcement.NewNotificationSound),
|
||||
) { spaceAnnouncementStatus, newNotificationSoundStatus ->
|
||||
buildList {
|
||||
if (spaceAnnouncementStatus == AnnouncementStatus.Show) {
|
||||
add(Announcement.Space)
|
||||
}
|
||||
if (newNotificationSoundStatus == 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.announcementStatusFlow(Announcement.Space).first()
|
||||
if (currentValue == AnnouncementStatus.NeverShown) {
|
||||
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.AnnouncementStatus
|
||||
import io.element.android.features.announcement.impl.store.AnnouncementStore
|
||||
import io.element.android.features.announcement.impl.store.AnnouncementStore.SpaceAnnouncement
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.announcement.impl.store
|
||||
|
||||
enum class AnnouncementStatus {
|
||||
NeverShown,
|
||||
Show,
|
||||
Shown,
|
||||
}
|
||||
@@ -7,17 +7,18 @@
|
||||
|
||||
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 announcementStatusFlow(
|
||||
announcement: Announcement,
|
||||
): Flow<AnnouncementStatus>
|
||||
|
||||
suspend fun reset()
|
||||
|
||||
enum class SpaceAnnouncement {
|
||||
NeverShown,
|
||||
Show,
|
||||
Shown,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 newNotificationSoundKey = intPreferencesKey("newNotificationSound")
|
||||
|
||||
@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: AnnouncementStatus) {
|
||||
val key = announcement.toKey()
|
||||
store.edit { prefs ->
|
||||
prefs[key] = status.ordinal
|
||||
}
|
||||
}
|
||||
|
||||
override fun spaceAnnouncementFlow(): Flow<AnnouncementStore.SpaceAnnouncement> {
|
||||
override fun announcementStatusFlow(announcement: Announcement): Flow<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 -> AnnouncementStatus.NeverShown
|
||||
Announcement.NewNotificationSound -> 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
|
||||
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 -> newNotificationSoundKey
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
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.AnnouncementStatus
|
||||
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 +35,10 @@ class AnnouncementPresenterTest {
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.showSpaceAnnouncement).isFalse()
|
||||
store.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Show)
|
||||
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
|
||||
val updatedState = awaitItem()
|
||||
assertThat(updatedState.showSpaceAnnouncement).isTrue()
|
||||
store.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Shown)
|
||||
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.showSpaceAnnouncement).isFalse()
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
|
||||
package io.element.android.features.announcement.impl
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
|
||||
import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState
|
||||
import io.element.android.features.announcement.impl.store.AnnouncementStatus
|
||||
import io.element.android.features.announcement.impl.store.AnnouncementStore
|
||||
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
@@ -25,14 +27,49 @@ class DefaultAnnouncementServiceTest {
|
||||
val sut = createDefaultAnnouncementService(
|
||||
announcementStore = announcementStore,
|
||||
)
|
||||
assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.NeverShown)
|
||||
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
|
||||
sut.showAnnouncement(Announcement.Space)
|
||||
assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Show)
|
||||
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(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.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when showing NewNotificationSound announcement, announcement is set to show even if it was already shown`() = runTest {
|
||||
val announcementStore = InMemoryAnnouncementStore()
|
||||
val sut = createDefaultAnnouncementService(
|
||||
announcementStore = announcementStore,
|
||||
)
|
||||
assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.NeverShown)
|
||||
sut.showAnnouncement(Announcement.NewNotificationSound)
|
||||
assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
|
||||
// Simulate user close the announcement
|
||||
sut.onAnnouncementDismissed(Announcement.NewNotificationSound)
|
||||
// Calling again showAnnouncement should set it back to Show
|
||||
sut.showAnnouncement(Announcement.NewNotificationSound)
|
||||
assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test announcementsToShowFlow`() = runTest {
|
||||
val announcementStore = InMemoryAnnouncementStore()
|
||||
val sut = createDefaultAnnouncementService(
|
||||
announcementStore = announcementStore,
|
||||
)
|
||||
sut.announcementsToShowFlow().test {
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
|
||||
assertThat(awaitItem()).containsExactly(Announcement.Space)
|
||||
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
|
||||
assertThat(awaitItem()).containsExactly(Announcement.Space, Announcement.NewNotificationSound)
|
||||
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
|
||||
assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound)
|
||||
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown)
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDefaultAnnouncementService(
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
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.AnnouncementStatus
|
||||
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 +25,10 @@ class SpaceAnnouncementPresenterTest {
|
||||
announcementStore = store,
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(store.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.NeverShown)
|
||||
assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
|
||||
val state = awaitItem()
|
||||
state.eventSink(SpaceAnnouncementEvents.Continue)
|
||||
assertThat(store.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Shown)
|
||||
assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: AnnouncementStatus = AnnouncementStatus.NeverShown,
|
||||
initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = 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: AnnouncementStatus) {
|
||||
announcement.toMutableStateFlow().value = status
|
||||
}
|
||||
|
||||
override fun spaceAnnouncementFlow(): Flow<AnnouncementStore.SpaceAnnouncement> {
|
||||
return spaceAnnouncement.asStateFlow()
|
||||
override fun announcementStatusFlow(announcement: Announcement): Flow<AnnouncementStatus> {
|
||||
return announcement.toMutableStateFlow().asStateFlow()
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
spaceAnnouncement.value = AnnouncementStore.SpaceAnnouncement.NeverShown
|
||||
spaceAnnouncement.value = AnnouncementStatus.NeverShown
|
||||
newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown
|
||||
}
|
||||
|
||||
private fun Announcement.toMutableStateFlow() = when (this) {
|
||||
Announcement.Space -> spaceAnnouncement
|
||||
Announcement.NewNotificationSound -> newNotificationSoundAnnouncement
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,34 @@ import androidx.compose.ui.Modifier
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class FakeAnnouncementService(
|
||||
initialAnnouncementsToShowFlowValue: List<Announcement> = emptyList(),
|
||||
val showAnnouncementResult: (Announcement) -> Unit = { lambdaError() },
|
||||
val onAnnouncementDismissedResult: (Announcement) -> Unit = { lambdaError() },
|
||||
val renderResult: (Modifier) -> Unit = { lambdaError() },
|
||||
) : AnnouncementService {
|
||||
private val announcementsToShowFlowValue = MutableStateFlow(initialAnnouncementsToShowFlowValue)
|
||||
|
||||
override suspend fun showAnnouncement(announcement: Announcement) {
|
||||
showAnnouncementResult(announcement)
|
||||
}
|
||||
|
||||
override suspend fun onAnnouncementDismissed(announcement: Announcement) {
|
||||
onAnnouncementDismissedResult(announcement)
|
||||
}
|
||||
|
||||
override fun announcementsToShowFlow(): Flow<List<Announcement>> {
|
||||
return announcementsToShowFlowValue.asStateFlow()
|
||||
}
|
||||
|
||||
fun emitAnnouncementsToShow(value: List<Announcement>) {
|
||||
announcementsToShowFlowValue.value = value
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Render(modifier: Modifier) {
|
||||
renderResult(modifier)
|
||||
|
||||
@@ -24,6 +24,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import dev.zacsweers.metro.Inject
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
||||
import io.element.android.features.home.impl.search.RoomListSearchEvents
|
||||
@@ -82,6 +84,7 @@ class RoomListPresenter(
|
||||
private val notificationCleaner: NotificationCleaner,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val seenInvitesStore: SeenInvitesStore,
|
||||
private val announcementService: AnnouncementService,
|
||||
) : Presenter<RoomListState> {
|
||||
private val encryptionService = client.encryptionService
|
||||
|
||||
@@ -98,7 +101,11 @@ class RoomListPresenter(
|
||||
}
|
||||
|
||||
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
|
||||
val showNewNotificationSoundBanner by appPreferencesStore.showNewNotificationSoundBanner().collectAsState(false)
|
||||
val showNewNotificationSoundBanner by remember {
|
||||
announcementService.announcementsToShowFlow().map { announcements ->
|
||||
announcements.contains(Announcement.NewNotificationSound)
|
||||
}
|
||||
}.collectAsState(false)
|
||||
|
||||
// Avatar indicator
|
||||
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
|
||||
@@ -114,7 +121,7 @@ class RoomListPresenter(
|
||||
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
|
||||
RoomListEvents.DismissBanner -> securityBannerDismissed = true
|
||||
RoomListEvents.DismissNewNotificationSoundBanner -> coroutineScope.launch {
|
||||
appPreferencesStore.setShowNewNotificationSoundBanner(false)
|
||||
announcementService.onAnnouncementDismissed(Announcement.NewNotificationSound)
|
||||
}
|
||||
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||
is RoomListEvents.ShowContextMenu -> {
|
||||
|
||||
@@ -12,6 +12,8 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.features.home.impl.FakeDateTimeObserver
|
||||
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
|
||||
@@ -28,6 +30,7 @@ import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInvit
|
||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
@@ -606,23 +609,27 @@ class RoomListPresenterTest {
|
||||
)
|
||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||
roomListService.postAllRooms(listOf(roomSummary))
|
||||
val store = InMemoryAppPreferencesStore()
|
||||
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
|
||||
val announcementService = FakeAnnouncementService(
|
||||
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
|
||||
)
|
||||
val presenter = createRoomListPresenter(
|
||||
client = matrixClient,
|
||||
appPreferencesStore = store,
|
||||
announcementService = announcementService,
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
|
||||
assertThat(announcementService.announcementsToShowFlow().first()).isEmpty()
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse()
|
||||
store.setShowNewNotificationSoundBanner(true)
|
||||
assertThat(store.showNewNotificationSoundBanner().first()).isTrue()
|
||||
announcementService.emitAnnouncementsToShow(listOf(Announcement.NewNotificationSound))
|
||||
assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isTrue()
|
||||
state.eventSink(RoomListEvents.DismissNewNotificationSoundBanner)
|
||||
onAnnouncementDismissedResult.assertions().isCalledOnce()
|
||||
.with(value(Announcement.NewNotificationSound))
|
||||
// Simulate service updating the value
|
||||
announcementService.emitAnnouncementsToShow(emptyList())
|
||||
assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isFalse()
|
||||
// Ensure store has been updated
|
||||
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,6 +646,7 @@ class RoomListPresenterTest {
|
||||
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||
announcementService: AnnouncementService = FakeAnnouncementService(),
|
||||
) = RoomListPresenter(
|
||||
client = client,
|
||||
leaveRoomPresenter = { leaveRoomState },
|
||||
@@ -663,5 +671,6 @@ class RoomListPresenterTest {
|
||||
notificationCleaner = notificationCleaner,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
seenInvitesStore = seenInvitesStore,
|
||||
announcementService = announcementService,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ android {
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.announcement.api)
|
||||
implementation(projects.features.migration.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.androidutils)
|
||||
@@ -34,5 +35,6 @@ dependencies {
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.features.announcement.test)
|
||||
testImplementation(projects.features.rageshake.test)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ package io.element.android.features.migration.impl.migrations
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
|
||||
/**
|
||||
* Ensure the new notification sound banner is displayed, but only on application upgrade.
|
||||
@@ -18,13 +19,13 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class AppMigration08(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val announcementService: AnnouncementService,
|
||||
) : AppMigration {
|
||||
override val order: Int = 8
|
||||
|
||||
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||
if (!isFreshInstall) {
|
||||
appPreferencesStore.setShowNewNotificationSoundBanner(true)
|
||||
announcementService.showAnnouncement(Announcement.NewNotificationSound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,27 +8,35 @@
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class AppMigration08Test {
|
||||
@Test
|
||||
fun `migration on fresh install should not modify the store`() = runTest {
|
||||
val store = InMemoryAppPreferencesStore()
|
||||
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
|
||||
val migration = AppMigration08(store)
|
||||
fun `migration on fresh install should not invoke the AnnouncementService`() = runTest {
|
||||
val service = FakeAnnouncementService(
|
||||
showAnnouncementResult = { lambdaError() },
|
||||
)
|
||||
val migration = AppMigration08(service)
|
||||
migration.migrate(isFreshInstall = true)
|
||||
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
|
||||
assertThat(service.announcementsToShowFlow().first()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migration on upgrade should modify the store`() = runTest {
|
||||
val store = InMemoryAppPreferencesStore()
|
||||
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
|
||||
val migration = AppMigration08(store)
|
||||
fun `migration on upgrade should invoke the AnnouncementService`() = runTest {
|
||||
val showAnnouncementResult = lambdaRecorder<Announcement, Unit> { }
|
||||
val service = FakeAnnouncementService(
|
||||
showAnnouncementResult = showAnnouncementResult,
|
||||
)
|
||||
val migration = AppMigration08(service)
|
||||
migration.migrate(isFreshInstall = false)
|
||||
assertThat(store.showNewNotificationSoundBanner().first()).isTrue()
|
||||
showAnnouncementResult.assertions().isCalledOnce()
|
||||
.with(value(Announcement.NewNotificationSound))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,5 @@ interface AppPreferencesStore {
|
||||
suspend fun setTracingLogPacks(targets: Set<TraceLogPack>)
|
||||
fun getTracingLogPacksFlow(): Flow<Set<TraceLogPack>>
|
||||
|
||||
suspend fun setShowNewNotificationSoundBanner(show: Boolean)
|
||||
fun showNewNotificationSoundBanner(): Flow<Boolean>
|
||||
|
||||
suspend fun reset()
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars")
|
||||
private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue")
|
||||
private val logLevelKey = stringPreferencesKey("logLevel")
|
||||
private val traceLogPacksKey = stringPreferencesKey("traceLogPacks")
|
||||
private val showNewNotificationSoundBannerKey = booleanPreferencesKey("showNewNotificationSoundBanner")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
@@ -146,19 +145,6 @@ class DefaultAppPreferencesStore(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setShowNewNotificationSoundBanner(show: Boolean) {
|
||||
store.edit { prefs ->
|
||||
prefs[showNewNotificationSoundBannerKey] = show
|
||||
}
|
||||
}
|
||||
|
||||
override fun showNewNotificationSoundBanner(): Flow<Boolean> {
|
||||
return store.data.map { prefs ->
|
||||
// Default is false, but a migration will set it to true on application upgrade (see AppMigration08)
|
||||
prefs[showNewNotificationSoundBannerKey] ?: false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
store.edit { it.clear() }
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ class InMemoryAppPreferencesStore(
|
||||
theme: String? = null,
|
||||
logLevel: LogLevel = LogLevel.INFO,
|
||||
traceLockPacks: Set<TraceLogPack> = emptySet(),
|
||||
showNewNotificationSoundBanner: Boolean = false,
|
||||
) : AppPreferencesStore {
|
||||
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
|
||||
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
|
||||
@@ -31,7 +30,6 @@ class InMemoryAppPreferencesStore(
|
||||
private val tracingLogPacks = MutableStateFlow(traceLockPacks)
|
||||
private val hideInviteAvatars = MutableStateFlow(hideInviteAvatars)
|
||||
private val timelineMediaPreviewValue = MutableStateFlow(timelineMediaPreviewValue)
|
||||
private val showNewNotificationSoundBanner = MutableStateFlow(showNewNotificationSoundBanner)
|
||||
|
||||
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
|
||||
isDeveloperModeEnabled.value = enabled
|
||||
@@ -93,14 +91,6 @@ class InMemoryAppPreferencesStore(
|
||||
return tracingLogPacks
|
||||
}
|
||||
|
||||
override suspend fun setShowNewNotificationSoundBanner(show: Boolean) {
|
||||
showNewNotificationSoundBanner.value = show
|
||||
}
|
||||
|
||||
override fun showNewNotificationSoundBanner(): Flow<Boolean> {
|
||||
return showNewNotificationSoundBanner
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
// No op
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user