forked from GitHub-Mirror/riotX-android
Direct room: finally handle selection with chips (not as Nad design)
This commit is contained in:
parent
507bc2f622
commit
5af6bf3762
@ -206,7 +206,7 @@ dependencies {
|
||||
|
||||
// UI
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha07'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha08'
|
||||
implementation 'me.gujun.android:span:1.7'
|
||||
implementation "ru.noties.markwon:core:$markwon_version"
|
||||
implementation "ru.noties.markwon:html:$markwon_version"
|
||||
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.platform
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
|
||||
import im.vector.riotx.R
|
||||
|
||||
private const val DEFAULT_MAX_HEIGHT = 200
|
||||
|
||||
class MaxHeightScrollView : ScrollView {
|
||||
|
||||
var maxHeight: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
constructor(context: Context) : super(context) {}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
if (!isInEditMode) {
|
||||
init(context, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
if (!isInEditMode) {
|
||||
init(context, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
if (!isInEditMode) {
|
||||
init(context, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun init(context: Context, attrs: AttributeSet?) {
|
||||
if (attrs != null) {
|
||||
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView)
|
||||
maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)
|
||||
styledAttrs.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)
|
||||
super.onMeasure(widthMeasureSpec, newHeightMeasureSpec)
|
||||
}
|
||||
}
|
@ -24,11 +24,11 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.airbnb.mvrx.Async
|
||||
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.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
@ -72,14 +72,16 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||
if (isFirstCreation()) {
|
||||
addFragment(CreateDirectRoomFragment(), R.id.container)
|
||||
}
|
||||
viewModel.subscribe(this) { renderState(it) }
|
||||
viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) {
|
||||
renderCreateAndInviteState(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderState(state: CreateDirectRoomViewState) {
|
||||
when (state.createAndInviteState) {
|
||||
private fun renderCreateAndInviteState(state: Async<String>) {
|
||||
when (state) {
|
||||
is Loading -> renderCreationLoading()
|
||||
is Success -> renderCreationSuccess(state.createAndInviteState())
|
||||
is Fail -> renderCreationFailure(state.createAndInviteState.error)
|
||||
is Success -> renderCreationSuccess(state())
|
||||
is Fail -> renderCreationFailure(state.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@ 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.extensions.hideKeyboard
|
||||
import im.vector.riotx.core.extensions.setupAsSearch
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.*
|
||||
import javax.inject.Inject
|
||||
@ -60,6 +61,7 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), CreateDirec
|
||||
}
|
||||
|
||||
private fun setupSearchByMatrixIdView() {
|
||||
createDirectRoomSearchById.setupAsSearch()
|
||||
createDirectRoomSearchById
|
||||
.textChanges()
|
||||
.subscribe {
|
||||
|
@ -19,23 +19,25 @@
|
||||
package im.vector.riotx.features.home.createdirect
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.ScrollView
|
||||
import androidx.core.view.size
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.jakewharton.rxbinding3.widget.beforeTextChangeEvents
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
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.extensions.hideKeyboard
|
||||
import im.vector.riotx.core.extensions.observeEvent
|
||||
import im.vector.riotx.core.glide.GlideApp
|
||||
import im.vector.riotx.core.extensions.setupAsSearch
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.utils.DimensionUtils
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.html.PillImageSpan
|
||||
import kotlinx.android.synthetic.main.fragment_create_direct_room.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -64,12 +66,23 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle
|
||||
setupAddByMatrixIdView()
|
||||
setupCloseView()
|
||||
viewModel.selectUserEvent.observeEvent(this) {
|
||||
updateFilterViewWith(it)
|
||||
|
||||
updateChipsView(it)
|
||||
}
|
||||
viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) {
|
||||
renderSelectedUsers(it)
|
||||
}
|
||||
viewModel.subscribe(this) { renderState(it) }
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
withState(viewModel) {
|
||||
val createMenuItem = menu.findItem(R.id.action_create_direct_room)
|
||||
val showMenuItem = it.selectedUsers.isNotEmpty()
|
||||
createMenuItem.setVisible(showMenuItem)
|
||||
}
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_create_direct_room -> {
|
||||
@ -100,14 +113,7 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle
|
||||
createDirectRoomFilter
|
||||
.textChanges()
|
||||
.subscribe { text ->
|
||||
val userMatches = MatrixPatterns.PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER.findAll(text)
|
||||
val lastUserMatch = userMatches.lastOrNull()
|
||||
val filterValue = if (lastUserMatch == null) {
|
||||
text
|
||||
} else {
|
||||
text.substring(startIndex = lastUserMatch.range.endInclusive + 1)
|
||||
}.trim()
|
||||
|
||||
val filterValue = text.trim()
|
||||
val action = if (filterValue.isBlank()) {
|
||||
CreateDirectRoomActions.ClearFilterKnownUsers
|
||||
} else {
|
||||
@ -117,23 +123,7 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
|
||||
createDirectRoomFilter
|
||||
.beforeTextChangeEvents()
|
||||
.subscribe { event ->
|
||||
if (event.after == 0) {
|
||||
val sub = event.text.substring(0, event.start)
|
||||
val startIndexOfUser = sub.lastIndexOf(" ") + 1
|
||||
val user = sub.substring(startIndexOfUser)
|
||||
val selectedUser = withState(viewModel) { state ->
|
||||
state.selectedUsers.find { it.userId == user }
|
||||
}
|
||||
if (selectedUser != null) {
|
||||
viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(selectedUser))
|
||||
}
|
||||
}
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
|
||||
createDirectRoomFilter.setupAsSearch()
|
||||
createDirectRoomFilter.requestFocus()
|
||||
}
|
||||
|
||||
@ -147,32 +137,40 @@ class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomControlle
|
||||
directRoomController.setData(state)
|
||||
}
|
||||
|
||||
private fun updateFilterViewWith(data: SelectUserAction) = withState(viewModel) { state ->
|
||||
if (state.selectedUsers.isEmpty()) {
|
||||
createDirectRoomFilter.text = null
|
||||
private fun updateChipsView(data: SelectUserAction) {
|
||||
if (data.isAdded) {
|
||||
addChipToGroup(data.user, chipGroup)
|
||||
} else {
|
||||
val editable = createDirectRoomFilter.editableText
|
||||
val user = data.user
|
||||
if (data.isAdded) {
|
||||
val startIndex = editable.lastIndexOf(" ") + 1
|
||||
val endIndex = editable.length
|
||||
editable.replace(startIndex, endIndex, "${user.userId} ")
|
||||
val span = PillImageSpan(GlideApp.with(this), avatarRenderer, requireContext(), user.userId, user)
|
||||
span.bind(createDirectRoomFilter)
|
||||
editable.setSpan(span, startIndex, startIndex + user.userId.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
} else {
|
||||
val startIndex = editable.indexOf(user.userId)
|
||||
if (startIndex != -1) {
|
||||
var endIndex = editable.indexOf(" ", startIndex) + 1
|
||||
if (endIndex == 0) {
|
||||
endIndex = editable.length
|
||||
}
|
||||
editable.replace(startIndex, endIndex, "")
|
||||
}
|
||||
if (chipGroup.size > data.index) {
|
||||
chipGroup.removeViewAt(data.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderSelectedUsers(selectedUsers: Set<User>) {
|
||||
vectorBaseActivity.invalidateOptionsMenu()
|
||||
if (selectedUsers.isNotEmpty() && chipGroup.size == 0) {
|
||||
selectedUsers.forEach { addChipToGroup(it, chipGroup) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun addChipToGroup(user: User, chipGroup: ChipGroup) {
|
||||
val chip = Chip(requireContext())
|
||||
chip.setChipBackgroundColorResource(android.R.color.transparent)
|
||||
chip.chipStrokeWidth = DimensionUtils.dpToPx(1, requireContext()).toFloat()
|
||||
chip.text = if (user.displayName.isNullOrBlank()) user.userId else user.displayName
|
||||
chip.isClickable = true
|
||||
chip.isCheckable = false
|
||||
chip.isCloseIconVisible = true
|
||||
chipGroup.addView(chip)
|
||||
chip.setOnCloseIconClickListener {
|
||||
viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(user))
|
||||
}
|
||||
chipGroupContainer.post {
|
||||
chipGroupContainer.fullScroll(ScrollView.FOCUS_DOWN)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(user: User) {
|
||||
view?.hideKeyboard()
|
||||
viewModel.handle(CreateDirectRoomActions.SelectUser(user))
|
||||
|
@ -40,6 +40,7 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserI
|
||||
@EpoxyAttribute var clickListener: View.OnClickListener? = null
|
||||
@EpoxyAttribute var selected: Boolean = false
|
||||
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.view.setOnClickListener(clickListener)
|
||||
// If name is empty, use userId as name and force it being centered
|
||||
@ -51,7 +52,11 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserI
|
||||
holder.nameView.text = name
|
||||
holder.userIdView.text = userId
|
||||
}
|
||||
if (selected) {
|
||||
renderSelection(holder, selected)
|
||||
}
|
||||
|
||||
private fun renderSelection(holder: Holder, isSelected: Boolean) {
|
||||
if (isSelected) {
|
||||
holder.avatarCheckedImageView.visibility = View.VISIBLE
|
||||
val backgroundColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent)
|
||||
val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor)
|
||||
|
@ -41,7 +41,8 @@ private typealias DirectoryUsersSearch = String
|
||||
|
||||
data class SelectUserAction(
|
||||
val user: User,
|
||||
val isAdded: Boolean
|
||||
val isAdded: Boolean,
|
||||
val index: Int
|
||||
)
|
||||
|
||||
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
||||
@ -102,28 +103,31 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState {
|
||||
val selectedUsers = it.selectedUsers.minus(action.user)
|
||||
private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState { state ->
|
||||
val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
|
||||
val selectedUsers = state.selectedUsers.minus(action.user)
|
||||
setState { copy(selectedUsers = selectedUsers) }
|
||||
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, false))
|
||||
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, false, index))
|
||||
}
|
||||
|
||||
private fun handleSelectUser(action: CreateDirectRoomActions.SelectUser) = withState {
|
||||
private fun handleSelectUser(action: CreateDirectRoomActions.SelectUser) = withState { state ->
|
||||
//Reset the filter asap
|
||||
knownUsersFilter.accept(Option.empty())
|
||||
directoryUsersSearch.accept("")
|
||||
|
||||
val isAddOperation: Boolean
|
||||
val selectedUsers: Set<User>
|
||||
if (it.selectedUsers.contains(action.user)) {
|
||||
selectedUsers = it.selectedUsers.minus(action.user)
|
||||
isAddOperation = false
|
||||
} else {
|
||||
selectedUsers = it.selectedUsers.plus(action.user)
|
||||
val indexOfUser = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
|
||||
val changeIndex: Int
|
||||
if (indexOfUser == -1) {
|
||||
changeIndex = state.selectedUsers.size
|
||||
selectedUsers = state.selectedUsers.plus(action.user)
|
||||
isAddOperation = true
|
||||
} else {
|
||||
changeIndex = indexOfUser
|
||||
selectedUsers = state.selectedUsers.minus(action.user)
|
||||
isAddOperation = false
|
||||
}
|
||||
setState { copy(selectedUsers = selectedUsers) }
|
||||
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation))
|
||||
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation, changeIndex))
|
||||
}
|
||||
|
||||
private fun observeDirectoryUsers() {
|
||||
@ -153,7 +157,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,28 +58,52 @@
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/createDirectRoomFilterContainer"
|
||||
<im.vector.riotx.core.platform.MaxHeightScrollView
|
||||
android:id="@+id/chipGroupContainer"
|
||||
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:cardElevation="4dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar">
|
||||
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar"
|
||||
app:maxHeight="80dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/createDirectRoomFilter"
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chipGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxHeight="80dp"
|
||||
android:importantForAutofill="no"
|
||||
android:hint="@string/room_directory_search_hint"/>
|
||||
app:lineSpacing="4dp" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
</im.vector.riotx.core.platform.MaxHeightScrollView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/createDirectRoomFilter"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
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:importantForAutofill="no"
|
||||
android:maxHeight="80dp"
|
||||
android:padding="8dp"
|
||||
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" />
|
||||
|
||||
<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"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomFilter" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/addByMatrixId"
|
||||
@ -97,7 +121,7 @@
|
||||
app:iconPadding="13dp"
|
||||
app:iconTint="@color/riotx_accent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/createDirectRoomFilterContainer" />
|
||||
app:layout_constraintTop_toBottomOf="@id/createDirectRoomFilterDivider" />
|
||||
|
||||
<com.airbnb.epoxy.EpoxyRecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
|
@ -30,6 +30,7 @@
|
||||
android:id="@+id/createDirectRoomUserAvatarChecked"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_material_done"
|
||||
android:tint="@android:color/white"
|
||||
android:visibility="visible" />
|
||||
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="MaxHeightScrollView">
|
||||
<attr name="maxHeight" format="dimension" />
|
||||
</declare-styleable>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user