Show new notification sound banner logic

This commit is contained in:
Benoit Marty
2025-10-07 12:20:24 +02:00
committed by Benoit Marty
parent 71d2c1d9df
commit 4475ed0d37
11 changed files with 150 additions and 2 deletions

View File

@@ -251,6 +251,12 @@ private fun RoomsViewList(
item {
BatteryOptimizationBanner(state = state.batteryOptimizationState)
}
} else if (state.showNewNotificationSoundBanner) {
item {
NewNotificationSoundBanner(
onDismissClick = { updatedEventSink(RoomListEvents.DismissNewNotificationSoundBanner) },
)
}
}
}

View File

@@ -26,17 +26,22 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
aSkeletonContentState(),
anEmptyContentState(),
anEmptyContentState(securityBannerState = SecurityBannerState.SetUpRecovery),
aRoomsContentState(
showNewNotificationSoundBanner = true,
),
)
}
internal fun aRoomsContentState(
securityBannerState: SecurityBannerState = SecurityBannerState.None,
showNewNotificationSoundBanner: Boolean = false,
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
seenRoomInvites: Set<RoomId> = emptySet(),
) = RoomListContentState.Rooms(
securityBannerState = securityBannerState,
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
batteryOptimizationState = batteryOptimizationState,
summaries = summaries,

View File

@@ -14,6 +14,7 @@ sealed interface RoomListEvents {
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
data object DismissRequestVerificationPrompt : RoomListEvents
data object DismissBanner : RoomListEvents
data object DismissNewNotificationSoundBanner : RoomListEvents
data object ToggleSearchResults : RoomListEvents
data class ShowContextMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents

View File

@@ -98,6 +98,7 @@ class RoomListPresenter(
}
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
val showNewNotificationSoundBanner by appPreferencesStore.showNewNotificationSoundBanner().collectAsState(false)
// Avatar indicator
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
@@ -112,6 +113,9 @@ class RoomListPresenter(
}
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
RoomListEvents.DismissBanner -> securityBannerDismissed = true
RoomListEvents.DismissNewNotificationSoundBanner -> coroutineScope.launch {
appPreferencesStore.setShowNewNotificationSoundBanner(false)
}
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
is RoomListEvents.ShowContextMenu -> {
coroutineScope.showContextMenu(event, contextMenu)
@@ -141,7 +145,10 @@ class RoomListPresenter(
}
}
val contentState = roomListContentState(securityBannerDismissed)
val contentState = roomListContentState(
securityBannerDismissed,
showNewNotificationSoundBanner,
)
val canReportRoom by produceState(false) { value = client.canReportRoom() }
@@ -197,6 +204,7 @@ class RoomListPresenter(
@Composable
private fun roomListContentState(
securityBannerDismissed: Boolean,
showNewNotificationSoundBanner: Boolean,
): RoomListContentState {
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
@@ -215,11 +223,14 @@ class RoomListPresenter(
val seenRoomInvites by remember { seenInvitesStore.seenRoomIds() }.collectAsState(emptySet())
val securityBannerState by rememberSecurityBannerState(securityBannerDismissed)
return when {
showEmpty -> RoomListContentState.Empty(securityBannerState = securityBannerState)
showEmpty -> RoomListContentState.Empty(
securityBannerState = securityBannerState,
)
showSkeleton -> RoomListContentState.Skeleton(count = 16)
else -> {
RoomListContentState.Rooms(
securityBannerState = securityBannerState,
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
batteryOptimizationState = batteryOptimizationPresenter.present(),
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList(),

View File

@@ -69,6 +69,7 @@ sealed interface RoomListContentState {
val securityBannerState: SecurityBannerState,
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
val batteryOptimizationState: BatteryOptimizationState,
val showNewNotificationSoundBanner: Boolean,
val summaries: ImmutableList<RoomListRoomSummary>,
val seenRoomInvites: ImmutableSet<RoomId>,
) : RoomListContentState

View File

@@ -75,6 +75,7 @@ import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@@ -593,6 +594,38 @@ class RoomListPresenterTest {
}
}
@Test
fun `present - notification sound banner`() = runTest {
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val roomSummary = aRoomSummary(
currentUserMembership = CurrentUserMembership.INVITED
)
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
roomListService.postAllRooms(listOf(roomSummary))
val store = InMemoryAppPreferencesStore()
val presenter = createRoomListPresenter(
client = matrixClient,
appPreferencesStore = store,
)
presenter.test {
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
skipItems(1)
val state = awaitItem()
assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse()
store.setShowNewNotificationSoundBanner(true)
assertThat(store.showNewNotificationSoundBanner().first()).isTrue()
assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isTrue()
state.eventSink(RoomListEvents.DismissNewNotificationSoundBanner)
assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isFalse()
// Ensure store has been updated
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
}
}
private fun TestScope.createRoomListPresenter(
client: MatrixClient = FakeMatrixClient(),
leaveRoomState: LeaveRoomState = aLeaveRoomState(),

View File

@@ -0,0 +1,30 @@
/*
* 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.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
/**
* Ensure the new notification sound banner is displayed, but only on application upgrade.
*/
@ContributesIntoSet(AppScope::class)
@Inject
class AppMigration08(
private val appPreferencesStore: AppPreferencesStore,
) : AppMigration {
override val order: Int = 8
override suspend fun migrate(isFreshInstall: Boolean) {
if (!isFreshInstall) {
appPreferencesStore.setShowNewNotificationSoundBanner(true)
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2024 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.migration.impl.migrations
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
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)
migration.migrate(isFreshInstall = true)
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
}
@Test
fun `migration on upgrade should modify the store`() = runTest {
val store = InMemoryAppPreferencesStore()
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
val migration = AppMigration08(store)
migration.migrate(isFreshInstall = false)
assertThat(store.showNewNotificationSoundBanner().first()).isTrue()
}
}