diff --git a/app/src/main/java/im/vector/riotredesign/core/extensions/EditText.kt b/app/src/main/java/im/vector/riotredesign/core/extensions/EditText.kt new file mode 100644 index 00000000..bace1321 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/extensions/EditText.kt @@ -0,0 +1,44 @@ +package im.vector.riotredesign.core.extensions + +import android.text.Editable +import android.text.InputType +import android.text.TextWatcher +import android.view.MotionEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import im.vector.riotredesign.R + +fun EditText.setupAsSearch() { + 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) + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + }) + + maxLines = 1 + inputType = InputType.TYPE_CLASS_TEXT + imeOptions = EditorInfo.IME_ACTION_SEARCH + setOnEditorActionListener { _, actionId, event -> + var consumed = false + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + hideKeyboard() + consumed = true + } + consumed + } + + setOnTouchListener(View.OnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_UP) { + if (event.rawX >= (this.right - this.compoundPaddingRight)) { + text = null + return@OnTouchListener true + } + } + return@OnTouchListener false + }) +} diff --git a/app/src/main/java/im/vector/riotredesign/core/extensions/View.kt b/app/src/main/java/im/vector/riotredesign/core/extensions/View.kt new file mode 100644 index 00000000..80217290 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/extensions/View.kt @@ -0,0 +1,10 @@ +package im.vector.riotredesign.core.extensions + +import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager + +fun View.hideKeyboard() { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(windowToken, 0) +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt index 17b8ec9b..bfed4431 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt @@ -8,4 +8,6 @@ sealed class RoomListActions { object RoomDisplayed : RoomListActions() + data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions() + } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt index f66ee630..43a2ef97 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt @@ -1,9 +1,12 @@ package im.vector.riotredesign.features.home.room.list import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.inputmethod.EditorInfo import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success @@ -11,6 +14,8 @@ import com.airbnb.mvrx.activityViewModel import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotredesign.R +import im.vector.riotredesign.core.extensions.hideKeyboard +import im.vector.riotredesign.core.extensions.setupAsSearch import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.StateView import im.vector.riotredesign.features.home.HomeNavigator @@ -38,6 +43,7 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback { roomController = RoomSummaryController(this) stateView.contentView = epoxyRecyclerView epoxyRecyclerView.setController(roomController) + setupFilterView() homeViewModel.subscribe { renderState(it) } } @@ -70,6 +76,21 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback { stateView.state = StateView.State.Error(message) } + private fun setupFilterView() { + filterRoomView.setupAsSearch() + filterRoomView.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) = Unit + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + homeViewModel.accept(RoomListActions.FilterRooms(s)) + } + }) + } + + // RoomSummaryController.Callback ************************************************************** + override fun onRoomSelected(room: RoomSummary) { homeViewModel.accept(RoomListActions.SelectRoom(room)) homeNavigator.openRoomDetail(room.roomId, null) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt index c39317aa..cbf2a489 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt @@ -3,6 +3,7 @@ package im.vector.riotredesign.features.home.room.list import arrow.core.Option import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext +import com.jakewharton.rxrelay2.BehaviorRelay import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.model.GroupSummary @@ -12,10 +13,12 @@ import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.features.home.group.SelectedGroupHolder import im.vector.riotredesign.features.home.room.VisibleRoomHolder import io.reactivex.Observable -import io.reactivex.functions.BiFunction +import io.reactivex.functions.Function3 import io.reactivex.rxkotlin.subscribeBy import org.koin.android.ext.android.get +typealias RoomListFilterName = CharSequence + class RoomListViewModel(initialState: RoomListViewState, private val session: Session, private val selectedGroupHolder: SelectedGroupHolder, @@ -35,6 +38,9 @@ class RoomListViewModel(initialState: RoomListViewState, } } + + private val roomListFilter = BehaviorRelay.createDefault>(Option.empty()) + init { observeRoomSummaries() observeVisibleRoom() @@ -42,7 +48,8 @@ class RoomListViewModel(initialState: RoomListViewState, fun accept(action: RoomListActions) { when (action) { - is RoomListActions.SelectRoom -> handleSelectRoom(action) + is RoomListActions.SelectRoom -> handleSelectRoom(action) + is RoomListActions.FilterRooms -> handleFilterRooms(action) } } @@ -54,6 +61,11 @@ class RoomListViewModel(initialState: RoomListViewState, } } + private fun handleFilterRooms(action: RoomListActions.FilterRooms) { + val optionalFilter = Option.fromNullable(action.roomName) + roomListFilter.accept(optionalFilter) + } + private fun observeVisibleRoom() { visibleRoomHolder.visibleRoom() .subscribeBy { @@ -63,13 +75,22 @@ class RoomListViewModel(initialState: RoomListViewState, } private fun observeRoomSummaries() { - Observable.combineLatest, Option, RoomSummaries>( + Observable.combineLatest, Option, Option, RoomSummaries>( session.rx().liveRoomSummaries(), selectedGroupHolder.selectedGroup(), - BiFunction { rooms, selectedGroupOption -> - val selectedGroup = selectedGroupOption.orNull() + roomListFilter, + Function3 { rooms, selectedGroupOption, filterRoomOption -> + val filterRoom = filterRoomOption.orNull() + val filteredRooms = rooms.filter { + if (filterRoom.isNullOrBlank()) { + true + } else { + it.displayName.contains(other = filterRoom, ignoreCase = true) + } + } - val filteredDirectRooms = rooms + val selectedGroup = selectedGroupOption.orNull() + val filteredDirectRooms = filteredRooms .filter { it.isDirect } .filter { if (selectedGroup == null) { @@ -81,7 +102,7 @@ class RoomListViewModel(initialState: RoomListViewState, } } - val filteredGroupRooms = rooms + val filteredGroupRooms = filteredRooms .filter { !it.isDirect } .filter { selectedGroup?.roomIds?.contains(it.roomId) ?: true diff --git a/app/src/main/res/drawable-hdpi/ic_clear_white.png b/app/src/main/res/drawable-hdpi/ic_clear_white.png new file mode 100644 index 00000000..fc5a294e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_clear_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_clear_white.png b/app/src/main/res/drawable-mdpi/ic_clear_white.png new file mode 100644 index 00000000..9813279c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_clear_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_clear_white.png b/app/src/main/res/drawable-xhdpi/ic_clear_white.png new file mode 100644 index 00000000..0b00a33a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_clear_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_clear_white.png b/app/src/main/res/drawable-xxhdpi/ic_clear_white.png new file mode 100644 index 00000000..717c7b59 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_clear_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_clear_white.png b/app/src/main/res/drawable-xxxhdpi/ic_clear_white.png new file mode 100644 index 00000000..9ef2d8f9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_clear_white.png differ diff --git a/app/src/main/res/layout/fragment_room_list.xml b/app/src/main/res/layout/fragment_room_list.xml index 96c1eead..89af7058 100644 --- a/app/src/main/res/layout/fragment_room_list.xml +++ b/app/src/main/res/layout/fragment_room_list.xml @@ -11,15 +11,16 @@ android:id="@+id/filterRoomView" android:layout_width="0dp" android:layout_height="32dp" - android:layout_marginBottom="8dp" - android:layout_marginEnd="16dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="8dp" android:background="@drawable/bg_search_edit_text" android:drawableLeft="@drawable/ic_search_white" android:drawablePadding="8dp" android:drawableTint="#9fa9ba" android:hint="Filter by name..." + android:lines="1" android:paddingLeft="8dp" app:layout_constraintBottom_toTopOf="@+id/stateView" app:layout_constraintEnd_toEndOf="parent" @@ -30,8 +31,8 @@ android:id="@+id/stateView" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginBottom="0dp" android:layout_marginEnd="0dp" + android:layout_marginBottom="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"