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


View File

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

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

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

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

View File

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

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

private fun renderCreationFailure(error: Throwable) {

View File

@ -18,19 +18,25 @@

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

import arrow.core.Option
import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.NoResultItem_
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.epoxy.noResultItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject

class CreateDirectRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer,
class CreateDirectRoomController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : EpoxyController() {

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

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

@ -75,9 +83,22 @@ class CreateDirectRoomController @Inject constructor(private val avatarRenderer:
}
}

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

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

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

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

View File

@ -21,6 +21,7 @@ import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
@ -50,7 +51,6 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec
setupRecyclerView()
setupSearchByMatrixIdView()
setupCloseView()
viewModel.subscribe(this) { renderState(it) }
}

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

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

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

override fun onItemClick(user: User) {

View File

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

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

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

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


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.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction
import java.util.concurrent.TimeUnit

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

private fun observeDirectoryUsers() {
directoryUsersSearch
.throttleLast(500, TimeUnit.MILLISECONDS)
.debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
session.rx()
.searchUsersDirectory(search, 50, emptySet())
.map { users ->
users.sortedBy { it.displayName }
}
.toAsync { copy(directoryUsers = it) }
val stream = if (search.isBlank()) {
Single.just(emptyList())
} else {
session.rx()
.searchUsersDirectory(search, 50, emptySet())
.map { users ->
users.sortedBy { it.displayName.firstLetterOfDisplayName() }
}
}
stream.toAsync {
copy(directoryUsers = it, searchTerm = search)
}
}
.subscribe()
.disposeOnClear()
@ -157,7 +166,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
} else {
users.filter {
it.displayName?.contains(filterValue, ignoreCase = true) ?: false
|| it.userId.contains(filterValue, ignoreCase = true)
|| it.userId.contains(filterValue, ignoreCase = true)
}
}
}

View File

@ -18,6 +18,7 @@

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

import arrow.core.Option
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
@ -27,7 +28,9 @@ data class CreateDirectRoomViewState(
val knownUsers: Async<List<User>> = Uninitialized,
val directoryUsers: Async<List<User>> = Uninitialized,
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 {

enum class DisplayMode {

View File

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

<im.vector.riotx.core.platform.MaxHeightScrollView
android:id="@+id/chipGroupContainer"
android:id="@+id/chipGroupScrollView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
@ -68,13 +68,13 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar"
app:maxHeight="80dp">
app:maxHeight="64dp">

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

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

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

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

View File

@ -65,7 +65,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -74,24 +74,32 @@
<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>

<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
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
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"
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomFilterDivider"
tools:listitem="@layout/item_create_direct_room_user" />

</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,5 +3,8 @@

<!-- Strings not defined in Riot -->
<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>