mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-07 04:14:44 +00:00
Add emoji reacts support (#889)
* feat: Add emoji reacts support * Remove message multi-selection * Add emoji reaction model * Add emoji reaction panel * Blur reacts panel background * Show emoji keyboard * Add emoji sprites * Update reaction proto * Emoji database updates * Emoji database refactor * Emoji reaction persistence * Optimize reactions retrieval * Fix emoji group query * Display emojis * Fix emoji persistence * Cleanup * Persistence refactor * Add reactions bottom sheet * Cleanup * Ui tweaks * React with any emoji * Show emoji react notifications * Remove reaction * Show reactions modal on long press * Click to react (+1) with an emoji * Click to react with an emoji * Enable emoji expand/collapse * fix: some compile issues from merge conflicts * fix: compile issues merging quote and media message UI * fix: xml IDs and adding in legacy is selected for future inclusion * Fix view constraints * Fix merge issue * Add message selection option in conversation context menu * Add sogs emoji integration * Handle sogs emoji reactions * Enable sending/deleting sogs emojis * fix: improve the visible message layout * fix: add file IDs to request parameters for message send (#940) * Fix open group polling from seqno instead of last hash (#939) * fix: reset seqno to get recent messages from open groups * build: upgrade build numbers * fix: actually run the migration * Using StringBuilder to construct request url * Fix reaction filter * fix: is_mms added in second projection query * Update default emojis * fix: include legacy and new open groups in server ID tracking (#941) * feat: add hidden moderator and admin roles, separated as they may be used independently in future (#942) * Cleanup * Fix view constraints * Add reactions capability check * Fix reactions alignment * Ui fixes * Display reactions list * feat: add formatted count strings * fix: account for negatives and add tests * Migrate old official open group locations for polling and adding (#932) * feat: adding in first part of open group migrations and tests for migration logic / helpers * feat: test code and migration logic for open groups in the case of no conflicts * feat: add in extra test cases and refactor code for migrator * refactor: migrate open group join URLs and references to server in adding new open groups to catch legacy and re-write it * refactor: joining open groups using OpenGroupUrlParser.kt now * fix: add in compile issues for renamed OpenGroupApi.kt from OpenGroupV2 * fix: prevent duplicates of http/https for new open group DNS and prevent adding new groups based on public key * fix: room and server swapped parameters * fix: replace default server for config messages * fix: actually using public key to de-dupe didn't work for rooms * build: bump version code and name * Display reactions list on open groups for moderators * Ui tweaks * Ui tweaks for moderation * Refactor * fix: compile issue * fix: de-duping joined queries in the get X from cursor * Restore import * fix: colouring the reaction overlay scrubber * fix: highlight colour, show reaction count if 1 or above * Cleanup * fix: light mode accent * fix: light / dark mode themeing in reactions dialog fragment * Emoji notification blinded id check * fix: show reaction list correctly and pass isUserModerator to bind methods * fix: remove unnecessary places for the moderator * fix: X button for removing own react not showing up properly * feat: add clear all header view * fix: migrate the clear all to the correct location * fix: use display instead of base * Truncate emoji sender ids * feat: add notify thread function in thread db * Notify threads on reaction received * fix: design fixes for the reaction list * fix: emoji reactions bottom sheet dialog UI designs * feat: add unsupported emoji reaction * fix: crash and doing vector properly * Fix reaction database queries * Fix background open group adder job * Show new open group reactions * Fetch a maximum of 5 reactors * Handle open group reactions polling conflicts * Add count to user reaction * Show number of additional reactors * fix: unreads set same as the unread query * fix: design changes * fix: update dependency to improve flexboxlayout behaviour, design consistencies * Add select message icon and update long press menu items order and wording * Fix crash on reactors dialog * fix: colours and backgrounds to match designs * fix: add header in recipient item * fix: margins * fix: alignments and layout issues for emoji reactions view * feat: add overflow previews and logic for overflow * Dim action bar * Add emoji search * Search index fix * Set count for 1:1 and closed group reactions when inserting in local database * Use on screen toolbar to allow overlaying * Show/hide scroll to bottom button * feat: add extended properties so it doesn't collapse on re-bind * Cleanup * feat: prevent keeping extended on rebinding if we get a new message ID * fix: long press works on devices now, fix release lint issue and crash for emoji search DBs from emoji builds * Display message timestamp * Fix modal items alignment * fix: sort order and emoji count in compareTo * Scale down really large messages to fit * Prevent closed group crash * Fix reaction author Co-authored-by: charles <charles@oxen.io> Co-authored-by: jubb <hjubb@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.emoji
|
||||
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StringRes
|
||||
import network.loki.messenger.R
|
||||
|
||||
/**
|
||||
* All the different Emoji categories the app is aware of in the order we want to display them.
|
||||
*/
|
||||
enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon: Int) {
|
||||
PEOPLE(0, "People", R.attr.emoji_category_people),
|
||||
NATURE(1, "Nature", R.attr.emoji_category_nature),
|
||||
FOODS(2, "Foods", R.attr.emoji_category_foods),
|
||||
ACTIVITY(3, "Activity", R.attr.emoji_category_activity),
|
||||
PLACES(4, "Places", R.attr.emoji_category_places),
|
||||
OBJECTS(5, "Objects", R.attr.emoji_category_objects),
|
||||
SYMBOLS(6, "Symbols", R.attr.emoji_category_symbol),
|
||||
FLAGS(7, "Flags", R.attr.emoji_category_flags),
|
||||
EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons);
|
||||
|
||||
@StringRes
|
||||
fun getCategoryLabel(): Int {
|
||||
return getCategoryLabel(icon)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun forKey(key: String) = values().first { it.key == key }
|
||||
|
||||
@JvmStatic
|
||||
@StringRes
|
||||
fun getCategoryLabel(@AttrRes iconAttr: Int): Int {
|
||||
return when (iconAttr) {
|
||||
R.attr.emoji_category_people -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people
|
||||
R.attr.emoji_category_nature -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature
|
||||
R.attr.emoji_category_foods -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food
|
||||
R.attr.emoji_category_activity -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities
|
||||
R.attr.emoji_category_places -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places
|
||||
R.attr.emoji_category_objects -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects
|
||||
R.attr.emoji_category_symbol -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols
|
||||
R.attr.emoji_category_flags -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags
|
||||
R.attr.emoji_category_emoticons -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package org.thoughtcrime.securesms.emoji
|
||||
|
||||
import android.net.Uri
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.thoughtcrime.securesms.components.emoji.CompositeEmojiPageModel
|
||||
import org.thoughtcrime.securesms.components.emoji.Emoji
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
|
||||
import org.thoughtcrime.securesms.components.emoji.StaticEmojiPageModel
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.Charset
|
||||
|
||||
typealias UriFactory = (sprite: String, format: String) -> Uri
|
||||
|
||||
/**
|
||||
* Takes an emoji_data.json file data and parses it into an EmojiSource
|
||||
*/
|
||||
object EmojiJsonParser {
|
||||
private val OBJECT_MAPPER = ObjectMapper()
|
||||
private const val ESTIMATED_EMOJI_COUNT = 3500
|
||||
|
||||
@JvmStatic
|
||||
fun verify(body: InputStream) {
|
||||
parse(body) { _, _ -> Uri.EMPTY }.getOrThrow()
|
||||
}
|
||||
|
||||
fun parse(body: InputStream, uriFactory: UriFactory): Result<ParsedEmojiData> {
|
||||
return try {
|
||||
Result.success(buildEmojiSourceFromNode(OBJECT_MAPPER.readTree(body), uriFactory))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEmojiSourceFromNode(node: JsonNode, uriFactory: UriFactory): ParsedEmojiData {
|
||||
val format: String = node["format"].textValue()
|
||||
val obsolete: List<ObsoleteEmoji> = node["obsolete"].toObseleteList()
|
||||
val dataPages: List<EmojiPageModel> = getDataPages(format, node["emoji"], uriFactory)
|
||||
val jumboPages: Map<String, String> = getJumboPages(node["jumbomoji"])
|
||||
val displayPages: List<EmojiPageModel> = mergeToDisplayPages(dataPages)
|
||||
val metrics: EmojiMetrics = node["metrics"].toEmojiMetrics()
|
||||
val densities: List<String> = node["densities"].toDensityList()
|
||||
|
||||
return ParsedEmojiData(metrics, densities, format, displayPages, dataPages, jumboPages, obsolete)
|
||||
}
|
||||
|
||||
private fun getDataPages(format: String, emoji: JsonNode, uriFactory: UriFactory): List<EmojiPageModel> {
|
||||
return emoji.fields()
|
||||
.asSequence()
|
||||
.sortedWith { lhs, rhs ->
|
||||
val lhsCategory = EmojiCategory.forKey(lhs.key.asCategoryKey())
|
||||
val rhsCategory = EmojiCategory.forKey(rhs.key.asCategoryKey())
|
||||
val comp = lhsCategory.priority.compareTo(rhsCategory.priority)
|
||||
|
||||
if (comp == 0) {
|
||||
val lhsIndex = lhs.key.getPageIndex()
|
||||
val rhsIndex = rhs.key.getPageIndex()
|
||||
|
||||
lhsIndex.compareTo(rhsIndex)
|
||||
} else {
|
||||
comp
|
||||
}
|
||||
}
|
||||
.map { createPage(it.key, format, it.value, uriFactory) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun getJumboPages(jumbo: JsonNode?): Map<String, String> {
|
||||
if (jumbo != null) {
|
||||
return jumbo.fields()
|
||||
.asSequence()
|
||||
.map { (page: String, node: JsonNode) ->
|
||||
node.associate { it.textValue() to page }
|
||||
}
|
||||
.flatMap { it.entries }
|
||||
.associateTo(HashMap(ESTIMATED_EMOJI_COUNT)) { it.key to it.value }
|
||||
}
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
private fun createPage(pageName: String, format: String, page: JsonNode, uriFactory: UriFactory): EmojiPageModel {
|
||||
val category = EmojiCategory.forKey(pageName.asCategoryKey())
|
||||
val pageList = page.mapIndexed { i, data ->
|
||||
if (data.size() == 0) {
|
||||
throw IllegalStateException("Page index $pageName.$i had no data")
|
||||
} else {
|
||||
val variations: MutableList<String> = mutableListOf()
|
||||
val rawVariations: MutableList<String> = mutableListOf()
|
||||
data.forEach {
|
||||
variations += it.textValue().encodeAsUtf16()
|
||||
rawVariations += it.textValue()
|
||||
}
|
||||
|
||||
Emoji(variations, rawVariations)
|
||||
}
|
||||
}
|
||||
|
||||
return StaticEmojiPageModel(category, pageList, uriFactory(pageName, format))
|
||||
}
|
||||
|
||||
private fun mergeToDisplayPages(dataPages: List<EmojiPageModel>): List<EmojiPageModel> {
|
||||
return dataPages.groupBy { it.iconAttr }
|
||||
.map { (icon, pages) -> if (pages.size <= 1) pages.first() else CompositeEmojiPageModel(icon, pages) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun JsonNode?.toObseleteList(): List<ObsoleteEmoji> {
|
||||
return if (this == null) {
|
||||
listOf()
|
||||
} else {
|
||||
map { node ->
|
||||
ObsoleteEmoji(node["obsoleted"].textValue().encodeAsUtf16(), node["replace_with"].textValue().encodeAsUtf16())
|
||||
}.toList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun JsonNode.toEmojiMetrics(): EmojiMetrics {
|
||||
return EmojiMetrics(this["raw_width"].asInt(), this["raw_height"].asInt(), this["per_row"].asInt())
|
||||
}
|
||||
|
||||
private fun JsonNode.toDensityList(): List<String> {
|
||||
return map { it.textValue() }
|
||||
}
|
||||
|
||||
private fun String.encodeAsUtf16() = String(Hex.fromStringCondensed(this), Charset.forName("UTF-16"))
|
||||
private fun String.asCategoryKey() = replace("(_\\d+)*$".toRegex(), "")
|
||||
private fun String.getPageIndex() = "^.*_(\\d+)+$".toRegex().find(this)?.let { it.groupValues[1] }?.toInt() ?: throw IllegalStateException("No index.")
|
||||
|
||||
data class ParsedEmojiData(
|
||||
override val metrics: EmojiMetrics,
|
||||
override val densities: List<String>,
|
||||
override val format: String,
|
||||
override val displayPages: List<EmojiPageModel>,
|
||||
override val dataPages: List<EmojiPageModel>,
|
||||
override val jumboPages: Map<String, String>,
|
||||
override val obsolete: List<ObsoleteEmoji>
|
||||
) : EmojiData
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.emoji
|
||||
|
||||
import android.net.Uri
|
||||
import com.bumptech.glide.load.Key
|
||||
import java.security.MessageDigest
|
||||
|
||||
typealias EmojiPageFactory = (Uri) -> EmojiPage
|
||||
|
||||
sealed class EmojiPage(open val uri: Uri) : Key {
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update("EmojiPage".encodeToByteArray())
|
||||
messageDigest.update(uri.toString().encodeToByteArray())
|
||||
}
|
||||
|
||||
data class Asset(override val uri: Uri) : EmojiPage(uri)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.emoji
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.session.libsession.utilities.ListenableFutureTask
|
||||
import org.session.libsession.utilities.SoftHashMap
|
||||
import org.session.libsession.utilities.concurrent.SimpleTask
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
object EmojiPageCache {
|
||||
|
||||
private val TAG = Log.tag(EmojiPageCache::class.java)
|
||||
|
||||
private val cache: SoftHashMap<EmojiPageRequest, Bitmap> = SoftHashMap()
|
||||
private val tasks: HashMap<EmojiPageRequest, ListenableFutureTask<Bitmap>> = hashMapOf()
|
||||
|
||||
@MainThread
|
||||
fun load(context: Context, emojiPage: EmojiPage, inSampleSize: Int): LoadResult {
|
||||
val applicationContext = context.applicationContext
|
||||
val emojiPageRequest = EmojiPageRequest(emojiPage, inSampleSize)
|
||||
val bitmap: Bitmap? = cache[emojiPageRequest]
|
||||
val task: ListenableFutureTask<Bitmap>? = tasks[emojiPageRequest]
|
||||
|
||||
return when {
|
||||
bitmap != null -> LoadResult.Immediate(bitmap)
|
||||
task != null -> LoadResult.Async(task)
|
||||
else -> {
|
||||
val newTask = ListenableFutureTask<Bitmap> {
|
||||
try {
|
||||
Log.i(TAG, "Loading page $emojiPageRequest")
|
||||
loadInternal(applicationContext, emojiPageRequest)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
tasks[emojiPageRequest] = newTask
|
||||
|
||||
SimpleTask.run(newTask::run) {
|
||||
try {
|
||||
val newBitmap: Bitmap? = newTask.get()
|
||||
if (newBitmap == null) {
|
||||
Log.w(TAG, "Failed to load emoji bitmap for request $emojiPageRequest")
|
||||
} else {
|
||||
cache[emojiPageRequest] = newBitmap
|
||||
}
|
||||
} finally {
|
||||
tasks.remove(emojiPageRequest)
|
||||
}
|
||||
}
|
||||
|
||||
LoadResult.Async(newTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
cache.clear()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun loadInternal(context: Context, emojiPageRequest: EmojiPageRequest): Bitmap? {
|
||||
val inputStream: InputStream = when (emojiPageRequest.emojiPage) {
|
||||
is EmojiPage.Asset -> context.assets.open(emojiPageRequest.emojiPage.uri.toString().replace("file:///android_asset/", ""))
|
||||
}
|
||||
|
||||
val bitmapOptions = BitmapFactory.Options()
|
||||
bitmapOptions.inSampleSize = emojiPageRequest.inSampleSize
|
||||
|
||||
return inputStream.use { BitmapFactory.decodeStream(it, null, bitmapOptions) }
|
||||
}
|
||||
|
||||
private data class EmojiPageRequest(val emojiPage: EmojiPage, val inSampleSize: Int)
|
||||
|
||||
sealed class LoadResult {
|
||||
data class Immediate(val bitmap: Bitmap) : LoadResult()
|
||||
data class Async(val task: ListenableFutureTask<Bitmap>) : LoadResult()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.thoughtcrime.securesms.emoji
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.components.emoji.Emoji
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
|
||||
import org.thoughtcrime.securesms.components.emoji.StaticEmojiPageModel
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiTree
|
||||
import org.thoughtcrime.securesms.util.ScreenDensity
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* The entry point for the application to request Emoji data for custom emojis.
|
||||
*/
|
||||
class EmojiSource(
|
||||
val decodeScale: Float,
|
||||
private val emojiData: EmojiData,
|
||||
private val emojiPageFactory: EmojiPageFactory
|
||||
) : EmojiData by emojiData {
|
||||
|
||||
val variationsToCanonical: Map<String, String> by lazy {
|
||||
val map = mutableMapOf<String, String>()
|
||||
|
||||
for (page: EmojiPageModel in dataPages) {
|
||||
for (emoji: Emoji in page.displayEmoji) {
|
||||
for (variation: String in emoji.variations) {
|
||||
map[variation] = emoji.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
val canonicalToVariations: Map<String, List<String>> by lazy {
|
||||
val map = mutableMapOf<String, List<String>>()
|
||||
|
||||
for (page: EmojiPageModel in dataPages) {
|
||||
for (emoji: Emoji in page.displayEmoji) {
|
||||
map[emoji.value] = emoji.variations
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
val maxEmojiLength: Int by lazy {
|
||||
dataPages.map { it.emoji.map(String::length) }
|
||||
.flatten()
|
||||
.maxOrZero()
|
||||
}
|
||||
|
||||
val emojiTree: EmojiTree by lazy {
|
||||
val tree = EmojiTree()
|
||||
|
||||
dataPages
|
||||
.filter { it.spriteUri != null }
|
||||
.forEach { page ->
|
||||
val emojiPage = emojiPageFactory(page.spriteUri!!)
|
||||
|
||||
var overallIndex = 0
|
||||
page.displayEmoji.forEach { emoji: Emoji ->
|
||||
emoji.variations.forEachIndexed { variationIndex, variation ->
|
||||
val raw = emoji.getRawVariation(variationIndex)
|
||||
tree.add(variation, EmojiDrawInfo(emojiPage, overallIndex++, variation, raw, jumboPages[raw]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
obsolete.forEach {
|
||||
tree.add(it.obsolete, tree.getEmoji(it.replaceWith, 0, it.replaceWith.length))
|
||||
}
|
||||
|
||||
tree
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val emojiSource = AtomicReference<EmojiSource>()
|
||||
private val emojiLatch = CountDownLatch(1)
|
||||
|
||||
@JvmStatic
|
||||
val latest: EmojiSource
|
||||
get() {
|
||||
emojiLatch.await()
|
||||
return emojiSource.get()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun refresh() {
|
||||
emojiSource.set(getEmojiSource())
|
||||
emojiLatch.countDown()
|
||||
}
|
||||
|
||||
private fun getEmojiSource(): EmojiSource {
|
||||
return loadAssetBasedEmojis()
|
||||
}
|
||||
|
||||
private fun loadAssetBasedEmojis(): EmojiSource {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val emojiData: InputStream = ApplicationContext.getInstance(context).assets.open("emoji/emoji_data.json")
|
||||
|
||||
emojiData.use {
|
||||
val parsedData: ParsedEmojiData = EmojiJsonParser.parse(it, ::getAssetsUri).getOrThrow()
|
||||
return EmojiSource(
|
||||
ScreenDensity.xhdpiRelativeDensityScaleFactor("xhdpi"),
|
||||
parsedData.copy(
|
||||
displayPages = parsedData.displayPages + PAGE_EMOTICONS,
|
||||
dataPages = parsedData.dataPages + PAGE_EMOTICONS
|
||||
)
|
||||
) { uri: Uri -> EmojiPage.Asset(uri) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Int>.maxOrZero(): Int = maxOrNull() ?: 0
|
||||
|
||||
interface EmojiData {
|
||||
val metrics: EmojiMetrics
|
||||
val densities: List<String>
|
||||
val format: String
|
||||
val displayPages: List<EmojiPageModel>
|
||||
val dataPages: List<EmojiPageModel>
|
||||
val jumboPages: Map<String, String>
|
||||
val obsolete: List<ObsoleteEmoji>
|
||||
}
|
||||
|
||||
data class ObsoleteEmoji(val obsolete: String, val replaceWith: String)
|
||||
|
||||
data class EmojiMetrics(val rawHeight: Int, val rawWidth: Int, val perRow: Int)
|
||||
|
||||
private fun getAssetsUri(name: String, format: String): Uri = Uri.parse("file:///android_asset/emoji/$name.$format")
|
||||
|
||||
private val PAGE_EMOTICONS: EmojiPageModel = StaticEmojiPageModel(
|
||||
EmojiCategory.EMOTICONS,
|
||||
arrayOf(
|
||||
":-)", ";-)", "(-:", ":->", ":-D", "\\o/",
|
||||
":-P", "B-)", ":-$", ":-*", "O:-)", "=-O",
|
||||
"O_O", "O_o", "o_O", ":O", ":-!", ":-x",
|
||||
":-|", ":-\\", ":-(", ":'(", ":-[", ">:-(",
|
||||
"^.^", "^_^", "\\(\u02c6\u02da\u02c6)/",
|
||||
"\u30fd(\u00b0\u25c7\u00b0 )\u30ce", "\u00af\\(\u00b0_o)/\u00af",
|
||||
"\u00af\\_(\u30c4)_/\u00af", "(\u00ac_\u00ac)",
|
||||
"(>_<)", "(\u2565\ufe4f\u2565)", "(\u261e\uff9f\u30ee\uff9f)\u261e",
|
||||
"\u261c(\uff9f\u30ee\uff9f\u261c)", "\u261c(\u2312\u25bd\u2312)\u261e",
|
||||
"(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35", "\u253b\u2501\u253b",
|
||||
"\u252c\u2500\u252c", "\u30ce(\u00b0\u2013\u00b0\u30ce)",
|
||||
"(^._.^)\uff89", "\u0e05^\u2022\ufecc\u2022^\u0e05",
|
||||
"\u0295\u2022\u1d25\u2022\u0294", "(\u2022_\u2022)",
|
||||
" \u25a0-\u25a0\u00ac <(\u2022_\u2022) ", "(\u25a0_\u25a0\u00ac)",
|
||||
"\u01aa(\u0693\u05f2)\u200e\u01aa\u200b\u200b"
|
||||
),
|
||||
null
|
||||
)
|
||||
Reference in New Issue
Block a user