Easier access to more custom emotes

- Expand button
- More emotes by default

Change-Id: Id18f0b36099465d83156fcee2d3b016f299402f4
This commit is contained in:
SpiritCroc 2023-03-25 10:19:38 +01:00
parent 1004bd19e3
commit 236c44a5a5
6 changed files with 146 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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