Make a generic user directory search & selection views.

This commit is contained in:
onurays 2020-04-30 02:50:30 +03:00
parent f25c981173
commit a4eba653a3
24 changed files with 780 additions and 269 deletions

View file

@ -23,8 +23,6 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment
@ -63,6 +61,8 @@ import im.vector.riotx.features.login.LoginSplashFragment
import im.vector.riotx.features.login.LoginWaitForEmailFragment import im.vector.riotx.features.login.LoginWaitForEmailFragment
import im.vector.riotx.features.login.LoginWebFragment import im.vector.riotx.features.login.LoginWebFragment
import im.vector.riotx.features.login.terms.LoginTermsFragment import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.userdirectory.KnownUsersFragment
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
import im.vector.riotx.features.qrcode.QrCodeScannerFragment import im.vector.riotx.features.qrcode.QrCodeScannerFragment
import im.vector.riotx.features.reactions.EmojiChooserFragment import im.vector.riotx.features.reactions.EmojiChooserFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment
@ -226,13 +226,13 @@ interface FragmentModule {
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(CreateDirectRoomDirectoryUsersFragment::class) @FragmentKey(UserDirectoryFragment::class)
fun bindCreateDirectRoomDirectoryUsersFragment(fragment: CreateDirectRoomDirectoryUsersFragment): Fragment fun bindUserDirectoryFragment(fragment: UserDirectoryFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(CreateDirectRoomKnownUsersFragment::class) @FragmentKey(KnownUsersFragment::class)
fun bindCreateDirectRoomKnownUsersFragment(fragment: CreateDirectRoomKnownUsersFragment): Fragment fun bindKnownUsersFragment(fragment: KnownUsersFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap

View file

@ -22,7 +22,6 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import im.vector.riotx.core.platform.ConfigurationViewModel import im.vector.riotx.core.platform.ConfigurationViewModel
import im.vector.riotx.features.createdirect.CreateDirectRoomSharedActionViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
@ -32,6 +31,7 @@ import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel
import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.riotx.features.login.LoginSharedActionViewModel import im.vector.riotx.features.login.LoginSharedActionViewModel
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.reactions.EmojiChooserViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel
@ -87,8 +87,8 @@ interface ViewModelModule {
@Binds @Binds
@IntoMap @IntoMap
@ViewModelKey(CreateDirectRoomSharedActionViewModel::class) @ViewModelKey(UserDirectorySharedActionViewModel::class)
fun bindCreateDirectRoomSharedActionViewModel(viewModel: CreateDirectRoomSharedActionViewModel): ViewModel fun bindUserDirectorySharedActionViewModel(viewModel: UserDirectorySharedActionViewModel): ViewModel
@Binds @Binds
@IntoMap @IntoMap

View file

@ -1,11 +1,11 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -20,10 +20,5 @@ import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
sealed class CreateDirectRoomAction : VectorViewModelAction { sealed class CreateDirectRoomAction : VectorViewModelAction {
object CreateRoomAndInviteSelectedUsers : CreateDirectRoomAction() data class CreateRoomAndInviteSelectedUsers(val selectedUsers: Set<User>) : CreateDirectRoomAction()
data class FilterKnownUsers(val value: String) : CreateDirectRoomAction()
data class SearchDirectoryUsers(val value: String) : CreateDirectRoomAction()
object ClearFilterKnownUsers : CreateDirectRoomAction()
data class SelectUser(val user: User) : CreateDirectRoomAction()
data class RemoveSelectedUser(val user: User) : CreateDirectRoomAction()
} }

View file

@ -37,6 +37,12 @@ import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.features.userdirectory.KnownUsersFragment
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.riotx.features.userdirectory.UserDirectoryViewModel
import kotlinx.android.synthetic.main.activity.* import kotlinx.android.synthetic.main.activity.*
import java.net.HttpURLConnection import java.net.HttpURLConnection
import javax.inject.Inject import javax.inject.Inject
@ -44,7 +50,8 @@ import javax.inject.Inject
class CreateDirectRoomActivity : SimpleFragmentActivity() { class CreateDirectRoomActivity : SimpleFragmentActivity() {
private val viewModel: CreateDirectRoomViewModel by viewModel() private val viewModel: CreateDirectRoomViewModel by viewModel()
private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
@ -56,26 +63,40 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
sharedActionViewModel sharedActionViewModel
.observe() .observe()
.subscribe { sharedAction -> .subscribe { sharedAction ->
when (sharedAction) { when (sharedAction) {
CreateDirectRoomSharedAction.OpenUsersDirectory -> UserDirectorySharedAction.OpenUsersDirectory ->
addFragmentToBackstack(R.id.container, CreateDirectRoomDirectoryUsersFragment::class.java) addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
CreateDirectRoomSharedAction.Close -> finish() UserDirectorySharedAction.Close -> finish()
CreateDirectRoomSharedAction.GoBack -> onBackPressed() UserDirectorySharedAction.GoBack -> onBackPressed()
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
} }
} }
.disposeOnDestroy() .disposeOnDestroy()
if (isFirstCreation()) { if (isFirstCreation()) {
addFragment(R.id.container, CreateDirectRoomKnownUsersFragment::class.java) addFragment(
R.id.container,
KnownUsersFragment::class.java,
KnownUsersFragmentArgs(
title = getString(R.string.fab_menu_create_chat),
menuResId = R.menu.vector_create_direct_room
)
)
} }
viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) { viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) {
renderCreateAndInviteState(it) renderCreateAndInviteState(it)
} }
} }
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_create_direct_room) {
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selectedUsers))
}
}
private fun renderCreateAndInviteState(state: Async<String>) { private fun renderCreateAndInviteState(state: Async<String>) {
when (state) { when (state) {
is Loading -> renderCreationLoading() is Loading -> renderCreationLoading()

View file

@ -18,7 +18,4 @@ package im.vector.riotx.features.createdirect
import im.vector.riotx.core.platform.VectorViewEvents import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for create direct room screen
*/
sealed class CreateDirectRoomViewEvents : VectorViewEvents sealed class CreateDirectRoomViewEvents : VectorViewEvents

View file

@ -1,42 +1,31 @@
/* /*
* Copyright (c) 2020 New Vector Ltd
* *
* * 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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * 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.
* *
* 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.createdirect package im.vector.riotx.features.createdirect
import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session 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.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.toggle
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
private typealias KnowUsersFilter = String
private typealias DirectoryUsersSearch = String
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState, initialState: CreateDirectRoomViewState,
@ -48,9 +37,6 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel
} }
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> { companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {
@JvmStatic @JvmStatic
@ -60,25 +46,15 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
} }
} }
init {
observeKnownUsers()
observeDirectoryUsers()
}
override fun handle(action: CreateDirectRoomAction) { override fun handle(action: CreateDirectRoomAction) {
when (action) { when (action) {
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.selectedUsers)
is CreateDirectRoomAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value))
is CreateDirectRoomAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty())
is CreateDirectRoomAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value)
is CreateDirectRoomAction.SelectUser -> handleSelectUser(action)
is CreateDirectRoomAction.RemoveSelectedUser -> handleRemoveSelectedUser(action)
} }
} }
private fun createRoomAndInviteSelectedUsers() = withState { currentState -> private fun createRoomAndInviteSelectedUsers(selectedUsers: Set<User>) {
val roomParams = CreateRoomParams( val roomParams = CreateRoomParams(
invitedUserIds = currentState.selectedUsers.map { it.userId } invitedUserIds = selectedUsers.map { it.userId }
) )
.setDirectMessage() .setDirectMessage()
.enableEncryptionIfInvitedUsersSupportIt() .enableEncryptionIfInvitedUsersSupportIt()
@ -89,52 +65,4 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
copy(createAndInviteState = it) copy(createAndInviteState = it)
} }
} }
private fun handleRemoveSelectedUser(action: CreateDirectRoomAction.RemoveSelectedUser) = withState { state ->
val selectedUsers = state.selectedUsers.minus(action.user)
setState { copy(selectedUsers = selectedUsers) }
}
private fun handleSelectUser(action: CreateDirectRoomAction.SelectUser) = withState { state ->
// Reset the filter asap
directoryUsersSearch.accept("")
val selectedUsers = state.selectedUsers.toggle(action.user)
setState { copy(selectedUsers = selectedUsers) }
}
private fun observeDirectoryUsers() {
directoryUsersSearch
.debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
val stream = if (search.isBlank()) {
Single.just(emptyList())
} else {
session.rx()
.searchUsersDirectory(search, 50, emptySet())
.map { users ->
users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
}
stream.toAsync {
copy(directoryUsers = it, directorySearchTerm = search)
}
}
.subscribe()
.disposeOnClear()
}
private fun observeKnownUsers() {
knownUsersFilter
.throttleLast(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
session.rx().livePagedUsers(it.orNull())
}
.execute { async ->
copy(
knownUsers = async,
filterKnownUsersValue = knownUsersFilter.value ?: Option.empty()
)
}
}
} }

View file

@ -1,41 +1,25 @@
/* /*
* Copyright (c) 2020 New Vector Ltd
* *
* * 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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * 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.
* *
* 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.createdirect package im.vector.riotx.features.createdirect
import androidx.paging.PagedList
import arrow.core.Option
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.user.model.User
data class CreateDirectRoomViewState( data class CreateDirectRoomViewState(
val knownUsers: Async<PagedList<User>> = Uninitialized, val createAndInviteState: Async<String> = Uninitialized
val directoryUsers: Async<List<User>> = Uninitialized, ) : MvRxState
val selectedUsers: Set<User> = emptySet(),
val createAndInviteState: Async<String> = Uninitialized,
val directorySearchTerm: String = "",
val filterKnownUsersValue: Option<String> = Option.empty()
) : MvRxState {
enum class DisplayMode {
KNOWN_USERS,
DIRECTORY_USERS
}
}

View file

@ -1,22 +1,20 @@
/* /*
* Copyright (c) 2020 New Vector Ltd
* *
* * 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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * 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.
* *
* 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.createdirect package im.vector.riotx.features.userdirectory
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
@ -41,7 +39,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : EpoxyController() { private val errorFormatter: ErrorFormatter) : EpoxyController() {
private var state: CreateDirectRoomViewState? = null private var state: UserDirectoryViewState? = null
var callback: Callback? = null var callback: Callback? = null
@ -49,7 +47,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
requestModelBuild() requestModelBuild()
} }
fun setData(state: CreateDirectRoomViewState) { fun setData(state: UserDirectoryViewState) {
this.state = state this.state = state
requestModelBuild() requestModelBuild()
} }
@ -110,7 +108,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
continue continue
} }
val isSelected = selectedUsers.contains(user.userId) val isSelected = selectedUsers.contains(user.userId)
createDirectRoomUserItem { userDirectoryUserItem {
id(user.userId) id(user.userId)
selected(isSelected) selected(isSelected)
matrixItem(user.toMatrixItem()) matrixItem(user.toMatrixItem())

View file

@ -1,11 +1,11 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.createdirect package im.vector.riotx.features.userdirectory
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController import com.airbnb.epoxy.paging.PagedListEpoxyController
@ -49,7 +49,7 @@ class KnownUsersController @Inject constructor(private val session: Session,
requestModelBuild() requestModelBuild()
} }
fun setData(state: CreateDirectRoomViewState) { fun setData(state: UserDirectoryViewState) {
this.isFiltering = !state.filterKnownUsersValue.isEmpty() this.isFiltering = !state.filterKnownUsersValue.isEmpty()
val newSelection = state.selectedUsers.map { it.userId } val newSelection = state.selectedUsers.map { it.userId }
this.users = state.knownUsers this.users = state.knownUsers
@ -65,7 +65,7 @@ class KnownUsersController @Inject constructor(private val session: Session,
EmptyItem_().id(currentPosition) EmptyItem_().id(currentPosition)
} else { } else {
val isSelected = selectedUsers.contains(item.userId) val isSelected = selectedUsers.contains(item.userId)
CreateDirectRoomUserItem_() UserDirectoryUserItem_()
.id(item.userId) .id(item.userId)
.selected(isSelected) .selected(isSelected)
.matrixItem(item.toMatrixItem()) .matrixItem(item.toMatrixItem())
@ -84,13 +84,13 @@ class KnownUsersController @Inject constructor(private val session: Session,
} else { } else {
var lastFirstLetter: String? = null var lastFirstLetter: String? = null
for (model in models) { for (model in models) {
if (model is CreateDirectRoomUserItem) { if (model is UserDirectoryUserItem) {
if (model.matrixItem.id == session.myUserId) continue if (model.matrixItem.id == session.myUserId) continue
val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName() val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName()
val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
lastFirstLetter = currentFirstLetter lastFirstLetter = currentFirstLetter
CreateDirectRoomLetterHeaderItem_() UserDirectoryLetterHeaderItem_()
.id(currentFirstLetter) .id(currentFirstLetter)
.letter(currentFirstLetter) .letter(currentFirstLetter)
.addIf(showLetter, this) .addIf(showLetter, this)

View file

@ -1,28 +1,27 @@
/* /*
* Copyright (c) 2020 New Vector Ltd
* *
* * 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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * 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.
* *
* 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.createdirect package im.vector.riotx.features.userdirectory
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ScrollView import android.widget.ScrollView
import androidx.core.view.forEach
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
@ -35,30 +34,33 @@ import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.setupAsSearch import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
import kotlinx.android.synthetic.main.fragment_create_direct_room.* import kotlinx.android.synthetic.main.fragment_known_users.*
import javax.inject.Inject import javax.inject.Inject
class CreateDirectRoomKnownUsersFragment @Inject constructor( class KnownUsersFragment @Inject constructor(
val userDirectoryViewModelFactory: UserDirectoryViewModel.Factory,
private val knownUsersController: KnownUsersController, private val knownUsersController: KnownUsersController,
private val dimensionConverter: DimensionConverter private val dimensionConverter: DimensionConverter
) : VectorBaseFragment(), KnownUsersController.Callback { ) : VectorBaseFragment(), KnownUsersController.Callback {
override fun getLayoutResId() = R.layout.fragment_create_direct_room override fun getLayoutResId() = R.layout.fragment_known_users
override fun getMenuRes() = R.menu.vector_create_direct_room override fun getMenuRes() = withState(viewModel) {
return@withState it.menuResId ?: -1
}
private val viewModel: CreateDirectRoomViewModel by activityViewModel() private val viewModel: UserDirectoryViewModel by activityViewModel()
private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
vectorBaseActivity.setSupportActionBar(createDirectRoomToolbar) vectorBaseActivity.setSupportActionBar(knownUsersToolbar)
setupRecyclerView() setupRecyclerView()
setupFilterView() setupFilterView()
setupAddByMatrixIdView() setupAddByMatrixIdView()
setupCloseView() setupCloseView()
viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) { viewModel.selectSubscribe(this, UserDirectoryViewState::selectedUsers) {
renderSelectedUsers(it) renderSelectedUsers(it)
} }
} }
@ -71,27 +73,22 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
withState(viewModel) { withState(viewModel) {
val createMenuItem = menu.findItem(R.id.action_create_direct_room)
val showMenuItem = it.selectedUsers.isNotEmpty() val showMenuItem = it.selectedUsers.isNotEmpty()
createMenuItem.setVisible(showMenuItem) menu.forEach { menuItem ->
menuItem.isVisible = showMenuItem
}
} }
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) {
return when (item.itemId) { sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(item.itemId, it.selectedUsers))
R.id.action_create_direct_room -> { return@withState true
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers)
true
}
else ->
super.onOptionsItemSelected(item)
}
} }
private fun setupAddByMatrixIdView() { private fun setupAddByMatrixIdView() {
addByMatrixId.setOnClickListener { addByMatrixId.setOnClickListener {
sharedActionViewModel.post(CreateDirectRoomSharedAction.OpenUsersDirectory) sharedActionViewModel.post(UserDirectorySharedAction.OpenUsersDirectory)
} }
} }
@ -102,26 +99,26 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
} }
private fun setupFilterView() { private fun setupFilterView() {
createDirectRoomFilter knownUsersFilter
.textChanges() .textChanges()
.startWith(createDirectRoomFilter.text) .startWith(knownUsersFilter.text)
.subscribe { text -> .subscribe { text ->
val filterValue = text.trim() val filterValue = text.trim()
val action = if (filterValue.isBlank()) { val action = if (filterValue.isBlank()) {
CreateDirectRoomAction.ClearFilterKnownUsers UserDirectoryAction.ClearFilterKnownUsers
} else { } else {
CreateDirectRoomAction.FilterKnownUsers(filterValue.toString()) UserDirectoryAction.FilterKnownUsers(filterValue.toString())
} }
viewModel.handle(action) viewModel.handle(action)
} }
.disposeOnDestroyView() .disposeOnDestroyView()
createDirectRoomFilter.setupAsSearch() knownUsersFilter.setupAsSearch()
createDirectRoomFilter.requestFocus() knownUsersFilter.requestFocus()
} }
private fun setupCloseView() { private fun setupCloseView() {
createDirectRoomClose.setOnClickListener { knownUsersClose.setOnClickListener {
requireActivity().finish() requireActivity().finish()
} }
} }
@ -157,12 +154,12 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
chip.isCloseIconVisible = true chip.isCloseIconVisible = true
chipGroup.addView(chip) chipGroup.addView(chip)
chip.setOnCloseIconClickListener { chip.setOnCloseIconClickListener {
viewModel.handle(CreateDirectRoomAction.RemoveSelectedUser(user)) viewModel.handle(UserDirectoryAction.RemoveSelectedUser(user))
} }
} }
override fun onItemClick(user: User) { override fun onItemClick(user: User) {
view?.hideKeyboard() view?.hideKeyboard()
viewModel.handle(CreateDirectRoomAction.SelectUser(user)) viewModel.handle(UserDirectoryAction.SelectUser(user))
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,12 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.createdirect package im.vector.riotx.features.userdirectory
import im.vector.riotx.core.platform.VectorSharedAction import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
sealed class CreateDirectRoomSharedAction : VectorSharedAction { @Parcelize
object OpenUsersDirectory : CreateDirectRoomSharedAction() data class KnownUsersFragmentArgs(
object Close : CreateDirectRoomSharedAction() val title: String,
object GoBack : CreateDirectRoomSharedAction() val menuResId: Int? = null
} ) : Parcelable

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2020 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.userdirectory
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class UserDirectoryAction : VectorViewModelAction {
data class FilterKnownUsers(val value: String) : UserDirectoryAction()
data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
object ClearFilterKnownUsers : UserDirectoryAction()
data class SelectUser(val user: User) : UserDirectoryAction()
data class RemoveSelectedUser(val user: User) : UserDirectoryAction()
}

View file

@ -1,11 +1,11 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.createdirect package im.vector.riotx.features.userdirectory
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
@ -29,22 +29,22 @@ import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.setupAsSearch import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.recyclerView
import kotlinx.android.synthetic.main.fragment_user_directory.*
import javax.inject.Inject import javax.inject.Inject
class CreateDirectRoomDirectoryUsersFragment @Inject constructor( class UserDirectoryFragment @Inject constructor(
private val directRoomController: DirectoryUsersController private val directRoomController: DirectoryUsersController
) : VectorBaseFragment(), DirectoryUsersController.Callback { ) : VectorBaseFragment(), DirectoryUsersController.Callback {
override fun getLayoutResId() = R.layout.fragment_create_direct_room_directory_users override fun getLayoutResId() = R.layout.fragment_user_directory
private val viewModel: UserDirectoryViewModel by activityViewModel()
private val viewModel: CreateDirectRoomViewModel by activityViewModel() private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
setupRecyclerView() setupRecyclerView()
setupSearchByMatrixIdView() setupSearchByMatrixIdView()
setupCloseView() setupCloseView()
@ -62,19 +62,19 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor(
} }
private fun setupSearchByMatrixIdView() { private fun setupSearchByMatrixIdView() {
createDirectRoomSearchById.setupAsSearch(searchIconRes = 0) userDirectorySearchById.setupAsSearch(searchIconRes = 0)
createDirectRoomSearchById userDirectorySearchById
.textChanges() .textChanges()
.subscribe { .subscribe {
viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(it.toString())) viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(it.toString()))
} }
.disposeOnDestroyView() .disposeOnDestroyView()
createDirectRoomSearchById.showKeyboard(andRequestFocus = true) userDirectorySearchById.showKeyboard(andRequestFocus = true)
} }
private fun setupCloseView() { private fun setupCloseView() {
createDirectRoomClose.setOnClickListener { userDirectoryClose.setOnClickListener {
sharedActionViewModel.post(CreateDirectRoomSharedAction.GoBack) sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
} }
} }
@ -84,12 +84,12 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor(
override fun onItemClick(user: User) { override fun onItemClick(user: User) {
view?.hideKeyboard() view?.hideKeyboard()
viewModel.handle(CreateDirectRoomAction.SelectUser(user)) viewModel.handle(UserDirectoryAction.SelectUser(user))
sharedActionViewModel.post(CreateDirectRoomSharedAction.GoBack) sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
} }
override fun retryDirectoryUsersRequest() { override fun retryDirectoryUsersRequest() {
val currentSearch = createDirectRoomSearchById.text.toString() val currentSearch = userDirectorySearchById.text.toString()
viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(currentSearch)) viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(currentSearch))
} }
} }

View file

@ -1,11 +1,11 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.createdirect package im.vector.riotx.features.userdirectory
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
@ -23,8 +23,8 @@ import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_create_direct_room_letter_header) @EpoxyModelClass(layout = R.layout.item_user_directory_letter_header)
abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel<CreateDirectRoomLetterHeaderItem.Holder>() { abstract class UserDirectoryLetterHeaderItem : VectorEpoxyModel<UserDirectoryLetterHeaderItem.Holder>() {
@EpoxyAttribute var letter: String = "" @EpoxyAttribute var letter: String = ""
@ -33,6 +33,6 @@ abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel<CreateDirectR
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val letterView by bind<TextView>(R.id.createDirectRoomLetterView) val letterView by bind<TextView>(R.id.userDirectoryLetterView)
} }
} }

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 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.userdirectory
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.core.platform.VectorSharedAction
sealed class UserDirectorySharedAction : VectorSharedAction {
object OpenUsersDirectory : UserDirectorySharedAction()
object Close : UserDirectorySharedAction()
object GoBack : UserDirectorySharedAction()
data class OnMenuItemSelected(val itemId: Int, val selectedUsers: Set<User>) : UserDirectorySharedAction()
}

View file

@ -1,11 +1,11 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.createdirect package im.vector.riotx.features.userdirectory
import im.vector.riotx.core.platform.VectorSharedActionViewModel import im.vector.riotx.core.platform.VectorSharedActionViewModel
import javax.inject.Inject import javax.inject.Inject
class CreateDirectRoomSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<CreateDirectRoomSharedAction>() class UserDirectorySharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<UserDirectorySharedAction>()

View file

@ -1,22 +1,20 @@
/* /*
* Copyright (c) 2020 New Vector Ltd
* *
* * 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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * 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.
* *
* 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.createdirect package im.vector.riotx.features.userdirectory
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
@ -31,8 +29,8 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_create_direct_room_user) @EpoxyModelClass(layout = R.layout.item_known_user)
abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserItem.Holder>() { abstract class UserDirectoryUserItem : VectorEpoxyModel<UserDirectoryUserItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var matrixItem: MatrixItem
@ -66,9 +64,9 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserI
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val userIdView by bind<TextView>(R.id.createDirectRoomUserID) val userIdView by bind<TextView>(R.id.knownUserID)
val nameView by bind<TextView>(R.id.createDirectRoomUserName) val nameView by bind<TextView>(R.id.knownUserName)
val avatarImageView by bind<ImageView>(R.id.createDirectRoomUserAvatar) val avatarImageView by bind<ImageView>(R.id.knownUserAvatar)
val avatarCheckedImageView by bind<ImageView>(R.id.createDirectRoomUserAvatarChecked) val avatarCheckedImageView by bind<ImageView>(R.id.knownUserAvatarChecked)
} }
} }

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 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.userdirectory
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for create direct room screen
*/
sealed class UserDirectoryViewEvents : VectorViewEvents

View file

@ -0,0 +1,132 @@
/*
* Copyright (c) 2020 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.userdirectory
import androidx.fragment.app.FragmentActivity
import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext
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.util.toMatrixItem
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.toggle
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
private typealias KnowUsersFilter = String
private typealias DirectoryUsersSearch = String
class UserDirectoryViewModel @AssistedInject constructor(@Assisted
initialState: UserDirectoryViewState,
private val session: Session)
: VectorViewModel<UserDirectoryViewState, UserDirectoryAction, UserDirectoryViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: UserDirectoryViewState): UserDirectoryViewModel
}
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
companion object : MvRxViewModelFactory<UserDirectoryViewModel, UserDirectoryViewState> {
override fun create(viewModelContext: ViewModelContext, state: UserDirectoryViewState): UserDirectoryViewModel? {
return when (viewModelContext) {
is FragmentViewModelContext -> (viewModelContext.fragment() as KnownUsersFragment).userDirectoryViewModelFactory.create(state)
is ActivityViewModelContext -> {
when (viewModelContext.activity<FragmentActivity>()) {
is CreateDirectRoomActivity -> viewModelContext.activity<CreateDirectRoomActivity>().userDirectoryViewModelFactory.create(state)
else -> error("Wrong activity or fragment")
}
}
else -> error("Wrong activity or fragment")
}
}
}
init {
observeKnownUsers()
observeDirectoryUsers()
}
override fun handle(action: UserDirectoryAction) {
when (action) {
is UserDirectoryAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value))
is UserDirectoryAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty())
is UserDirectoryAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value)
is UserDirectoryAction.SelectUser -> handleSelectUser(action)
is UserDirectoryAction.RemoveSelectedUser -> handleRemoveSelectedUser(action)
}
}
private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemoveSelectedUser) = withState { state ->
val selectedUsers = state.selectedUsers.minus(action.user)
setState { copy(selectedUsers = selectedUsers) }
}
private fun handleSelectUser(action: UserDirectoryAction.SelectUser) = withState { state ->
// Reset the filter asap
directoryUsersSearch.accept("")
val selectedUsers = state.selectedUsers.toggle(action.user)
setState { copy(selectedUsers = selectedUsers) }
}
private fun observeDirectoryUsers() {
directoryUsersSearch
.debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
val stream = if (search.isBlank()) {
Single.just(emptyList())
} else {
session.rx()
.searchUsersDirectory(search, 50, emptySet())
.map { users ->
users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
}
stream.toAsync {
copy(directoryUsers = it, directorySearchTerm = search)
}
}
.subscribe()
.disposeOnClear()
}
private fun observeKnownUsers() {
knownUsersFilter
.throttleLast(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
session.rx().livePagedUsers(it.orNull())
}
.execute { async ->
copy(
knownUsers = async,
filterKnownUsersValue = knownUsersFilter.value ?: Option.empty()
)
}
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 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.userdirectory
import androidx.paging.PagedList
import arrow.core.Option
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 UserDirectoryViewState(
val knownUsers: Async<PagedList<User>> = Uninitialized,
val directoryUsers: Async<List<User>> = Uninitialized,
val selectedUsers: Set<User> = emptySet(),
val createAndInviteState: Async<String> = Uninitialized,
val directorySearchTerm: String = "",
val filterKnownUsersValue: Option<String> = Option.empty(),
val title: String,
val menuResId: Int?
) : MvRxState {
constructor(args: KnownUsersFragmentArgs) : this(title = args.title, menuResId = args.menuResId)
enum class DisplayMode {
KNOWN_USERS,
DIRECTORY_USERS
}
}

View file

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/knownUsersToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
android:elevation="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/knownUsersClose"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_x_18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/knownUsersTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/fab_menu_create_chat"
android:textColor="?riotx_text_primary"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/knownUsersClose"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
<im.vector.riotx.core.platform.MaxHeightScrollView
android:id="@+id/chipGroupScrollView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/knownUsersToolbar"
app:maxHeight="64dp">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:lineSpacing="2dp" />
</im.vector.riotx.core.platform.MaxHeightScrollView>
<EditText
android:id="@+id/knownUsersFilter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:background="@null"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:hint="@string/direct_room_filter_hint"
android:importantForAutofill="no"
android:inputType="text"
android:maxHeight="80dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollView" />
<View
android:id="@+id/knownUsersFilterDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/knownUsersFilter" />
<com.google.android.material.button.MaterialButton
android:id="@+id/addByMatrixId"
style="@style/VectorButtonStyleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:minHeight="@dimen/layout_touch_size"
android:text="@string/add_by_matrix_id"
android:visibility="visible"
app:icon="@drawable/ic_plus_circle"
app:iconPadding="13dp"
app:iconTint="@color/riotx_accent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/knownUsersFilterDivider" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:fastScrollEnabled="true"
android:overScrollMode="always"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addByMatrixId"
tools:listitem="@layout/item_known_user" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/userDirectoryToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
android:elevation="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/userDirectoryClose"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_x_18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/userDirectoryTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/direct_chats_header"
android:textColor="?riotx_text_primary"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/userDirectoryClose"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/userDirectorySearchByIdContainer"
style="@style/VectorTextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/userDirectoryToolbar">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/userDirectorySearchById"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/add_by_matrix_id" />
</com.google.android.material.textfield.TextInputLayout>
<View
android:id="@+id/userDirectoryFilterDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/userDirectorySearchByIdContainer" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:fastScrollEnabled="true"
android:overScrollMode="always"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/userDirectoryFilterDivider"
tools:listitem="@layout/item_known_user" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
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">
<FrameLayout
android:id="@+id/knownUserAvatarContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/knownUserAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/knownUserAvatarChecked"
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="centerInside"
android:src="@drawable/ic_material_done"
android:tint="@android:color/white"
android:visibility="visible" />
</FrameLayout>
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/knownUserName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/knownUserID"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/knownUserAvatarContainer"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/knownUserID"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/knownUserName"
app:layout_constraintTop_toBottomOf="@+id/knownUserName"
tools:text="Blabla" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/userDirectoryLetterView"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:fontFamily="sans-serif-medium"
android:padding="8dp"
android:textColor="?attr/riotx_text_primary"
android:textSize="20sp"
android:textStyle="normal"
tools:text="C" />