Direct chat : finalize flow

This commit is contained in:
ganfra 2019-07-25 16:26:45 +02:00
parent 5af6bf3762
commit 76a9625f25
11 changed files with 107 additions and 44 deletions

View File

@ -31,7 +31,6 @@ import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.user.model.SearchUserTask import im.vector.matrix.android.internal.session.user.model.SearchUserTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith 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



View File

@ -23,13 +23,16 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.EditText import android.widget.EditText
import androidx.annotation.DrawableRes
import im.vector.riotx.R import im.vector.riotx.R


fun EditText.setupAsSearch() { fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter,
@DrawableRes clearIconRes: Int = R.drawable.ic_x_green) {

addTextChangedListener(object : TextWatcher { addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) { override fun afterTextChanged(editable: Editable?) {
val clearIcon = if (editable?.isNotEmpty() == true) R.drawable.ic_clear_white else 0 val clearIcon = if (editable?.isNotEmpty() == true) clearIconRes else 0
setCompoundDrawablesWithIntrinsicBounds(0, 0, clearIcon, 0) setCompoundDrawablesWithIntrinsicBounds(searchIconRes, 0, clearIcon, 0)
} }


override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit

View File

@ -86,7 +86,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
} }


private fun renderCreationLoading() { private fun renderCreationLoading() {
updateWaitingView(WaitingViewData(getString(R.string.room_recents_create_room))) updateWaitingView(WaitingViewData(getString(R.string.creating_direct_room)))
} }


private fun renderCreationFailure(error: Throwable) { private fun renderCreationFailure(error: Throwable) {

View File

@ -18,19 +18,25 @@


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


import arrow.core.Option
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.*
import com.airbnb.mvrx.Incomplete import im.vector.matrix.android.api.session.Session
import com.airbnb.mvrx.Success
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.R
import im.vector.riotx.core.epoxy.NoResultItem_
import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.epoxy.noResultItem
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.StringProvider
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, class CreateDirectRoomController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : EpoxyController() { private val errorFormatter: ErrorFormatter) : EpoxyController() {


private var state: CreateDirectRoomViewState? = null private var state: CreateDirectRoomViewState? = null
@ -49,14 +55,16 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer:


override fun buildModels() { override fun buildModels() {
val currentState = state ?: return val currentState = state ?: return
val hasSearch = currentState.searchTerm.isNotBlank()
val asyncUsers = if (displayMode == CreateDirectRoomViewState.DisplayMode.DIRECTORY_USERS) { val asyncUsers = if (displayMode == CreateDirectRoomViewState.DisplayMode.DIRECTORY_USERS) {
currentState.directoryUsers currentState.directoryUsers
} else { } else {
currentState.knownUsers currentState.knownUsers
} }
when (asyncUsers) { when (asyncUsers) {
is Incomplete -> renderLoading() is Uninitialized -> renderEmptyState(false)
is Success -> renderUsers(asyncUsers(), currentState.selectedUsers.map { it.userId }) is Loading -> renderLoading()
is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch)
is Fail -> renderFailure(asyncUsers.error) is Fail -> renderFailure(asyncUsers.error)
} }
} }
@ -75,9 +83,22 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer:
} }
} }


private fun renderSuccess(users: List<User>,
selectedUsers: List<String>,
hasSearch: Boolean) {
if (users.isEmpty()) {
renderEmptyState(hasSearch)
} else {
renderUsers(users, selectedUsers)
}
}

private fun renderUsers(users: List<User>, selectedUsers: List<String>) { private fun renderUsers(users: List<User>, selectedUsers: List<String>) {
var lastFirstLetter: String? = null var lastFirstLetter: String? = null
users.forEach { user -> for (user in users) {
if (user.userId == session.myUserId) {
continue
}
val isSelected = selectedUsers.contains(user.userId) val isSelected = selectedUsers.contains(user.userId)
val currentFirstLetter = user.displayName.firstLetterOfDisplayName() val currentFirstLetter = user.displayName.firstLetterOfDisplayName()
val showLetter = currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter val showLetter = currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
@ -102,6 +123,22 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer:
} }
} }


private fun renderEmptyState(hasSearch: Boolean) {
val noResultRes = if (displayMode == CreateDirectRoomViewState.DisplayMode.DIRECTORY_USERS) {
if (hasSearch) {
R.string.no_result_placeholder
} else {
R.string.direct_room_start_search
}
} else {
R.string.direct_room_no_known_users
}
noResultItem {
id("noResult")
text(stringProvider.getString(noResultRes))
}
}

interface Callback { interface Callback {
fun onItemClick(user: User) fun onItemClick(user: User)
fun retryDirectoryUsersRequest() { fun retryDirectoryUsersRequest() {

View File

@ -21,6 +21,7 @@ import android.os.Bundle
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
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
@ -50,7 +51,6 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec
setupRecyclerView() setupRecyclerView()
setupSearchByMatrixIdView() setupSearchByMatrixIdView()
setupCloseView() setupCloseView()
viewModel.subscribe(this) { renderState(it) }
} }


private fun setupRecyclerView() { private fun setupRecyclerView() {
@ -61,7 +61,7 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec
} }


private fun setupSearchByMatrixIdView() { private fun setupSearchByMatrixIdView() {
createDirectRoomSearchById.setupAsSearch() createDirectRoomSearchById.setupAsSearch(searchIconRes = 0)
createDirectRoomSearchById createDirectRoomSearchById
.textChanges() .textChanges()
.subscribe { .subscribe {
@ -80,8 +80,8 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec
} }
} }


private fun renderState(state: CreateDirectRoomViewState) { override fun invalidate() = withState(viewModel) {
directRoomController.setData(state) directRoomController.setData(it)
} }


override fun onItemClick(user: User) { override fun onItemClick(user: User) {

View File

@ -71,7 +71,6 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle
viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) { viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) {
renderSelectedUsers(it) renderSelectedUsers(it)
} }
viewModel.subscribe(this) { renderState(it) }
} }


