From 236c44a5a5ccb9048144e56aaeccab5fdf5c6fd6 Mon Sep 17 00:00:00 2001
From: SpiritCroc <dev@spiritcroc.de>
Date: Sat, 25 Mar 2023 10:19:38 +0100
Subject: [PATCH] Easier access to more custom emotes

- Expand button
- More emotes by default

Change-Id: Id18f0b36099465d83156fcee2d3b016f299402f4
---
 .../autocomplete/AutocompleteClickListener.kt |  6 ++
 .../emoji/AutocompleteEmojiController.kt      | 24 +++++--
 .../emoji/AutocompleteEmojiItem.kt            |  3 +
 .../emoji/AutocompleteEmojiPresenter.kt       | 59 +++++++++++++++--
 .../emoji/AutocompleteExpandItem.kt           | 65 +++++++++++++++++++
 .../member/AutocompleteEmojiDataItem.kt       |  1 +
 6 files changed, 146 insertions(+), 12 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteExpandItem.kt

diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteClickListener.kt b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteClickListener.kt
index 867269ae4a..18d5a81ef9 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteClickListener.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteClickListener.kt
@@ -16,10 +16,16 @@
 
 package im.vector.app.features.autocomplete
 
+import im.vector.app.features.autocomplete.member.AutocompleteEmojiDataItem
+
 /**
  * Simple generic listener interface.
  */
 interface AutocompleteClickListener<T> {
 
     fun onItemClick(t: T)
+
+    fun onLoadMoreClick(item: AutocompleteEmojiDataItem.Expand) {}
+
+    fun maxShowSizeOverride(): Int? = null
 }
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiController.kt
index 349de333bd..7f78a4816e 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiController.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiController.kt
@@ -23,7 +23,6 @@ import im.vector.app.EmojiCompatFontProvider
 import im.vector.app.features.autocomplete.AutocompleteClickListener
 import im.vector.app.features.autocomplete.autocompleteHeaderItem
 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.reactions.data.EmojiItem
 import org.matrix.android.sdk.api.session.Session
