Verified user badge.
Add disable action to verify user.
This commit is contained in:
committed by
Benoit Marty
parent
639089718d
commit
200ae60a8b
@@ -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?,
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
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(
|
||||
userName = userProfile?.displayName,
|
||||
avatarUrl = userProfile?.avatarUrl,
|
||||
isBlocked = isBlocked.value,
|
||||
isVerified = isVerified.value,
|
||||
startDmActionState = startDmActionState.value,
|
||||
displayConfirmationDialog = confirmationDialog,
|
||||
isCurrentUser = isCurrentUser,
|
||||
|
||||
@@ -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 {
|
||||
@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 {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
@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 {
|
||||
|
||||
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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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 {
|
||||
|
||||
dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
|
||||
assertThat(awaitItem().displayConfirmationDialog).isNull()
|
||||
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
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(
|
||||
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 {
|
||||
avatarUrl = null,
|
||||
userId = UserId("@alice:example.com"),
|
||||
userName = "Alice",
|
||||
isUserVerified = AsyncData.Success(true),
|
||||
openAvatarPreview = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
userName = userName,
|
||||
avatarUrl = avatarUrl,
|
||||
isBlocked = isBlocked,
|
||||
isVerified = isVerified,
|
||||
startDmActionState = startDmActionState,
|
||||
displayConfirmationDialog = displayConfirmationDialog,
|
||||
isCurrentUser = isCurrentUser,
|
||||
|
||||
@@ -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
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
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>()
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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(
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
return pinUserIdentityResult(userId)
|
||||
}
|
||||
|
||||
override suspend fun isUserVerified(userId: UserId): Result<Boolean> = simulateLongTask {
|
||||
isUserVerifiedResult(userId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FAKE_RECOVERY_KEY = "fake"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user