mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
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:
committed by
GitHub
parent
a3ff7e1486
commit
2dd0a47eb4
+1
@@ -280,6 +280,7 @@ private fun RoomsViewList(
|
||||
isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE &&
|
||||
state.seenRoomInvites.contains(room.roomId),
|
||||
showUnreadCount = state.showUnreadCount,
|
||||
showAllActivity = state.showAllActivity,
|
||||
onClick = onRoomClick,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
+35
-14
@@ -80,6 +80,7 @@ internal fun RoomSummaryRow(
|
||||
onClick: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
showUnreadCount: Boolean = false,
|
||||
showAllActivity: Boolean = false,
|
||||
eventSink: (RoomListEvent) -> Unit,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
@@ -131,7 +132,7 @@ internal fun RoomSummaryRow(
|
||||
isHighlighted = room.isHighlighted,
|
||||
dmUserStatus = room.dmUserStatus,
|
||||
)
|
||||
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
|
||||
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount, showAllActivity = showAllActivity)
|
||||
}
|
||||
}
|
||||
RoomSummaryDisplayType.KNOCKED -> {
|
||||
@@ -278,6 +279,7 @@ private fun InviteSubtitle(
|
||||
private fun MessagePreviewAndIndicatorRow(
|
||||
room: RoomListRoomSummary,
|
||||
showUnreadCount: Boolean,
|
||||
showAllActivity: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
@@ -348,7 +350,7 @@ private fun MessagePreviewAndIndicatorRow(
|
||||
.height(16.dp)
|
||||
// Used to force this line to be read aloud earlier than the latest event when using Talkback
|
||||
.zIndex(-1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val tint = if (room.isHighlighted) ElementTheme.colors.unreadIndicator else ElementTheme.colors.iconQuaternary
|
||||
@@ -358,25 +360,17 @@ private fun MessagePreviewAndIndicatorRow(
|
||||
isAudio = room.activeCallIntent == CallIntent.AUDIO
|
||||
)
|
||||
}
|
||||
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
|
||||
val isMuted = room.userDefinedNotificationMode == RoomNotificationMode.MUTE
|
||||
if (isMuted) {
|
||||
NotificationOffIndicatorAtom()
|
||||
} else if (room.numberOfUnreadMentions > 0) {
|
||||
MentionIndicatorAtom()
|
||||
}
|
||||
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,
|
||||
count = room.unreadIndicatorCount(showUnreadCount = showUnreadCount, showAllActivity = showAllActivity),
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
@@ -384,6 +378,30 @@ private fun MessagePreviewAndIndicatorRow(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the value to pass to [UnreadIndicatorAtom] as `count`.
|
||||
* - `null`: nothing is displayed.
|
||||
* - `0`: dot indicator without a count.
|
||||
* - `> 0`: the number to display.
|
||||
*/
|
||||
internal fun RoomListRoomSummary.unreadIndicatorCount(
|
||||
showUnreadCount: Boolean,
|
||||
showAllActivity: Boolean,
|
||||
): Long? {
|
||||
val isMuted = userDefinedNotificationMode == RoomNotificationMode.MUTE
|
||||
return when {
|
||||
// Show only unread notifications when the room is not muted and there are unread notifications
|
||||
!isMuted && numberOfUnreadNotifications > 0 -> numberOfUnreadNotifications
|
||||
// Otherwise, show unread messages when showAllActivity is true
|
||||
showAllActivity -> numberOfUnreadMessages.takeIf { it > 0 }
|
||||
// Or don't display anything
|
||||
else -> null
|
||||
}?.let {
|
||||
// We only want to show the count if the option is enabled, otherwise we just show the dot indicator (value 0)
|
||||
if (showUnreadCount) it else 0
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InviteNameAndIndicatorRow(
|
||||
name: String?,
|
||||
@@ -408,7 +426,8 @@ private fun InviteNameAndIndicatorRow(
|
||||
)
|
||||
if (!isInviteSeen) {
|
||||
UnreadIndicatorAtom(
|
||||
color = ElementTheme.colors.unreadIndicator
|
||||
color = ElementTheme.colors.unreadIndicator,
|
||||
count = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -457,5 +476,7 @@ internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryPreviewP
|
||||
isInviteSeen = data.name == "Bob",
|
||||
onClick = {},
|
||||
eventSink = {},
|
||||
showUnreadCount = data.numberOfUnreadNotifications > 0,
|
||||
showAllActivity = true,
|
||||
)
|
||||
}
|
||||
|
||||
+18
@@ -82,6 +82,24 @@ open class RoomListRoomSummaryPreviewParam : PreviewParameterProvider<RoomListRo
|
||||
numberOfUnreadMentions = 1,
|
||||
hasRoomCall = hasCall,
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
name = roomNotificationMode.name,
|
||||
latestEvent = LatestEvent.Synced("New notifications" + if (hasCall) ", call" else ""),
|
||||
notificationMode = roomNotificationMode,
|
||||
numberOfUnreadMessages = 0,
|
||||
numberOfUnreadMentions = 0,
|
||||
numberOfUnreadNotifications = 1,
|
||||
hasRoomCall = hasCall,
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
name = roomNotificationMode.name,
|
||||
latestEvent = LatestEvent.Synced("New notifications" + if (hasCall) ", call" else ""),
|
||||
notificationMode = roomNotificationMode,
|
||||
numberOfUnreadMessages = 0,
|
||||
numberOfUnreadMentions = 0,
|
||||
numberOfUnreadNotifications = 100,
|
||||
hasRoomCall = hasCall,
|
||||
),
|
||||
)
|
||||
}.flatten()
|
||||
}.flatten(),
|
||||
|
||||
+2
@@ -40,6 +40,7 @@ internal fun aRoomsContentState(
|
||||
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
||||
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
|
||||
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
|
||||
showAllActivity: Boolean = false,
|
||||
seenRoomInvites: Set<RoomId> = emptySet(),
|
||||
) = RoomListContentState.Rooms(
|
||||
securityBannerState = securityBannerState,
|
||||
@@ -48,6 +49,7 @@ internal fun aRoomsContentState(
|
||||
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
|
||||
batteryOptimizationState = batteryOptimizationState,
|
||||
summaries = summaries,
|
||||
showAllActivity = showAllActivity,
|
||||
seenRoomInvites = seenRoomInvites.toImmutableSet(),
|
||||
)
|
||||
|
||||
|
||||
+8
-1
@@ -118,6 +118,10 @@ class RoomListPresenter(
|
||||
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
||||
val declineInviteMenu = remember { mutableStateOf<RoomListState.DeclineInviteMenu>(RoomListState.DeclineInviteMenu.Hidden) }
|
||||
|
||||
val showAllActivity by produceState(initialValue = false) {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.ShowAllActivityInRoomList).collect { value = it }
|
||||
}
|
||||
|
||||
fun handleEvent(event: RoomListEvent) {
|
||||
when (event) {
|
||||
is RoomListEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||
@@ -165,13 +169,14 @@ class RoomListPresenter(
|
||||
|
||||
val canReportRoom by produceState(false) { value = client.canReportRoom() }
|
||||
val showUnreadCount by produceState(false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.UnreadIndicatorCount)
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.UnreadIndicatorCount).collect { value = it }
|
||||
}
|
||||
|
||||
val contentState = roomListContentState(
|
||||
securityBannerDismissed,
|
||||
showNewNotificationSoundBanner,
|
||||
showUnreadCount,
|
||||
showAllActivity,
|
||||
)
|
||||
|
||||
return RoomListState(
|
||||
@@ -230,6 +235,7 @@ class RoomListPresenter(
|
||||
securityBannerDismissed: Boolean,
|
||||
showNewNotificationSoundBanner: Boolean,
|
||||
showUnreadCount: Boolean,
|
||||
showAllActivity: Boolean,
|
||||
): RoomListContentState {
|
||||
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
||||
roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) }
|
||||
@@ -262,6 +268,7 @@ class RoomListPresenter(
|
||||
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
|
||||
batteryOptimizationState = batteryOptimizationPresenter.present(),
|
||||
summaries = roomSummaries.dataOrNull().orEmpty().toImmutableList(),
|
||||
showAllActivity = showAllActivity,
|
||||
seenRoomInvites = seenRoomInvites.toImmutableSet(),
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -76,5 +76,6 @@ sealed interface RoomListContentState {
|
||||
val showUnreadCount: Boolean,
|
||||
val summaries: ImmutableList<RoomListRoomSummary>,
|
||||
val seenRoomInvites: ImmutableSet<RoomId>,
|
||||
val showAllActivity: Boolean,
|
||||
) : RoomListContentState
|
||||
}
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.home.impl.components
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.home.impl.model.createRoomListRoomSummary
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import org.junit.Test
|
||||
|
||||
class RoomSummaryRowUnreadIndicatorCountTest {
|
||||
@Test
|
||||
fun `unread notifications are used when the room is not muted`() {
|
||||
val room = createRoomListRoomSummary(
|
||||
numberOfUnreadNotifications = 3,
|
||||
numberOfUnreadMessages = 5,
|
||||
)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = false)).isEqualTo(3L)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(3L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unread notifications only show a dot when showUnreadCount is false`() {
|
||||
val room = createRoomListRoomSummary(
|
||||
numberOfUnreadNotifications = 3,
|
||||
numberOfUnreadMessages = 5,
|
||||
)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = false)).isEqualTo(0L)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = true)).isEqualTo(0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unread messages are used when there are no unread notifications and showAllActivity is true`() {
|
||||
val room = createRoomListRoomSummary(
|
||||
numberOfUnreadNotifications = 0,
|
||||
numberOfUnreadMessages = 5,
|
||||
)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(5L)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = true)).isEqualTo(0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unread messages are ignored when showAllActivity is false`() {
|
||||
val room = createRoomListRoomSummary(
|
||||
numberOfUnreadNotifications = 0,
|
||||
numberOfUnreadMessages = 5,
|
||||
)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = false)).isNull()
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = false)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `muted room ignores unread notifications and falls back to unread messages when showAllActivity is true`() {
|
||||
val room = createRoomListRoomSummary(
|
||||
numberOfUnreadNotifications = 3,
|
||||
numberOfUnreadMessages = 5,
|
||||
userDefinedNotificationMode = RoomNotificationMode.MUTE,
|
||||
)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(5L)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = true)).isEqualTo(0L)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = false)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no count when there are no unread notifications nor unread messages`() {
|
||||
val room = createRoomListRoomSummary(
|
||||
numberOfUnreadNotifications = 0,
|
||||
numberOfUnreadMessages = 0,
|
||||
isMarkedUnread = true,
|
||||
)
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isNull()
|
||||
assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = false)).isNull()
|
||||
}
|
||||
}
|
||||
+50
-4
@@ -39,6 +39,7 @@ 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.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
@@ -218,7 +219,7 @@ class RoomListPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
// Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
|
||||
// Skip intermediate event for loading the value of the feature flags
|
||||
skipItems(1)
|
||||
|
||||
val summary = createRoomListRoomSummary()
|
||||
@@ -265,7 +266,7 @@ class RoomListPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
// Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
|
||||
// Skip intermediate event for loading the value of the feature flags
|
||||
skipItems(1)
|
||||
|
||||
val summary = createRoomListRoomSummary()
|
||||
@@ -318,7 +319,7 @@ class RoomListPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
// Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
|
||||
// Skip intermediate event for loading the value of the feature flags
|
||||
skipItems(1)
|
||||
|
||||
eventRecorder.assertEmpty()
|
||||
@@ -629,7 +630,7 @@ class RoomListPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(announcementService.announcementsToShowFlow().first()).isEmpty()
|
||||
skipItems(2) // Skip initial state and intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
|
||||
skipItems(2) // Skip initial state and intermediate event for loading the value of the feature flags
|
||||
val state = awaitItem()
|
||||
assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse()
|
||||
announcementService.emitAnnouncementsToShow(listOf(Announcement.NewNotificationSound))
|
||||
@@ -643,6 +644,51 @@ class RoomListPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - showAllActivity and showUnreadCount reflect the feature flag values`() = runTest {
|
||||
val roomList = FakeDynamicRoomList(
|
||||
summaries = MutableStateFlow(listOf(aRoomSummary())),
|
||||
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1)),
|
||||
)
|
||||
val matrixClient = FakeMatrixClient(
|
||||
roomListService = FakeRoomListService(createRoomListLambda = { roomList }),
|
||||
)
|
||||
val featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
FeatureFlags.ShowAllActivityInRoomList.key to true,
|
||||
FeatureFlags.UnreadIndicatorCount.key to true,
|
||||
)
|
||||
)
|
||||
val presenter = createRoomListPresenter(
|
||||
client = matrixClient,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
presenter.test {
|
||||
val enabledState = consumeItemsUntilPredicate { state ->
|
||||
state.contentState is RoomListContentState.Rooms &&
|
||||
state.contentAsRooms().showAllActivity &&
|
||||
state.contentAsRooms().showUnreadCount
|
||||
}.last()
|
||||
assertThat(enabledState.contentAsRooms().showAllActivity).isTrue()
|
||||
assertThat(enabledState.contentAsRooms().showUnreadCount).isTrue()
|
||||
|
||||
featureFlagService.setFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList, false)
|
||||
val allActivityDisabledState = consumeItemsUntilPredicate { state ->
|
||||
state.contentState is RoomListContentState.Rooms && !state.contentAsRooms().showAllActivity
|
||||
}.last()
|
||||
assertThat(allActivityDisabledState.contentAsRooms().showAllActivity).isFalse()
|
||||
assertThat(allActivityDisabledState.contentAsRooms().showUnreadCount).isTrue()
|
||||
|
||||
featureFlagService.setFeatureEnabled(FeatureFlags.UnreadIndicatorCount, false)
|
||||
val unreadCountDisabledState = consumeItemsUntilPredicate { state ->
|
||||
state.contentState is RoomListContentState.Rooms && !state.contentAsRooms().showUnreadCount
|
||||
}.last()
|
||||
assertThat(unreadCountDisabledState.contentAsRooms().showAllActivity).isFalse()
|
||||
assertThat(unreadCountDisabledState.contentAsRooms().showUnreadCount).isFalse()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTestMarkRoomAsRead(
|
||||
client: MatrixClient,
|
||||
notificationCleaner: NotificationCleaner,
|
||||
|
||||
+1
@@ -267,6 +267,7 @@ private fun ThreadListItemRow(
|
||||
UnreadIndicatorAtom(
|
||||
size = 14.dp,
|
||||
isVisible = hasUnreadNotifications,
|
||||
count = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -580,6 +580,7 @@ private fun JumpToPositionButton(
|
||||
.offset { IntOffset(x = 0, y = dotYOffset.roundToPx()) },
|
||||
color = ElementTheme.colors.iconSuccessPrimary,
|
||||
border = BorderStroke(2.dp, ElementTheme.colors.bgCanvasDefault),
|
||||
count = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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
|
||||
}
|
||||
|
||||
+12
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+4
@@ -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 {
|
||||
|
||||
+3
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
+10
@@ -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>
|
||||
|
||||
+45
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -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 {
|
||||
|
||||
+50
-36
@@ -8,39 +8,43 @@
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.atoms
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceAtLeast
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.SemanticColors
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
private const val MAX_COUNT = 99
|
||||
private const val MAX_COUNT_STRING = "+$MAX_COUNT"
|
||||
private const val MAX_COUNT_STRING = "$MAX_COUNT+"
|
||||
|
||||
/**
|
||||
* A counter atom that displays a number in a circle.
|
||||
* Figma link : https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2805-2649&m=dev
|
||||
*
|
||||
* @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, just a dot will be displayed instead.
|
||||
* @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 contentColor The text color inside the counter. When null, uses [SemanticColors.textOnSolidPrimary].
|
||||
* @param contentPadding The padding to apply to the text inside the counter.
|
||||
* @param textStyle The style to apply to the text inside the counter.
|
||||
* @param dotSize The size of the dot when [count] is less than 1. It defaults to `12.0.dp`.
|
||||
* @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.
|
||||
*/
|
||||
@@ -50,41 +54,51 @@ fun CounterAtom(
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color? = null,
|
||||
contentColor: Color? = null,
|
||||
contentPadding: PaddingValues = PaddingValues.Zero,
|
||||
textStyle: TextStyle = CounterAtomDefaults.textStyle,
|
||||
dotSize: Dp = 12.0.dp,
|
||||
isCritical: Boolean = false,
|
||||
) {
|
||||
if (count < 1) return
|
||||
val countAsText = when (count) {
|
||||
in 0..MAX_COUNT -> count.toString()
|
||||
in Int.MIN_VALUE..0 -> null
|
||||
in 1..MAX_COUNT -> count.toString()
|
||||
else -> MAX_COUNT_STRING
|
||||
}
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
// Measure the maximum count string size
|
||||
val textLayoutResult = textMeasurer.measure(
|
||||
text = MAX_COUNT_STRING,
|
||||
style = textStyle
|
||||
)
|
||||
val textSize = textLayoutResult.size
|
||||
val squareSize = maxOf(textSize.width, textSize.height)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(squareSize.toDp() + 1.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
containerColor ?: if (isCritical) {
|
||||
ElementTheme.colors.iconCriticalPrimary
|
||||
} else {
|
||||
ElementTheme.colors.iconAccentPrimary
|
||||
}
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
|
||||
val minSingleDigitSize = if (countAsText?.length == 1) {
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
// Measure the maximum count string size
|
||||
val textLayoutResult = textMeasurer.measure(
|
||||
text = countAsText,
|
||||
style = textStyle,
|
||||
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
|
||||
style = textStyle
|
||||
)
|
||||
val textSize = textLayoutResult.size
|
||||
maxOf(textSize.width, textSize.height)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
androidx.compose.material3.Badge(
|
||||
modifier = modifier.then(
|
||||
if (countAsText == null) Modifier.sizeIn(minWidth = dotSize, minHeight = dotSize) else Modifier
|
||||
),
|
||||
containerColor = containerColor ?: if (isCritical) ElementTheme.colors.iconCriticalPrimary else ElementTheme.colors.iconAccentPrimary,
|
||||
contentColor = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
|
||||
content = countAsText?.let {
|
||||
@Composable {
|
||||
Text(
|
||||
modifier = Modifier.sizeIn(
|
||||
minWidth = (minSingleDigitSize.toDp() - 8.dp).coerceAtLeast(0.dp),
|
||||
minHeight = minSingleDigitSize.toDp()
|
||||
).padding(contentPadding),
|
||||
text = countAsText,
|
||||
style = textStyle,
|
||||
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
object CounterAtomDefaults {
|
||||
@@ -95,7 +109,7 @@ object CounterAtomDefaults {
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun CounterAtomPreview() = ElementPreview {
|
||||
Column(verticalArrangement = spacedBy(2.dp)) {
|
||||
Row(horizontalArrangement = spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
CounterAtom(count = 0)
|
||||
CounterAtom(count = 4)
|
||||
CounterAtom(count = 99)
|
||||
|
||||
+28
-26
@@ -9,15 +9,16 @@
|
||||
package io.element.android.libraries.designsystem.atomic.atoms
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
@@ -37,35 +38,36 @@ fun UnreadIndicatorAtom(
|
||||
isVisible: Boolean = true,
|
||||
contentDescription: String? = null,
|
||||
border: BorderStroke? = null,
|
||||
contentPadding: PaddingValues = PaddingValues.Zero,
|
||||
) {
|
||||
when {
|
||||
!isVisible -> Spacer(modifier = modifier.size(size))
|
||||
count != null && count >= 1 -> CounterAtom(
|
||||
count = count.toInt(),
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
contentDescription?.let { this.contentDescription = it }
|
||||
}
|
||||
.then(if (border != null) Modifier.border(border, CircleShape) else Modifier),
|
||||
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)
|
||||
.then(if (border != null) Modifier.border(border, CircleShape) else Modifier),
|
||||
)
|
||||
else -> count?.let {
|
||||
CounterAtom(
|
||||
count = count.toInt(),
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
contentDescription?.let { this.contentDescription = it }
|
||||
}
|
||||
.then(if (border != null) Modifier.border(border, RoundedCornerShape(percent = 50)) else Modifier),
|
||||
containerColor = color,
|
||||
contentColor = ElementTheme.colors.bgCanvasDefault,
|
||||
textStyle = ElementTheme.typography.fontBodySmMedium,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun UnreadIndicatorAtomPreview() = ElementPreview {
|
||||
UnreadIndicatorAtom()
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
UnreadIndicatorAtom(count = null)
|
||||
UnreadIndicatorAtom(count = 0)
|
||||
UnreadIndicatorAtom(count = 1)
|
||||
UnreadIndicatorAtom(count = 10)
|
||||
UnreadIndicatorAtom(count = 99)
|
||||
UnreadIndicatorAtom(count = 999)
|
||||
}
|
||||
}
|
||||
|
||||
+60
-27
@@ -8,16 +8,17 @@
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.FloatingToolbarColors
|
||||
import androidx.compose.material3.FloatingToolbarDefaults
|
||||
import androidx.compose.material3.FloatingToolbarHorizontalFabPosition
|
||||
@@ -27,7 +28,6 @@ import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
@@ -137,8 +137,20 @@ fun HorizontalFloatingToolbarItem(
|
||||
contentColor = ElementTheme.colors.iconSecondary,
|
||||
)
|
||||
}
|
||||
Box {
|
||||
FilledIconButton(
|
||||
BadgedBox(
|
||||
badge = {
|
||||
counter?.let {
|
||||
CounterAtom(
|
||||
count = it,
|
||||
// Tweak the offset so it's centered on the icon
|
||||
modifier = Modifier.offset(x = (-16).dp, y = 10.dp),
|
||||
contentPadding = PaddingValues(vertical = 0.5.dp),
|
||||
textStyle = ElementTheme.typography.fontBodySmMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.widthIn(min = 56.dp),
|
||||
colors = colors,
|
||||
onClick = onClick,
|
||||
@@ -149,15 +161,6 @@ fun HorizontalFloatingToolbarItem(
|
||||
contentDescription = tooltipLabel,
|
||||
)
|
||||
}
|
||||
if (counter != null) {
|
||||
CounterAtom(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = 6.dp, end = 3.dp),
|
||||
count = counter,
|
||||
textStyle = ElementTheme.typography.fontBodyXsMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,18 +173,47 @@ fun HorizontalFloatingToolbarSeparator(modifier: Modifier = Modifier) {
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun HorizontalFloatingToolbarPreview() = ElementPreview {
|
||||
ContentToPreview(
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Plus(),
|
||||
contentDescription = null,
|
||||
)
|
||||
Column {
|
||||
ContentToPreview(
|
||||
count = 1,
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Plus(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
ContentToPreview(
|
||||
count = 99,
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Plus(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
ContentToPreview(
|
||||
count = 999,
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Plus(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@@ -194,10 +226,11 @@ internal fun HorizontalFloatingToolbarNoFabPreview() = ElementPreview {
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(
|
||||
count: Int? = 6,
|
||||
floatingActionButton: (@Composable () -> Unit)?,
|
||||
) {
|
||||
HorizontalFloatingToolbar(
|
||||
modifier = Modifier.padding(28.dp),
|
||||
modifier = Modifier.padding(horizontal = 28.dp, vertical = 16.dp),
|
||||
floatingActionButton = floatingActionButton,
|
||||
) {
|
||||
listOf(
|
||||
@@ -211,7 +244,7 @@ private fun ContentToPreview(
|
||||
icon = icon,
|
||||
tooltipLabel = "Label",
|
||||
isSelected = index == 0,
|
||||
counter = if (index == 0) 6 else null,
|
||||
counter = if (index == 0) count else null,
|
||||
forceRenderingTooltip = true,
|
||||
onClick = { },
|
||||
)
|
||||
|
||||
+11
-10
@@ -8,12 +8,10 @@
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.CounterAtom
|
||||
|
||||
@@ -24,16 +22,19 @@ fun NavigationBarIcon(
|
||||
count: Int = 0,
|
||||
isCritical: Boolean = false,
|
||||
) {
|
||||
Box(modifier) {
|
||||
BadgedBox(
|
||||
modifier = modifier,
|
||||
badge = {
|
||||
CounterAtom(
|
||||
textStyle = ElementTheme.typography.fontBodyXsMedium,
|
||||
count = count,
|
||||
isCritical = isCritical,
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
)
|
||||
CounterAtom(
|
||||
modifier = Modifier.offset(11.dp, (-11).dp),
|
||||
textStyle = ElementTheme.typography.fontBodyXsMedium,
|
||||
count = count,
|
||||
isCritical = isCritical,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -169,4 +169,13 @@ enum class FeatureFlags(
|
||||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
ShowAllActivityInRoomList(
|
||||
key = "feature.show_all_activity_in_room_list",
|
||||
title = "Show all activity in room list",
|
||||
description = "Show all activity indicators in the room list, using unread messages instead of notifications.",
|
||||
defaultValue = { true },
|
||||
// To hide it from the developer options, since it's displayed in the notification settings instead
|
||||
isFinished = true,
|
||||
isInLabs = false,
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -169,7 +169,8 @@ private fun NameAndIndicatorRow(
|
||||
)
|
||||
if (showIndicator) {
|
||||
UnreadIndicatorAtom(
|
||||
color = ElementTheme.colors.unreadIndicator
|
||||
color = ElementTheme.colors.unreadIndicator,
|
||||
count = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_10_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_11_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_12_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_13_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_14_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_15_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_16_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_17_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_18_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_19_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_20_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_21_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_22_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_23_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_24_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_25_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_26_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_27_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_28_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_29_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_30_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_31_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_32_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_33_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_34_en.png
LFS
BIN
Binary file not shown.
tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_35_en.png
LFS
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user