mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:55:41 +00:00
Add unread count to the room unread indicator (#6887)
* Add unread indicator count * Address review
This commit is contained in:
+1
@@ -279,6 +279,7 @@ private fun RoomsViewList(
|
|||||||
hideInviteAvatars = hideInvitesAvatars,
|
hideInviteAvatars = hideInvitesAvatars,
|
||||||
isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE &&
|
isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE &&
|
||||||
state.seenRoomInvites.contains(room.roomId),
|
state.seenRoomInvites.contains(room.roomId),
|
||||||
|
showUnreadCount = state.showUnreadCount,
|
||||||
onClick = onRoomClick,
|
onClick = onRoomClick,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|||||||
+13
-1
@@ -75,6 +75,7 @@ internal fun RoomSummaryRow(
|
|||||||
room: RoomListRoomSummary,
|
room: RoomListRoomSummary,
|
||||||
hideInviteAvatars: Boolean,
|
hideInviteAvatars: Boolean,
|
||||||
isInviteSeen: Boolean,
|
isInviteSeen: Boolean,
|
||||||
|
showUnreadCount: Boolean = false,
|
||||||
onClick: (RoomListRoomSummary) -> Unit,
|
onClick: (RoomListRoomSummary) -> Unit,
|
||||||
eventSink: (RoomListEvent) -> Unit,
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -127,7 +128,7 @@ internal fun RoomSummaryRow(
|
|||||||
timestamp = room.timestamp,
|
timestamp = room.timestamp,
|
||||||
isHighlighted = room.isHighlighted
|
isHighlighted = room.isHighlighted
|
||||||
)
|
)
|
||||||
MessagePreviewAndIndicatorRow(room = room)
|
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RoomSummaryDisplayType.KNOCKED -> {
|
RoomSummaryDisplayType.KNOCKED -> {
|
||||||
@@ -273,6 +274,7 @@ private fun InviteSubtitle(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun MessagePreviewAndIndicatorRow(
|
private fun MessagePreviewAndIndicatorRow(
|
||||||
room: RoomListRoomSummary,
|
room: RoomListRoomSummary,
|
||||||
|
showUnreadCount: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -360,8 +362,18 @@ private fun MessagePreviewAndIndicatorRow(
|
|||||||
}
|
}
|
||||||
if (room.hasNewContent) {
|
if (room.hasNewContent) {
|
||||||
val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages)
|
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(
|
UnreadIndicatorAtom(
|
||||||
color = tint,
|
color = tint,
|
||||||
|
count = count,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -36,6 +36,7 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
|
|||||||
internal fun aRoomsContentState(
|
internal fun aRoomsContentState(
|
||||||
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
||||||
showNewNotificationSoundBanner: Boolean = false,
|
showNewNotificationSoundBanner: Boolean = false,
|
||||||
|
showUnreadCount: Boolean = false,
|
||||||
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
||||||
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
|
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
|
||||||
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
|
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
|
||||||
@@ -43,6 +44,7 @@ internal fun aRoomsContentState(
|
|||||||
) = RoomListContentState.Rooms(
|
) = RoomListContentState.Rooms(
|
||||||
securityBannerState = securityBannerState,
|
securityBannerState = securityBannerState,
|
||||||
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
||||||
|
showUnreadCount = showUnreadCount,
|
||||||
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
|
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
|
||||||
batteryOptimizationState = batteryOptimizationState,
|
batteryOptimizationState = batteryOptimizationState,
|
||||||
summaries = summaries,
|
summaries = summaries,
|
||||||
|
|||||||
+11
-2
@@ -44,6 +44,8 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
|||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.fullscreenintent.api.FullScreenIntentPermissionsState
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
@@ -90,6 +92,7 @@ class RoomListPresenter(
|
|||||||
private val announcementService: AnnouncementService,
|
private val announcementService: AnnouncementService,
|
||||||
private val coldStartWatcher: AnalyticsColdStartWatcher,
|
private val coldStartWatcher: AnalyticsColdStartWatcher,
|
||||||
private val spaceFiltersPresenter: Presenter<SpaceFiltersState>,
|
private val spaceFiltersPresenter: Presenter<SpaceFiltersState>,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
) : Presenter<RoomListState> {
|
) : Presenter<RoomListState> {
|
||||||
private val encryptionService = client.encryptionService
|
private val encryptionService = client.encryptionService
|
||||||
|
|
||||||
@@ -165,13 +168,17 @@ class RoomListPresenter(
|
|||||||
roomListDataSource.updateFilter(allFilters)
|
roomListDataSource.updateFilter(allFilters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val canReportRoom by produceState(false) { value = client.canReportRoom() }
|
||||||
|
val showUnreadCount by produceState(false) {
|
||||||
|
value = featureFlagService.isFeatureEnabled(FeatureFlags.UnreadIndicatorCount)
|
||||||
|
}
|
||||||
|
|
||||||
val contentState = roomListContentState(
|
val contentState = roomListContentState(
|
||||||
securityBannerDismissed,
|
securityBannerDismissed,
|
||||||
showNewNotificationSoundBanner,
|
showNewNotificationSoundBanner,
|
||||||
|
showUnreadCount,
|
||||||
)
|
)
|
||||||
|
|
||||||
val canReportRoom by produceState(false) { value = client.canReportRoom() }
|
|
||||||
|
|
||||||
return RoomListState(
|
return RoomListState(
|
||||||
contextMenu = contextMenu.value,
|
contextMenu = contextMenu.value,
|
||||||
declineInviteMenu = declineInviteMenu.value,
|
declineInviteMenu = declineInviteMenu.value,
|
||||||
@@ -226,6 +233,7 @@ class RoomListPresenter(
|
|||||||
private fun roomListContentState(
|
private fun roomListContentState(
|
||||||
securityBannerDismissed: Boolean,
|
securityBannerDismissed: Boolean,
|
||||||
showNewNotificationSoundBanner: Boolean,
|
showNewNotificationSoundBanner: Boolean,
|
||||||
|
showUnreadCount: Boolean,
|
||||||
): RoomListContentState {
|
): RoomListContentState {
|
||||||
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
||||||
roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) }
|
roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) }
|
||||||
@@ -254,6 +262,7 @@ class RoomListPresenter(
|
|||||||
RoomListContentState.Rooms(
|
RoomListContentState.Rooms(
|
||||||
securityBannerState = securityBannerState,
|
securityBannerState = securityBannerState,
|
||||||
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
||||||
|
showUnreadCount = showUnreadCount,
|
||||||
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
|
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
|
||||||
batteryOptimizationState = batteryOptimizationPresenter.present(),
|
batteryOptimizationState = batteryOptimizationPresenter.present(),
|
||||||
summaries = roomSummaries.dataOrNull().orEmpty().toImmutableList(),
|
summaries = roomSummaries.dataOrNull().orEmpty().toImmutableList(),
|
||||||
|
|||||||
+1
@@ -72,6 +72,7 @@ sealed interface RoomListContentState {
|
|||||||
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
|
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
|
||||||
val batteryOptimizationState: BatteryOptimizationState,
|
val batteryOptimizationState: BatteryOptimizationState,
|
||||||
val showNewNotificationSoundBanner: Boolean,
|
val showNewNotificationSoundBanner: Boolean,
|
||||||
|
val showUnreadCount: Boolean,
|
||||||
val summaries: ImmutableList<RoomListRoomSummary>,
|
val summaries: ImmutableList<RoomListRoomSummary>,
|
||||||
val seenRoomInvites: ImmutableSet<RoomId>,
|
val seenRoomInvites: ImmutableSet<RoomId>,
|
||||||
) : RoomListContentState
|
) : RoomListContentState
|
||||||
|
|||||||
+4
@@ -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.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
||||||
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
|
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.fullscreenintent.api.aFullScreenIntentPermissionsState
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
@@ -669,6 +671,7 @@ class RoomListPresenterTest {
|
|||||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||||
announcementService: AnnouncementService = FakeAnnouncementService(),
|
announcementService: AnnouncementService = FakeAnnouncementService(),
|
||||||
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
) = RoomListPresenter(
|
) = RoomListPresenter(
|
||||||
client = client,
|
client = client,
|
||||||
leaveRoomPresenter = { leaveRoomState },
|
leaveRoomPresenter = { leaveRoomState },
|
||||||
@@ -697,5 +700,6 @@ class RoomListPresenterTest {
|
|||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
announcementService = announcementService,
|
announcementService = announcementService,
|
||||||
coldStartWatcher = FakeAnalyticsColdStartWatcher(),
|
coldStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||||
|
featureFlagService = featureFlagService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.unit.dp
|
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].
|
* @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.
|
* If the number is less than 1, the counter will not be displayed.
|
||||||
* @param modifier The modifier to apply to this layout.
|
* @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 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.
|
* @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
|
@Composable
|
||||||
fun CounterAtom(
|
fun CounterAtom(
|
||||||
count: Int,
|
count: Int,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
containerColor: Color? = null,
|
||||||
|
contentColor: Color? = null,
|
||||||
textStyle: TextStyle = CounterAtomDefaults.textStyle,
|
textStyle: TextStyle = CounterAtomDefaults.textStyle,
|
||||||
isCritical: Boolean = false,
|
isCritical: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -65,7 +71,7 @@ fun CounterAtom(
|
|||||||
.size(squareSize.toDp() + 1.dp)
|
.size(squareSize.toDp() + 1.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
if (isCritical) {
|
containerColor ?: if (isCritical) {
|
||||||
ElementTheme.colors.iconCriticalPrimary
|
ElementTheme.colors.iconCriticalPrimary
|
||||||
} else {
|
} else {
|
||||||
ElementTheme.colors.iconAccentPrimary
|
ElementTheme.colors.iconAccentPrimary
|
||||||
@@ -76,7 +82,7 @@ fun CounterAtom(
|
|||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
text = countAsText,
|
text = countAsText,
|
||||||
style = textStyle,
|
style = textStyle,
|
||||||
color = ElementTheme.colors.textOnSolidPrimary,
|
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-2
@@ -10,6 +10,7 @@ package io.element.android.libraries.designsystem.atomic.atoms
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -29,20 +30,33 @@ import io.element.android.libraries.designsystem.theme.unreadIndicator
|
|||||||
fun UnreadIndicatorAtom(
|
fun UnreadIndicatorAtom(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
size: Dp = 12.dp,
|
size: Dp = 12.dp,
|
||||||
|
count: Long? = null,
|
||||||
color: Color = ElementTheme.colors.unreadIndicator,
|
color: Color = ElementTheme.colors.unreadIndicator,
|
||||||
isVisible: Boolean = true,
|
isVisible: Boolean = true,
|
||||||
contentDescription: String? = null,
|
contentDescription: String? = null,
|
||||||
) {
|
) {
|
||||||
Box(
|
when {
|
||||||
|
!isVisible -> Spacer(modifier = modifier.size(size))
|
||||||
|
count != null && count >= 1 -> CounterAtom(
|
||||||
|
count = count.toInt(),
|
||||||
|
modifier = modifier.semantics {
|
||||||
|
contentDescription?.let { this.contentDescription = it }
|
||||||
|
},
|
||||||
|
containerColor = color,
|
||||||
|
contentColor = ElementTheme.colors.bgCanvasDefault,
|
||||||
|
textStyle = ElementTheme.typography.fontBodySmMedium,
|
||||||
|
)
|
||||||
|
else -> Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.semantics {
|
.semantics {
|
||||||
contentDescription?.let { this.contentDescription = it }
|
contentDescription?.let { this.contentDescription = it }
|
||||||
}
|
}
|
||||||
.size(size)
|
.size(size)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(if (isVisible) color else Color.Transparent)
|
.background(color),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+7
@@ -130,4 +130,11 @@ enum class FeatureFlags(
|
|||||||
defaultValue = { false },
|
defaultValue = { false },
|
||||||
isFinished = 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,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user