diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt index 91167d896f..c28bdd5420 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt @@ -28,4 +28,5 @@ object UserAccountDataTypes { const val TYPE_IDENTITY_SERVER = "m.identity_server" const val TYPE_ACCEPTED_TERMS = "m.accepted_terms" const val TYPE_OVERRIDE_COLORS = "im.vector.setting.override_colors" + const val TYPE_EMOTE_ROOMS = "im.ponies.emote_rooms" } 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 b9e6e39560..ca20436675 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 @@ -21,6 +21,9 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.TypedEpoxyController 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 @@ -30,7 +33,7 @@ import javax.inject.Inject class AutocompleteEmojiController @Inject constructor( private val fontProvider: EmojiCompatFontProvider, private val session: Session -) : TypedEpoxyController>() { +) : TypedEpoxyController>() { var emojiTypeface: Typeface? = fontProvider.typeface @@ -42,22 +45,16 @@ class AutocompleteEmojiController @Inject constructor( var listener: AutocompleteClickListener? = null - override fun buildModels(data: List?) { + override fun buildModels(data: List?) { if (data.isNullOrEmpty()) { return } - val host = this data .take(MAX) - .forEach { emojiItem -> - autocompleteEmojiItem { - id(emojiItem.name) - emojiItem(emojiItem) - // For caching reasons, we use the AvatarRenderer's thumbnail size here - emoteUrl(host.session.contentUrlResolver().resolveThumbnail(emojiItem.mxcUrl, - AvatarRenderer.THUMBNAIL_SIZE, AvatarRenderer.THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)) - emojiTypeFace(host.emojiTypeface) - onClickListener { host.listener?.onItemClick(emojiItem) } + .forEach { item -> + when (item) { + is AutocompleteEmojiDataItem.Header -> buildHeaderItem(item) + is AutocompleteEmojiDataItem.Emoji -> buildEmojiItem(item.emojiItem) } } @@ -68,6 +65,31 @@ class AutocompleteEmojiController @Inject constructor( } } + private fun buildHeaderItem(header: AutocompleteEmojiDataItem.Header) { + autocompleteHeaderItem { + id(header.id) + title(header.title) + } + } + + private fun buildEmojiItem(emojiItem: EmojiItem) { + val host = this + autocompleteEmojiItem { + id(emojiItem.name) + emojiItem(emojiItem) + // For caching reasons, we use the AvatarRenderer's thumbnail size here + emoteUrl( + host.session.contentUrlResolver().resolveThumbnail( + emojiItem.mxcUrl, + AvatarRenderer.THUMBNAIL_SIZE, AvatarRenderer.THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE + ) + ) + emojiTypeFace(host.emojiTypeface) + onClickListener { host.listener?.onItemClick(emojiItem) } + } + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { fontProvider.addListener(fontProviderListener) } @@ -78,6 +100,13 @@ class AutocompleteEmojiController @Inject constructor( } companion object { + // 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 + // Count of other image packs + const val MAX_CUSTOM_OTHER_ROOMS = 3 + // Total max const val MAX = 50 } } 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 68c1db3bfb..7f2aac1047 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 @@ -21,8 +21,10 @@ import androidx.recyclerview.widget.RecyclerView import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.R import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter +import im.vector.app.features.autocomplete.member.AutocompleteEmojiDataItem import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.reactions.data.EmojiItem import im.vector.app.features.settings.VectorPreferences @@ -32,11 +34,14 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.model.RoomEmoteContent +import kotlin.math.min class AutocompleteEmojiPresenter @AssistedInject constructor(context: Context, @Assisted val roomId: String, @@ -80,24 +85,75 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(context: Context, emojiDataSource.getQuickReactions() } else { emojiDataSource.filterWith(query.toString()) + }.toAutocompleteItems() + + // Custom emotes: This room's emotes + val currentRoomEmotes = room.getEmojiItems(query) + val emoteData = 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 emoteUrls = HashSet() + emoteUrls.addAll(currentRoomEmotes.map { it.mxcUrl }) + // Global emotes (only while searching) + if (!query.isNullOrBlank()) { + val globalPacks = session.accountDataService().getUserAccountDataEvent(UserAccountDataTypes.TYPE_EMOTE_ROOMS) + var packsAdded = 0 + (globalPacks?.content?.get("rooms") as? Map<*, *>)?.forEach { pack -> + if (packsAdded >= AutocompleteEmojiController.MAX_CUSTOM_OTHER_ROOMS) { + return@forEach + } + val packRoomId = pack.key + if (packRoomId is String && packRoomId != roomId) { + val packRoom = session.getRoom(packRoomId) ?: return@forEach + // Filter out duplicate emotes with the exact same mxc url + val packImages = packRoom.getEmojiItems(query).filter { + it.mxcUrl !in emoteUrls + }.limit(AutocompleteEmojiController.CUSTOM_OTHER_ROOM_MAX) + // Add header + emotes + if (packImages.isNotEmpty()) { + packsAdded++ + emoteUrls.addAll(packImages.map { it.mxcUrl }) + emoteData += listOf(AutocompleteEmojiDataItem.Header( + packRoomId, + context.getString(R.string.custom_emotes_other_room, + packRoom.roomSummary()?.displayName ?: packRoomId) + )) + emoteData += packImages.toAutocompleteItems() + } + } + } } - // Custom emotes - // TODO may want to add headers (compare @room vs @person completion) for - // - Standard emojis - // - Room-specific emotes - // - Global emotes (exported from other rooms) - val images = room.getStateEvent(EventType.ROOM_EMOTES)?.content?.toModel()?.images.orEmpty() - val emoteData = images.filter { - val usages = it.value.usage - usages.isNullOrEmpty() || RoomEmoteContent.USAGE_EMOTICON in usages - }.filter { - query == null || it.key.contains(query, true) - }.map { - EmojiItem(it.key, "", mxcUrl = it.value.url) - }.sortedBy { it.name }.distinct() - - controller.setData(emoteData + data) + val dataHeader = if (data.isNotEmpty() && emoteData.isNotEmpty()) { + listOf(AutocompleteEmojiDataItem.Header("de.spiritcroc.riotx.STANDARD_EMOJI_HEADER", context.getString(R.string.standard_emojis))) + } else { + emptyList() + } + controller.setData(emoteData + dataHeader + data) } } + + private fun List.toAutocompleteItems(): List { + return map { AutocompleteEmojiDataItem.Emoji(it) } + } + + private fun Room.getEmojiItems(query: CharSequence?): List { + return getStateEvent(EventType.ROOM_EMOTES)?.content?.toModel()?.images.orEmpty() + .filter { + val usages = it.value.usage + usages.isNullOrEmpty() || RoomEmoteContent.USAGE_EMOTICON in usages + }.filter { + query == null || it.key.contains(query, true) + }.map { + EmojiItem(it.key, "", mxcUrl = it.value.url) + }.sortedBy { it.name }.distinctBy { it.mxcUrl } + } + + private fun List.limit(count: Int): List { + return subList(0, min(count, size)) + } } 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 new file mode 100644 index 0000000000..496ac0e903 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteEmojiDataItem.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 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.member + +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() +} diff --git a/vector/src/main/res/values/strings_sc.xml b/vector/src/main/res/values/strings_sc.xml index d75d18adec..f39028109d 100644 --- a/vector/src/main/res/values/strings_sc.xml +++ b/vector/src/main/res/values/strings_sc.xml @@ -198,6 +198,9 @@ React with \"%1$s\" Free-form reaction + Room emojis + Emojis from %1$s + Standard emojis Hidden events Event visibilities