diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index dee175a7b1..266b6f5a59 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -279,6 +279,7 @@ private fun RoomsViewList( hideInviteAvatars = hideInvitesAvatars, isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE && state.seenRoomInvites.contains(room.roomId), + showUnreadCount = state.showUnreadCount, onClick = onRoomClick, eventSink = eventSink, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index 72ecb5046d..934f60aa29 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -75,6 +75,7 @@ internal fun RoomSummaryRow( room: RoomListRoomSummary, hideInviteAvatars: Boolean, isInviteSeen: Boolean, + showUnreadCount: Boolean = false, onClick: (RoomListRoomSummary) -> Unit, eventSink: (RoomListEvent) -> Unit, modifier: Modifier = Modifier, @@ -127,7 +128,7 @@ internal fun RoomSummaryRow( timestamp = room.timestamp, isHighlighted = room.isHighlighted ) - MessagePreviewAndIndicatorRow(room = room) + MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount) } } RoomSummaryDisplayType.KNOCKED -> { @@ -273,6 +274,7 @@ private fun InviteSubtitle( @Composable private fun MessagePreviewAndIndicatorRow( room: RoomListRoomSummary, + showUnreadCount: Boolean, modifier: Modifier = Modifier, ) { Row( @@ -360,8 +362,18 @@ private fun MessagePreviewAndIndicatorRow( } if (room.hasNewContent) { val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages) + val count = if (showUnreadCount) { + if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) { + room.numberOfUnreadMessages + } else { + room.numberOfUnreadNotifications + } + } else { + null + } UnreadIndicatorAtom( color = tint, + count = count, contentDescription = contentDescription, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt index c79e79be73..0ac2b7bf62 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt @@ -36,6 +36,7 @@ open class RoomListContentStateProvider : PreviewParameterProvider = aRoomListRoomSummaryList(), fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(), @@ -43,6 +44,7 @@ internal fun aRoomsContentState( ) = RoomListContentState.Rooms( securityBannerState = securityBannerState, showNewNotificationSoundBanner = showNewNotificationSoundBanner, + showUnreadCount = showUnreadCount, fullScreenIntentPermissionsState = fullScreenIntentPermissionsState, batteryOptimizationState = batteryOptimizationState, summaries = summaries, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index 2010555cd7..a741da14e9 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -44,6 +44,8 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +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.core.RoomId @@ -90,6 +92,7 @@ class RoomListPresenter( private val announcementService: AnnouncementService, private val coldStartWatcher: AnalyticsColdStartWatcher, private val spaceFiltersPresenter: Presenter, + private val featureFlagService: FeatureFlagService, ) : Presenter { private val encryptionService = client.encryptionService @@ -165,13 +168,17 @@ class RoomListPresenter( roomListDataSource.updateFilter(allFilters) } + val canReportRoom by produceState(false) { value = client.canReportRoom() } + val showUnreadCount by produceState(false) { + value = featureFlagService.isFeatureEnabled(FeatureFlags.UnreadIndicatorCount) + } + val contentState = roomListContentState( securityBannerDismissed, showNewNotificationSoundBanner, + showUnreadCount, ) - val canReportRoom by produceState(false) { value = client.canReportRoom() } - return RoomListState( contextMenu = contextMenu.value, declineInviteMenu = declineInviteMenu.value, @@ -226,6 +233,7 @@ class RoomListPresenter( private fun roomListContentState( securityBannerDismissed: Boolean, showNewNotificationSoundBanner: Boolean, + showUnreadCount: Boolean, ): RoomListContentState { val roomSummaries by produceState(initialValue = AsyncData.Loading()) { roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) } @@ -254,6 +262,7 @@ class RoomListPresenter( RoomListContentState.Rooms( securityBannerState = securityBannerState, showNewNotificationSoundBanner = showNewNotificationSoundBanner, + showUnreadCount = showUnreadCount, fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(), batteryOptimizationState = batteryOptimizationPresenter.present(), summaries = roomSummaries.dataOrNull().orEmpty().toImmutableList(), diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt index e0f4943621..c03bd56664 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt @@ -72,6 +72,7 @@ sealed interface RoomListContentState { val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState, val batteryOptimizationState: BatteryOptimizationState, val showNewNotificationSoundBanner: Boolean, + val showUnreadCount: Boolean, val summaries: ImmutableList, val seenRoomInvites: ImmutableSet, ) : RoomListContentState diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt index 5c2c3e294b..2f3f1c87a5 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt @@ -36,6 +36,8 @@ import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -669,6 +671,7 @@ class RoomListPresenterTest { appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), announcementService: AnnouncementService = FakeAnnouncementService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), ) = RoomListPresenter( client = client, leaveRoomPresenter = { leaveRoomState }, @@ -697,5 +700,6 @@ class RoomListPresenterTest { seenInvitesStore = seenInvitesStore, announcementService = announcementService, coldStartWatcher = FakeAnalyticsColdStartWatcher(), + featureFlagService = featureFlagService, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt index a051fd4c80..3bbd3c12a7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.dp @@ -37,13 +38,18 @@ private const val MAX_COUNT_STRING = "+$MAX_COUNT" * @param count The number to display. If the number is greater than [MAX_COUNT], the counter will display [MAX_COUNT_STRING]. * If the number is less than 1, the counter will not be displayed. * @param modifier The modifier to apply to this layout. + * @param containerColor The background color of the counter. When null, uses [isCritical] to pick a default. + * @param contentColor The text color inside the counter. When null, uses [textOnSolidPrimary]. * @param textStyle The style to apply to the text inside the counter. * @param isCritical If true, the counter will use a critical color scheme, otherwise it will use an accent color scheme. + * Only used when [containerColor] is null. */ @Composable fun CounterAtom( count: Int, modifier: Modifier = Modifier, + containerColor: Color? = null, + contentColor: Color? = null, textStyle: TextStyle = CounterAtomDefaults.textStyle, isCritical: Boolean = false, ) { @@ -65,7 +71,7 @@ fun CounterAtom( .size(squareSize.toDp() + 1.dp) .clip(CircleShape) .background( - if (isCritical) { + containerColor ?: if (isCritical) { ElementTheme.colors.iconCriticalPrimary } else { ElementTheme.colors.iconAccentPrimary @@ -76,7 +82,7 @@ fun CounterAtom( modifier = Modifier.align(Alignment.Center), text = countAsText, style = textStyle, - color = ElementTheme.colors.textOnSolidPrimary, + color = contentColor ?: ElementTheme.colors.textOnSolidPrimary, ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt index d2db3aec8e..ff49bab06f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.designsystem.atomic.atoms import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable @@ -29,19 +30,32 @@ import io.element.android.libraries.designsystem.theme.unreadIndicator fun UnreadIndicatorAtom( modifier: Modifier = Modifier, size: Dp = 12.dp, + count: Long? = null, color: Color = ElementTheme.colors.unreadIndicator, isVisible: Boolean = true, contentDescription: String? = null, ) { - Box( - modifier = modifier - .semantics { + when { + !isVisible -> Spacer(modifier = modifier.size(size)) + count != null && count >= 1 -> CounterAtom( + count = count.toInt(), + modifier = modifier.semantics { contentDescription?.let { this.contentDescription = it } - } - .size(size) - .clip(CircleShape) - .background(if (isVisible) color else Color.Transparent) - ) + }, + containerColor = color, + contentColor = ElementTheme.colors.bgCanvasDefault, + textStyle = ElementTheme.typography.fontBodySmMedium, + ) + else -> Box( + modifier = modifier + .semantics { + contentDescription?.let { this.contentDescription = it } + } + .size(size) + .clip(CircleShape) + .background(color), + ) + } } @PreviewsDayNight diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index ec6aaba3d3..0127e41c99 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -130,4 +130,11 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + UnreadIndicatorCount( + key = "feature.unread_indicator_count", + title = "Unread indicator count", + description = "Show the number of unread messages on the unread indicator in the room list.", + defaultValue = { false }, + isFinished = false, + ), }