2021-05-17 07:03:18 +02:00
|
|
|
package jp.juggler.subwaytooter.api.entity
|
|
|
|
|
|
|
|
import android.text.Spannable
|
|
|
|
import android.text.SpannableStringBuilder
|
|
|
|
import jp.juggler.subwaytooter.App1
|
2021-06-28 09:09:00 +02:00
|
|
|
import jp.juggler.subwaytooter.columnviewholder.ColumnViewHolder
|
2021-11-06 04:00:29 +01:00
|
|
|
import jp.juggler.subwaytooter.pref.PrefB
|
2021-05-17 07:03:18 +02:00
|
|
|
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
|
|
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
|
|
|
import jp.juggler.subwaytooter.util.DecodeOptions
|
|
|
|
import jp.juggler.subwaytooter.util.EmojiDecoder
|
2021-05-19 22:14:30 +02:00
|
|
|
import jp.juggler.util.*
|
2021-05-17 10:37:39 +02:00
|
|
|
import java.util.*
|
2021-05-17 07:03:18 +02:00
|
|
|
|
|
|
|
class TootReaction(
|
|
|
|
// (fedibird絵文字リアクション) unicode絵文字はunicodeそのまま、 カスタム絵文字はコロンなしのshortcode
|
|
|
|
// (Misskey)カスタム絵文字は前後にコロンがつく
|
|
|
|
val name: String,
|
|
|
|
|
|
|
|
// カスタム絵文字の場合は定義される
|
|
|
|
val url: String? = null,
|
2021-06-21 05:03:09 +02:00
|
|
|
val staticUrl: String? = null,
|
2021-05-17 07:03:18 +02:00
|
|
|
|
|
|
|
// (fedibird絵文字リアクション) 通知オブジェクト直下ではcountは常に0)
|
|
|
|
var count: Long = 0,
|
|
|
|
|
|
|
|
// (fedibird絵文字リアクション) 通知オブジェクト直下ではmeは常にfalse
|
|
|
|
// 告知のストリーミングイベントではmeは定義されない
|
|
|
|
var me: Boolean = false,
|
|
|
|
|
|
|
|
// 告知のストリーミングイベントでは告知IDが定義される
|
|
|
|
val announcement_id: EntityId? = null,
|
|
|
|
|
2021-05-18 16:35:54 +02:00
|
|
|
// (fedibird絵文字リアクション) userストリームのemoji_reactionイベントで設定される。
|
2021-05-18 16:44:12 +02:00
|
|
|
val status_id: EntityId? = null,
|
2021-05-27 07:54:04 +02:00
|
|
|
) {
|
2021-05-17 07:03:18 +02:00
|
|
|
companion object {
|
|
|
|
|
2021-10-27 22:58:19 +02:00
|
|
|
private fun appendDomain(name: String, domain: String?) =
|
2021-05-17 07:03:18 +02:00
|
|
|
if (domain?.isNotEmpty() == true) {
|
|
|
|
"$name@$domain"
|
|
|
|
} else {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fedibirdの投稿や通知に含まれる
|
|
|
|
fun parseFedibird(src: JsonObject) = TootReaction(
|
|
|
|
// (fedibird絵文字リアクション) リモートのカスタム絵文字の場合はdomainが指定される
|
|
|
|
name = appendDomain(src.string("name") ?: "?", src.string("domain")),
|
|
|
|
url = src.string("url"),
|
2021-06-21 05:03:09 +02:00
|
|
|
staticUrl = src.string("static_url"),
|
2021-05-17 07:03:18 +02:00
|
|
|
count = src.long("count") ?: 0,
|
|
|
|
me = src.boolean("me") ?: false,
|
|
|
|
announcement_id = EntityId.mayNull(src.string("announcement_id")),
|
2021-05-18 16:35:54 +02:00
|
|
|
status_id = EntityId.mayNull(src.string("status_id")),
|
2021-05-17 07:03:18 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Misskeyの通知にあるreaction文字列
|
|
|
|
fun parseMisskey(name: String?, count: Long = 0L) =
|
|
|
|
name?.let {
|
|
|
|
TootReaction(name = it, count = count)
|
|
|
|
}
|
|
|
|
|
|
|
|
private val misskeyOldReactions = mapOf(
|
|
|
|
"like" to "\ud83d\udc4d",
|
|
|
|
"love" to "\u2665",
|
|
|
|
"laugh" to "\ud83d\ude06",
|
|
|
|
"hmm" to "\ud83e\udd14",
|
|
|
|
"surprise" to "\ud83d\ude2e",
|
|
|
|
"congrats" to "\ud83c\udf89",
|
|
|
|
"angry" to "\ud83d\udca2",
|
|
|
|
"confused" to "\ud83d\ude25",
|
|
|
|
"rip" to "\ud83d\ude07",
|
|
|
|
"pudding" to "\ud83c\udf6e",
|
|
|
|
"star" to "\u2B50", // リモートからのFavを示す代替リアクション。ピッカーには表示しない
|
|
|
|
)
|
|
|
|
|
|
|
|
private val reCustomEmoji = """\A:([^:]+):\z""".toRegex()
|
|
|
|
|
|
|
|
fun getAnotherExpression(reaction: String): String? {
|
|
|
|
val customCode =
|
|
|
|
reCustomEmoji.find(reaction)?.groupValues?.elementAtOrNull(1) ?: return null
|
|
|
|
val cols = customCode.split("@")
|
|
|
|
val name = cols.elementAtOrNull(0)
|
|
|
|
val domain = cols.elementAtOrNull(1)
|
|
|
|
return if (domain == null) ":$name@.:" else if (domain == ".") ":$name:" else null
|
|
|
|
}
|
|
|
|
|
2021-05-17 10:37:39 +02:00
|
|
|
fun equalsName(a: String?, b: String?) = when {
|
2021-05-17 07:03:18 +02:00
|
|
|
a == null -> b == null
|
|
|
|
b == null -> false
|
|
|
|
else -> a == b || getAnotherExpression(a) == b || a == getAnotherExpression(b)
|
|
|
|
}
|
|
|
|
|
|
|
|
val UNKNOWN = TootReaction(name = "?")
|
2021-05-19 22:14:30 +02:00
|
|
|
|
2021-05-22 14:02:55 +02:00
|
|
|
private fun isUnicodeEmoji(code: String): Boolean =
|
2021-05-21 10:20:14 +02:00
|
|
|
code.any { it.code >= 0x7f }
|
2021-05-21 08:29:32 +02:00
|
|
|
|
2021-05-27 07:54:04 +02:00
|
|
|
fun isCustomEmoji(code: String): Boolean = !isUnicodeEmoji(code)
|
|
|
|
|
2021-05-21 10:20:14 +02:00
|
|
|
fun splitEmojiDomain(code: String): Pair<String?, String?> {
|
2021-05-21 08:29:32 +02:00
|
|
|
// unicode絵文字ならnull,nullを返す
|
2021-05-21 10:20:14 +02:00
|
|
|
if (isUnicodeEmoji(code)) return Pair(null, null)
|
2021-05-21 08:29:32 +02:00
|
|
|
// Misskeyカスタム絵文字リアクションのコロンを除去
|
2021-05-21 10:20:14 +02:00
|
|
|
val a = code.replace(":", "")
|
2021-05-21 08:29:32 +02:00
|
|
|
val idx = a.indexOf("@")
|
2021-05-21 10:20:14 +02:00
|
|
|
return if (idx == -1) Pair(a, null) else Pair(a.substring(0, idx), a.substring(idx + 1))
|
2021-05-19 22:14:30 +02:00
|
|
|
}
|
|
|
|
|
2021-05-21 08:29:32 +02:00
|
|
|
fun canReaction(
|
2021-06-20 15:12:25 +02:00
|
|
|
accessInfo: SavedAccount,
|
|
|
|
ti: TootInstance? = TootInstance.getCached(accessInfo),
|
|
|
|
) = InstanceCapability.emojiReaction(accessInfo, ti)
|
2021-05-21 10:20:14 +02:00
|
|
|
|
|
|
|
fun decodeEmojiQuery(jsonText: String?): List<TootReaction> =
|
|
|
|
jsonText.notEmpty()?.let { src ->
|
|
|
|
try {
|
|
|
|
src.decodeJsonArray().objectList().map { parseFedibird(it) }
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
ColumnViewHolder.log.e(ex, "updateReactionQueryView: decode failed.")
|
|
|
|
null
|
|
|
|
}
|
|
|
|
} ?: emptyList()
|
|
|
|
|
|
|
|
fun encodeEmojiQuery(src: List<TootReaction>): String =
|
|
|
|
jsonArray {
|
|
|
|
for (reaction in src) {
|
|
|
|
add(reaction.jsonFedibird())
|
|
|
|
}
|
|
|
|
}.toString()
|
|
|
|
|
2021-05-23 14:54:52 +02:00
|
|
|
fun urlToSpan(options: DecodeOptions, code: String, url: String) =
|
|
|
|
SpannableStringBuilder(code).apply {
|
|
|
|
setSpan(
|
|
|
|
NetworkEmojiSpan(url, scale = options.enlargeCustomEmoji),
|
|
|
|
0,
|
|
|
|
length,
|
|
|
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun toSpannableStringBuilder(
|
|
|
|
options: DecodeOptions,
|
|
|
|
code: String,
|
2021-06-20 15:12:25 +02:00
|
|
|
url: String?,
|
2021-05-23 14:54:52 +02:00
|
|
|
): SpannableStringBuilder {
|
2021-05-21 10:20:14 +02:00
|
|
|
|
2021-05-23 14:54:52 +02:00
|
|
|
url?.let { return urlToSpan(options, code, url) }
|
|
|
|
|
|
|
|
if (options.linkHelper?.isMisskey == true) {
|
|
|
|
// 古い形式の絵文字はUnicode絵文字にする
|
|
|
|
misskeyOldReactions[code]?.let {
|
|
|
|
return EmojiDecoder.decodeEmoji(options, it)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return EmojiDecoder.decodeEmoji(options, code)
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
|
|
private fun isSameDomain(accessInfo: SavedAccount, domain: String?) = when (domain) {
|
|
|
|
null, "", ".", accessInfo.apDomain.ascii -> true
|
|
|
|
else -> false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-27 22:58:19 +02:00
|
|
|
private fun chooseUrl() = when {
|
2021-11-20 01:36:43 +01:00
|
|
|
PrefB.bpDisableEmojiAnimation() -> staticUrl
|
2021-06-21 05:03:09 +02:00
|
|
|
else -> url
|
2021-05-17 07:03:18 +02:00
|
|
|
}
|
|
|
|
|
2021-05-21 10:20:14 +02:00
|
|
|
fun splitEmojiDomain() =
|
2021-05-21 08:29:32 +02:00
|
|
|
splitEmojiDomain(name)
|
|
|
|
|
2021-05-17 10:37:39 +02:00
|
|
|
override fun equals(other: Any?): Boolean =
|
|
|
|
when (other) {
|
|
|
|
is TootReaction -> equalsName(this.name, other.name)
|
|
|
|
is String -> equalsName(this.name, other)
|
|
|
|
else -> false
|
|
|
|
}
|
|
|
|
|
2021-05-18 16:35:54 +02:00
|
|
|
override fun hashCode(): Int =
|
|
|
|
name.hashCode()
|
|
|
|
|
2021-05-17 07:03:18 +02:00
|
|
|
fun toSpannableStringBuilder(
|
|
|
|
options: DecodeOptions,
|
2021-06-20 15:12:25 +02:00
|
|
|
status: TootStatus?,
|
2021-05-17 07:03:18 +02:00
|
|
|
): SpannableStringBuilder {
|
|
|
|
|
|
|
|
val code = this.name
|
|
|
|
|
|
|
|
if (options.linkHelper?.isMisskey == true) {
|
|
|
|
|
|
|
|
// 古い形式の絵文字はUnicode絵文字にする
|
|
|
|
misskeyOldReactions[code]?.let {
|
|
|
|
return EmojiDecoder.decodeEmoji(options, it)
|
|
|
|
}
|
|
|
|
|
|
|
|
// カスタム絵文字
|
|
|
|
val customCode = reCustomEmoji.find(code)?.groupValues?.elementAtOrNull(1)
|
|
|
|
if (customCode != null) {
|
2021-05-20 14:11:32 +02:00
|
|
|
// 投稿中に絵文字の情報があればそれを使う
|
2021-05-17 07:03:18 +02:00
|
|
|
status?.custom_emojis?.get(customCode)
|
|
|
|
?.chooseUrl()
|
|
|
|
?.notEmpty()
|
|
|
|
?.let { return urlToSpan(options, code, it) }
|
|
|
|
|
2021-05-20 14:11:32 +02:00
|
|
|
// ストリーミングからきた絵文字などの場合は情報がない
|
2021-05-17 07:03:18 +02:00
|
|
|
val accessInfo = options.linkHelper as? SavedAccount
|
|
|
|
val cols = customCode.split("@", limit = 2)
|
|
|
|
val key = cols.elementAtOrNull(0)
|
|
|
|
val domain = cols.elementAtOrNull(1)
|
2021-06-21 05:03:09 +02:00
|
|
|
if (key != null && accessInfo != null && isSameDomain(accessInfo, domain)) {
|
|
|
|
// デコードオプションのアカウント情報と同じドメインの絵文字なら、
|
|
|
|
// そのドメインの絵文字一覧を取得済みなら
|
|
|
|
// それを使う
|
|
|
|
App1.custom_emoji_lister
|
2022-05-29 15:38:21 +02:00
|
|
|
.getMapNonBlocking(accessInfo)
|
2021-06-21 05:03:09 +02:00
|
|
|
?.get(key)
|
|
|
|
?.chooseUrl()
|
|
|
|
?.notEmpty()
|
|
|
|
?.let { return urlToSpan(options, code, it) }
|
2021-05-17 07:03:18 +02:00
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
// 見つからない場合もある
|
2021-05-17 07:03:18 +02:00
|
|
|
}
|
|
|
|
} else {
|
2021-06-21 05:03:09 +02:00
|
|
|
chooseUrl()?.notEmpty()?.let { return urlToSpan(options, code, it) }
|
2021-05-20 14:11:32 +02:00
|
|
|
// 見つからない場合はあるのだろうか…?
|
2021-05-17 07:03:18 +02:00
|
|
|
}
|
2021-05-20 14:11:32 +02:00
|
|
|
// フォールバック
|
2021-05-17 07:03:18 +02:00
|
|
|
// unicode絵文字、もしくは :xxx: などのshortcode表現
|
|
|
|
return EmojiDecoder.decodeEmoji(options, code)
|
|
|
|
}
|
2021-05-21 10:20:14 +02:00
|
|
|
|
|
|
|
// リアクションカラムの絵文字絞り込み用
|
|
|
|
fun jsonFedibird() = jsonObject {
|
|
|
|
val (basename, domain) = splitEmojiDomain()
|
|
|
|
put("name", basename ?: name)
|
|
|
|
putNotNull("domain", domain)
|
|
|
|
putNotNull("url", url)
|
2021-06-21 05:03:09 +02:00
|
|
|
putNotNull("static_url", staticUrl)
|
2021-05-21 10:20:14 +02:00
|
|
|
}
|
2021-05-17 07:03:18 +02:00
|
|
|
}
|
|
|
|
|
2021-05-17 10:37:39 +02:00
|
|
|
class TootReactionSet(val isMisskey: Boolean) : LinkedList<TootReaction>() {
|
|
|
|
|
2021-05-27 07:54:04 +02:00
|
|
|
fun isMyReaction(reaction: TootReaction?): Boolean {
|
|
|
|
return reaction?.me == true
|
|
|
|
}
|
|
|
|
|
|
|
|
fun hasReaction() = any { it.count > 0 }
|
|
|
|
|
|
|
|
fun hasMyReaction() = any { it.count > 0 && isMyReaction(it) }
|
2021-05-17 10:37:39 +02:00
|
|
|
|
|
|
|
private fun getRaw(name: String?): TootReaction? =
|
|
|
|
find { it.name == name }
|
2021-05-17 07:03:18 +02:00
|
|
|
|
2021-05-18 16:44:12 +02:00
|
|
|
operator fun get(name: String?): TootReaction? = when {
|
|
|
|
name == null || name.isEmpty() -> null
|
|
|
|
isMisskey -> getRaw(name) ?: getRaw(TootReaction.getAnotherExpression(name))
|
|
|
|
else -> getRaw(name)
|
|
|
|
}
|
2021-05-17 07:03:18 +02:00
|
|
|
|
|
|
|
companion object {
|
2021-05-17 10:37:39 +02:00
|
|
|
fun parseMisskey(src: JsonObject?, myReactionCode: String? = null) =
|
|
|
|
if (src == null) {
|
|
|
|
null
|
|
|
|
} else TootReactionSet(isMisskey = true).apply {
|
|
|
|
for (entry in src.entries) {
|
|
|
|
val key = entry.key.notEmpty() ?: continue
|
|
|
|
val v = src.long(key)?.notZero() ?: continue
|
|
|
|
add(TootReaction(name = key, count = v))
|
2021-05-17 07:03:18 +02:00
|
|
|
}
|
2021-05-17 10:37:39 +02:00
|
|
|
if (myReactionCode != null) {
|
2021-05-27 07:54:04 +02:00
|
|
|
forEach { it.me = (it.name == myReactionCode) }
|
2021-05-17 10:37:39 +02:00
|
|
|
}
|
|
|
|
}.notEmpty()
|
|
|
|
|
|
|
|
fun parseFedibird(src: JsonArray? = null) =
|
|
|
|
if (src == null) {
|
|
|
|
null
|
|
|
|
} else TootReactionSet(isMisskey = false).apply {
|
|
|
|
src.objectList().forEach {
|
|
|
|
val tr = TootReaction.parseFedibird(it)
|
|
|
|
if (tr.count > 0) add(tr)
|
|
|
|
}
|
|
|
|
}.notEmpty()
|
2021-05-17 07:03:18 +02:00
|
|
|
}
|
2021-06-20 15:12:25 +02:00
|
|
|
}
|