SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt

411 lines
16 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package jp.juggler.subwaytooter.action
import android.content.Context
import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.actmain.addColumn
import jp.juggler.subwaytooter.actmain.reloadAccountSetting
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount
import jp.juggler.subwaytooter.api.ApiTask
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.api.errorApiResult
import jp.juggler.subwaytooter.api.runApiTask2
import jp.juggler.subwaytooter.api.syncStatus
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.findStatus
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.getVisibilityCaption
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.accountListNonPseudo
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.emptyCallback
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.JsonObject
import jp.juggler.util.log.showToast
import jp.juggler.util.network.toPostRequestBuilder
import kotlinx.coroutines.CancellationException
import kotlin.math.max
private class BoostImpl(
val activity: ActMain,
val accessInfo: SavedAccount,
val statusArg: TootStatus,
val statusOwner: Acct,
val crossAccountMode: CrossAccountMode,
val bSet: Boolean = true,
val visibility: TootVisibility? = null,
val callback: () -> Unit,
) {
val parser = TootParser(activity, accessInfo)
var resultStatus: TootStatus? = null
var resultUnrenoteId: EntityId? = null
// Mastodonは非公開トゥートをブーストできるのは本人だけ
private val isPrivateToot = accessInfo.isMastodon &&
statusArg.visibility == TootVisibility.PrivateFollowers
private fun preCheck(): Boolean {
// アカウントからステータスにブースト操作を行っているなら、何もしない
if (activity.appState.isBusyBoost(accessInfo, statusArg)) {
activity.showToast(false, R.string.wait_previous_operation)
return false
}
if (isPrivateToot && accessInfo.acct != statusOwner) {
activity.showToast(false, R.string.boost_private_toot_not_allowed)
return false
}
// DMとかのブーストはAPI側がエラーを出すだろう
return true
}
private suspend fun Context.syncStatus(client: TootApiClient) =
if (!crossAccountMode.isRemote) {
// 既に自タンスのステータスがある
statusArg
} else {
val (result, status) = client.syncStatus(accessInfo, statusArg)
when {
result == null -> throw CancellationException()
status == null -> errorApiResult(result)
status.reblogged -> errorApiResult(R.string.already_boosted)
else -> status
}
}
// ブースト結果をUIに反映させる
private fun after(newStatus: TootStatus?, unrenoteId: EntityId?) {
when {
// Misskeyでunrenoteに成功した
unrenoteId != null -> {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にしない
val count = max(0, (statusArg.reblogs_count ?: 1) - 1)
for (column in activity.appState.columnList) {
column.findStatus(accessInfo.apDomain, statusArg.id) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = count
// 同アカウントならreblogged状態を変化させる
if (accessInfo == account && status.myRenoteId == unrenoteId) {
status.myRenoteId = null
status.reblogged = false
}
true
}
}
callback()
}
// 処理に成功した
newStatus != null -> {
// カウント数は遅延があるみたいなので、恣意的に表示を変更する
// ブーストカウント数を加工する
val oldCount = statusArg.reblogs_count
val newCount = newStatus.reblogs_count
if (oldCount != null && newCount != null) {
if (bSet && newStatus.reblogged && newCount <= oldCount) {
// 星をつけたのにカウントが上がらないのは違和感あるので、表示をいじる
newStatus.reblogs_count = oldCount + 1
} else if (!bSet && !newStatus.reblogged && newCount >= oldCount) {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはしない
newStatus.reblogs_count = if (oldCount < 1) 0 else oldCount - 1
}
}
for (column in activity.appState.columnList) {
column.findStatus(accessInfo.apDomain, newStatus.id) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = newStatus.reblogs_count
if (accessInfo == account) {
// 同アカウントならreblog状態を変化させる
when {
accessInfo.isMastodon ->
status.reblogged = newStatus.reblogged
bSet && status.myRenoteId == null -> {
status.myRenoteId = newStatus.myRenoteId
status.reblogged = true
}
// Misskey のunrenote時はここを通らない
}
}
true
}
}
callback()
}
}
}
suspend fun boostApi(client: TootApiClient, targetStatus: TootStatus): TootApiResult =
when {
accessInfo.isMisskey -> when {
// misskey, create renote
bSet -> client.requestOrThrow(
"/api/notes/create",
accessInfo.putMisskeyApiToken().apply {
put("renoteId", targetStatus.id.toString())
}.toPostRequestBuilder()
).apply {
jsonObject?.let { jsonObject ->
val outerStatus =
parser.status(jsonObject.jsonObject("createdNote") ?: jsonObject)
val innerStatus = outerStatus?.reblog ?: outerStatus
if (outerStatus != null && innerStatus != null && outerStatus != innerStatus) {
innerStatus.myRenoteId = outerStatus.id
innerStatus.reblogged = true
}
// renoteそのものではなくrenoteされた元noteが欲しい
resultStatus = innerStatus
}
}
// misskey, delete renote
else -> {
val myRenoteId = targetStatus.myRenoteId
?: error("missing renote id.")
client.requestOrThrow(
"/api/notes/delete",
accessInfo.putMisskeyApiToken().apply {
put("noteId", myRenoteId.toString())
put("renoteId", targetStatus.id.toString())
}.toPostRequestBuilder()
).apply {
if (response?.code == 204) {
resultUnrenoteId = myRenoteId
}
}
}
}
// mastodon, reblog or unreblog
else -> client.requestOrThrow(
"/api/v1/statuses/${targetStatus.id}/${if (bSet) "reblog" else "unreblog"}",
requestBuilder = JsonObject().apply {
if (visibility != null) put("visibility", visibility.strMastodon)
}.toPostRequestBuilder()
).apply {
// reblogはreblogを表すStatusを返す
// unreblogはreblogしたStatusを返す
val s = parser.status(jsonObject)
resultStatus = s?.reblog ?: s
}
}
fun run() = activity.launchAndShowError {
if (!preCheck()) return@launchAndShowError
val confirmMessage = activity.getString(
when {
!bSet -> R.string.confirm_unboost_from
isPrivateToot -> R.string.confirm_boost_private_from
visibility == TootVisibility.PrivateFollowers ->
R.string.confirm_private_boost_from
else -> R.string.confirm_boost_from
},
daoAcctColor.getNickname(accessInfo)
)
activity.confirm(
message = confirmMessage,
isConfirmEnabled = when (bSet) {
true -> accessInfo.confirmBoost
else -> accessInfo.confirmUnboost
}
) { newConfirmEnabled ->
when (bSet) {
true -> accessInfo.confirmBoost = newConfirmEnabled
else -> accessInfo.confirmUnboost = newConfirmEnabled
}
daoSavedAccount.save(accessInfo)
activity.reloadAccountSetting(accessInfo)
}
// ブースト表示を更新中にする
activity.appState.setBusyBoost(accessInfo, statusArg)
activity.showColumnMatchAccount(accessInfo)
try {
activity.runApiTask2(
accessInfo = accessInfo,
progressStyle = ApiTask.PROGRESS_NONE
) { client ->
boostApi(client, syncStatus(client))
}
// カラムデータの書き換え
after(resultStatus, resultUnrenoteId)
} finally {
// 更新中状態をリセット
activity.appState.resetBusyBoost(accessInfo, statusArg)
// 失敗やキャンセルの場合でも表示を更新する
activity.showColumnMatchAccount(accessInfo)
}
}
}
fun ActMain.boost(
accessInfo: SavedAccount,
statusArg: TootStatus,
statusOwner: Acct,
crossAccountMode: CrossAccountMode,
bSet: Boolean = true,
visibility: TootVisibility? = null,
callback: () -> Unit,
) {
BoostImpl(
activity = this,
accessInfo = accessInfo,
statusArg = statusArg,
statusOwner = statusOwner,
crossAccountMode = crossAccountMode,
bSet = bSet,
visibility = visibility,
callback = callback,
).run()
}
fun ActMain.boostFromAnotherAccount(
timelineAccount: SavedAccount,
status: TootStatus?,
) {
status ?: return
launchMain {
val statusOwner = timelineAccount.getFullAcct(status.account)
val isPrivateToot = timelineAccount.isMastodon &&
status.visibility == TootVisibility.PrivateFollowers
if (isPrivateToot) {
val list = ArrayList<SavedAccount>()
for (a in daoSavedAccount.loadAccountList()) {
if (a.acct == statusOwner) list.add(a)
}
if (list.isEmpty()) {
showToast(false, R.string.boost_private_toot_not_allowed)
return@launchMain
}
pickAccount(
bAllowPseudo = false,
bAuto = false,
message = getString(R.string.account_picker_boost),
accountListArg = list
)?.let { action_account ->
boost(
action_account,
status,
statusOwner,
calcCrossAccountMode(timelineAccount, action_account),
callback = boostCompleteCallback
)
}
} else {
pickAccount(
bAllowPseudo = false,
bAuto = false,
message = getString(R.string.account_picker_boost),
accountListArg = accountListNonPseudo(timelineAccount.apDomain)
)?.let { action_account ->
boost(
action_account,
status,
statusOwner,
calcCrossAccountMode(timelineAccount, action_account),
callback = boostCompleteCallback
)
}
}
}
}
fun ActMain.clickBoostWithVisibility(
accessInfo: SavedAccount,
status: TootStatus?,
) {
status ?: return
val list = if (accessInfo.isMisskey) {
arrayOf(
TootVisibility.Public,
TootVisibility.UnlistedHome,
TootVisibility.PrivateFollowers,
TootVisibility.LocalPublic,
TootVisibility.LocalHome,
TootVisibility.LocalFollowers,
TootVisibility.DirectSpecified,
TootVisibility.DirectPrivate
)
} else {
arrayOf(
TootVisibility.Public,
TootVisibility.UnlistedHome,
TootVisibility.PrivateFollowers
)
}
val captionList = list
.map { getVisibilityCaption(this, accessInfo.isMisskey, it) }
.toTypedArray()
AlertDialog.Builder(this)
.setTitle(R.string.choose_visibility)
.setItems(captionList) { _, which ->
if (which in list.indices) {
boost(
accessInfo,
status,
accessInfo.getFullAcct(status.account),
CrossAccountMode.SameAccount,
visibility = list[which],
callback = boostCompleteCallback,
)
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
fun ActMain.clickBoostBy(
pos: Int,
accessInfo: SavedAccount,
status: TootStatus?,
columnType: ColumnType = ColumnType.BOOSTED_BY,
) {
status ?: return
addColumn(false, pos, accessInfo, columnType, params = arrayOf(status.id))
}
fun ActMain.clickBoost(accessInfo: SavedAccount, status: TootStatus, willToast: Boolean) {
if (accessInfo.isPseudo) {
boostFromAnotherAccount(accessInfo, status)
} else {
// トグル動作
val bSet = !status.reblogged
boost(
accessInfo,
status,
accessInfo.getFullAcct(status.account),
CrossAccountMode.SameAccount,
bSet = bSet,
callback = when {
!willToast -> emptyCallback
// 簡略表示なら結果をトースト表示
bSet -> boostCompleteCallback
else -> unboostCompleteCallback
},
)
}
}