Skip to content

Commit 98637b8

Browse files
committed
Show new notification sound banner logic
1 parent df384f6 commit 98637b8

File tree

11 files changed

+150
-2
lines changed

11 files changed

+150
-2
lines changed

features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@ private fun RoomsViewList(
251251
item {
252252
BatteryOptimizationBanner(state = state.batteryOptimizationState)
253253
}
254+
} else if (state.showNewNotificationSoundBanner) {
255+
item {
256+
NewNotificationSoundBanner(
257+
onDismissClick = { updatedEventSink(RoomListEvents.DismissNewNotificationSoundBanner) },
258+
)
259+
}
254260
}
255261
}
256262

features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,22 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
2626
aSkeletonContentState(),
2727
anEmptyContentState(),
2828
anEmptyContentState(securityBannerState = SecurityBannerState.SetUpRecovery),
29+
aRoomsContentState(
30+
showNewNotificationSoundBanner = true,
31+
),
2932
)
3033
}
3134

3235
internal fun aRoomsContentState(
3336
securityBannerState: SecurityBannerState = SecurityBannerState.None,
37+
showNewNotificationSoundBanner: Boolean = false,
3438
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
3539
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
3640
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
3741
seenRoomInvites: Set<RoomId> = emptySet(),
3842
) = RoomListContentState.Rooms(
3943
securityBannerState = securityBannerState,
44+
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
4045
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
4146
batteryOptimizationState = batteryOptimizationState,
4247
summaries = summaries,

features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ sealed interface RoomListEvents {
1414
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
1515
data object DismissRequestVerificationPrompt : RoomListEvents
1616
data object DismissBanner : RoomListEvents
17+
data object DismissNewNotificationSoundBanner : RoomListEvents
1718
data object ToggleSearchResults : RoomListEvents
1819
data class ShowContextMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents
1920

features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class RoomListPresenter(
9898
}
9999

100100
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
101+
val showNewNotificationSoundBanner by appPreferencesStore.showNewNotificationSoundBanner().collectAsState(false)
101102

102103
// Avatar indicator
103104
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
@@ -112,6 +113,9 @@ class RoomListPresenter(
112113
}
113114
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
114115
RoomListEvents.DismissBanner -> securityBannerDismissed = true
116+
RoomListEvents.DismissNewNotificationSoundBanner -> coroutineScope.launch {
117+
appPreferencesStore.setShowNewNotificationSoundBanner(false)
118+
}
115119
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
116120
is RoomListEvents.ShowContextMenu -> {
117121
coroutineScope.showContextMenu(event, contextMenu)
@@ -141,7 +145,10 @@ class RoomListPresenter(
141145
}
142146
}
143147

144-
val contentState = roomListContentState(securityBannerDismissed)
148+
val contentState = roomListContentState(
149+
securityBannerDismissed,
150+
showNewNotificationSoundBanner,
151+
)
145152

146153
val canReportRoom by produceState(false) { value = client.canReportRoom() }
147154

@@ -197,6 +204,7 @@ class RoomListPresenter(
197204
@Composable
198205
private fun roomListContentState(
199206
securityBannerDismissed: Boolean,
207+
showNewNotificationSoundBanner: Boolean,
200208
): RoomListContentState {
201209
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
202210
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
@@ -215,11 +223,14 @@ class RoomListPresenter(
215223
val seenRoomInvites by remember { seenInvitesStore.seenRoomIds() }.collectAsState(emptySet())
216224
val securityBannerState by rememberSecurityBannerState(securityBannerDismissed)
217225
return when {
218-
showEmpty -> RoomListContentState.Empty(securityBannerState = securityBannerState)
226+
showEmpty -> RoomListContentState.Empty(
227+
securityBannerState = securityBannerState,
228+
)
219229
showSkeleton -> RoomListContentState.Skeleton(count = 16)
220230
else -> {
221231
RoomListContentState.Rooms(
222232
securityBannerState = securityBannerState,
233+
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
223234
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
224235
batteryOptimizationState = batteryOptimizationPresenter.present(),
225236
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList(),

features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ sealed interface RoomListContentState {
6969
val securityBannerState: SecurityBannerState,
7070
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
7171
val batteryOptimizationState: BatteryOptimizationState,
72+
val showNewNotificationSoundBanner: Boolean,
7273
val summaries: ImmutableList<RoomListRoomSummary>,
7374
val seenRoomInvites: ImmutableSet<RoomId>,
7475
) : RoomListContentState

features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import io.element.android.tests.testutils.lambda.value
7575
import io.element.android.tests.testutils.test
7676
import io.element.android.tests.testutils.testCoroutineDispatchers
7777
import kotlinx.coroutines.ExperimentalCoroutinesApi
78+
import kotlinx.coroutines.flow.first
7879
import kotlinx.coroutines.test.TestScope
7980
import kotlinx.coroutines.test.advanceTimeBy
8081
import kotlinx.coroutines.test.runTest
@@ -593,6 +594,38 @@ class RoomListPresenterTest {
593594
}
594595
}
595596

597+
@Test
598+
fun `present - notification sound banner`() = runTest {
599+
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
600+
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
601+
val matrixClient = FakeMatrixClient(
602+
roomListService = roomListService,
603+
)
604+
val roomSummary = aRoomSummary(
605+
currentUserMembership = CurrentUserMembership.INVITED
606+
)
607+
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
608+
roomListService.postAllRooms(listOf(roomSummary))
609+
val store = InMemoryAppPreferencesStore()
610+
val presenter = createRoomListPresenter(
611+
client = matrixClient,
612+
appPreferencesStore = store,
613+
)
614+
presenter.test {
615+
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
616+
skipItems(1)
617+
val state = awaitItem()
618+
assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse()
619+
store.setShowNewNotificationSoundBanner(true)
620+
assertThat(store.showNewNotificationSoundBanner().first()).isTrue()
621+
assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isTrue()
622+
state.eventSink(RoomListEvents.DismissNewNotificationSoundBanner)
623+
assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isFalse()
624+
// Ensure store has been updated
625+
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
626+
}
627+
}
628+
596629
private fun TestScope.createRoomListPresenter(
597630
client: MatrixClient = FakeMatrixClient(),
598631
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.migration.impl.migrations
9+
10+
import dev.zacsweers.metro.AppScope
11+
import dev.zacsweers.metro.ContributesIntoSet
12+
import dev.zacsweers.metro.Inject
13+
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
14+
15+
/**
16+
* Ensure the new notification sound banner is displayed, but only on application upgrade.
17+
*/
18+
@ContributesIntoSet(AppScope::class)
19+
@Inject
20+
class AppMigration08(
21+
private val appPreferencesStore: AppPreferencesStore,
22+
) : AppMigration {
23+
override val order: Int = 8
24+
25+
override suspend fun migrate(isFreshInstall: Boolean) {
26+
if (!isFreshInstall) {
27+
appPreferencesStore.setShowNewNotificationSoundBanner(true)
28+
}
29+
}
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2024 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.migration.impl.migrations
9+
10+
import com.google.common.truth.Truth.assertThat
11+
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
12+
import kotlinx.coroutines.flow.first
13+
import kotlinx.coroutines.test.runTest
14+
import org.junit.Test
15+
16+
class AppMigration08Test {
17+
@Test
18+
fun `migration on fresh install should not modify the store`() = runTest {
19+
val store = InMemoryAppPreferencesStore()
20+
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
21+
val migration = AppMigration08(store)
22+
migration.migrate(isFreshInstall = true)
23+
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
24+
}
25+
26+
@Test
27+
fun `migration on upgrade should modify the store`() = runTest {
28+
val store = InMemoryAppPreferencesStore()
29+
assertThat(store.showNewNotificationSoundBanner().first()).isFalse()
30+
val migration = AppMigration08(store)
31+
migration.migrate(isFreshInstall = false)
32+
assertThat(store.showNewNotificationSoundBanner().first()).isTrue()
33+
}
34+
}

libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,8 @@ interface AppPreferencesStore {
3737
suspend fun setTracingLogPacks(targets: Set<TraceLogPack>)
3838
fun getTracingLogPacksFlow(): Flow<Set<TraceLogPack>>
3939

40+
suspend fun setShowNewNotificationSoundBanner(show: Boolean)
41+
fun showNewNotificationSoundBanner(): Flow<Boolean>
42+
4043
suspend fun reset()
4144
}

libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars")
3030
private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue")
3131
private val logLevelKey = stringPreferencesKey("logLevel")
3232
private val traceLogPacksKey = stringPreferencesKey("traceLogPacks")
33+
private val showNewNotificationSoundBannerKey = booleanPreferencesKey("showNewNotificationSoundBanner")
3334

3435
@ContributesBinding(AppScope::class)
3536
@Inject
@@ -145,6 +146,19 @@ class DefaultAppPreferencesStore(
145146
}
146147
}
147148

149+
override suspend fun setShowNewNotificationSoundBanner(show: Boolean) {
150+
store.edit { prefs ->
151+
prefs[showNewNotificationSoundBannerKey] = show
152+
}
153+
}
154+
155+
override fun showNewNotificationSoundBanner(): Flow<Boolean> {
156+
return store.data.map { prefs ->
157+
// Default is false, but a migration will set it to true on application upgrade (see AppMigration08)
158+
prefs[showNewNotificationSoundBannerKey] ?: false
159+
}
160+
}
161+
148162
override suspend fun reset() {
149163
store.edit { it.clear() }
150164
}

0 commit comments

Comments
 (0)