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 { 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>() + 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() 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().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 List.maybeLimit(limit: Int, loadMoreKey: String, loadMoreKeySecondary: String?): List { + 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.toAutocompleteItems(): List { 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(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(R.id.itemAutocompleteEmoji) + val emoteImage by bind(R.id.itemAutocompleteEmote) + val emojiNameText by bind(R.id.itemAutocompleteEmojiName) + val emojiKeywordText by bind(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() }