Autocomplete : handle click

This commit is contained in:
Benoit Marty 2019-04-09 09:58:07 +02:00
parent c64d6b6b28
commit 3f1cc466ed
5 changed files with 48 additions and 26 deletions

View File

@ -19,6 +19,9 @@ package im.vector.riotredesign.core.epoxy
import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.EpoxyModelWithHolder
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState


/**
* EpoxyModelWithHolder which can listen to visibility state change
*/
abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>() { abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>() {


private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null

View File

@ -22,7 +22,6 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.epoxy.EpoxyRecyclerView
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.AutocompletePresenter import com.otaliastudios.autocomplete.AutocompletePresenter
import im.vector.riotredesign.core.listener.Listener import im.vector.riotredesign.core.listener.Listener


@ -60,25 +59,15 @@ abstract class EpoxyAutocompletePresenter<T>(context: Context) : AutocompletePre
} }


abstract fun providesController(): EpoxyController abstract fun providesController(): EpoxyController
/**
* Dispatch click event to [AutocompleteCallback].
* Should be called when items are clicked.
*
* @param item the clicked item.
*/
protected fun dispatchClick(item: T) {
clicks?.click(item)
}


protected fun dispatchLayoutChange() { protected fun dispatchLayoutChange() {
observer?.onChanged() observer?.onChanged()
} }


override fun onEvent(t: T) { override fun onEvent(t: T) {
dispatchClick(t) clicks?.click(t)
} }



private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() { private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() {


override fun onChanged() { override fun onChanged() {

View File

@ -19,7 +19,7 @@ package im.vector.riotredesign.features.autocomplete.command
import android.text.Spannable import android.text.Spannable
import com.otaliastudios.autocomplete.AutocompletePolicy import com.otaliastudios.autocomplete.AutocompletePolicy


class CommandPolicy : AutocompletePolicy { class CommandAutocompletePolicy : AutocompletePolicy {
override fun getQuery(text: Spannable): CharSequence { override fun getQuery(text: Spannable): CharSequence {
if (text.length > 0) { if (text.length > 0) {
return text.substring(1, text.length) return text.substring(1, text.length)

View File

@ -21,6 +21,7 @@ import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable import android.text.Editable
import android.text.Spannable
import android.view.View import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -29,14 +30,16 @@ import com.airbnb.mvrx.fragmentViewModel
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotredesign.features.autocomplete.command.CommandPolicy import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.command.Command import im.vector.riotredesign.features.command.Command
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
@ -47,6 +50,7 @@ import im.vector.riotredesign.features.home.room.detail.composer.TextComposerVie
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
import im.vector.riotredesign.features.html.PillImageSpan
import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.MediaContentRenderer
import im.vector.riotredesign.features.media.MediaViewerActivity import im.vector.riotredesign.features.media.MediaViewerActivity
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -75,6 +79,12 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
} }
} }


private val session by inject<Session>()
// TODO Inject?
private val glideRequests by lazy {
GlideApp.with(this)
}

private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel() private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
private val timelineEventController: TimelineEventController by inject { parametersOf(this) } private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
@ -136,16 +146,16 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
val elevation = 6f val elevation = 6f
val backgroundDrawable = ColorDrawable(Color.WHITE) val backgroundDrawable = ColorDrawable(Color.WHITE)
Autocomplete.on<Command>(composerEditText) Autocomplete.on<Command>(composerEditText)
.with(CommandPolicy()) .with(CommandAutocompletePolicy())
.with(autocompleteCommandPresenter) .with(autocompleteCommandPresenter)
.with(elevation) .with(elevation)
.with(backgroundDrawable) .with(backgroundDrawable)
.with(object : AutocompleteCallback<Command> { .with(object : AutocompleteCallback<Command> {
override fun onPopupItemClicked(editable: Editable?, item: Command?): Boolean { override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
editable?.clear() editable.clear()
editable editable
?.append(item?.command) .append(item.command)
?.append(" ") .append(" ")
return true return true
} }


@ -156,15 +166,37 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac


autocompleteUserPresenter.callback = this autocompleteUserPresenter.callback = this
Autocomplete.on<User>(composerEditText) Autocomplete.on<User>(composerEditText)
.with(CharPolicy('@', false)) .with(CharPolicy('@', true))
.with(autocompleteUserPresenter) .with(autocompleteUserPresenter)
.with(elevation) .with(elevation)
.with(backgroundDrawable) .with(backgroundDrawable)
.with(object : AutocompleteCallback<User> { .with(object : AutocompleteCallback<User> {
override fun onPopupItemClicked(editable: Editable?, item: User?): Boolean { override fun onPopupItemClicked(editable: Editable, item: User): Boolean {
// TODO // Detect last '@' and remove it
editable?.append(item?.displayName) var startIndex = editable.lastIndexOf("@")
?.append(" ") if (startIndex == -1) {
startIndex = 0
}

// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}

// Replace the word by its completion
val displayName = item.displayName ?: item.userId

// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")

// Add the span
val user = session.getUser(item.userId)
// FIXME avatar is not displayed
val span = PillImageSpan(glideRequests, context!!, item.userId, user)

editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

return true return true
} }


@ -224,5 +256,4 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
override fun onQueryUsers(query: CharSequence?) { override fun onQueryUsers(query: CharSequence?) {
textComposerViewModel.process(TextComposerActions.QueryUsers(query)) textComposerViewModel.process(TextComposerActions.QueryUsers(query))
} }

} }

View File

@ -36,7 +36,6 @@ import java.lang.ref.WeakReference
* This span is able to replace a text by a [ChipDrawable] * This span is able to replace a text by a [ChipDrawable]
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached. * It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
*/ */

class PillImageSpan(private val glideRequests: GlideRequests, class PillImageSpan(private val glideRequests: GlideRequests,
private val context: Context, private val context: Context,
private val userId: String, private val userId: String,