Create direct room: almost finished, still need to handle showing selected users in search field

This commit is contained in:
ganfra 2019-07-19 18:12:42 +02:00
parent cb274d6a33
commit cb44ab547c
32 changed files with 766 additions and 77 deletions

View File

@ -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<String>): Single<List<User>> = Single.create {
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
}

}

fun Session.rx(): RxSession {

View File

@ -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<String>, callback: MatrixCallback<List<User>>): Cancelable

/**
* Observe a live user from a userId
* @param userId the userId to look for.

View File

@ -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<String>,
callback: MatrixCallback<List<User>>): Cancelable {
val params = SearchUserTask.Params(limit, search, excludedUserIds)
return searchUserTask
.configureWith(params)
.dispatchTo(callback)
.executeBy(taskExecutor)
}
}

View File

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

View File

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

}

View File

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

View File

@ -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<SearchUserTask.Params, List<User>> {

data class Params(
val limit: Int,
val search: String,
val excludedUserIds: Set<String>
)
}

internal class DefaultSearchUserTask @Inject constructor(private val searchUserAPI: SearchUserAPI) : SearchUserTask {

override suspend fun execute(params: SearchUserTask.Params): Try<List<User>> {
return executeRequest<SearchUsersRequestResponse> {
apiCall = searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit))
}.map { response ->
response.users.map {
User(it.userId, it.displayName, it.avatarUrl)
}
}
}

}

View File

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

View File

@ -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<SearchUser> = emptyList()
)

View File

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

View File

@ -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.
*/

View File

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

View File

@ -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
* ========================================================================================== */

View File

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

View File

@ -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 <T> Single<T>.subscribeLogError(): Disposable {
return subscribe(Functions.emptyConsumer(), Consumer { Timber.e(it) })
}

fun Completable.subscribeLogError(): Disposable {
return subscribe({}, { Timber.e(it) })
}

View File

@ -43,6 +43,7 @@ class KeysBackupManageActivity : SimpleFragmentActivity() {
@Inject lateinit var keysBackupSettingsViewModelFactory: KeysBackupSettingsViewModel.Factory

override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}


View File

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

View File

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

}

View File

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

View File

@ -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<User>, selectedUsers: Set<User>) {
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
}
}

}

View File

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

View File

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

View File

@ -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<CreateDirectRoomActivity.Navigation>()

View File

@ -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<CreateDirectRoomUserItem.Holder>() {
@ -36,6 +40,7 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserI
@EpoxyAttribute var userId: String = ""
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null
@EpoxyAttribute var selected: Boolean = false

override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener)
@ -48,13 +53,22 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserI
holder.nameView.text = name
holder.userIdView.text = userId
}
avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView)
if (selected) {
holder.avatarCheckedImageView.visibility = View.VISIBLE
val backgroundColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent)
val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor)
holder.avatarImageView.setImageDrawable(backgroundDrawable)
} else {
holder.avatarCheckedImageView.visibility = View.GONE
avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView)
}
}

class Holder : VectorEpoxyHolder() {
val userIdView by bind<TextView>(R.id.createDirectRoomUserID)
val nameView by bind<TextView>(R.id.createDirectRoomUserName)
val avatarImageView by bind<ImageView>(R.id.createDirectRoomUserAvatar)
val avatarCheckedImageView by bind<ImageView>(R.id.createDirectRoomUserAvatarChecked)
}

}

View File

@ -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<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()

companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {

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

View File

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

enum class DisplayMode {
KNOWN_USERS,
MATRIX_ID_USERS
DIRECTORY_USERS
}

}

View File

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

View File

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

View File

@ -10,7 +10,7 @@
android:layout_height="match_parent">

<androidx.appcompat.widget.Toolbar
android:id="@+id/createRoomToolbar"
android:id="@+id/createDirectRoomToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
@ -27,6 +27,9 @@
android:id="@+id/createDirectRoomClose"
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"
@ -59,16 +62,20 @@
android:id="@+id/createDirectRoomFilterContainer"
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:cardElevation="4dp"
app:cardUseCompatPadding="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/createRoomToolbar">
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar">

<androidx.appcompat.widget.SearchView
android:id="@+id/createDirectRoomFilter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text|textMultiLine"
app:closeIcon="@drawable/ic_x_green"
app:iconifiedByDefault="false"
app:queryBackground="@android:color/transparent"
@ -88,6 +95,7 @@
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"

View File

@ -0,0 +1,100 @@
<?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/createRoomToolbar"
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/createDirectRoomClose"
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" />

<TextView
android:id="@+id/createDirectRoomTitle"
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/createDirectRoomClose"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.appcompat.widget.Toolbar>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/createDirectRoomSearchByIdContainer"
style="@style/VectorTextInputLayout"
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/createRoomToolbar">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/createDirectRoomSearchById"
android:layout_width="match_parent"

android:layout_height="wrap_content"
android:hint="@string/add_by_matrix_id" />

</com.google.android.material.textfield.TextInputLayout>


<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:fastScrollEnabled="true"
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/createDirectRoomSearchByIdContainer"
tools:listitem="@layout/item_create_direct_room_user" />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

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

<ImageView
android:id="@+id/createDirectRoomUserAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
<FrameLayout
android:id="@+id/createDirectRoomUserAvatarContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
app:layout_constraintTop_toTopOf="parent">

<ImageView
android:id="@+id/createDirectRoomUserAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
tools:src="@tools:sample/avatars" />

<ImageView
android:id="@+id/createDirectRoomUserAvatarChecked"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_material_done"
android:tint="@android:color/white"
android:visibility="visible" />
</FrameLayout>

<TextView
android:id="@+id/createDirectRoomUserName"
@ -34,7 +49,7 @@
app:layout_constraintBottom_toTopOf="@+id/createDirectRoomUserID"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/createDirectRoomUserAvatar"
app:layout_constraintStart_toEndOf="@+id/createDirectRoomUserAvatarContainer"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />


View File

@ -1,11 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu 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"
tools:context=".features.roomdirectory.RoomDirectoryActivity">
xmlns:app="http://schemas.android.com/apk/res-auto">

<item
android:id="@+id/action_create_room"
android:id="@+id/action_create_direct_room"
android:title="@string/create_room_action_create"
app:showAsAction="always" />