Direct room: finally use PagedList as we can get a lot of users in DB.

This commit is contained in:
ganfra 2019-07-30 14:51:14 +02:00
parent ff6ce8a4b7
commit 6deba31111
16 changed files with 245 additions and 72 deletions

View file

@ -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'

View file

@ -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<PagedList<User>> {
return session.livePagedUsers(filter).asObservable()
}
fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create {
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it)
}

View file

@ -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<List<User>>
/**
* 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<PagedList<User>>
}

View file

@ -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<E : RealmObject>(private val realmConfiguration: RealmConfiguration,
private val realmQueryBuilder: (Realm) -> RealmQuery<E>) {
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<E : RealmObject>(private val realmConfiguration: RealmConf
})
}
handler.post(runnable)
latch.await()
handlerThread.quit()
try {
latch.await(timeout, timeUnit)
} catch (exception: InterruptedException) {
throw exception
} finally {
handlerThread.quit()
}
}

View file

@ -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<CreateRoomParams, String>
@ -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)

View file

@ -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<JoinRoomTask.Params, Unit> {
@ -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)
}

View file

@ -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<UserEntity> by lazy {
monarchy.createDataSourceFactory { realm ->
realm.where(UserEntity::class.java)
.isNotEmpty(UserEntityFields.USER_ID)
.sort(UserEntityFields.DISPLAY_NAME)
}
}
private val domainDataSourceFactory: DataSource.Factory<Int, User> by lazy {
realmDataSourceFactory.map {
it.asDomain()
}
}
private val livePagedListBuilder: LivePagedListBuilder<Int, User> 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<PagedList<User>> {
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<String>,
@ -77,4 +117,4 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
.dispatchTo(callback)
.executeBy(taskExecutor)
}
}
}

View file

@ -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"

View file

@ -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)

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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()) {

View file

@ -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<List<User>, Option<KnowUsersFilter>, List<User>>(
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()

View file

@ -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<List<User>> = Uninitialized,
val knownUsers: Async<PagedList<User>> = Uninitialized,
val directoryUsers: Async<List<User>> = Uninitialized,
val selectedUsers: Set<User> = emptySet(),
val createAndInviteState: Async<String> = Uninitialized,

View file

@ -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<User>,
selectedUsers: List<String>,
hasSearch: Boolean,
isFiltering: Boolean) {
hasSearch: Boolean) {
if (users.isEmpty()) {
renderEmptyState(hasSearch)
} else {
renderUsers(users, selectedUsers, isFiltering)
renderUsers(users, selectedUsers)
}
}
private fun renderUsers(users: List<User>, selectedUsers: List<String>, isFiltering: Boolean) {
var lastFirstLetter: String? = null
private fun renderUsers(users: List<User>, selectedUsers: List<String>) {
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()
}
}

View file

@ -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<User>(
modelBuildingHandler = createUIHandler()
) {
private var selectedUsers: List<String> = emptyList()
private var users: Async<List<User>> = 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<EpoxyModel<*>>) {
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)
}
}