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
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
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.toggle
import im.vector.app.core.platform.VectorViewModel
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.Dispatchers
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.extensions.tryOrNull
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.ThreePid
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.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(
val email: String,
@ -54,9 +56,9 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
private val session: Session)
: VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {
private val knownUsersSearch = BehaviorRelay.create<KnownUsersSearch>()
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
private val identityServerUsersSearch = BehaviorRelay.create<String>()
private val knownUsersSearch = MutableStateFlow("")
private val directoryUsersSearch = MutableStateFlow("")
private val identityServerUsersSearch = MutableStateFlow("")
@AssistedFactory
interface Factory {
@ -77,11 +79,10 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
private val identityServerListener = object : IdentityServiceListener {
override fun onIdentityServerChange() {
withState {
identityServerUsersSearch.accept(it.searchTerm)
identityServerUsersSearch.tryEmit(it.searchTerm)
val identityServerURL = cleanISURL(session.identityService().getCurrentIdentityServerUrl())
setState {
copy(
configuredIdentityServer = cleanISURL(session.identityService().getCurrentIdentityServerUrl())
)
copy(configuredIdentityServer = identityServerURL)
}
}
}
@ -120,7 +121,7 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
private fun handleISUpdateConsent(action: UserListAction.UpdateUserConsent) {
session.identityService().setUserConsent(action.consent)
withState {
identityServerUsersSearch.accept(it.searchTerm)
identityServerUsersSearch.tryEmit(it.searchTerm)
}
}
@ -139,9 +140,9 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
)
}
}
identityServerUsersSearch.accept(searchTerm)
knownUsersSearch.accept(searchTerm)
directoryUsersSearch.accept(searchTerm)
identityServerUsersSearch.tryEmit(searchTerm)
knownUsersSearch.tryEmit(searchTerm)
directoryUsersSearch.tryEmit(searchTerm)
}
private fun handleShareMyMatrixToLink() {
@ -151,9 +152,9 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
}
private fun handleClearSearchUsers() {
knownUsersSearch.accept("")
directoryUsersSearch.accept("")
identityServerUsersSearch.accept("")
knownUsersSearch.tryEmit("")
directoryUsersSearch.tryEmit("")
identityServerUsersSearch.tryEmit("")
setState {
copy(searchTerm = "")
}
@ -162,103 +163,82 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
private fun observeUsers() = withState { state ->
identityServerUsersSearch
.filter { it.isEmail() }
.throttleLast(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
val flowSession = session.rx()
val stream =
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()
.sample(300)
.onEach { search ->
executeSearchEmail(search)
}.launchIn(viewModelScope)
knownUsersSearch
.throttleLast(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
session.rx().livePagedUsers(it, state.excludedUserIds)
}
.execute { async ->
copy(knownUsers = async)
.sample(300)
.flowOn(Dispatchers.Main)
.flatMapLatest { search ->
session.getPagedUsersLive(search, state.excludedUserIds).asFlow()
}.execute {
copy(knownUsers = it)
}
directoryUsersSearch
.debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
val stream = if (search.isBlank()) {
Single.just(emptyList<User>())
} 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()
)
}
.debounce(300)
.onEach { search ->
executeSearchDirectory(state, search)
}.launchIn(viewModelScope)
}
Single.zip(
searchObservable,
profileObservable,
{ searchResults, optionalProfile ->
val profile = optionalProfile.getOrNull() ?: return@zip searchResults
val searchContainsProfile = searchResults.any { it.userId == profile.userId }
if (searchContainsProfile) {
searchResults
} else {
listOf(profile) + searchResults
}
}
private suspend fun executeSearchEmail(search: String) {
suspend {
val params = listOf(ThreePid.Email(search))
val foundThreePid = tryOrNull {
session.identityService().lookUp(params).firstOrNull()
}
if (foundThreePid == null) {
null
} else {
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
)
}
}
stream.toAsync {
copy(directoryUsers = it)
}
)
} catch (failure: Throwable) {
ThreePidUser(email = search, user = User(foundThreePid.matrixId))
}
.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 ->