Browse Source

Verified user badge.

Add disable action to verify user.
pull/3718/head
Benoit Marty 2 weeks ago committed by Benoit Marty
parent
commit
5378c4efad
  1. 1
      features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt
  2. 8
      features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt
  3. 43
      features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt
  4. 20
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt
  5. 7
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt
  6. 23
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
  7. 3
      features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt
  8. 2
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
  9. 15
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
  10. 5
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt

1
features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt

@ -16,6 +16,7 @@ data class UserProfileState( @@ -16,6 +16,7 @@ data class UserProfileState(
val userId: UserId,
val userName: String?,
val avatarUrl: String?,
val isVerified: AsyncData<Boolean>,
val isBlocked: AsyncData<Boolean>,
val startDmActionState: AsyncAction<RoomId>,
val displayConfirmationDialog: ConfirmationDialog?,

8
features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt

@ -27,6 +27,7 @@ import io.element.android.features.userprofile.api.UserProfileState.Confirmation @@ -27,6 +27,7 @@ import io.element.android.features.userprofile.api.UserProfileState.Confirmation
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
@ -75,6 +76,7 @@ class UserProfilePresenter @AssistedInject constructor( @@ -75,6 +76,7 @@ class UserProfilePresenter @AssistedInject constructor(
var userProfile by remember { mutableStateOf<MatrixUser?>(null) }
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val isBlocked: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
val isVerified: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
val dmRoomId by getDmRoomId()
val canCall by getCanCall(dmRoomId)
LaunchedEffect(Unit) {
@ -87,6 +89,11 @@ class UserProfilePresenter @AssistedInject constructor( @@ -87,6 +89,11 @@ class UserProfilePresenter @AssistedInject constructor(
LaunchedEffect(Unit) {
userProfile = client.getProfile(userId).getOrNull()
}
LaunchedEffect(Unit) {
suspend {
client.encryptionService().isUserVerified(userId).getOrThrow()
}.runCatchingUpdatingState(isVerified)
}
fun handleEvents(event: UserProfileEvents) {
when (event) {
@ -126,6 +133,7 @@ class UserProfilePresenter @AssistedInject constructor( @@ -126,6 +133,7 @@ class UserProfilePresenter @AssistedInject constructor(
userName = userProfile?.displayName,
avatarUrl = userProfile?.avatarUrl,
isBlocked = isBlocked.value,
isVerified = isVerified.value,
startDmActionState = startDmActionState.value,
displayConfirmationDialog = confirmationDialog,
isCurrentUser = isCurrentUser,

43
features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt

@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.test.A_THROWABLE @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.tests.testutils.WarmUpRule
@ -43,7 +44,7 @@ class UserProfilePresenterTest { @@ -43,7 +44,7 @@ class UserProfilePresenterTest {
@Test
fun `present - returns the user profile data`() = runTest {
val matrixUser = aMatrixUser(A_USER_ID.value, "Alice", "anAvatarUrl")
val client = FakeMatrixClient().apply {
val client = createFakeMatrixClient().apply {
givenGetProfileResult(A_USER_ID, Result.success(matrixUser))
}
val presenter = createUserProfilePresenter(
@ -55,6 +56,7 @@ class UserProfilePresenterTest { @@ -55,6 +56,7 @@ class UserProfilePresenterTest {
assertThat(initialState.userName).isEqualTo(matrixUser.displayName)
assertThat(initialState.avatarUrl).isEqualTo(matrixUser.avatarUrl)
assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(false))
assertThat(initialState.isVerified.dataOrNull()).isFalse()
assertThat(initialState.dmRoomId).isEqualTo(A_ROOM_ID)
assertThat(initialState.canCall).isFalse()
}
@ -108,7 +110,7 @@ class UserProfilePresenterTest { @@ -108,7 +110,7 @@ class UserProfilePresenterTest {
val room = FakeMatrixRoom(
canUserJoinCallResult = { canUserJoinCallResult },
)
val client = FakeMatrixClient().apply {
val client = createFakeMatrixClient().apply {
if (canFindRoom) {
givenGetRoomResult(A_ROOM_ID, room)
}
@ -126,7 +128,7 @@ class UserProfilePresenterTest { @@ -126,7 +128,7 @@ class UserProfilePresenterTest {
@Test
fun `present - returns empty data in case of failure`() = runTest {
val client = FakeMatrixClient().apply {
val client = createFakeMatrixClient().apply {
givenGetProfileResult(A_USER_ID, Result.failure(AN_EXCEPTION))
}
val presenter = createUserProfilePresenter(
@ -153,14 +155,12 @@ class UserProfilePresenterTest { @@ -153,14 +155,12 @@ class UserProfilePresenterTest {
dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
assertThat(awaitItem().displayConfirmationDialog).isNull()
ensureAllEventsConsumed()
}
}
@Test
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
val client = FakeMatrixClient()
val client = createFakeMatrixClient()
val presenter = createUserProfilePresenter(
client = client,
userId = A_USER_ID
@ -181,7 +181,7 @@ class UserProfilePresenterTest { @@ -181,7 +181,7 @@ class UserProfilePresenterTest {
@Test
fun `present - BlockUser with error`() = runTest {
val matrixClient = FakeMatrixClient()
val matrixClient = createFakeMatrixClient()
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE))
val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test {
@ -198,7 +198,7 @@ class UserProfilePresenterTest { @@ -198,7 +198,7 @@ class UserProfilePresenterTest {
@Test
fun `present - UnblockUser with error`() = runTest {
val matrixClient = FakeMatrixClient()
val matrixClient = createFakeMatrixClient()
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE))
val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test {
@ -225,8 +225,6 @@ class UserProfilePresenterTest { @@ -225,8 +225,6 @@ class UserProfilePresenterTest {
dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
assertThat(awaitItem().displayConfirmationDialog).isNull()
ensureAllEventsConsumed()
}
}
@ -262,13 +260,34 @@ class UserProfilePresenterTest { @@ -262,13 +260,34 @@ class UserProfilePresenterTest {
}
}
@Test
fun `present - when user is verified, the value in the state is true`() = runTest {
val client = createFakeMatrixClient(isUserVerified = true)
val presenter = createUserProfilePresenter(
client = client,
)
presenter.test {
assertThat(awaitItem().isVerified.isUninitialized()).isTrue()
assertThat(awaitItem().isVerified.isLoading()).isTrue()
assertThat(awaitItem().isVerified.dataOrNull()).isTrue()
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1)
skipItems(2)
return awaitItem()
}
private fun createFakeMatrixClient(
isUserVerified: Boolean = false,
) = FakeMatrixClient(
encryptionService = FakeEncryptionService(
isUserVerifiedResult = { Result.success(isUserVerified) }
),
)
private fun createUserProfilePresenter(
client: MatrixClient = FakeMatrixClient(),
client: MatrixClient = createFakeMatrixClient(),
userId: UserId = UserId("@alice:server.org"),
startDMAction: StartDMAction = FakeStartDMAction()
): UserProfilePresenter {

20
features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt

@ -18,9 +18,14 @@ import androidx.compose.runtime.Composable @@ -18,9 +18,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom
import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRowMolecule
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -30,12 +35,15 @@ import io.element.android.libraries.designsystem.theme.components.Text @@ -30,12 +35,15 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList
@Composable
fun UserProfileHeaderSection(
avatarUrl: String?,
userId: UserId,
userName: String?,
isUserVerified: AsyncData<Boolean>,
openAvatarPreview: (url: String) -> Unit,
modifier: Modifier = Modifier
) {
@ -67,6 +75,17 @@ fun UserProfileHeaderSection( @@ -67,6 +75,17 @@ fun UserProfileHeaderSection(
color = MaterialTheme.colorScheme.secondary,
textAlign = TextAlign.Center,
)
if (isUserVerified.dataOrNull() == true) {
MatrixBadgeRowMolecule(
data = listOf(
MatrixBadgeAtom.MatrixBadgeData(
text = stringResource(CommonStrings.common_verified),
icon = CompoundIcons.Verified(),
type = MatrixBadgeAtom.Type.Positive,
)
).toImmutableList(),
)
}
Spacer(Modifier.height(40.dp))
}
}
@ -78,6 +97,7 @@ internal fun UserProfileHeaderSectionPreview() = ElementPreview { @@ -78,6 +97,7 @@ internal fun UserProfileHeaderSectionPreview() = ElementPreview {
avatarUrl = null,
userId = UserId("@alice:example.com"),
userName = "Alice",
isUserVerified = AsyncData.Success(true),
openAvatarPreview = {},
)
}

7
features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt

@ -20,13 +20,12 @@ open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> @@ -20,13 +20,12 @@ open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState>
get() = sequenceOf(
aUserProfileState(),
aUserProfileState(userName = null),
aUserProfileState(isBlocked = AsyncData.Success(true)),
aUserProfileState(isBlocked = AsyncData.Success(true), isVerified = AsyncData.Success(true)),
aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block),
aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock),
aUserProfileState(isBlocked = AsyncData.Loading(true)),
aUserProfileState(isBlocked = AsyncData.Loading(true), isVerified = AsyncData.Loading()),
aUserProfileState(startDmActionState = AsyncAction.Loading),
aUserProfileState(canCall = true),
aUserProfileState(dmRoomId = null),
// Add other states here
)
}
@ -36,6 +35,7 @@ fun aUserProfileState( @@ -36,6 +35,7 @@ fun aUserProfileState(
userName: String? = "Daniel",
avatarUrl: String? = null,
isBlocked: AsyncData<Boolean> = AsyncData.Success(false),
isVerified: AsyncData<Boolean> = AsyncData.Success(false),
startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized,
displayConfirmationDialog: UserProfileState.ConfirmationDialog? = null,
isCurrentUser: Boolean = false,
@ -47,6 +47,7 @@ fun aUserProfileState( @@ -47,6 +47,7 @@ fun aUserProfileState(
userName = userName,
avatarUrl = avatarUrl,
isBlocked = isBlocked,
isVerified = isVerified,
startDmActionState = startDmActionState,
displayConfirmationDialog = displayConfirmationDialog,
isCurrentUser = isCurrentUser,

23
features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt

@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier @@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
@ -28,9 +29,13 @@ import io.element.android.features.userprofile.shared.blockuser.BlockUserSection @@ -28,9 +29,13 @@ import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings
@ -63,11 +68,11 @@ fun UserProfileView( @@ -63,11 +68,11 @@ fun UserProfileView(
avatarUrl = state.avatarUrl,
userId = state.userId,
userName = state.userName,
isUserVerified = state.isVerified,
openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.userName ?: state.userId.value, avatarUrl)
},
)
UserProfileMainActionsSection(
isCurrentUser = state.isCurrentUser,
canCall = state.canCall,
@ -75,10 +80,9 @@ fun UserProfileView( @@ -75,10 +80,9 @@ fun UserProfileView(
onStartDM = { state.eventSink(UserProfileEvents.StartDM) },
onCall = { state.dmRoomId?.let { onStartCall(it) } }
)
Spacer(modifier = Modifier.height(26.dp))
if (!state.isCurrentUser) {
VerifyUserSection(state)
BlockUserSection(state)
BlockUserDialogs(state)
}
@ -98,6 +102,19 @@ fun UserProfileView( @@ -98,6 +102,19 @@ fun UserProfileView(
}
}
@Composable
private fun VerifyUserSection(state: UserProfileState) {
if (state.isVerified.dataOrNull() == false) {
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_member_details_verify_button_title, state.userName ?: state.userId)) },
supportingContent = { Text(stringResource(R.string.screen_room_member_details_verify_button_subtitle)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
enabled = false,
onClick = { },
)
}
}
@PreviewsDayNight
@Composable
internal fun UserProfileViewPreview(

3
features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt

@ -40,6 +40,7 @@ import org.junit.Rule @@ -40,6 +40,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class UserProfileViewTest {
@ -123,6 +124,7 @@ class UserProfileViewTest { @@ -123,6 +124,7 @@ class UserProfileViewTest {
}
}
@Config(qualifiers = "h1024dp")
@Test
fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
@ -161,6 +163,7 @@ class UserProfileViewTest { @@ -161,6 +163,7 @@ class UserProfileViewTest {
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
}
@Config(qualifiers = "h1024dp")
@Test
fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>()

2
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt

@ -60,6 +60,8 @@ interface EncryptionService { @@ -60,6 +60,8 @@ interface EncryptionService {
*/
suspend fun startIdentityReset(): Result<IdentityResetHandle?>
suspend fun isUserVerified(userId: UserId): Result<Boolean>
/**
* Remember this identity, ensuring it does not result in a pin violation.
*/

15
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt

@ -38,6 +38,7 @@ import org.matrix.rustcomponents.sdk.BackupSteadyStateListener @@ -38,6 +38,7 @@ import org.matrix.rustcomponents.sdk.BackupSteadyStateListener
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener
import org.matrix.rustcomponents.sdk.Encryption
import org.matrix.rustcomponents.sdk.UserIdentity
import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState
import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress
import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException
@ -204,8 +205,18 @@ internal class RustEncryptionService( @@ -204,8 +205,18 @@ internal class RustEncryptionService(
}
}
override suspend fun isUserVerified(userId: UserId): Result<Boolean> = runCatching {
getUserIdentity(userId).isVerified()
}
override suspend fun pinUserIdentity(userId: UserId): Result<Unit> = runCatching {
val userIdentity = service.userIdentity(userId.value) ?: error("User identity not found")
userIdentity.pin()
getUserIdentity(userId).pin()
}
private suspend fun getUserIdentity(userId: UserId): UserIdentity {
return service.userIdentity(
userId = userId.value,
// requestFromHomeserverIfNeeded = true,
) ?: error("User identity not found")
}
}

5
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt

@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.flowOf @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.flowOf
class FakeEncryptionService(
var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() },
private val pinUserIdentityResult: (UserId) -> Result<Unit> = { lambdaError() },
private val isUserVerifiedResult: (UserId) -> Result<Boolean> = { lambdaError() },
) : EncryptionService {
private var disableRecoveryFailure: Exception? = null
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
@ -123,6 +124,10 @@ class FakeEncryptionService( @@ -123,6 +124,10 @@ class FakeEncryptionService(
return pinUserIdentityResult(userId)
}
override suspend fun isUserVerified(userId: UserId): Result<Boolean> = simulateLongTask {
isUserVerifiedResult(userId)
}
companion object {
const val FAKE_RECOVERY_KEY = "fake"
}

Loading…
Cancel
Save