diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index 1db73c847c..06a7ba7fda 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt b/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt index ecc7795a7b..ace13754aa 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt index c31cb8c0c6..eb26c321ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomController.kt index 4d83477dd3..80ee4fb956 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomController.kt @@ -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, + selectedUsers: List, + hasSearch: Boolean) { + if (users.isEmpty()) { + renderEmptyState(hasSearch) + } else { + renderUsers(users, selectedUsers) + } + } + private fun renderUsers(users: List, selectedUsers: List) { 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() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt index ad3a8f3354..f7f9eca9a1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomFragment.kt index 57c1783bdc..63c6aaf1bf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomFragment.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt index 9ce91585f0..71ef00e0a5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt @@ -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) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt index 566078022c..e6bc212f25 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt @@ -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> = Uninitialized, val directoryUsers: Async> = Uninitialized, val selectedUsers: Set = emptySet(), - val createAndInviteState: Async = Uninitialized + val createAndInviteState: Async = Uninitialized, + val searchTerm: String = "", + val filterKnownUsersValue: Option = Option.empty() ) : MvRxState { enum class DisplayMode { diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml index 11a74d6b07..427df61588 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -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 @@ + app:maxHeight="64dp"> + app:lineSpacing="2dp" /> @@ -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" /> + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 504cee65a1..45dc5c5308 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -3,5 +3,8 @@ Add by matrix ID - + "Creating room…" + "No result found, use Add by matrix ID to search on server." + "Start typing to get results" + "Filter by username or ID…" \ No newline at end of file