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

1526 lines
43 KiB
Kotlin
Raw Normal View History

package jp.juggler.subwaytooter.action
import android.text.SpannableStringBuilder
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.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
2018-12-01 00:02:18 +01:00
import jp.juggler.subwaytooter.util.SavedAccountCallback
import jp.juggler.subwaytooter.util.matchHost
2020-09-29 19:44:56 +02:00
import jp.juggler.subwaytooter.util.openCustomTab
2018-12-02 11:25:00 +01:00
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
2018-01-13 07:15:52 +01:00
AccountPicker.pick(
activity,
bAllowPseudo = false,
bAuto = false,
message = activity.getString(R.string.account_picker_favourite),
accountListArg = makeAccountListNonPseudo(activity, timeline_account.apDomain)
2018-01-13 07:15:52 +01:00
) { 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,
2020-09-29 19:44:56 +02:00
callback : () -> Unit,
bSet : Boolean = true,
bConfirmed : Boolean = false
) {
2020-12-21 03:13:03 +01:00
if(activity.app_state.isBusyFav(access_info, arg_status)) {
2020-09-29 19:44:56 +02:00
activity.showToast(false, R.string.wait_previous_operation)
return
}
// 必要なら確認を出す
2019-12-09 09:05:13 +01:00
if(! bConfirmed && access_info.isMastodon) {
DlgConfirm.open(
activity,
activity.getString(
when(bSet) {
true -> R.string.confirm_favourite_from
else -> R.string.confirm_unfavourite_from
},
2020-02-01 14:26:57 +01:00
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
}
//
2020-12-21 03:13:03 +01:00
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? {
2019-01-16 16:27:37 +01:00
val target_status = if(nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE) {
val (result, status) = client.syncStatus(access_info, arg_status)
2019-01-16 16:27:37 +01:00
status ?: return result
if(status.favourited) {
return TootApiResult(activity.getString(R.string.already_favourited))
}
2019-01-16 16:27:37 +01:00
status
} else {
2019-01-16 16:27:37 +01:00
arg_status
}
2019-01-16 16:27:37 +01:00
return if(access_info.isMisskey) {
client.request(
if(bSet) {
"/api/notes/favorites/create"
} else {
"/api/notes/favorites/delete"
2019-01-16 16:27:37 +01:00
},
access_info.putMisskeyApiToken().apply {
put("noteId", target_status.id.toString())
}
2019-01-16 16:27:37 +01:00
.toPostRequestBuilder()
)?.also { result ->
2019-01-16 16:27:37 +01:00
// 正常レスポンスは 204 no content
// 既にお気に入り済みならエラー文字列に'already favorited' が返る
if(result.response?.code == 204
2019-01-16 16:27:37 +01:00
|| result.error?.contains("already favorited") == true
|| result.error?.contains("already not favorited") == true
) {
// 成功した
new_status = target_status.apply {
2019-01-16 16:27:37 +01:00
favourited = bSet
}
}
}
} else {
2019-01-16 16:27:37 +01:00
client.request(
2018-12-02 11:25:00 +01:00
"/api/v1/statuses/${target_status.id}/${if(bSet) "favourite" else "unfavourite"}",
"".toFormRequestBody().toPost()
)?.also { result ->
2019-01-16 16:27:37 +01:00
new_status = TootParser(activity, access_info).status(result.jsonObject)
}
}
}
override suspend fun handleResult(result : TootApiResult?) {
2020-12-21 03:13:03 +01:00
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
}
}
2020-12-21 03:13:03 +01:00
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
}
}
2020-09-29 19:44:56 +02:00
callback()
}
2020-09-29 19:44:56 +02:00
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,
2020-09-29 19:44:56 +02:00
callback : () -> Unit,
bSet : Boolean = true,
bConfirmed : Boolean = false
) {
2020-12-21 03:13:03 +01:00
if(activity.app_state.isBusyFav(access_info, arg_status)) {
2020-09-29 19:44:56 +02:00
activity.showToast(false, R.string.wait_previous_operation)
return
}
2019-12-08 16:54:25 +01:00
if(access_info.isMisskey) {
2020-09-29 19:44:56 +02:00
activity.showToast(false, R.string.misskey_account_not_supported)
return
}
// 必要なら確認を出す
// ブックマークは解除する時だけ確認する
2019-12-08 16:54:25 +01:00
if(! bConfirmed && ! bSet) {
DlgConfirm.openSimple(
activity,
activity.getString(
R.string.confirm_unbookmark_from,
2020-02-01 14:26:57 +01:00
AcctColor.getNickname(access_info)
)
2019-12-08 16:54:25 +01:00
) {
bookmark(
activity,
access_info,
arg_status,
nCrossAccountMode,
callback,
bSet = bSet,
bConfirmed = true
)
}
return
}
//
2020-12-21 03:13:03 +01:00
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?) {
2020-12-21 03:13:03 +01:00
activity.app_state.resetBusyBookmark(access_info, arg_status)
val new_status = this.new_status
when {
result == null -> {
} // cancelled.
2019-12-08 16:54:25 +01:00
new_status != null -> {
2020-12-21 03:13:03 +01:00
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
}
}
2020-09-29 19:44:56 +02:00
callback()
}
2020-09-29 19:44:56 +02:00
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 &&
2019-12-09 09:05:13 +01:00
status.visibility == TootVisibility.PrivateFollowers
2018-08-25 10:05:50 +02:00
if(isPrivateToot) {
val list = ArrayList<SavedAccount>()
for(a in SavedAccount.loadAccountList(activity)) {
if(a.acct == status_owner) list.add(a)
}
if(list.isEmpty()) {
2020-09-29 19:44:56 +02:00
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),
2020-09-29 19:44:56 +02:00
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),
2020-09-29 19:44:56 +02:00
callback = activity.boost_complete_callback
)
}
}
}
fun boost(
2018-01-13 07:15:52 +01:00
activity : ActMain,
access_info : SavedAccount,
arg_status : TootStatus,
status_owner : Acct,
2018-01-13 07:15:52 +01:00
nCrossAccountMode : Int,
bSet : Boolean = true,
bConfirmed : Boolean = false,
2020-09-29 19:44:56 +02:00
visibility : TootVisibility? = null,
callback : () -> Unit
) {
// アカウントからステータスにブースト操作を行っているなら、何もしない
2020-12-21 03:13:03 +01:00
if(activity.app_state.isBusyBoost(access_info, arg_status)) {
2020-09-29 19:44:56 +02:00
activity.showToast(false, R.string.wait_previous_operation)
return
}
// Mastodonは非公開トゥートをブーストできるのは本人だけ
2019-12-09 09:05:13 +01:00
val isPrivateToot = access_info.isMastodon &&
arg_status.visibility == TootVisibility.PrivateFollowers
if(isPrivateToot && access_info.acct != status_owner) {
2020-09-29 19:44:56 +02:00
activity.showToast(false, R.string.boost_private_toot_not_allowed)
return
}
// 必要なら確認を出す
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
},
2020-02-01 14:26:57 +01:00
AcctColor.getNickname(access_info)
),
object : DlgConfirm.Callback {
override fun onOK() {
boost(
activity,
access_info,
arg_status,
status_owner,
nCrossAccountMode,
bSet = bSet,
bConfirmed = true,
2020-09-29 19:44:56 +02:00
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
}
2020-12-21 03:13:03 +01:00
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)
2019-01-16 16:27:37 +01:00
val target_status = if(nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE) {
val (result, status) = client.syncStatus(access_info, arg_status)
2019-01-16 16:27:37 +01:00
if(status == null) return result
if(status.reblogged) {
return TootApiResult(activity.getString(R.string.already_boosted))
}
2019-01-16 16:27:37 +01:00
status
} else {
// 既に自タンスのステータスがある
2019-01-16 16:27:37 +01:00
arg_status
}
if(access_info.isMisskey) {
return if(! bSet) {
val myRenoteId = target_status.myRenoteId
?: return TootApiResult("missing renote id.")
2019-12-08 16:54:25 +01:00
client.request(
"/api/notes/delete",
access_info.putMisskeyApiToken().apply {
put("noteId", myRenoteId.toString())
put("renoteId", target_status.id.toString())
}
.toPostRequestBuilder()
)
2019-12-08 16:54:25 +01:00
?.also {
if(it.response?.code == 204)
unrenoteId = myRenoteId
}
} else {
client.request(
2019-12-08 16:54:25 +01:00
"/api/notes/create",
access_info.putMisskeyApiToken().apply {
put("renoteId", target_status.id.toString())
}
2019-12-08 16:54:25 +01:00
.toPostRequestBuilder()
)
?.also { result ->
val jsonObject = result.jsonObject
if(jsonObject != null) {
val outerStatus = parser.status(
2020-01-08 04:23:45 +01:00
jsonObject.jsonObject("createdNote")
?: jsonObject
)
val innerStatus = outerStatus?.reblog ?: outerStatus
2019-12-08 16:54:25 +01:00
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()
2019-01-16 16:27:37 +01:00
val result = client.request(
2018-12-02 10:35:04 +01:00
"/api/v1/statuses/${target_status.id}/${if(bSet) "reblog" else "unreblog"}",
b
)
2018-12-02 10:35:04 +01:00
// 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?) {
2020-12-21 03:13:03 +01:00
activity.app_state.resetBusyBoost(access_info, arg_status)
val unrenoteId = this.unrenoteId
val new_status = this.new_status
when {
2019-09-23 19:14:50 +02:00
// cancelled.
result == null -> {
2019-09-23 19:14:50 +02:00
}
// Misskeyでunrenoteに成功した
2019-12-08 16:54:25 +01:00
unrenoteId != null -> {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはならない
2019-12-08 16:54:25 +01:00
val count = max(0, (arg_status.reblogs_count ?: 1) - 1)
2020-12-21 03:13:03 +01:00
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
}
}
2020-09-29 19:44:56 +02:00
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
}
}
2020-12-21 03:13:03 +01:00
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) {
2019-12-08 16:54:25 +01:00
// 同アカウントならreblog状態を変化させる
2019-12-08 16:54:25 +01:00
when {
2019-12-09 09:05:13 +01:00
access_info.isMastodon ->
2019-12-08 16:54:25 +01:00
status.reblogged = new_status.reblogged
bSet && status.myRenoteId == null -> {
status.myRenoteId = new_status.myRenoteId
status.reblogged = true
}
}
2019-12-08 16:54:25 +01:00
// Misskey のunrenote時はここを通らない
}
true
}
}
2020-09-29 19:44:56 +02:00
callback()
}
2020-09-29 19:44:56 +02:00
else -> activity.showToast(true, result.error)
}
// 結果に関わらず、更新中状態から復帰させる
activity.showColumnMatchAccount(access_info)
}
})
// ブースト表示を更新中にする
activity.showColumnMatchAccount(access_info)
// misskeyは非公開トゥートをブーストできないっぽい
}
2019-09-23 19:14:50 +02:00
fun delete(activity : ActMain, access_info : SavedAccount, status_id : EntityId) {
TootTaskRunner(activity).run(access_info, object : TootTask {
override suspend fun background(client : TootApiClient) : TootApiResult? {
2018-11-02 01:39:32 +01:00
return if(access_info.isMisskey) {
2019-12-09 09:05:13 +01:00
client.request(
"/api/notes/delete",
access_info.putMisskeyApiToken().apply {
put("noteId", status_id)
}
2019-12-09 09:05:13 +01:00
.toPostRequestBuilder()
)
2018-11-02 01:39:32 +01:00
// 204 no content
} else {
2019-12-09 09:05:13 +01:00
client.request(
"/api/v1/statuses/$status_id",
Request.Builder().delete()
)
2018-11-02 01:39:32 +01:00
}
}
override suspend fun handleResult(result : TootApiResult?) {
if(result == null) return // cancelled.
if(result.jsonObject != null) {
2020-09-29 19:44:56 +02:00
activity.showToast(false, R.string.delete_succeeded)
2020-12-21 03:13:03 +01:00
for(column in activity.app_state.columnList) {
column.onStatusRemoved(access_info.apDomain, status_id)
}
} else {
2020-09-29 19:44:56 +02:00
activity.showToast(false, result.error)
}
}
})
}
/////////////////////////////////////////////////////////////////////////////////////
// open conversation
internal fun clearConversationUnread(
activity : ActMain,
access_info : SavedAccount,
conversationSummary : TootConversationSummary?
2018-11-02 01:39:32 +01:00
) {
conversationSummary ?: return
TootTaskRunner(activity, progress_style = TootTaskRunner.PROGRESS_NONE)
.run(access_info, object : TootTask {
override suspend fun background(client : TootApiClient) : TootApiResult? {
return client.request(
2018-12-02 10:35:04 +01:00
"/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
) {
2019-08-23 01:49:20 +02:00
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
2018-01-11 10:31:25 +01:00
if(url == null || url.isEmpty()) {
// URLが不明なトゥートというのはreblogの外側のアレ
return
}
when {
// 検索サービスではステータスTLをどのタンスから読んだのか分からない
status.readerApDomain == null ->
2018-08-25 10:05:50 +02:00
conversationOtherInstance(
activity, pos, url, TootStatus.validStatusId(status.id)
2018-08-25 10:05:50 +02:00
?: TootStatus.findStatusIdFromUri(
status.uri,
2019-01-28 19:02:09 +01:00
status.url
2018-08-25 10:05:50 +02:00
)
)
// TLアカウントのホストとトゥートのアカウントのホストが同じ
status.originalApDomain == status.readerApDomain ->
2018-08-25 10:05:50 +02:00
conversationOtherInstance(
activity, pos, url, TootStatus.validStatusId(status.id)
2018-08-25 10:05:50 +02:00
?: TootStatus.findStatusIdFromUri(
status.uri,
2019-01-28 19:02:09 +01:00
status.url
2018-08-25 10:05:50 +02:00
)
)
else -> {
2018-01-11 10:31:25 +01:00
// トゥートを取得したタンスと投稿元タンスが異なる場合
// status.id はトゥートを取得したタンスでのIDである
// 投稿元タンスでのIDはuriやURLから調べる
// pleromaではIDがuuidなので失敗する(その時はURLを検索してIDを見つける)
conversationOtherInstance(
activity, pos, url, TootStatus.findStatusIdFromUri(
2018-08-25 10:05:50 +02:00
status.uri,
2019-01-28 19:02:09 +01:00
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 ?: "")
// 選択肢:ブラウザで表示する
2020-09-29 19:44:56 +02:00
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
2020-02-01 19:28:16 +01:00
if(status_id_original != null && a.matchHost(host_original)) {
// アクセス情報ステータスID でアクセスできるなら
// 同タンスのアカウントならステータスIDの変換なしに表示できる
local_account_list.add(a)
2020-02-01 19:28:16 +01:00
} 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) {
2018-01-11 10:31:25 +01:00
// 疑似アカウントではURLからIDを取得するのにHTMLと正規表現を使う
2019-01-16 16:27:37 +01:00
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.")
}
2019-01-16 16:27:37 +01:00
if(result.error == null && local_status_id == null) {
result.setError(activity.getString(R.string.status_id_conversion_failed))
}
}
2019-01-16 16:27:37 +01:00
result
} else {
val (result, status) = client.syncStatus(access_info, remote_status_url)
if(status != null) {
2019-01-16 16:27:37 +01:00
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 {
2020-09-29 19:44:56 +02:00
activity.showToast(true, result.error)
}
}
})
}
2019-09-23 19:14:50 +02:00
// tootsearch APIは投稿の返信元を示すreplyの情報がない。
// in_reply_to_idを参照するしかない
// ところがtootsearchでは投稿をどのタンスから読んだか分からないので、IDは全面的に信用できない。
// 疑似ではないアカウントを選んだ後に表示中の投稿を検索APIで調べて、そのリプライのIDを取得しなおす
fun showReplyTootsearch(
activity : ActMain,
pos : Int,
2019-09-23 19:08:38 +02:00
statusArg : TootStatus?
) {
2019-09-23 19:08:38 +02:00
statusArg ?: return
// step2: 選択したアカウントで投稿を検索して返信元の投稿のIDを調べる
fun step2(a : SavedAccount) = TootTaskRunner(activity).run(a, object : TootTask {
2019-09-23 19:08:38 +02:00
var tmp : TootStatus? = null
override suspend fun background(client : TootApiClient) : TootApiResult? {
2019-09-23 19:08:38 +02:00
val (result, status) = client.syncStatus(a, statusArg)
this.tmp = status
return result
}
override suspend fun handleResult(result : TootApiResult?) {
2019-09-23 19:08:38 +02:00
result ?: return
val status = tmp
val replyId = status?.in_reply_to_id
when {
2020-09-29 19:44:56 +02:00
status == null -> activity.showToast(true, result.error ?: "?")
replyId == null -> activity.showToast(
true,
2019-09-23 19:08:38 +02:00
"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
2020-02-01 19:28:16 +01:00
if(a.matchHost(host)) {
local_account_list.add(a)
} else {
other_account_list.add(a)
}
}
2019-09-23 19:08:38 +02:00
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? {
2018-12-02 10:35:04 +01:00
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 -> {
2020-12-21 03:13:03 +01:00
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
}
}
}
}
2020-09-29 19:44:56 +02:00
else -> activity.showToast(true, result.error)
}
// 結果に関わらず、更新中状態から復帰させる
activity.showColumnMatchAccount(access_info)
}
})
}
/////////////////////////////////////////////////////////////////////////////////
// reply
fun reply(
activity : ActMain,
access_info : SavedAccount,
status : TootStatus,
quote : Boolean = false
) {
2018-08-31 12:29:34 +02:00
ActPost.open(
activity,
ActMain.REQUEST_CODE_POST,
access_info.db_id,
reply_status = status,
quote = quote
2018-08-31 12:29:34 +02:00
)
}
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)
2019-01-16 16:27:37 +01:00
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 {
2020-09-29 19:44:56 +02:00
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
)
}
}
2018-06-23 04:43:18 +02:00
// 投稿画面を開く。初期テキストを指定する
fun redraft(
activity : ActMain,
accessInfo : SavedAccount,
2018-06-23 04:43:18 +02:00
status : TootStatus
) {
activity.post_helper.closeAcctPopup()
2018-08-31 12:29:34 +02:00
2018-11-02 01:39:32 +01:00
if(accessInfo.isMisskey) {
ActPost.open(
activity,
ActMain.REQUEST_CODE_POST,
accessInfo.db_id,
redraft_status = status,
reply_status = status.reply
)
2018-08-31 12:29:34 +02:00
return
}
if(status.in_reply_to_id == null) {
2018-11-02 01:39:32 +01:00
ActPost.open(
activity,
ActMain.REQUEST_CODE_POST,
accessInfo.db_id,
redraft_status = status
)
2018-06-23 04:43:18 +02:00
return
}
TootTaskRunner(activity).run(accessInfo, object : TootTask {
var reply_status : TootStatus? = null
override suspend fun background(client : TootApiClient) : TootApiResult? {
2018-06-23 04:43:18 +02:00
val result = client.request("/api/v1/statuses/${status.in_reply_to_id}")
reply_status = TootParser(activity, accessInfo).status(result?.jsonObject)
2018-06-23 04:43:18 +02:00
return result
}
override suspend fun handleResult(result : TootApiResult?) {
2018-06-23 04:43:18 +02:00
if(result == null) return // cancelled.
val reply_status = this.reply_status
if(reply_status != null) {
2018-08-31 12:29:34 +02:00
ActPost.open(
activity,
ActMain.REQUEST_CODE_POST,
accessInfo.db_id,
2018-08-31 12:29:34 +02:00
redraft_status = status,
reply_status = reply_status
)
2018-06-23 04:43:18 +02:00
return
}
val error = result.error ?: "(no information)"
2020-09-29 19:44:56 +02:00
activity.showToast(true, activity.getString(R.string.cant_sync_toot) + " : $error")
2018-06-23 04:43:18 +02:00
}
})
}
////////////////////////////////////////
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(
2018-12-02 10:35:04 +01:00
"/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) {
2020-12-21 03:13:03 +01:00
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
}
}
}
2020-09-29 19:44:56 +02:00
activity.showToast(
true,
if(bMute) R.string.mute_succeeded else R.string.unmute_succeeded
)
} else {
2020-09-29 19:44:56 +02:00
activity.showToast(true, result.error)
}
}
})
}
fun reaction(
activity : ActMain,
access_info : SavedAccount,
arg_status : TootStatus,
status_owner_acct : Acct,
nCrossAccountMode : Int,
2020-09-29 19:44:56 +02:00
callback : () -> Unit,
bSet : Boolean = true,
code : String? = null
) {
if(access_info.isPseudo || ! access_info.isMisskey) return
// 自分の投稿にはリアクション出来ない
if(access_info.acct == status_owner_acct) {
2020-09-29 19:44:56 +02:00
activity.showToast(false, R.string.it_is_you)
return
}
if(bSet && code == null) {
val ad = ActionsDialog()
for(mr in MisskeyReaction.values()) {
if(! mr.showOnPicker) continue
val newCode = mr.shortcode
val sb = SpannableStringBuilder()
.appendMisskeyReaction(activity, mr.emojiUtf16, " ")
.append(' ')
.append(mr.shortcode)
ad.addAction(sb) {
reaction(
activity,
access_info,
arg_status,
status_owner_acct,
nCrossAccountMode,
callback,
bSet,
newCode
)
}
}
ad.show(activity)
return
}
TootTaskRunner(activity, TootTaskRunner.PROGRESS_NONE).run(access_info, object : TootTask {
override suspend fun background(client : TootApiClient) : TootApiResult? {
2019-01-16 16:27:37 +01:00
val target_status = if(nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE) {
val (result, status) = client.syncStatus(access_info, arg_status)
status ?: return result
2019-01-16 16:27:37 +01:00
if(status.myReaction != null) {
return TootApiResult(activity.getString(R.string.already_reactioned))
}
2019-01-16 16:27:37 +01:00
status
} else {
// 既に自タンスのステータスがある
2019-01-16 16:27:37 +01:00
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) {
2020-09-29 19:44:56 +02:00
activity.showToast(false, error)
return
}
2020-09-29 19:44:56 +02:00
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(
2019-01-06 15:55:25 +01:00
activity : ActMain,
access_info : SavedAccount,
item : TootScheduled,
bConfirmed : Boolean = false,
callback : () -> Unit
) {
2019-01-06 15:55:25 +01:00
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) {
2020-09-29 19:44:56 +02:00
activity.showToast(false, error)
return
}
callback()
}
})
}
2019-01-06 15:55:25 +01:00
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? {
2019-01-06 15:55:25 +01:00
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?) {
2019-01-06 15:55:25 +01:00
result ?: return
val error = result.error
if(error != null) {
2020-09-29 19:44:56 +02:00
activity.showToast(false, error)
2019-01-06 15:55:25 +01:00
return
}
ActPost.open(
activity,
ActMain.REQUEST_CODE_POST,
access_info.db_id,
scheduledStatus = item,
reply_status = reply_status
)
}
})
}
}