diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 546922f25b..655df2c2ab 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -38,6 +38,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.1.0-beta01' implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + // Paging + implementation "androidx.paging:paging-runtime-ktx:2.1.0" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index 4126ff6f87..97661cebd1 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -16,6 +16,7 @@ package im.vector.matrix.rx +import androidx.paging.PagedList import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.pushers.Pusher @@ -48,6 +49,10 @@ class RxSession(private val session: Session) { return session.liveUsers().asObservable() } + fun livePagedUsers(filter: String? = null): Observable> { + return session.livePagedUsers(filter).asObservable() + } + fun createRoom(roomParams: CreateRoomParams): Single = Single.create { session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index 292c90ef60..d3c58edd94 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.user import androidx.lifecycle.LiveData +import androidx.paging.PagedList import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.Cancelable @@ -56,4 +57,11 @@ interface UserService { */ fun liveUsers(): LiveData> + /** + * Observe a live [PagedList] of users sorted alphabetically. You can filter the users. + * @param filter the filter. It will look into userId and displayName. + * @return a Livedata of users + */ + fun livePagedUsers(filter: String? = null): LiveData> + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt index 64afa3d494..1fc60d8098 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt @@ -19,14 +19,17 @@ package im.vector.matrix.android.internal.database import android.os.Handler import android.os.HandlerThread import io.realm.* +import timber.log.Timber import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit private const val THREAD_NAME = "REALM_QUERY_LATCH" class RealmQueryLatch(private val realmConfiguration: RealmConfiguration, private val realmQueryBuilder: (Realm) -> RealmQuery) { - fun await() { + @Throws(InterruptedException::class) + fun await(timeout: Long = Long.MAX_VALUE, timeUnit: TimeUnit = TimeUnit.MILLISECONDS) { val latch = CountDownLatch(1) val handlerThread = HandlerThread(THREAD_NAME + hashCode()) handlerThread.start() @@ -46,8 +49,13 @@ class RealmQueryLatch(private val realmConfiguration: RealmConf }) } handler.post(runnable) - latch.await() - handlerThread.quit() + try { + latch.await(timeout, timeUnit) + } catch (exception: InterruptedException) { + throw exception + } finally { + handlerThread.quit() + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt index e77cafc8f2..f9cad783a6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt @@ -34,6 +34,7 @@ import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAcco import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.tryTransactionSync import io.realm.RealmConfiguration +import java.util.concurrent.TimeUnit import javax.inject.Inject internal interface CreateRoomTask : Task @@ -56,8 +57,10 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro realm.where(RoomEntity::class.java) .equalTo(RoomEntityFields.ROOM_ID, roomId) } - rql.await() - Try.just(roomId) + Try { + rql.await(timeout = 20L, timeUnit = TimeUnit.SECONDS) + roomId + } }.flatMap { roomId -> if (params.isDirect()) { handleDirectChatCreation(params, roomId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt index e71a9fe39e..7d78069e78 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.task.Task import io.realm.RealmConfiguration +import java.util.concurrent.TimeUnit import javax.inject.Inject internal interface JoinRoomTask : Task { @@ -48,8 +49,10 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room realm.where(RoomEntity::class.java) .equalTo(RoomEntityFields.ROOM_ID, roomId) } - rql.await() - Try.just(roomId) + Try { + rql.await(20L, TimeUnit.SECONDS) + roomId + } }.flatMap { roomId -> setReadMarkers(roomId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index 06a7ba7fda..8d47d401a7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -18,6 +18,9 @@ package im.vector.matrix.android.internal.session.user import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import androidx.paging.DataSource +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.user.UserService @@ -38,6 +41,24 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona private val searchUserTask: SearchUserTask, private val taskExecutor: TaskExecutor) : UserService { + private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory by lazy { + monarchy.createDataSourceFactory { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + } + } + + private val domainDataSourceFactory: DataSource.Factory by lazy { + realmDataSourceFactory.map { + it.asDomain() + } + } + + private val livePagedListBuilder: LivePagedListBuilder by lazy { + LivePagedListBuilder(domainDataSourceFactory, PagedList.Config.Builder().setPageSize(100).setEnablePlaceholders(false).build()) + } + override fun getUser(userId: String): User? { val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } ?: return null @@ -67,6 +88,25 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona ) } + override fun livePagedUsers(filter: String?): LiveData> { + realmDataSourceFactory.updateQuery { realm -> + val query = realm.where(UserEntity::class.java) + if (filter.isNullOrEmpty()) { + query.isNotEmpty(UserEntityFields.USER_ID) + } else { + query + .beginGroup() + .contains(UserEntityFields.DISPLAY_NAME, filter) + .or() + .contains(UserEntityFields.USER_ID, filter) + .endGroup() + } + query.sort(UserEntityFields.DISPLAY_NAME) + } + return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) + } + + override fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set, @@ -77,4 +117,4 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona .dispatchTo(callback) .executeBy(taskExecutor) } -} \ No newline at end of file +} diff --git a/vector/build.gradle b/vector/build.gradle index a864574792..85a4dae165 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -148,7 +148,7 @@ android { dependencies { - def epoxy_version = "3.3.0" + def epoxy_version = "3.7.0" def arrow_version = "0.8.2" def coroutines_version = "1.0.1" def markwon_version = '3.0.0' @@ -193,11 +193,15 @@ dependencies { implementation("com.airbnb.android:epoxy:$epoxy_version") kapt "com.airbnb.android:epoxy-processor:$epoxy_version" + implementation "com.airbnb.android:epoxy-paging:$epoxy_version" implementation 'com.airbnb.android:mvrx:1.0.1' // Work implementation "androidx.work:work-runtime-ktx:2.1.0-rc01" + // Paging + implementation "androidx.paging:paging-runtime-ktx:2.1.0" + // Functional Programming implementation "io.arrow-kt:arrow-core:$arrow_version" diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 90284011cf..35cda2e6c6 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -38,7 +38,7 @@ import im.vector.riotx.features.home.HomeDrawerFragment import im.vector.riotx.features.home.HomeModule import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment -import im.vector.riotx.features.home.createdirect.CreateDirectRoomFragment +import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.timeline.action.* @@ -159,7 +159,7 @@ interface ScreenComponent { fun inject(pushGatewaysFragment: PushGatewaysFragment) - fun inject(createDirectRoomKnownUsersFragment: CreateDirectRoomFragment) + fun inject(createDirectRoomKnownUsersFragment: CreateDirectRoomKnownUsersFragment) fun inject(createDirectRoomDirectoryUsersFragment: CreateDirectRoomDirectoryUsersFragment) diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt index eb26c321ed..13bc93686f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt @@ -70,7 +70,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { } } if (isFirstCreation()) { - addFragment(CreateDirectRoomFragment(), R.id.container) + addFragment(CreateDirectRoomKnownUsersFragment(), R.id.container) } viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) { renderCreateAndInviteState(it) diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt index f7f9eca9a1..3916ff7bbb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -32,13 +32,13 @@ import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* import javax.inject.Inject -class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirectRoomController.Callback { +class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), DirectoryUsersController.Callback { override fun getLayoutResId() = R.layout.fragment_create_direct_room_directory_users private val viewModel: CreateDirectRoomViewModel by activityViewModel() - @Inject lateinit var directRoomController: CreateDirectRoomController + @Inject lateinit var directRoomController: DirectoryUsersController private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel override fun injectWith(injector: ScreenComponent) { @@ -56,7 +56,6 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec private fun setupRecyclerView() { recyclerView.setHasFixedSize(true) directRoomController.callback = this - directRoomController.displayMode = CreateDirectRoomViewState.DisplayMode.DIRECTORY_USERS recyclerView.setController(directRoomController) } @@ -76,7 +75,7 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec private fun setupCloseView() { createDirectRoomClose.setOnClickListener { - navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.Close) + navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.Previous) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomFragment.kt rename to vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt index 63c6aaf1bf..7747336627 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt @@ -41,7 +41,7 @@ import im.vector.riotx.features.home.AvatarRenderer import kotlinx.android.synthetic.main.fragment_create_direct_room.* import javax.inject.Inject -class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomController.Callback { +class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersController.Callback { override fun getLayoutResId() = R.layout.fragment_create_direct_room @@ -49,7 +49,7 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle private val viewModel: CreateDirectRoomViewModel by activityViewModel() - @Inject lateinit var directRoomController: CreateDirectRoomController + @Inject lateinit var directRoomController: KnownUsersController @Inject lateinit var avatarRenderer: AvatarRenderer private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel @@ -104,13 +104,13 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle // Don't activate animation as we might have way to much item animation when filtering recyclerView.itemAnimator = null directRoomController.callback = this - directRoomController.displayMode = CreateDirectRoomViewState.DisplayMode.KNOWN_USERS recyclerView.setController(directRoomController) } private fun setupFilterView() { createDirectRoomFilter .textChanges() + .startWith(createDirectRoomFilter.text) .subscribe { text -> val filterValue = text.trim() val action = if (filterValue.isBlank()) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt index a93514182a..b0fed9b8e8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt @@ -35,6 +35,7 @@ import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.LiveEvent import io.reactivex.Observable import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.functions.BiFunction import java.util.concurrent.TimeUnit @@ -154,22 +155,13 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } private fun observeKnownUsers() { - Observable - .combineLatest, Option, List>( - session.rx().liveUsers(), - knownUsersFilter.throttleLast(300, TimeUnit.MILLISECONDS), - BiFunction { users, filter -> - val filterValue = filter.orNull() - if (filterValue.isNullOrEmpty()) { - users - } else { - users.filter { - it.displayName?.contains(filterValue, ignoreCase = true) ?: false - || it.userId.contains(filterValue, ignoreCase = true) - } - } - } - ).execute { async -> + knownUsersFilter + .throttleLast(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { + session.rx().livePagedUsers(it.orNull()) + } + .execute { async -> copy( knownUsers = async, filterKnownUsersValue = knownUsersFilter.value ?: Option.empty() diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt index 95c76008dc..e1c9ad4609 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.createdirect +import androidx.paging.PagedList import arrow.core.Option import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState @@ -25,7 +26,7 @@ import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.user.model.User data class CreateDirectRoomViewState( - val knownUsers: Async> = Uninitialized, + val knownUsers: Async> = Uninitialized, val directoryUsers: Async> = Uninitialized, val selectedUsers: Set = emptySet(), val createAndInviteState: Async = Uninitialized, diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt similarity index 64% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomController.kt rename to vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt index eea10452bf..c174ac6b46 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt @@ -32,13 +32,12 @@ import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject -class CreateDirectRoomController @Inject constructor(private val session: Session, - private val avatarRenderer: AvatarRenderer, - private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter) : EpoxyController() { +class DirectoryUsersController @Inject constructor(private val session: Session, + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter) : EpoxyController() { private var state: CreateDirectRoomViewState? = null - var displayMode = CreateDirectRoomViewState.DisplayMode.KNOWN_USERS var callback: Callback? = null @@ -51,19 +50,15 @@ class CreateDirectRoomController @Inject constructor(private val session: Sessio requestModelBuild() } + override fun buildModels() { val currentState = state ?: return val hasSearch = currentState.directorySearchTerm.isNotBlank() - val isFiltering = currentState.filterKnownUsersValue.nonEmpty() - val asyncUsers = if (displayMode == CreateDirectRoomViewState.DisplayMode.DIRECTORY_USERS) { - currentState.directoryUsers - } else { - currentState.knownUsers - } + val asyncUsers = currentState.directoryUsers when (asyncUsers) { is Uninitialized -> renderEmptyState(false) is Loading -> renderLoading() - is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch, isFiltering) + is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch) is Fail -> renderFailure(asyncUsers.error) } } @@ -84,31 +79,20 @@ class CreateDirectRoomController @Inject constructor(private val session: Sessio private fun renderSuccess(users: List, selectedUsers: List, - hasSearch: Boolean, - isFiltering: Boolean) { + hasSearch: Boolean) { if (users.isEmpty()) { renderEmptyState(hasSearch) } else { - renderUsers(users, selectedUsers, isFiltering) + renderUsers(users, selectedUsers) } } - private fun renderUsers(users: List, selectedUsers: List, isFiltering: Boolean) { - var lastFirstLetter: String? = null + private fun renderUsers(users: List, selectedUsers: List) { for (user in users) { if (user.userId == session.myUserId) { continue } val isSelected = selectedUsers.contains(user.userId) - val currentFirstLetter = user.displayName.firstLetterOfDisplayName() - val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter - lastFirstLetter = currentFirstLetter - - CreateDirectRoomLetterHeaderItem_() - .id(currentFirstLetter) - .letter(currentFirstLetter) - .addIf(showLetter, this) - createDirectRoomUserItem { id(user.userId) selected(isSelected) @@ -124,14 +108,10 @@ class CreateDirectRoomController @Inject constructor(private val session: Sessio } private fun renderEmptyState(hasSearch: Boolean) { - val noResultRes = if (displayMode == CreateDirectRoomViewState.DisplayMode.DIRECTORY_USERS) { - if (hasSearch) { - R.string.no_result_placeholder - } else { - R.string.direct_room_start_search - } + val noResultRes = if (hasSearch) { + R.string.no_result_placeholder } else { - R.string.direct_room_no_known_users + R.string.direct_room_start_search } noResultItem { id("noResult") @@ -141,9 +121,7 @@ class CreateDirectRoomController @Inject constructor(private val session: Sessio interface Callback { fun onItemClick(user: User) - fun retryDirectoryUsersRequest() { - // NO-OP - } + fun retryDirectoryUsersRequest() } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt new file mode 100644 index 0000000000..fbb1cfcc4e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.createdirect + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.paging.PagedListEpoxyController +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.util.createUIHandler +import im.vector.matrix.android.internal.util.firstLetterOfDisplayName +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.EmptyItem_ +import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.epoxy.noResultItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +class KnownUsersController @Inject constructor(private val session: Session, + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider) : PagedListEpoxyController( + modelBuildingHandler = createUIHandler() +) { + + private var selectedUsers: List = emptyList() + private var users: Async> = Uninitialized + private var isFiltering: Boolean = false + + var callback: Callback? = null + + init { + requestModelBuild() + } + + fun setData(state: CreateDirectRoomViewState) { + this.isFiltering = !state.filterKnownUsersValue.isEmpty() + val newSelection = state.selectedUsers.map { it.userId } + this.users = state.knownUsers + if (newSelection != selectedUsers) { + this.selectedUsers = newSelection + requestForcedModelBuild() + } + submitList(state.knownUsers()) + } + + override fun buildItemModel(currentPosition: Int, item: User?): EpoxyModel<*> { + return if (item == null) { + EmptyItem_().id(currentPosition) + } else { + val isSelected = selectedUsers.contains(item.userId) + CreateDirectRoomUserItem_() + .id(item.userId) + .selected(isSelected) + .userId(item.userId) + .name(item.displayName) + .avatarUrl(item.avatarUrl) + .avatarRenderer(avatarRenderer) + .clickListener { _ -> + callback?.onItemClick(item) + } + } + } + + override fun addModels(models: List>) { + if (users is Incomplete) { + renderLoading() + } else if (models.isEmpty()) { + renderEmptyState() + } else { + var lastFirstLetter: String? = null + for (model in models) { + if (model is CreateDirectRoomUserItem) { + if (model.userId == session.myUserId) continue + val currentFirstLetter = model.name.firstLetterOfDisplayName() + val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter + lastFirstLetter = currentFirstLetter + + CreateDirectRoomLetterHeaderItem_() + .id(currentFirstLetter) + .letter(currentFirstLetter) + .addIf(showLetter, this) + + model.addTo(this) + } else { + continue + } + } + } + } + + private fun renderLoading() { + loadingItem { + id("loading") + } + } + + private fun renderEmptyState() { + noResultItem { + id("noResult") + text(stringProvider.getString(R.string.direct_room_no_known_users)) + } + } + + interface Callback { + fun onItemClick(user: User) + } + +} \ No newline at end of file