mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-01-31 03:17:13 +01:00
Merge pull request #789 from vector-im/feature/christmas_fix
Auto completion for emojis
This commit is contained in:
commit
6245763577
@ -11,6 +11,7 @@ Improvements 🙌:
|
||||
- Introduce developer mode in the settings (#796)
|
||||
- Improve devices list screen
|
||||
- Add settings for rageshake sensibility
|
||||
- Fix autocompletion issues and add support for rooms, groups, and emoji (#780)
|
||||
|
||||
Other changes:
|
||||
-
|
||||
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.autocomplete.emoji
|
||||
|
||||
import android.graphics.Typeface
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.riotx.EmojiCompatFontProvider
|
||||
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
|
||||
import im.vector.riotx.features.reactions.ReactionClickListener
|
||||
import im.vector.riotx.features.reactions.data.EmojiItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class AutocompleteEmojiController @Inject constructor(
|
||||
private val fontProvider: EmojiCompatFontProvider
|
||||
) : TypedEpoxyController<List<EmojiItem>>() {
|
||||
|
||||
var emojiTypeface: Typeface? = fontProvider.typeface
|
||||
|
||||
private val fontProviderListener = object : EmojiCompatFontProvider.FontProviderListener {
|
||||
override fun compatibilityFontUpdate(typeface: Typeface?) {
|
||||
emojiTypeface = typeface
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
fontProvider.addListener(fontProviderListener)
|
||||
}
|
||||
|
||||
var listener: AutocompleteClickListener<String>? = null
|
||||
|
||||
override fun buildModels(data: List<EmojiItem>?) {
|
||||
if (data.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
data
|
||||
.take(MAX)
|
||||
.forEach { emojiItem ->
|
||||
autocompleteEmojiItem {
|
||||
id(emojiItem.name)
|
||||
emojiItem(emojiItem)
|
||||
emojiTypeFace(emojiTypeface)
|
||||
onClickListener(
|
||||
object : ReactionClickListener {
|
||||
override fun onReactionSelected(reaction: String) {
|
||||
listener?.onItemClick(reaction)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.size > MAX) {
|
||||
autocompleteMoreResultItem {
|
||||
id("more_result")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView)
|
||||
fontProvider.removeListener(fontProviderListener)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX = 50
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.autocomplete.emoji
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
import im.vector.riotx.features.reactions.ReactionClickListener
|
||||
import im.vector.riotx.features.reactions.data.EmojiItem
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji)
|
||||
abstract class AutocompleteEmojiItem : VectorEpoxyModel<AutocompleteEmojiItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var emojiItem: EmojiItem
|
||||
|
||||
@EpoxyAttribute
|
||||
var emojiTypeFace: Typeface? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var onClickListener: ReactionClickListener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.emojiText.text = emojiItem.emoji
|
||||
holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT
|
||||
holder.emojiNameText.text = emojiItem.name
|
||||
holder.emojiKeywordText.setTextOrHide(emojiItem.keywords.joinToString())
|
||||
|
||||
holder.view.setOnClickListener {
|
||||
onClickListener?.onReactionSelected(emojiItem.emoji)
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val emojiText by bind<TextView>(R.id.itemAutocompleteEmoji)
|
||||
val emojiNameText by bind<TextView>(R.id.itemAutocompleteEmojiName)
|
||||
val emojiKeywordText by bind<TextView>(R.id.itemAutocompleteEmojiSubname)
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.autocomplete.emoji
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.otaliastudios.autocomplete.RecyclerViewPresenter
|
||||
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
|
||||
import im.vector.riotx.features.reactions.data.EmojiDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
class AutocompleteEmojiPresenter @Inject constructor(context: Context,
|
||||
private val emojiDataSource: EmojiDataSource,
|
||||
private val controller: AutocompleteEmojiController) :
|
||||
RecyclerViewPresenter<String>(context), AutocompleteClickListener<String> {
|
||||
|
||||
init {
|
||||
controller.listener = this
|
||||
}
|
||||
|
||||
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
|
||||
// Also remove animation
|
||||
recyclerView?.itemAnimator = null
|
||||
return controller.adapter
|
||||
}
|
||||
|
||||
override fun onItemClick(t: String) {
|
||||
dispatchClick(t)
|
||||
}
|
||||
|
||||
override fun onQuery(query: CharSequence?) {
|
||||
val data = if (query.isNullOrBlank()) {
|
||||
// Return common emojis
|
||||
emojiDataSource.getQuickReactions()
|
||||
} else {
|
||||
emojiDataSource.filterWith(query.toString())
|
||||
}
|
||||
controller.setData(data)
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.autocomplete.emoji
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_autocomplete_more_result)
|
||||
abstract class AutocompleteMoreResultItem : VectorEpoxyModel<AutocompleteMoreResultItem.Holder>() {
|
||||
|
||||
class Holder : VectorEpoxyHolder()
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
/*
|
||||
* 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.graphics.drawable.Drawable
|
||||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.widget.EditText
|
||||
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.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.glide.GlideApp
|
||||
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
||||
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
|
||||
import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter
|
||||
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 val autocompleteEmojiPresenter: AutocompleteEmojiPresenter
|
||||
) {
|
||||
private lateinit var editText: EditText
|
||||
|
||||
fun enterSpecialMode() {
|
||||
commandAutocompletePolicy.enabled = false
|
||||
}
|
||||
|
||||
fun exitSpecialMode() {
|
||||
commandAutocompletePolicy.enabled = true
|
||||
}
|
||||
|
||||
private val glideRequests by lazy {
|
||||
GlideApp.with(editText)
|
||||
}
|
||||
|
||||
fun setup(editText: EditText, listener: AutoCompleterListener) {
|
||||
this.editText = editText
|
||||
|
||||
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background))
|
||||
|
||||
setupCommands(backgroundDrawable, editText)
|
||||
setupUsers(backgroundDrawable, editText, listener)
|
||||
setupRooms(backgroundDrawable, editText, listener)
|
||||
setupGroups(backgroundDrawable, editText, listener)
|
||||
setupEmojis(backgroundDrawable, editText)
|
||||
}
|
||||
|
||||
fun render(state: TextComposerViewState) {
|
||||
autocompleteUserPresenter.render(state.asyncUsers)
|
||||
autocompleteRoomPresenter.render(state.asyncRooms)
|
||||
autocompleteGroupPresenter.render(state.asyncGroups)
|
||||
}
|
||||
|
||||
private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) {
|
||||
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()
|
||||
}
|
||||
|
||||
private fun setupUsers(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteUserPresenter.Callback) {
|
||||
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 {
|
||||
insertMatrixItem(editText, editable, "@", item.toMatrixItem())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteRoomPresenter.Callback) {
|
||||
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 {
|
||||
insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteGroupPresenter.Callback) {
|
||||
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 {
|
||||
insertMatrixItem(editText, editable, "+", item.toMatrixItem())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
|
||||
Autocomplete.on<String>(editText)
|
||||
.with(CharPolicy(':', false))
|
||||
.with(autocompleteEmojiPresenter)
|
||||
.with(ELEVATION)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<String> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: String): 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
|
||||
editable.replace(startIndex, endIndex, item)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) {
|
||||
// Detect last firstChar and remove it
|
||||
var startIndex = editable.lastIndexOf(firstChar)
|
||||
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 = matrixItem.getBestName()
|
||||
|
||||
// with a trailing space
|
||||
editable.replace(startIndex, endIndex, "$displayName ")
|
||||
|
||||
// Add the span
|
||||
val span = PillImageSpan(
|
||||
glideRequests,
|
||||
avatarRenderer,
|
||||
editText.context,
|
||||
matrixItem
|
||||
)
|
||||
span.bind(editText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
|
||||
interface AutoCompleterListener :
|
||||
AutocompleteUserPresenter.Callback,
|
||||
AutocompleteRoomPresenter.Callback,
|
||||
AutocompleteGroupPresenter.Callback
|
||||
|
||||
companion object {
|
||||
private const val ELEVATION = 6f
|
||||
}
|
||||
}
|
@ -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(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>) {
|
||||
|
@ -43,6 +43,7 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformatio
|
||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||
import im.vector.riotx.features.html.VectorHtmlCompressor
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import im.vector.riotx.features.reactions.data.EmojiDataSource
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@ -101,9 +102,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
|
||||
|
||||
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
|
||||
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
@ -161,7 +159,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
RxRoom(room)
|
||||
.liveAnnotationSummary(eventId)
|
||||
.map { annotations ->
|
||||
quickEmojis.map { emoji ->
|
||||
EmojiDataSource.quickEmojis.map { emoji ->
|
||||
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
|
||||
}
|
||||
}
|
||||
|
@ -56,26 +56,10 @@ class EmojiSearchResultViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun updateQuery(action: EmojiSearchAction.UpdateQuery) {
|
||||
val words = action.queryString.split("\\s".toRegex())
|
||||
setState {
|
||||
copy(
|
||||
query = action.queryString,
|
||||
// First add emojis with name matching query, sorted by name
|
||||
// Then emojis with keyword matching any of the word in the query, sorted by name
|
||||
results = dataSource.rawData.emojis
|
||||
.values
|
||||
.filter { emojiItem ->
|
||||
emojiItem.name.contains(action.queryString, true)
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
+ dataSource.rawData.emojis
|
||||
.values
|
||||
.filter { emojiItem ->
|
||||
words.fold(true, { prev, word ->
|
||||
prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
|
||||
})
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
results = dataSource.filterWith(action.queryString)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -33,4 +33,49 @@ class EmojiDataSource @Inject constructor(
|
||||
.fromJson(input.bufferedReader().use { it.readText() })
|
||||
}
|
||||
?: EmojiData(emptyList(), emptyMap(), emptyMap())
|
||||
|
||||
private val quickReactions = mutableListOf<EmojiItem>()
|
||||
|
||||
fun filterWith(query: String): List<EmojiItem> {
|
||||
val words = query.split("\\s".toRegex())
|
||||
|
||||
// First add emojis with name matching query, sorted by name
|
||||
return (rawData.emojis.values
|
||||
.filter { emojiItem ->
|
||||
emojiItem.name.contains(query, true)
|
||||
}
|
||||
.sortedBy { it.name } +
|
||||
// Then emojis with keyword matching any of the word in the query, sorted by name
|
||||
rawData.emojis.values
|
||||
.filter { emojiItem ->
|
||||
words.fold(true, { prev, word ->
|
||||
prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
|
||||
})
|
||||
}
|
||||
.sortedBy { it.name })
|
||||
// and ensure they will not be present twice
|
||||
.distinct()
|
||||
}
|
||||
|
||||
fun getQuickReactions(): List<EmojiItem> {
|
||||
if (quickReactions.isEmpty()) {
|
||||
listOf(
|
||||
"+1", // 👍
|
||||
"-1", // 👎
|
||||
"grinning", // 😄
|
||||
"tada", // 🎉
|
||||
"confused", // 😕
|
||||
"heart", // ❤️
|
||||
"rocket", // 🚀
|
||||
"eyes" // 👀
|
||||
)
|
||||
.mapNotNullTo(quickReactions) { rawData.emojis[it] }
|
||||
}
|
||||
|
||||
return quickReactions
|
||||
}
|
||||
|
||||
companion object {
|
||||
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
|
||||
}
|
||||
}
|
||||
|
56
vector/src/main/res/layout/item_autocomplete_emoji.xml
Normal file
56
vector/src/main/res/layout/item_autocomplete_emoji.xml
Normal file
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?riotx_background"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemAutocompleteEmoji"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="20dp"
|
||||
tools:ignore="SpUsage"
|
||||
tools:text="@sample/reactions.json/data/reaction" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemAutocompleteEmojiName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemAutocompleteEmojiSubname"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
tools:text="name"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?riotx_background"
|
||||
android:padding="8dp"
|
||||
android:text="@string/autocomplete_limited_results"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp" />
|
@ -18,4 +18,6 @@
|
||||
<string name="devices_current_device">Current device</string>
|
||||
<string name="devices_other_devices">Other devices</string>
|
||||
|
||||
<string name="autocomplete_limited_results">Showing only the first results, type more letters…</string>
|
||||
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user