Mavericks 2: migrate UserListViewModel

This commit is contained in:
ganfra 2021-10-07 12:24:08 +02:00
parent 362ebcbe42
commit acf3b84781

View File

@ -16,12 +16,12 @@
package im.vector.app.features.userdirectory package im.vector.app.features.userdirectory
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -29,21 +29,23 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.toggle import im.vector.app.core.extensions.toggle
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import io.reactivex.Single import kotlinx.coroutines.Dispatchers
import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.IdentityServiceListener import org.matrix.android.sdk.api.session.identity.IdentityServiceListener
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.rx.rx
import java.util.concurrent.TimeUnit
private typealias KnownUsersSearch = String
private typealias DirectoryUsersSearch = String
data class ThreePidUser( data class ThreePidUser(
val email: String, val email: String,
@ -54,9 +56,9 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
private val session: Session) private val session: Session)
: VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) { : VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {
private val knownUsersSearch = BehaviorRelay.create<KnownUsersSearch>() private val knownUsersSearch = MutableStateFlow("")
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>() private val directoryUsersSearch = MutableStateFlow("")
private val identityServerUsersSearch = BehaviorRelay.create<String>() private val identityServerUsersSearch = MutableStateFlow("")
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -77,11 +79,10 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
private val identityServerListener = object : IdentityServiceListener { private val identityServerListener = object : IdentityServiceListener {
override fun onIdentityServerChange() { override fun onIdentityServerChange() {
withState { withState {
identityServerUsersSearch.accept(it.searchTerm) identityServerUsersSearch.tryEmit(it.searchTerm)
val identityServerURL = cleanISURL(session.identityService().getCurrentIdentityServerUrl())
setState { setState {
copy( copy(configuredIdentityServer = identityServerURL)
configuredIdentityServer = cleanISURL(session.identityService().getCurrentIdentityServerUrl())
)
} }
} }
} }
@ -120,7 +121,7 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
private fun handleISUpdateConsent(action: UserListAction.UpdateUserConsent) { private fun handleISUpdateConsent(action: UserListAction.UpdateUserConsent) {
session.identityService().setUserConsent(action.consent) session.identityService().setUserConsent(action.consent)
withState { withState {
identityServerUsersSearch.accept(it.searchTerm) identityServerUsersSearch.tryEmit(it.searchTerm)
} }
} }
@ -139,9 +140,9 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
) )
} }
} }
identityServerUsersSearch.accept(searchTerm) identityServerUsersSearch.tryEmit(searchTerm)
knownUsersSearch.accept(searchTerm) knownUsersSearch.tryEmit(searchTerm)
directoryUsersSearch.accept(searchTerm) directoryUsersSearch.tryEmit(searchTerm)
} }
private fun handleShareMyMatrixToLink() { private fun handleShareMyMatrixToLink() {
@ -151,9 +152,9 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
} }
private fun handleClearSearchUsers() { private fun handleClearSearchUsers() {
knownUsersSearch.accept("") knownUsersSearch.tryEmit("")
directoryUsersSearch.accept("") directoryUsersSearch.tryEmit("")
identityServerUsersSearch.accept("") identityServerUsersSearch.tryEmit("")
setState { setState {
copy(searchTerm = "") copy(searchTerm = "")
} }
@ -162,103 +163,82 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
private fun observeUsers() = withState { state -> private fun observeUsers() = withState { state ->
identityServerUsersSearch identityServerUsersSearch
.filter { it.isEmail() } .filter { it.isEmail() }
.throttleLast(300, TimeUnit.MILLISECONDS) .sample(300)
.switchMapSingle { search -> .onEach { search ->
val flowSession = session.rx() executeSearchEmail(search)
val stream = }.launchIn(viewModelScope)
flowSession.lookupThreePid(ThreePid.Email(search)).flatMap {
it.getOrNull()?.let { foundThreePid ->
flowSession.getProfileInfo(foundThreePid.matrixId)
.map { json ->
ThreePidUser(
email = search,
user = User(
userId = foundThreePid.matrixId,
displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String,
avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
)
)
}
.onErrorResumeNext {
Single.just(ThreePidUser(email = search, user = User(foundThreePid.matrixId)))
}
} ?: Single.just(ThreePidUser(email = search, user = null))
}
stream.toAsync {
copy(matchingEmail = it)
}
}
.subscribe()
.disposeOnClear()
knownUsersSearch knownUsersSearch
.throttleLast(300, TimeUnit.MILLISECONDS) .sample(300)
.observeOn(AndroidSchedulers.mainThread()) .flowOn(Dispatchers.Main)
.switchMap { .flatMapLatest { search ->
session.rx().livePagedUsers(it, state.excludedUserIds) session.getPagedUsersLive(search, state.excludedUserIds).asFlow()
} }.execute {
.execute { async -> copy(knownUsers = it)
copy(knownUsers = async)
} }
directoryUsersSearch directoryUsersSearch
.debounce(300, TimeUnit.MILLISECONDS) .debounce(300)
.switchMapSingle { search -> .onEach { search ->
val stream = if (search.isBlank()) { executeSearchDirectory(state, search)
Single.just(emptyList<User>()) }.launchIn(viewModelScope)
} else { }
val searchObservable = session.rx()
.searchUsersDirectory(search, 50, state.excludedUserIds.orEmpty())
.map { users ->
users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
// If it's a valid user id try to use Profile API
// because directory only returns users that are in public rooms or share a room with you, where as
// profile will work other federations
if (!MatrixPatterns.isUserId(search)) {
searchObservable
} else {
val profileObservable = session.rx().getProfileInfo(search)
.map { json ->
User(
userId = search,
displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String,
avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
).toOptional()
}
.onErrorResumeNext {
// Profile API can be restricted and doesn't have to return result.
// In this case allow inviting valid user ids.
Single.just(
User(
userId = search,
displayName = null,
avatarUrl = null
).toOptional()
)
}
Single.zip( private suspend fun executeSearchEmail(search: String) {
searchObservable, suspend {
profileObservable, val params = listOf(ThreePid.Email(search))
{ searchResults, optionalProfile -> val foundThreePid = tryOrNull {
val profile = optionalProfile.getOrNull() ?: return@zip searchResults session.identityService().lookUp(params).firstOrNull()
val searchContainsProfile = searchResults.any { it.userId == profile.userId } }
if (searchContainsProfile) { if (foundThreePid == null) {
searchResults null
} else { } else {
listOf(profile) + searchResults try {
} val json = session.getProfile(foundThreePid.matrixId)
} ThreePidUser(
email = search,
user = User(
userId = foundThreePid.matrixId,
displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String,
avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
) )
} )
} } catch (failure: Throwable) {
stream.toAsync { ThreePidUser(email = search, user = User(foundThreePid.matrixId))
copy(directoryUsers = it)
}
} }
.subscribe() }
.disposeOnClear() }.execute {
copy(matchingEmail = it)
}
}
private suspend fun executeSearchDirectory(state: UserListViewState, search: String) {
suspend {
if (search.isBlank()) {
emptyList()
} else {
val searchResult = session
.searchUsersDirectory(search, 50, state.excludedUserIds.orEmpty())
.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
val userProfile = if (MatrixPatterns.isUserId(search)) {
val json = tryOrNull { session.getProfile(search) }
User(
userId = search,
displayName = json?.get(ProfileService.DISPLAY_NAME_KEY) as? String,
avatarUrl = json?.get(ProfileService.AVATAR_URL_KEY) as? String
)
} else {
null
}
if (userProfile == null || searchResult.any { it.userId == userProfile.userId }) {
searchResult
} else {
listOf(userProfile) + searchResult
}
}
}.execute {
copy(directoryUsers = it)
}
} }
private fun handleSelectUser(action: UserListAction.AddPendingSelection) = withState { state -> private fun handleSelectUser(action: UserListAction.AddPendingSelection) = withState { state ->