Merge pull request #789 from vector-im/feature/christmas_fix

Auto completion for emojis
This commit is contained in:
Benoit Marty 2020-01-06 14:20:44 +01:00 committed by GitHub
commit 6245763577
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 581 additions and 207 deletions

View file

@ -11,6 +11,7 @@ Improvements 🙌:
- Introduce developer mode in the settings (#796) - Introduce developer mode in the settings (#796)
- Improve devices list screen - Improve devices list screen
- Add settings for rageshake sensibility - Add settings for rageshake sensibility
- Fix autocompletion issues and add support for rooms, groups, and emoji (#780)
Other changes: Other changes:
- -

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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
}
}

View file

@ -20,12 +20,10 @@ import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable
import android.text.Spannable import android.text.Spannable
import android.view.* import android.view.*
import android.widget.TextView import android.widget.TextView
@ -52,25 +50,18 @@ import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText 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.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData 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.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.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.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState 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.Timeline
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.room.timeline.getLastMessageContent 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.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem 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.R
import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer 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.AttachmentTypeSelectorView
import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.attachments.ContactAttachment 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.command.Command
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.getColorFromUserId 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.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.themes.ThemeUtils
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -142,11 +127,7 @@ class RoomDetailFragment @Inject constructor(
private val session: Session, private val session: Session,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val timelineEventController: TimelineEventController, private val timelineEventController: TimelineEventController,
private val commandAutocompletePolicy: CommandAutocompletePolicy, private val autoCompleter: AutoCompleter,
private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
private val autocompleteUserPresenter: AutocompleteUserPresenter,
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
private val permalinkHandler: PermalinkHandler, private val permalinkHandler: PermalinkHandler,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory, val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
@ -156,9 +137,7 @@ class RoomDetailFragment @Inject constructor(
) : ) :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
AutocompleteUserPresenter.Callback, AutoCompleter.AutoCompleterListener,
AutocompleteRoomPresenter.Callback,
AutocompleteGroupPresenter.Callback,
VectorInviteView.Callback, VectorInviteView.Callback,
JumpToReadMarkerView.Callback, JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback, AttachmentTypeSelectorView.Callback,
@ -397,7 +376,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun renderRegularMode(text: String) { private fun renderRegularMode(text: String) {
commandAutocompletePolicy.enabled = true autoCompleter.exitSpecialMode()
composerLayout.collapse() composerLayout.collapse()
updateComposerText(text) updateComposerText(text)
@ -408,7 +387,7 @@ class RoomDetailFragment @Inject constructor(
@DrawableRes iconRes: Int, @DrawableRes iconRes: Int,
@StringRes descriptionRes: Int, @StringRes descriptionRes: Int,
defaultContent: String) { defaultContent: String) {
commandAutocompletePolicy.enabled = false autoCompleter.enterSpecialMode()
// switch to expanded bar // switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply { composerLayout.composerRelatedMessageTitle.apply {
text = event.getDisambiguatedDisplayName() text = event.getDisambiguatedDisplayName()
@ -580,164 +559,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun setupComposer() { private fun setupComposer() {
val elevation = 6f autoCompleter.setup(composerLayout.composerEditText, this)
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()
composerLayout.callback = object : TextComposerView.Callback { composerLayout.callback = object : TextComposerView.Callback {
override fun onAddAttachment() { override fun onAddAttachment() {
@ -834,9 +656,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun renderTextComposerState(state: TextComposerViewState) { private fun renderTextComposerState(state: TextComposerViewState) {
autocompleteUserPresenter.render(state.asyncUsers) autoCompleter.render(state)
autocompleteRoomPresenter.render(state.asyncRooms)
autocompleteGroupPresenter.render(state.asyncGroups)
} }
private fun renderTombstoneEventHandling(async: Async<String>) { private fun renderTombstoneEventHandling(async: Async<String>) {

View file

@ -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.EventHtmlRenderer
import im.vector.riotx.features.html.VectorHtmlCompressor import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.reactions.data.EmojiDataSource
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -101,9 +102,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> { companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
@ -161,7 +159,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
RxRoom(room) RxRoom(room)
.liveAnnotationSummary(eventId) .liveAnnotationSummary(eventId)
.map { annotations -> .map { annotations ->
quickEmojis.map { emoji -> EmojiDataSource.quickEmojis.map { emoji ->
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false) ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
} }
} }

View file

@ -56,26 +56,10 @@ class EmojiSearchResultViewModel @AssistedInject constructor(
} }
private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { private fun updateQuery(action: EmojiSearchAction.UpdateQuery) {
val words = action.queryString.split("\\s".toRegex())
setState { setState {
copy( copy(
query = action.queryString, query = action.queryString,
// First add emojis with name matching query, sorted by name results = dataSource.filterWith(action.queryString)
// 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 }
) )
} }
} }

View file

@ -33,4 +33,49 @@ class EmojiDataSource @Inject constructor(
.fromJson(input.bufferedReader().use { it.readText() }) .fromJson(input.bufferedReader().use { it.readText() })
} }
?: EmojiData(emptyList(), emptyMap(), emptyMap()) ?: 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("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
}
} }

View 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>

View file

@ -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" />

View file

@ -18,4 +18,6 @@
<string name="devices_current_device">Current device</string> <string name="devices_current_device">Current device</string>
<string name="devices_other_devices">Other devices</string> <string name="devices_other_devices">Other devices</string>
<string name="autocomplete_limited_results">Showing only the first results, type more letters…</string>
</resources> </resources>