リアクション動作の改善。絵文字描画時にrequestLayoutしすぎる問題の対策。

This commit is contained in:
tateisu 2023-04-23 22:56:30 +09:00
parent dd46422344
commit 9ba7e1d8d2
14 changed files with 296 additions and 105 deletions

View File

@ -6,6 +6,7 @@ import jp.juggler.subwaytooter.api.ApiTask
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.InstanceCapability
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.TootReaction
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.runApiTask
@ -18,17 +19,21 @@ import jp.juggler.subwaytooter.dialog.launchEmojiPicker
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.util.emojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.accountListCanReaction
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.emojiSizeMode
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.encodePercent
import jp.juggler.util.log.showToast
import jp.juggler.util.network.*
import jp.juggler.util.network.toDelete
import jp.juggler.util.network.toFormRequestBody
import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.network.toPut
import okhttp3.Request
private val rePleromaStatusUrl = """/objects/""".toRegex()
@ -51,10 +56,14 @@ fun ActMain.reactionAdd(
) {
val activity = this@reactionAdd
val accessInfo = column.accessInfo
val ti = TootInstance.getCached(accessInfo)
val canMultipleReaction = InstanceCapability.canMultipleReaction(accessInfo)
val hasMyReaction = status.reactionSet?.hasMyReaction() == true
if (hasMyReaction && !canMultipleReaction) {
val maxReactionPerAccount = InstanceCapability.maxReactionPerAccount(accessInfo, ti)
val myReactionCount = status.reactionSet?.myReactionCount ?: 0
if (maxReactionPerAccount <= 0) {
return
} else if (myReactionCount >= maxReactionPerAccount) {
showToast(false, R.string.already_reactioned)
return
}
@ -108,7 +117,11 @@ fun ActMain.reactionAdd(
}
}
if (canMultipleReaction && TootReaction.isCustomEmoji(code)) {
if (TootReaction.isCustomEmoji(code) && !InstanceCapability.canCustomEmojiReaction(
accessInfo,
ti
)
) {
showToast(false, "can't reaction with custom emoji from this account")
return
}
@ -122,7 +135,7 @@ fun ActMain.reactionAdd(
decodeEmoji = true,
enlargeEmoji = DecodeOptions.emojiScaleReaction,
enlargeCustomEmoji = DecodeOptions.emojiScaleReaction,
emojiSizeMode = accessInfo.emojiSizeMode(),
emojiSizeMode = accessInfo.emojiSizeMode(),
)
val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg)
confirm(
@ -149,7 +162,7 @@ fun ActMain.reactionAdd(
}.toPostRequestBuilder()
) // 成功すると204 no content
canMultipleReaction -> client.request(
ti?.pleromaFeatures != null -> client.request(
"/api/v1/pleroma/statuses/${status.id}/reactions/${code.encodePercent("@")}",
"".toFormRequestBody().toPut()
)?.also { result ->
@ -198,12 +211,11 @@ fun ActMain.reactionRemove(
column: Column,
status: TootStatus,
reactionArg: TootReaction? = null,
confirmed: Boolean = false,
) {
val activity = this
val accessInfo = column.accessInfo
val canMultipleReaction = InstanceCapability.canMultipleReaction(accessInfo)
val ti = TootInstance.getCached(accessInfo)
// 指定されたリアクションまたは自分がリアクションした最初のもの
val reaction = reactionArg ?: status.reactionSet?.find { it.count > 0 && it.me }
@ -213,19 +225,18 @@ fun ActMain.reactionRemove(
}
launchAndShowError {
if (!confirmed) {
val options = DecodeOptions(
activity,
accessInfo,
decodeEmoji = true,
enlargeEmoji = DecodeOptions.emojiScaleReaction,
enlargeCustomEmoji = DecodeOptions.emojiScaleReaction,
emojiSizeMode = accessInfo.emojiSizeMode(),
)
val emojiSpan = reaction.toSpannableStringBuilder(options, status)
confirm(R.string.reaction_remove_confirm, emojiSpan)
}
// 確認
val options = DecodeOptions(
activity,
accessInfo,
decodeEmoji = true,
enlargeEmoji = DecodeOptions.emojiScaleReaction,
enlargeCustomEmoji = DecodeOptions.emojiScaleReaction,
emojiSizeMode = accessInfo.emojiSizeMode(),
)
val emojiSpan = reaction.toSpannableStringBuilder(options, status)
confirm(R.string.reaction_remove_confirm, emojiSpan)
// 削除
var resultStatus: TootStatus? = null
runApiTask(accessInfo) { client ->
when {
@ -236,7 +247,7 @@ fun ActMain.reactionRemove(
}.toPostRequestBuilder()
) // 成功すると204 no content
canMultipleReaction -> client.request(
ti?.pleromaFeatures != null -> client.request(
"/api/v1/pleroma/statuses/${status.id}/reactions/${reaction.name.encodePercent("@")}",
"".toFormRequestBody().toDelete()
)?.also { result ->
@ -245,8 +256,8 @@ fun ActMain.reactionRemove(
}
else -> client.request(
"/api/v1/statuses/${status.id}/emoji_unreaction",
"".toFormRequestBody().toPost()
"/api/v1/statuses/${status.id}/emoji_reactions/${reaction.name.encodePercent("@")}",
Request.Builder().delete(),
)?.also { result ->
// 成功すると新しいステータス
resultStatus = TootParser(activity, accessInfo).status(result.jsonObject)
@ -324,7 +335,7 @@ private fun ActMain.reactionWithoutUi(
return
}
val canMultipleReaction = InstanceCapability.canMultipleReaction(accessInfo)
val ti = TootInstance.getCached(accessInfo)
val options = DecodeOptions(
activity,
@ -332,7 +343,7 @@ private fun ActMain.reactionWithoutUi(
decodeEmoji = true,
enlargeEmoji = DecodeOptions.emojiScaleReaction,
enlargeCustomEmoji = DecodeOptions.emojiScaleReaction,
emojiSizeMode = accessInfo.emojiSizeMode(),
emojiSizeMode = accessInfo.emojiSizeMode(),
)
val emojiSpan = TootReaction.toSpannableStringBuilder(options, reactionCode, reactionImage)
val isCustomEmoji = TootReaction.isCustomEmoji(reactionCode)
@ -340,8 +351,8 @@ private fun ActMain.reactionWithoutUi(
launchAndShowError {
when {
isCustomEmoji && canMultipleReaction ->
error("can't reaction with custom emoji from this account")
isCustomEmoji && !InstanceCapability.canCustomEmojiReaction(accessInfo, ti) ->
error("can't use custom emoji for reaction from this account.")
isCustomEmoji && url?.likePleromaStatusUrl() == true -> confirm(
R.string.confirm_reaction_to_pleroma,
@ -374,7 +385,7 @@ private fun ActMain.reactionWithoutUi(
}.toPostRequestBuilder()
) // 成功すると204 no content
canMultipleReaction -> client.request(
ti?.pleromaFeatures != null -> client.request(
"/api/v1/pleroma/statuses/${resolvedStatus.id}/reactions/${
reactionCode.encodePercent("@")
}",
@ -510,18 +521,29 @@ fun ActMain.reactionFromAnotherAccount(
}
fun ActMain.clickReaction(accessInfo: SavedAccount, column: Column, status: TootStatus) {
val canMultipleReaction = InstanceCapability.canMultipleReaction(accessInfo)
val hasMyReaction = status.reactionSet?.hasMyReaction() == true
val bRemoveButton = hasMyReaction && !canMultipleReaction
when {
!TootReaction.canReaction(accessInfo) ->
reactionFromAnotherAccount(
accessInfo,
status
)
bRemoveButton ->
reactionRemove(column, status)
else ->
reactionAdd(column, status)
val activity = this
launchMain {
try {
val myReactionCount = status.reactionSet?.myReactionCount ?: 0
val maxReactionPerAccount = InstanceCapability.maxReactionPerAccount(accessInfo)
when {
!TootReaction.canReaction(accessInfo) ->
reactionFromAnotherAccount(
accessInfo,
status
)
myReactionCount >= maxReactionPerAccount ->
activity.showToast(
true,
getString(R.string.exceed_reaction_per_account, maxReactionPerAccount)
)
else ->
reactionAdd(column, status)
}
} catch (ex: Throwable) {
showToast(ex, "clickReaction failed.")
}
}
}

View File

@ -12,7 +12,11 @@ import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.VersionString
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
import jp.juggler.util.coroutine.launchDefault
import jp.juggler.util.data.*
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.asciiPattern
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.data.groupEx
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.withCaption
import jp.juggler.util.network.toPostRequestBuilder
@ -55,15 +59,6 @@ object InstanceCapability {
fun visibilityLimited(ti: TootInstance?) =
ti?.fedibirdCapabilities?.contains("visibility_limited") == true
fun emojiReaction(ai: SavedAccount, ti: TootInstance?) =
when {
ai.isPseudo -> false
ai.isMisskey -> true
else ->
ti?.fedibirdCapabilities?.contains("emoji_reaction") == true ||
ti?.pleromaFeatures?.contains("pleroma_emoji_reactions") == true
}
fun statusReference(ai: SavedAccount, ti: TootInstance?) =
when {
ai.isPseudo -> false
@ -79,19 +74,55 @@ object InstanceCapability {
else -> ti?.fedibirdCapabilities != null && ti.versionGE(TootInstance.VERSION_2_7_0_rc1)
}
fun canMultipleReaction(ai: SavedAccount, ti: TootInstance? = TootInstance.getCached(ai)) =
fun canReaction(ai: SavedAccount, ti: TootInstance? = TootInstance.getCached(ai)) =
when {
ai.isPseudo -> false
ai.isMisskey -> false
else -> ti?.pleromaFeatures?.contains("pleroma_emoji_reactions") == true
ai.isMisskey -> true
ti?.fedibirdCapabilities?.contains("emoji_reaction") == true -> true
ti?.pleromaFeatures?.contains("pleroma_emoji_reactions") == true -> true
else -> false
}
fun canCustomEmojiReaction(ai: SavedAccount, ti: TootInstance? = TootInstance.getCached(ai)) =
when {
ai.isPseudo -> false
ai.isMisskey -> true
ti?.fedibirdCapabilities?.contains("emoji_reaction") == true -> true
ti?.pleromaFeatures?.contains("custom_emoji_reactions") == true -> true
else -> false
}
fun maxReactionPerAccount(
ai: SavedAccount,
ti: TootInstance? = TootInstance.getCached(ai),
): Int =
when {
!canReaction(ai, ti) -> 0
ai.isMisskey -> 1
ti?.pleromaFeatures?.contains("pleroma_emoji_reactions") == true -> Int.MAX_VALUE - 10
else ->
ti?.configuration?.jsonObject("emoji_reactions")
?.int("max_reactions_per_account")
?: ti?.configuration?.int("emoji_reactions_per_account")
?: 1
}
// fun canMultipleReaction(ai: SavedAccount, ti: TootInstance? = TootInstance.getCached(ai)) =
// when {
// ai.isPseudo -> false
// ti?.pleromaFeatures?.contains("pleroma_emoji_reactions") == true -> true
// (ti?.configuration?.int("emoji_reactions_per_account") ?: 1) > 1 -> true
// ai.isMisskey -> false
// else -> false
// }
fun listMyReactions(ai: SavedAccount, ti: TootInstance?) =
when {
ai.isPseudo -> false
ai.isMisskey ->
// m544 extension
ti?.misskeyEndpoints?.contains("i/reactions") == true
else ->
// fedibird extension
ti?.fedibirdCapabilities?.contains("emoji_reaction") == true
@ -402,7 +433,8 @@ class TootInstance(parser: TootParser, src: JsonObject) {
*/
private val reOldMisskeyCompatible = """\A[\d.]+:compatible:misskey:""".toRegex()
private val reBothCompatible = """\b(?:misskey|calckey)\b""".toRegex(RegexOption.IGNORE_CASE)
private val reBothCompatible =
"""\b(?:misskey|calckey)\b""".toRegex(RegexOption.IGNORE_CASE)
// 疑似アカウントの追加時に、インスタンスの検証を行う
private suspend fun TootApiClient.getInstanceInformation(
@ -496,6 +528,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
!PrefB.bpEnablePixelfed.value &&
!req.allowPixelfed ->
tiError("currently Pixelfed instance is not supported.")
else -> qrr
}
} catch (ex: Throwable) {

View File

@ -9,8 +9,13 @@ import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.EmojiDecoder
import jp.juggler.util.data.*
import java.util.*
import jp.juggler.util.data.JsonArray
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.data.decodeJsonArray
import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.notZero
import java.util.LinkedList
class TootReaction(
// (fedibird絵文字リアクション) unicode絵文字はunicodeそのまま、 カスタム絵文字はコロンなしのshortcode
@ -122,7 +127,7 @@ class TootReaction(
fun canReaction(
accessInfo: SavedAccount,
ti: TootInstance? = TootInstance.getCached(accessInfo),
) = InstanceCapability.emojiReaction(accessInfo, ti)
) = InstanceCapability.canReaction(accessInfo, ti)
fun decodeEmojiQuery(jsonText: String?): List<TootReaction> =
jsonText.notEmpty()?.let { src ->
@ -256,23 +261,21 @@ class TootReaction(
putNotNull("url", url)
putNotNull("static_url", staticUrl)
}
val isMyReaction get() = me
}
class TootReactionSet(val isMisskey: Boolean) : LinkedList<TootReaction>() {
fun isMyReaction(reaction: TootReaction?): Boolean {
return reaction?.me == true
}
fun hasReaction() = any { it.count > 0 }
fun hasMyReaction() = any { it.count > 0 && isMyReaction(it) }
val myReactionCount get() = count { it.count > 0 && it.me }
private fun getRaw(name: String?): TootReaction? =
find { it.name == name }
operator fun get(name: String?): TootReaction? = when {
name == null || name.isEmpty() -> null
name.isNullOrEmpty() -> null
isMisskey -> getRaw(name) ?: getRaw(TootReaction.getAnotherExpression(name))
else -> getRaw(name)
}

View File

@ -400,7 +400,7 @@ class TootStatus(
return list
}
fun updateReactionMastodon(newReactionSet: TootReactionSet) {
fun updateReactionMastodon(newReactionSet: TootReactionSet?) {
synchronized(this) {
this.reactionSet = newReactionSet
}

View File

@ -351,7 +351,7 @@ private fun Column.scanStatusById(
// ストリーミングイベント受信時、該当アカウントのカラム全て対して呼ばれる
fun Column.updateEmojiReactionByApiResponse(newStatus: TootStatus?) {
newStatus ?: return
val newReactionSet = newStatus.reactionSet ?: TootReactionSet(isMisskey = false)
val newReactionSet = newStatus.reactionSet // Reaction削除の場合、nullは正常ケース
scanStatusById("updateEmojiReactionByApiResponse", newStatus.id) { s ->
s.updateReactionMastodon(newReactionSet)
true

View File

@ -9,6 +9,7 @@ import androidx.core.content.ContextCompat
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.ActMain.Companion.boostButtonSize
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.action.reactionAdd
@ -16,9 +17,17 @@ import jp.juggler.subwaytooter.action.reactionFromAnotherAccount
import jp.juggler.subwaytooter.action.reactionRemove
import jp.juggler.subwaytooter.api.entity.TootReaction
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
import jp.juggler.subwaytooter.util.copyToClipboard
import jp.juggler.subwaytooter.util.emojiSizeMode
import jp.juggler.subwaytooter.util.minWidthCompat
import jp.juggler.subwaytooter.util.startMargin
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.notZero
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.attrColor
@ -89,7 +98,7 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
gravity = Gravity.CENTER
minWidthCompat = textHeight.round()
background = if (reactionSet.isMyReaction(reaction)) {
background = if (reaction.me) {
// 自分がリアクションしたやつは背景を変える
getAdaptiveRippleDrawableRound(
act,
@ -112,19 +121,15 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
tag = reaction
setOnClickListener {
val taggedReaction = it.tag as? TootReaction
if (status.reactionSet?.isMyReaction(taggedReaction) == true) {
if (taggedReaction?.me == true) {
act.reactionRemove(column, status, taggedReaction)
} else {
act.reactionAdd(column, status, taggedReaction?.name, taggedReaction?.staticUrl)
}
}
setOnLongClickListener {
val taggedReaction = it.tag as? TootReaction
act.reactionFromAnotherAccount(
accessInfo,
statusShowing,
taggedReaction
)
setOnLongClickListener { v ->
(v.tag as? TootReaction)
?.let { act.reactionLongClick(accessInfo, statusShowing, it) }
true
}
// カスタム絵文字の場合、アニメーション等のコールバックを処理する必要がある
@ -143,3 +148,23 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
llExtra.addView(box)
}
fun ActMain.reactionLongClick(
accessInfo: SavedAccount,
statusShowing: TootStatus?,
reaction: TootReaction?,
) = launchAndShowError {
reaction ?: return@launchAndShowError
actionsDialog(getString(R.string.reaction) + " " + reaction.name) {
action(getString(R.string.reaction_from_another_account)) {
reactionFromAnotherAccount(
accessInfo,
statusShowing,
reaction
)
}
action(getString(R.string.copy_reaction_name)) {
reaction.name.copyToClipboard(this@reactionLongClick)
}
}
}

View File

@ -13,9 +13,29 @@ import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.action.*
import jp.juggler.subwaytooter.action.bookmarkFromAnotherAccount
import jp.juggler.subwaytooter.action.boostFromAnotherAccount
import jp.juggler.subwaytooter.action.clickBookmark
import jp.juggler.subwaytooter.action.clickBoost
import jp.juggler.subwaytooter.action.clickConversation
import jp.juggler.subwaytooter.action.clickFavourite
import jp.juggler.subwaytooter.action.clickFollow
import jp.juggler.subwaytooter.action.clickQuote
import jp.juggler.subwaytooter.action.clickReaction
import jp.juggler.subwaytooter.action.clickReply
import jp.juggler.subwaytooter.action.conversationOtherInstance
import jp.juggler.subwaytooter.action.favouriteFromAnotherAccount
import jp.juggler.subwaytooter.action.followFromAnotherAccount
import jp.juggler.subwaytooter.action.quoteFromAnotherAccount
import jp.juggler.subwaytooter.action.reactionFromAnotherAccount
import jp.juggler.subwaytooter.action.replyFromAnotherAccount
import jp.juggler.subwaytooter.actmain.nextPosition
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.InstanceCapability
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.api.entity.TootReaction
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.column.getContentColor
import jp.juggler.subwaytooter.pref.PrefB
@ -30,9 +50,20 @@ import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.util.startMargin
import jp.juggler.util.data.notZero
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.*
import org.jetbrains.anko.*
import jp.juggler.util.ui.applyAlphaMultiplier
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.createColoredDrawable
import jp.juggler.util.ui.setIconDrawableId
import jp.juggler.util.ui.vg
import org.jetbrains.anko.UI
import org.jetbrains.anko.custom.customView
import org.jetbrains.anko.dip
import org.jetbrains.anko.frameLayout
import org.jetbrains.anko.imageButton
import org.jetbrains.anko.imageResource
import org.jetbrains.anko.imageView
import org.jetbrains.anko.matchParent
import org.jetbrains.anko.wrapContent
enum class AdditionalButtonsPosition(
val idx: Int, // spinner index start from 0
@ -177,6 +208,7 @@ class StatusButtons(
repliesCount == 1L -> "1"
else -> ""
}
PrefI.RC_ACTUAL -> repliesCount.toString()
else -> ""
}
@ -216,6 +248,7 @@ class StatusButtons(
status.reblogged ->
PrefI.ipButtonBoostedColor.value.notZero()
?: activity.attrColor(R.attr.colorButtonAccentBoost)
else ->
colorTextContent
},
@ -228,6 +261,7 @@ class StatusButtons(
boostsCount == 1L -> "1"
else -> ""
}
PrefI.RC_ACTUAL -> boostsCount.toString()
else -> ""
}
@ -251,16 +285,26 @@ class StatusButtons(
private fun bindReactionButton(status: TootStatus) {
btnReaction.vg(TootReaction.canReaction(accessInfo, ti))?.let {
val canMultipleReaction = InstanceCapability.canMultipleReaction(accessInfo, ti)
val hasMyReaction = status.reactionSet?.hasMyReaction() == true
val bRemoveButton = hasMyReaction && !canMultipleReaction
val myReactionCount: Int = status.reactionSet?.myReactionCount ?: 0
val maxReactionPerAccount: Int =
InstanceCapability.maxReactionPerAccount(accessInfo, ti)
setButton(
it,
true,
colorTextContent,
if (bRemoveButton) R.drawable.ic_remove else R.drawable.ic_add,
when (myReactionCount) {
0 -> colorTextContent
else -> PrefI.ipButtonReactionedColor.value.notZero()
?: activity.attrColor(R.attr.colorButtonAccentReaction)
},
when (myReactionCount >= maxReactionPerAccount) {
true -> R.drawable.outline_face_retouching_off
else -> R.drawable.outline_face
},
activity.getString(
if (bRemoveButton) R.string.reaction_remove else R.string.reaction_add
when (myReactionCount >= maxReactionPerAccount) {
true -> R.string.reaction_remove
else -> R.string.reaction_add
},
)
)
}
@ -286,6 +330,7 @@ class StatusButtons(
status.favourited ->
PrefI.ipButtonFavoritedColor.value.notZero()
?: activity.attrColor(R.attr.colorButtonAccentFavourite)
else -> colorTextContent
},
when {
@ -300,6 +345,7 @@ class StatusButtons(
favouritesCount == 1L -> "1"
else -> ""
}
PrefI.RC_ACTUAL -> favouritesCount.toString()
else -> ""
}
@ -330,6 +376,7 @@ class StatusButtons(
status.bookmarked ->
PrefI.ipButtonBookmarkedColor.value.notZero()
?: activity.attrColor(R.attr.colorButtonAccentBookmark)
else ->
colorTextContent
},
@ -536,6 +583,7 @@ class StatusButtons(
itemViewHolder.listAdapter,
status = status
)
btnReply -> clickReply(accessInfo, status)
btnQuote -> clickQuote(accessInfo, status)
btnBoost -> clickBoost(accessInfo, status, willToast = bSimpleList)
@ -836,6 +884,7 @@ class StatusButtonsViewHolder(
additionalButtons()
normalButtons()
}
else -> {
normalButtons()
additionalButtons()

View File

@ -9,7 +9,11 @@ import jp.juggler.subwaytooter.api.ApiError
import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.auth.AuthMastodon
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.InstanceCapability
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.api.entity.TootPushSubscription
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.push.ApiPushMastodon
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.lazyContext
@ -18,7 +22,13 @@ import jp.juggler.subwaytooter.table.AccountNotificationStatus
import jp.juggler.subwaytooter.table.PushMessage
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.appDatabase
import jp.juggler.util.data.*
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.decodeBase64
import jp.juggler.util.data.digestSHA256Base64Url
import jp.juggler.util.data.ellipsizeDot3
import jp.juggler.util.data.encodeBase64Url
import jp.juggler.util.data.notBlank
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import jp.juggler.util.time.parseTimeIso8601
import kotlinx.coroutines.isActive
@ -57,6 +67,7 @@ class PushMastodon(
subLog.i(msg)
null
}
else -> lazyContext.getString(
R.string.push_subscription_app_server_hash_missing_error
)
@ -106,6 +117,7 @@ class PushMastodon(
null -> {
subLog.i(R.string.push_subscription_is_not_required)
}
else -> {
subLog.i(R.string.push_subscription_delete_current)
api.deletePushSubscription(account)
@ -261,9 +273,9 @@ class PushMastodon(
// fedibird拡張
// https://github.com/fedibird/mastodon/blob/fedibird/app/controllers/api/v1/push/subscriptions_controller.rb#L55
// https://github.com/fedibird/mastodon/blob/fedibird/app/models/notification.rb
if (!ti.pleromaFeatures.isNullOrEmpty()) {
if (ti.pleromaFeatures?.contains("pleroma_emoji_reactions") == true) {
dst[TootNotification.TYPE_EMOJI_REACTION_PLEROMA] = notificationReaction
} else if (!ti.fedibirdCapabilities.isNullOrEmpty()) {
} else if (ti.fedibirdCapabilities?.contains("emoji_reaction") == true) {
dst[TootNotification.TYPE_EMOJI_REACTION] = notificationReaction
}
dst[TootNotification.TYPE_SCHEDULED_STATUS] = notificationPost // 設定項目不足
@ -293,11 +305,11 @@ class PushMastodon(
// Fedibird拡張
TootNotification.TYPE_EMOJI_REACTION,
-> InstanceCapability.emojiReaction(account, ti)
-> InstanceCapability.canReaction(account, ti)
// pleromaの絵文字リアクションはalertに指定できない
TootNotification.TYPE_EMOJI_REACTION_PLEROMA,
-> InstanceCapability.emojiReaction(account, ti)
-> InstanceCapability.canReaction(account, ti)
TootNotification.TYPE_SCHEDULED_STATUS,
-> InstanceCapability.scheduledStatus(account, ti)
@ -368,6 +380,7 @@ class PushMastodon(
pm.iconSmall = a.supplyBaseUrl(json.string("badge"))
}
else -> {
// Mastodon 4.0
// {

View File

@ -4,6 +4,7 @@ import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.SystemClock
import android.text.style.ReplacementSpan
import androidx.annotation.IntRange
import androidx.core.content.ContextCompat
@ -16,6 +17,8 @@ import jp.juggler.subwaytooter.util.EmojiImageRect
import jp.juggler.subwaytooter.util.EmojiSizeMode
import jp.juggler.util.log.LogCategory
import java.lang.ref.WeakReference
import kotlin.math.max
import kotlin.math.min
class NetworkEmojiSpan constructor(
private val url: String,
@ -48,6 +51,7 @@ class NetworkEmojiSpan constructor(
private val rectSrc = Rect()
private var lastMeasuredWidth = 0f
private var lastRequestTime = 0L
private val emojiImageRect = EmojiImageRect(
sizeMode = sizeMode,
@ -156,22 +160,35 @@ class NetworkEmojiSpan constructor(
return false
}
rectSrc.set(0, 0, srcWidth, srcHeight)
val aspect = srcWidth.toFloat() / srcHeight.toFloat()
emojiImageRect.updateRect(
url = url,
aspectArg = srcWidth.toFloat() / srcHeight.toFloat(),
aspectArg = aspect,
textPaint.textSize,
baseline.toFloat()
)
val clipBounds = canvas.clipBounds
val clipWidth = clipBounds.width()
// 最後にgetSizeで返した幅と異なるか、現在のTextViewのClip幅より大きいなら
// 再レイアウトを要求する
if (emojiImageRect.emojiWidth != lastMeasuredWidth) {
log.i("requestLayout by width changed")
invalidateCallback.requestLayout()
} else if (emojiImageRect.emojiWidth > clipWidth) {
log.i("requestLayout by clipWidth ${emojiImageRect.emojiWidth}/${clipWidth}")
invalidateCallback.requestLayout()
val now = SystemClock.elapsedRealtime()
if (now - lastRequestTime >= 1000L) {
// 最後にgetSizeで返した幅と異なるか、現在のTextViewのClip幅より大きいなら
// 再レイアウトを要求する
val willLayout = when {
!equalsEmojiWidth(emojiImageRect.emojiWidth, lastMeasuredWidth) -> true
emojiImageRect.emojiWidth > clipWidth -> {
log.i("requestLayout by clipWidth ${emojiImageRect.emojiWidth}/${clipWidth}")
true
}
else -> false
}
if (willLayout) {
invalidateCallback.requestLayout()
lastRequestTime = now
}
}
canvas.save()
@ -201,6 +218,19 @@ class NetworkEmojiSpan constructor(
return true
}
// getSizeで使うinitialAspectはリサイズの影響を受けないため、誤差が出る
// 数%の誤差を許容するような比較を行う
private fun equalsEmojiWidth(a: Float, b: Float): Boolean {
if (a == b) return true
val max = max(a, b)
val min = min(a, b)
if (min < 1f) return false
val scale = max / min
if (scale in 0.95f..1.05f) return true
log.i("equalsEmojiWidth: a=$a b=$b scale=$scale")
return false
}
private fun handleFrameLoaded(frames: ApngFrames?) {
frames?.aspect?.let {
invalidateCallback?.requestLayout()

View File

@ -130,7 +130,7 @@ suspend fun Context.accountListCanReaction(pickupHost: Host? = null) =
if (ti == null) {
ri?.error?.let { log.w(it) }
false
} else InstanceCapability.emojiReaction(a, ti)
} else InstanceCapability.canReaction(a, ti)
}
}
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M10.25,13c0,0.69 -0.56,1.25 -1.25,1.25S7.75,13.69 7.75,13s0.56,-1.25 1.25,-1.25 1.25,0.56 1.25,1.25zM15,11.75c-0.69,0 -1.25,0.56 -1.25,1.25s0.56,1.25 1.25,1.25 1.25,-0.56 1.25,-1.25 -0.56,-1.25 -1.25,-1.25zM22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12 6.48,2 12,2s10,4.48 10,10zM10.66,4.12C12.06,6.44 14.6,8 17.5,8c0.46,0 0.91,-0.05 1.34,-0.12C17.44,5.56 14.9,4 12,4c-0.46,0 -0.91,0.05 -1.34,0.12zM4.42,9.47c1.71,-0.97 3.03,-2.55 3.66,-4.44C6.37,6 5.05,7.58 4.42,9.47zM20,12c0,-0.78 -0.12,-1.53 -0.33,-2.24 -0.7,0.15 -1.42,0.24 -2.17,0.24 -3.13,0 -5.92,-1.44 -7.76,-3.69C8.69,8.87 6.6,10.88 4,11.86c0.01,0.04 0,0.09 0,0.14 0,4.41 3.59,8 8,8s8,-3.59 8,-8z"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,13m-1.25,0a1.25,1.25 0,1 1,2.5 0a1.25,1.25 0,1 1,-2.5 0"/>
<path android:fillColor="@android:color/white" android:pathData="M17.5,10c0.75,0 1.47,-0.09 2.17,-0.24C19.88,10.47 20,11.22 20,12c0,1.22 -0.28,2.37 -0.77,3.4l1.49,1.49C21.53,15.44 22,13.78 22,12c0,-5.52 -4.48,-10 -10,-10c-1.78,0 -3.44,0.47 -4.89,1.28l5.33,5.33C13.93,9.49 15.65,10 17.5,10zM10.66,4.12C11.09,4.05 11.54,4 12,4c2.9,0 5.44,1.56 6.84,3.88C18.41,7.95 17.96,8 17.5,8C14.6,8 12.06,6.44 10.66,4.12z"/>
<path android:fillColor="@android:color/white" android:pathData="M1.89,3.72l2.19,2.19C2.78,7.6 2,9.71 2,12c0,5.52 4.48,10 10,10c2.29,0 4.4,-0.78 6.09,-2.08l2.19,2.19l1.41,-1.41L3.31,2.31L1.89,3.72zM16.66,18.49C15.35,19.44 13.74,20 12,20c-4.41,0 -8,-3.59 -8,-8c0,-0.05 0.01,-0.1 0,-0.14c1.39,-0.52 2.63,-1.35 3.64,-2.39L16.66,18.49zM6.23,8.06C5.7,8.61 5.09,9.09 4.42,9.47C4.68,8.7 5.05,7.99 5.51,7.34L6.23,8.06z"/>
</vector>

View File

@ -1274,4 +1274,6 @@
<string name="autocomplete_list_loading">入力補完リストの読み込み中…</string>
<string name="media_count">%1$d個の添付データ (タップで全て表示)</string>
<string name="applied_when_post">投稿送信時に反映されます</string>
<string name="exceed_reaction_per_account">リアクション個数の制限(%1$d)</string>
<string name="copy_reaction_name">Copy reaction name</string>
</resources>

View File

@ -1282,4 +1282,6 @@
<string name="autocomplete_list_loading">loading autocomplete list…</string>
<string name="media_count">%1$d attachments (tap to see all)</string>
<string name="applied_when_post">applied when post.</string>
<string name="exceed_reaction_per_account">Reaction limit is %1$d.</string>
<string name="copy_reaction_name">Copy reaction name</string>
</resources>