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_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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_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>
|
||||||
|
|
Loading…
Reference in New Issue