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

1524 lines
44 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.text.SpannableStringBuilder
import jp.juggler.emoji.EmojiMap
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.dialog.AccountPicker
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgConfirm
import jp.juggler.subwaytooter.dialog.EmojiPicker
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.SavedAccountCallback
import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.subwaytooter.util.openCustomTab
import jp.juggler.util.*
import okhttp3.Request
import java.util.*
import kotlin.math.max
object Action_Toot {
private val log = LogCategory("Action_Toot")
private val reDetailedStatusTime =
"""<a\b[^>]*?\bdetailed-status__datetime\b[^>]*href="https://[^/]+/@[^/]+/([^\s?#/"]+)"""
.asciiPattern()
// アカウントを選んでお気に入り
fun favouriteFromAnotherAccount(
activity: ActMain,
timeline_account: SavedAccount,
status: TootStatus?
) {
if (status == null) return
AccountPicker.pick(
activity,
bAllowPseudo = false,
bAuto = false,
message = activity.getString(R.string.account_picker_favourite),
accountListArg = makeAccountListNonPseudo(activity, timeline_account.apDomain)
) { action_account ->
favourite(
activity,
action_account,
status,
calcCrossAccountMode(timeline_account, action_account),
callback = activity.favourite_complete_callback
)
}
}
// お気に入りの非同期処理
fun favourite(
activity: ActMain,
access_info: SavedAccount,
arg_status: TootStatus,
nCrossAccountMode: Int,
callback: () -> Unit,
bSet: Boolean = true,
bConfirmed: Boolean = false
) {
if (activity.app_state.isBusyFav(access_info, arg_status)) {
activity.showToast(false, R.string.wait_previous_operation)
return
}
// 必要なら確認を出す
if (!bConfirmed && access_info.isMastodon) {
DlgConfirm.open(
activity,
activity.getString(
when (bSet) {
true -> R.string.confirm_favourite_from
else -> R.string.confirm_unfavourite_from
},
AcctColor.getNickname(access_info)
),
object : DlgConfirm.Callback {
override fun onOK() {
favourite(
activity,
access_info,
arg_status,
nCrossAccountMode,
callback,
bSet = bSet,
bConfirmed = true
)
}
override var isConfirmEnabled: Boolean
get() = when (bSet) {
true -> access_info.confirm_favourite
else -> access_info.confirm_unfavourite
}
set(value) {
when (bSet) {
true -> access_info.confirm_favourite = value
else -> access_info.confirm_unfavourite = value
}
access_info.saveSetting()
activity.reloadAccountSetting(access_info)
}
})
return
}
//
activity.app_state.setBusyFav(access_info, arg_status)
//
TootTaskRunner(activity, TootTaskRunner.PROGRESS_NONE).run(access_info, object : TootTask {
var new_status: TootStatus? = null
override suspend fun background(client: TootApiClient): TootApiResult? {
val target_status = if (nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE) {
val (result, status) = client.syncStatus(access_info, arg_status)
status ?: return result
if (status.favourited) {
return TootApiResult(activity.getString(R.string.already_favourited))
}
status
} else {
arg_status
}
return if (access_info.isMisskey) {
client.request(
if (bSet) {
"/api/notes/favorites/create"
} else {
"/api/notes/favorites/delete"
},
access_info.putMisskeyApiToken().apply {
put("noteId", target_status.id.toString())
}
.toPostRequestBuilder()
)?.also { result ->
// 正常レスポンスは 204 no content
// 既にお気に入り済みならエラー文字列に'already favorited' が返る
if (result.response?.code == 204
|| result.error?.contains("already favorited") == true
|| result.error?.contains("already not favorited") == true
) {
// 成功した
new_status = target_status.apply {
favourited = bSet
}
}
}
} else {
client.request(
"/api/v1/statuses/${target_status.id}/${if (bSet) "favourite" else "unfavourite"}",
"".toFormRequestBody().toPost()
)?.also { result ->
new_status = TootParser(activity, access_info).status(result.jsonObject)
}
}
}
override suspend fun handleResult(result: TootApiResult?) {
activity.app_state.resetBusyFav(access_info, arg_status)
val new_status = this.new_status
when {
result == null -> {
} // cancelled.
new_status != null -> {
val old_count = arg_status.favourites_count
val new_count = new_status.favourites_count
if (old_count != null && new_count != null) {
if (access_info.isMisskey) {
new_status.favourited = bSet
}
if (bSet && new_status.favourited && new_count <= old_count) {
// 星をつけたのにカウントが上がらないのは違和感あるので、表示をいじる
new_status.favourites_count = old_count + 1L
} else if (!bSet && !new_status.favourited && new_count >= old_count) {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはならない
new_status.favourites_count =
if (old_count < 1L) 0L else old_count - 1L
}
}
for (column in activity.app_state.columnList) {
column.findStatus(
access_info.apDomain,
new_status.id
) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.favourites_count = new_status.favourites_count
// 同アカウントならfav状態を変化させる
if (access_info == account) {
status.favourited = new_status.favourited
}
true
}
}
callback()
}
else -> activity.showToast(true, result.error)
}
// 結果に関わらず、更新中状態から復帰させる
activity.showColumnMatchAccount(access_info)
}
})
// ファボ表示を更新中にする
activity.showColumnMatchAccount(access_info)
}
// アカウントを選んでお気に入り
fun bookmarkFromAnotherAccount(
activity: ActMain,
timeline_account: SavedAccount,
status: TootStatus?
) {
status ?: return
AccountPicker.pick(
activity,
bAllowPseudo = false,
bAuto = false,
message = activity.getString(R.string.account_picker_bookmark),
accountListArg = makeAccountListNonPseudo(activity, timeline_account.apDomain)
) { action_account ->
bookmark(
activity,
action_account,
status,
calcCrossAccountMode(timeline_account, action_account),
callback = activity.bookmark_complete_callback
)
}
}
// お気に入りの非同期処理
fun bookmark(
activity: ActMain,
access_info: SavedAccount,
arg_status: TootStatus,
nCrossAccountMode: Int,
callback: () -> Unit,
bSet: Boolean = true,
bConfirmed: Boolean = false
) {
if (activity.app_state.isBusyFav(access_info, arg_status)) {
activity.showToast(false, R.string.wait_previous_operation)
return
}
if (access_info.isMisskey) {
activity.showToast(false, R.string.misskey_account_not_supported)
return
}
// 必要なら確認を出す
// ブックマークは解除する時だけ確認する
if (!bConfirmed && !bSet) {
DlgConfirm.openSimple(
activity,
activity.getString(
R.string.confirm_unbookmark_from,
AcctColor.getNickname(access_info)
)
) {
bookmark(
activity,
access_info,
arg_status,
nCrossAccountMode,
callback,
bSet = bSet,
bConfirmed = true
)
}
return
}
//
activity.app_state.setBusyBookmark(access_info, arg_status)
//
TootTaskRunner(activity, TootTaskRunner.PROGRESS_NONE).run(access_info, object : TootTask {
var new_status: TootStatus? = null
override suspend fun background(client: TootApiClient): TootApiResult? {
val target_status = if (nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE) {
val (result, status) = client.syncStatus(access_info, arg_status)
status ?: return result
if (status.bookmarked) {
return TootApiResult(activity.getString(R.string.already_bookmarked))
}
status
} else {
arg_status
}
return client.request(
"/api/v1/statuses/${target_status.id}/${if (bSet) "bookmark" else "unbookmark"}",
"".toFormRequestBody().toPost()
)?.also { result ->
new_status = TootParser(activity, access_info).status(result.jsonObject)
}
}
override suspend fun handleResult(result: TootApiResult?) {
activity.app_state.resetBusyBookmark(access_info, arg_status)
val new_status = this.new_status
when {
result == null -> {
} // cancelled.
new_status != null -> {
for (column in activity.app_state.columnList) {
column.findStatus(
access_info.apDomain,
new_status.id
) { account, status ->
// 同アカウントならブックマーク状態を伝播する
if (access_info == account) {
status.bookmarked = new_status.bookmarked
}
true
}
}
callback()
}
else -> activity.showToast(true, result.error)
}
// 結果に関わらず、更新中状態から復帰させる
activity.showColumnMatchAccount(access_info)
}
})
// ファボ表示を更新中にする
activity.showColumnMatchAccount(access_info)
}
fun boostFromAnotherAccount(
activity: ActMain,
timeline_account: SavedAccount,
status: TootStatus?
) {
status ?: return
val status_owner = timeline_account.getFullAcct(status.account)
val isPrivateToot = timeline_account.isMastodon &&
status.visibility == TootVisibility.PrivateFollowers
if (isPrivateToot) {
val list = ArrayList<SavedAccount>()
for (a in SavedAccount.loadAccountList(activity)) {
if (a.acct == status_owner) list.add(a)
}
if (list.isEmpty()) {
activity.showToast(false, R.string.boost_private_toot_not_allowed)
return
}
AccountPicker.pick(
activity,
bAllowPseudo = false,
bAuto = false,
message = activity.getString(R.string.account_picker_boost),
accountListArg = list
) { action_account ->
boost(
activity,
action_account,
status,
status_owner,
calcCrossAccountMode(timeline_account, action_account),
callback = activity.boost_complete_callback
)
}
} else {
AccountPicker.pick(
activity,
bAllowPseudo = false,
bAuto = false,
message = activity.getString(R.string.account_picker_boost),
accountListArg = makeAccountListNonPseudo(activity, timeline_account.apDomain)
) { action_account ->
boost(
activity,
action_account,
status,
status_owner,
calcCrossAccountMode(timeline_account, action_account),
callback = activity.boost_complete_callback
)
}
}
}
fun boost(
activity: ActMain,
access_info: SavedAccount,
arg_status: TootStatus,
status_owner: Acct,
nCrossAccountMode: Int,
bSet: Boolean = true,
bConfirmed: Boolean = false,
visibility: TootVisibility? = null,
callback: () -> Unit
) {
// アカウントからステータスにブースト操作を行っているなら、何もしない
if (activity.app_state.isBusyBoost(access_info, arg_status)) {
activity.showToast(false, R.string.wait_previous_operation)
return
}
// Mastodonは非公開トゥートをブーストできるのは本人だけ
val isPrivateToot = access_info.isMastodon &&
arg_status.visibility == TootVisibility.PrivateFollowers
if( isPrivateToot && access_info.acct != status_owner) {
activity.showToast(false, R.string.boost_private_toot_not_allowed)
return
}
// DMとかのブーストはAPI側がエラーを出すだろう
// 必要なら確認を出す
if (!bConfirmed) {
DlgConfirm.open(
activity,
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
},
AcctColor.getNickname(access_info)
),
object : DlgConfirm.Callback {
override fun onOK() {
boost(
activity,
access_info,
arg_status,
status_owner,
nCrossAccountMode,
bSet = bSet,
bConfirmed = true,
visibility = visibility,
callback = callback,
)
}
override var isConfirmEnabled: Boolean
get() = when (bSet) {
true -> access_info.confirm_boost
else -> access_info.confirm_unboost
}
set(value) {
when (bSet) {
true -> access_info.confirm_boost = value
else -> access_info.confirm_unboost = value
}
access_info.saveSetting()
activity.reloadAccountSetting(access_info)
}
})
return
}
activity.app_state.setBusyBoost(access_info, arg_status)
TootTaskRunner(activity, TootTaskRunner.PROGRESS_NONE).run(access_info, object : TootTask {
var new_status: TootStatus? = null
var unrenoteId: EntityId? = null
override suspend fun background(client: TootApiClient): TootApiResult? {
val parser = TootParser(activity, access_info)
val target_status = if (nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE) {
val (result, status) = client.syncStatus(access_info, arg_status)
if (status == null) return result
if (status.reblogged) {
return TootApiResult(activity.getString(R.string.already_boosted))
}
status
} else {
// 既に自タンスのステータスがある
arg_status
}
if (access_info.isMisskey) {
return if (!bSet) {
val myRenoteId = target_status.myRenoteId
?: return TootApiResult("missing renote id.")
client.request(
"/api/notes/delete",
access_info.putMisskeyApiToken().apply {
put("noteId", myRenoteId.toString())
put("renoteId", target_status.id.toString())
}
.toPostRequestBuilder()
)
?.also {
if (it.response?.code == 204)
unrenoteId = myRenoteId
}
} else {
client.request(
"/api/notes/create",
access_info.putMisskeyApiToken().apply {
put("renoteId", target_status.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が欲しい
this.new_status = innerStatus
}
}
}
} else {
val b = JsonObject().apply {
if (visibility != null) put("visibility", visibility.strMastodon)
}.toPostRequestBuilder()
val result = client.request(
"/api/v1/statuses/${target_status.id}/${if (bSet) "reblog" else "unreblog"}",
b
)
// reblogはreblogを表すStatusを返す
// unreblogはreblogしたStatusを返す
val s = parser.status(result?.jsonObject)
this.new_status = s?.reblog ?: s
return result
}
}
override suspend fun handleResult(result: TootApiResult?) {
activity.app_state.resetBusyBoost(access_info, arg_status)
val unrenoteId = this.unrenoteId
val new_status = this.new_status
when {
// cancelled.
result == null -> {
}
// Misskeyでunrenoteに成功した
unrenoteId != null -> {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはならない
val count = max(0, (arg_status.reblogs_count ?: 1) - 1)
for (column in activity.app_state.columnList) {
column.findStatus(
access_info.apDomain,
arg_status.id
) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = count
// 同アカウントならreblogged状態を変化させる
if (access_info == account && status.myRenoteId == unrenoteId) {
status.myRenoteId = null
status.reblogged = false
}
true
}
}
callback()
}
new_status != null -> {
// カウント数は遅延があるみたいなので、恣意的に表示を変更する
// ブーストカウント数を加工する
val old_count = arg_status.reblogs_count
val new_count = new_status.reblogs_count
if (old_count != null && new_count != null) {
if (bSet && new_status.reblogged && new_count <= old_count) {
// 星をつけたのにカウントが上がらないのは違和感あるので、表示をいじる
new_status.reblogs_count = old_count + 1
} else if (!bSet && !new_status.reblogged && new_count >= old_count) {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはならない
new_status.reblogs_count = if (old_count < 1) 0 else old_count - 1
}
}
for (column in activity.app_state.columnList) {
column.findStatus(
access_info.apDomain,
new_status.id
) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = new_status.reblogs_count
if (access_info == account) {
// 同アカウントならreblog状態を変化させる
when {
access_info.isMastodon ->
status.reblogged = new_status.reblogged
bSet && status.myRenoteId == null -> {
status.myRenoteId = new_status.myRenoteId
status.reblogged = true
}
}
// Misskey のunrenote時はここを通らない
}
true
}
}
callback()
}
else -> activity.showToast(true, result.error)
}
// 結果に関わらず、更新中状態から復帰させる
activity.showColumnMatchAccount(access_info)
}
})
// ブースト表示を更新中にする
activity.showColumnMatchAccount(access_info)
// misskeyは非公開トゥートをブーストできないっぽい
}
fun delete(activity: ActMain, access_info: SavedAccount, status_id: EntityId) {
TootTaskRunner(activity).run(access_info, object : TootTask {
override suspend fun background(client: TootApiClient): TootApiResult? {
return if (access_info.isMisskey) {
client.request(
"/api/notes/delete",
access_info.putMisskeyApiToken().apply {
put("noteId", status_id)
}
.toPostRequestBuilder()
)
// 204 no content
} else {
client.request(
"/api/v1/statuses/$status_id",
Request.Builder().delete()
)
}
}
override suspend fun handleResult(result: TootApiResult?) {
if (result == null) return // cancelled.
if (result.jsonObject != null) {
activity.showToast(false, R.string.delete_succeeded)
for (column in activity.app_state.columnList) {
column.onStatusRemoved(access_info.apDomain, status_id)
}
} else {
activity.showToast(false, result.error)
}
}
})
}
/////////////////////////////////////////////////////////////////////////////////////
// open conversation
internal fun clearConversationUnread(
activity: ActMain,
access_info: SavedAccount,
conversationSummary: TootConversationSummary?
) {
conversationSummary ?: return
TootTaskRunner(activity, progress_style = TootTaskRunner.PROGRESS_NONE)
.run(access_info, object : TootTask {
override suspend fun background(client: TootApiClient): TootApiResult? {
return client.request(
"/api/v1/conversations/${conversationSummary.id}/read",
"".toFormRequestBody().toPost()
)
}
override suspend fun handleResult(result: TootApiResult?) {
// 何もしない
}
})
}
// ローカルかリモートか判断する
fun conversation(
activity: ActMain,
pos: Int,
access_info: SavedAccount,
status: TootStatus
) {
if (access_info.isNA || !access_info.matchHost(status.readerApDomain)) {
conversationOtherInstance(activity, pos, status)
} else {
conversationLocal(activity, pos, access_info, status.id)
}
}
// ローカルから見える会話の流れを表示する
fun conversationLocal(
activity: ActMain,
pos: Int,
access_info: SavedAccount,
status_id: EntityId
) {
activity.addColumn(pos, access_info, ColumnType.CONVERSATION, status_id)
}
// リモートかもしれない会話の流れを表示する
fun conversationOtherInstance(
activity: ActMain, pos: Int, status: TootStatus?
) {
if (status == null) return
val url = status.url
if (url == null || url.isEmpty()) {
// URLが不明なトゥートというのはreblogの外側のアレ
return
}
when {
// 検索サービスではステータスTLをどのタンスから読んだのか分からない
status.readerApDomain == null ->
conversationOtherInstance(
activity, pos, url, TootStatus.validStatusId(status.id)
?: TootStatus.findStatusIdFromUri(
status.uri,
status.url
)
)
// TLアカウントのホストとトゥートのアカウントのホストが同じ
status.originalApDomain == status.readerApDomain ->
conversationOtherInstance(
activity, pos, url, TootStatus.validStatusId(status.id)
?: TootStatus.findStatusIdFromUri(
status.uri,
status.url
)
)
else -> {
// トゥートを取得したタンスと投稿元タンスが異なる場合
// status.id はトゥートを取得したタンスでのIDである
// 投稿元タンスでのIDはuriやURLから調べる
// pleromaではIDがuuidなので失敗する(その時はURLを検索してIDを見つける)
conversationOtherInstance(
activity, pos, url, TootStatus.findStatusIdFromUri(
status.uri,
status.url
), status.readerApDomain, TootStatus.validStatusId(status.id)
)
}
}
}
// アプリ外部からURLを渡された場合に呼ばれる
fun conversationOtherInstance(
activity: ActMain,
pos: Int,
url: String,
status_id_original: EntityId? = null,
host_access: Host? = null,
status_id_access: EntityId? = null
) {
val dialog = ActionsDialog()
val host_original = Host.parse(url.toUri().authority ?: "")
// 選択肢:ブラウザで表示する
dialog.addAction(activity.getString(R.string.open_web_on_host, host_original.pretty))
{ activity.openCustomTab(url) }
// トゥートの投稿元タンスにあるアカウント
val local_account_list = ArrayList<SavedAccount>()
// TLを読んだタンスにあるアカウント
val access_account_list = ArrayList<SavedAccount>()
// その他のタンスにあるアカウント
val other_account_list = ArrayList<SavedAccount>()
for (a in SavedAccount.loadAccountList(activity)) {
// 疑似アカウントは後でまとめて処理する
if (a.isPseudo) continue
if (status_id_original != null && a.matchHost(host_original)) {
// アクセス情報ステータスID でアクセスできるなら
// 同タンスのアカウントならステータスIDの変換なしに表示できる
local_account_list.add(a)
} else if (status_id_access != null && a.matchHost(host_access)) {
// 既に変換済みのステータスIDがあるなら、そのアカウントでもステータスIDの変換は必要ない
access_account_list.add(a)
} else {
// 別タンスでも実アカウントなら検索APIでステータスIDを変換できる
other_account_list.add(a)
}
}
// 同タンスのアカウントがないなら、疑似アカウントで開く選択肢
if (local_account_list.isEmpty()) {
if (status_id_original != null) {
dialog.addAction(
activity.getString(R.string.open_in_pseudo_account, "?@${host_original.pretty}")
) {
addPseudoAccount(activity, host_original) { sa ->
conversationLocal(activity, pos, sa, status_id_original)
}
}
} else {
dialog.addAction(
activity.getString(R.string.open_in_pseudo_account, "?@${host_original.pretty}")
) {
addPseudoAccount(activity, host_original) { sa ->
conversationRemote(activity, pos, sa, url)
}
}
}
}
// ローカルアカウント
if (status_id_original != null) {
SavedAccount.sort(local_account_list)
for (a in local_account_list) {
dialog.addAction(
AcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) { conversationLocal(activity, pos, a, status_id_original) }
}
}
// アクセスしたアカウント
if (status_id_access != null) {
SavedAccount.sort(access_account_list)
for (a in access_account_list) {
dialog.addAction(
AcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) { conversationLocal(activity, pos, a, status_id_access) }
}
}
// その他の実アカウント
SavedAccount.sort(other_account_list)
for (a in other_account_list) {
dialog.addAction(
AcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) { conversationRemote(activity, pos, a, url) }
}
dialog.show(activity, activity.getString(R.string.open_status_from))
}
private fun conversationRemote(
activity: ActMain, pos: Int, access_info: SavedAccount, remote_status_url: String
) {
TootTaskRunner(activity)
.progressPrefix(activity.getString(R.string.progress_synchronize_toot))
.run(access_info, object : TootTask {
var local_status_id: EntityId? = null
override suspend fun background(client: TootApiClient): TootApiResult? =
if (access_info.isPseudo) {
// 疑似アカウントではURLからIDを取得するのにHTMLと正規表現を使う
val result = client.getHttp(remote_status_url)
val string = result?.string
if (string != null) {
try {
val m = reDetailedStatusTime.matcher(string)
if (m.find()) {
local_status_id = EntityId(m.groupEx(1)!!)
}
} catch (ex: Throwable) {
log.e(ex, "openStatusRemote: can't parse status id from HTML data.")
}
if (result.error == null && local_status_id == null) {
result.setError(activity.getString(R.string.status_id_conversion_failed))
}
}
result
} else {
val (result, status) = client.syncStatus(access_info, remote_status_url)
if (status != null) {
local_status_id = status.id
log.d("status id conversion %s => %s", remote_status_url, status.id)
}
result
}
override suspend fun handleResult(result: TootApiResult?) {
if (result == null) return // cancelled.
val local_status_id = this.local_status_id
if (local_status_id != null) {
conversationLocal(activity, pos, access_info, local_status_id)
} else {
activity.showToast(true, result.error)
}
}
})
}
// tootsearch APIは投稿の返信元を示すreplyの情報がない。
// in_reply_to_idを参照するしかない
// ところがtootsearchでは投稿をどのタンスから読んだか分からないので、IDは全面的に信用できない。
// 疑似ではないアカウントを選んだ後に表示中の投稿を検索APIで調べて、そのリプライのIDを取得しなおす
fun showReplyTootsearch(
activity: ActMain,
pos: Int,
statusArg: TootStatus?
) {
statusArg ?: return
// step2: 選択したアカウントで投稿を検索して返信元の投稿のIDを調べる
fun step2(a: SavedAccount) = TootTaskRunner(activity).run(a, object : TootTask {
var tmp: TootStatus? = null
override suspend fun background(client: TootApiClient): TootApiResult? {
val (result, status) = client.syncStatus(a, statusArg)
this.tmp = status
return result
}
override suspend fun handleResult(result: TootApiResult?) {
result ?: return
val status = tmp
val replyId = status?.in_reply_to_id
when {
status == null -> activity.showToast(true, result.error ?: "?")
replyId == null -> activity.showToast(
true,
"showReplyTootsearch: in_reply_to_id is null"
)
else -> conversationLocal(activity, pos, a, replyId)
}
}
})
// step 1: choose account
val host = statusArg.account.apDomain
val local_account_list = ArrayList<SavedAccount>()
val other_account_list = ArrayList<SavedAccount>()
for (a in SavedAccount.loadAccountList(activity)) {
// 検索APIはログイン必須なので疑似アカウントは使えない
if (a.isPseudo) continue
if (a.matchHost(host)) {
local_account_list.add(a)
} else {
other_account_list.add(a)
}
}
val dialog = ActionsDialog()
SavedAccount.sort(local_account_list)
for (a in local_account_list) {
dialog.addAction(
AcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) { step2(a) }
}
SavedAccount.sort(other_account_list)
for (a in other_account_list) {
dialog.addAction(
AcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) { step2(a) }
}
dialog.show(activity, activity.getString(R.string.open_status_from))
}
////////////////////////////////////////
// profile pin
fun pin(
activity: ActMain, access_info: SavedAccount, status: TootStatus, bSet: Boolean
) {
TootTaskRunner(activity)
.progressPrefix(activity.getString(R.string.profile_pin_progress))
.run(access_info, object : TootTask {
var new_status: TootStatus? = null
override suspend fun background(client: TootApiClient): TootApiResult? {
val result = client.request(
"/api/v1/statuses/${status.id}/${if (bSet) "pin" else "unpin"}",
"".toFormRequestBody().toPost()
)
new_status = TootParser(activity, access_info).status(result?.jsonObject)
return result
}
override suspend fun handleResult(result: TootApiResult?) {
val new_status = this.new_status
when {
result == null -> {
// cancelled.
}
new_status != null -> {
for (column in activity.app_state.columnList) {
if (access_info == column.access_info) {
column.findStatus(
access_info.apDomain,
new_status.id
) { _, status ->
status.pinned = bSet
true
}
}
}
}
else -> activity.showToast(true, result.error)
}
// 結果に関わらず、更新中状態から復帰させる
activity.showColumnMatchAccount(access_info)
}
})
}
/////////////////////////////////////////////////////////////////////////////////
// reply
fun reply(
activity: ActMain,
access_info: SavedAccount,
status: TootStatus,
quote: Boolean = false
) {
ActPost.open(
activity,
ActMain.REQUEST_CODE_POST,
access_info.db_id,
reply_status = status,
quote = quote
)
}
private fun replyRemote(
activity: ActMain,
access_info: SavedAccount,
remote_status_url: String?,
quote: Boolean = false
) {
if (remote_status_url == null || remote_status_url.isEmpty()) return
TootTaskRunner(activity)
.progressPrefix(activity.getString(R.string.progress_synchronize_toot))
.run(access_info, object : TootTask {
var local_status: TootStatus? = null
override suspend fun background(client: TootApiClient): TootApiResult? {
val (result, status) = client.syncStatus(access_info, remote_status_url)
local_status = status
return result
}
override suspend fun handleResult(result: TootApiResult?) {
result ?: return // cancelled.
val ls = local_status
if (ls != null) {
reply(activity, access_info, ls, quote = quote)
} else {
activity.showToast(true, result.error)
}
}
})
}
fun replyFromAnotherAccount(
activity: ActMain,
timeline_account: SavedAccount,
status: TootStatus?,
quote: Boolean = false
) {
status ?: return
val accountCallback: SavedAccountCallback = { ai ->
if (ai.matchHost(status.readerApDomain)) {
// アクセス元ホストが同じならステータスIDを使って返信できる
reply(activity, ai, status, quote = quote)
} else {
// それ以外の場合、ステータスのURLを検索APIに投げることで返信できる
replyRemote(activity, ai, status.url, quote = quote)
}
}
if (quote) {
AccountPicker.pick(
activity,
bAllowPseudo = false,
bAllowMisskey = true,
bAllowMastodon = true,
bAuto = true,
message = activity.getString(R.string.account_picker_quote_toot),
callback = accountCallback
)
} else {
AccountPicker.pick(
activity,
bAllowPseudo = false,
bAuto = false,
message = activity.getString(R.string.account_picker_reply),
accountListArg = makeAccountListNonPseudo(activity, timeline_account.apDomain),
callback = accountCallback
)
}
}
// 投稿画面を開く。初期テキストを指定する
fun redraft(
activity: ActMain,
accessInfo: SavedAccount,
status: TootStatus
) {
activity.post_helper.closeAcctPopup()
if (accessInfo.isMisskey) {
ActPost.open(
activity,
ActMain.REQUEST_CODE_POST,
accessInfo.db_id,
redraft_status = status,
reply_status = status.reply
)
return
}
if (status.in_reply_to_id == null) {
ActPost.open(
activity,
ActMain.REQUEST_CODE_POST,
accessInfo.db_id,
redraft_status = status
)
return
}
TootTaskRunner(activity).run(accessInfo, object : TootTask {
var reply_status: TootStatus? = null
override suspend fun background(client: TootApiClient): TootApiResult? {
val result = client.request("/api/v1/statuses/${status.in_reply_to_id}")
reply_status = TootParser(activity, accessInfo).status(result?.jsonObject)
return result
}
override suspend fun handleResult(result: TootApiResult?) {
if (result == null) return // cancelled.
val reply_status = this.reply_status
if (reply_status != null) {
ActPost.open(
activity,
ActMain.REQUEST_CODE_POST,
accessInfo.db_id,
redraft_status = status,
reply_status = reply_status
)
return
}
val error = result.error ?: "(no information)"
activity.showToast(true, activity.getString(R.string.cant_sync_toot) + " : $error")
}
})
}
////////////////////////////////////////
fun muteConversation(
activity: ActMain, access_info: SavedAccount, status: TootStatus
) {
// toggle change
val bMute = !status.muted
TootTaskRunner(activity).run(access_info, object : TootTask {
var local_status: TootStatus? = null
override suspend fun background(client: TootApiClient): TootApiResult? {
val result = client.request(
"/api/v1/statuses/${status.id}/${if (bMute) "mute" else "unmute"}",
"".toFormRequestBody().toPost()
)
local_status = TootParser(activity, access_info).status(result?.jsonObject)
return result
}
override suspend fun handleResult(result: TootApiResult?) {
result ?: return // cancelled.
val ls = local_status
if (ls != null) {
for (column in activity.app_state.columnList) {
if (access_info == column.access_info) {
column.findStatus(access_info.apDomain, ls.id) { _, status ->
status.muted = bMute
true
}
}
}
activity.showToast(
true,
if (bMute) R.string.mute_succeeded else R.string.unmute_succeeded
)
} else {
activity.showToast(true, result.error)
}
}
})
}
fun reaction(
activity: ActMain,
access_info: SavedAccount,
arg_status: TootStatus,
status_owner_acct: Acct,
nCrossAccountMode: Int,
callback: () -> Unit,
bSet: Boolean = true,
code: String? = null
) {
if (access_info.isPseudo || !access_info.isMisskey) return
// 自分の投稿にはリアクション出来ない
if (access_info.acct == status_owner_acct) {
activity.showToast(false, R.string.it_is_you)
return
}
if (code == null) {
if (!bSet) error("will not happen")
EmojiPicker(activity, access_info, closeOnSelected = true) { name, instance, _, _, _ ->
val item = EmojiMap.shortNameToEmojiInfo[name]
val newCode = if (item == null || instance != null) {
":$name:"
} else {
item.unified
}
reaction(
activity,
access_info,
arg_status,
status_owner_acct,
nCrossAccountMode,
callback,
bSet,
newCode
)
}.show()
return
}
TootTaskRunner(activity, TootTaskRunner.PROGRESS_NONE).run(access_info, object : TootTask {
override suspend fun background(client: TootApiClient): TootApiResult? {
val target_status = if (nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE) {
val (result, status) = client.syncStatus(access_info, arg_status)
status ?: return result
if (status.myReaction != null) {
return TootApiResult(activity.getString(R.string.already_reactioned))
}
status
} else {
// 既に自タンスのステータスがある
arg_status
}
return if (!bSet) {
client.request(
"/api/notes/reactions/delete",
access_info.putMisskeyApiToken().apply {
put("noteId", target_status.id.toString())
}
.toPostRequestBuilder()
)
// 成功すると204 no content
} else {
client.request(
"/api/notes/reactions/create",
access_info.putMisskeyApiToken().apply {
put("noteId", target_status.id.toString())
put("reaction", code)
}
.toPostRequestBuilder()
)
// 成功すると204 no content
}
}
override suspend fun handleResult(result: TootApiResult?) {
result ?: return
val error = result.error
if (error != null) {
activity.showToast(false, error)
return
}
callback()
}
})
}
fun reactionFromAnotherAccount(
activity: ActMain,
timeline_account: SavedAccount,
status: TootStatus?,
code: String? = null
) {
status ?: return
val status_owner = timeline_account.getFullAcct(status.account)
AccountPicker.pick(
activity,
bAllowPseudo = false,
bAllowMisskey = true,
bAllowMastodon = false,
bAuto = false,
message = activity.getString(R.string.account_picker_reaction)
) { action_account ->
reaction(
activity,
action_account,
status,
status_owner,
calcCrossAccountMode(timeline_account, action_account),
activity.reaction_complete_callback,
code = code
)
}
}
fun deleteScheduledPost(
activity: ActMain,
access_info: SavedAccount,
item: TootScheduled,
bConfirmed: Boolean = false,
callback: () -> Unit
) {
if (!bConfirmed) {
DlgConfirm.openSimple(
activity,
activity.getString(R.string.scheduled_status_delete_confirm)
) {
deleteScheduledPost(
activity,
access_info,
item,
bConfirmed = true,
callback = callback
)
}
return
}
TootTaskRunner(activity).run(access_info, object : TootTask {
override suspend fun background(client: TootApiClient): TootApiResult? {
return client.request(
"/api/v1/scheduled_statuses/${item.id}",
Request.Builder().delete()
)
}
override suspend fun handleResult(result: TootApiResult?) {
result ?: return
val error = result.error
if (error != null) {
activity.showToast(false, error)
return
}
callback()
}
})
}
fun editScheduledPost(
activity: ActMain,
access_info: SavedAccount,
item: TootScheduled
) {
TootTaskRunner(activity).run(access_info, object : TootTask {
var reply_status: TootStatus? = null
override suspend fun background(client: TootApiClient): TootApiResult? {
val reply_status_id = item.in_reply_to_id
?: return TootApiResult()
return client.request("/api/v1/statuses/$reply_status_id")?.also { result ->
reply_status = TootParser(activity, access_info).status(result.jsonObject)
}
}
override suspend fun handleResult(result: TootApiResult?) {
result ?: return
val error = result.error
if (error != null) {
activity.showToast(false, error)
return
}
ActPost.open(
activity,
ActMain.REQUEST_CODE_POST,
access_info.db_id,
scheduledStatus = item,
reply_status = reply_status
)
}
})
}
}