Support MSC2545 global room image packs for custom emojis
Change-Id: Ic2e2961e5a75b098c2d1ad46f9bf0f36eef85b2e
This commit is contained in:
parent
07e4581021
commit
e47183de8b
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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<List<EmojiItem>>() {
|
||||
) : TypedEpoxyController<List<AutocompleteEmojiDataItem>>() {
|
||||
|
||||
var emojiTypeface: Typeface? = fontProvider.typeface
|
||||
|
||||
|
@ -42,22 +45,16 @@ class AutocompleteEmojiController @Inject constructor(
|
|||
|
||||
var listener: AutocompleteClickListener<EmojiItem>? = null
|
||||
|
||||
override fun buildModels(data: List<EmojiItem>?) {
|
||||
override fun buildModels(data: List<AutocompleteEmojiDataItem>?) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
// 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<RoomEmoteContent>()?.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<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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -198,6 +198,9 @@
|
|||
|
||||
<string name="freeform_react_with">React with \"%1$s\"</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_event_visibilities">Event visibilities</string>
|
||||
|
|
Loading…
Reference in New Issue