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.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import io.reactivex.Completable
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single


@ -53,6 +52,12 @@ class RxSession(private val session: Session) {
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it) 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 { fun Session.rx(): RxSession {

View File

@ -17,7 +17,9 @@
package im.vector.matrix.android.api.session.user package im.vector.matrix.android.api.session.user


import androidx.lifecycle.LiveData 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.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. * 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? 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 * Observe a live user from a userId
* @param userId the userId to look for. * @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.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy 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.UserService
import im.vector.matrix.android.api.session.user.model.User 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.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.asDomain 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.UserEntity
import im.vector.matrix.android.internal.database.model.UserEntityFields import im.vector.matrix.android.internal.database.model.UserEntityFields
import im.vector.matrix.android.internal.database.query.where 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 im.vector.matrix.android.internal.util.fetchCopied
import javax.inject.Inject 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? { override fun getUser(userId: String): User? {
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() }
@ -62,4 +67,15 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
{ it.asDomain() } { 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.Binds
import dagger.Module import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.session.user.UserService 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 @Module
internal abstract class UserModule { internal abstract class UserModule {


@Module
companion object {
@Provides
@JvmStatic
@SessionScope
fun providesSearchUserAPI(retrofit: Retrofit): SearchUserAPI {
return retrofit.create(SearchUserAPI::class.java)
}
}

@Binds @Binds
abstract fun bindUserService(userService: DefaultUserService): UserService 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.HomeDetailFragment
import im.vector.riotx.features.home.HomeDrawerFragment import im.vector.riotx.features.home.HomeDrawerFragment
import im.vector.riotx.features.home.HomeModule import im.vector.riotx.features.home.HomeModule
import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity
import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment
import im.vector.riotx.features.home.createdirect.CreateDirectRoomFragment import im.vector.riotx.features.home.createdirect.CreateDirectRoomFragment
import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment
@ -46,6 +48,7 @@ import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoMediaViewerActivity 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.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.rageshake.RageShake
@ -74,6 +77,8 @@ interface ScreenComponent {


fun rageShake(): RageShake fun rageShake(): RageShake


fun navigator(): Navigator

fun inject(activity: HomeActivity) fun inject(activity: HomeActivity)


fun inject(roomDetailFragment: RoomDetailFragment) fun inject(roomDetailFragment: RoomDetailFragment)
@ -154,7 +159,11 @@ interface ScreenComponent {


fun inject(pushGatewaysFragment: PushGatewaysFragment) fun inject(pushGatewaysFragment: PushGatewaysFragment)


fun inject(createDirectRoomFragment: CreateDirectRoomFragment) fun inject(createDirectRoomKnownUsersFragment: CreateDirectRoomFragment)

fun inject(createDirectRoomDirectoryUsersFragment: CreateDirectRoomDirectoryUsersFragment)

fun inject(createDirectRoomActivity: CreateDirectRoomActivity)


@Component.Factory @Component.Factory
interface 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.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
import im.vector.riotx.features.home.* 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
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory
import im.vector.riotx.features.home.group.GroupListViewModel import im.vector.riotx.features.home.group.GroupListViewModel
@ -118,6 +119,11 @@ interface ViewModelModule {
@ViewModelKey(ConfigurationViewModel::class) @ViewModelKey(ConfigurationViewModel::class)
fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel 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. * 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.view.View
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import butterknife.BindView import butterknife.BindView
@ -46,6 +47,7 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() {


@Inject lateinit var session: Session @Inject lateinit var session: Session


@CallSuper
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
session = injector.session() session = injector.session()
} }

View File

@ -26,6 +26,7 @@ import androidx.annotation.*
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
@ -40,6 +41,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.* import im.vector.riotx.core.di.*
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.configuration.VectorConfiguration 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.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.rageshake.RageShake
@ -70,6 +72,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
private lateinit var configurationViewModel: ConfigurationViewModel private lateinit var configurationViewModel: ConfigurationViewModel
protected lateinit var bugReporter: BugReporter protected lateinit var bugReporter: BugReporter
private lateinit var rageShake: RageShake private lateinit var rageShake: RageShake
protected lateinit var navigator: Navigator


private var unBinder: Unbinder? = null private var unBinder: Unbinder? = null


@ -121,6 +124,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
configurationViewModel = ViewModelProviders.of(this, viewModelFactory).get(ConfigurationViewModel::class.java) configurationViewModel = ViewModelProviders.of(this, viewModelFactory).get(ConfigurationViewModel::class.java)
bugReporter = screenComponent.bugReporter() bugReporter = screenComponent.bugReporter()
rageShake = screenComponent.rageShake() rageShake = screenComponent.rageShake()
navigator = screenComponent.navigator()
configurationViewModel.activityRestarter.observe(this, Observer { configurationViewModel.activityRestarter.observe(this, Observer {
if (!it.hasBeenHandled) { if (!it.hasBeenHandled) {
// Recreate the Activity because configuration has changed // Recreate the Activity because configuration has changed
@ -262,6 +266,24 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
return super.onOptionsItemSelected(item) 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 * PROTECTED METHODS
* ========================================================================================== */ * ========================================================================================== */

View File

@ -65,7 +65,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed, HasScreen


override fun onAttach(context: Context) { override fun onAttach(context: Context) {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
navigator = vectorBaseActivity.getVectorComponent().navigator() navigator = screenComponent.navigator()
viewModelFactory = screenComponent.viewModelFactory() viewModelFactory = screenComponent.viewModelFactory()
injectWith(injector()) injectWith(injector())
super.onAttach(context) 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 @Inject lateinit var keysBackupSettingsViewModelFactory: KeysBackupSettingsViewModel.Factory


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



View File

@ -65,7 +65,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory @Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory
@Inject lateinit var homeNavigator: HomeNavigator @Inject lateinit var homeNavigator: HomeNavigator
@Inject lateinit var navigator: Navigator
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushManager: PushersManager @Inject lateinit var pushManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @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 { companion object {

View File

@ -16,11 +16,15 @@


package im.vector.riotx.features.home.createdirect package im.vector.riotx.features.home.createdirect


import im.vector.matrix.android.api.session.user.model.User

sealed class CreateDirectRoomActions { sealed class CreateDirectRoomActions {


object CreateRoomAndInviteSelectedUsers : CreateDirectRoomActions() object CreateRoomAndInviteSelectedUsers : CreateDirectRoomActions()
data class FilterKnownUsers(val value: String) : CreateDirectRoomActions() data class FilterKnownUsers(val value: String) : CreateDirectRoomActions()
object ClearFilterKnownUsers: CreateDirectRoomActions() data class SearchDirectoryUsers(val value: String) : CreateDirectRoomActions()
object SelectAddByMatrixId : 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.Context
import android.content.Intent 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.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.addFragment 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()) { 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 { companion object {
fun getIntent(context: Context): Intent { fun getIntent(context: Context): Intent {
return Intent(context, CreateDirectRoomActivity::class.java) return Intent(context, CreateDirectRoomActivity::class.java)

View File

@ -19,14 +19,27 @@
package im.vector.riotx.features.home.createdirect package im.vector.riotx.features.home.createdirect


import com.airbnb.epoxy.EpoxyController 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.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName 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 im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject 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 private var state: CreateDirectRoomViewState? = null
var displayMode = CreateDirectRoomViewState.DisplayMode.KNOWN_USERS

var callback: Callback? = null var callback: Callback? = null


init { init {
@ -40,10 +53,36 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer:


override fun buildModels() { override fun buildModels() {
val currentState = state ?: return 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 var lastFirstLetter: String? = null
knownUsers.forEach { user -> users.forEach { user ->
val isSelected = selectedUsers.contains(user)
val currentFirstLetter = user.displayName.firstLetterOfDisplayName() val currentFirstLetter = user.displayName.firstLetterOfDisplayName()
val showLetter = currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter val showLetter = currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
lastFirstLetter = currentFirstLetter lastFirstLetter = currentFirstLetter
@ -55,6 +94,7 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer:


createDirectRoomUserItem { createDirectRoomUserItem {
id(user.userId) id(user.userId)
selected(isSelected)
userId(user.userId) userId(user.userId)
name(user.displayName) name(user.displayName)
avatarUrl(user.avatarUrl) avatarUrl(user.avatarUrl)
@ -64,11 +104,13 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer:
} }
} }
} }

} }


interface Callback { interface Callback {
fun onItemClick(user: User) 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.os.Bundle
import android.view.MenuItem 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 com.jakewharton.rxbinding3.appcompat.queryTextChanges
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import kotlinx.android.synthetic.main.fragment_create_direct_room.* 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 import javax.inject.Inject


class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomController.Callback { class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomController.Callback {
@ -36,10 +42,10 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle


override fun getMenuRes() = R.menu.vector_create_direct_room 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 @Inject lateinit var directRoomController: CreateDirectRoomController
private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel


override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)
@ -47,27 +53,38 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle


override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
navigationViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java)
vectorBaseActivity.setSupportActionBar(createDirectRoomToolbar)
setupRecyclerView() setupRecyclerView()
setupFilterView() setupFilterView()
setupAddByMatrixIdView()
setupCloseView()
viewModel.subscribe(this) { renderState(it) } viewModel.subscribe(this) { renderState(it) }
} }


override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_create_room -> { R.id.action_create_direct_room -> {
viewModel.handle(CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers) viewModel.handle(CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers)
true true
} }
else -> else ->
super.onOptionsItemSelected(item) super.onOptionsItemSelected(item)
} }
} }


private fun setupAddByMatrixIdView() {
addByMatrixId.setOnClickListener {
navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.UsersDirectory)
}
}

private fun setupRecyclerView() { private fun setupRecyclerView() {
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
// Don't activate animation as we might have way to much item animation when filtering // Don't activate animation as we might have way to much item animation when filtering
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
directRoomController.callback = this directRoomController.callback = this
directRoomController.displayMode = CreateDirectRoomViewState.DisplayMode.KNOWN_USERS
recyclerView.setController(directRoomController) recyclerView.setController(directRoomController)
} }


@ -85,11 +102,18 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle
.disposeOnDestroy() .disposeOnDestroy()
} }


private fun setupCloseView() {
createDirectRoomClose.setOnClickListener {
requireActivity().finish()
}
}

private fun renderState(state: CreateDirectRoomViewState) { private fun renderState(state: CreateDirectRoomViewState) {

directRoomController.setData(state) directRoomController.setData(state)
} }


override fun onItemClick(user: User) { 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.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import com.amulyakhare.textdrawable.TextDrawable
import im.vector.riotx.R 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
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.getColorFromUserId


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


override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener) holder.view.setOnClickListener(clickListener)
@ -48,13 +53,22 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserI
holder.nameView.text = name holder.nameView.text = name
holder.userIdView.text = userId 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() { class Holder : VectorEpoxyHolder() {
val userIdView by bind<TextView>(R.id.createDirectRoomUserID) val userIdView by bind<TextView>(R.id.createDirectRoomUserID)
val nameView by bind<TextView>(R.id.createDirectRoomUserName) val nameView by bind<TextView>(R.id.createDirectRoomUserName)
val avatarImageView by bind<ImageView>(R.id.createDirectRoomUserAvatar) 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 package im.vector.riotx.features.home.createdirect


import arrow.core.Option import arrow.core.Option
import com.airbnb.mvrx.FragmentViewModelContext 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.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.user.model.User 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.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import io.reactivex.subjects.BehaviorSubject
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit


private typealias KnowUsersFilter = String private typealias KnowUsersFilter = String
private typealias DirectoryUsersSearch = String


class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState, initialState: CreateDirectRoomViewState,
@ -47,35 +48,77 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
} }


private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty()) 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
override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? { override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? {
val fragment: CreateDirectRoomFragment = (viewModelContext as FragmentViewModelContext).fragment() val activity: CreateDirectRoomActivity = (viewModelContext as ActivityViewModelContext).activity()
return fragment.createDirectRoomViewModelFactory.create(state) return activity.createDirectRoomViewModelFactory.create(state)
} }
} }


init { init {
observeKnownUsers() observeKnownUsers()
observeDirectoryUsers()
} }


fun handle(createDirectRoomActions: CreateDirectRoomActions) { fun handle(action: CreateDirectRoomActions) {
when (createDirectRoomActions) { when (action) {
is CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() is CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers()
is CreateDirectRoomActions.SelectAddByMatrixId -> handleSelectAddByMatrixId() is CreateDirectRoomActions.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value))
is CreateDirectRoomActions.FilterKnownUsers -> knownUsersFilter.accept(Option.just(createDirectRoomActions.value))
is CreateDirectRoomActions.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) 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() { private fun createRoomAndInviteSelectedUsers() = withState {
// TODO 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() { private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState {
// TODO 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() { private fun observeKnownUsers() {

View File

@ -24,14 +24,15 @@ import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User


data class CreateDirectRoomViewState( data class CreateDirectRoomViewState(
val displayMode: DisplayMode = DisplayMode.KNOWN_USERS,
val knownUsers: Async<List<User>> = Uninitialized, 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 { ) : MvRxState {


enum class DisplayMode { enum class DisplayMode {
KNOWN_USERS, 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.platform.VectorViewModel
import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.utils.LiveEvent 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.CommandParser
import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
@ -94,7 +95,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
observeRoomSummary() observeRoomSummary()
observeEventDisplayedActions() observeEventDisplayedActions()
observeInvitationState() observeInvitationState()
cancelableBag += room.loadRoomMembersIfNeeded() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
timeline.start() timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) } setState { copy(timeline = this@RoomDetailViewModel.timeline) }
} }
@ -235,12 +236,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else { } else {
val messageContent: MessageContent? = val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val existingBody = messageContent?.body ?: "" val existingBody = messageContent?.body ?: ""
if (existingBody != action.text) { if (existingBody != action.text) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId room.editTextMessage(state.sendMode.timelineEvent.root.eventId
?: "", messageContent?.type ?: "", messageContent?.type
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
} else { } else {
Timber.w("Same message content, do not send edition") Timber.w("Same message content, do not send edition")
} }
@ -255,7 +256,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is SendMode.QUOTE -> { is SendMode.QUOTE -> {
val messageContent: MessageContent? = val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val textMsg = messageContent?.body val textMsg = messageContent?.body


val finalText = legacyRiotQuoteText(textMsg, action.text) 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.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.riotx.features.debug.DebugMenuActivity import im.vector.riotx.features.debug.DebugMenuActivity
import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity 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.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity

View File

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


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


<androidx.appcompat.widget.SearchView <androidx.appcompat.widget.SearchView
android:id="@+id/createDirectRoomFilter" android:id="@+id/createDirectRoomFilter"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="text|textMultiLine"
app:closeIcon="@drawable/ic_x_green" app:closeIcon="@drawable/ic_x_green"
app:iconifiedByDefault="false" app:iconifiedByDefault="false"
app:queryBackground="@android:color/transparent" app:queryBackground="@android:color/transparent"
@ -88,6 +95,7 @@
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:minHeight="@dimen/layout_touch_size" android:minHeight="@dimen/layout_touch_size"
android:text="@string/add_by_matrix_id" android:text="@string/add_by_matrix_id"
android:visibility="visible"
app:icon="@drawable/ic_plus_circle" app:icon="@drawable/ic_plus_circle"
app:iconPadding="13dp" app:iconPadding="13dp"
app:iconTint="@color/riotx_accent" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?riotx_background" android:background="?riotx_background"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="8dp"> android:padding="8dp">


<ImageView <FrameLayout
android:id="@+id/createDirectRoomUserAvatar" android:id="@+id/createDirectRoomUserAvatarContainer"
android:layout_width="40dp" android:layout_width="wrap_content"
android:layout_height="40dp" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent">
tools:src="@tools:sample/avatars" />
<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 <TextView
android:id="@+id/createDirectRoomUserName" android:id="@+id/createDirectRoomUserName"
@ -34,7 +49,7 @@
app:layout_constraintBottom_toTopOf="@+id/createDirectRoomUserID" app:layout_constraintBottom_toTopOf="@+id/createDirectRoomUserID"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/createDirectRoomUserAvatar" app:layout_constraintStart_toEndOf="@+id/createDirectRoomUserAvatarContainer"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />



View File

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


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