diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt index 69b98200c1..cae8540f8b 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -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(initialState) { - private val knownUsersSearch = BehaviorRelay.create() - private val directoryUsersSearch = BehaviorRelay.create() - private val identityServerUsersSearch = BehaviorRelay.create() + 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()) - } 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 ->