Add checkbox to filter contacts with MatrixId only

This commit is contained in:
Benoit Marty 2020-07-08 21:52:14 +02:00
parent 1c733e6661
commit 6ceac578a3
7 changed files with 108 additions and 57 deletions

View file

@ -20,4 +20,5 @@ import im.vector.riotx.core.platform.VectorViewModelAction
sealed class PhoneBookAction : VectorViewModelAction { sealed class PhoneBookAction : VectorViewModelAction {
data class FilterWith(val filter: String) : PhoneBookAction() data class FilterWith(val filter: String) : PhoneBookAction()
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : PhoneBookAction()
} }

View file

@ -52,11 +52,11 @@ class PhoneBookController @Inject constructor(
override fun buildModels() { override fun buildModels() {
val currentState = state ?: return val currentState = state ?: return
val hasSearch = currentState.searchTerm.isNotBlank() val hasSearch = currentState.searchTerm.isNotEmpty()
when (val asyncMappedContacts = currentState.mappedContacts) { when (val asyncMappedContacts = currentState.mappedContacts) {
is Uninitialized -> renderEmptyState(false) is Uninitialized -> renderEmptyState(false)
is Loading -> renderLoading() is Loading -> renderLoading()
is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch) is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts)
is Fail -> renderFailure(asyncMappedContacts.error) is Fail -> renderFailure(asyncMappedContacts.error)
} }
} }
@ -75,22 +75,25 @@ class PhoneBookController @Inject constructor(
} }
private fun renderSuccess(mappedContacts: List<ContactModel>, private fun renderSuccess(mappedContacts: List<ContactModel>,
hasSearch: Boolean) { hasSearch: Boolean,
onlyBoundContacts: Boolean) {
if (mappedContacts.isEmpty()) { if (mappedContacts.isEmpty()) {
renderEmptyState(hasSearch) renderEmptyState(hasSearch)
} else { } else {
renderContacts(mappedContacts) renderContacts(mappedContacts, onlyBoundContacts)
} }
} }
private fun renderContacts(mappedContacts: List<ContactModel>) { private fun renderContacts(mappedContacts: List<ContactModel>, onlyBoundContacts: Boolean) {
for (mappedContact in mappedContacts) { for (mappedContact in mappedContacts) {
contactItem { contactItem {
id(mappedContact.id) id(mappedContact.id)
contact(mappedContact) contact(mappedContact)
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
} }
mappedContact.emails.forEach { mappedContact.emails
.filter { !onlyBoundContacts || it.matrixId != null }
.forEach {
contactDetailItem { contactDetailItem {
id("$mappedContact.id${it.email}") id("$mappedContact.id${it.email}")
threePid(it.email) threePid(it.email)
@ -104,7 +107,9 @@ class PhoneBookController @Inject constructor(
} }
} }
} }
mappedContact.msisdns.forEach { mappedContact.msisdns
.filter { !onlyBoundContacts || it.matrixId != null }
.forEach {
contactDetailItem { contactDetailItem {
id("$mappedContact.id${it.phoneNumber}") id("$mappedContact.id${it.phoneNumber}")
threePid(it.phoneNumber) threePid(it.phoneNumber)

View file

@ -18,8 +18,10 @@ package im.vector.riotx.features.userdirectory
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.checkedChanges
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
@ -50,9 +52,18 @@ class PhoneBookFragment @Inject constructor(
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
setupRecyclerView() setupRecyclerView()
setupFilterView() setupFilterView()
setupOnlyBoundContactsView()
setupCloseView() setupCloseView()
} }
private fun setupOnlyBoundContactsView() {
phoneBookOnlyBoundContacts.checkedChanges()
.subscribe {
phoneBookViewModel.handle(PhoneBookAction.OnlyBoundContacts(it))
}
.disposeOnDestroyView()
}
private fun setupFilterView() { private fun setupFilterView() {
phoneBookFilter phoneBookFilter
.textChanges() .textChanges()
@ -81,8 +92,9 @@ class PhoneBookFragment @Inject constructor(
} }
} }
override fun invalidate() = withState(phoneBookViewModel) { override fun invalidate() = withState(phoneBookViewModel) { state ->
phoneBookController.setData(it) phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
phoneBookController.setData(state)
} }
override fun onMatrixIdClick(matrixId: String) { override fun onMatrixIdClick(matrixId: String) {

View file

@ -37,7 +37,9 @@ import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
private typealias PhoneBookSearch = String private typealias PhoneBookSearch = String
@ -71,13 +73,12 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
private var allContacts: List<ContactModel> = emptyList() private var allContacts: List<ContactModel> = emptyList()
private var mappedContacts: List<ContactModel> = emptyList() private var mappedContacts: List<ContactModel> = emptyList()
private var foundThreePid: List<FoundThreePid> = emptyList()
init { init {
loadContacts() loadContacts()
selectSubscribe(PhoneBookViewState::searchTerm) { selectSubscribe(PhoneBookViewState::searchTerm, PhoneBookViewState::onlyBoundContacts) { _, _ ->
updateState() updateFilteredMappedContacts()
} }
} }
@ -88,7 +89,7 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
) )
} }
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
allContacts = contactsDataSource.getContacts() allContacts = contactsDataSource.getContacts()
mappedContacts = allContacts mappedContacts = allContacts
@ -99,7 +100,7 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
} }
performLookup(allContacts) performLookup(allContacts)
updateState() updateFilteredMappedContacts()
} }
} }
@ -111,24 +112,23 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
} }
session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> { session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> {
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
// Ignore? // Ignore
Timber.w(failure, "Unable to perform the lookup")
} }
override fun onSuccess(data: List<FoundThreePid>) { override fun onSuccess(data: List<FoundThreePid>) {
foundThreePid = data
mappedContacts = allContacts.map { contactModel -> mappedContacts = allContacts.map { contactModel ->
contactModel.copy( contactModel.copy(
emails = contactModel.emails.map { email -> emails = contactModel.emails.map { email ->
email.copy( email.copy(
matrixId = foundThreePid matrixId = data
.firstOrNull { foundThreePid -> foundThreePid.threePid.value == email.email } .firstOrNull { foundThreePid -> foundThreePid.threePid.value == email.email }
?.matrixId ?.matrixId
) )
}, },
msisdns = contactModel.msisdns.map { msisdn -> msisdns = contactModel.msisdns.map { msisdn ->
msisdn.copy( msisdn.copy(
matrixId = foundThreePid matrixId = data
.firstOrNull { foundThreePid -> foundThreePid.threePid.value == msisdn.phoneNumber } .firstOrNull { foundThreePid -> foundThreePid.threePid.value == msisdn.phoneNumber }
?.matrixId ?.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 val filteredMappedContacts = mappedContacts
.filter { it.displayName.contains(state.searchTerm, true) } .filter { it.displayName.contains(state.searchTerm, true) }
.filter { contactModel ->
!state.onlyBoundContacts
|| contactModel.emails.any { it.matrixId != null } || contactModel.msisdns.any { it.matrixId != null }
}
setState { setState {
copy( copy(
@ -156,9 +166,18 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
override fun handle(action: PhoneBookAction) { override fun handle(action: PhoneBookAction) {
when (action) { when (action) {
is PhoneBookAction.FilterWith -> handleFilterWith(action) is PhoneBookAction.FilterWith -> handleFilterWith(action)
is PhoneBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
}.exhaustive }.exhaustive
} }
private fun handleOnlyBoundContacts(action: PhoneBookAction.OnlyBoundContacts) {
setState {
copy(
onlyBoundContacts = action.onlyBoundContacts
)
}
}
private fun handleFilterWith(action: PhoneBookAction.FilterWith) { private fun handleFilterWith(action: PhoneBookAction.FilterWith) {
setState { setState {
copy( copy(

View file

@ -22,14 +22,14 @@ import com.airbnb.mvrx.MvRxState
import im.vector.riotx.core.contacts.ContactModel import im.vector.riotx.core.contacts.ContactModel
data class PhoneBookViewState( data class PhoneBookViewState(
val searchTerm: String = "", // All the contacts on the phone
val mappedContacts: Async<List<ContactModel>> = Loading(), val mappedContacts: Async<List<ContactModel>> = Loading(),
val filteredMappedContacts: List<ContactModel> = emptyList() // Use to filter contacts by display name
/* val searchTerm: String = "",
val knownUsers: Async<PagedList<User>> = Uninitialized, // Tru to display only bound contacts with their bound 2pid
val directoryUsers: Async<List<User>> = Uninitialized, val onlyBoundContacts: Boolean = false,
val selectedUsers: Set<User> = emptySet(), // All contacts, filtered by searchTerm and onlyBoundContacts
val createAndInviteState: Async<String> = Uninitialized, val filteredMappedContacts: List<ContactModel> = emptyList(),
val filterKnownUsersValue: Option<String> = Option.empty() // True when the identity service has return some data
*/ val isBoundRetrieved: Boolean = false
) : MvRxState ) : MvRxState

View file

@ -79,6 +79,20 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/phoneBookOnlyBoundContacts"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="4dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:text="@string/matrix_only_filter"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
tools:visibility="visible" />
<View <View
android:id="@+id/phoneBookFilterDivider" android:id="@+id/phoneBookFilterDivider"
android:layout_width="0dp" android:layout_width="0dp"
@ -87,13 +101,12 @@
android:background="?attr/vctr_list_divider_color" android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer" /> app:layout_constraintTop_toBottomOf="@+id/phoneBookOnlyBoundContacts" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/phoneBookRecyclerView" android:id="@+id/phoneBookRecyclerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="16dp"
android:fastScrollEnabled="true" android:fastScrollEnabled="true"
android:overScrollMode="always" android:overScrollMode="always"
android:scrollbars="vertical" android:scrollbars="vertical"

View file

@ -6,8 +6,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?riotx_background" android:background="?riotx_background"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:minHeight="72dp" android:paddingStart="8dp"
android:padding="8dp"> android:paddingTop="12dp"
android:paddingEnd="8dp">
<ImageView <ImageView
android:id="@+id/contactAvatar" android:id="@+id/contactAvatar"