Remove the green badge on a pending invite after a first preview (#4532)

* Remove condition on displayType as I believe, that it has no effect.

* Remove the green badge on a pending invite after a first preview

* Update screenshots

* Fix test

* Improve DefaultSeenInvitesStore, clear it on logout, and on clear cache. Also create a store per session.

* Remember the returned flow.

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Benoit Marty 2025-04-04 16:51:31 +02:00 committed by GitHub
parent 77a7c0b2e5
commit ef8eeb804e
26 changed files with 326 additions and 29 deletions

View file

@ -11,8 +11,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentSet
open class RoomListContentStateProvider : PreviewParameterProvider<RoomListContentState> {
override val values: Sequence<RoomListContentState>
@ -29,10 +31,12 @@ internal fun aRoomsContentState(
securityBannerState: SecurityBannerState = SecurityBannerState.None,
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
seenRoomInvites: Set<RoomId> = emptySet(),
) = RoomListContentState.Rooms(
securityBannerState = securityBannerState,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
summaries = summaries,
seenRoomInvites = seenRoomInvites.toPersistentSet(),
)
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)

View file

@ -24,6 +24,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData
@ -57,6 +58,7 @@ import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@ -92,6 +94,7 @@ class RoomListPresenter @Inject constructor(
private val logoutPresenter: Presenter<DirectLogoutState>,
private val appPreferencesStore: AppPreferencesStore,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val seenInvitesStore: SeenInvitesStore,
) : Presenter<RoomListState> {
private val encryptionService: EncryptionService = client.encryptionService()
@ -227,6 +230,7 @@ class RoomListPresenter @Inject constructor(
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
}
}
val seenRoomInvites by remember { seenInvitesStore.seenRoomIds() }.collectAsState(emptySet())
val securityBannerState by rememberSecurityBannerState(securityBannerDismissed)
return when {
showEmpty -> RoomListContentState.Empty(securityBannerState = securityBannerState)
@ -235,7 +239,8 @@ class RoomListPresenter @Inject constructor(
RoomListContentState.Rooms(
securityBannerState = securityBannerState,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList()
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList(),
seenRoomInvites = seenRoomInvites.toPersistentSet(),
)
}
}

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermiss
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
@Immutable
data class RoomListState(
@ -65,9 +66,11 @@ sealed interface RoomListContentState {
data class Empty(
val securityBannerState: SecurityBannerState,
) : RoomListContentState
data class Rooms(
val securityBannerState: SecurityBannerState,
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
val summaries: ImmutableList<RoomListRoomSummary>,
val seenRoomInvites: ImmutableSet<RoomId>,
) : RoomListContentState
}

View file

@ -46,6 +46,7 @@ import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@ -239,6 +240,8 @@ private fun RoomsViewList(
) { index, room ->
RoomSummaryRow(
room = room,
isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE &&
state.seenRoomInvites.contains(room.roomId),
onClick = onRoomClick,
eventSink = eventSink,
)

View file

@ -68,6 +68,7 @@ internal val minHeight = 84.dp
@Composable
internal fun RoomSummaryRow(
room: RoomListRoomSummary,
isInviteSeen: Boolean,
onClick: (RoomListRoomSummary) -> Unit,
eventSink: (RoomListEvents) -> Unit,
modifier: Modifier = Modifier,
@ -85,7 +86,7 @@ internal fun RoomSummaryRow(
Timber.d("Long click on invite room")
},
) {
InviteNameAndIndicatorRow(name = room.name)
InviteNameAndIndicatorRow(name = room.name, isInviteSeen = isInviteSeen)
InviteSubtitle(isDm = room.isDm, inviteSender = room.inviteSender)
if (!room.isDm && room.inviteSender != null) {
Spacer(modifier = Modifier.height(4.dp))
@ -300,6 +301,7 @@ private fun LastMessageAndIndicatorRow(
@Composable
private fun InviteNameAndIndicatorRow(
name: String?,
isInviteSeen: Boolean,
modifier: Modifier = Modifier,
) {
Row(
@ -316,9 +318,11 @@ private fun InviteNameAndIndicatorRow(
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
UnreadIndicatorAtom(
color = ElementTheme.colors.unreadIndicator
)
if (!isInviteSeen) {
UnreadIndicatorAtom(
color = ElementTheme.colors.unreadIndicator
)
}
}
}
@ -384,6 +388,8 @@ private fun MentionIndicatorAtom() {
internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryProvider::class) data: RoomListRoomSummary) = ElementPreview {
RoomSummaryRow(
room = data,
// Set isInviteSeen to true for the preview when the room has name "Bob"
isInviteSeen = data.name == "Bob",
onClick = {},
eventSink = {},
)

View file

@ -39,12 +39,10 @@ data class RoomListRoomSummary(
) {
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
isMarkedUnread ||
displayType == RoomSummaryDisplayType.INVITE
isMarkedUnread
val hasNewContent = numberOfUnreadMessages > 0 ||
numberOfUnreadMentions > 0 ||
numberOfUnreadNotifications > 0 ||
isMarkedUnread ||
displayType == RoomSummaryDisplayType.INVITE
isMarkedUnread
}

View file

@ -173,6 +173,8 @@ private fun RoomListSearchContent(
) { room ->
RoomSummaryRow(
room = room,
// TODO
isInviteSeen = false,
onClick = ::onRoomClick,
eventSink = eventSink,
)

View file

@ -12,9 +12,11 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.aLeaveRoomState
@ -169,10 +171,11 @@ class RoomListPresenterTest {
val matrixClient = FakeMatrixClient(
roomListService = roomListService
)
val presenter = createRoomListPresenter(client = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val presenter = createRoomListPresenter(
client = matrixClient,
seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)),
)
presenter.test {
val initialState = consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Skeleton }.last()
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
@ -194,6 +197,7 @@ class RoomListPresenterTest {
timestamp = "0 TimeOrDate true",
)
)
assertThat(withRoomsState.contentAsRooms().seenRoomInvites).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
cancelAndIgnoreRemainingEvents()
}
}
@ -680,6 +684,7 @@ class RoomListPresenterTest {
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true },
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore()
) = RoomListPresenter(
client = client,
syncService = syncService,
@ -711,6 +716,7 @@ class RoomListPresenterTest {
logoutPresenter = { aDirectLogoutState() },
appPreferencesStore = appPreferencesStore,
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
seenInvitesStore = seenInvitesStore,
)
}

View file

@ -65,12 +65,12 @@ class RoomListRoomSummaryTest {
}
@Test
fun `when display type is invite then isHighlighted and hasNewContent are true`() {
fun `when display type is invite then isHighlighted and hasNewContent are false`() {
val sut = createRoomListRoomSummary(
displayType = RoomSummaryDisplayType.INVITE,
)
assertThat(sut.isHighlighted).isTrue()
assertThat(sut.hasNewContent).isTrue()
assertThat(sut.isHighlighted).isFalse()
assertThat(sut.hasNewContent).isFalse()
}
}