From 838003b68a4f04211fd758d0ea63380dc7cd3223 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 Jul 2019 18:30:14 +0200 Subject: [PATCH 01/14] Create direct room: start creating all the required stuff --- .../java/im/vector/matrix/rx/RxSession.kt | 5 ++ .../android/api/session/user/UserService.kt | 8 ++- .../session/user/DefaultUserService.kt | 14 +++- .../android/internal/util/StringUtils.kt | 8 +++ vector/src/main/AndroidManifest.xml | 1 + .../vector/riotx/core/di/ScreenComponent.kt | 3 + .../vector/riotx/core/di/ViewModelModule.kt | 5 ++ .../riotx/features/home/AvatarRenderer.kt | 8 +-- .../features/home/HomeDetailViewModel.kt | 20 +++--- .../riotx/features/home/HomeDrawerFragment.kt | 2 +- .../createdirect/CreateDirectRoomActivity.kt | 44 ++++++++++++ .../CreateDirectRoomController.kt | 70 +++++++++++++++++++ .../createdirect/CreateDirectRoomFragment.kt | 62 ++++++++++++++++ .../createdirect/CreateDirectRoomUserItem.kt | 56 +++++++++++++++ .../createdirect/CreateDirectRoomViewModel.kt | 59 ++++++++++++++++ .../createdirect/CreateDirectRoomViewState.kt | 28 ++++++++ .../home/room/list/RoomListFragment.kt | 4 +- .../features/navigation/DefaultNavigator.kt | 7 ++ .../riotx/features/navigation/Navigator.kt | 2 + .../layout/fragment_create_direct_room.xml | 14 ++++ .../layout/item_create_direct_room_user.xml | 43 ++++++++++++ 21 files changed, 442 insertions(+), 21 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt create mode 100644 vector/src/main/res/layout/fragment_create_direct_room.xml create mode 100644 vector/src/main/res/layout/item_create_direct_room_user.xml 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 30d31f94..709b28e1 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 @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.sync.SyncState +import im.vector.matrix.android.api.session.user.model.User import io.reactivex.Observable import io.reactivex.schedulers.Schedulers @@ -42,6 +43,10 @@ class RxSession(private val session: Session) { return session.livePushers().asObservable().observeOn(Schedulers.computation()) } + fun liveUsers(): Observable> { + return session.liveUsers().asObservable().observeOn(Schedulers.computation()) + } + } fun Session.rx(): RxSession { 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 eb09fbd2..74dc444f 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 @@ -36,6 +36,12 @@ interface UserService { * @param userId the userId to look for. * @return a Livedata of user with userId */ - fun observeUser(userId: String): LiveData + fun liveUser(userId: String): LiveData + + /** + * Observe a live list of users sorted alphabetically + * @return a Livedata of users + */ + fun liveUsers(): LiveData> } \ No newline at end of file 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 477d5a78..b0fded3b 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 @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.UserEntity +import im.vector.matrix.android.internal.database.model.UserEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.fetchCopied @@ -33,12 +34,12 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona override fun getUser(userId: String): User? { val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } - ?: return null + ?: return null return userEntity.asDomain() } - override fun observeUser(userId: String): LiveData { + override fun liveUser(userId: String): LiveData { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> UserEntity.where(realm, userId) } @@ -48,4 +49,13 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona .firstOrNull() } } + + override fun liveUsers(): LiveData> { + val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> + realm.where(UserEntity::class.java).sort(UserEntityFields.DISPLAY_NAME) + } + return Transformations.map(liveRealmData) { results -> + results.map { it.asDomain() } + } + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt index a83ab013..a2774985 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.util +import im.vector.matrix.android.api.MatrixPatterns import timber.log.Timber /** @@ -49,3 +50,10 @@ fun convertFromUTF8(s: String): String? { null } } + +fun String?.firstLetterOfDisplayName(): String { + if (this.isNullOrEmpty()) return "" + val isUserId = MatrixPatterns.isUserId(this) + val firstLetterIndex = if (isUserId) 1 else 0 + return this[firstLetterIndex].toString().toUpperCase() +} \ No newline at end of file diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e0deced9..e4cdaee2 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -64,6 +64,7 @@ + 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 a42eec49..0ff348b5 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 @@ -36,6 +36,7 @@ import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeDetailFragment import im.vector.riotx.features.home.HomeDrawerFragment import im.vector.riotx.features.home.HomeModule +import im.vector.riotx.features.home.createdirect.CreateDirectRoomFragment 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.* @@ -153,6 +154,8 @@ interface ScreenComponent { fun inject(pushGatewaysFragment: PushGatewaysFragment) + fun inject(createDirectRoomFragment: CreateDirectRoomFragment) + @Component.Factory interface Factory { fun create(vectorComponent: VectorComponent, diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 37abde20..c1daeb86 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -30,6 +30,8 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.riotx.features.crypto.verification.SasVerificationViewModel import im.vector.riotx.features.home.* +import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel +import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory import im.vector.riotx.features.home.group.GroupListViewModel import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.RoomDetailViewModel @@ -158,6 +160,9 @@ interface ViewModelModule { @Binds fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory + @Binds + fun bindCreateDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel_AssistedFactory): CreateDirectRoomViewModel.Factory + @Binds fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index 83829e46..2b6c1eb4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -26,10 +26,10 @@ import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.Target -import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.util.firstLetterOfDisplayName import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideApp @@ -41,7 +41,7 @@ import javax.inject.Inject * This helper centralise ways to retrieve avatar into ImageView or even generic Target */ -class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){ +class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) { companion object { private const val THUMBNAIL_SIZE = 250 @@ -92,9 +92,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active return if (text.isEmpty()) { TextDrawable.builder().buildRound("", avatarColor) } else { - val isUserId = MatrixPatterns.isUserId(text) - val firstLetterIndex = if (isUserId) 1 else 0 - val firstLetter = text[firstLetterIndex].toString().toUpperCase() + val firstLetter = text.firstLetterOfDisplayName() TextDrawable.builder() .beginConfig() .bold() diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index 7f0b610d..917cafe1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -73,21 +73,21 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho .subscribe { list -> list.let { summaries -> val peopleNotifications = summaries - .filter { it.isDirect } - .map { it.notificationCount } - .takeIf { it.isNotEmpty() } - ?.sumBy { i -> i } - ?: 0 + .filter { it.isDirect } + .map { it.notificationCount } + .takeIf { it.isNotEmpty() } + ?.sumBy { i -> i } + ?: 0 val peopleHasHighlight = summaries .filter { it.isDirect } .any { it.highlightCount > 0 } val roomsNotifications = summaries - .filter { !it.isDirect } - .map { it.notificationCount } - .takeIf { it.isNotEmpty() } - ?.sumBy { i -> i } - ?: 0 + .filter { !it.isDirect } + .map { it.notificationCount } + .takeIf { it.isNotEmpty() } + ?.sumBy { i -> i } + ?: 0 val roomsHasHighlight = summaries .filter { !it.isDirect } .any { it.highlightCount > 0 } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index 832e8a5e..9159e32b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -52,7 +52,7 @@ class HomeDrawerFragment : VectorBaseFragment() { replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer) } - session.observeUser(session.sessionParams.credentials.userId).observeK(this) { user -> + session.liveUser(session.sessionParams.credentials.userId).observeK(this) { user -> if (user != null) { avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView) homeDrawerUsernameView.text = user.displayName 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 new file mode 100644 index 00000000..4ecb5f9f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt @@ -0,0 +1,44 @@ +/* + * + * * 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 android.content.Context +import android.content.Intent +import im.vector.riotx.R +import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.platform.VectorBaseActivity + +class CreateDirectRoomActivity : VectorBaseActivity() { + + override fun getLayoutRes() = R.layout.activity_simple + + override fun initUiAndData() { + if (isFirstCreation()) { + addFragment(CreateDirectRoomFragment(), R.id.simpleFragmentContainer) + } + } + + companion object { + fun getIntent(context: Context): Intent { + return Intent(context, CreateDirectRoomActivity::class.java) + } + } + + +} \ No newline at end of file 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/CreateDirectRoomController.kt new file mode 100644 index 00000000..e435e726 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomController.kt @@ -0,0 +1,70 @@ +/* + * + * * 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.EpoxyController +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.util.firstLetterOfDisplayName +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +class CreateDirectRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer) : EpoxyController() { + + private var state: CreateDirectRoomViewState? = null + var callback: Callback? = null + + init { + requestModelBuild() + } + + fun setData(state: CreateDirectRoomViewState) { + this.state = state + requestModelBuild() + } + + override fun buildModels() { + val currentState = state ?: return + val knownUsers = currentState.knownUsers() ?: return + + var lastFirstLetter: String? = null + knownUsers.forEach { user -> + val currentFirstLetter = user.displayName.firstLetterOfDisplayName() + val showLetter = lastFirstLetter != currentFirstLetter + lastFirstLetter = currentFirstLetter + createDirectRoomUserItem { + id(user.userId) + userId(user.userId) + showLetter(showLetter) + firstLetter(currentFirstLetter) + name(user.displayName) + avatarUrl(user.avatarUrl) + avatarRenderer(avatarRenderer) + clickListener { _ -> + callback?.onItemClick(user) + } + } + } + + } + + interface Callback { + fun onItemClick(user: User) + } + +} \ No newline at end of file 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/CreateDirectRoomFragment.kt new file mode 100644 index 00000000..f71998cb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomFragment.kt @@ -0,0 +1,62 @@ +/* + * + * * 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 android.os.Bundle +import com.airbnb.mvrx.fragmentViewModel +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_create_direct_room.* +import javax.inject.Inject + +class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomController.Callback { + + override fun getLayoutResId() = R.layout.fragment_create_direct_room + + private val viewModel: CreateDirectRoomViewModel by fragmentViewModel() + + @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory + @Inject lateinit var directRoomController: CreateDirectRoomController + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + setupRecyclerView() + viewModel.subscribe(this) { renderState(it) } + } + + private fun setupRecyclerView() { + recyclerView.setHasFixedSize(true) + directRoomController.callback = this + recyclerView.setController(directRoomController) + } + + private fun renderState(state: CreateDirectRoomViewState) { + directRoomController.setData(state) + } + + override fun onItemClick(user: User) { + vectorBaseActivity.notImplemented("IMPLEMENT ON USER CLICKED") + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt new file mode 100644 index 00000000..c3b2d4f5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt @@ -0,0 +1,56 @@ +/* + * + * * 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 android.view.View +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_create_direct_room_user) +abstract class CreateDirectRoomUserItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute var showLetter: Boolean = false + @EpoxyAttribute var firstLetter: String = "" + @EpoxyAttribute var name: String? = null + @EpoxyAttribute var userId: String = "" + @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute var clickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + holder.view.setOnClickListener(clickListener) + holder.nameView.text = name + holder.letterView.visibility = if (showLetter) View.VISIBLE else View.INVISIBLE + holder.letterView.text = firstLetter + avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView) + } + + class Holder : VectorEpoxyHolder() { + val letterView by bind(R.id.createDirectRoomUserLetter) + val nameView by bind(R.id.createDirectRoomUserName) + val avatarImageView by bind(R.id.createDirectRoomUserAvatar) + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..ce40bc0a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt @@ -0,0 +1,59 @@ +/* + * + * * 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.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.rx.rx +import im.vector.riotx.core.platform.VectorViewModel + +class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted + initialState: CreateDirectRoomViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? { + val fragment: CreateDirectRoomFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.createDirectRoomViewModelFactory.create(state) + } + } + + init { + observeKnownUsers() + } + + private fun observeKnownUsers() { + session.rx().liveUsers().execute { + this.copy(knownUsers = it) + } + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..18b65357 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt @@ -0,0 +1,28 @@ +/* + * + * * 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.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.user.model.User + +data class CreateDirectRoomViewState( + val knownUsers: Async> = Uninitialized +) : MvRxState \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index 491b0f4e..935ae689 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -144,7 +144,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O } override fun createDirectChat() { - vectorBaseActivity.notImplemented("creating direct chat") + navigator.openCreateDirectRoom(requireActivity()) } private fun setupRecyclerView() { @@ -248,7 +248,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O return super.onBackPressed() } -// RoomSummaryController.Callback ************************************************************** + // RoomSummaryController.Callback ************************************************************** override fun onRoomSelected(room: RoomSummary) { roomListViewModel.accept(RoomListActions.SelectRoom(room)) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index c9af93a3..8b8476e0 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -25,6 +25,8 @@ import im.vector.riotx.core.utils.toast import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.riotx.features.debug.DebugMenuActivity +import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity +import im.vector.riotx.features.home.createdirect.CreateDirectRoomFragment import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity @@ -68,6 +70,11 @@ class DefaultNavigator @Inject constructor() : Navigator { context.startActivity(intent) } + override fun openCreateDirectRoom(context: Context) { + val intent = CreateDirectRoomActivity.getIntent(context) + context.startActivity(intent) + } + override fun openRoomsFiltering(context: Context) { val intent = FilteredRoomsActivity.newIntent(context) context.startActivity(intent) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 48f77118..7c5a8c0a 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -29,6 +29,8 @@ interface Navigator { fun openCreateRoom(context: Context) + fun openCreateDirectRoom(context: Context) + fun openRoomDirectory(context: Context) fun openRoomsFiltering(context: Context) diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml new file mode 100644 index 00000000..b683d033 --- /dev/null +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/vector/src/main/res/layout/item_create_direct_room_user.xml b/vector/src/main/res/layout/item_create_direct_room_user.xml new file mode 100644 index 00000000..a078a5ee --- /dev/null +++ b/vector/src/main/res/layout/item_create_direct_room_user.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file From 001603cf9acb7a014536034a2d3978aa1853a129 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 18 Jul 2019 17:42:22 +0200 Subject: [PATCH 02/14] Create direct room: add filtering and enhance design a bit --- .../im/vector/matrix/rx/LiveDataObservable.kt | 4 +- .../main/java/im/vector/matrix/rx/RxRoom.kt | 8 +- .../java/im/vector/matrix/rx/RxSession.kt | 11 +- .../session/user/DefaultUserService.kt | 18 +-- .../createdirect/CreateDirectRoomActions.kt | 26 ++++ .../CreateDirectRoomController.kt | 10 +- .../createdirect/CreateDirectRoomFragment.kt | 33 +++++ .../CreateDirectRoomLetterHeaderItem.kt | 39 ++++++ .../createdirect/CreateDirectRoomUserItem.kt | 16 ++- .../createdirect/CreateDirectRoomViewModel.kt | 49 +++++++- .../createdirect/CreateDirectRoomViewState.kt | 13 +- .../layout/fragment_create_direct_room.xml | 113 ++++++++++++++++-- .../item_create_direct_room_letter_header.xml | 14 +++ .../layout/item_create_direct_room_user.xml | 41 ++++--- .../res/menu/vector_create_direct_room.xml | 12 ++ vector/src/main/res/values/strings_riotX.xml | 2 + 16 files changed, 357 insertions(+), 52 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt create mode 100644 vector/src/main/res/layout/item_create_direct_room_letter_header.xml create mode 100755 vector/src/main/res/menu/vector_create_direct_room.xml diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt index d4c9a79f..a1943bbe 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt @@ -20,6 +20,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import io.reactivex.Observable import io.reactivex.android.MainThreadDisposable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers private class LiveDataObservable( private val liveData: LiveData, @@ -57,5 +59,5 @@ private class LiveDataObservable( } fun LiveData.asObservable(): Observable { - return LiveDataObservable(this) + return LiveDataObservable(this).observeOn(Schedulers.computation()) } \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 2c9c7d8b..a01a99f5 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -26,19 +26,19 @@ import io.reactivex.schedulers.Schedulers class RxRoom(private val room: Room) { fun liveRoomSummary(): Observable { - return room.liveRoomSummary().asObservable().observeOn(Schedulers.computation()) + return room.liveRoomSummary().asObservable() } fun liveRoomMemberIds(): Observable> { - return room.getRoomMemberIdsLive().asObservable().observeOn(Schedulers.computation()) + return room.getRoomMemberIdsLive().asObservable() } fun liveAnnotationSummary(eventId: String): Observable { - return room.getEventSummaryLive(eventId).asObservable().observeOn(Schedulers.computation()) + return room.getEventSummaryLive(eventId).asObservable() } fun liveTimelineEvent(eventId: String): Observable { - return room.liveTimeLineEvent(eventId).asObservable().observeOn(Schedulers.computation()) + return room.liveTimeLineEvent(eventId).asObservable() } } 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 709b28e1..76724939 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 @@ -23,28 +23,29 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers class RxSession(private val session: Session) { fun liveRoomSummaries(): Observable> { - return session.liveRoomSummaries().asObservable().observeOn(Schedulers.computation()) + return session.liveRoomSummaries().asObservable() } fun liveGroupSummaries(): Observable> { - return session.liveGroupSummaries().asObservable().observeOn(Schedulers.computation()) + return session.liveGroupSummaries().asObservable() } fun liveSyncState(): Observable { - return session.syncState().asObservable().observeOn(Schedulers.computation()) + return session.syncState().asObservable() } fun livePushers(): Observable> { - return session.livePushers().asObservable().observeOn(Schedulers.computation()) + return session.livePushers().asObservable() } fun liveUsers(): Observable> { - return session.liveUsers().asObservable().observeOn(Schedulers.computation()) + return session.liveUsers().asObservable() } } 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 b0fded3b..7eaf966a 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 @@ -23,6 +23,8 @@ import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.model.UserEntityFields import im.vector.matrix.android.internal.database.query.where @@ -34,7 +36,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona override fun getUser(userId: String): User? { val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } - ?: return null + ?: return null return userEntity.asDomain() } @@ -51,11 +53,13 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona } override fun liveUsers(): LiveData> { - val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> - realm.where(UserEntity::class.java).sort(UserEntityFields.DISPLAY_NAME) - } - return Transformations.map(liveRealmData) { results -> - results.map { it.asDomain() } - } + return monarchy.findAllMappedWithChanges( + { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + }, + { it.asDomain() } + ) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt new file mode 100644 index 00000000..a44be66a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt @@ -0,0 +1,26 @@ +/* + * 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 + +sealed class CreateDirectRoomActions { + + object CreateRoomAndInviteSelectedUsers : CreateDirectRoomActions() + data class FilterKnownUsers(val value: String) : CreateDirectRoomActions() + object ClearFilterKnownUsers: CreateDirectRoomActions() + object SelectAddByMatrixId : CreateDirectRoomActions() + +} 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/CreateDirectRoomController.kt index e435e726..af7de9c6 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/CreateDirectRoomController.kt @@ -45,13 +45,17 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer: var lastFirstLetter: String? = null knownUsers.forEach { user -> val currentFirstLetter = user.displayName.firstLetterOfDisplayName() - val showLetter = lastFirstLetter != currentFirstLetter + val showLetter = currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter lastFirstLetter = currentFirstLetter + + CreateDirectRoomLetterHeaderItem_() + .id(currentFirstLetter) + .letter(currentFirstLetter) + .addIf(showLetter, this) + createDirectRoomUserItem { id(user.userId) userId(user.userId) - showLetter(showLetter) - firstLetter(currentFirstLetter) name(user.displayName) avatarUrl(user.avatarUrl) avatarRenderer(avatarRenderer) 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/CreateDirectRoomFragment.kt index f71998cb..eedb5617 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/CreateDirectRoomFragment.kt @@ -19,18 +19,23 @@ package im.vector.riotx.features.home.createdirect import android.os.Bundle +import android.view.MenuItem import com.airbnb.mvrx.fragmentViewModel +import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_create_direct_room.* +import java.util.concurrent.TimeUnit import javax.inject.Inject class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomController.Callback { override fun getLayoutResId() = R.layout.fragment_create_direct_room + override fun getMenuRes() = R.menu.vector_create_direct_room + private val viewModel: CreateDirectRoomViewModel by fragmentViewModel() @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory @@ -43,15 +48,43 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setupRecyclerView() + setupFilterView() viewModel.subscribe(this) { renderState(it) } } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_create_room -> { + viewModel.handle(CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers) + true + } + else -> + super.onOptionsItemSelected(item) + } + } + private fun setupRecyclerView() { recyclerView.setHasFixedSize(true) + // Don't activate animation as we might have way to much item animation when filtering + recyclerView.itemAnimator = null directRoomController.callback = this recyclerView.setController(directRoomController) } + private fun setupFilterView() { + createDirectRoomFilter + .queryTextChanges() + .subscribe { + val action = if (it.isNullOrEmpty()) { + CreateDirectRoomActions.ClearFilterKnownUsers + } else { + CreateDirectRoomActions.FilterKnownUsers(it.toString()) + } + viewModel.handle(action) + } + .disposeOnDestroy() + } + private fun renderState(state: CreateDirectRoomViewState) { directRoomController.setData(state) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt new file mode 100644 index 00000000..fcb3b10c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt @@ -0,0 +1,39 @@ +/* + * 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 android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_create_direct_room_letter_header) +abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel() { + + @EpoxyAttribute var letter: String = "" + + override fun bind(holder: Holder) { + holder.letterView.text = letter + } + + class Holder : VectorEpoxyHolder() { + val letterView by bind(R.id.createDirectRoomLetterView) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt index c3b2d4f5..57d9347e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt @@ -32,8 +32,6 @@ import im.vector.riotx.features.home.AvatarRenderer abstract class CreateDirectRoomUserItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer - @EpoxyAttribute var showLetter: Boolean = false - @EpoxyAttribute var firstLetter: String = "" @EpoxyAttribute var name: String? = null @EpoxyAttribute var userId: String = "" @EpoxyAttribute var avatarUrl: String? = null @@ -41,14 +39,20 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel(R.id.createDirectRoomUserLetter) + val userIdView by bind(R.id.createDirectRoomUserID) val nameView by bind(R.id.createDirectRoomUserName) val avatarImageView by bind(R.id.createDirectRoomUserAvatar) } 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 ce40bc0a..c9b976cd 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 @@ -18,14 +18,23 @@ package im.vector.riotx.features.home.createdirect +import arrow.core.Option import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext +import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.rx.rx import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.Observable +import io.reactivex.functions.BiFunction +import io.reactivex.subjects.BehaviorSubject +import java.util.concurrent.TimeUnit + +private typealias KnowUsersFilter = String class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateDirectRoomViewState, @@ -37,6 +46,8 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel } + private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) + companion object : MvRxViewModelFactory { @JvmStatic @@ -50,10 +61,42 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted observeKnownUsers() } - private fun observeKnownUsers() { - session.rx().liveUsers().execute { - this.copy(knownUsers = it) + fun handle(createDirectRoomActions: CreateDirectRoomActions) { + when (createDirectRoomActions) { + is CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() + is CreateDirectRoomActions.SelectAddByMatrixId -> handleSelectAddByMatrixId() + is CreateDirectRoomActions.FilterKnownUsers -> knownUsersFilter.accept(Option.just(createDirectRoomActions.value)) + is CreateDirectRoomActions.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) } } + private fun handleSelectAddByMatrixId() { + // TODO + } + + private fun createRoomAndInviteSelectedUsers() { + // TODO + } + + 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 -> + copy(knownUsers = async) + } + } + } \ No newline at end of file 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 18b65357..1ad1f888 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 @@ -24,5 +24,14 @@ import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.user.model.User data class CreateDirectRoomViewState( - val knownUsers: Async> = Uninitialized -) : MvRxState \ No newline at end of file + val displayMode: DisplayMode = DisplayMode.KNOWN_USERS, + val knownUsers: Async> = Uninitialized, + val filteredKnownUsers: Async> = Uninitialized +) : MvRxState { + + enum class DisplayMode { + KNOWN_USERS, + MATRIX_ID_USERS + } + +} \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml index b683d033..78955389 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -1,14 +1,113 @@ - - + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/vector/src/main/res/layout/item_create_direct_room_letter_header.xml b/vector/src/main/res/layout/item_create_direct_room_letter_header.xml new file mode 100644 index 00000000..80a0fc4c --- /dev/null +++ b/vector/src/main/res/layout/item_create_direct_room_letter_header.xml @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_create_direct_room_user.xml b/vector/src/main/res/layout/item_create_direct_room_user.xml index a078a5ee..de1afd5f 100644 --- a/vector/src/main/res/layout/item_create_direct_room_user.xml +++ b/vector/src/main/res/layout/item_create_direct_room_user.xml @@ -1,7 +1,8 @@ - - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/vector_create_direct_room.xml b/vector/src/main/res/menu/vector_create_direct_room.xml new file mode 100755 index 00000000..42a21da9 --- /dev/null +++ b/vector/src/main/res/menu/vector_create_direct_room.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index f6ec4892..5de5c81b 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -39,4 +39,6 @@ Link copied to clipboard + Add by matrix ID + \ No newline at end of file From cb274d6a33f5f7b295c2c35eba8b819e7fd0f5a2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 19 Jul 2019 11:21:16 +0200 Subject: [PATCH 03/14] Add some cancelable on service methods and start branching Rx --- .../matrix/rx/MatrixCallbackCompletable.kt | 39 +++++++++++++++++++ .../vector/matrix/rx/MatrixCallbackSingle.kt | 38 ++++++++++++++++++ .../main/java/im/vector/matrix/rx/RxRoom.kt | 6 ++- .../java/im/vector/matrix/rx/RxSession.kt | 9 ++++- .../android/api/pushrules/PushRuleService.kt | 3 +- .../api/session/room/RoomDirectoryService.kt | 9 ++--- .../android/api/session/room/RoomService.kt | 6 +-- .../session/room/members/MembershipService.kt | 8 ++-- .../notification/DefaultPushRuleService.kt | 5 ++- .../room/DefaultRoomDirectoryService.kt | 8 ++-- .../session/room/DefaultRoomService.kt | 5 ++- .../membership/DefaultMembershipService.kt | 18 +++++---- 12 files changed, 121 insertions(+), 33 deletions(-) create mode 100644 matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt create mode 100644 matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt new file mode 100644 index 00000000..5c6e7498 --- /dev/null +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt @@ -0,0 +1,39 @@ +/* + * 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.matrix.rx + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable +import io.reactivex.CompletableEmitter +import io.reactivex.SingleEmitter + +internal class MatrixCallbackCompletable(private val completableEmitter: CompletableEmitter) : MatrixCallback { + + override fun onSuccess(data: T) { + completableEmitter.onComplete() + } + + override fun onFailure(failure: Throwable) { + completableEmitter.onError(failure) + } +} + +fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) { + completableEmitter.setCancellable { + this.cancel() + } +} \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt new file mode 100644 index 00000000..05756d49 --- /dev/null +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt @@ -0,0 +1,38 @@ +/* + * 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.matrix.rx + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable +import io.reactivex.SingleEmitter + +internal class MatrixCallbackSingle(private val singleEmitter: SingleEmitter) : MatrixCallback { + + override fun onSuccess(data: T) { + singleEmitter.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + singleEmitter.onError(failure) + } +} + +fun Cancelable.toSingle(singleEmitter: SingleEmitter) { + singleEmitter.setCancellable { + this.cancel() + } +} \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index a01a99f5..419fb6c9 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers +import io.reactivex.Single class RxRoom(private val room: Room) { @@ -41,6 +41,10 @@ class RxRoom(private val room: Room) { return room.liveTimeLineEvent(eventId).asObservable() } + fun loadRoomMembersIfNeeded(): Single = Single.create { + room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) + } + } fun Room.rx(): RxRoom { 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 76724939..32d7ab54 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 @@ -20,11 +20,12 @@ 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 import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User +import io.reactivex.Completable import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import io.reactivex.Single class RxSession(private val session: Session) { @@ -48,6 +49,10 @@ class RxSession(private val session: Session) { return session.liveUsers().asObservable() } + fun createRoom(roomParams: CreateRoomParams): Single = Single.create { + session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it) + } + } fun Session.rx(): RxSession { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt index b00450b5..9a953ffc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.pushrules import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.util.Cancelable interface PushRuleService { @@ -31,7 +32,7 @@ interface PushRuleService { //TODO update rule - fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback) + fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable fun addPushRuleListener(listener: PushRuleListener) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt index 5b66ddd8..272ab567 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt @@ -30,20 +30,17 @@ interface RoomDirectoryService { /** * Get rooms from directory */ - fun getPublicRooms(server: String?, - publicRoomsParams: PublicRoomsParams, - callback: MatrixCallback): Cancelable + fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback): Cancelable /** * Join a room by id */ - fun joinRoom(roomId: String, - callback: MatrixCallback) + fun joinRoom(roomId: String, callback: MatrixCallback): Cancelable /** * Fetches the overall metadata about protocols supported by the homeserver. * Includes both the available protocols and all fields required for queries against each protocol. */ - fun getThirdPartyProtocol(callback: MatrixCallback>) + fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index fc0bf499..837ea5bb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams +import im.vector.matrix.android.api.util.Cancelable /** * This interface defines methods to get rooms. It's implemented at the session level. @@ -27,10 +28,9 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams interface RoomService { /** - * Create a room + * Create a room asynchronously */ - fun createRoom(createRoomParams: CreateRoomParams, - callback: MatrixCallback) + fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable /** * Get a room from a roomId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index ca3b99b6..870c1075 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -30,7 +30,7 @@ interface MembershipService { * This methods load all room members if it was done yet. * @return a [Cancelable] */ - fun loadRoomMembersIfNeeded(): Cancelable + fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable /** * Return the roomMember with userId or null. @@ -52,16 +52,16 @@ interface MembershipService { /** * Invite a user in the room */ - fun invite(userId: String, callback: MatrixCallback) + fun invite(userId: String, callback: MatrixCallback): Cancelable /** * Join the room, or accept an invitation. */ - fun join(callback: MatrixCallback) + fun join(callback: MatrixCallback): Cancelable /** * Leave the room, or reject an invitation. */ - fun leave(callback: MatrixCallback) + fun leave(callback: MatrixCallback): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt index 5b21f0e5..ed9470b2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.model.PushRulesEntity import im.vector.matrix.android.internal.database.model.PusherEntityFields @@ -82,8 +83,8 @@ internal class DefaultPushRuleService @Inject constructor( return contentRules + overrideRules + roomRules + senderRules + underrideRules } - override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback) { - updatePushRuleEnableStatusTask + override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable { + return updatePushRuleEnableStatusTask .configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) // TODO Fetch the rules .dispatchTo(callback) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt index 0b13fa3c..158802f8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt @@ -44,15 +44,15 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu .executeBy(taskExecutor) } - override fun joinRoom(roomId: String, callback: MatrixCallback) { - joinRoomTask + override fun joinRoom(roomId: String, callback: MatrixCallback): Cancelable { + return joinRoomTask .configureWith(JoinRoomTask.Params(roomId)) .dispatchTo(callback) .executeBy(taskExecutor) } - override fun getThirdPartyProtocol(callback: MatrixCallback>) { - getThirdPartyProtocolsTask + override fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable { + return getThirdPartyProtocolsTask .toConfigurableTask() .dispatchTo(callback) .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 2c0f1ce9..7b548368 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams +import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity @@ -40,8 +41,8 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona private val roomFactory: RoomFactory, private val taskExecutor: TaskExecutor) : RoomService { - override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback) { - createRoomTask + override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { + return createRoomTask .configureWith(createRoomParams) .dispatchTo(callback) .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 01fb7461..405c1ad6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -42,9 +42,11 @@ internal class DefaultMembershipService @Inject constructor(private val roomId: private val leaveRoomTask: LeaveRoomTask ) : MembershipService { - override fun loadRoomMembersIfNeeded(): Cancelable { + override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable { val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) - return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor) + return loadRoomMembersTask.configureWith(params) + .dispatchTo(matrixCallback) + .executeBy(taskExecutor) } override fun getRoomMember(userId: String): RoomMember? { @@ -73,23 +75,23 @@ internal class DefaultMembershipService @Inject constructor(private val roomId: return result } - override fun invite(userId: String, callback: MatrixCallback) { + override fun invite(userId: String, callback: MatrixCallback): Cancelable { val params = InviteTask.Params(roomId, userId) - inviteTask.configureWith(params) + return inviteTask.configureWith(params) .dispatchTo(callback) .executeBy(taskExecutor) } - override fun join(callback: MatrixCallback) { + override fun join(callback: MatrixCallback): Cancelable { val params = JoinRoomTask.Params(roomId) - joinTask.configureWith(params) + return joinTask.configureWith(params) .dispatchTo(callback) .executeBy(taskExecutor) } - override fun leave(callback: MatrixCallback) { + override fun leave(callback: MatrixCallback): Cancelable { val params = LeaveRoomTask.Params(roomId) - leaveRoomTask.configureWith(params) + return leaveRoomTask.configureWith(params) .dispatchTo(callback) .executeBy(taskExecutor) } From cb44ab547cd314feb31f97bd21fff6116be20b36 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 19 Jul 2019 18:12:42 +0200 Subject: [PATCH 04/14] Create direct room: almost finished, still need to handle showing selected users in search field --- .../java/im/vector/matrix/rx/RxSession.kt | 7 +- .../android/api/session/user/UserService.kt | 12 +++ .../session/user/DefaultUserService.kt | 24 ++++- .../internal/session/user/SearchUserAPI.kt | 35 ++++++ .../internal/session/user/UserModule.kt | 19 ++++ .../internal/session/user/model/SearchUser.kt | 27 +++++ .../session/user/model/SearchUserTask.kt | 47 ++++++++ .../session/user/model/SearchUsersParams.kt | 31 ++++++ .../session/user/model/SearchUsersResponse.kt | 14 +++ .../vector/riotx/core/di/ScreenComponent.kt | 11 +- .../vector/riotx/core/di/ViewModelModule.kt | 6 ++ .../core/platform/SimpleFragmentActivity.kt | 2 + .../riotx/core/platform/VectorBaseActivity.kt | 22 ++++ .../riotx/core/platform/VectorBaseFragment.kt | 2 +- .../riotx/core/utils/DefaultSubscriber.kt | 32 ++++++ .../settings/KeysBackupManageActivity.kt | 1 + .../riotx/features/home/HomeActivity.kt | 17 --- .../createdirect/CreateDirectRoomActions.kt | 8 +- .../createdirect/CreateDirectRoomActivity.kt | 74 ++++++++++++- .../CreateDirectRoomController.kt | 50 ++++++++- .../CreateDirectRoomDirectoryUsersFragment.kt | 93 ++++++++++++++++ .../createdirect/CreateDirectRoomFragment.kt | 38 +++++-- .../CreateDirectRoomNavigationViewModel.kt | 22 ++++ .../createdirect/CreateDirectRoomUserItem.kt | 16 ++- .../createdirect/CreateDirectRoomViewModel.kt | 67 +++++++++--- .../createdirect/CreateDirectRoomViewState.kt | 7 +- .../home/room/detail/RoomDetailViewModel.kt | 11 +- .../features/navigation/DefaultNavigator.kt | 1 - .../layout/fragment_create_direct_room.xml | 12 ++- ...ent_create_direct_room_directory_users.xml | 100 ++++++++++++++++++ .../layout/item_create_direct_room_user.xml | 29 +++-- .../res/menu/vector_create_direct_room.xml | 6 +- 32 files changed, 766 insertions(+), 77 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUser.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUserTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersParams.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersResponse.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/DefaultSubscriber.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt create mode 100644 vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml 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 32d7ab54..4126ff6f 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 @@ -23,7 +23,6 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User -import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.Single @@ -53,6 +52,12 @@ class RxSession(private val session: Session) { session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it) } + fun searchUsersDirectory(search: String, + limit: Int, + excludedUserIds: Set): Single> = Single.create { + session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) + } + } fun Session.rx(): RxSession { 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 74dc444f..292c90ef 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,7 +17,9 @@ package im.vector.matrix.android.api.session.user import androidx.lifecycle.LiveData +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 /** * This interface defines methods to get users. It's implemented at the session level. @@ -31,6 +33,16 @@ interface UserService { */ fun getUser(userId: String): User? + /** + * Search list of users on server directory. + * @param search the searched term + * @param limit the max number of users to return + * @param excludedUserIds the user ids to filter from the search + * @param callback the async callback + * @return Cancelable + */ + fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set, callback: MatrixCallback>): Cancelable + /** * Observe a live user from a userId * @param userId the userId to look for. 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 7eaf966a..1db73c84 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 @@ -19,20 +19,25 @@ package im.vector.matrix.android.internal.session.user import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.model.UserEntityFields import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.user.model.SearchUserTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.task.toConfigurableTask import im.vector.matrix.android.internal.util.fetchCopied import javax.inject.Inject -internal class DefaultUserService @Inject constructor(private val monarchy: Monarchy) : UserService { +internal class DefaultUserService @Inject constructor(private val monarchy: Monarchy, + private val searchUserTask: SearchUserTask, + private val taskExecutor: TaskExecutor) : UserService { override fun getUser(userId: String): User? { val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } @@ -62,4 +67,15 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona { it.asDomain() } ) } + + override fun searchUsersDirectory(search: String, + limit: Int, + excludedUserIds: Set, + callback: MatrixCallback>): Cancelable { + val params = SearchUserTask.Params(limit, search, excludedUserIds) + return searchUserTask + .configureWith(params) + .dispatchTo(callback) + .executeBy(taskExecutor) + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt new file mode 100644 index 00000000..aa4d50df --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt @@ -0,0 +1,35 @@ +/* + * 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.matrix.android.internal.session.user + +import im.vector.matrix.android.internal.network.NetworkConstants.URI_API_PREFIX_PATH_R0 +import im.vector.matrix.android.internal.session.user.model.SearchUsersParams +import im.vector.matrix.android.internal.session.user.model.SearchUsersRequestResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +internal interface SearchUserAPI { + + /** + * Perform a user search. + * + * @param searchUsersParams the search params. + */ + @POST(URI_API_PREFIX_PATH_R0 + "user_directory/search") + fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt index 00368dfa..46ae4e38 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt @@ -18,12 +18,31 @@ package im.vector.matrix.android.internal.session.user import dagger.Binds import dagger.Module +import dagger.Provides import im.vector.matrix.android.api.session.user.UserService +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.sync.SyncAPI +import im.vector.matrix.android.internal.session.user.model.DefaultSearchUserTask +import im.vector.matrix.android.internal.session.user.model.SearchUserTask +import retrofit2.Retrofit @Module internal abstract class UserModule { + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSearchUserAPI(retrofit: Retrofit): SearchUserAPI { + return retrofit.create(SearchUserAPI::class.java) + } + } + @Binds abstract fun bindUserService(userService: DefaultUserService): UserService + @Binds + abstract fun bindSearchUserTask(searchUserTask: DefaultSearchUserTask): SearchUserTask + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUser.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUser.kt new file mode 100644 index 00000000..da447830 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUser.kt @@ -0,0 +1,27 @@ +/* + * 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.matrix.android.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SearchUser( + @Json(name = "user_id") val userId: String, + @Json(name = "display_name") val displayName: String? = null, + @Json(name = "avatar_url") val avatarUrl: String? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUserTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUserTask.kt new file mode 100644 index 00000000..85264dba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUserTask.kt @@ -0,0 +1,47 @@ +/* + * 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.matrix.android.internal.session.user.model + +import arrow.core.Try +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.user.SearchUserAPI +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface SearchUserTask : Task> { + + data class Params( + val limit: Int, + val search: String, + val excludedUserIds: Set + ) +} + +internal class DefaultSearchUserTask @Inject constructor(private val searchUserAPI: SearchUserAPI) : SearchUserTask { + + override suspend fun execute(params: SearchUserTask.Params): Try> { + return executeRequest { + apiCall = searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit)) + }.map { response -> + response.users.map { + User(it.userId, it.displayName, it.avatarUrl) + } + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersParams.kt new file mode 100644 index 00000000..6ea689e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersParams.kt @@ -0,0 +1,31 @@ +/* + * 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.matrix.android.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an user search parameters + */ +@JsonClass(generateAdapter = true) +internal data class SearchUsersParams( + // the searched term + @Json(name = "search_term") val searchTerm: String, + // set a limit to the request response + @Json(name = "limit") val limit: Int +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersResponse.kt new file mode 100644 index 00000000..b0a8f937 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersResponse.kt @@ -0,0 +1,14 @@ +package im.vector.matrix.android.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an users search response + */ +@JsonClass(generateAdapter = true) +internal data class SearchUsersRequestResponse( + @Json(name = "limited") val limited: Boolean = false, + @Json(name = "results") val users: List = emptyList() +) + 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 0ff348b5..90284011 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 @@ -36,6 +36,8 @@ import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeDetailFragment 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.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment @@ -46,6 +48,7 @@ import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity +import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.RageShake @@ -74,6 +77,8 @@ interface ScreenComponent { fun rageShake(): RageShake + fun navigator(): Navigator + fun inject(activity: HomeActivity) fun inject(roomDetailFragment: RoomDetailFragment) @@ -154,7 +159,11 @@ interface ScreenComponent { fun inject(pushGatewaysFragment: PushGatewaysFragment) - fun inject(createDirectRoomFragment: CreateDirectRoomFragment) + fun inject(createDirectRoomKnownUsersFragment: CreateDirectRoomFragment) + + fun inject(createDirectRoomDirectoryUsersFragment: CreateDirectRoomDirectoryUsersFragment) + + fun inject(createDirectRoomActivity: CreateDirectRoomActivity) @Component.Factory interface Factory { diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index c21a6c8f..80410f87 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -30,6 +30,7 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.riotx.features.crypto.verification.SasVerificationViewModel import im.vector.riotx.features.home.* +import im.vector.riotx.features.home.createdirect.CreateDirectRoomNavigationViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory import im.vector.riotx.features.home.group.GroupListViewModel @@ -118,6 +119,11 @@ interface ViewModelModule { @ViewModelKey(ConfigurationViewModel::class) fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(CreateDirectRoomNavigationViewModel::class) + fun bindCreateDirectRoomNavigationViewModel(viewModel: CreateDirectRoomNavigationViewModel): ViewModel + /** * Below are bindings for the MvRx view models (which extend VectorViewModel). Will be the only usage in the future. */ diff --git a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt index 546937da..ff301389 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt @@ -18,6 +18,7 @@ package im.vector.riotx.core.platform import android.view.View import android.widget.ProgressBar import android.widget.TextView +import androidx.annotation.CallSuper import androidx.core.view.isGone import androidx.core.view.isVisible import butterknife.BindView @@ -46,6 +47,7 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { @Inject lateinit var session: Session + @CallSuper override fun injectWith(injector: ScreenComponent) { session = injector.session() } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 92f72de6..e9c60942 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -26,6 +26,7 @@ import androidx.annotation.* import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders @@ -40,6 +41,7 @@ import im.vector.riotx.R import im.vector.riotx.core.di.* import im.vector.riotx.core.utils.toast import im.vector.riotx.features.configuration.VectorConfiguration +import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.RageShake @@ -70,6 +72,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { private lateinit var configurationViewModel: ConfigurationViewModel protected lateinit var bugReporter: BugReporter private lateinit var rageShake: RageShake + protected lateinit var navigator: Navigator private var unBinder: Unbinder? = null @@ -121,6 +124,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { configurationViewModel = ViewModelProviders.of(this, viewModelFactory).get(ConfigurationViewModel::class.java) bugReporter = screenComponent.bugReporter() rageShake = screenComponent.rageShake() + navigator = screenComponent.navigator() configurationViewModel.activityRestarter.observe(this, Observer { if (!it.hasBeenHandled) { // Recreate the Activity because configuration has changed @@ -262,6 +266,24 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { return super.onOptionsItemSelected(item) } + protected fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { + // if (fm.backStackEntryCount == 0) + // return false + + val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed() + for (f in reverseOrder) { + val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager) + if (handledByChildFragments) { + return true + } + val backPressable = f as OnBackPressed + if (backPressable.onBackPressed()) { + return true + } + } + return false + } + /* ========================================================================================== * PROTECTED METHODS * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index ec5e419d..aac19d80 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -65,7 +65,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed, HasScreen override fun onAttach(context: Context) { screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) - navigator = vectorBaseActivity.getVectorComponent().navigator() + navigator = screenComponent.navigator() viewModelFactory = screenComponent.viewModelFactory() injectWith(injector()) super.onAttach(context) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DefaultSubscriber.kt b/vector/src/main/java/im/vector/riotx/core/utils/DefaultSubscriber.kt new file mode 100644 index 00000000..05415991 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/DefaultSubscriber.kt @@ -0,0 +1,32 @@ +/* + * 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.core.utils + +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.disposables.Disposable +import io.reactivex.functions.Consumer +import io.reactivex.internal.functions.Functions +import timber.log.Timber + +fun Single.subscribeLogError(): Disposable { + return subscribe(Functions.emptyConsumer(), Consumer { Timber.e(it) }) +} + +fun Completable.subscribeLogError(): Disposable { + return subscribe({}, { Timber.e(it) }) +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt index 32e08597..69ad2cd1 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt @@ -43,6 +43,7 @@ class KeysBackupManageActivity : SimpleFragmentActivity() { @Inject lateinit var keysBackupSettingsViewModelFactory: KeysBackupSettingsViewModel.Factory override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) injector.inject(this) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 4ec2c0ad..07d9416c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -65,7 +65,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory @Inject lateinit var homeNavigator: HomeNavigator - @Inject lateinit var navigator: Navigator @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var pushManager: PushersManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @@ -214,23 +213,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { } } - private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { - // if (fm.backStackEntryCount == 0) - // return false - val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed() - for (f in reverseOrder) { - val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager) - if (handledByChildFragments) { - return true - } - val backPressable = f as OnBackPressed - if (backPressable.onBackPressed()) { - return true - } - } - return false - } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt index a44be66a..50f99a6d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt @@ -16,11 +16,15 @@ package im.vector.riotx.features.home.createdirect +import im.vector.matrix.android.api.session.user.model.User + sealed class CreateDirectRoomActions { object CreateRoomAndInviteSelectedUsers : CreateDirectRoomActions() data class FilterKnownUsers(val value: String) : CreateDirectRoomActions() - object ClearFilterKnownUsers: CreateDirectRoomActions() - object SelectAddByMatrixId : CreateDirectRoomActions() + data class SearchDirectoryUsers(val value: String) : CreateDirectRoomActions() + object ClearFilterKnownUsers : CreateDirectRoomActions() + data class SelectUser(val user: User) : CreateDirectRoomActions() + data class RemoveSelectedUser(val user: User) : CreateDirectRoomActions() } 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 4ecb5f9f..7b41c226 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 @@ -20,20 +20,84 @@ package im.vector.riotx.features.home.createdirect import android.content.Context import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProviders +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.viewModel +import com.google.android.gms.common.GooglePlayServicesNotAvailableException import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment -import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.platform.SimpleFragmentActivity +import im.vector.riotx.core.platform.WaitingViewData +import kotlinx.android.synthetic.main.activity.* +import javax.inject.Inject -class CreateDirectRoomActivity : VectorBaseActivity() { +class CreateDirectRoomActivity : SimpleFragmentActivity() { - override fun getLayoutRes() = R.layout.activity_simple + sealed class Navigation { + object UsersDirectory : Navigation() + object Close : Navigation() + object Previous : Navigation() + } - override fun initUiAndData() { + private val viewModel: CreateDirectRoomViewModel by viewModel() + lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel + @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory + + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + toolbar.visibility = View.GONE + navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java) + navigationViewModel.navigateTo.observeEvent(this) { navigation -> + when (navigation) { + is Navigation.UsersDirectory -> addFragmentToBackstack(CreateDirectRoomDirectoryUsersFragment(), R.id.container) + Navigation.Close -> finish() + Navigation.Previous -> onBackPressed() + } + } if (isFirstCreation()) { - addFragment(CreateDirectRoomFragment(), R.id.simpleFragmentContainer) + addFragment(CreateDirectRoomFragment(), R.id.container) + } + viewModel.subscribe(this) { renderState(it) } + } + + private fun renderState(state: CreateDirectRoomViewState) { + when (state.createAndInviteState) { + is Loading -> renderCreationLoading() + is Success -> renderCreationSuccess(state.createAndInviteState()) + is Fail -> renderCreationFailure(state.createAndInviteState.error) } } + private fun renderCreationLoading() { + updateWaitingView(WaitingViewData(getString(R.string.room_recents_create_room))) + } + + private fun renderCreationFailure(error: Throwable) { + + } + + private fun renderCreationSuccess(roomId: String?) { + // Navigate to freshly created room + if (roomId != null) { + navigator.openRoom(this, roomId) + } + finish() + } + + companion object { fun getIntent(context: Context): Intent { return Intent(context, CreateDirectRoomActivity::class.java) 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/CreateDirectRoomController.kt index af7de9c6..8e56813a 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/CreateDirectRoomController.kt @@ -19,14 +19,27 @@ package im.vector.riotx.features.home.createdirect import com.airbnb.epoxy.EpoxyController +import com.airbnb.epoxy.VisibilityState +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.failure.Failure import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.internal.util.firstLetterOfDisplayName +import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject -class CreateDirectRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer) : EpoxyController() { +class CreateDirectRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer, + private val errorFormatter: ErrorFormatter) : EpoxyController() { private var state: CreateDirectRoomViewState? = null + var displayMode = CreateDirectRoomViewState.DisplayMode.KNOWN_USERS + var callback: Callback? = null init { @@ -40,10 +53,36 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer: override fun buildModels() { val currentState = state ?: return - val knownUsers = currentState.knownUsers() ?: return + val asyncUsers = if (displayMode == CreateDirectRoomViewState.DisplayMode.DIRECTORY_USERS) { + currentState.directoryUsers + } else { + currentState.knownUsers + } + when (asyncUsers) { + is Incomplete -> renderLoading() + is Success -> renderUsers(asyncUsers(), currentState.selectedUsers) + is Fail -> renderFailure(asyncUsers.error) + } + } + private fun renderLoading() { + loadingItem { + id("loading") + } + } + + private fun renderFailure(failure: Throwable) { + errorWithRetryItem { + id("error") + text(errorFormatter.toHumanReadable(failure)) + listener { callback?.retryDirectoryUsersRequest() } + } + } + + private fun renderUsers(users: List, selectedUsers: Set) { var lastFirstLetter: String? = null - knownUsers.forEach { user -> + users.forEach { user -> + val isSelected = selectedUsers.contains(user) val currentFirstLetter = user.displayName.firstLetterOfDisplayName() val showLetter = currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter lastFirstLetter = currentFirstLetter @@ -55,6 +94,7 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer: createDirectRoomUserItem { id(user.userId) + selected(isSelected) userId(user.userId) name(user.displayName) avatarUrl(user.avatarUrl) @@ -64,11 +104,13 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer: } } } - } interface Callback { fun onItemClick(user: User) + fun retryDirectoryUsersRequest() { + // NO-OP + } } } \ No newline at end of file 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 new file mode 100644 index 00000000..838c7d6e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -0,0 +1,93 @@ +/* + * 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 android.content.Context +import android.os.Bundle +import android.view.inputmethod.InputMethodManager +import androidx.lifecycle.ViewModelProviders +import com.airbnb.mvrx.activityViewModel +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +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 { + + override fun getLayoutResId() = R.layout.fragment_create_direct_room_directory_users + + private val viewModel: CreateDirectRoomViewModel by activityViewModel() + + @Inject lateinit var directRoomController: CreateDirectRoomController + private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + navigationViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java) + setupRecyclerView() + setupSearchByMatrixIdView() + setupCloseView() + viewModel.subscribe(this) { renderState(it) } + } + + private fun setupRecyclerView() { + recyclerView.setHasFixedSize(true) + directRoomController.callback = this + directRoomController.displayMode = CreateDirectRoomViewState.DisplayMode.DIRECTORY_USERS + recyclerView.setController(directRoomController) + } + + private fun setupSearchByMatrixIdView() { + createDirectRoomSearchById + .textChanges() + .subscribe { + viewModel.handle(CreateDirectRoomActions.SearchDirectoryUsers(it.toString())) + } + .disposeOnDestroy() + createDirectRoomSearchById.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showSoftInput(createDirectRoomSearchById, InputMethodManager.SHOW_IMPLICIT) + + } + + private fun setupCloseView() { + createDirectRoomClose.setOnClickListener { + navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.Close) + } + } + + private fun renderState(state: CreateDirectRoomViewState) { + directRoomController.setData(state) + } + + override fun onItemClick(user: User) { + viewModel.handle(CreateDirectRoomActions.SelectUser(user)) + navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.Previous) + } + + override fun retryDirectoryUsersRequest() { + val currentSearch = createDirectRoomSearchById.text.toString() + viewModel.handle(CreateDirectRoomActions.SearchDirectoryUsers(currentSearch)) + } +} \ No newline at end of file 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/CreateDirectRoomFragment.kt index eedb5617..e28346a6 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/CreateDirectRoomFragment.kt @@ -20,14 +20,20 @@ package im.vector.riotx.features.home.createdirect import android.os.Bundle import android.view.MenuItem -import com.airbnb.mvrx.fragmentViewModel +import androidx.lifecycle.ViewModelProviders +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.activityViewModel import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import kotlinx.android.synthetic.main.fragment_create_direct_room.* -import java.util.concurrent.TimeUnit +import kotlinx.android.synthetic.main.fragment_public_rooms.* import javax.inject.Inject class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomController.Callback { @@ -36,10 +42,10 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle override fun getMenuRes() = R.menu.vector_create_direct_room - private val viewModel: CreateDirectRoomViewModel by fragmentViewModel() + private val viewModel: CreateDirectRoomViewModel by activityViewModel() - @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory @Inject lateinit var directRoomController: CreateDirectRoomController + private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -47,27 +53,38 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) + navigationViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java) + vectorBaseActivity.setSupportActionBar(createDirectRoomToolbar) setupRecyclerView() setupFilterView() + setupAddByMatrixIdView() + setupCloseView() viewModel.subscribe(this) { renderState(it) } } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - R.id.action_create_room -> { + R.id.action_create_direct_room -> { viewModel.handle(CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers) true } - else -> + else -> super.onOptionsItemSelected(item) } } + private fun setupAddByMatrixIdView() { + addByMatrixId.setOnClickListener { + navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.UsersDirectory) + } + } + private fun setupRecyclerView() { recyclerView.setHasFixedSize(true) // 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) } @@ -85,11 +102,18 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle .disposeOnDestroy() } + private fun setupCloseView() { + createDirectRoomClose.setOnClickListener { + requireActivity().finish() + } + } + private fun renderState(state: CreateDirectRoomViewState) { + directRoomController.setData(state) } override fun onItemClick(user: User) { - vectorBaseActivity.notImplemented("IMPLEMENT ON USER CLICKED") + viewModel.handle(CreateDirectRoomActions.SelectUser(user)) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt new file mode 100644 index 00000000..442dc23d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt @@ -0,0 +1,22 @@ +/* + * 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 im.vector.riotx.core.mvrx.NavigationViewModel +import javax.inject.Inject + +class CreateDirectRoomNavigationViewModel @Inject constructor(): NavigationViewModel() \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt index 57d9347e..33f7c22f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt @@ -21,12 +21,16 @@ package im.vector.riotx.features.home.createdirect import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import com.amulyakhare.textdrawable.TextDrawable import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.getColorFromUserId @EpoxyModelClass(layout = R.layout.item_create_direct_room_user) abstract class CreateDirectRoomUserItem : VectorEpoxyModel() { @@ -36,6 +40,7 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel(R.id.createDirectRoomUserID) val nameView by bind(R.id.createDirectRoomUserName) val avatarImageView by bind(R.id.createDirectRoomUserAvatar) + val avatarCheckedImageView by bind(R.id.createDirectRoomUserAvatarChecked) } } \ No newline at end of file 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 c9b976cd..87ee8005 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 @@ -19,22 +19,23 @@ package im.vector.riotx.features.home.createdirect import arrow.core.Option -import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.rx.rx import im.vector.riotx.core.platform.VectorViewModel import io.reactivex.Observable import io.reactivex.functions.BiFunction -import io.reactivex.subjects.BehaviorSubject import java.util.concurrent.TimeUnit private typealias KnowUsersFilter = String +private typealias DirectoryUsersSearch = String class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateDirectRoomViewState, @@ -47,35 +48,77 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) + private val directoryUsersSearch = BehaviorRelay.create() companion object : MvRxViewModelFactory { @JvmStatic override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? { - val fragment: CreateDirectRoomFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.createDirectRoomViewModelFactory.create(state) + val activity: CreateDirectRoomActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.createDirectRoomViewModelFactory.create(state) } } init { observeKnownUsers() + observeDirectoryUsers() } - fun handle(createDirectRoomActions: CreateDirectRoomActions) { - when (createDirectRoomActions) { + fun handle(action: CreateDirectRoomActions) { + when (action) { is CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() - is CreateDirectRoomActions.SelectAddByMatrixId -> handleSelectAddByMatrixId() - is CreateDirectRoomActions.FilterKnownUsers -> knownUsersFilter.accept(Option.just(createDirectRoomActions.value)) + is CreateDirectRoomActions.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value)) is CreateDirectRoomActions.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) + is CreateDirectRoomActions.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value) + is CreateDirectRoomActions.SelectUser -> handleSelectUser(action) + is CreateDirectRoomActions.RemoveSelectedUser -> handleRemoveSelectedUser(action) } } - private fun handleSelectAddByMatrixId() { - // TODO + private fun createRoomAndInviteSelectedUsers() = withState { + val isDirect = it.selectedUsers.size == 1 + val roomParams = CreateRoomParams().apply { + invitedUserIds = ArrayList(it.selectedUsers.map { user -> user.userId }) + if (isDirect) { + setDirectMessage() + } + } + session.rx() + .createRoom(roomParams) + .execute { + copy(createAndInviteState = it) + } + .disposeOnClear() } - private fun createRoomAndInviteSelectedUsers() { - // TODO + private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState { + val selectedUsers = it.selectedUsers.minusElement(action.user) + setState { copy(selectedUsers = selectedUsers) } + } + + private fun handleSelectUser(action: CreateDirectRoomActions.SelectUser) = withState { + val selectedUsers = if (it.selectedUsers.contains(action.user)) { + it.selectedUsers.minusElement(action.user) + } else { + it.selectedUsers.plus(action.user) + } + setState { copy(selectedUsers = selectedUsers) } + } + + private fun observeDirectoryUsers() { + directoryUsersSearch + .throttleLast(300, TimeUnit.MILLISECONDS) + .switchMapSingle { search -> + session.rx() + .searchUsersDirectory(search, 50, emptySet()) + .map { users -> + users.sortedBy { it.displayName } + } + } + .execute { async -> + copy(directoryUsers = async) + } + } private fun observeKnownUsers() { 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 1ad1f888..56607802 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 @@ -24,14 +24,15 @@ import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.user.model.User data class CreateDirectRoomViewState( - val displayMode: DisplayMode = DisplayMode.KNOWN_USERS, val knownUsers: Async> = Uninitialized, - val filteredKnownUsers: Async> = Uninitialized + val directoryUsers: Async> = Uninitialized, + val selectedUsers: Set = emptySet(), + val createAndInviteState: Async = Uninitialized ) : MvRxState { enum class DisplayMode { KNOWN_USERS, - MATRIX_ID_USERS + DIRECTORY_USERS } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index a15eae5b..14de38bc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -43,6 +43,7 @@ import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.utils.LiveEvent +import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents @@ -94,7 +95,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeRoomSummary() observeEventDisplayedActions() observeInvitationState() - cancelableBag += room.loadRoomMembersIfNeeded() + room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } } @@ -235,12 +236,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId - ?: "", messageContent?.type - ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + ?: "", messageContent?.type + ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -255,7 +256,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 496b4e47..1428a0ac 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -26,7 +26,6 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActiv import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.riotx.features.debug.DebugMenuActivity import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity -import im.vector.riotx.features.home.createdirect.CreateDirectRoomFragment import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml index 78955389..3899d3d2 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -10,7 +10,7 @@ android:layout_height="match_parent"> + app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_create_direct_room_user.xml b/vector/src/main/res/layout/item_create_direct_room_user.xml index de1afd5f..532f6c15 100644 --- a/vector/src/main/res/layout/item_create_direct_room_user.xml +++ b/vector/src/main/res/layout/item_create_direct_room_user.xml @@ -7,18 +7,33 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?riotx_background" + android:foreground="?attr/selectableItemBackground" android:gravity="center_vertical" android:orientation="horizontal" android:padding="8dp"> - + app:layout_constraintTop_toTopOf="parent"> + + + + + diff --git a/vector/src/main/res/menu/vector_create_direct_room.xml b/vector/src/main/res/menu/vector_create_direct_room.xml index 42a21da9..8c6eab1c 100755 --- a/vector/src/main/res/menu/vector_create_direct_room.xml +++ b/vector/src/main/res/menu/vector_create_direct_room.xml @@ -1,11 +1,9 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> From 151ae7f4ddc0b1e95488f94a9ae407bc23d0a082 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 22 Jul 2019 18:58:55 +0200 Subject: [PATCH 05/14] Direct chat: handle user account data --- .../database/model/RoomSummaryEntity.kt | 1 + .../query/RoomSummaryEntityQueries.kt | 18 ++++++ .../query/TimelineEventEntityQueries.kt | 3 +- .../internal/session/SessionComponent.kt | 31 ++++++----- .../session/room/RoomSummaryUpdater.kt | 10 +++- .../session/room/create/CreateRoomTask.kt | 55 ++++++++++++++++--- .../membership/RoomDisplayNameResolver.kt | 34 ++++++------ .../session/room/membership/RoomMembers.kt | 6 +- .../room/membership/joining/JoinRoomTask.kt | 29 +++++++++- .../session/room/read/DefaultReadService.kt | 10 +--- .../session/room/read/SetReadMarkersTask.kt | 51 +++++++++++------ .../internal/session/sync/RoomSyncHandler.kt | 47 +++++++++++++--- .../sync/UserAccountDataSyncHandler.kt | 27 +++++---- .../session/user/UserEntityFactory.kt | 5 ++ .../user/accountdata/AccountDataAPI.kt | 48 ++++++++++++++++ .../user/accountdata/AccountDataModule.kt | 41 ++++++++++++++ .../user/accountdata/DirectChatsHelper.kt | 54 ++++++++++++++++++ .../accountdata/UpdateUserAccountDataTask.kt | 55 +++++++++++++++++++ 18 files changed, 433 insertions(+), 92 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index c178711c..6f60fd94 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -32,6 +32,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var joinedMembersCount: Int? = 0, var invitedMembersCount: Int? = 0, var isDirect: Boolean = false, + var directUserId: String? = null, var otherMemberIds: RealmList = RealmList(), var notificationCount: Int = 0, var highlightCount: Int = 0, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt index 7cc0713f..f2c26042 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import io.realm.Realm import io.realm.RealmQuery +import io.realm.RealmResults import io.realm.kotlin.where internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { @@ -29,3 +30,20 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n } return query } + +internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults { + return RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll() +} + +internal fun RoomSummaryEntity.Companion.isDirect(realm: Realm, roomId: String): Boolean { + return RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll() + .isNotEmpty() +} + + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 3669ada7..cfa56914 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -65,11 +65,12 @@ internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, roomId: String, + includesSending: Boolean, includedTypes: List = emptyList(), excludedTypes: List = emptyList()): TimelineEventEntity? { val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null - val eventList = if (roomEntity.sendingTimelineEvents.isNotEmpty()) { + val eventList = if (includesSending && roomEntity.sendingTimelineEvents.isNotEmpty()) { roomEntity.sendingTimelineEvents } else { ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 1738cedd..36f42895 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -19,11 +19,11 @@ package im.vector.matrix.android.internal.session import dagger.BindsInstance import dagger.Component import im.vector.matrix.android.api.auth.data.SessionParams -import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.crypto.CryptoModule import im.vector.matrix.android.internal.di.MatrixComponent import im.vector.matrix.android.internal.network.NetworkConnectivityChecker +import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule import im.vector.matrix.android.internal.session.cache.CacheModule import im.vector.matrix.android.internal.session.content.ContentModule import im.vector.matrix.android.internal.session.content.UploadContentWorker @@ -46,20 +46,21 @@ import im.vector.matrix.android.internal.session.user.UserModule import im.vector.matrix.android.internal.task.TaskExecutor @Component(dependencies = [MatrixComponent::class], - modules = [ - SessionModule::class, - RoomModule::class, - SyncModule::class, - SignOutModule::class, - GroupModule::class, - UserModule::class, - FilterModule::class, - GroupModule::class, - ContentModule::class, - CacheModule::class, - CryptoModule::class, - PushersModule::class - ] + modules = [ + SessionModule::class, + RoomModule::class, + SyncModule::class, + SignOutModule::class, + GroupModule::class, + UserModule::class, + FilterModule::class, + GroupModule::class, + ContentModule::class, + CacheModule::class, + CryptoModule::class, + PushersModule::class, + AccountDataModule::class + ] ) @SessionScope internal interface SessionComponent { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 6bcac9b8..a65c466a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -62,7 +62,9 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C roomId: String, membership: Membership? = null, roomSummary: RoomSyncSummary? = null, - unreadNotifications: RoomSyncUnreadNotifications? = null) { + unreadNotifications: RoomSyncUnreadNotifications? = null, + isDirect: Boolean? = null, + directUserId: String? = null) { val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) @@ -85,7 +87,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C roomSummaryEntity.membership = membership } - val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includedTypes = PREVIEWABLE_TYPES) + val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES) val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() val otherRoomMembers = RoomMembers(realm, roomId) @@ -95,6 +97,10 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C .asSequence() .map { it.stateKey } + if (isDirect != null) { + roomSummaryEntity.isDirect = isDirect + roomSummaryEntity.directUserId = directUserId + } roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.topic = lastTopicEvent?.content.toModel()?.topic 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 73d9b6f2..e77cafc8 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 @@ -17,22 +17,32 @@ package im.vector.matrix.android.internal.session.room.create import arrow.core.Try +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.internal.database.RealmQueryLatch -import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntityFields +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope 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.session.user.accountdata.DirectChatsHelper +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.tryTransactionSync import io.realm.RealmConfiguration import javax.inject.Inject internal interface CreateRoomTask : Task internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: RoomAPI, + private val monarchy: Monarchy, + private val directChatsHelper: DirectChatsHelper, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val readMarkersTask: SetReadMarkersTask, @SessionDatabase private val realmConfiguration: RealmConfiguration) : CreateRoomTask { @@ -41,17 +51,48 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro apiCall = roomAPI.createRoom(params) }.flatMap { createRoomResponse -> val roomId = createRoomResponse.roomId!! - - // TODO Maybe do the same code for join room request ? // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) val rql = RealmQueryLatch(realmConfiguration) { realm -> realm.where(RoomEntity::class.java) .equalTo(RoomEntityFields.ROOM_ID, roomId) } - rql.await() - - return Try.just(roomId) + Try.just(roomId) + }.flatMap { roomId -> + if (params.isDirect()) { + handleDirectChatCreation(params, roomId) + } else { + Try.just(roomId) + } + }.flatMap { roomId -> + setReadMarkers(roomId) } } + + private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String): Try { + val otherUserId = params.getFirstInvitedUserId() + ?: return Try.raise(IllegalStateException("You can't create a direct room without an invitedUser")) + + return monarchy.tryTransactionSync { realm -> + RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { + this.directUserId = otherUserId + this.isDirect = true + } + }.flatMap { + val directChats = directChatsHelper.getDirectChats() + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats)) + }.flatMap { + Try.just(roomId) + } + } + + private suspend fun setReadMarkers(roomId: String): Try { + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true) + return readMarkersTask + .execute(setReadMarkerParams) + .flatMap { + Try.just(roomId) + } + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 948f1741..53e9e55a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -34,7 +34,6 @@ import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where -import io.realm.RealmResults import javax.inject.Inject /** @@ -81,10 +80,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: val roomMembers = RoomMembers(realm, roomId) val loadedMembers = roomMembers.queryRoomMembersEvent().findAll() - val otherMembersSubset = loadedMembers.where() - .notEqualTo(EventEntityFields.STATE_KEY, credentials.userId) - .limit(3) - .findAll() + if (roomEntity?.membership == Membership.INVITE) { val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst() @@ -97,23 +93,29 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: } else { context.getString(R.string.room_displayname_room_invite) } - } else { + } else if (roomEntity?.membership == Membership.JOIN) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - val memberIds: List = if (roomSummary?.heroes?.isNotEmpty() == true) { - roomSummary.heroes + val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { + roomSummary.heroes.mapNotNull { + roomMembers.getStateEvent(it) + } } else { - otherMembersSubset.mapNotNull { it.stateKey } + loadedMembers.where() + .notEqualTo(EventEntityFields.STATE_KEY, credentials.userId) + .limit(3) + .findAll() } - name = when (memberIds.size) { + val otherMembersCount = roomMembers.getNumberOfMembers() - 1 + name = when (otherMembersCount) { 0 -> context.getString(R.string.room_displayname_empty_room) - 1 -> resolveRoomMember(otherMembersSubset[0], roomMembers) + 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) 2 -> context.getString(R.string.room_displayname_two_members, - resolveRoomMember(otherMembersSubset[0], roomMembers), - resolveRoomMember(otherMembersSubset[1], roomMembers) + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers) ) else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, roomMembers.getNumberOfJoinedMembers() - 1, - resolveRoomMember(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[0], roomMembers), roomMembers.getNumberOfJoinedMembers() - 1) } } @@ -122,8 +124,8 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return name ?: roomId } - private fun resolveRoomMember(eventEntity: EventEntity?, - roomMembers: RoomMembers): String? { + private fun resolveRoomMemberName(eventEntity: EventEntity?, + roomMembers: RoomMembers): String? { if (eventEntity == null) return null val roomMember = eventEntity.toRoomMember() ?: return null val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index fb8326f2..8db3f170 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -42,12 +42,16 @@ internal class RoomMembers(private val realm: Realm, RoomSummaryEntity.where(realm, roomId).findFirst() } - fun get(userId: String): RoomMember? { + fun getStateEvent(userId: String): EventEntity? { return EventEntity .where(realm, roomId, EventType.STATE_ROOM_MEMBER) .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) .equalTo(EventEntityFields.STATE_KEY, userId) .findFirst() + } + + fun get(userId: String): RoomMember? { + return getStateEvent(userId) ?.let { it.asDomain().content?.toModel() } 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 96454cbf..e71a9fe3 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 @@ -17,10 +17,15 @@ package im.vector.matrix.android.internal.session.room.membership.joining import arrow.core.Try +import im.vector.matrix.android.internal.database.RealmQueryLatch +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.RoomEntityFields +import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope 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 javax.inject.Inject internal interface JoinRoomTask : Task { @@ -29,12 +34,30 @@ internal interface JoinRoomTask : Task { ) } -internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: RoomAPI) : JoinRoomTask { +internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: RoomAPI, + private val readMarkersTask: SetReadMarkersTask, + @SessionDatabase private val realmConfiguration: RealmConfiguration) : JoinRoomTask { override suspend fun execute(params: JoinRoomTask.Params): Try { - return executeRequest { + return executeRequest { apiCall = roomAPI.join(params.roomId, HashMap()) + }.flatMap { + val roomId = params.roomId + // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) + val rql = RealmQueryLatch(realmConfiguration) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, roomId) + } + rql.await() + Try.just(roomId) + }.flatMap { roomId -> + setReadMarkers(roomId) } } + private suspend fun setReadMarkers(roomId: String): Try { + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true) + return readMarkersTask.execute(setReadMarkerParams) + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index ff899968..7830ce0e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -22,14 +22,11 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.fetchCopied import javax.inject.Inject internal class DefaultReadService @Inject constructor(private val roomId: String, @@ -39,9 +36,7 @@ internal class DefaultReadService @Inject constructor(private val roomId: String private val credentials: Credentials) : ReadService { override fun markAllAsRead(callback: MatrixCallback) { - //TODO shouldn't it be latest synced event? - val latestEvent = getLatestEvent() - val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId) + val params = SetReadMarkersTask.Params(roomId, markAllAsRead = true) setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor) } @@ -55,9 +50,6 @@ internal class DefaultReadService @Inject constructor(private val roomId: String setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor) } - private fun getLatestEvent(): TimelineEventEntity? { - return monarchy.fetchCopied { TimelineEventEntity.latestEvent(it, roomId) } - } override fun isEventRead(eventId: String): Boolean { var isEventRead = false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 2106ab55..c32fc598 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -20,7 +20,6 @@ import arrow.core.Try import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -33,6 +32,7 @@ import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.tryTransactionAsync +import io.realm.Realm import timber.log.Timber import javax.inject.Inject @@ -40,8 +40,9 @@ internal interface SetReadMarkersTask : Task { data class Params( val roomId: String, - val fullyReadEventId: String?, - val readReceiptEventId: String? + val markAllAsRead: Boolean = false, + val fullyReadEventId: String? = null, + val readReceiptEventId: String? = null ) } @@ -55,21 +56,35 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI override suspend fun execute(params: SetReadMarkersTask.Params): Try { val markers = HashMap() - if (params.fullyReadEventId != null) { - if (LocalEchoEventFactory.isLocalEchoId(params.fullyReadEventId)) { + val fullyReadEventId: String? + val readReceiptEventId: String? + + if (params.markAllAsRead) { + val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> + TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId + } + fullyReadEventId = latestSyncedEventId + readReceiptEventId = latestSyncedEventId + } else { + fullyReadEventId = params.fullyReadEventId + readReceiptEventId = params.readReceiptEventId + } + + if (fullyReadEventId != null) { + if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") } else { - markers[READ_MARKER] = params.fullyReadEventId + markers[READ_MARKER] = fullyReadEventId } } - if (params.readReceiptEventId != null - && !isEventRead(params.roomId, params.readReceiptEventId)) { + if (readReceiptEventId != null + && !isEventRead(params.roomId, readReceiptEventId)) { - if (LocalEchoEventFactory.isLocalEchoId(params.readReceiptEventId)) { - Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") + if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { + Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") } else { - updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId) - markers[READ_RECEIPT] = params.readReceiptEventId + updateNotificationCountIfNecessary(params.roomId, readReceiptEventId) + markers[READ_RECEIPT] = readReceiptEventId } } return if (markers.isEmpty()) { @@ -83,10 +98,10 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) { monarchy.tryTransactionAsync { realm -> - val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId)?.eventId == eventId + val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId if (isLatestReceived) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@tryTransactionAsync + ?: return@tryTransactionAsync roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 } @@ -97,13 +112,13 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI var isEventRead = false monarchy.doWithRealm { val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm + ?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm + ?: return@doWithRealm val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE + ?: Int.MAX_VALUE isEventRead = eventToCheckIndex <= readReceiptIndex } return isEventRead diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 215321bd..4b42ee99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -18,27 +18,41 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent import im.vector.matrix.android.internal.crypto.CryptoManager -import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.addStateEvent +import im.vector.matrix.android.internal.database.helper.updateSenderDataFor +import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.UserEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.isDirect import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService +import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater +import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection -import im.vector.matrix.android.internal.session.sync.model.* +import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync +import im.vector.matrix.android.internal.session.sync.model.RoomSync +import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData +import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral +import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -55,6 +69,9 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private val tokenStore: SyncTokenStore, private val pushRuleService: DefaultPushRuleService, private val processForPushTask: ProcessEventForPushTask, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val credentials: Credentials, + private val directChatsHelper: DirectChatsHelper, private val taskExecutor: TaskExecutor) { sealed class HandlingStrategy { @@ -118,7 +135,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch Timber.v("Handle join sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) if (roomEntity.membership == Membership.INVITE) { roomEntity.chunks.deleteAllFromRealm() @@ -128,7 +145,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch // State event if (roomSync.state != null && roomSync.state.events.isNotEmpty()) { val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt() - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val untimelinedStateIndex = minStateIndex + 1 roomSync.state.events.forEach { event -> roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex) @@ -169,13 +186,27 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch InvitedRoomSync): RoomEntity { Timber.v("Handle invited sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) roomEntity.membership = Membership.INVITE if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) roomEntity.addOrUpdate(chunkEntity) } - roomSummaryUpdater.update(realm, roomId, Membership.INVITE) + val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(credentials.userId) + val inviterId = myUserStateEvent?.sender + val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() } + val isDirect = myUserRoomMember?.isDirect + if (isDirect == true && inviterId != null) { + val isAlreadyDirect = RoomSummaryEntity.isDirect(realm, roomId) + if (!isAlreadyDirect) { + val directChatsMap = directChatsHelper.getDirectChats(include = Pair(inviterId, roomId)) + val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams( + directMessages = directChatsMap + ) + updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor) + } + } + roomSummaryUpdater.update(realm, roomId, Membership.INVITE, isDirect = isDirect, directUserId = inviterId) return roomEntity } @@ -183,7 +214,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomId: String, roomSync: RoomSync): RoomEntity { val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) roomEntity.membership = Membership.LEAVE roomEntity.chunks.deleteAllFromRealm() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index 9c876049..e0be3b14 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -19,8 +19,8 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields +import im.vector.matrix.android.internal.database.query.getDirectRooms import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages import im.vector.matrix.android.internal.session.sync.model.UserAccountDataSync import javax.inject.Inject @@ -37,19 +37,22 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc } private fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) { - val newDirectRoomIds = directMessages.content.values.flatten() monarchy.runTransactionSync { realm -> - val oldDirectRooms = RoomSummaryEntity.where(realm) - .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) - .findAll() - oldDirectRooms.forEach { it.isDirect = false } - - newDirectRoomIds.forEach { roomId -> - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - if (roomSummaryEntity != null) { - roomSummaryEntity.isDirect = true - realm.insertOrUpdate(roomSummaryEntity) + val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm) + oldDirectRooms.forEach { + it.isDirect = false + it.directUserId = null + } + directMessages.content.forEach { + val userId = it.key + it.value.forEach { roomId -> + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + if (roomSummaryEntity != null) { + roomSummaryEntity.isDirect = true + roomSummaryEntity.directUserId = userId + realm.insertOrUpdate(roomSummaryEntity) + } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt index 188c7d84..c5c059eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.user import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.model.UserEntity @@ -29,6 +30,10 @@ internal object UserEntityFactory { return null } val roomMember = event.content.toModel() ?: return null + // We only use JOIN and INVITED memberships to create User data + if (roomMember.membership != Membership.JOIN || roomMember.membership != Membership.INVITE) { + return null + } return UserEntity(event.stateKey ?: "", roomMember.displayName ?: "", roomMember.avatarUrl ?: "" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt new file mode 100644 index 00000000..824af2d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt @@ -0,0 +1,48 @@ +/* + * 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.matrix.android.internal.session.user.accountdata + +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path + +interface AccountDataAPI { + + /** + * Set some account_data for the client. + * + * @param userId the user id + * @param type the type + * @param params the put params + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}") + fun setAccountData(@Path("userId") userId: String, @Path("type") type: String, @Body params: Any): Call + + /** + * Gets a bearer token from the homeserver that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * + * @param userId the user id + * @param body the body content + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") + fun openIdToken(@Path("userId") userId: String, @Body body: Map): Call> +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt new file mode 100644 index 00000000..e4b76ca1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt @@ -0,0 +1,41 @@ +/* + * 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.matrix.android.internal.session.user.accountdata + +import dagger.Binds +import dagger.Module +import dagger.Provides +import retrofit2.Retrofit + +@Module +internal abstract class AccountDataModule { + + @Module + companion object { + + @JvmStatic + @Provides + fun providesAccountDataAPI(retrofit: Retrofit): AccountDataAPI { + return retrofit.create(AccountDataAPI::class.java) + } + + } + + @Binds + abstract fun bindUpdateUserAccountDataTask(updateUserAccountDataTask: DefaultUpdateUserAcountDataTask): UpdateUserAccountDataTask + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt new file mode 100644 index 00000000..5d135b7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt @@ -0,0 +1,54 @@ +/* + * 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.matrix.android.internal.session.user.accountdata + +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.getDirectRooms +import im.vector.matrix.android.internal.di.SessionDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import timber.log.Timber +import javax.inject.Inject + +internal class DirectChatsHelper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { + + fun getDirectChats(include: Pair? = null, filterRoomId: String? = null): Map> { + return Realm.getInstance(realmConfiguration).use { realm -> + val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm) + val directChatsMap = mutableMapOf>() + for (directRoom in currentDirectRooms) { + if (directRoom.roomId == filterRoomId) continue + val directUserId = directRoom.directUserId ?: continue + directChatsMap.getOrPut(directUserId, { arrayListOf() }).apply { + add(directRoom.roomId) + } + } + if (include != null) { + directChatsMap.getOrPut(include.first, { arrayListOf() }).apply { + if (contains(include.second)) { + Timber.v("Direct chats already include room ${include.second} with user ${include.first}") + } else { + add(include.second) + } + } + } + directChatsMap + } + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt new file mode 100644 index 00000000..57ee9632 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -0,0 +1,55 @@ +/* + * 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.matrix.android.internal.session.user.accountdata + +import arrow.core.Try +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.sync.model.UserAccountData +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface UpdateUserAccountDataTask : Task { + + interface Params { + val type: String + fun getData(): Any + } + + data class DirectChatParams(override val type: String = UserAccountData.TYPE_DIRECT_MESSAGES, + private val directMessages: Map> + ) : Params { + + override fun getData(): Any { + return directMessages + } + } + + +} + +internal class DefaultUpdateUserAcountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI, + private val credentials: Credentials) : UpdateUserAccountDataTask { + + override suspend fun execute(params: UpdateUserAccountDataTask.Params): Try { + + return executeRequest { + apiCall = accountDataApi.setAccountData(credentials.userId, params.type, params.getData()) + } + } + +} \ No newline at end of file From 03974c8bdfb02f470cc38411386c1fe1316b749c Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 22 Jul 2019 19:01:17 +0200 Subject: [PATCH 06/14] Create Direct Room : fix loading/error state (WIP) --- .../home/createdirect/CreateDirectRoomActivity.kt | 10 ++++++++-- .../home/createdirect/CreateDirectRoomViewModel.kt | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) 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 7b41c226..82b8b812 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 @@ -22,6 +22,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View +import androidx.appcompat.app.AlertDialog import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading @@ -30,6 +31,7 @@ import com.airbnb.mvrx.viewModel import com.google.android.gms.common.GooglePlayServicesNotAvailableException import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.extensions.observeEvent @@ -49,7 +51,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { private val viewModel: CreateDirectRoomViewModel by viewModel() lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory - + @Inject lateinit var errorFormatter: ErrorFormatter override fun injectWith(injector: ScreenComponent) { super.injectWith(injector) @@ -86,7 +88,11 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { } private fun renderCreationFailure(error: Throwable) { - + hideWaitingView() + AlertDialog.Builder(this) + .setMessage(errorFormatter.toHumanReadable(error)) + .setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } + .show() } private fun renderCreationSuccess(roomId: String?) { 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 87ee8005..481ca922 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 @@ -75,10 +75,10 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } } - private fun createRoomAndInviteSelectedUsers() = withState { - val isDirect = it.selectedUsers.size == 1 + private fun createRoomAndInviteSelectedUsers() = withState { currentState -> + val isDirect = currentState.selectedUsers.size == 1 val roomParams = CreateRoomParams().apply { - invitedUserIds = ArrayList(it.selectedUsers.map { user -> user.userId }) + invitedUserIds = ArrayList(currentState.selectedUsers.map { user -> user.userId }) if (isDirect) { setDirectMessage() } From 125eacb20b55122f5ff573aa5b0ab2d691553885 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 Jul 2019 19:53:47 +0200 Subject: [PATCH 07/14] Direct messages: try to handle selecting/deselecting users (WIP) --- .../matrix/android/api/MatrixPatterns.kt | 2 +- .../vector/riotx/core/extensions/LiveData.kt | 5 ++ .../riotx/core/mvrx/NavigationViewModel.kt | 3 +- .../riotx/core/platform/VectorViewModel.kt | 30 +++++-- .../CreateDirectRoomController.kt | 10 +-- .../CreateDirectRoomDirectoryUsersFragment.kt | 2 + .../createdirect/CreateDirectRoomFragment.kt | 85 ++++++++++++++++--- .../createdirect/CreateDirectRoomUserItem.kt | 2 - .../createdirect/CreateDirectRoomViewModel.kt | 48 ++++++++--- .../features/home/group/GroupListViewModel.kt | 3 +- .../home/room/detail/RoomDetailViewModel.kt | 59 ++++++------- .../home/room/list/RoomListViewModel.kt | 5 +- .../roomdirectory/RoomDirectoryViewModel.kt | 3 +- .../layout/fragment_create_direct_room.xml | 11 +-- 14 files changed, 184 insertions(+), 84 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt index 5cb7f4ca..e843128e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt @@ -28,7 +28,7 @@ object MatrixPatterns { // regex pattern to find matrix user ids in a string. // See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX" - private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) // regex pattern to find room ids in a string. private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX" diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt b/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt index a278eab0..97215e1e 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt @@ -18,6 +18,7 @@ package im.vector.riotx.core.extensions import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import im.vector.riotx.core.utils.FirstThrottler import im.vector.riotx.core.utils.EventObserver @@ -44,3 +45,7 @@ inline fun LiveData>.observeEventFirstThrottle(owner: Lifecycle } }) } + +fun MutableLiveData>.postLiveEvent(content: T) { + this.postValue(LiveEvent(content)) +} diff --git a/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt b/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt index ab3ce7c8..a6bf07e0 100644 --- a/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt @@ -19,6 +19,7 @@ package im.vector.riotx.core.mvrx import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.utils.LiveEvent abstract class NavigationViewModel : ViewModel() { @@ -29,6 +30,6 @@ abstract class NavigationViewModel : ViewModel() { fun goTo(navigation: NavigationClass) { - _navigateTo.postValue(LiveEvent(navigation)) + _navigateTo.postLiveEvent(navigation) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt index 1570a7f8..1c2f1d53 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt @@ -16,20 +16,36 @@ package im.vector.riotx.core.platform -import com.airbnb.mvrx.BaseMvRxViewModel -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.* import im.vector.matrix.android.api.util.CancelableBag import im.vector.riotx.BuildConfig +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.disposables.Disposable abstract class VectorViewModel(initialState: S) : BaseMvRxViewModel(initialState, false) { - protected val cancelableBag = CancelableBag() - - override fun onCleared() { - super.onCleared() - cancelableBag.cancel() + /** + * This method does the same thing as the execute function, but it doesn't subscribe to the stream + * so you can use this in a switchMap or a flatMap + */ + fun Single.toAsync(stateReducer: S.(Async) -> S): Single> { + setState { stateReducer(Loading()) } + return this.map { Success(it) as Async } + .onErrorReturn { Fail(it) } + .doOnSuccess { setState { stateReducer(it) } } } + /** + * This method does the same thing as the execute function, but it doesn't subscribe to the stream + * so you can use this in a switchMap or a flatMap + */ + fun Observable.toAsync(stateReducer: S.(Async) -> S): Observable> { + setState { stateReducer(Loading()) } + return this.map { Success(it) as Async } + .onErrorReturn { Fail(it) } + .doOnNext { setState { stateReducer(it) } } + } } \ No newline at end of file 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/CreateDirectRoomController.kt index 8e56813a..4d83477d 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/CreateDirectRoomController.kt @@ -19,13 +19,9 @@ package im.vector.riotx.features.home.createdirect import com.airbnb.epoxy.EpoxyController -import com.airbnb.epoxy.VisibilityState 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.failure.Failure import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.internal.util.firstLetterOfDisplayName import im.vector.riotx.core.epoxy.errorWithRetryItem @@ -60,7 +56,7 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer: } when (asyncUsers) { is Incomplete -> renderLoading() - is Success -> renderUsers(asyncUsers(), currentState.selectedUsers) + is Success -> renderUsers(asyncUsers(), currentState.selectedUsers.map { it.userId }) is Fail -> renderFailure(asyncUsers.error) } } @@ -79,10 +75,10 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer: } } - private fun renderUsers(users: List, selectedUsers: Set) { + private fun renderUsers(users: List, selectedUsers: List) { var lastFirstLetter: String? = null users.forEach { user -> - val isSelected = selectedUsers.contains(user) + val isSelected = selectedUsers.contains(user.userId) val currentFirstLetter = user.displayName.firstLetterOfDisplayName() val showLetter = currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter lastFirstLetter = currentFirstLetter 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 838c7d6e..f19abaa3 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 @@ -25,6 +25,7 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* import javax.inject.Inject @@ -82,6 +83,7 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec } override fun onItemClick(user: User) { + view?.hideKeyboard() viewModel.handle(CreateDirectRoomActions.SelectUser(user)) 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/CreateDirectRoomFragment.kt index e28346a6..6f0c1727 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/CreateDirectRoomFragment.kt @@ -19,21 +19,24 @@ package im.vector.riotx.features.home.createdirect import android.os.Bundle +import android.text.Spannable import android.view.MenuItem import androidx.lifecycle.ViewModelProviders -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.activityViewModel -import com.jakewharton.rxbinding3.appcompat.queryTextChanges +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.beforeTextChangeEvents +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.html.PillImageSpan import kotlinx.android.synthetic.main.fragment_create_direct_room.* -import kotlinx.android.synthetic.main.fragment_public_rooms.* import javax.inject.Inject class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomController.Callback { @@ -45,6 +48,7 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle private val viewModel: CreateDirectRoomViewModel by activityViewModel() @Inject lateinit var directRoomController: CreateDirectRoomController + @Inject lateinit var avatarRenderer: AvatarRenderer private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel override fun injectWith(injector: ScreenComponent) { @@ -59,6 +63,10 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle setupFilterView() setupAddByMatrixIdView() setupCloseView() + viewModel.selectUserEvent.observeEvent(this) { + updateFilterViewWith(it) + + } viewModel.subscribe(this) { renderState(it) } } @@ -90,16 +98,43 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle private fun setupFilterView() { createDirectRoomFilter - .queryTextChanges() - .subscribe { - val action = if (it.isNullOrEmpty()) { + .textChanges() + .subscribe { text -> + val userMatches = MatrixPatterns.PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER.findAll(text) + val lastUserMatch = userMatches.lastOrNull() + val filterValue = if (lastUserMatch == null) { + text + } else { + text.substring(startIndex = lastUserMatch.range.endInclusive + 1) + }.trim() + + val action = if (filterValue.isBlank()) { CreateDirectRoomActions.ClearFilterKnownUsers } else { - CreateDirectRoomActions.FilterKnownUsers(it.toString()) + CreateDirectRoomActions.FilterKnownUsers(filterValue.toString()) } viewModel.handle(action) } .disposeOnDestroy() + + createDirectRoomFilter + .beforeTextChangeEvents() + .subscribe { event -> + if (event.after == 0) { + val sub = event.text.substring(0, event.start) + val startIndexOfUser = sub.lastIndexOf(" ") + 1 + val user = sub.substring(startIndexOfUser) + val selectedUser = withState(viewModel) { state -> + state.selectedUsers.find { it.userId == user } + } + if (selectedUser != null) { + viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(selectedUser)) + } + } + } + .disposeOnDestroy() + + createDirectRoomFilter.requestFocus() } private fun setupCloseView() { @@ -109,11 +144,37 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle } private fun renderState(state: CreateDirectRoomViewState) { - directRoomController.setData(state) } + private fun updateFilterViewWith(data: SelectUserAction) = withState(viewModel) { state -> + if (state.selectedUsers.isEmpty()) { + createDirectRoomFilter.text = null + } else { + val editable = createDirectRoomFilter.editableText + val user = data.user + if (data.isAdded) { + val startIndex = editable.lastIndexOf(" ") + 1 + val endIndex = editable.length + editable.replace(startIndex, endIndex, "${user.userId} ") + val span = PillImageSpan(GlideApp.with(this), avatarRenderer, requireContext(), user.userId, user) + span.bind(createDirectRoomFilter) + editable.setSpan(span, startIndex, startIndex + user.userId.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + val startIndex = editable.indexOf(user.userId) + if (startIndex != -1) { + var endIndex = editable.indexOf(" ", startIndex) + 1 + if (endIndex == 0) { + endIndex = editable.length + } + editable.replace(startIndex, endIndex, "") + } + } + } + } + override fun onItemClick(user: User) { + view?.hideKeyboard() viewModel.handle(CreateDirectRoomActions.SelectUser(user)) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt index 33f7c22f..96a5ce0b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt @@ -28,9 +28,7 @@ import com.amulyakhare.textdrawable.TextDrawable import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel -import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.getColorFromUserId @EpoxyModelClass(layout = R.layout.item_create_direct_room_user) abstract class CreateDirectRoomUserItem : VectorEpoxyModel() { 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 481ca922..2d609388 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 @@ -18,10 +18,10 @@ package im.vector.riotx.features.home.createdirect +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import arrow.core.Option -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.* import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -29,7 +29,9 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.utils.LiveEvent import io.reactivex.Observable import io.reactivex.functions.BiFunction import java.util.concurrent.TimeUnit @@ -37,6 +39,11 @@ import java.util.concurrent.TimeUnit private typealias KnowUsersFilter = String private typealias DirectoryUsersSearch = String +data class SelectUserAction( + val user: User, + val isAdded: Boolean +) + class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateDirectRoomViewState, private val session: Session) @@ -50,6 +57,10 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) private val directoryUsersSearch = BehaviorRelay.create() + private val _selectUserEvent = MutableLiveData>() + val selectUserEvent: LiveData> + get() = _selectUserEvent + companion object : MvRxViewModelFactory { @JvmStatic @@ -78,7 +89,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted private fun createRoomAndInviteSelectedUsers() = withState { currentState -> val isDirect = currentState.selectedUsers.size == 1 val roomParams = CreateRoomParams().apply { - invitedUserIds = ArrayList(currentState.selectedUsers.map { user -> user.userId }) + invitedUserIds = ArrayList(currentState.selectedUsers.map { it.userId }) if (isDirect) { setDirectMessage() } @@ -92,33 +103,42 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState { - val selectedUsers = it.selectedUsers.minusElement(action.user) + val selectedUsers = it.selectedUsers.minus(action.user) setState { copy(selectedUsers = selectedUsers) } + _selectUserEvent.postLiveEvent(SelectUserAction(action.user, false)) } private fun handleSelectUser(action: CreateDirectRoomActions.SelectUser) = withState { - val selectedUsers = if (it.selectedUsers.contains(action.user)) { - it.selectedUsers.minusElement(action.user) + //Reset the filter asap + knownUsersFilter.accept(Option.empty()) + directoryUsersSearch.accept("") + + val isAddOperation: Boolean + val selectedUsers: Set + if (it.selectedUsers.contains(action.user)) { + selectedUsers = it.selectedUsers.minus(action.user) + isAddOperation = false } else { - it.selectedUsers.plus(action.user) + selectedUsers = it.selectedUsers.plus(action.user) + isAddOperation = true } setState { copy(selectedUsers = selectedUsers) } + _selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation)) } private fun observeDirectoryUsers() { directoryUsersSearch - .throttleLast(300, TimeUnit.MILLISECONDS) + .throttleLast(500, TimeUnit.MILLISECONDS) .switchMapSingle { search -> session.rx() .searchUsersDirectory(search, 50, emptySet()) .map { users -> users.sortedBy { it.displayName } } + .toAsync { copy(directoryUsers = it) } } - .execute { async -> - copy(directoryUsers = async) - } - + .subscribe() + .disposeOnClear() } private fun observeKnownUsers() { @@ -133,7 +153,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } else { users.filter { it.displayName?.contains(filterValue, ignoreCase = true) ?: false - || it.userId.contains(filterValue, ignoreCase = true) + || it.userId.contains(filterValue, ignoreCase = true) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index 513379bd..7aff4a32 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.rx.rx import im.vector.riotx.R +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.LiveEvent @@ -67,7 +68,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro private fun observeSelectionState() { selectSubscribe(GroupListViewState::selectedGroup) { if (it != null) { - _openGroupLiveData.postValue(LiveEvent(it)) + _openGroupLiveData.postLiveEvent(it) val optionGroup = Option.fromNullable(it) selectedGroupHolder.post(optionGroup) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 208b8e64..4b734c95 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -40,6 +40,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.UserPreferencesProvider @@ -168,62 +169,62 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) } is ParsedCommand.ErrorSyntax -> { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)) } is ParsedCommand.ErrorEmptySlashCommand -> { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/"))) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown("/")) } is ParsedCommand.ErrorUnknownSlashCommand -> { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)) } is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) } is ParsedCommand.SetUserPowerLevel -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.ClearScalarToken -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.SetMarkdown -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.UnbanUser -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.BanUser -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.KickUser -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.JoinRoom -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.PartRoom -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.SendEmote -> { room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled) } is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) } is ParsedCommand.ChangeDisplayName -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } } } @@ -255,7 +256,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro sendMode = SendMode.REGULAR ) } - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) } is SendMode.QUOTE -> { val messageContent: MessageContent? = @@ -280,7 +281,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro sendMode = SendMode.REGULAR ) } - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) } is SendMode.REPLY -> { state.sendMode.timelineEvent.let { @@ -290,7 +291,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro sendMode = SendMode.REGULAR ) } - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) } } @@ -319,29 +320,29 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled) room.updateTopic(changeTopic.topic, object : MatrixCallback { override fun onSuccess(data: Unit) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk) } override fun onFailure(failure: Throwable) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure))) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure)) } }) } private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled) room.invite(invite.userId, object : MatrixCallback { override fun onSuccess(data: Unit) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk) } override fun onFailure(failure: Throwable) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure))) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure)) } }) } @@ -453,19 +454,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), object : MatrixCallback { override fun onSuccess(data: File) { - _downloadedFileEvent.postValue(LiveEvent(DownloadFileState( + _downloadedFileEvent.postLiveEvent(DownloadFileState( action.messageFileContent.getMimeType(), data, null - ))) + )) } override fun onFailure(failure: Throwable) { - _downloadedFileEvent.postValue(LiveEvent(DownloadFileState( + _downloadedFileEvent.postLiveEvent(DownloadFileState( action.messageFileContent.getMimeType(), null, failure - ))) + )) } }) @@ -494,7 +495,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - _navigateToEvent.postValue(LiveEvent(targetEventId)) + _navigateToEvent.postLiveEvent(targetEventId) } else { // change timeline timeline.dispose() @@ -519,7 +520,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - _navigateToEvent.postValue(LiveEvent(targetEventId)) + _navigateToEvent.postLiveEvent(targetEventId) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index a1ae4fdf..f590a789 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.tag.RoomTag +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.features.home.HomeRoomListObservableStore @@ -142,7 +143,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room override fun onFailure(failure: Throwable) { // Notify the user - _invitationAnswerErrorLiveData.postValue(LiveEvent(failure)) + _invitationAnswerErrorLiveData.postLiveEvent(failure) setState { copy( @@ -178,7 +179,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room override fun onFailure(failure: Throwable) { // Notify the user - _invitationAnswerErrorLiveData.postValue(LiveEvent(failure)) + _invitationAnswerErrorLiveData.postLiveEvent(failure) setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index c47e8bbd..8d0b6284 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.LiveEvent import timber.log.Timber @@ -207,7 +208,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: override fun onFailure(failure: Throwable) { // Notify the user - _joinRoomErrorLiveData.postValue(LiveEvent(failure)) + _joinRoomErrorLiveData.postLiveEvent(failure) setState { copy( diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml index 3899d3d2..987f902b 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -71,16 +71,13 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar"> - + android:maxHeight="80dp" + android:importantForAutofill="no" + android:hint="@string/room_directory_search_hint"/> From 507bc2f6226cc47712d2dba35f8550a06f172c51 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 Jul 2019 21:31:58 +0200 Subject: [PATCH 08/14] UserEntity: fix not inserted at all --- .../android/internal/session/user/UserEntityFactory.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt index c5c059eb..7873bf2f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt @@ -31,12 +31,12 @@ internal object UserEntityFactory { } val roomMember = event.content.toModel() ?: return null // We only use JOIN and INVITED memberships to create User data - if (roomMember.membership != Membership.JOIN || roomMember.membership != Membership.INVITE) { + if (roomMember.membership != Membership.JOIN && roomMember.membership != Membership.INVITE) { return null } return UserEntity(event.stateKey ?: "", - roomMember.displayName ?: "", - roomMember.avatarUrl ?: "" + roomMember.displayName ?: "", + roomMember.avatarUrl ?: "" ) } From 5af6bf3762e460e7db1bb4860e59ba14d1091f01 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 Jul 2019 18:28:03 +0200 Subject: [PATCH 09/14] Direct room: finally handle selection with chips (not as Nad design) --- vector/build.gradle | 2 +- .../core/platform/MaxHeightScrollView.kt | 72 ++++++++++++ .../createdirect/CreateDirectRoomActivity.kt | 14 ++- .../CreateDirectRoomDirectoryUsersFragment.kt | 2 + .../createdirect/CreateDirectRoomFragment.kt | 104 +++++++++--------- .../createdirect/CreateDirectRoomUserItem.kt | 7 +- .../createdirect/CreateDirectRoomViewModel.kt | 32 +++--- .../layout/fragment_create_direct_room.xml | 48 ++++++-- .../layout/item_create_direct_room_user.xml | 1 + .../values/attrs_max_height_scroll_view.xml | 6 + 10 files changed, 201 insertions(+), 87 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt create mode 100644 vector/src/main/res/values/attrs_max_height_scroll_view.xml diff --git a/vector/build.gradle b/vector/build.gradle index db9ad6d6..a8645747 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -206,7 +206,7 @@ dependencies { // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.1.0-alpha07' + implementation 'com.google.android.material:material:1.1.0-alpha08' implementation 'me.gujun.android:span:1.7' implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version" diff --git a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt new file mode 100644 index 00000000..92796bbd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt @@ -0,0 +1,72 @@ +/* + * 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.core.platform + +import android.annotation.TargetApi +import android.content.Context +import android.content.res.TypedArray +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.widget.ScrollView + +import im.vector.riotx.R + +private const val DEFAULT_MAX_HEIGHT = 200 + +class MaxHeightScrollView : ScrollView { + + var maxHeight: Int = 0 + set(value) { + field = value + requestLayout() + } + + constructor(context: Context) : super(context) {} + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + if (!isInEditMode) { + init(context, attrs) + } + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + if (!isInEditMode) { + init(context, attrs) + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { + if (!isInEditMode) { + init(context, attrs) + } + } + + private fun init(context: Context, attrs: AttributeSet?) { + if (attrs != null) { + val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView) + maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) + styledAttrs.recycle() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST) + super.onMeasure(widthMeasureSpec, newHeightMeasureSpec) + } +} 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 82b8b812..c31cb8c0 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 @@ -24,11 +24,11 @@ import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog import androidx.lifecycle.ViewModelProviders +import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel -import com.google.android.gms.common.GooglePlayServicesNotAvailableException import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter @@ -72,14 +72,16 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { if (isFirstCreation()) { addFragment(CreateDirectRoomFragment(), R.id.container) } - viewModel.subscribe(this) { renderState(it) } + viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) { + renderCreateAndInviteState(it) + } } - private fun renderState(state: CreateDirectRoomViewState) { - when (state.createAndInviteState) { + private fun renderCreateAndInviteState(state: Async) { + when (state) { is Loading -> renderCreationLoading() - is Success -> renderCreationSuccess(state.createAndInviteState()) - is Fail -> renderCreationFailure(state.createAndInviteState.error) + is Success -> renderCreationSuccess(state()) + is Fail -> renderCreationFailure(state.error) } } 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 f19abaa3..ad3a8f33 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 @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.extensions.setupAsSearch import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* import javax.inject.Inject @@ -60,6 +61,7 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec } private fun setupSearchByMatrixIdView() { + createDirectRoomSearchById.setupAsSearch() createDirectRoomSearchById .textChanges() .subscribe { 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/CreateDirectRoomFragment.kt index 6f0c1727..57c1783b 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/CreateDirectRoomFragment.kt @@ -19,23 +19,25 @@ package im.vector.riotx.features.home.createdirect import android.os.Bundle -import android.text.Spannable +import android.view.Menu import android.view.MenuItem +import android.widget.ScrollView +import androidx.core.view.size import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.beforeTextChangeEvents +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup import com.jakewharton.rxbinding3.widget.textChanges -import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.observeEvent -import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.core.extensions.setupAsSearch import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.DimensionUtils import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.html.PillImageSpan import kotlinx.android.synthetic.main.fragment_create_direct_room.* import javax.inject.Inject @@ -64,12 +66,23 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle setupAddByMatrixIdView() setupCloseView() viewModel.selectUserEvent.observeEvent(this) { - updateFilterViewWith(it) - + updateChipsView(it) + } + viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) { + renderSelectedUsers(it) } viewModel.subscribe(this) { renderState(it) } } + override fun onPrepareOptionsMenu(menu: Menu) { + withState(viewModel) { + val createMenuItem = menu.findItem(R.id.action_create_direct_room) + val showMenuItem = it.selectedUsers.isNotEmpty() + createMenuItem.setVisible(showMenuItem) + } + super.onPrepareOptionsMenu(menu) + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_create_direct_room -> { @@ -100,14 +113,7 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle createDirectRoomFilter .textChanges() .subscribe { text -> - val userMatches = MatrixPatterns.PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER.findAll(text) - val lastUserMatch = userMatches.lastOrNull() - val filterValue = if (lastUserMatch == null) { - text - } else { - text.substring(startIndex = lastUserMatch.range.endInclusive + 1) - }.trim() - + val filterValue = text.trim() val action = if (filterValue.isBlank()) { CreateDirectRoomActions.ClearFilterKnownUsers } else { @@ -117,23 +123,7 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle } .disposeOnDestroy() - createDirectRoomFilter - .beforeTextChangeEvents() - .subscribe { event -> - if (event.after == 0) { - val sub = event.text.substring(0, event.start) - val startIndexOfUser = sub.lastIndexOf(" ") + 1 - val user = sub.substring(startIndexOfUser) - val selectedUser = withState(viewModel) { state -> - state.selectedUsers.find { it.userId == user } - } - if (selectedUser != null) { - viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(selectedUser)) - } - } - } - .disposeOnDestroy() - + createDirectRoomFilter.setupAsSearch() createDirectRoomFilter.requestFocus() } @@ -147,32 +137,40 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle directRoomController.setData(state) } - private fun updateFilterViewWith(data: SelectUserAction) = withState(viewModel) { state -> - if (state.selectedUsers.isEmpty()) { - createDirectRoomFilter.text = null + private fun updateChipsView(data: SelectUserAction) { + if (data.isAdded) { + addChipToGroup(data.user, chipGroup) } else { - val editable = createDirectRoomFilter.editableText - val user = data.user - if (data.isAdded) { - val startIndex = editable.lastIndexOf(" ") + 1 - val endIndex = editable.length - editable.replace(startIndex, endIndex, "${user.userId} ") - val span = PillImageSpan(GlideApp.with(this), avatarRenderer, requireContext(), user.userId, user) - span.bind(createDirectRoomFilter) - editable.setSpan(span, startIndex, startIndex + user.userId.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } else { - val startIndex = editable.indexOf(user.userId) - if (startIndex != -1) { - var endIndex = editable.indexOf(" ", startIndex) + 1 - if (endIndex == 0) { - endIndex = editable.length - } - editable.replace(startIndex, endIndex, "") - } + if (chipGroup.size > data.index) { + chipGroup.removeViewAt(data.index) } } } + private fun renderSelectedUsers(selectedUsers: Set) { + vectorBaseActivity.invalidateOptionsMenu() + if (selectedUsers.isNotEmpty() && chipGroup.size == 0) { + selectedUsers.forEach { addChipToGroup(it, chipGroup) } + } + } + + private fun addChipToGroup(user: User, chipGroup: ChipGroup) { + val chip = Chip(requireContext()) + chip.setChipBackgroundColorResource(android.R.color.transparent) + chip.chipStrokeWidth = DimensionUtils.dpToPx(1, requireContext()).toFloat() + chip.text = if (user.displayName.isNullOrBlank()) user.userId else user.displayName + chip.isClickable = true + chip.isCheckable = false + chip.isCloseIconVisible = true + chipGroup.addView(chip) + chip.setOnCloseIconClickListener { + viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(user)) + } + chipGroupContainer.post { + chipGroupContainer.fullScroll(ScrollView.FOCUS_DOWN) + } + } + override fun onItemClick(user: User) { view?.hideKeyboard() viewModel.handle(CreateDirectRoomActions.SelectUser(user)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt index 96a5ce0b..c6d7f85b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt @@ -40,6 +40,7 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel + val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId } + val selectedUsers = state.selectedUsers.minus(action.user) setState { copy(selectedUsers = selectedUsers) } - _selectUserEvent.postLiveEvent(SelectUserAction(action.user, false)) + _selectUserEvent.postLiveEvent(SelectUserAction(action.user, false, index)) } - private fun handleSelectUser(action: CreateDirectRoomActions.SelectUser) = withState { + private fun handleSelectUser(action: CreateDirectRoomActions.SelectUser) = withState { state -> //Reset the filter asap - knownUsersFilter.accept(Option.empty()) directoryUsersSearch.accept("") - val isAddOperation: Boolean val selectedUsers: Set - if (it.selectedUsers.contains(action.user)) { - selectedUsers = it.selectedUsers.minus(action.user) - isAddOperation = false - } else { - selectedUsers = it.selectedUsers.plus(action.user) + val indexOfUser = state.selectedUsers.indexOfFirst { it.userId == action.user.userId } + val changeIndex: Int + if (indexOfUser == -1) { + changeIndex = state.selectedUsers.size + selectedUsers = state.selectedUsers.plus(action.user) isAddOperation = true + } else { + changeIndex = indexOfUser + selectedUsers = state.selectedUsers.minus(action.user) + isAddOperation = false } setState { copy(selectedUsers = selectedUsers) } - _selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation)) + _selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation, changeIndex)) } private fun observeDirectoryUsers() { @@ -153,7 +157,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } else { users.filter { it.displayName?.contains(filterValue, ignoreCase = true) ?: false - || it.userId.contains(filterValue, ignoreCase = true) + || it.userId.contains(filterValue, ignoreCase = true) } } } diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml index 987f902b..11a74d6b 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -58,28 +58,52 @@ - + app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar" + app:maxHeight="80dp"> - + app:lineSpacing="4dp" /> - + + + + + + app:layout_constraintTop_toBottomOf="@id/createDirectRoomFilterDivider" /> diff --git a/vector/src/main/res/values/attrs_max_height_scroll_view.xml b/vector/src/main/res/values/attrs_max_height_scroll_view.xml new file mode 100644 index 00000000..1b135066 --- /dev/null +++ b/vector/src/main/res/values/attrs_max_height_scroll_view.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From 76a9625f25088141d8c0d496a4652cbb9b343cf1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 Jul 2019 16:26:45 +0200 Subject: [PATCH 10/14] Direct chat : finalize flow --- .../session/user/DefaultUserService.kt | 1 - .../vector/riotx/core/extensions/EditText.kt | 9 ++-- .../createdirect/CreateDirectRoomActivity.kt | 2 +- .../CreateDirectRoomController.kt | 53 ++++++++++++++++--- .../CreateDirectRoomDirectoryUsersFragment.kt | 8 +-- .../createdirect/CreateDirectRoomFragment.kt | 9 ++-- .../createdirect/CreateDirectRoomViewModel.kt | 25 ++++++--- .../createdirect/CreateDirectRoomViewState.kt | 5 +- .../layout/fragment_create_direct_room.xml | 18 ++++--- ...ent_create_direct_room_directory_users.xml | 16 ++++-- vector/src/main/res/values/strings_riotX.xml | 5 +- 11 files changed, 107 insertions(+), 44 deletions(-) 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 1db73c84..06a7ba7f 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 @@ -31,7 +31,6 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.user.model.SearchUserTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.task.toConfigurableTask import im.vector.matrix.android.internal.util.fetchCopied import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt b/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt index ecc7795a..ace13754 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt @@ -23,13 +23,16 @@ import android.view.MotionEvent import android.view.View import android.view.inputmethod.EditorInfo import android.widget.EditText +import androidx.annotation.DrawableRes import im.vector.riotx.R -fun EditText.setupAsSearch() { +fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter, + @DrawableRes clearIconRes: Int = R.drawable.ic_x_green) { + addTextChangedListener(object : TextWatcher { override fun afterTextChanged(editable: Editable?) { - val clearIcon = if (editable?.isNotEmpty() == true) R.drawable.ic_clear_white else 0 - setCompoundDrawablesWithIntrinsicBounds(0, 0, clearIcon, 0) + val clearIcon = if (editable?.isNotEmpty() == true) clearIconRes else 0 + setCompoundDrawablesWithIntrinsicBounds(searchIconRes, 0, clearIcon, 0) } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit 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 c31cb8c0..eb26c321 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 @@ -86,7 +86,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { } private fun renderCreationLoading() { - updateWaitingView(WaitingViewData(getString(R.string.room_recents_create_room))) + updateWaitingView(WaitingViewData(getString(R.string.creating_direct_room))) } private fun renderCreationFailure(error: Throwable) { 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/CreateDirectRoomController.kt index 4d83477d..80ee4fb9 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/CreateDirectRoomController.kt @@ -18,19 +18,25 @@ package im.vector.riotx.features.home.createdirect +import arrow.core.Option import com.airbnb.epoxy.EpoxyController -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Incomplete -import com.airbnb.mvrx.Success +import com.airbnb.mvrx.* +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.firstLetterOfDisplayName +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.NoResultItem_ 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 CreateDirectRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer, +class CreateDirectRoomController @Inject constructor(private val session: Session, + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider, private val errorFormatter: ErrorFormatter) : EpoxyController() { private var state: CreateDirectRoomViewState? = null @@ -49,15 +55,17 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer: override fun buildModels() { val currentState = state ?: return + val hasSearch = currentState.searchTerm.isNotBlank() val asyncUsers = if (displayMode == CreateDirectRoomViewState.DisplayMode.DIRECTORY_USERS) { currentState.directoryUsers } else { currentState.knownUsers } when (asyncUsers) { - is Incomplete -> renderLoading() - is Success -> renderUsers(asyncUsers(), currentState.selectedUsers.map { it.userId }) - is Fail -> renderFailure(asyncUsers.error) + is Uninitialized -> renderEmptyState(false) + is Loading -> renderLoading() + is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch) + is Fail -> renderFailure(asyncUsers.error) } } @@ -75,9 +83,22 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer: } } + private fun renderSuccess(users: List, + selectedUsers: List, + hasSearch: Boolean) { + if (users.isEmpty()) { + renderEmptyState(hasSearch) + } else { + renderUsers(users, selectedUsers) + } + } + private fun renderUsers(users: List, selectedUsers: List) { var lastFirstLetter: String? = null - users.forEach { user -> + for (user in users) { + if (user.userId == session.myUserId) { + continue + } val isSelected = selectedUsers.contains(user.userId) val currentFirstLetter = user.displayName.firstLetterOfDisplayName() val showLetter = currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter @@ -102,6 +123,22 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer: } } + 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 + } + } else { + R.string.direct_room_no_known_users + } + noResultItem { + id("noResult") + text(stringProvider.getString(noResultRes)) + } + } + interface Callback { fun onItemClick(user: User) fun retryDirectoryUsersRequest() { 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 ad3a8f33..f7f9eca9 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 @@ -21,6 +21,7 @@ import android.os.Bundle import android.view.inputmethod.InputMethodManager import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R @@ -50,7 +51,6 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec setupRecyclerView() setupSearchByMatrixIdView() setupCloseView() - viewModel.subscribe(this) { renderState(it) } } private fun setupRecyclerView() { @@ -61,7 +61,7 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec } private fun setupSearchByMatrixIdView() { - createDirectRoomSearchById.setupAsSearch() + createDirectRoomSearchById.setupAsSearch(searchIconRes = 0) createDirectRoomSearchById .textChanges() .subscribe { @@ -80,8 +80,8 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec } } - private fun renderState(state: CreateDirectRoomViewState) { - directRoomController.setData(state) + override fun invalidate() = withState(viewModel) { + directRoomController.setData(it) } override fun onItemClick(user: User) { 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/CreateDirectRoomFragment.kt index 57c1783b..63c6aaf1 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/CreateDirectRoomFragment.kt @@ -71,7 +71,6 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) { renderSelectedUsers(it) } - viewModel.subscribe(this) { renderState(it) } } override fun onPrepareOptionsMenu(menu: Menu) { @@ -133,8 +132,8 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle } } - private fun renderState(state: CreateDirectRoomViewState) { - directRoomController.setData(state) + override fun invalidate() = withState(viewModel) { + directRoomController.setData(it) } private fun updateChipsView(data: SelectUserAction) { @@ -166,8 +165,8 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle chip.setOnCloseIconClickListener { viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(user)) } - chipGroupContainer.post { - chipGroupContainer.fullScroll(ScrollView.FOCUS_DOWN) + chipGroupScrollView.post { + chipGroupScrollView.fullScroll(ScrollView.FOCUS_DOWN) } } 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 9ce91585..71ef00e0 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 @@ -28,11 +28,14 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.util.firstLetterOfDisplayName import im.vector.matrix.rx.rx import im.vector.riotx.core.extensions.postLiveEvent 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 @@ -132,14 +135,20 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted private fun observeDirectoryUsers() { directoryUsersSearch - .throttleLast(500, TimeUnit.MILLISECONDS) + .debounce(300, TimeUnit.MILLISECONDS) .switchMapSingle { search -> - session.rx() - .searchUsersDirectory(search, 50, emptySet()) - .map { users -> - users.sortedBy { it.displayName } - } - .toAsync { copy(directoryUsers = it) } + val stream = if (search.isBlank()) { + Single.just(emptyList()) + } else { + session.rx() + .searchUsersDirectory(search, 50, emptySet()) + .map { users -> + users.sortedBy { it.displayName.firstLetterOfDisplayName() } + } + } + stream.toAsync { + copy(directoryUsers = it, searchTerm = search) + } } .subscribe() .disposeOnClear() @@ -157,7 +166,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } else { users.filter { it.displayName?.contains(filterValue, ignoreCase = true) ?: false - || it.userId.contains(filterValue, ignoreCase = true) + || it.userId.contains(filterValue, ignoreCase = true) } } } 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 56607802..e6bc212f 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 arrow.core.Option import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized @@ -27,7 +28,9 @@ data class CreateDirectRoomViewState( val knownUsers: Async> = Uninitialized, val directoryUsers: Async> = Uninitialized, val selectedUsers: Set = emptySet(), - val createAndInviteState: Async = Uninitialized + val createAndInviteState: Async = Uninitialized, + val searchTerm: String = "", + val filterKnownUsersValue: Option = Option.empty() ) : MvRxState { enum class DisplayMode { diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml index 11a74d6b..427df615 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -44,7 +44,7 @@ android:layout_marginEnd="8dp" android:ellipsize="end" android:maxLines="1" - android:text="@string/direct_chats_header" + android:text="@string/fab_menu_create_chat" android:textColor="?riotx_text_primary" android:textSize="18sp" android:textStyle="bold" @@ -59,7 +59,7 @@ + app:maxHeight="64dp"> + app:lineSpacing="2dp" /> @@ -85,21 +85,23 @@ android:layout_marginStart="@dimen/layout_horizontal_margin" android:layout_marginEnd="@dimen/layout_horizontal_margin" android:background="@null" - android:hint="@string/room_directory_search_hint" + android:drawablePadding="8dp" + android:gravity="center_vertical" + android:hint="@string/direct_room_filter_hint" android:importantForAutofill="no" android:maxHeight="80dp" - android:padding="8dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/chipGroupContainer" /> + app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollView" /> + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 504cee65..45dc5c53 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -3,5 +3,8 @@ Add by matrix ID - + "Creating room…" + "No result found, use Add by matrix ID to search on server." + "Start typing to get results" + "Filter by username or ID…" \ No newline at end of file From 0255696c88cc0a6d03394baa471c630ef62651b8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 Jul 2019 16:49:15 +0200 Subject: [PATCH 11/14] Update CHANGES --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1ba5b11e..777b3bfc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.3.0 (2019-XX-XX) =================================================== Features: - - + - Create Direct Room flow Improvements: - From 65f0af918f007cebb9897f0dc0db49d5b42d5f6f Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 29 Jul 2019 18:26:26 +0200 Subject: [PATCH 12/14] Remove default identity server as we don't use it. --- .../auth/data/HomeServerConnectionConfig.kt | 4 ++-- .../room/model/create/CreateRoomParams.kt | 19 +++++++-------- .../homeserver/ServerUrlsRepository.kt | 24 ++----------------- .../riotx/features/login/LoginActivity.kt | 8 ++----- vector/src/main/res/values/config.xml | 12 +--------- 5 files changed, 16 insertions(+), 51 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt index f231d3f1..850c4f71 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt @@ -31,7 +31,7 @@ import okhttp3.TlsVersion @JsonClass(generateAdapter = true) data class HomeServerConnectionConfig( val homeServerUri: Uri, - val identityServerUri: Uri, + val identityServerUri: Uri? = null, val antiVirusServerUri: Uri? = null, val allowedFingerprints: MutableList = ArrayList(), val shouldPin: Boolean = false, @@ -48,7 +48,7 @@ data class HomeServerConnectionConfig( class Builder { private lateinit var homeServerUri: Uri - private lateinit var identityServerUri: Uri + private var identityServerUri: Uri? = null private var antiVirusServerUri: Uri? = null private val allowedFingerprints: MutableList = ArrayList() private var shouldPin: Boolean = false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index 77c4fcf1..94d6e0a8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -128,8 +128,8 @@ class CreateRoomParams { contentMap["algorithm"] = algorithm val algoEvent = Event(type = EventType.ENCRYPTION, - stateKey = "", - content = contentMap.toContent() + stateKey = "", + content = contentMap.toContent() ) if (null == initialStates) { @@ -162,8 +162,8 @@ class CreateRoomParams { contentMap["history_visibility"] = historyVisibility val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY, - stateKey = "", - content = contentMap.toContent()) + stateKey = "", + content = contentMap.toContent()) if (null == initialStates) { initialStates = Arrays.asList(historyVisibilityEvent) @@ -202,8 +202,8 @@ class CreateRoomParams { */ fun isDirect(): Boolean { return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT - && isDirect == true - && (1 == getInviteCount() || 1 == getInvite3PidCount()) + && isDirect == true + && (1 == getInviteCount() || 1 == getInvite3PidCount()) } /** @@ -223,14 +223,13 @@ class CreateRoomParams { credentials: Credentials, ids: List) { for (id in ids) { - if (Patterns.EMAIL_ADDRESS.matcher(id).matches()) { + if (Patterns.EMAIL_ADDRESS.matcher(id).matches() && hsConfig.identityServerUri != null) { if (null == invite3pids) { invite3pids = ArrayList() } - val pid = Invite3Pid(idServer = hsConfig.identityServerUri.host!!, - medium = ThreePidMedium.EMAIL, - address = id) + medium = ThreePidMedium.EMAIL, + address = id) invite3pids!!.add(pid) } else if (isUserId(id)) { diff --git a/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt b/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt index 37ac4400..18587086 100644 --- a/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt +++ b/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt @@ -69,39 +69,19 @@ object ServerUrlsRepository { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getString(HOME_SERVER_URL_PREF, - prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, - getDefaultHomeServerUrl(context))) + prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, + getDefaultHomeServerUrl(context))) } - /** - * Return last used identity server url, or the default one from referrer or the default one from resources - */ - fun getLastIdentityServerUrl(context: Context): String { - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - - return prefs.getString(IDENTITY_SERVER_URL_PREF, - prefs.getString(DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF, - getDefaultIdentityServerUrl(context))) - } - /** * Return true if url is the default home server url form resources */ fun isDefaultHomeServerUrl(context: Context, url: String) = url == getDefaultHomeServerUrl(context) - /** - * Return true if url is the default identity server url form resources - */ - fun isDefaultIdentityServerUrl(context: Context, url: String) = url == getDefaultIdentityServerUrl(context) - /** * Return default home server url from resources */ fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.default_hs_server_url) - /** - * Return default identity server url from resources - */ - fun getDefaultIdentityServerUrl(context: Context): String = context.getString(R.string.default_identity_server_url) } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 53519fc4..41eed536 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -37,6 +37,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.home.HomeActivity +import im.vector.riotx.features.homeserver.ServerUrlsRepository import im.vector.riotx.features.notifications.PushRuleTriggerListener import io.reactivex.Observable import io.reactivex.functions.Function3 @@ -44,9 +45,6 @@ import io.reactivex.rxkotlin.subscribeBy import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject -private const val DEFAULT_HOME_SERVER_URI = "https://matrix.org" -private const val DEFAULT_IDENTITY_SERVER_URI = "https://vector.im" -private const val DEFAULT_ANTIVIRUS_SERVER_URI = "https://matrix.org" class LoginActivity : VectorBaseActivity() { @@ -66,7 +64,7 @@ class LoginActivity : VectorBaseActivity() { setupNotice() setupAuthButton() setupPasswordReveal() - homeServerField.setText(DEFAULT_HOME_SERVER_URI) + homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(this)) } private fun setupNotice() { @@ -118,8 +116,6 @@ class LoginActivity : VectorBaseActivity() { val homeServerUri = homeServerField.text?.trim().toString() HomeServerConnectionConfig.Builder() .withHomeServerUri(homeServerUri) - .withIdentityServerUri(DEFAULT_IDENTITY_SERVER_URI) - .withAntiVirusServerUri(DEFAULT_ANTIVIRUS_SERVER_URI) .build() } } diff --git a/vector/src/main/res/values/config.xml b/vector/src/main/res/values/config.xml index 508b942a..4d81a54e 100755 --- a/vector/src/main/res/values/config.xml +++ b/vector/src/main/res/values/config.xml @@ -4,26 +4,16 @@ - https://vector.im https://matrix.org https://matrix.org - https://vector.im https://piwik.riot.im https://riot.im/bugreports/submit - - "https://scalar-staging.riot.im/scalar-web/" - "https://scalar-staging.riot.im/scalar/api" - - - https://scalar-staging.riot.im/scalar/api - https://scalar.vector.im/api - - + https://matrix.org/_matrix/push/v1/notify im.vector.app.android From ff6ce8a4b771ba6655dd69cfebbf7acf1ca45214 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 29 Jul 2019 19:13:06 +0200 Subject: [PATCH 13/14] Create direct : remove letter headers when filtering --- .../createdirect/CreateDirectRoomController.kt | 16 ++++++++-------- .../createdirect/CreateDirectRoomViewModel.kt | 10 ++++++---- .../createdirect/CreateDirectRoomViewState.kt | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) 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/CreateDirectRoomController.kt index 80ee4fb9..eea10452 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/CreateDirectRoomController.kt @@ -18,14 +18,12 @@ package im.vector.riotx.features.home.createdirect -import arrow.core.Option import com.airbnb.epoxy.EpoxyController import com.airbnb.mvrx.* 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.firstLetterOfDisplayName import im.vector.riotx.R -import im.vector.riotx.core.epoxy.NoResultItem_ import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.noResultItem @@ -55,7 +53,8 @@ class CreateDirectRoomController @Inject constructor(private val session: Sessio override fun buildModels() { val currentState = state ?: return - val hasSearch = currentState.searchTerm.isNotBlank() + val hasSearch = currentState.directorySearchTerm.isNotBlank() + val isFiltering = currentState.filterKnownUsersValue.nonEmpty() val asyncUsers = if (displayMode == CreateDirectRoomViewState.DisplayMode.DIRECTORY_USERS) { currentState.directoryUsers } else { @@ -64,7 +63,7 @@ class CreateDirectRoomController @Inject constructor(private val session: Sessio when (asyncUsers) { is Uninitialized -> renderEmptyState(false) is Loading -> renderLoading() - is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch) + is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch, isFiltering) is Fail -> renderFailure(asyncUsers.error) } } @@ -85,15 +84,16 @@ class CreateDirectRoomController @Inject constructor(private val session: Sessio private fun renderSuccess(users: List, selectedUsers: List, - hasSearch: Boolean) { + hasSearch: Boolean, + isFiltering: Boolean) { if (users.isEmpty()) { renderEmptyState(hasSearch) } else { - renderUsers(users, selectedUsers) + renderUsers(users, selectedUsers, isFiltering) } } - private fun renderUsers(users: List, selectedUsers: List) { + private fun renderUsers(users: List, selectedUsers: List, isFiltering: Boolean) { var lastFirstLetter: String? = null for (user in users) { if (user.userId == session.myUserId) { @@ -101,7 +101,7 @@ class CreateDirectRoomController @Inject constructor(private val session: Sessio } val isSelected = selectedUsers.contains(user.userId) val currentFirstLetter = user.displayName.firstLetterOfDisplayName() - val showLetter = currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter + val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter lastFirstLetter = currentFirstLetter CreateDirectRoomLetterHeaderItem_() 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 71ef00e0..a9351418 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,7 +35,6 @@ 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 @@ -147,7 +146,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } } stream.toAsync { - copy(directoryUsers = it, searchTerm = search) + copy(directoryUsers = it, directorySearchTerm = search) } } .subscribe() @@ -166,12 +165,15 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } else { users.filter { it.displayName?.contains(filterValue, ignoreCase = true) ?: false - || it.userId.contains(filterValue, ignoreCase = true) + || it.userId.contains(filterValue, ignoreCase = true) } } } ).execute { async -> - copy(knownUsers = 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 e6bc212f..95c76008 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 @@ -29,7 +29,7 @@ data class CreateDirectRoomViewState( val directoryUsers: Async> = Uninitialized, val selectedUsers: Set = emptySet(), val createAndInviteState: Async = Uninitialized, - val searchTerm: String = "", + val directorySearchTerm: String = "", val filterKnownUsersValue: Option = Option.empty() ) : MvRxState { From 6deba311112200fea9ed959ae758e2798b00dc05 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 30 Jul 2019 14:51:14 +0200 Subject: [PATCH 14/14] Direct room: finally use PagedList as we can get a lot of users in DB. --- matrix-sdk-android-rx/build.gradle | 2 + .../java/im/vector/matrix/rx/RxSession.kt | 5 + .../android/api/session/user/UserService.kt | 8 ++ .../internal/database/RealmQueryLatch.kt | 14 +- .../session/room/create/CreateRoomTask.kt | 7 +- .../room/membership/joining/JoinRoomTask.kt | 7 +- .../session/user/DefaultUserService.kt | 42 +++++- vector/build.gradle | 6 +- .../vector/riotx/core/di/ScreenComponent.kt | 4 +- .../createdirect/CreateDirectRoomActivity.kt | 2 +- .../CreateDirectRoomDirectoryUsersFragment.kt | 7 +- ... => CreateDirectRoomKnownUsersFragment.kt} | 6 +- .../createdirect/CreateDirectRoomViewModel.kt | 24 ++-- .../createdirect/CreateDirectRoomViewState.kt | 3 +- ...troller.kt => DirectoryUsersController.kt} | 50 ++----- .../home/createdirect/KnownUsersController.kt | 130 ++++++++++++++++++ 16 files changed, 245 insertions(+), 72 deletions(-) rename vector/src/main/java/im/vector/riotx/features/home/createdirect/{CreateDirectRoomFragment.kt => CreateDirectRoomKnownUsersFragment.kt} (96%) rename vector/src/main/java/im/vector/riotx/features/home/createdirect/{CreateDirectRoomController.kt => DirectoryUsersController.kt} (64%) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 546922f2..655df2c2 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 4126ff6f..97661ceb 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 292c90ef..d3c58edd 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 64afa3d4..1fc60d80 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 e77cafc8..f9cad783 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 e71a9fe3..7d78069e 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 06a7ba7f..8d47d401 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 a8645747..85a4dae1 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 90284011..35cda2e6 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 eb26c321..13bc9368 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 f7f9eca9..3916ff7b 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 63c6aaf1..77473366 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 a9351418..b0fed9b8 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 95c76008..e1c9ad46 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 eea10452..c174ac6b 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 00000000..fbb1cfcc --- /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