diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt index d3d534e694..a8c993e99d 100644 --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt @@ -20,4 +20,5 @@ import im.vector.riotx.core.platform.VectorViewModelAction sealed class PhoneBookAction : VectorViewModelAction { data class FilterWith(val filter: String) : PhoneBookAction() + data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : PhoneBookAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt index 39f00d6557..6a79a4b15d 100644 --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt @@ -52,11 +52,11 @@ class PhoneBookController @Inject constructor( override fun buildModels() { val currentState = state ?: return - val hasSearch = currentState.searchTerm.isNotBlank() + val hasSearch = currentState.searchTerm.isNotEmpty() when (val asyncMappedContacts = currentState.mappedContacts) { is Uninitialized -> renderEmptyState(false) is Loading -> renderLoading() - is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch) + is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts) is Fail -> renderFailure(asyncMappedContacts.error) } } @@ -75,49 +75,54 @@ class PhoneBookController @Inject constructor( } private fun renderSuccess(mappedContacts: List, - hasSearch: Boolean) { + hasSearch: Boolean, + onlyBoundContacts: Boolean) { if (mappedContacts.isEmpty()) { renderEmptyState(hasSearch) } else { - renderContacts(mappedContacts) + renderContacts(mappedContacts, onlyBoundContacts) } } - private fun renderContacts(mappedContacts: List) { + private fun renderContacts(mappedContacts: List, onlyBoundContacts: Boolean) { for (mappedContact in mappedContacts) { contactItem { id(mappedContact.id) contact(mappedContact) avatarRenderer(avatarRenderer) } - mappedContact.emails.forEach { - contactDetailItem { - id("$mappedContact.id${it.email}") - threePid(it.email) - matrixId(it.matrixId) - clickListener { - if (it.matrixId != null) { - callback?.onMatrixIdClick(it.matrixId) - } else { - callback?.onThreePidClick(ThreePid.Email(it.email)) + mappedContact.emails + .filter { !onlyBoundContacts || it.matrixId != null } + .forEach { + contactDetailItem { + id("$mappedContact.id${it.email}") + threePid(it.email) + matrixId(it.matrixId) + clickListener { + if (it.matrixId != null) { + callback?.onMatrixIdClick(it.matrixId) + } else { + callback?.onThreePidClick(ThreePid.Email(it.email)) + } + } } } - } - } - mappedContact.msisdns.forEach { - contactDetailItem { - id("$mappedContact.id${it.phoneNumber}") - threePid(it.phoneNumber) - matrixId(it.matrixId) - clickListener { - if (it.matrixId != null) { - callback?.onMatrixIdClick(it.matrixId) - } else { - callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber)) + mappedContact.msisdns + .filter { !onlyBoundContacts || it.matrixId != null } + .forEach { + contactDetailItem { + id("$mappedContact.id${it.phoneNumber}") + threePid(it.phoneNumber) + matrixId(it.matrixId) + clickListener { + if (it.matrixId != null) { + callback?.onMatrixIdClick(it.matrixId) + } else { + callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber)) + } + } } } - } - } } } diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt index 9f1f8268c3..ac8d3290cb 100644 --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt @@ -18,8 +18,10 @@ package im.vector.riotx.features.userdirectory import android.os.Bundle import android.view.View +import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.checkedChanges import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.user.model.User @@ -50,9 +52,18 @@ class PhoneBookFragment @Inject constructor( sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) setupRecyclerView() setupFilterView() + setupOnlyBoundContactsView() setupCloseView() } + private fun setupOnlyBoundContactsView() { + phoneBookOnlyBoundContacts.checkedChanges() + .subscribe { + phoneBookViewModel.handle(PhoneBookAction.OnlyBoundContacts(it)) + } + .disposeOnDestroyView() + } + private fun setupFilterView() { phoneBookFilter .textChanges() @@ -81,8 +92,9 @@ class PhoneBookFragment @Inject constructor( } } - override fun invalidate() = withState(phoneBookViewModel) { - phoneBookController.setData(it) + override fun invalidate() = withState(phoneBookViewModel) { state -> + phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved + phoneBookController.setData(state) } override fun onMatrixIdClick(matrixId: String) { diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt index d894bbe908..d78932ccf2 100644 --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt @@ -37,7 +37,9 @@ import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.invite.InviteUsersToRoomActivity +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import timber.log.Timber private typealias PhoneBookSearch = String @@ -71,13 +73,12 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted private var allContacts: List = emptyList() private var mappedContacts: List = emptyList() - private var foundThreePid: List = emptyList() init { loadContacts() - selectSubscribe(PhoneBookViewState::searchTerm) { - updateState() + selectSubscribe(PhoneBookViewState::searchTerm, PhoneBookViewState::onlyBoundContacts) { _, _ -> + updateFilteredMappedContacts() } } @@ -88,7 +89,7 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted ) } - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { allContacts = contactsDataSource.getContacts() mappedContacts = allContacts @@ -99,7 +100,7 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted } performLookup(allContacts) - updateState() + updateFilteredMappedContacts() } } @@ -111,24 +112,23 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted } session.identityService().lookUp(threePids, object : MatrixCallback> { override fun onFailure(failure: Throwable) { - // Ignore? + // Ignore + Timber.w(failure, "Unable to perform the lookup") } override fun onSuccess(data: List) { - foundThreePid = data - mappedContacts = allContacts.map { contactModel -> contactModel.copy( emails = contactModel.emails.map { email -> email.copy( - matrixId = foundThreePid + matrixId = data .firstOrNull { foundThreePid -> foundThreePid.threePid.value == email.email } ?.matrixId ) }, msisdns = contactModel.msisdns.map { msisdn -> msisdn.copy( - matrixId = foundThreePid + matrixId = data .firstOrNull { foundThreePid -> foundThreePid.threePid.value == msisdn.phoneNumber } ?.matrixId ) @@ -136,15 +136,25 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted ) } - updateState() + setState { + copy( + isBoundRetrieved = true + ) + } + + updateFilteredMappedContacts() } }) } } - private fun updateState() = withState { state -> + private fun updateFilteredMappedContacts() = withState { state -> val filteredMappedContacts = mappedContacts .filter { it.displayName.contains(state.searchTerm, true) } + .filter { contactModel -> + !state.onlyBoundContacts + || contactModel.emails.any { it.matrixId != null } || contactModel.msisdns.any { it.matrixId != null } + } setState { copy( @@ -155,10 +165,19 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted override fun handle(action: PhoneBookAction) { when (action) { - is PhoneBookAction.FilterWith -> handleFilterWith(action) + is PhoneBookAction.FilterWith -> handleFilterWith(action) + is PhoneBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action) }.exhaustive } + private fun handleOnlyBoundContacts(action: PhoneBookAction.OnlyBoundContacts) { + setState { + copy( + onlyBoundContacts = action.onlyBoundContacts + ) + } + } + private fun handleFilterWith(action: PhoneBookAction.FilterWith) { setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt index 81709f84b4..bfca2bc6b0 100644 --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt @@ -22,14 +22,14 @@ import com.airbnb.mvrx.MvRxState import im.vector.riotx.core.contacts.ContactModel data class PhoneBookViewState( - val searchTerm: String = "", + // All the contacts on the phone val mappedContacts: Async> = Loading(), - val filteredMappedContacts: List = emptyList() - /* - val knownUsers: Async> = Uninitialized, - val directoryUsers: Async> = Uninitialized, - val selectedUsers: Set = emptySet(), - val createAndInviteState: Async = Uninitialized, - val filterKnownUsersValue: Option = Option.empty() - */ + // Use to filter contacts by display name + val searchTerm: String = "", + // Tru 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 ) : MvRxState diff --git a/vector/src/main/res/layout/fragment_phonebook.xml b/vector/src/main/res/layout/fragment_phonebook.xml index 297201e2b1..14c44c11f0 100644 --- a/vector/src/main/res/layout/fragment_phonebook.xml +++ b/vector/src/main/res/layout/fragment_phonebook.xml @@ -79,6 +79,20 @@ + + + app:layout_constraintTop_toBottomOf="@+id/phoneBookOnlyBoundContacts" /> + android:paddingStart="8dp" + android:paddingTop="12dp" + android:paddingEnd="8dp">