@@ -49,16 +48,18 @@ class AutocompleteEmojiController @Inject constructor(
         if (data.isNullOrEmpty()) {
             return
         }
+        val max = listener?.maxShowSizeOverride() ?: MAX
         data
-                .take(MAX)
+                .take(max)
                 .forEach { item ->
                     when (item) {
                         is AutocompleteEmojiDataItem.Header -> buildHeaderItem(item)
                         is AutocompleteEmojiDataItem.Emoji  -> buildEmojiItem(item.emojiItem)
+                        is AutocompleteEmojiDataItem.Expand -> buildExpandItem(item)
                     }
                 }
 
-        if (data.size > MAX) {
+        if (data.size > max) {
             autocompleteMoreResultItem {
                 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) {
         fontProvider.addListener(fontProviderListener)
@@ -103,12 +113,16 @@ class AutocompleteEmojiController @Inject constructor(
         // Count of emojis for the current room's image pack
         const val CUSTOM_THIS_ROOM_MAX = 10
         // 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
         const val CUSTOM_ACCOUNT_MAX = 5
         // Count of other image packs
-        const val MAX_CUSTOM_OTHER_ROOMS = 3
+        const val MAX_CUSTOM_OTHER_ROOMS = 15
         // Total max
         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"
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiItem.kt
index 036fbc2982..f55f27a9a7 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiItem.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiItem.kt
@@ -16,6 +16,7 @@
 
 package im.vector.app.features.autocomplete.emoji
 
+import android.content.res.ColorStateList
 import android.graphics.Typeface
 import android.widget.ImageView
 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.glide.GlideApp
 import im.vector.app.features.reactions.data.EmojiItem
+import im.vector.app.features.themes.ThemeUtils
 import org.matrix.android.sdk.api.extensions.orFalse
 
 @EpoxyModelClass
@@ -52,6 +54,7 @@ abstract class AutocompleteEmojiItem : VectorEpoxyModel<AutocompleteEmojiItem.Ho
         if (emoteUrl?.isNotEmpty().orFalse()) {
             holder.emojiText.isVisible = false
             holder.emoteImage.isVisible = true
+            holder.emoteImage.imageTintList = null
             GlideApp.with(holder.emoteImage)
                     .load(emoteUrl)
                     .centerCrop()
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
index 14a2c3126b..ac094a9c2c 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
@@ -59,6 +59,9 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
 
     private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
 
+    private val expandedSections = HashSet<Pair<String, String?>>()
+    private var lastQuery: CharSequence? = null
+
     init {
         controller.listener = this
     }
@@ -66,6 +69,7 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
     fun clear() {
         coroutineScope.coroutineContext.cancelChildren()
         controller.listener = null
+        expandedSections.clear()
     }
 
     @AssistedFactory
@@ -81,7 +85,24 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
         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?) {
+        if (query?.isNotEmpty() != true && lastQuery?.isEmpty() != true) {
+            expandedSections.clear()
+        }
+        lastQuery = query
         coroutineScope.launch {
             // Plain emojis
             val data = if (query.isNullOrBlank()) {
@@ -93,30 +114,37 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
 
             // Custom emotes: This room's emotes
             val currentRoomEmotes = room.getAllEmojiItems(query)
-            val emoteData = currentRoomEmotes.toAutocompleteItems().let {
+            val allEmoteData = currentRoomEmotes.toAutocompleteItems().let {
                 if (it.isNotEmpty()) {
                     listOf(AutocompleteEmojiDataItem.Header(roomId, context.getString(R.string.custom_emotes_this_room))) + it
                 } else {
                     emptyList()
                 }
-            }.limit(AutocompleteEmojiController.CUSTOM_THIS_ROOM_MAX).toMutableList()
+            }
+            val emoteData = allEmoteData.maybeLimit(AutocompleteEmojiController.CUSTOM_THIS_ROOM_MAX, roomId, null).toMutableList()
             val emoteUrls = HashSet<String>()
             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)
             if (!query.isNullOrBlank()) {
                 // 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)
-                        .limit(AutocompleteEmojiController.CUSTOM_ACCOUNT_MAX)
+                val userPack = allUserPack.maybeLimit(AutocompleteEmojiController.CUSTOM_ACCOUNT_MAX, AutocompleteEmojiController.ACCOUNT_DATA_EMOTE_ID, null)
                 if (userPack.isNotEmpty()) {
                     emoteUrls.addAll(userPack.map { it.mxcUrl })
                     emoteData += listOf(
                             AutocompleteEmojiDataItem.Header(
-                                    "de.spiritcroc.riotx.ACCOUNT_EMOJI_HEADER",
+                                    AutocompleteEmojiController.ACCOUNT_DATA_EMOTE_ID,
                                     context.getString(R.string.custom_emotes_account_data)
                             )
                     )
                     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
                 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 packName = emojiItems.first
                             // Filter out duplicate emotes with the exact same mxc url
-                            val packImages = emojiItems.second.filter {
+                            val allPackImages = emojiItems.second.filter {
                                 it.mxcUrl !in emoteUrls
-                            }.limit(AutocompleteEmojiController.CUSTOM_OTHER_ROOM_MAX)
+                            }
+                            val packImages = allPackImages.maybeLimit(AutocompleteEmojiController.CUSTOM_OTHER_ROOM_MAX, packRoomId, packId)
                             // Add header + emotes
                             if (packImages.isNotEmpty()) {
                                 packsAdded++
@@ -165,6 +194,9 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
                                         )
                                 )
                                 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> {
         return map { AutocompleteEmojiDataItem.Emoji(it) }
     }
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteExpandItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteExpandItem.kt
new file mode 100644
index 0000000000..085dc7eb59
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteExpandItem.kt
@@ -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)
+    }
+     */
+}
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteEmojiDataItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteEmojiDataItem.kt
index 496ac0e903..6d3668f07b 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteEmojiDataItem.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteEmojiDataItem.kt
@@ -21,4 +21,5 @@ import im.vector.app.features.reactions.data.EmojiItem
 sealed class AutocompleteEmojiDataItem {
     data class Header(val id: String, val title: String) : AutocompleteEmojiDataItem()
     data class Emoji(val emojiItem: EmojiItem) : AutocompleteEmojiDataItem()
+    data class Expand(val loadMoreKey: String, val loadMoreKeySecondary: String?, val count: Int?) : AutocompleteEmojiDataItem()
 }