mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +03:00
Extract AutoComplete feature from RoomDetailFragment
This commit is contained in:
parent
ed097bcf37
commit
d73a1135ae
2 changed files with 247 additions and 186 deletions
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
* 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.features.home.room.detail
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.widget.EditText
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.otaliastudios.autocomplete.Autocomplete
|
||||
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||
import com.otaliastudios.autocomplete.CharPolicy
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.glide.GlideApp
|
||||
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
||||
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
|
||||
import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
|
||||
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
|
||||
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
||||
import im.vector.riotx.features.command.Command
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
|
||||
import im.vector.riotx.features.html.PillImageSpan
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
class AutoCompleter @Inject constructor(
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val commandAutocompletePolicy: CommandAutocompletePolicy,
|
||||
private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
|
||||
private val autocompleteUserPresenter: AutocompleteUserPresenter,
|
||||
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
|
||||
private val autocompleteGroupPresenter: AutocompleteGroupPresenter
|
||||
) {
|
||||
private lateinit var fragment: Fragment
|
||||
|
||||
fun enterSpecialMode() {
|
||||
commandAutocompletePolicy.enabled = false
|
||||
}
|
||||
|
||||
fun exitSpecialMode() {
|
||||
commandAutocompletePolicy.enabled = true
|
||||
}
|
||||
|
||||
private val glideRequests by lazy {
|
||||
GlideApp.with(fragment)
|
||||
}
|
||||
|
||||
fun setup(fragment: Fragment, editText: EditText, listener: AutoCompleterListener) {
|
||||
this.fragment = fragment
|
||||
|
||||
val elevation = 6f
|
||||
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(fragment.requireContext(), R.attr.riotx_background))
|
||||
Autocomplete.on<Command>(editText)
|
||||
.with(commandAutocompletePolicy)
|
||||
.with(autocompleteCommandPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<Command> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
|
||||
editable.clear()
|
||||
editable
|
||||
.append(item.command)
|
||||
.append(" ")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
autocompleteRoomPresenter.callback = listener
|
||||
Autocomplete.on<RoomSummary>(editText)
|
||||
.with(CharPolicy('#', true))
|
||||
.with(autocompleteRoomPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<RoomSummary> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
|
||||
// Detect last '#' and remove it
|
||||
var startIndex = editable.lastIndexOf("#")
|
||||
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 matrixItem = item.toRoomAliasMatrixItem()
|
||||
val displayName = matrixItem.getBestName()
|
||||
|
||||
// with a trailing space
|
||||
editable.replace(startIndex, endIndex, "$displayName ")
|
||||
|
||||
// Add the span
|
||||
val span = PillImageSpan(
|
||||
glideRequests,
|
||||
avatarRenderer,
|
||||
fragment.requireContext(),
|
||||
matrixItem
|
||||
)
|
||||
span.bind(editText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
autocompleteGroupPresenter.callback = listener
|
||||
Autocomplete.on<GroupSummary>(editText)
|
||||
.with(CharPolicy('+', true))
|
||||
.with(autocompleteGroupPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<GroupSummary> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
|
||||
// Detect last '+' and remove it
|
||||
var startIndex = editable.lastIndexOf("+")
|
||||
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 matrixItem = item.toMatrixItem()
|
||||
val displayName = matrixItem.getBestName()
|
||||
|
||||
// with a trailing space
|
||||
editable.replace(startIndex, endIndex, "$displayName ")
|
||||
|
||||
// Add the span
|
||||
val span = PillImageSpan(
|
||||
glideRequests,
|
||||
avatarRenderer,
|
||||
fragment.requireContext(),
|
||||
matrixItem
|
||||
)
|
||||
span.bind(editText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
autocompleteUserPresenter.callback = listener
|
||||
Autocomplete.on<User>(editText)
|
||||
.with(CharPolicy('@', true))
|
||||
.with(autocompleteUserPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<User> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: User): Boolean {
|
||||
// Detect last '@' and remove it
|
||||
var startIndex = editable.lastIndexOf("@")
|
||||
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 matrixItem = item.toMatrixItem()
|
||||
val displayName = matrixItem.getBestName()
|
||||
|
||||
// with a trailing space
|
||||
editable.replace(startIndex, endIndex, "$displayName ")
|
||||
|
||||
// Add the span
|
||||
val span = PillImageSpan(
|
||||
glideRequests,
|
||||
avatarRenderer,
|
||||
fragment.requireContext(),
|
||||
matrixItem
|
||||
)
|
||||
span.bind(editText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
fun render(state: TextComposerViewState) {
|
||||
autocompleteUserPresenter.render(state.asyncUsers)
|
||||
autocompleteRoomPresenter.render(state.asyncRooms)
|
||||
autocompleteGroupPresenter.render(state.asyncGroups)
|
||||
}
|
||||
|
||||
interface AutoCompleterListener :
|
||||
AutocompleteUserPresenter.Callback,
|
||||
AutocompleteRoomPresenter.Callback,
|
||||
AutocompleteGroupPresenter.Callback
|
||||
}
|
|
@ -20,12 +20,10 @@ import android.annotation.SuppressLint
|
|||
import android.app.Activity.RESULT_OK
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.view.*
|
||||
import android.widget.TextView
|
||||
|
@ -52,25 +50,18 @@ import com.github.piasy.biv.BigImageViewer
|
|||
import com.github.piasy.biv.loader.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.otaliastudios.autocomplete.Autocomplete
|
||||
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||
import com.otaliastudios.autocomplete.CharPolicy
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.dialogs.withColoredButton
|
||||
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
|
||||
|
@ -84,11 +75,6 @@ import im.vector.riotx.core.utils.*
|
|||
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
|
||||
import im.vector.riotx.features.attachments.AttachmentsHelper
|
||||
import im.vector.riotx.features.attachments.ContactAttachment
|
||||
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
||||
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
|
||||
import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
|
||||
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
|
||||
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
||||
import im.vector.riotx.features.command.Command
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.getColorFromUserId
|
||||
|
@ -117,7 +103,6 @@ import im.vector.riotx.features.permalink.PermalinkHandler
|
|||
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import im.vector.riotx.features.share.SharedData
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
@ -142,11 +127,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
private val session: Session,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val timelineEventController: TimelineEventController,
|
||||
private val commandAutocompletePolicy: CommandAutocompletePolicy,
|
||||
private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
|
||||
private val autocompleteUserPresenter: AutocompleteUserPresenter,
|
||||
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
|
||||
private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
|
||||
private val autoCompleter: AutoCompleter,
|
||||
private val permalinkHandler: PermalinkHandler,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
|
||||
|
@ -156,9 +137,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
) :
|
||||
VectorBaseFragment(),
|
||||
TimelineEventController.Callback,
|
||||
AutocompleteUserPresenter.Callback,
|
||||
AutocompleteRoomPresenter.Callback,
|
||||
AutocompleteGroupPresenter.Callback,
|
||||
AutoCompleter.AutoCompleterListener,
|
||||
VectorInviteView.Callback,
|
||||
JumpToReadMarkerView.Callback,
|
||||
AttachmentTypeSelectorView.Callback,
|
||||
|
@ -397,7 +376,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun renderRegularMode(text: String) {
|
||||
commandAutocompletePolicy.enabled = true
|
||||
autoCompleter.exitSpecialMode()
|
||||
composerLayout.collapse()
|
||||
|
||||
updateComposerText(text)
|
||||
|
@ -408,7 +387,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
@DrawableRes iconRes: Int,
|
||||
@StringRes descriptionRes: Int,
|
||||
defaultContent: String) {
|
||||
commandAutocompletePolicy.enabled = false
|
||||
autoCompleter.enterSpecialMode()
|
||||
// switch to expanded bar
|
||||
composerLayout.composerRelatedMessageTitle.apply {
|
||||
text = event.getDisambiguatedDisplayName()
|
||||
|
@ -580,164 +559,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun setupComposer() {
|
||||
val elevation = 6f
|
||||
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background))
|
||||
Autocomplete.on<Command>(composerLayout.composerEditText)
|
||||
.with(commandAutocompletePolicy)
|
||||
.with(autocompleteCommandPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<Command> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
|
||||
editable.clear()
|
||||
editable
|
||||
.append(item.command)
|
||||
.append(" ")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
autocompleteRoomPresenter.callback = this
|
||||
Autocomplete.on<RoomSummary>(composerLayout.composerEditText)
|
||||
.with(CharPolicy('#', true))
|
||||
.with(autocompleteRoomPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<RoomSummary> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
|
||||
// Detect last '#' and remove it
|
||||
var startIndex = editable.lastIndexOf("#")
|
||||
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 matrixItem = item.toRoomAliasMatrixItem()
|
||||
val displayName = matrixItem.getBestName()
|
||||
|
||||
// with a trailing space
|
||||
editable.replace(startIndex, endIndex, "$displayName ")
|
||||
|
||||
// Add the span
|
||||
val span = PillImageSpan(
|
||||
glideRequests,
|
||||
avatarRenderer,
|
||||
requireContext(),
|
||||
matrixItem
|
||||
)
|
||||
span.bind(composerLayout.composerEditText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
autocompleteGroupPresenter.callback = this
|
||||
Autocomplete.on<GroupSummary>(composerLayout.composerEditText)
|
||||
.with(CharPolicy('+', true))
|
||||
.with(autocompleteGroupPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<GroupSummary> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
|
||||
// Detect last '+' and remove it
|
||||
var startIndex = editable.lastIndexOf("+")
|
||||
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 matrixItem = item.toMatrixItem()
|
||||
val displayName = matrixItem.getBestName()
|
||||
|
||||
// with a trailing space
|
||||
editable.replace(startIndex, endIndex, "$displayName ")
|
||||
|
||||
// Add the span
|
||||
val span = PillImageSpan(
|
||||
glideRequests,
|
||||
avatarRenderer,
|
||||
requireContext(),
|
||||
matrixItem
|
||||
)
|
||||
span.bind(composerLayout.composerEditText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
autocompleteUserPresenter.callback = this
|
||||
Autocomplete.on<User>(composerLayout.composerEditText)
|
||||
.with(CharPolicy('@', true))
|
||||
.with(autocompleteUserPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<User> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: User): Boolean {
|
||||
// Detect last '@' and remove it
|
||||
var startIndex = editable.lastIndexOf("@")
|
||||
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 matrixItem = item.toMatrixItem()
|
||||
val displayName = matrixItem.getBestName()
|
||||
|
||||
// with a trailing space
|
||||
editable.replace(startIndex, endIndex, "$displayName ")
|
||||
|
||||
// Add the span
|
||||
val span = PillImageSpan(
|
||||
glideRequests,
|
||||
avatarRenderer,
|
||||
requireContext(),
|
||||
matrixItem
|
||||
)
|
||||
span.bind(composerLayout.composerEditText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
autoCompleter.setup(this, composerLayout.composerEditText, this)
|
||||
|
||||
composerLayout.callback = object : TextComposerView.Callback {
|
||||
override fun onAddAttachment() {
|
||||
|
@ -834,9 +656,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun renderTextComposerState(state: TextComposerViewState) {
|
||||
autocompleteUserPresenter.render(state.asyncUsers)
|
||||
autocompleteRoomPresenter.render(state.asyncRooms)
|
||||
autocompleteGroupPresenter.render(state.asyncGroups)
|
||||
autoCompleter.render(state)
|
||||
}
|
||||
|
||||
private fun renderTombstoneEventHandling(async: Async<String>) {
|
||||
|
|
Loading…
Reference in a new issue