override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
@ -133,8 +132,8 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle
} }
} }


private fun renderState(state: CreateDirectRoomViewState) { override fun invalidate() = withState(viewModel) {
directRoomController.setData(state) directRoomController.setData(it)
} }


private fun updateChipsView(data: SelectUserAction) { private fun updateChipsView(data: SelectUserAction) {
@ -166,8 +165,8 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle
chip.setOnCloseIconClickListener { chip.setOnCloseIconClickListener {
viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(user)) viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(user))
} }
chipGroupContainer.post { chipGroupScrollView.post {
chipGroupContainer.fullScroll(ScrollView.FOCUS_DOWN) chipGroupScrollView.fullScroll(ScrollView.FOCUS_DOWN)
} }
} }



View File

@ -28,11 +28,14 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.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.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit


@ -132,14 +135,20 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted


private fun observeDirectoryUsers() { private fun observeDirectoryUsers() {
directoryUsersSearch directoryUsersSearch
.throttleLast(500, TimeUnit.MILLISECONDS) .debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search -> .switchMapSingle { search ->
val stream = if (search.isBlank()) {
Single.just(emptyList())
} else {
session.rx() session.rx()
.searchUsersDirectory(search, 50, emptySet()) .searchUsersDirectory(search, 50, emptySet())
.map { users -> .map { users ->
users.sortedBy { it.displayName } users.sortedBy { it.displayName.firstLetterOfDisplayName() }
}
}
stream.toAsync {
copy(directoryUsers = it, searchTerm = search)
} }
.toAsync { copy(directoryUsers = it) }
} }
.subscribe() .subscribe()
.disposeOnClear() .disposeOnClear()

View File

@ -18,6 +18,7 @@


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


import arrow.core.Option
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
@ -27,7 +28,9 @@ data class CreateDirectRoomViewState(
val knownUsers: Async<List<User>> = Uninitialized, val knownUsers: Async<List<User>> = Uninitialized,
val directoryUsers: Async<List<User>> = Uninitialized, val directoryUsers: Async<List<User>> = Uninitialized,
val selectedUsers: Set<User> = emptySet(), val selectedUsers: Set<User> = emptySet(),
val createAndInviteState: Async<String> = Uninitialized val createAndInviteState: Async<String> = Uninitialized,
val searchTerm: String = "",
val filterKnownUsersValue: Option<String> = Option.empty()
) : MvRxState { ) : MvRxState {


enum class DisplayMode { enum class DisplayMode {

View File

@ -44,7 +44,7 @@
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:text="@string/direct_chats_header" android:text="@string/fab_menu_create_chat"
android:textColor="?riotx_text_primary" android:textColor="?riotx_text_primary"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
@ -59,7 +59,7 @@
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>


<im.vector.riotx.core.platform.MaxHeightScrollView <im.vector.riotx.core.platform.MaxHeightScrollView
android:id="@+id/chipGroupContainer" android:id="@+id/chipGroupScrollView"
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_marginStart="@dimen/layout_horizontal_margin"
@ -68,13 +68,13 @@
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/createDirectRoomToolbar" app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar"
app:maxHeight="80dp"> app:maxHeight="64dp">


<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup" android:id="@+id/chipGroup"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:lineSpacing="4dp" /> app:lineSpacing="2dp" />


</im.vector.riotx.core.platform.MaxHeightScrollView> </im.vector.riotx.core.platform.MaxHeightScrollView>


@ -85,21 +85,23 @@
android:layout_marginStart="@dimen/layout_horizontal_margin" android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin" android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:background="@null" android:background="@null"
android:hint="@string/room_directory_search_hint" android:drawablePadding="8dp"
android:gravity="center_vertical"
android:hint="@string/direct_room_filter_hint"
android:importantForAutofill="no" android:importantForAutofill="no"
android:maxHeight="80dp" android:maxHeight="80dp"
android:padding="8dp" android:paddingTop="16dp"
android:paddingBottom="16dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chipGroupContainer" /> app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollView" />


<View <View
android:id="@+id/createDirectRoomFilterDivider" android:id="@+id/createDirectRoomFilterDivider"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?attr/vctr_list_divider_color" android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@ -65,7 +65,7 @@
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_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="8dp" android:layout_marginTop="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin" android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -74,24 +74,32 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/createDirectRoomSearchById" android:id="@+id/createDirectRoomSearchById"
android:layout_width="match_parent" android:layout_width="match_parent"

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


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


<View
android:id="@+id/createDirectRoomFilterDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomSearchByIdContainer" />


<com.airbnb.epoxy.EpoxyRecyclerView <com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="16dp"
android:fastScrollEnabled="true" android:fastScrollEnabled="true"
android:scrollbars="vertical" android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
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/createDirectRoomSearchByIdContainer" app:layout_constraintTop_toBottomOf="@+id/createDirectRoomFilterDivider"
tools:listitem="@layout/item_create_direct_room_user" /> tools:listitem="@layout/item_create_direct_room_user" />


</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,5 +3,8 @@


<!-- Strings not defined in Riot --> <!-- Strings not defined in Riot -->
<string name="add_by_matrix_id">Add by matrix ID</string> <string name="add_by_matrix_id">Add by matrix ID</string>

<string name="creating_direct_room">"Creating room…"</string>
<string name="direct_room_no_known_users">"No result found, use Add by matrix ID to search on server."</string>
<string name="direct_room_start_search">"Start typing to get results"</string>
<string name="direct_room_filter_hint">"Filter by username or ID…"</string>
</resources> </resources>