diff --git a/CHANGES.md b/CHANGES.md index 4a6133c103..c2c287eb20 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Open an existing DM instead of creating a new one (#2319) + - Ask for explicit user consent to send their contact details to the identity server (#2375) Bugfix 🐛: - Fix issue when restoring draft after sharing (#2287) diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt index 8eb5bc733b..e380998fd2 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt @@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class ContactsBookAction : VectorViewModelAction { data class FilterWith(val filter: String) : ContactsBookAction() data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction() + object UserConsentGranted : ContactsBookAction() } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index c4cf9eab39..23d21f5240 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -18,6 +18,7 @@ package im.vector.app.features.contactsbook import android.os.Bundle import android.view.View +import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState @@ -57,10 +58,26 @@ class ContactsBookFragment @Inject constructor( sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) setupRecyclerView() setupFilterView() + setupConsentView() setupOnlyBoundContactsView() setupCloseView() } + private fun setupConsentView() { + phoneBookSearchForMatrixContacts.setOnClickListener { + withState(contactsBookViewModel) { state -> + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.identity_server_consent_dialog_title) + .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServerUrl ?: "")) + .setPositiveButton(R.string.yes) { _, _ -> + contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) + } + .setNegativeButton(R.string.no, null) + .show() + } + } + } + private fun setupOnlyBoundContactsView() { phoneBookOnlyBoundContacts.checkedChanges() .subscribe { @@ -98,6 +115,7 @@ class ContactsBookFragment @Inject constructor( } override fun invalidate() = withState(contactsBookViewModel) { state -> + phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved contactsBookController.setData(state) } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt index 167660d11e..2c4c5d0596 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt @@ -38,11 +38,10 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.identity.FoundThreePid +import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.ThreePid import timber.log.Timber -private typealias PhoneBookSearch = String - class ContactsBookViewModel @AssistedInject constructor(@Assisted initialState: ContactsBookViewState, private val contactsDataSource: ContactsDataSource, @@ -85,7 +84,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted private fun loadContacts() { setState { copy( - mappedContacts = Loading() + mappedContacts = Loading(), + identityServerUrl = session.identityService().getCurrentIdentityServerUrl(), + userConsent = session.identityService().getUserConsent() ) } @@ -109,6 +110,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted } private fun performLookup(data: List) { + if (!session.identityService().getUserConsent()) { + return + } viewModelScope.launch { val threePids = data.flatMap { contact -> contact.emails.map { ThreePid.Email(it.email) } + @@ -116,8 +120,14 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted } session.identityService().lookUp(threePids, object : MatrixCallback> { override fun onFailure(failure: Throwable) { - // Ignore Timber.w(failure, "Unable to perform the lookup") + + // Should not happen, but just to be sure + if (failure is IdentityServiceError.UserConsentNotProvided) { + setState { + copy(userConsent = false) + } + } } override fun onSuccess(data: List) { @@ -171,9 +181,21 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted when (action) { is ContactsBookAction.FilterWith -> handleFilterWith(action) is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action) + ContactsBookAction.UserConsentGranted -> handleUserConsentGranted() }.exhaustive } + private fun handleUserConsentGranted() { + session.identityService().setUserConsent(true) + + setState { + copy(userConsent = true) + } + + // Perform the lookup + performLookup(allContacts) + } + private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) { setState { copy( diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt index 3e4f4ddcb6..d2ee684c4d 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt @@ -26,10 +26,14 @@ data class ContactsBookViewState( val mappedContacts: Async> = Loading(), // Use to filter contacts by display name val searchTerm: String = "", - // Tru to display only bound contacts with their bound 2pid + // True to display only bound contacts with their bound 2pid val onlyBoundContacts: Boolean = false, // All contacts, filtered by searchTerm and onlyBoundContacts val filteredMappedContacts: List = emptyList(), // True when the identity service has return some data - val isBoundRetrieved: Boolean = false + val isBoundRetrieved: Boolean = false, + // The current identity server url if any + val identityServerUrl: String? = null, + // User consent to perform lookup (send emails to the identity server) + val userConsent: Boolean = false ) : MvRxState diff --git a/vector/src/main/res/layout/fragment_contacts_book.xml b/vector/src/main/res/layout/fragment_contacts_book.xml index eb90da1bbe..1f8566e05e 100644 --- a/vector/src/main/res/layout/fragment_contacts_book.xml +++ b/vector/src/main/res/layout/fragment_contacts_book.xml @@ -93,6 +93,27 @@ app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer" tools:visibility="visible" /> + + + + + app:layout_constraintTop_toBottomOf="@+id/phoneBookBottomBarrier" /> Retrieving your contacts… Your contact book is empty Contacts book + Search for contacts on Matrix Revoke invite Revoke invite to %1$s?