Support MSC2545 global room image packs for custom emojis

Change-Id: Ic2e2961e5a75b098c2d1ad46f9bf0f36eef85b2e
This commit is contained in:
SpiritCroc 2022-06-21 18:36:13 +02:00
parent 07e4581021
commit e47183de8b
5 changed files with 141 additions and 28 deletions

View File

@ -28,4 +28,5 @@ object UserAccountDataTypes {
const val TYPE_IDENTITY_SERVER = "m.identity_server" const val TYPE_IDENTITY_SERVER = "m.identity_server"
const val TYPE_ACCEPTED_TERMS = "m.accepted_terms" const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
const val TYPE_OVERRIDE_COLORS = "im.vector.setting.override_colors" const val TYPE_OVERRIDE_COLORS = "im.vector.setting.override_colors"
const val TYPE_EMOTE_ROOMS = "im.ponies.emote_rooms"
} }

View File

@ -21,6 +21,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.EmojiCompatFontProvider 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.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
@ -30,7 +33,7 @@ import javax.inject.Inject
class AutocompleteEmojiController @Inject constructor( class AutocompleteEmojiController @Inject constructor(
private val fontProvider: EmojiCompatFontProvider, private val fontProvider: EmojiCompatFontProvider,
private val session: Session private val session: Session
) : TypedEpoxyController<List<EmojiItem>>() { ) : TypedEpoxyController<List<AutocompleteEmojiDataItem>>() {
var emojiTypeface: Typeface? = fontProvider.typeface var emojiTypeface: Typeface? = fontProvider.typeface
@ -42,22 +45,16 @@ class AutocompleteEmojiController @Inject constructor(
var listener: AutocompleteClickListener<EmojiItem>? = null var listener: AutocompleteClickListener<EmojiItem>? = null
override fun buildModels(data: List<EmojiItem>?) { override fun buildModels(data: List<AutocompleteEmojiDataItem>?) {
if (data.isNullOrEmpty()) { if (data.isNullOrEmpty()) {
return return
} }
val host = this
data data
.take(MAX) .take(MAX)
.forEach { emojiItem -> .forEach { item ->
autocompleteEmojiItem { when (item) {
id(emojiItem.name) is AutocompleteEmojiDataItem.Header -> buildHeaderItem(item)
emojiItem(emojiItem) is AutocompleteEmojiDataItem.Emoji -> buildEmojiItem(item.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) }
} }
} }
@ -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) { override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
fontProvider.addListener(fontProviderListener) fontProvider.addListener(fontProviderListener)
} }
@ -78,6 +100,13 @@ class AutocompleteEmojiController @Inject constructor(
} }
companion object { 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 const val MAX = 50
} }
} }

View File

@ -21,8 +21,10 @@ import androidx.recyclerview.widget.RecyclerView
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.AutocompleteClickListener
import im.vector.app.features.autocomplete.RecyclerViewPresenter 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.EmojiDataSource
import im.vector.app.features.reactions.data.EmojiItem import im.vector.app.features.reactions.data.EmojiItem
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
@ -32,11 +34,14 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session 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.EventType
import org.matrix.android.sdk.api.session.events.model.toModel 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.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.getStateEvent
import org.matrix.android.sdk.api.session.room.model.RoomEmoteContent import org.matrix.android.sdk.api.session.room.model.RoomEmoteContent
import kotlin.math.min
class AutocompleteEmojiPresenter @AssistedInject constructor(context: Context, class AutocompleteEmojiPresenter @AssistedInject constructor(context: Context,
@Assisted val roomId: String, @Assisted val roomId: String,
@ -80,24 +85,75 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(context: Context,
emojiDataSource.getQuickReactions() emojiDataSource.getQuickReactions()
} else { } else {
emojiDataSource.filterWith(query.toString()) 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<String>()
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 val dataHeader = if (data.isNotEmpty() && emoteData.isNotEmpty()) {
// TODO may want to add headers (compare @room vs @person completion) for listOf(AutocompleteEmojiDataItem.Header("de.spiritcroc.riotx.STANDARD_EMOJI_HEADER", context.getString(R.string.standard_emojis)))
// - Standard emojis } else {
// - Room-specific emotes emptyList()
// - Global emotes (exported from other rooms) }
val images = room.getStateEvent(EventType.ROOM_EMOTES)?.content?.toModel<RoomEmoteContent>()?.images.orEmpty() controller.setData(emoteData + dataHeader + data)
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)
} }
} }
private fun List<EmojiItem>.toAutocompleteItems(): List<AutocompleteEmojiDataItem> {
return map { AutocompleteEmojiDataItem.Emoji(it) }
}
private fun Room.getEmojiItems(query: CharSequence?): List<EmojiItem> {
return getStateEvent(EventType.ROOM_EMOTES)?.content?.toModel<RoomEmoteContent>()?.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 <T>List<T>.limit(count: Int): List<T> {
return subList(0, min(count, size))
}
} }

View File

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

View File

@ -198,6 +198,9 @@
<string name="freeform_react_with">React with \"%1$s\"</string> <string name="freeform_react_with">React with \"%1$s\"</string>
<string name="freeform_reaction_summary">Free-form reaction</string> <string name="freeform_reaction_summary">Free-form reaction</string>
<string name="custom_emotes_this_room">Room emojis</string>
<string name="custom_emotes_other_room">Emojis from %1$s</string>
<string name="standard_emojis">Standard emojis</string>
<string name="dev_tools_menu_hidden_events">Hidden events</string> <string name="dev_tools_menu_hidden_events">Hidden events</string>
<string name="dev_tools_menu_event_visibilities">Event visibilities</string> <string name="dev_tools_menu_event_visibilities">Event visibilities</string>