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.* 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.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 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 var bConfirmed = false 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 { status == null -> errorApiResult(result) status.reblogged -> errorApiResult(getString(R.string.already_boosted)) else -> status } } // ブースト結果をUIに反映させる private fun after(result: TootApiResult?, newStatus: TootStatus?, unrenoteId: EntityId?) { result ?: return // cancelled. 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() } else -> activity.showToast(true, result.error) } } suspend fun boostApi(client: TootApiClient, targetStatus: TootStatus): TootApiResult? = if (accessInfo.isMisskey) { if (!bSet) { val myRenoteId = targetStatus.myRenoteId ?: errorApiResult("missing renote id.") client.request( "/api/notes/delete", accessInfo.putMisskeyApiToken().apply { put("noteId", myRenoteId.toString()) put("renoteId", targetStatus.id.toString()) }.toPostRequestBuilder() )?.also { if (it.response?.code == 204) { resultUnrenoteId = myRenoteId } } } else { client.request( "/api/notes/create", accessInfo.putMisskeyApiToken().apply { put("renoteId", targetStatus.id.toString()) }.toPostRequestBuilder() )?.also { result -> val jsonObject = result.jsonObject if (jsonObject != null) { 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 } } } } else { val b = JsonObject().apply { if (visibility != null) put("visibility", visibility.strMastodon) }.toPostRequestBuilder() client.request( "/api/v1/statuses/${targetStatus.id}/${if (bSet) "reblog" else "unreblog"}", b )?.also { result -> // reblogはreblogを表すStatusを返す // unreblogはreblogしたStatusを返す val s = parser.status(result.jsonObject) resultStatus = s?.reblog ?: s } } fun run() { activity.launchAndShowError { if (!preCheck()) return@launchAndShowError if (!bConfirmed) { activity.confirm( 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) ), when (bSet) { true -> accessInfo.confirm_boost else -> accessInfo.confirm_unboost } ) { newConfirmEnabled -> when (bSet) { true -> accessInfo.confirm_boost = newConfirmEnabled else -> accessInfo.confirm_unboost = newConfirmEnabled } daoSavedAccount.saveSetting(accessInfo) activity.reloadAccountSetting(accessInfo) } } // ブースト表示を更新中にする activity.appState.setBusyBoost(accessInfo, statusArg) activity.showColumnMatchAccount(accessInfo) val result = activity.runApiTask( accessInfo, progressStyle = ApiTask.PROGRESS_NONE ) { client -> try { val targetStatus = syncStatus(client) boostApi(client, targetStatus) } catch (ex: TootApiResultException) { ex.result } } // 更新中状態をリセット activity.appState.resetBusyBoost(accessInfo, statusArg) // カラムデータの書き換え after(result, resultStatus, resultUnrenoteId) // result == null の場合でも更新中表示の解除が必要になる 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() 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, 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 }, ) } }