Allow displaying only unread notifications (#7803)

* Allow displaying only unread notifications

Add `FeatureFlags.ShowAllActivityInRoomList` (enabled by default, to match the current behavior) that, when disabled, will display the unread messages of a room instead of unread notifications if no unread notifications are found

* Restore UI changes to the badges, `RoomSummaryRow` and `HorizontalFloatingToolbar`

* Fix `NavigationBarIcon`: use `BadgeBox` component.

* Adjust padding in `RoomSummaryRow` to 6.dp

* Fix other indicators not displaying a dot anymore

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2026-10-01 19:46:01 +02:00
committed by GitHub
parent a3ff7e1486
commit 2dd0a47eb4
197 changed files with 825 additions and 414 deletions
@@ -34,4 +34,5 @@ sealed interface NotificationSettingsEvent {
data object DismissCallRingtoneDialog : NotificationSettingsEvent
data class SelectCallRingtonePreset(val sound: NotificationSound) : NotificationSettingsEvent
data object LaunchCallRingtonePicker : NotificationSettingsEvent
data object ToggleShowAllActivityInRoomList : NotificationSettingsEvent
}
@@ -28,6 +28,8 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
@@ -73,6 +75,7 @@ class NotificationSettingsPresenter(
private val stringProvider: StringProvider,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val featureFlagService: FeatureFlagService,
) : Presenter<NotificationSettingsState> {
// Serializes the pick → copy → persist → recreate pipeline per slot. The copier already locks
// its `<slot>.tmp` file, but the persist+recreate window outside the copier is unprotected;
@@ -196,6 +199,11 @@ class NotificationSettingsPresenter(
)
}
val showAllActivityInRoomList by produceState(false) {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.ShowAllActivityInRoomList)
.collect { value = it }
}
fun handleEvent(event: NotificationSettingsEvent) {
when (event) {
is NotificationSettingsEvent.SetAtRoomNotificationsEnabled -> {
@@ -276,6 +284,9 @@ class NotificationSettingsPresenter(
NotificationSettingsEvent.DismissCallRingtoneCopyError -> {
callRingtoneCopyError = false
}
NotificationSettingsEvent.ToggleShowAllActivityInRoomList -> localCoroutineScope.launch {
featureFlagService.setFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList, !showAllActivityInRoomList)
}
}
}
@@ -304,6 +315,7 @@ class NotificationSettingsPresenter(
showCallRingtoneDialog = showCallRingtoneDialog,
pendingMessageSoundPickerLaunch = pendingMessageSoundPickerLaunch,
pendingCallRingtonePickerLaunch = pendingCallRingtonePickerLaunch,
showAllActivityInRoomList = showAllActivityInRoomList,
eventSink = ::handleEvent,
)
}
@@ -42,6 +42,10 @@ data class NotificationSettingsState(
* auto-opens on screen entry.
*/
val pendingCallRingtonePickerLaunch: Int,
/**
* Whether to show all activity indicators in the room list, using unread messages instead of notifications.
*/
val showAllActivityInRoomList: Boolean,
val eventSink: (NotificationSettingsEvent) -> Unit,
) {
sealed interface MatrixSettings {
@@ -99,6 +99,7 @@ fun aValidNotificationSettingsState(
callRingtoneCopyError: Boolean = false,
showMessageSoundDialog: Boolean = false,
showCallRingtoneDialog: Boolean = false,
showAllActivityInRoomList: Boolean = false,
pendingMessageSoundPickerLaunch: Int = 0,
pendingCallRingtonePickerLaunch: Int = 0,
eventSink: (NotificationSettingsEvent) -> Unit = {},
@@ -133,6 +134,7 @@ fun aValidNotificationSettingsState(
showCallRingtoneDialog = showCallRingtoneDialog,
pendingMessageSoundPickerLaunch = pendingMessageSoundPickerLaunch,
pendingCallRingtonePickerLaunch = pendingCallRingtonePickerLaunch,
showAllActivityInRoomList = showAllActivityInRoomList,
eventSink = eventSink,
)
@@ -166,6 +168,7 @@ fun aInvalidNotificationSettingsState(
showCallRingtoneDialog = false,
pendingMessageSoundPickerLaunch = 0,
pendingCallRingtonePickerLaunch = 0,
showAllActivityInRoomList = false,
eventSink = eventSink,
)
@@ -199,6 +199,16 @@ private fun NotificationSettingsContentView(
onCheckedChange = onMentionNotificationsChange
)
}
PreferenceCategory {
PreferenceSwitch(
title = stringResource(R.string.screen_notification_settings_show_all_activity_title),
subtitle = stringResource(R.string.screen_notification_settings_show_all_activity_subtitle),
isChecked = state.showAllActivityInRoomList,
onCheckedChange = { state.eventSink(NotificationSettingsEvent.ToggleShowAllActivityInRoomList) }
)
}
SoundsPreferenceCategory(state = state)
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_additional_settings_section_title)) {
// TODO We are removing the call notification toggle until support for call notifications has been added
@@ -93,7 +93,6 @@
<string name="screen_notification_settings_mode_mentions">"Згадки"</string>
<string name="screen_notification_settings_notification_section_title">"Сповіщати мене про"</string>
<string name="screen_notification_settings_room_mention_label">"Згадки @room"</string>
<string name="screen_notification_settings_show_all_activity">"Показати всю діяльність у списку кімнат (крапки або кількість непрочитаних повідомлень)"</string>
<string name="screen_notification_settings_sound_custom_fallback">"Власний"</string>
<string name="screen_notification_settings_sound_custom_sound_button_title">"Користувацький звук…"</string>
<string name="screen_notification_settings_sound_delete_sound_error_title">"Помилка видалення файлу"</string>
@@ -91,7 +91,6 @@
<string name="screen_notification_settings_mode_mentions">"提及"</string>
<string name="screen_notification_settings_notification_section_title">"通知我以下类型"</string>
<string name="screen_notification_settings_room_mention_label">"含有 @room 的提及"</string>
<string name="screen_notification_settings_show_all_activity">"在房间列表上显示所有活动(用于未读消息的圆点或数字)"</string>
<string name="screen_notification_settings_sound_custom_fallback">"自定义"</string>
<string name="screen_notification_settings_sound_custom_sound_button_title">"自定义提示音…"</string>
<string name="screen_notification_settings_sound_delete_sound_error_title">"删除文件时出错"</string>
@@ -92,7 +92,8 @@ If you proceed, some of your settings may change."</string>
<string name="screen_notification_settings_mode_mentions">"Mentions"</string>
<string name="screen_notification_settings_notification_section_title">"Notify me for"</string>
<string name="screen_notification_settings_room_mention_label">"Mentions of @room"</string>
<string name="screen_notification_settings_show_all_activity">"Show all activity in the room list (dots or number of unread messages)"</string>
<string name="screen_notification_settings_show_all_activity_subtitle">"Dots or number of unread messages"</string>
<string name="screen_notification_settings_show_all_activity_title">"Show all activity in the room list"</string>
<string name="screen_notification_settings_sound_custom_fallback">"Custom"</string>
<string name="screen_notification_settings_sound_custom_sound_button_title">"Custom sound…"</string>
<string name="screen_notification_settings_sound_delete_sound_error_title">"Error deleting file"</string>
@@ -10,6 +10,9 @@ package io.element.android.features.preferences.impl.notifications
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.MatrixClient
@@ -1104,6 +1107,46 @@ class NotificationSettingsPresenterTest {
}
}
@Test
fun `present - showAllActivityInRoomList reflects the feature flag value`() = runTest {
val featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.ShowAllActivityInRoomList.key to true)
)
val presenter = createNotificationSettingsPresenter(featureFlagService = featureFlagService)
presenter.test {
val state = consumeItemsUntilPredicate { it.showAllActivityInRoomList }.last()
assertThat(state.showAllActivityInRoomList).isTrue()
featureFlagService.setFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList, false)
val updatedState = consumeItemsUntilPredicate { !it.showAllActivityInRoomList }.last()
assertThat(updatedState.showAllActivityInRoomList).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - ToggleShowAllActivityInRoomList toggles the feature flag`() = runTest {
val featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.ShowAllActivityInRoomList.key to false)
)
val presenter = createNotificationSettingsPresenter(featureFlagService = featureFlagService)
presenter.test {
val initialState = awaitItem()
assertThat(initialState.showAllActivityInRoomList).isFalse()
initialState.eventSink(NotificationSettingsEvent.ToggleShowAllActivityInRoomList)
val enabledState = consumeItemsUntilPredicate { it.showAllActivityInRoomList }.last()
assertThat(enabledState.showAllActivityInRoomList).isTrue()
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList)).isTrue()
enabledState.eventSink(NotificationSettingsEvent.ToggleShowAllActivityInRoomList)
val disabledState = consumeItemsUntilPredicate { !it.showAllActivityInRoomList }.last()
assertThat(disabledState.showAllActivityInRoomList).isFalse()
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList)).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
private fun TestScope.createNotificationSettingsPresenter(
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
pushService: PushService = FakePushService(),
@@ -1116,6 +1159,7 @@ class NotificationSettingsPresenterTest {
NotificationSoundCopier.CopyResult.Success(source, source)
}),
soundDisplayNameResolver: FakeSoundDisplayNameResolver = FakeSoundDisplayNameResolver(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
): NotificationSettingsPresenter {
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
return NotificationSettingsPresenter(
@@ -1131,6 +1175,7 @@ class NotificationSettingsPresenterTest {
soundDisplayNameResolver = soundDisplayNameResolver,
stringProvider = FakeStringProvider(),
sessionCoroutineScope = backgroundScope,
featureFlagService = featureFlagService,
)
}
}
@@ -183,6 +183,35 @@ class NotificationSettingsViewTest : RobolectricTest() {
)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on disable show all activity emits the expected events`() {
testShowAllActivityToggle(true)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on enable show all activity emits the expected events`() {
testShowAllActivityToggle(false)
}
private fun testShowAllActivityToggle(initialState: Boolean) = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvent>()
setNotificationSettingsView(
state = aValidNotificationSettingsState(
showAllActivityInRoomList = initialState,
eventSink = eventsRecorder
),
)
clickOn(R.string.screen_notification_settings_show_all_activity_title)
eventsRecorder.assertList(
listOf(
NotificationSettingsEvent.RefreshSystemNotificationsEnabled,
NotificationSettingsEvent.ToggleShowAllActivityInRoomList,
)
)
}
@Config(qualifiers = "h1024dp")
@Test
fun `with an error configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest {