Easier access to more custom emotes
- Expand button - More emotes by default Change-Id: Id18f0b36099465d83156fcee2d3b016f299402f4
This commit is contained in:
parent
1004bd19e3
commit
236c44a5a5
|
@ -16,10 +16,16 @@
|
||||||
|
|
||||||
package im.vector.app.features.autocomplete
|
package im.vector.app.features.autocomplete
|
||||||
|
|
||||||
|
import im.vector.app.features.autocomplete.member.AutocompleteEmojiDataItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple generic listener interface.
|
* Simple generic listener interface.
|
||||||
*/
|
*/
|
||||||
interface AutocompleteClickListener<T> {
|
interface AutocompleteClickListener<T> {
|
||||||
|
|
||||||
fun onItemClick(t: T)
|
fun onItemClick(t: T)
|
||||||
|
|
||||||
|
fun onLoadMoreClick(item: AutocompleteEmojiDataItem.Expand) {}
|
||||||
|
|
||||||
|
fun maxShowSizeOverride(): Int? = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ import im.vector.app.EmojiCompatFontProvider
|
||||||
import im.vector.app.features.autocomplete.AutocompleteClickListener
|
import im.vector.app.features.autocomplete.AutocompleteClickListener
|
||||||
import im.vector.app.features.autocomplete.autocompleteHeaderItem
|
import im.vector.app.features.autocomplete.autocompleteHeaderItem
|
||||||
import im.vector.app.features.autocomplete.member.AutocompleteEmojiDataItem
|
import im.vector.app.features.autocomplete.member.AutocompleteEmojiDataItem
|
||||||
import im.vector.app.features.autocomplete.member.AutocompleteMemberItem
|
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.reactions.data.EmojiItem
|
import im.vector.app.features.reactions.data.EmojiItem
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
@ -49,16 +48,18 @@ class AutocompleteEmojiController @Inject constructor(
|
||||||
if (data.isNullOrEmpty()) {
|
if (data.isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val max = listener?.maxShowSizeOverride() ?: MAX
|
||||||
data
|
data
|
||||||
.take(MAX)
|
.take(max)
|
||||||
.forEach { item ->
|
.forEach { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is AutocompleteEmojiDataItem.Header -> buildHeaderItem(item)
|
is AutocompleteEmojiDataItem.Header -> buildHeaderItem(item)
|
||||||
is AutocompleteEmojiDataItem.Emoji -> buildEmojiItem(item.emojiItem)
|
is AutocompleteEmojiDataItem.Emoji -> buildEmojiItem(item.emojiItem)
|
||||||
|
is AutocompleteEmojiDataItem.Expand -> buildExpandItem(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.size > MAX) {
|
if (data.size > max) {
|
||||||
autocompleteMoreResultItem {
|
autocompleteMoreResultItem {
|
||||||
id("more_result")
|
id("more_result")
|
||||||
}
|
}
|
||||||
|
@ -89,6 +90,15 @@ class AutocompleteEmojiController @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildExpandItem(item: AutocompleteEmojiDataItem.Expand) {
|
||||||
|
val host = this
|
||||||
|
autocompleteExpandItem {
|
||||||
|
id(item.loadMoreKey + "/" + item.loadMoreKeySecondary)
|
||||||
|
count(item.count)
|
||||||
|
onClickListener { host.listener?.onLoadMoreClick(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||||
fontProvider.addListener(fontProviderListener)
|
fontProvider.addListener(fontProviderListener)
|
||||||
|
@ -103,12 +113,16 @@ class AutocompleteEmojiController @Inject constructor(
|
||||||
// Count of emojis for the current room's image pack
|
// Count of emojis for the current room's image pack
|
||||||
const val CUSTOM_THIS_ROOM_MAX = 10
|
const val CUSTOM_THIS_ROOM_MAX = 10
|
||||||
// Count of emojis per other image pack
|
// Count of emojis per other image pack
|
||||||
const val CUSTOM_OTHER_ROOM_MAX = 3
|
const val CUSTOM_OTHER_ROOM_MAX = 5
|
||||||
// Count of emojis for global account data
|
// Count of emojis for global account data
|
||||||
const val CUSTOM_ACCOUNT_MAX = 5
|
const val CUSTOM_ACCOUNT_MAX = 5
|
||||||
// Count of other image packs
|
// Count of other image packs
|
||||||
const val MAX_CUSTOM_OTHER_ROOMS = 3
|
const val MAX_CUSTOM_OTHER_ROOMS = 15
|
||||||
// Total max
|
// Total max
|
||||||
const val MAX = 50
|
const val MAX = 50
|
||||||
|
// Total max after expanding a section
|
||||||
|
const val MAX_EXPAND = 10000
|
||||||
|
// Internal ID
|
||||||
|
const val ACCOUNT_DATA_EMOTE_ID = "de.spiritcroc.riotx.ACCOUNT_DATA_EMOTES"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.autocomplete.emoji
|
package im.vector.app.features.autocomplete.emoji
|
||||||
|
|
||||||
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -30,6 +31,7 @@ import im.vector.app.core.epoxy.onClick
|
||||||
import im.vector.app.core.extensions.setTextOrHide
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
import im.vector.app.core.glide.GlideApp
|
import im.vector.app.core.glide.GlideApp
|
||||||
import im.vector.app.features.reactions.data.EmojiItem
|
import im.vector.app.features.reactions.data.EmojiItem
|
||||||
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
|
||||||
@EpoxyModelClass
|
@EpoxyModelClass
|
||||||
|
@ -52,6 +54,7 @@ abstract class AutocompleteEmojiItem : VectorEpoxyModel<AutocompleteEmojiItem.Ho
|
||||||
if (emoteUrl?.isNotEmpty().orFalse()) {
|
if (emoteUrl?.isNotEmpty().orFalse()) {
|
||||||
holder.emojiText.isVisible = false
|
holder.emojiText.isVisible = false
|
||||||
holder.emoteImage.isVisible = true
|
holder.emoteImage.isVisible = true
|
||||||
|
holder.emoteImage.imageTintList = null
|
||||||
GlideApp.with(holder.emoteImage)
|
GlideApp.with(holder.emoteImage)
|
||||||
.load(emoteUrl)
|
.load(emoteUrl)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
|
|
|
@ -59,6 +59,9 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
|
|
||||||
|
private val expandedSections = HashSet<Pair<String, String?>>()
|
||||||
|
private var lastQuery: CharSequence? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
controller.listener = this
|
controller.listener = this
|
||||||
}
|
}
|
||||||
|
@ -66,6 +69,7 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
|
||||||
fun clear() {
|
fun clear() {
|
||||||
coroutineScope.coroutineContext.cancelChildren()
|
coroutineScope.coroutineContext.cancelChildren()
|
||||||
controller.listener = null
|
controller.listener = null
|
||||||
|
expandedSections.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
@ -81,7 +85,24 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
|
||||||
dispatchClick(t)
|
dispatchClick(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLoadMoreClick(item: AutocompleteEmojiDataItem.Expand) {
|
||||||
|
expandedSections.add(Pair(item.loadMoreKey, item.loadMoreKeySecondary))
|
||||||
|
//Timber.d("Load more emojis for ${item.loadMoreKey}/${item.loadMoreKeySecondary} ${expandedSections.contains(Pair(item.loadMoreKey, item.loadMoreKeySecondary))}")
|
||||||
|
onQuery(lastQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun maxShowSizeOverride(): Int? {
|
||||||
|
if (expandedSections.isNotEmpty()) {
|
||||||
|
return AutocompleteEmojiController.MAX_EXPAND
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onQuery(query: CharSequence?) {
|
override fun onQuery(query: CharSequence?) {
|
||||||
|
if (query?.isNotEmpty() != true && lastQuery?.isEmpty() != true) {
|
||||||
|
expandedSections.clear()
|
||||||
|
}
|
||||||
|
lastQuery = query
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
// Plain emojis
|
// Plain emojis
|
||||||
val data = if (query.isNullOrBlank()) {
|
val data = if (query.isNullOrBlank()) {
|
||||||
|
@ -93,30 +114,37 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
// Custom emotes: This room's emotes
|
// Custom emotes: This room's emotes
|
||||||
val currentRoomEmotes = room.getAllEmojiItems(query)
|
val currentRoomEmotes = room.getAllEmojiItems(query)
|
||||||
val emoteData = currentRoomEmotes.toAutocompleteItems().let {
|
val allEmoteData = currentRoomEmotes.toAutocompleteItems().let {
|
||||||
if (it.isNotEmpty()) {
|
if (it.isNotEmpty()) {
|
||||||
listOf(AutocompleteEmojiDataItem.Header(roomId, context.getString(R.string.custom_emotes_this_room))) + it
|
listOf(AutocompleteEmojiDataItem.Header(roomId, context.getString(R.string.custom_emotes_this_room))) + it
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}.limit(AutocompleteEmojiController.CUSTOM_THIS_ROOM_MAX).toMutableList()
|
}
|
||||||
|
val emoteData = allEmoteData.maybeLimit(AutocompleteEmojiController.CUSTOM_THIS_ROOM_MAX, roomId, null).toMutableList()
|
||||||
val emoteUrls = HashSet<String>()
|
val emoteUrls = HashSet<String>()
|
||||||
emoteUrls.addAll(currentRoomEmotes.map { it.mxcUrl })
|
emoteUrls.addAll(currentRoomEmotes.map { it.mxcUrl })
|
||||||
|
if (allEmoteData.size > emoteData.size) {
|
||||||
|
emoteData += listOf(AutocompleteEmojiDataItem.Expand(roomId, null, allEmoteData.size - emoteData.size))
|
||||||
|
}
|
||||||
// Global emotes (only while searching)
|
// Global emotes (only while searching)
|
||||||
if (!query.isNullOrBlank()) {
|
if (!query.isNullOrBlank()) {
|
||||||
// Account emotes
|
// Account emotes
|
||||||
val userPack = session.accountDataService().getUserAccountDataEvent(UserAccountDataTypes.TYPE_USER_EMOTES)?.content
|
val allUserPack = session.accountDataService().getUserAccountDataEvent(UserAccountDataTypes.TYPE_USER_EMOTES)?.content
|
||||||
?.toModel<RoomEmoteContent>().getEmojiItems(query)
|
?.toModel<RoomEmoteContent>().getEmojiItems(query)
|
||||||
.limit(AutocompleteEmojiController.CUSTOM_ACCOUNT_MAX)
|
val userPack = allUserPack.maybeLimit(AutocompleteEmojiController.CUSTOM_ACCOUNT_MAX, AutocompleteEmojiController.ACCOUNT_DATA_EMOTE_ID, null)
|
||||||
if (userPack.isNotEmpty()) {
|
if (userPack.isNotEmpty()) {
|
||||||
emoteUrls.addAll(userPack.map { it.mxcUrl })
|
emoteUrls.addAll(userPack.map { it.mxcUrl })
|
||||||
emoteData += listOf(
|
emoteData += listOf(
|
||||||
AutocompleteEmojiDataItem.Header(
|
AutocompleteEmojiDataItem.Header(
|
||||||
"de.spiritcroc.riotx.ACCOUNT_EMOJI_HEADER",
|
AutocompleteEmojiController.ACCOUNT_DATA_EMOTE_ID,
|
||||||
context.getString(R.string.custom_emotes_account_data)
|
context.getString(R.string.custom_emotes_account_data)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
emoteData += userPack.toAutocompleteItems()
|
emoteData += userPack.toAutocompleteItems()
|
||||||
|
if (allUserPack.size > userPack.size) {
|
||||||
|
emoteData += listOf(AutocompleteEmojiDataItem.Expand(AutocompleteEmojiController.ACCOUNT_DATA_EMOTE_ID, null, allUserPack.size - userPack.size))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Global emotes from rooms
|
// Global emotes from rooms
|
||||||
val globalPacks = session.accountDataService().getUserAccountDataEvent(UserAccountDataTypes.TYPE_EMOTE_ROOMS)
|
val globalPacks = session.accountDataService().getUserAccountDataEvent(UserAccountDataTypes.TYPE_EMOTE_ROOMS)
|
||||||
|
@ -138,9 +166,10 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
|
||||||
val emojiItems = packRoom.getEmojiItems(query, QueryStringValue.Equals(packId))
|
val emojiItems = packRoom.getEmojiItems(query, QueryStringValue.Equals(packId))
|
||||||
val packName = emojiItems.first
|
val packName = emojiItems.first
|
||||||
// Filter out duplicate emotes with the exact same mxc url
|
// Filter out duplicate emotes with the exact same mxc url
|
||||||
val packImages = emojiItems.second.filter {
|
val allPackImages = emojiItems.second.filter {
|
||||||
it.mxcUrl !in emoteUrls
|
it.mxcUrl !in emoteUrls
|
||||||
}.limit(AutocompleteEmojiController.CUSTOM_OTHER_ROOM_MAX)
|
}
|
||||||
|
val packImages = allPackImages.maybeLimit(AutocompleteEmojiController.CUSTOM_OTHER_ROOM_MAX, packRoomId, packId)
|
||||||
// Add header + emotes
|
// Add header + emotes
|
||||||
if (packImages.isNotEmpty()) {
|
if (packImages.isNotEmpty()) {
|
||||||
packsAdded++
|
packsAdded++
|
||||||
|
@ -165,6 +194,9 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
emoteData += packImages.toAutocompleteItems()
|
emoteData += packImages.toAutocompleteItems()
|
||||||
|
if (allPackImages.size > packImages.size) {
|
||||||
|
emoteData += listOf(AutocompleteEmojiDataItem.Expand(packRoomId, packId, allPackImages.size - packImages.size))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,6 +212,19 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't limit if only one more would be required, such that showing a "load more" button would be a waste
|
||||||
|
*/
|
||||||
|
private fun <T>List<T>.maybeLimit(limit: Int, loadMoreKey: String, loadMoreKeySecondary: String?): List<T> {
|
||||||
|
return if (size > limit + 1 && !expandedSections.contains(Pair(loadMoreKey, loadMoreKeySecondary))) {
|
||||||
|
//Timber.d("maybeLimit $loadMoreKey/$loadMoreKeySecondary true ${expandedSections.contains(Pair(loadMoreKey, loadMoreKeySecondary))}")
|
||||||
|
limit(limit)
|
||||||
|
} else {
|
||||||
|
//Timber.d("maybeLimit $loadMoreKey/$loadMoreKeySecondary false")
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun List<EmojiItem>.toAutocompleteItems(): List<AutocompleteEmojiDataItem> {
|
private fun List<EmojiItem>.toAutocompleteItems(): List<AutocompleteEmojiDataItem> {
|
||||||
return map { AutocompleteEmojiDataItem.Emoji(it) }
|
return map { AutocompleteEmojiDataItem.Emoji(it) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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.app.features.autocomplete.emoji
|
||||||
|
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.epoxy.ClickListener
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.app.core.epoxy.onClick
|
||||||
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
|
|
||||||
|
@EpoxyModelClass // Re-using item_autocomplete_emoji layout for now because I'm lazy - may want to change that if it causes troubles
|
||||||
|
abstract class AutocompleteExpandItem : VectorEpoxyModel<AutocompleteEmojiItem.Holder>(R.layout.item_autocomplete_emoji) {
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var count: Int? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||||
|
var onClickListener: ClickListener? = null
|
||||||
|
|
||||||
|
override fun bind(holder: AutocompleteEmojiItem.Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.emojiText.isVisible = false
|
||||||
|
holder.emoteImage.isVisible = true
|
||||||
|
holder.emoteImage.setImageResource(R.drawable.ic_expand_more)
|
||||||
|
holder.emoteImage.imageTintList = ColorStateList.valueOf(ThemeUtils.getColor(holder.emoteImage.context, R.attr.vctr_content_secondary))
|
||||||
|
holder.emojiText.typeface = Typeface.DEFAULT
|
||||||
|
count.let {
|
||||||
|
if (it == null) {
|
||||||
|
holder.emojiNameText.setText(R.string.room_profile_section_more)
|
||||||
|
} else {
|
||||||
|
holder.emojiNameText.text = holder.emojiNameText.resources.getQuantityString(R.plurals.message_reaction_show_more, it, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.emojiKeywordText.isVisible = false
|
||||||
|
holder.view.onClick(onClickListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val emojiText by bind<TextView>(R.id.itemAutocompleteEmoji)
|
||||||
|
val emoteImage by bind<ImageView>(R.id.itemAutocompleteEmote)
|
||||||
|
val emojiNameText by bind<TextView>(R.id.itemAutocompleteEmojiName)
|
||||||
|
val emojiKeywordText by bind<TextView>(R.id.itemAutocompleteEmojiSubname)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
|
@ -21,4 +21,5 @@ import im.vector.app.features.reactions.data.EmojiItem
|
||||||
sealed class AutocompleteEmojiDataItem {
|
sealed class AutocompleteEmojiDataItem {
|
||||||
data class Header(val id: String, val title: String) : AutocompleteEmojiDataItem()
|
data class Header(val id: String, val title: String) : AutocompleteEmojiDataItem()
|
||||||
data class Emoji(val emojiItem: EmojiItem) : AutocompleteEmojiDataItem()
|
data class Emoji(val emojiItem: EmojiItem) : AutocompleteEmojiDataItem()
|
||||||
|
data class Expand(val loadMoreKey: String, val loadMoreKeySecondary: String?, val count: Int?) : AutocompleteEmojiDataItem()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue