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 a230b83e99
commit d8d9f2cf14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 326 additions and 29 deletions

View file

@ -0,0 +1,90 @@
/*
* Copyright 2025 New Vector 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.invite.impl
import android.content.Context
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
private val seenInvitesKey = stringSetPreferencesKey("seenInvites")
@SingleIn(SessionScope::class)
@ContributesBinding(SessionScope::class)
class DefaultSeenInvitesStore @Inject constructor(
@ApplicationContext context: Context,
currentSessionIdHolder: CurrentSessionIdHolder,
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
sessionObserver: SessionObserver,
) : SeenInvitesStore {
private val sessionId: SessionId = currentSessionIdHolder.current
init {
sessionObserver.addListener(object : SessionListener {
override suspend fun onSessionCreated(userId: String) = Unit
override suspend fun onSessionDeleted(userId: String) {
if (sessionId.value == userId) {
clear()
}
}
})
}
private val dataStoreFile = sessionId.value.hash().take(16).let { hashedUserId ->
context.preferencesDataStoreFile("session_${hashedUserId}_seen-invites")
}
private val store = PreferenceDataStoreFactory.create(
scope = sessionCoroutineScope,
migrations = emptyList(),
) {
dataStoreFile
}
override fun seenRoomIds(): Flow<Set<RoomId>> =
store.data.map { prefs ->
prefs[seenInvitesKey]
.orEmpty()
.map { RoomId(it) }
.toSet()
}
override suspend fun markAsSeen(roomId: RoomId) {
store.edit { prefs ->
prefs[seenInvitesKey] = prefs[seenInvitesKey].orEmpty() + roomId.value
}
}
override suspend fun markAsUnSeen(roomId: RoomId) {
store.edit { prefs ->
prefs[seenInvitesKey] = prefs[seenInvitesKey].orEmpty() - roomId.value
}
}
override suspend fun clear() {
dataStoreFile.safeDelete()
}
}

View file

@ -13,6 +13,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import im.vector.app.features.analytics.plan.JoinedRoom
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.ConfirmingDeclineInvite
@ -34,6 +35,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
private val client: MatrixClient,
private val joinRoom: JoinRoom,
private val notificationCleaner: NotificationCleaner,
private val seenInvitesStore: SeenInvitesStore,
) : Presenter<AcceptDeclineInviteState> {
@Composable
override fun present(): AcceptDeclineInviteState {
@ -107,6 +109,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
)
.onSuccess {
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
seenInvitesStore.markAsUnSeen(roomId)
}
.map { roomId }
}
@ -125,6 +128,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
client.ignoreUser(inviteData.senderId).getOrThrow()
}
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, inviteData.roomId)
seenInvitesStore.markAsUnSeen(inviteData.roomId)
inviteData.roomId
}.runCatchingUpdatingState(declinedAction)
}

View file

@ -9,9 +9,11 @@ package io.element.android.features.invite.impl.response
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
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.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
@ -20,6 +22,8 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
@ -33,6 +37,7 @@ import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -54,7 +59,10 @@ class AcceptDeclineInvitePresenterTest {
@Test
fun `present - declining invite cancel flow`() = runTest {
val presenter = createAcceptDeclineInvitePresenter()
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
@ -72,6 +80,7 @@ class AcceptDeclineInvitePresenterTest {
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
}
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
@ -84,7 +93,11 @@ class AcceptDeclineInvitePresenterTest {
Result.success(FakeRoomPreview(declineInviteResult = declineInviteFailure))
}
)
val presenter = createAcceptDeclineInvitePresenter(client = client)
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
client = client,
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
@ -111,6 +124,7 @@ class AcceptDeclineInvitePresenterTest {
cancelAndConsumeRemainingEvents()
}
assert(declineInviteFailure).isCalledOnce()
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
@ -129,9 +143,11 @@ class AcceptDeclineInvitePresenterTest {
Result.success(FakeRoomPreview(declineInviteResult = declineInviteSuccess))
}
)
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
client = client,
notificationCleaner = fakeNotificationCleaner,
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
@ -156,6 +172,7 @@ class AcceptDeclineInvitePresenterTest {
clearMembershipNotificationForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
@ -174,9 +191,11 @@ class AcceptDeclineInvitePresenterTest {
},
ignoreUserResult = ignoreUserSuccess
)
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
client = client,
notificationCleaner = fakeNotificationCleaner,
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
@ -202,6 +221,7 @@ class AcceptDeclineInvitePresenterTest {
clearMembershipNotificationForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
@ -214,7 +234,11 @@ class AcceptDeclineInvitePresenterTest {
Result.success(FakeRoomPreview(declineInviteResult = declineInviteFailure))
}
)
val presenter = createAcceptDeclineInvitePresenter(client = client)
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
client = client,
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
@ -230,6 +254,7 @@ class AcceptDeclineInvitePresenterTest {
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
}
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
@ -237,7 +262,11 @@ class AcceptDeclineInvitePresenterTest {
val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
Result.failure<Unit>(RuntimeException("Failed to join room $roomIdOrAlias"))
}
val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomFailure)
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
joinRoomLambda = joinRoomFailure,
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
@ -266,6 +295,7 @@ class AcceptDeclineInvitePresenterTest {
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
)
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
@ -279,9 +309,11 @@ class AcceptDeclineInvitePresenterTest {
val joinRoomSuccess = lambdaRecorder { _: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
Result.success(Unit)
}
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
joinRoomLambda = joinRoomSuccess,
notificationCleaner = fakeNotificationCleaner,
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
@ -308,6 +340,7 @@ class AcceptDeclineInvitePresenterTest {
clearMembershipNotificationForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3)
}
private fun anInviteData(
@ -330,11 +363,13 @@ class AcceptDeclineInvitePresenterTest {
Result.success(Unit)
},
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
): AcceptDeclineInvitePresenter {
return AcceptDeclineInvitePresenter(
client = client,
joinRoom = FakeJoinRoom(joinRoomLambda),
notificationCleaner = notificationCleaner,
seenInvitesStore = seenInvitesStore,
)
}
}