リアクション時に確認ダイアログを表示する
This commit is contained in:
parent
fd452c0974
commit
99fe09f1bf
|
@ -129,6 +129,7 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
private lateinit var cbConfirmUnboost: CheckBox
|
||||
private lateinit var cbConfirmUnfavourite: CheckBox
|
||||
private lateinit var cbConfirmToot: CheckBox
|
||||
private lateinit var cbConfirmReaction: CheckBox
|
||||
|
||||
private lateinit var tvUserCustom: TextView
|
||||
private lateinit var btnUserCustom: View
|
||||
|
@ -236,7 +237,7 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
}
|
||||
} else {
|
||||
// 失敗したら DBからデータを削除
|
||||
state.uriCameraImage?.let{
|
||||
state.uriCameraImage?.let {
|
||||
contentResolver.delete(it, null, null)
|
||||
}
|
||||
state.uriCameraImage = null
|
||||
|
@ -283,9 +284,9 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
|
||||
val encodedState = Json.encodeToString(state)
|
||||
log.d("encodedState=$encodedState")
|
||||
val decodedState :State = Json.decodeFromString(encodedState)
|
||||
val decodedState: State = Json.decodeFromString(encodedState)
|
||||
log.d("encodedState.uriCameraImage=${decodedState.uriCameraImage}")
|
||||
outState.putString(ACTIVITY_STATE,encodedState )
|
||||
outState.putString(ACTIVITY_STATE, encodedState)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
@ -342,6 +343,7 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
cbConfirmUnboost = findViewById(R.id.cbConfirmUnboost)
|
||||
cbConfirmUnfavourite = findViewById(R.id.cbConfirmUnfavourite)
|
||||
cbConfirmToot = findViewById(R.id.cbConfirmToot)
|
||||
cbConfirmReaction = findViewById(R.id.cbConfirmReaction)
|
||||
|
||||
tvUserCustom = findViewById(R.id.tvUserCustom)
|
||||
btnUserCustom = findViewById(R.id.btnUserCustom)
|
||||
|
@ -423,66 +425,10 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
).map { findViewById(it) }
|
||||
|
||||
btnFields = findViewById(R.id.btnFields)
|
||||
|
||||
|
||||
|
||||
|
||||
btnOpenBrowser.setOnClickListener(this)
|
||||
btnPushSubscription.setOnClickListener(this)
|
||||
btnPushSubscriptionNotForce.setOnClickListener(this)
|
||||
btnResetNotificationTracking.setOnClickListener(this)
|
||||
btnAccessToken.setOnClickListener(this)
|
||||
btnInputAccessToken.setOnClickListener(this)
|
||||
btnAccountRemove.setOnClickListener(this)
|
||||
btnLoadPreference.setOnClickListener(this)
|
||||
btnVisibility.setOnClickListener(this)
|
||||
btnUserCustom.setOnClickListener(this)
|
||||
btnProfileAvatar.setOnClickListener(this)
|
||||
btnProfileHeader.setOnClickListener(this)
|
||||
btnDisplayName.setOnClickListener(this)
|
||||
btnNote.setOnClickListener(this)
|
||||
btnFields.setOnClickListener(this)
|
||||
|
||||
swNSFWOpen.setOnCheckedChangeListener(this)
|
||||
swDontShowTimeout.setOnCheckedChangeListener(this)
|
||||
swExpandCW.setOnCheckedChangeListener(this)
|
||||
swMarkSensitive.setOnCheckedChangeListener(this)
|
||||
cbNotificationMention.setOnCheckedChangeListener(this)
|
||||
cbNotificationBoost.setOnCheckedChangeListener(this)
|
||||
cbNotificationFavourite.setOnCheckedChangeListener(this)
|
||||
cbNotificationFollow.setOnCheckedChangeListener(this)
|
||||
cbNotificationFollowRequest.setOnCheckedChangeListener(this)
|
||||
cbNotificationReaction.setOnCheckedChangeListener(this)
|
||||
cbNotificationVote.setOnCheckedChangeListener(this)
|
||||
cbNotificationPost.setOnCheckedChangeListener(this)
|
||||
|
||||
cbLocked.setOnCheckedChangeListener(this)
|
||||
|
||||
|
||||
cbConfirmFollow.setOnCheckedChangeListener(this)
|
||||
cbConfirmFollowLockedUser.setOnCheckedChangeListener(this)
|
||||
cbConfirmUnfollow.setOnCheckedChangeListener(this)
|
||||
cbConfirmBoost.setOnCheckedChangeListener(this)
|
||||
cbConfirmFavourite.setOnCheckedChangeListener(this)
|
||||
cbConfirmUnboost.setOnCheckedChangeListener(this)
|
||||
cbConfirmUnfavourite.setOnCheckedChangeListener(this)
|
||||
cbConfirmToot.setOnCheckedChangeListener(this)
|
||||
|
||||
btnNotificationSoundEdit = findViewById(R.id.btnNotificationSoundEdit)
|
||||
btnNotificationSoundReset = findViewById(R.id.btnNotificationSoundReset)
|
||||
btnNotificationSoundEdit.setOnClickListener(this)
|
||||
btnNotificationSoundReset.setOnClickListener(this)
|
||||
|
||||
btnNotificationStyleEdit = findViewById(R.id.btnNotificationStyleEdit)
|
||||
btnNotificationStyleEditReply = findViewById(R.id.btnNotificationStyleEditReply)
|
||||
btnNotificationStyleEdit.setOnClickListener(this)
|
||||
btnNotificationStyleEditReply.setOnClickListener(this)
|
||||
|
||||
|
||||
spResizeImage.onItemSelectedListener = this
|
||||
|
||||
spPushPolicy.onItemSelectedListener = this
|
||||
|
||||
btnNotificationStyleEditReply.vg(Pref.bpSeparateReplyNotificationGroup(pref))
|
||||
|
||||
name_invalidator = NetworkEmojiInvalidator(handler, etDisplayName)
|
||||
|
@ -544,6 +490,57 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
}
|
||||
})
|
||||
|
||||
arrayOf(
|
||||
btnOpenBrowser,
|
||||
btnPushSubscription,
|
||||
btnPushSubscriptionNotForce,
|
||||
btnResetNotificationTracking,
|
||||
btnAccessToken,
|
||||
btnInputAccessToken,
|
||||
btnAccountRemove,
|
||||
btnLoadPreference,
|
||||
btnVisibility,
|
||||
btnUserCustom,
|
||||
btnProfileAvatar,
|
||||
btnProfileHeader,
|
||||
btnDisplayName,
|
||||
btnNote,
|
||||
btnFields,
|
||||
btnNotificationSoundEdit,
|
||||
btnNotificationSoundReset,
|
||||
btnNotificationStyleEdit,
|
||||
btnNotificationStyleEditReply,
|
||||
).forEach { it.setOnClickListener(this) }
|
||||
|
||||
arrayOf(
|
||||
swNSFWOpen,
|
||||
swDontShowTimeout,
|
||||
swExpandCW,
|
||||
swMarkSensitive,
|
||||
cbNotificationMention,
|
||||
cbNotificationBoost,
|
||||
cbNotificationFavourite,
|
||||
cbNotificationFollow,
|
||||
cbNotificationFollowRequest,
|
||||
cbNotificationReaction,
|
||||
cbNotificationVote,
|
||||
cbNotificationPost,
|
||||
cbLocked,
|
||||
cbConfirmFollow,
|
||||
cbConfirmFollowLockedUser,
|
||||
cbConfirmUnfollow,
|
||||
cbConfirmBoost,
|
||||
cbConfirmFavourite,
|
||||
cbConfirmUnboost,
|
||||
cbConfirmUnfavourite,
|
||||
cbConfirmToot,
|
||||
cbConfirmReaction,
|
||||
).forEach { it.setOnCheckedChangeListener(this) }
|
||||
|
||||
arrayOf(
|
||||
spResizeImage,
|
||||
spPushPolicy,
|
||||
).forEach { it.onItemSelectedListener = this }
|
||||
}
|
||||
|
||||
private fun EditText.parseInt(): Int? {
|
||||
|
@ -588,6 +585,7 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
|
||||
|
||||
cbConfirmToot.isChecked = a.confirm_post
|
||||
cbConfirmReaction.isChecked = a.confirm_reaction
|
||||
|
||||
notification_sound_uri = a.sound_uri
|
||||
|
||||
|
@ -597,34 +595,44 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
loading = false
|
||||
|
||||
val enabled = !a.isPseudo
|
||||
btnAccessToken.isEnabledAlpha = enabled
|
||||
btnInputAccessToken.isEnabledAlpha = enabled
|
||||
btnVisibility.isEnabledAlpha = enabled
|
||||
btnPushSubscription.isEnabledAlpha = enabled
|
||||
btnPushSubscriptionNotForce.isEnabledAlpha = enabled
|
||||
btnResetNotificationTracking.isEnabledAlpha = enabled
|
||||
btnNotificationSoundEdit.isEnabledAlpha = Build.VERSION.SDK_INT < 26 && enabled
|
||||
btnNotificationSoundReset.isEnabledAlpha = Build.VERSION.SDK_INT < 26 && enabled
|
||||
btnNotificationStyleEdit.isEnabledAlpha = Build.VERSION.SDK_INT >= 26 && enabled
|
||||
btnNotificationStyleEditReply.isEnabledAlpha = Build.VERSION.SDK_INT >= 26 && enabled
|
||||
|
||||
cbNotificationMention.isEnabledAlpha = enabled
|
||||
cbNotificationBoost.isEnabledAlpha = enabled
|
||||
cbNotificationFavourite.isEnabledAlpha = enabled
|
||||
cbNotificationFollow.isEnabledAlpha = enabled
|
||||
cbNotificationFollowRequest.isEnabledAlpha = enabled
|
||||
cbNotificationReaction.isEnabledAlpha = enabled
|
||||
cbNotificationVote.isEnabledAlpha = enabled
|
||||
cbNotificationPost.isEnabledAlpha = enabled
|
||||
arrayOf(
|
||||
btnAccessToken,
|
||||
btnInputAccessToken,
|
||||
btnVisibility,
|
||||
btnPushSubscription,
|
||||
btnPushSubscriptionNotForce,
|
||||
btnResetNotificationTracking,
|
||||
cbNotificationMention,
|
||||
cbNotificationBoost,
|
||||
cbNotificationFavourite,
|
||||
cbNotificationFollow,
|
||||
cbNotificationFollowRequest,
|
||||
cbNotificationReaction,
|
||||
cbNotificationVote,
|
||||
cbNotificationPost,
|
||||
cbConfirmFollow,
|
||||
cbConfirmFollowLockedUser,
|
||||
cbConfirmUnfollow,
|
||||
cbConfirmBoost,
|
||||
cbConfirmFavourite,
|
||||
cbConfirmUnboost,
|
||||
cbConfirmUnfavourite,
|
||||
cbConfirmToot,
|
||||
cbConfirmReaction,
|
||||
).forEach { it.isEnabledAlpha = enabled }
|
||||
|
||||
cbConfirmFollow.isEnabledAlpha = enabled
|
||||
cbConfirmFollowLockedUser.isEnabledAlpha = enabled
|
||||
cbConfirmUnfollow.isEnabledAlpha = enabled
|
||||
cbConfirmBoost.isEnabledAlpha = enabled
|
||||
cbConfirmFavourite.isEnabledAlpha = enabled
|
||||
cbConfirmUnboost.isEnabledAlpha = enabled
|
||||
cbConfirmUnfavourite.isEnabledAlpha = enabled
|
||||
cbConfirmToot.isEnabledAlpha = enabled
|
||||
val enabledOldNotification = enabled && Build.VERSION.SDK_INT < 26
|
||||
arrayOf(
|
||||
btnNotificationSoundEdit,
|
||||
btnNotificationSoundReset,
|
||||
).forEach { it.isEnabledAlpha = enabledOldNotification }
|
||||
|
||||
val enabledNewNotification = enabled && Build.VERSION.SDK_INT >= 26
|
||||
arrayOf(
|
||||
btnNotificationStyleEdit,
|
||||
btnNotificationStyleEditReply,
|
||||
).forEach { it.isEnabledAlpha = enabledNewNotification }
|
||||
|
||||
val ti = TootInstance.getCached(a.apiHost)
|
||||
if (ti == null) {
|
||||
|
@ -695,6 +703,7 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
account.confirm_unboost = cbConfirmUnboost.isChecked
|
||||
account.confirm_unfavourite = cbConfirmUnfavourite.isChecked
|
||||
account.confirm_post = cbConfirmToot.isChecked
|
||||
account.confirm_reaction = cbConfirmReaction.isChecked
|
||||
account.default_text = etDefaultText.text.toString()
|
||||
|
||||
val num = etMaxTootChars.parseInt()
|
||||
|
@ -1010,13 +1019,15 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
etNote.setText(loadingText)
|
||||
|
||||
// 初期状態では編集不可能
|
||||
btnProfileAvatar.isEnabledAlpha = false
|
||||
btnProfileHeader.isEnabledAlpha = false
|
||||
etDisplayName.isEnabledAlpha = false
|
||||
btnDisplayName.isEnabledAlpha = false
|
||||
etNote.isEnabledAlpha = false
|
||||
btnNote.isEnabledAlpha = false
|
||||
cbLocked.isEnabledAlpha = false
|
||||
arrayOf(
|
||||
btnProfileAvatar,
|
||||
btnProfileHeader,
|
||||
etDisplayName,
|
||||
btnDisplayName,
|
||||
etNote,
|
||||
btnNote,
|
||||
cbLocked,
|
||||
).forEach { it.isEnabledAlpha = false }
|
||||
|
||||
for (et in listEtFieldName) {
|
||||
et.setText(loadingText)
|
||||
|
@ -1132,13 +1143,15 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
cbLocked.isChecked = src.locked
|
||||
|
||||
// 編集可能にする
|
||||
btnProfileAvatar.isEnabledAlpha = true
|
||||
btnProfileHeader.isEnabledAlpha = true
|
||||
etDisplayName.isEnabledAlpha = true
|
||||
btnDisplayName.isEnabledAlpha = true
|
||||
etNote.isEnabledAlpha = true
|
||||
btnNote.isEnabledAlpha = true
|
||||
cbLocked.isEnabledAlpha = true
|
||||
arrayOf(
|
||||
btnProfileAvatar,
|
||||
btnProfileHeader,
|
||||
etDisplayName,
|
||||
btnDisplayName,
|
||||
etNote,
|
||||
btnNote,
|
||||
cbLocked,
|
||||
).forEach { it.isEnabledAlpha = true }
|
||||
|
||||
if (src.source?.fields != null) {
|
||||
val fields = src.source.fields
|
||||
|
|
|
@ -18,7 +18,6 @@ import android.view.*
|
|||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -1542,7 +1541,7 @@ class ActMain : AsyncActivity(), View.OnClickListener,
|
|||
}
|
||||
)
|
||||
|
||||
fun updateColumnStrip() {
|
||||
private fun updateColumnStrip() {
|
||||
llEmpty.vg(app_state.columnCount == 0)
|
||||
|
||||
val iconSize = stripIconSize
|
||||
|
@ -1695,7 +1694,7 @@ class ActMain : AsyncActivity(), View.OnClickListener,
|
|||
val statusInfo = url.findStatusIdFromUrl()
|
||||
if (statusInfo != null) {
|
||||
// ステータスをアプリ内で開く
|
||||
Action_Toot.conversationOtherInstance(
|
||||
Action_Conversation.conversationOtherInstance(
|
||||
this@ActMain,
|
||||
defaultInsertPosition,
|
||||
statusInfo.url,
|
||||
|
@ -2175,7 +2174,7 @@ class ActMain : AsyncActivity(), View.OnClickListener,
|
|||
}
|
||||
|
||||
// アクセストークンの手動入力(更新)
|
||||
fun checkAccessToken2(db_id: Long) {
|
||||
private fun checkAccessToken2(db_id: Long) {
|
||||
|
||||
val sa = SavedAccount.loadAccount(this, db_id) ?: return
|
||||
|
||||
|
|
|
@ -125,8 +125,9 @@ class App1 : Application() {
|
|||
// 2020/9/20 57=>58 UserRelationテーブルに項目追加
|
||||
// 2021/2/10 58=>59 SavedAccountテーブルに項目追加
|
||||
// 2021/5/11 59=>60 SavedAccountテーブルに項目追加
|
||||
// 2021/5/23 60=>61 SavedAccountテーブルに項目追加
|
||||
|
||||
internal const val DB_VERSION = 60
|
||||
internal const val DB_VERSION = 61
|
||||
|
||||
private val tableList = arrayOf(
|
||||
LogData,
|
||||
|
|
|
@ -21,7 +21,6 @@ import jp.juggler.subwaytooter.util.NetworkStateTracker
|
|||
import jp.juggler.subwaytooter.util.PostAttachment
|
||||
import jp.juggler.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.io.IOUtils
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.Gravity
|
||||
|
@ -993,24 +992,24 @@ internal class DlgContextMenu(
|
|||
status
|
||||
)
|
||||
|
||||
R.id.btnReactionAnotherAccount -> Action_Toot.reactionFromAnotherAccount(
|
||||
R.id.btnReactionAnotherAccount -> Action_Reaction.reactionFromAnotherAccount(
|
||||
activity,
|
||||
access_info,
|
||||
status
|
||||
)
|
||||
|
||||
R.id.btnReplyAnotherAccount -> Action_Toot.replyFromAnotherAccount(
|
||||
R.id.btnReplyAnotherAccount -> Action_Reply.replyFromAnotherAccount(
|
||||
activity,
|
||||
access_info,
|
||||
status
|
||||
)
|
||||
R.id.btnQuoteToot -> Action_Toot.replyFromAnotherAccount(
|
||||
R.id.btnQuoteToot -> Action_Reply.replyFromAnotherAccount(
|
||||
activity,
|
||||
access_info,
|
||||
status,
|
||||
quote = true
|
||||
)
|
||||
R.id.btnQuoteTootBT -> Action_Toot.replyFromAnotherAccount(
|
||||
R.id.btnQuoteTootBT -> Action_Reply.replyFromAnotherAccount(
|
||||
activity,
|
||||
access_info,
|
||||
status?.reblogParent,
|
||||
|
@ -1018,7 +1017,7 @@ internal class DlgContextMenu(
|
|||
)
|
||||
|
||||
R.id.btnConversationAnotherAccount -> status?.let { status ->
|
||||
Action_Toot.conversationOtherInstance(activity, pos, status)
|
||||
Action_Conversation.conversationOtherInstance(activity, pos, status)
|
||||
}
|
||||
|
||||
R.id.btnDelete -> status?.let { status ->
|
||||
|
@ -1078,7 +1077,7 @@ internal class DlgContextMenu(
|
|||
}
|
||||
|
||||
R.id.btnConversationMute -> status?.let { status ->
|
||||
Action_Toot.muteConversation(activity, access_info, status)
|
||||
Action_Conversation.muteConversation(activity, access_info, status)
|
||||
}
|
||||
|
||||
R.id.btnProfilePin -> status?.let { status ->
|
||||
|
|
|
@ -37,10 +37,10 @@ fun ItemViewHolder.openConversationSummary() {
|
|||
reset = true
|
||||
)
|
||||
// 未読フラグのクリアをサーバに送る
|
||||
Action_Toot.clearConversationUnread(activity, access_info, cs)
|
||||
Action_Conversation.clearConversationUnread(activity, access_info, cs)
|
||||
}
|
||||
|
||||
Action_Toot.conversation(
|
||||
Action_Conversation.conversation(
|
||||
activity,
|
||||
activity.nextPosition(column),
|
||||
access_info,
|
||||
|
@ -152,17 +152,17 @@ fun ItemViewHolder.onClickImpl(v: View?) {
|
|||
val s = status_reply
|
||||
|
||||
when {
|
||||
s != null -> Action_Toot.conversation(activity, pos, access_info, s)
|
||||
s != null -> Action_Conversation.conversation(activity, pos, access_info, s)
|
||||
|
||||
// tootsearchは返信元のIDを取得するのにひと手間必要
|
||||
column.type == ColumnType.SEARCH_TS ||
|
||||
column.type == ColumnType.SEARCH_NOTESTOCK ->
|
||||
Action_Toot.showReplyTootsearch(activity, pos, status_showing)
|
||||
Action_Reply.showReplyTootsearch(activity, pos, status_showing)
|
||||
|
||||
else -> {
|
||||
val id = status_showing?.in_reply_to_id
|
||||
if (id != null) {
|
||||
Action_Toot.conversationLocal(activity, pos, access_info, id)
|
||||
Action_Conversation.conversationLocal(activity, pos, access_info, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -318,7 +318,7 @@ fun ItemViewHolder.onClickImpl(v: View?) {
|
|||
ivCardImage -> status_showing?.card?.let { card ->
|
||||
val originalStatus = card.originalStatus
|
||||
if (originalStatus != null) {
|
||||
Action_Toot.conversation(
|
||||
Action_Conversation.conversation(
|
||||
activity,
|
||||
activity.nextPosition(column),
|
||||
access_info,
|
||||
|
@ -395,7 +395,7 @@ fun ItemViewHolder.onLongClickImpl(v: View?): Boolean {
|
|||
// tootsearchは返信元のIDを取得するのにひと手間必要
|
||||
column.type == ColumnType.SEARCH_TS ||
|
||||
column.type == ColumnType.SEARCH_NOTESTOCK ->
|
||||
Action_Toot.showReplyTootsearch(
|
||||
Action_Reply.showReplyTootsearch(
|
||||
activity,
|
||||
activity.nextPosition(column),
|
||||
status_showing
|
||||
|
@ -404,7 +404,7 @@ fun ItemViewHolder.onLongClickImpl(v: View?): Boolean {
|
|||
else -> {
|
||||
val id = status_showing?.in_reply_to_id
|
||||
if (id != null) {
|
||||
Action_Toot.conversationLocal(
|
||||
Action_Conversation.conversationLocal(
|
||||
activity,
|
||||
activity.nextPosition(column),
|
||||
access_info,
|
||||
|
@ -440,7 +440,7 @@ fun ItemViewHolder.onLongClickImpl(v: View?): Boolean {
|
|||
return true
|
||||
}
|
||||
|
||||
ivCardImage -> Action_Toot.conversationOtherInstance(
|
||||
ivCardImage -> Action_Conversation.conversationOtherInstance(
|
||||
activity,
|
||||
activity.nextPosition(column),
|
||||
status_showing?.card?.originalStatus
|
||||
|
@ -490,7 +490,7 @@ fun ItemViewHolder.clickMedia(i: Int) {
|
|||
is TootAttachmentMSP -> {
|
||||
// マストドン検索ポータルのデータではmedia_attachmentsが簡略化されている
|
||||
// 会話の流れを表示する
|
||||
Action_Toot.conversationOtherInstance(
|
||||
Action_Conversation.conversationOtherInstance(
|
||||
activity,
|
||||
activity.nextPosition(column),
|
||||
status_showing
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
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.action.Action_Toot
|
||||
import jp.juggler.subwaytooter.action.Action_Reaction
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.TootReaction
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
|
@ -20,8 +19,8 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
|
|||
val myReaction = status.reactionSet?.myReaction
|
||||
val reactionSet = status.reactionSet?.filter { it.count > 0 }
|
||||
|
||||
if (reactionSet?.isEmpty() != false){
|
||||
if( !TootReaction.canReaction(access_info) || !Pref.bpKeepReactionSpace(activity.pref) ) return
|
||||
if (reactionSet?.isEmpty() != false) {
|
||||
if (!TootReaction.canReaction(access_info) || !Pref.bpKeepReactionSpace(activity.pref)) return
|
||||
}
|
||||
|
||||
val density = activity.density
|
||||
|
@ -45,67 +44,7 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
|
|||
}
|
||||
}
|
||||
|
||||
// // +/- ボタン
|
||||
// box.addView(ImageButton(act).also { b ->
|
||||
// b.layoutParams = FlexboxLayout.LayoutParams(
|
||||
// buttonHeight,
|
||||
// buttonHeight
|
||||
// ).apply {
|
||||
// endMargin = marginBetween
|
||||
// }
|
||||
//
|
||||
// b.background = ContextCompat.getDrawable(
|
||||
// activity,
|
||||
// R.drawable.btn_bg_transparent_round6dp
|
||||
// )
|
||||
//
|
||||
// val myReaction = status.reactionSet?.myReaction
|
||||
//
|
||||
// b.contentDescription = activity.getString(
|
||||
// if (myReaction != null)
|
||||
// R.string.reaction_remove
|
||||
// else
|
||||
// R.string.reaction_add
|
||||
// )
|
||||
// b.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
// b.padding = paddingV
|
||||
//
|
||||
// b.setOnClickListener {
|
||||
// if (!TootReaction.canReaction(access_info)) {
|
||||
// Action_Toot.reactionFromAnotherAccount(
|
||||
// activity,
|
||||
// access_info,
|
||||
// status_showing
|
||||
// )
|
||||
// } else if (myReaction != null) {
|
||||
// Action_Toot.removeReaction(activity, column, status)
|
||||
// } else {
|
||||
// Action_Toot.addReaction(activity, column, status)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// b.setOnLongClickListener {
|
||||
// Action_Toot.reactionFromAnotherAccount(
|
||||
// activity,
|
||||
// access_info,
|
||||
// status_showing
|
||||
// )
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// setIconDrawableId(
|
||||
// act,
|
||||
// b,
|
||||
// if (myReaction != null)
|
||||
// R.drawable.ic_remove
|
||||
// else
|
||||
// R.drawable.ic_add,
|
||||
// color = content_color,
|
||||
// alphaMultiplier = Styler.boost_alpha
|
||||
// )
|
||||
// })
|
||||
|
||||
if( reactionSet?.isEmpty() != false){
|
||||
if (reactionSet?.isEmpty() != false) {
|
||||
val v = View(act).apply {
|
||||
layoutParams = FlexboxLayout.LayoutParams(
|
||||
buttonHeight,
|
||||
|
@ -133,7 +72,7 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
|
|||
FlexboxLayout.LayoutParams.WRAP_CONTENT,
|
||||
buttonHeight
|
||||
).apply {
|
||||
if(index >0 ) startMargin = marginBetween
|
||||
if (index > 0) startMargin = marginBetween
|
||||
}
|
||||
minWidthCompat = buttonHeight
|
||||
|
||||
|
@ -162,15 +101,15 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
|
|||
setOnClickListener {
|
||||
val taggedReaction = it.tag as? TootReaction
|
||||
if (taggedReaction == status.reactionSet?.myReaction) {
|
||||
Action_Toot. removeReaction(act, column, status)
|
||||
Action_Reaction.removeReaction(act, column, status)
|
||||
} else {
|
||||
Action_Toot. addReaction(act, column, status, taggedReaction?.name)
|
||||
Action_Reaction.addReaction(act, column, status, taggedReaction?.name, taggedReaction?.static_url)
|
||||
}
|
||||
}
|
||||
|
||||
setOnLongClickListener {
|
||||
val taggedReaction = it.tag as? TootReaction
|
||||
Action_Toot.reactionFromAnotherAccount(
|
||||
Action_Reaction.reactionFromAnotherAccount(
|
||||
this@makeReactionsView.activity,
|
||||
access_info,
|
||||
status_showing,
|
||||
|
|
|
@ -466,11 +466,11 @@ class StatusButtons(
|
|||
reset = true
|
||||
)
|
||||
// 未読フラグのクリアをサーバに送る
|
||||
Action_Toot.clearConversationUnread(activity, access_info, cs)
|
||||
Action_Conversation.clearConversationUnread(activity, access_info, cs)
|
||||
}
|
||||
}
|
||||
|
||||
Action_Toot.conversation(
|
||||
Action_Conversation.conversation(
|
||||
activity,
|
||||
activity.nextPosition(column),
|
||||
access_info,
|
||||
|
@ -480,15 +480,15 @@ class StatusButtons(
|
|||
}
|
||||
|
||||
btnReply -> if (!access_info.isPseudo) {
|
||||
Action_Toot.reply(activity, access_info, status)
|
||||
Action_Reply.reply(activity, access_info, status)
|
||||
} else {
|
||||
Action_Toot.replyFromAnotherAccount(activity, access_info, status)
|
||||
Action_Reply.replyFromAnotherAccount(activity, access_info, status)
|
||||
}
|
||||
|
||||
btnQuote -> if (!access_info.isPseudo) {
|
||||
Action_Toot.reply(activity, access_info, status, quote = true)
|
||||
Action_Reply.reply(activity, access_info, status, quote = true)
|
||||
} else {
|
||||
Action_Toot.replyFromAnotherAccount(activity, access_info, status, quote = true)
|
||||
Action_Reply.replyFromAnotherAccount(activity, access_info, status, quote = true)
|
||||
}
|
||||
|
||||
btnBoost -> {
|
||||
|
@ -564,20 +564,20 @@ class StatusButtons(
|
|||
}
|
||||
}
|
||||
|
||||
btnReaction -> {
|
||||
if (!TootReaction.canReaction(access_info)) {
|
||||
Action_Toot.reactionFromAnotherAccount(
|
||||
btnReaction -> when {
|
||||
!TootReaction.canReaction(access_info) ->
|
||||
Action_Reaction.reactionFromAnotherAccount(
|
||||
activity,
|
||||
access_info,
|
||||
status
|
||||
)
|
||||
} else if (status.reactionSet?.myReaction != null) {
|
||||
Action_Toot.removeReaction(activity, column, status)
|
||||
} else {
|
||||
Action_Toot.addReaction(activity, column, status)
|
||||
}
|
||||
status.reactionSet?.myReaction != null ->
|
||||
Action_Reaction.removeReaction(activity, column, status)
|
||||
else ->
|
||||
Action_Reaction.addReaction(activity, column, status)
|
||||
}
|
||||
|
||||
|
||||
btnFollow2 -> {
|
||||
val accountRef = status.accountRef
|
||||
val account = accountRef.get()
|
||||
|
@ -682,54 +682,26 @@ class StatusButtons(
|
|||
val status = this.status ?: return true
|
||||
|
||||
when (v) {
|
||||
btnConversation -> Action_Toot.conversationOtherInstance(
|
||||
activity, activity.nextPosition(column), status
|
||||
)
|
||||
btnBoost -> Action_Toot.boostFromAnotherAccount(activity, access_info, status)
|
||||
btnFavourite -> Action_Toot.favouriteFromAnotherAccount(activity, access_info, status)
|
||||
btnBookmark -> Action_Toot.bookmarkFromAnotherAccount(activity, access_info, status)
|
||||
|
||||
btnBoost -> Action_Toot.boostFromAnotherAccount(
|
||||
activity, access_info, status
|
||||
)
|
||||
btnReply -> Action_Reply.replyFromAnotherAccount(activity, access_info, status)
|
||||
btnQuote -> Action_Reply.replyFromAnotherAccount(activity, access_info, status, quote = true)
|
||||
|
||||
btnFavourite -> Action_Toot.favouriteFromAnotherAccount(
|
||||
activity, access_info, status
|
||||
)
|
||||
btnReaction -> Action_Reaction.reactionFromAnotherAccount(activity, access_info, status)
|
||||
|
||||
btnBookmark -> Action_Toot.bookmarkFromAnotherAccount(
|
||||
activity, access_info, status
|
||||
)
|
||||
btnConversation -> Action_Conversation.conversationOtherInstance(activity, activity.nextPosition(column), status)
|
||||
|
||||
btnReply -> Action_Toot.replyFromAnotherAccount(
|
||||
activity, access_info, status
|
||||
)
|
||||
|
||||
btnQuote -> Action_Toot.replyFromAnotherAccount(activity, access_info, status, quote = true)
|
||||
|
||||
btnReaction -> Action_Toot.reactionFromAnotherAccount(activity, access_info, status)
|
||||
|
||||
btnFollow2 -> Action_Follow.followFromAnotherAccount(
|
||||
activity, activity.nextPosition(column), access_info, status.account
|
||||
)
|
||||
|
||||
btnTranslate -> shareUrl(
|
||||
status,
|
||||
CustomShareTarget.Translate
|
||||
)
|
||||
|
||||
btnCustomShare1 -> shareUrl(
|
||||
status,
|
||||
CustomShareTarget.CustomShare1
|
||||
)
|
||||
|
||||
btnCustomShare2 -> shareUrl(
|
||||
status,
|
||||
CustomShareTarget.CustomShare2
|
||||
)
|
||||
|
||||
btnCustomShare3 -> shareUrl(
|
||||
status,
|
||||
CustomShareTarget.CustomShare3
|
||||
)
|
||||
btnFollow2 ->
|
||||
Action_Follow.followFromAnotherAccount(
|
||||
activity, activity.nextPosition(column), access_info, status.account
|
||||
)
|
||||
|
||||
btnTranslate -> shareUrl(status, CustomShareTarget.Translate)
|
||||
btnCustomShare1 -> shareUrl(status, CustomShareTarget.CustomShare1)
|
||||
btnCustomShare2 -> shareUrl(status, CustomShareTarget.CustomShare2)
|
||||
btnCustomShare3 -> shareUrl(status, CustomShareTarget.CustomShare3)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -0,0 +1,334 @@
|
|||
package jp.juggler.subwaytooter.action
|
||||
|
||||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.ColumnType
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||
import jp.juggler.subwaytooter.api.entity.Host
|
||||
import jp.juggler.subwaytooter.api.entity.TootConversationSummary
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
||||
import jp.juggler.subwaytooter.findStatus
|
||||
import jp.juggler.subwaytooter.table.AcctColor
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.matchHost
|
||||
import jp.juggler.subwaytooter.util.openCustomTab
|
||||
import jp.juggler.util.*
|
||||
import java.util.ArrayList
|
||||
|
||||
object Action_Conversation {
|
||||
|
||||
private val log= LogCategory("Action_Conversation")
|
||||
|
||||
private val reDetailedStatusTime =
|
||||
"""<a\b[^>]*?\bdetailed-status__datetime\b[^>]*href="https://[^/]+/@[^/]+/([^\s?#/"]+)"""
|
||||
.asciiPattern()
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,491 @@
|
|||
package jp.juggler.subwaytooter.action
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import jp.juggler.subwaytooter.*
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.Host
|
||||
import jp.juggler.subwaytooter.api.entity.TootReaction
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.dialog.AccountPicker
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
||||
import jp.juggler.subwaytooter.dialog.EmojiPicker
|
||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
|
||||
import jp.juggler.subwaytooter.table.AcctColor
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.DecodeOptions
|
||||
import jp.juggler.util.*
|
||||
|
||||
object Action_Reaction {
|
||||
|
||||
// 長押しでない普通のリアクション操作
|
||||
fun addReaction(
|
||||
activity: ActMain,
|
||||
column: Column,
|
||||
status: TootStatus,
|
||||
|
||||
// Unicode絵文字、 :name: :name@.: :name@domain: name name@domain 等
|
||||
codeArg: String? = null,
|
||||
|
||||
urlArg: String? = null,
|
||||
|
||||
// 確認済みなら真
|
||||
isConfirmed: Boolean = false
|
||||
) {
|
||||
if (status.reactionSet?.myReaction != null) {
|
||||
activity.showToast(false, R.string.already_reactioned)
|
||||
return
|
||||
}
|
||||
|
||||
val access_info = column.access_info
|
||||
|
||||
var code = codeArg
|
||||
if (code == null) {
|
||||
EmojiPicker(activity, access_info, closeOnSelected = true) { result ->
|
||||
var newUrl: String? = null
|
||||
val newCode: String = when (val emoji = result.emoji) {
|
||||
is UnicodeEmoji -> emoji.unifiedCode
|
||||
is CustomEmoji -> {
|
||||
newUrl = emoji.static_url
|
||||
if (access_info.isMisskey) {
|
||||
":${emoji.shortcode}:"
|
||||
} else {
|
||||
emoji.shortcode
|
||||
}
|
||||
}
|
||||
}
|
||||
addReaction(activity, column, status, newCode, newUrl)
|
||||
}.show()
|
||||
return
|
||||
}
|
||||
|
||||
if (access_info.isMisskey) {
|
||||
val pair = TootReaction.splitEmojiDomain(code)
|
||||
when (/* val domain = */ pair.second) {
|
||||
null, "", ".", access_info.apDomain.ascii -> {
|
||||
// normalize to local custom emoji
|
||||
code = ":${pair.first}:"
|
||||
}
|
||||
else -> {
|
||||
/*
|
||||
#misskey のリアクションAPIはリモートのカスタム絵文字のコードをフォールバック絵文字に変更して、
|
||||
何の追加情報もなしに204 no contentを返す。
|
||||
よってクライアントはAPI応答からフォールバックが発生したことを認識できず、
|
||||
後から投稿をリロードするまで気が付かない。
|
||||
この挙動はこの挙動は多くのユーザにとって受け入れられないと判断するので、
|
||||
クライアント側で事前にエラー扱いにする方が良い。
|
||||
*/
|
||||
activity.showToast(true, R.string.cant_reaction_remote_custom_emoji, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isConfirmed) {
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
access_info,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg)
|
||||
DlgConfirm.open(
|
||||
activity,
|
||||
activity.getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(access_info)),
|
||||
object : DlgConfirm.Callback {
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = access_info.confirm_reaction
|
||||
set(bv) {
|
||||
access_info.confirm_reaction = bv
|
||||
access_info.saveSetting()
|
||||
}
|
||||
|
||||
override fun onOK() {
|
||||
addReaction(
|
||||
activity,
|
||||
column,
|
||||
status,
|
||||
codeArg = code,
|
||||
urlArg = urlArg,
|
||||
isConfirmed = true
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
TootTaskRunner(activity, progress_style = TootTaskRunner.PROGRESS_NONE).run(access_info,
|
||||
object : TootTask {
|
||||
|
||||
var newStatus: TootStatus? = null
|
||||
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? {
|
||||
return if (access_info.isMisskey) {
|
||||
client.request("/api/notes/reactions/create", access_info.putMisskeyApiToken().apply {
|
||||
put("noteId", status.id.toString())
|
||||
put("reaction", code)
|
||||
}.toPostRequestBuilder())
|
||||
// 成功すると204 no content
|
||||
} else {
|
||||
client.request(
|
||||
"/api/v1/statuses/${status.id}/emoji_reactions/${code.encodePercent("@")}",
|
||||
"".toFormRequestBody().toPut()
|
||||
)
|
||||
// 成功すると新しいステータス
|
||||
?.also { result ->
|
||||
newStatus = 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
|
||||
}
|
||||
when (val resCode = result.response?.code) {
|
||||
in 200 until 300 -> {
|
||||
if (newStatus != null) {
|
||||
activity.app_state.columnList.forEach { column ->
|
||||
if (column.access_info.acct == access_info.acct)
|
||||
column.updateEmojiReactionByApiResponse(newStatus)
|
||||
}
|
||||
} else {
|
||||
if (status.increaseReactionMisskey(code, true, caller = "addReaction")) {
|
||||
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
|
||||
column.fireShowContent(
|
||||
reason = "addReaction complete",
|
||||
reset = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> activity.showToast(false, "HTTP error $resCode")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 長押しでない普通のリアクション操作
|
||||
fun removeReaction(
|
||||
activity: ActMain,
|
||||
column: Column,
|
||||
status: TootStatus,
|
||||
confirmed: Boolean = false
|
||||
) {
|
||||
val access_info = column.access_info
|
||||
|
||||
val myReaction = status.reactionSet?.myReaction
|
||||
|
||||
if (myReaction == null) {
|
||||
activity.showToast(false, R.string.not_reactioned)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!confirmed) {
|
||||
AlertDialog.Builder(activity)
|
||||
.setMessage(activity.getString(R.string.reaction_remove_confirm, myReaction.name))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
removeReaction(activity, column, status, confirmed = true)
|
||||
}
|
||||
.show()
|
||||
return
|
||||
}
|
||||
|
||||
TootTaskRunner(activity, progress_style = TootTaskRunner.PROGRESS_NONE).run(access_info,
|
||||
object : TootTask {
|
||||
|
||||
var newStatus: TootStatus? = null
|
||||
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? =
|
||||
if (access_info.isMisskey) {
|
||||
client.request(
|
||||
"/api/notes/reactions/delete",
|
||||
access_info.putMisskeyApiToken().apply {
|
||||
put("noteId", status.id.toString())
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
)
|
||||
// 成功すると204 no content
|
||||
} else {
|
||||
client.request(
|
||||
"/api/v1/statuses/${status.id}/emoji_unreaction",
|
||||
"".toFormRequestBody().toPost()
|
||||
)
|
||||
// 成功すると新しいステータス
|
||||
?.also { result ->
|
||||
newStatus = TootParser(activity, access_info).status(result.jsonObject)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
|
||||
result ?: return
|
||||
|
||||
result.error?.let {
|
||||
activity.showToast(false, it)
|
||||
return
|
||||
}
|
||||
|
||||
val resCode = result.response?.code ?: -1
|
||||
if (resCode !in 200 until 300) {
|
||||
activity.showToast(false, "HTTP error $resCode")
|
||||
return
|
||||
}
|
||||
|
||||
if (newStatus != null) {
|
||||
activity.app_state.columnList.forEach { column ->
|
||||
if (column.access_info.acct == access_info.acct)
|
||||
column.updateEmojiReactionByApiResponse(newStatus)
|
||||
}
|
||||
} else if (status.decreaseReactionMisskey(myReaction.name, true, "removeReaction")) {
|
||||
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
|
||||
column.fireShowContent(
|
||||
reason = "removeReaction complete",
|
||||
reset = true
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// リアクションの別アカ操作で使う
|
||||
// 選択済みのアカウントと同期済みのステータスにリアクションを行う
|
||||
private fun reactionWithoutUi(
|
||||
activity: ActMain,
|
||||
access_info: SavedAccount,
|
||||
resolvedStatus: TootStatus,
|
||||
reactionCode: String? = null,
|
||||
reactionImage: String? = null,
|
||||
isConfirmed: Boolean = false,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
if (reactionCode == null) {
|
||||
EmojiPicker(activity, access_info, closeOnSelected = true) { result ->
|
||||
var newUrl :String? = null
|
||||
val newCode = when (val emoji = result.emoji) {
|
||||
is UnicodeEmoji -> emoji.unifiedCode
|
||||
is CustomEmoji ->{
|
||||
newUrl = emoji.static_url
|
||||
if (access_info.isMisskey) {
|
||||
":${emoji.shortcode}:"
|
||||
} else {
|
||||
emoji.shortcode
|
||||
}
|
||||
}
|
||||
}
|
||||
reactionWithoutUi(
|
||||
activity = activity,
|
||||
access_info = access_info,
|
||||
resolvedStatus = resolvedStatus,
|
||||
reactionCode = newCode,
|
||||
reactionImage = newUrl,
|
||||
isConfirmed = isConfirmed,
|
||||
callback = callback
|
||||
)
|
||||
}.show()
|
||||
return
|
||||
}
|
||||
|
||||
if (!isConfirmed) {
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
access_info,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
val emojiSpan = TootReaction.toSpannableStringBuilder(options, reactionCode, reactionImage)
|
||||
DlgConfirm.open(
|
||||
activity,
|
||||
activity.getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(access_info)),
|
||||
object : DlgConfirm.Callback {
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = access_info.confirm_reaction
|
||||
set(bv) {
|
||||
access_info.confirm_reaction = bv
|
||||
access_info.saveSetting()
|
||||
}
|
||||
|
||||
override fun onOK() {
|
||||
reactionWithoutUi(
|
||||
activity = activity,
|
||||
access_info = access_info,
|
||||
resolvedStatus = resolvedStatus,
|
||||
reactionCode = reactionCode,
|
||||
reactionImage = reactionImage,
|
||||
isConfirmed = true,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
TootTaskRunner(activity, TootTaskRunner.PROGRESS_NONE).run(access_info,
|
||||
object : TootTask {
|
||||
|
||||
var newStatus: TootStatus? = null
|
||||
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? {
|
||||
return if (access_info.isMisskey) {
|
||||
client.request(
|
||||
"/api/notes/reactions/create",
|
||||
access_info.putMisskeyApiToken().apply {
|
||||
put("noteId", resolvedStatus.id.toString())
|
||||
put("reaction", reactionCode)
|
||||
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
)
|
||||
// 成功すると204 no content
|
||||
} else {
|
||||
client.request(
|
||||
"/api/v1/statuses/${resolvedStatus.id}/emoji_reactions/${reactionCode.encodePercent()}",
|
||||
"".toFormRequestBody().toPut()
|
||||
)?.also { result ->
|
||||
// 成功すると更新された投稿
|
||||
newStatus = TootParser(activity, access_info).status(result.jsonObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
result ?: return
|
||||
result.error?.let {
|
||||
activity.showToast(true, it)
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// リアクションの別アカ操作で使う
|
||||
// 選択済みのアカウントと同期済みのステータスと同期まえのリアクションから、同期後のリアクションコードを計算する
|
||||
// 解決できなかった場合はnullを返す
|
||||
private fun fixReactionCode(
|
||||
activity: ActMain,
|
||||
timelineAccount: SavedAccount,
|
||||
actionAccount: SavedAccount,
|
||||
reaction: TootReaction,
|
||||
): String? {
|
||||
val pair = reaction.splitEmojiDomain()
|
||||
pair.first ?: return reaction.name // null または Unicode絵文字
|
||||
|
||||
val srcDomain = when (val d = pair.second) {
|
||||
null, ".", "" -> timelineAccount.apDomain
|
||||
else -> Host.parse(d)
|
||||
}
|
||||
// リアクション者から見てローカルな絵文字
|
||||
if (srcDomain == actionAccount.apDomain) {
|
||||
return when {
|
||||
actionAccount.isMisskey -> ":${pair.first}:"
|
||||
else -> pair.first
|
||||
}
|
||||
}
|
||||
// リアクション者からみてリモートの絵文字
|
||||
val newName = "${pair.first}@${srcDomain}"
|
||||
|
||||
if (actionAccount.isMisskey) {
|
||||
/*
|
||||
Misskey のリアクションAPIはリモートのカスタム絵文字のコードをフォールバック絵文字に変更して、
|
||||
何の追加情報もなしに204 no contentを返す。
|
||||
よってクライアントはAPI応答からフォールバックが発生したことを認識できず、
|
||||
後から投稿をリロードするまで気が付かない。
|
||||
この挙動はこの挙動は多くのユーザにとって受け入れられないと判断するので、
|
||||
クライアント側で事前にエラー扱いにする方が良い。
|
||||
*/
|
||||
} else {
|
||||
// Fedibirdの場合、ステータスを同期した時点で絵文字も同期されてると期待できるのだろうか?
|
||||
// 実際に試してみると
|
||||
// nightly.fedibird.comの投稿にローカルな絵文字を付けた後、
|
||||
// その投稿のURLをfedibird.comの検索欄にいれてローカルに同期すると、
|
||||
// すでにインポート済みの投稿だとリアクション集合は古いままなのだった。
|
||||
//
|
||||
// if (resolvedStatus.reactionSet?.any { it.name == newName } == true)
|
||||
|
||||
return newName
|
||||
}
|
||||
|
||||
// エラー
|
||||
activity.showToast(true, R.string.cant_reaction_remote_custom_emoji, newName)
|
||||
return null
|
||||
}
|
||||
|
||||
fun reactionFromAnotherAccount(
|
||||
activity: ActMain,
|
||||
timeline_account: SavedAccount,
|
||||
statusArg: TootStatus?,
|
||||
reaction: TootReaction? = null,
|
||||
) {
|
||||
statusArg ?: return
|
||||
|
||||
fun afterResolveStatus(actionAccount: SavedAccount, resolvedStatus: TootStatus) {
|
||||
val code = if (reaction == null) {
|
||||
null // あとで選択する
|
||||
} else {
|
||||
fixReactionCode(
|
||||
activity = activity,
|
||||
timelineAccount = timeline_account,
|
||||
actionAccount = actionAccount,
|
||||
reaction = reaction,
|
||||
) ?: return // エラー終了の場合がある
|
||||
}
|
||||
|
||||
reactionWithoutUi(
|
||||
activity = activity,
|
||||
access_info = actionAccount,
|
||||
resolvedStatus = resolvedStatus,
|
||||
reactionCode = code,
|
||||
callback = activity.reaction_complete_callback,
|
||||
)
|
||||
}
|
||||
|
||||
Action_Account.listAccountsReactionable(activity) { list ->
|
||||
|
||||
if (list.isEmpty()) {
|
||||
activity.showToast(false, R.string.not_available_for_current_accounts)
|
||||
return@listAccountsReactionable
|
||||
}
|
||||
|
||||
AccountPicker.pick(
|
||||
activity,
|
||||
accountListArg = list,
|
||||
bAuto = false,
|
||||
message = activity.getString(R.string.account_picker_reaction)
|
||||
) { action_account ->
|
||||
if (calcCrossAccountMode(timeline_account, action_account).isNotRemote) {
|
||||
afterResolveStatus(action_account, statusArg)
|
||||
} else {
|
||||
TootTaskRunner(activity, TootTaskRunner.PROGRESS_NONE).run(action_account,
|
||||
object : TootTask {
|
||||
var newStatus: TootStatus? = null
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? {
|
||||
val (result, status) = client.syncStatus(action_account, statusArg)
|
||||
if (status?.reactionSet?.myReaction != null) {
|
||||
return TootApiResult(activity.getString(R.string.already_reactioned))
|
||||
}
|
||||
newStatus = status
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
result?.error?.let {
|
||||
activity.showToast(true, it)
|
||||
return
|
||||
}
|
||||
newStatus?.let { afterResolveStatus(action_account, it) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
package jp.juggler.subwaytooter.action
|
||||
|
||||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.ActPost
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.dialog.AccountPicker
|
||||
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
||||
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.util.showToast
|
||||
import java.util.*
|
||||
|
||||
object Action_Reply {
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// reply
|
||||
|
||||
fun reply(
|
||||
activity: ActMain,
|
||||
access_info: SavedAccount,
|
||||
status: TootStatus,
|
||||
quote: Boolean = false
|
||||
) {
|
||||
activity.launchActPost(
|
||||
ActPost.createIntent(
|
||||
activity,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
// 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 -> Action_Conversation.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))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,20 +1,14 @@
|
|||
package jp.juggler.subwaytooter.action
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
|
||||
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.emoji.CustomEmoji
|
||||
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.*
|
||||
|
@ -22,12 +16,6 @@ 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,
|
||||
|
@ -710,344 +698,6 @@ object Action_Toot {
|
|||
})
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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
|
||||
|
||||
|
@ -1107,99 +757,6 @@ object Action_Toot {
|
|||
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// reply
|
||||
|
||||
fun reply(
|
||||
activity: ActMain,
|
||||
access_info: SavedAccount,
|
||||
status: TootStatus,
|
||||
quote: Boolean = false
|
||||
) {
|
||||
activity.launchActPost(
|
||||
ActPost.createIntent(
|
||||
activity,
|
||||
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,
|
||||
|
@ -1260,437 +817,6 @@ object Action_Toot {
|
|||
}
|
||||
})
|
||||
}
|
||||
////////////////////////////////////////
|
||||
|
||||
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 addReaction(
|
||||
activity: ActMain,
|
||||
column: Column,
|
||||
status: TootStatus,
|
||||
codeArg: String? = null // Unicode絵文字、 :name: :name@.: :name@domain: name name@domain 等
|
||||
) {
|
||||
if (status.reactionSet?.myReaction != null) {
|
||||
activity.showToast(false, R.string.already_reactioned)
|
||||
return
|
||||
}
|
||||
|
||||
val access_info = column.access_info
|
||||
|
||||
var code = codeArg
|
||||
if (code == null) {
|
||||
EmojiPicker(activity, access_info, closeOnSelected = true) { result ->
|
||||
val newCode = when (val emoji = result.emoji) {
|
||||
is UnicodeEmoji -> emoji.unifiedCode
|
||||
is CustomEmoji -> if (access_info.isMisskey) {
|
||||
":${emoji.shortcode}:"
|
||||
} else {
|
||||
emoji.shortcode
|
||||
}
|
||||
}
|
||||
addReaction(activity, column, status, newCode)
|
||||
}.show()
|
||||
return
|
||||
}
|
||||
|
||||
if (access_info.isMisskey) {
|
||||
val pair = TootReaction.splitEmojiDomain(code)
|
||||
when (/* val domain = */ pair.second) {
|
||||
null, "", ".", access_info.apDomain.ascii -> {
|
||||
// normalize to local custom emoji
|
||||
code = ":${pair.first}:"
|
||||
}
|
||||
else -> {
|
||||
/*
|
||||
#misskey のリアクションAPIはリモートのカスタム絵文字のコードをフォールバック絵文字に変更して、
|
||||
何の追加情報もなしに204 no contentを返す。
|
||||
よってクライアントはAPI応答からフォールバックが発生したことを認識できず、
|
||||
後から投稿をリロードするまで気が付かない。
|
||||
この挙動はこの挙動は多くのユーザにとって受け入れられないと判断するので、
|
||||
クライアント側で事前にエラー扱いにする方が良い。
|
||||
*/
|
||||
activity.showToast(true, R.string.cant_reaction_remote_custom_emoji, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TootTaskRunner(activity, progress_style = TootTaskRunner.PROGRESS_NONE).run(access_info,
|
||||
object : TootTask {
|
||||
|
||||
var newStatus: TootStatus? = null
|
||||
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? {
|
||||
return if (access_info.isMisskey) {
|
||||
client.request("/api/notes/reactions/create", access_info.putMisskeyApiToken().apply {
|
||||
put("noteId", status.id.toString())
|
||||
put("reaction", code)
|
||||
}.toPostRequestBuilder())
|
||||
// 成功すると204 no content
|
||||
} else {
|
||||
client.request(
|
||||
"/api/v1/statuses/${status.id}/emoji_reactions/${code.encodePercent("@")}",
|
||||
"".toFormRequestBody().toPut()
|
||||
)
|
||||
// 成功すると新しいステータス
|
||||
?.also { result ->
|
||||
newStatus = 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
|
||||
}
|
||||
when (val resCode = result.response?.code) {
|
||||
in 200 until 300 -> {
|
||||
if (newStatus != null) {
|
||||
activity.app_state.columnList.forEach { column ->
|
||||
if (column.access_info.acct == access_info.acct)
|
||||
column.updateEmojiReactionByApiResponse(newStatus)
|
||||
}
|
||||
} else {
|
||||
if (status.increaseReactionMisskey(code, true, caller = "addReaction")) {
|
||||
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
|
||||
column.fireShowContent(
|
||||
reason = "addReaction complete",
|
||||
reset = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> activity.showToast(false, "HTTP error $resCode")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 長押しでない普通のリアクション操作
|
||||
fun removeReaction(
|
||||
activity: ActMain,
|
||||
column: Column,
|
||||
status: TootStatus,
|
||||
confirmed: Boolean = false
|
||||
) {
|
||||
val access_info = column.access_info
|
||||
|
||||
val myReaction = status.reactionSet?.myReaction
|
||||
|
||||
if (myReaction == null) {
|
||||
activity.showToast(false, R.string.not_reactioned)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!confirmed) {
|
||||
AlertDialog.Builder(activity)
|
||||
.setMessage(activity.getString(R.string.reaction_remove_confirm, myReaction.name))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
removeReaction(activity, column, status, confirmed = true)
|
||||
}
|
||||
.show()
|
||||
return
|
||||
}
|
||||
|
||||
TootTaskRunner(activity, progress_style = TootTaskRunner.PROGRESS_NONE).run(access_info,
|
||||
object : TootTask {
|
||||
|
||||
var newStatus: TootStatus? = null
|
||||
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? =
|
||||
if (access_info.isMisskey) {
|
||||
client.request(
|
||||
"/api/notes/reactions/delete",
|
||||
access_info.putMisskeyApiToken().apply {
|
||||
put("noteId", status.id.toString())
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
)
|
||||
// 成功すると204 no content
|
||||
} else {
|
||||
client.request(
|
||||
"/api/v1/statuses/${status.id}/emoji_unreaction",
|
||||
"".toFormRequestBody().toPost()
|
||||
)
|
||||
// 成功すると新しいステータス
|
||||
?.also { result ->
|
||||
newStatus = TootParser(activity, access_info).status(result.jsonObject)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
|
||||
result ?: return
|
||||
|
||||
result.error?.let {
|
||||
activity.showToast(false, it)
|
||||
return
|
||||
}
|
||||
|
||||
val resCode = result.response?.code ?: -1
|
||||
if (resCode !in 200 until 300) {
|
||||
activity.showToast(false, "HTTP error $resCode")
|
||||
return
|
||||
}
|
||||
|
||||
if (newStatus != null) {
|
||||
activity.app_state.columnList.forEach { column ->
|
||||
if (column.access_info.acct == access_info.acct)
|
||||
column.updateEmojiReactionByApiResponse(newStatus)
|
||||
}
|
||||
} else if (status.decreaseReactionMisskey(myReaction.name, true, "removeReaction")) {
|
||||
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
|
||||
column.fireShowContent(
|
||||
reason = "removeReaction complete",
|
||||
reset = true
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// リアクションの別アカ操作で使う
|
||||
// 選択済みのアカウントと同期済みのステータスにリアクションを行う
|
||||
private fun reactionWithoutUi(
|
||||
activity: ActMain,
|
||||
access_info: SavedAccount,
|
||||
resolvedStatus: TootStatus,
|
||||
reactionCode: String? = null,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
if (reactionCode == null) {
|
||||
EmojiPicker(activity, access_info, closeOnSelected = true) { result ->
|
||||
val code = when (val emoji = result.emoji) {
|
||||
is UnicodeEmoji -> emoji.unifiedCode
|
||||
is CustomEmoji -> if (access_info.isMisskey) {
|
||||
":${emoji.shortcode}:"
|
||||
} else {
|
||||
emoji.shortcode
|
||||
}
|
||||
}
|
||||
reactionWithoutUi(
|
||||
activity = activity,
|
||||
access_info = access_info,
|
||||
resolvedStatus = resolvedStatus,
|
||||
reactionCode = code,
|
||||
callback = callback
|
||||
)
|
||||
}.show()
|
||||
return
|
||||
}
|
||||
|
||||
TootTaskRunner(activity, TootTaskRunner.PROGRESS_NONE).run(access_info,
|
||||
object : TootTask {
|
||||
|
||||
var newStatus: TootStatus? = null
|
||||
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? {
|
||||
return if (access_info.isMisskey) {
|
||||
client.request(
|
||||
"/api/notes/reactions/create",
|
||||
access_info.putMisskeyApiToken().apply {
|
||||
put("noteId", resolvedStatus.id.toString())
|
||||
put("reaction", reactionCode)
|
||||
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
)
|
||||
// 成功すると204 no content
|
||||
} else {
|
||||
client.request(
|
||||
"/api/v1/statuses/${resolvedStatus.id}/emoji_reactions/${reactionCode.encodePercent()}",
|
||||
"".toFormRequestBody().toPut()
|
||||
)?.also { result ->
|
||||
// 成功すると更新された投稿
|
||||
newStatus = TootParser(activity, access_info).status(result.jsonObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
result ?: return
|
||||
result.error?.let {
|
||||
activity.showToast(true, it)
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// リアクションの別アカ操作で使う
|
||||
// 選択済みのアカウントと同期済みのステータスと同期まえのリアクションから、同期後のリアクションコードを計算する
|
||||
// 解決できなかった場合はnullを返す
|
||||
private fun fixReactionCode(
|
||||
activity: ActMain,
|
||||
timelineAccount: SavedAccount,
|
||||
actionAccount: SavedAccount,
|
||||
reaction: TootReaction,
|
||||
): String? {
|
||||
val pair = reaction.splitEmojiDomain()
|
||||
pair.first ?: return reaction.name // null または Unicode絵文字
|
||||
|
||||
val srcDomain = when (val d = pair.second) {
|
||||
null, ".", "" -> timelineAccount.apDomain
|
||||
else -> Host.parse(d)
|
||||
}
|
||||
// リアクション者から見てローカルな絵文字
|
||||
if (srcDomain == actionAccount.apDomain) {
|
||||
return when {
|
||||
actionAccount.isMisskey -> ":${pair.first}:"
|
||||
else -> pair.first
|
||||
}
|
||||
}
|
||||
// リアクション者からみてリモートの絵文字
|
||||
val newName = "${pair.first}@${srcDomain}"
|
||||
|
||||
if (actionAccount.isMisskey) {
|
||||
/*
|
||||
Misskey のリアクションAPIはリモートのカスタム絵文字のコードをフォールバック絵文字に変更して、
|
||||
何の追加情報もなしに204 no contentを返す。
|
||||
よってクライアントはAPI応答からフォールバックが発生したことを認識できず、
|
||||
後から投稿をリロードするまで気が付かない。
|
||||
この挙動はこの挙動は多くのユーザにとって受け入れられないと判断するので、
|
||||
クライアント側で事前にエラー扱いにする方が良い。
|
||||
*/
|
||||
} else {
|
||||
// Fedibirdの場合、ステータスを同期した時点で絵文字も同期されてると期待できるのだろうか?
|
||||
// 実際に試してみると
|
||||
// nightly.fedibird.comの投稿にローカルな絵文字を付けた後、
|
||||
// その投稿のURLをfedibird.comの検索欄にいれてローカルに同期すると、
|
||||
// すでにインポート済みの投稿だとリアクション集合は古いままなのだった。
|
||||
//
|
||||
// if (resolvedStatus.reactionSet?.any { it.name == newName } == true)
|
||||
|
||||
return newName
|
||||
}
|
||||
|
||||
// エラー
|
||||
activity.showToast(true, R.string.cant_reaction_remote_custom_emoji, newName)
|
||||
return null
|
||||
}
|
||||
|
||||
fun reactionFromAnotherAccount(
|
||||
activity: ActMain,
|
||||
timeline_account: SavedAccount,
|
||||
statusArg: TootStatus?,
|
||||
reaction: TootReaction? = null,
|
||||
) {
|
||||
statusArg ?: return
|
||||
|
||||
fun afterResolveStatus(actionAccount: SavedAccount, resolvedStatus: TootStatus) {
|
||||
val code = if (reaction == null) {
|
||||
null // あとで選択する
|
||||
} else {
|
||||
fixReactionCode(
|
||||
activity = activity,
|
||||
timelineAccount = timeline_account,
|
||||
actionAccount = actionAccount,
|
||||
reaction = reaction,
|
||||
) ?: return // エラー終了の場合がある
|
||||
}
|
||||
|
||||
reactionWithoutUi(
|
||||
activity = activity,
|
||||
access_info = actionAccount,
|
||||
resolvedStatus = resolvedStatus,
|
||||
reactionCode = code,
|
||||
callback = activity.reaction_complete_callback,
|
||||
)
|
||||
}
|
||||
|
||||
Action_Account.listAccountsReactionable(activity) { list ->
|
||||
|
||||
if (list.isEmpty()) {
|
||||
activity.showToast(false, R.string.not_available_for_current_accounts)
|
||||
return@listAccountsReactionable
|
||||
}
|
||||
|
||||
AccountPicker.pick(
|
||||
activity,
|
||||
accountListArg = list,
|
||||
bAuto = false,
|
||||
message = activity.getString(R.string.account_picker_reaction)
|
||||
) { action_account ->
|
||||
if (calcCrossAccountMode(timeline_account, action_account).isNotRemote) {
|
||||
afterResolveStatus(action_account, statusArg)
|
||||
} else {
|
||||
TootTaskRunner(activity, TootTaskRunner.PROGRESS_NONE).run(action_account,
|
||||
object : TootTask {
|
||||
var newStatus: TootStatus? = null
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? {
|
||||
val (result, status) = client.syncStatus(action_account, statusArg)
|
||||
if (status?.reactionSet?.myReaction != null) {
|
||||
return TootApiResult(activity.getString(R.string.already_reactioned))
|
||||
}
|
||||
newStatus = status
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
result?.error?.let {
|
||||
activity.showToast(true, it)
|
||||
return
|
||||
}
|
||||
newStatus?.let { afterResolveStatus(action_account, it) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun deleteScheduledPost(
|
||||
activity: ActMain,
|
||||
|
|
|
@ -111,7 +111,7 @@ class TootReaction(
|
|||
fun canReaction(
|
||||
access_info: SavedAccount,
|
||||
ti: TootInstance? = TootInstance.getCached(access_info.apiHost)
|
||||
) = InstanceCapability.emojiReaction(access_info,ti)
|
||||
) = InstanceCapability.emojiReaction(access_info, ti)
|
||||
|
||||
fun decodeEmojiQuery(jsonText: String?): List<TootReaction> =
|
||||
jsonText.notEmpty()?.let { src ->
|
||||
|
@ -130,7 +130,33 @@ class TootReaction(
|
|||
}
|
||||
}.toString()
|
||||
|
||||
fun urlToSpan(options: DecodeOptions, code: String, url: String) =
|
||||
SpannableStringBuilder(code).apply {
|
||||
setSpan(
|
||||
NetworkEmojiSpan(url, scale = options.enlargeCustomEmoji),
|
||||
0,
|
||||
length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
fun toSpannableStringBuilder(
|
||||
options: DecodeOptions,
|
||||
code: String,
|
||||
url: String?
|
||||
): SpannableStringBuilder {
|
||||
|
||||
url?.let { return urlToSpan(options, code, url) }
|
||||
|
||||
if (options.linkHelper?.isMisskey == true) {
|
||||
// 古い形式の絵文字はUnicode絵文字にする
|
||||
misskeyOldReactions[code]?.let {
|
||||
return EmojiDecoder.decodeEmoji(options, it)
|
||||
}
|
||||
}
|
||||
|
||||
return EmojiDecoder.decodeEmoji(options, code)
|
||||
}
|
||||
}
|
||||
|
||||
fun splitEmojiDomain() =
|
||||
|
@ -160,15 +186,7 @@ class TootReaction(
|
|||
url
|
||||
}
|
||||
|
||||
fun urlToSpan(options: DecodeOptions, code: String, url: String) =
|
||||
SpannableStringBuilder(code).apply {
|
||||
setSpan(
|
||||
NetworkEmojiSpan(url, scale = options.enlargeCustomEmoji),
|
||||
0,
|
||||
length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (options.linkHelper?.isMisskey == true) {
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ class SavedAccount(
|
|||
var confirm_follow_locked : Boolean = false
|
||||
var confirm_unfollow : Boolean = false
|
||||
var confirm_post : Boolean = false
|
||||
var confirm_reaction : Boolean = true
|
||||
|
||||
var notification_tag : String? = null
|
||||
private var register_key : String? = null
|
||||
|
@ -131,6 +132,7 @@ class SavedAccount(
|
|||
confirm_follow_locked = cursor.getBoolean(COL_CONFIRM_FOLLOW_LOCKED)
|
||||
confirm_unfollow = cursor.getBoolean(COL_CONFIRM_UNFOLLOW)
|
||||
confirm_post = cursor.getBoolean(COL_CONFIRM_POST)
|
||||
confirm_reaction = cursor.getBoolean(COL_CONFIRM_REACTION)
|
||||
|
||||
notification_mention = cursor.getBoolean(COL_NOTIFICATION_MENTION)
|
||||
notification_boost = cursor.getBoolean(COL_NOTIFICATION_BOOST)
|
||||
|
@ -224,7 +226,8 @@ class SavedAccount(
|
|||
cv.put(COL_CONFIRM_FOLLOW_LOCKED, confirm_follow_locked.b2i())
|
||||
cv.put(COL_CONFIRM_UNFOLLOW, confirm_unfollow.b2i())
|
||||
cv.put(COL_CONFIRM_POST, confirm_post.b2i())
|
||||
|
||||
cv.put(COL_CONFIRM_REACTION, confirm_reaction.b2i())
|
||||
|
||||
cv.put(COL_SOUND_URI, sound_uri)
|
||||
cv.put(COL_DEFAULT_TEXT, default_text)
|
||||
|
||||
|
@ -279,6 +282,8 @@ class SavedAccount(
|
|||
this.confirm_favourite = b.confirm_favourite
|
||||
this.confirm_unboost = b.confirm_unboost
|
||||
this.confirm_unfavourite = b.confirm_unfavourite
|
||||
this.confirm_post = b.confirm_post
|
||||
this.confirm_reaction = b.confirm_reaction
|
||||
|
||||
this.dont_hide_nsfw = b.dont_hide_nsfw
|
||||
this.dont_show_timeout = b.dont_show_timeout
|
||||
|
@ -370,7 +375,8 @@ class SavedAccount(
|
|||
private const val COL_CONFIRM_FAVOURITE = "confirm_favourite" // スキーマ23
|
||||
private const val COL_CONFIRM_UNBOOST = "confirm_unboost" // スキーマ24
|
||||
private const val COL_CONFIRM_UNFAVOURITE = "confirm_unfavourite" // スキーマ24
|
||||
|
||||
private const val COL_CONFIRM_REACTION = "confirm_reaction" // スキーマ61
|
||||
|
||||
// スキーマ13から
|
||||
const val COL_NOTIFICATION_TAG = "notification_server"
|
||||
|
||||
|
@ -507,6 +513,9 @@ class SavedAccount(
|
|||
// スキーマ60から
|
||||
+ ",$COL_PUSH_POLICY text default null"
|
||||
|
||||
// スキーマ61から
|
||||
+ ",$COL_CONFIRM_REACTION integer default 1"
|
||||
|
||||
+ ")"
|
||||
)
|
||||
db.execSQL("create index if not exists ${table}_user on ${table}(u)")
|
||||
|
@ -749,6 +758,13 @@ class SavedAccount(
|
|||
log.trace(ex)
|
||||
}
|
||||
}
|
||||
isUpgraded(61){
|
||||
try {
|
||||
db.execSQL("alter table $table add column $COL_CONFIRM_REACTION integer default 1")
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val defaultResizeConfig = ResizeConfig(ResizeType.LongSide, 1280)
|
||||
|
|
|
@ -14,6 +14,7 @@ import androidx.browser.customtabs.CustomTabsIntent
|
|||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.Pref
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.action.Action_Conversation
|
||||
import jp.juggler.subwaytooter.action.Action_HashTag
|
||||
import jp.juggler.subwaytooter.action.Action_Toot
|
||||
import jp.juggler.subwaytooter.action.Action_User
|
||||
|
@ -235,7 +236,7 @@ fun openCustomTab(
|
|||
statusInfo.statusId == null ||
|
||||
!accessInfo.matchHost(statusInfo.host)
|
||||
) {
|
||||
Action_Toot.conversationOtherInstance(
|
||||
Action_Conversation.conversationOtherInstance(
|
||||
activity,
|
||||
pos,
|
||||
statusInfo.url,
|
||||
|
@ -244,7 +245,7 @@ fun openCustomTab(
|
|||
statusInfo.statusId
|
||||
)
|
||||
} else {
|
||||
Action_Toot.conversationLocal(
|
||||
Action_Conversation.conversationLocal(
|
||||
activity,
|
||||
pos,
|
||||
accessInfo,
|
||||
|
|
|
@ -498,6 +498,11 @@
|
|||
style="@style/setting_row_form"
|
||||
android:text="@string/act_post" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbConfirmReaction"
|
||||
style="@style/setting_row_form"
|
||||
android:text="@string/reaction" />
|
||||
|
||||
<View style="@style/setting_divider" />
|
||||
|
||||
<TextView
|
||||
|
|
|
@ -791,7 +791,7 @@
|
|||
<string name="remove_endorse_succeeded">Recomanació eliminada.</string>
|
||||
<string name="follow_request_cancelled">Sol·licitud de seguiment cancel·lada.</string>
|
||||
<string name="confirm_cancel_follow_request_who_from">Cancel·lar la sol·licitud de seguiment de %2$s a %1$s\?</string>
|
||||
<string name="reaction">Reacció (Misskey)</string>
|
||||
<string name="reaction">Reacció</string>
|
||||
<string name="vote_polls">votació o el seu resultat</string>
|
||||
<string name="timeout_for_embed_media_viewer">Temps d\'espera per al visor de mèdia incorporat (Unitat:segons, cal reiniciar(eliminar de l\'historial) l\'aplicació)</string>
|
||||
<string name="link_color">Color dels enllaços (cal reiniciar aplicació)</string>
|
||||
|
|
|
@ -704,7 +704,7 @@
|
|||
<string name="link_color">链接颜色(应用需要重启)</string>
|
||||
<string name="timeout_for_embed_media_viewer">嵌入式媒体查看器超时(单位: 秒,应用重启(从应用历史记录删除))</string>
|
||||
<string name="vote_polls">投票或结果</string>
|
||||
<string name="reaction">动作 (Misskey)</string>
|
||||
<string name="reaction">动作</string>
|
||||
<string name="confirm_cancel_follow_request_who_from">从%2$s到%1$s取消请求吗?</string>
|
||||
<string name="follow_request_cancelled">请求取消.</string>
|
||||
<string name="remove_endorse_succeeded">未签署.</string>
|
||||
|
|
|
@ -560,7 +560,7 @@
|
|||
<string name="link_color">Linkfarbe (App-Neustart erforderlich)</string>
|
||||
<string name="timeout_for_embed_media_viewer">Timeout für den integrierten Medienbetrachter (Einheit: Sekunden, App-Neustart erforderlich)</string>
|
||||
<string name="vote_polls">Abstimmung oder Ergebnis</string>
|
||||
<string name="reaction">Reaktion (Misskey)</string>
|
||||
<string name="reaction">Reaktion</string>
|
||||
<string name="confirm_cancel_follow_request_who_from">Followanfrage von %2$s bis %1$s abbrechen\?</string>
|
||||
<string name="follow_request_cancelled">Follow-Anfrage abgebrochen.</string>
|
||||
<string name="remove_endorse_succeeded">Promotion entfernt.</string>
|
||||
|
|
|
@ -753,7 +753,7 @@
|
|||
<string name="remove_endorse_succeeded">N’est plus recommandé.</string>
|
||||
<string name="follow_request_cancelled">Demande d’abonnement annulée.</string>
|
||||
<string name="confirm_cancel_follow_request_who_from">Annuler la demande d’abonnement de %2$s à %1$s \?</string>
|
||||
<string name="reaction">Réaction (Misskey)</string>
|
||||
<string name="reaction">Réaction</string>
|
||||
<string name="timeout_for_embed_media_viewer">Délai d’expiration du visualiseur multimédia intégré (unité: secondes, redémarrage de l’application (suppression de l’historique de application) requis)</string>
|
||||
<string name="media_attachment_max_byte_size_movie">Taille limite des octets de la pièce jointe (vidéo) (Unité : MB. La valeur par défaut est 40)</string>
|
||||
<string name="link_color">Couleur des liens (redémarrage requis)</string>
|
||||
|
|
|
@ -569,7 +569,7 @@
|
|||
<string name="quote_name">名前を引用…</string>
|
||||
<string name="quote_url">URLを引用…</string>
|
||||
<string name="rate_on_store">ストアで評価</string>
|
||||
<string name="reaction">リアクション (Misskey)</string>
|
||||
<string name="reaction">リアクション</string>
|
||||
<string name="reaction_add">リアクションの追加</string>
|
||||
<string name="reaction_remove">リアクションの削除</string>
|
||||
<string name="read_gap">ギャップを読む</string>
|
||||
|
@ -1091,4 +1091,5 @@
|
|||
<string name="discard_changes">変更を破棄しますか?</string>
|
||||
<string name="saved">保存しました</string>
|
||||
<string name="not_available_for_current_accounts">この機能を利用できるアカウントがありません</string>
|
||||
<string name="confirm_reaction">%2$s から %1$s でリアクションしますか?</string>
|
||||
</resources>
|
||||
|
|
|
@ -767,7 +767,7 @@
|
|||
<string name="remove_endorse_succeeded">소개 삭제됨.</string>
|
||||
<string name="follow_request_cancelled">팔로우 요청 취소됨.</string>
|
||||
<string name="confirm_cancel_follow_request_who_from">%2$s로부터 %1$s로의 팔로우 요청을 취소할까요\?</string>
|
||||
<string name="reaction">반응 (Misskey)</string>
|
||||
<string name="reaction">반응</string>
|
||||
<string name="timeout_for_embed_media_viewer">내장 미디어 뷰어 시간제한 (단위:초, 앱 재시작(앱 사용이력에서 삭제) 필요)</string>
|
||||
<string name="link_color">링크 색 (앱 재시작 필요)</string>
|
||||
<string name="missing_closeable_column">닫을 칼럼이 표시 범위에 없음.</string>
|
||||
|
|
|
@ -698,7 +698,7 @@
|
|||
<string name="already_voted">Du har allerede stemt.</string>
|
||||
<string name="follow_request_cancelled">Følgingsforespørsel forkastet.</string>
|
||||
<string name="confirm_cancel_follow_request_who_from">Følgingsforespørsel fra %1$s til %2$s vil forkastes. Er du sikker\?</string>
|
||||
<string name="reaction">Reaksjon (Misskey)</string>
|
||||
<string name="reaction">Reaksjon</string>
|
||||
<string name="timeout_for_embed_media_viewer">Tidsavbrudd for innebygd mediaviser (enhet:sekunder, programomstart (sletting fra programhistorikk) kreves)</string>
|
||||
<string name="link_color">Lenkefarge (programomstart kreves)</string>
|
||||
<string name="missing_closeable_column">mangler lukkbar kolonne i synlig område.</string>
|
||||
|
|
|
@ -788,7 +788,7 @@
|
|||
<string name="remove_endorse_succeeded">Unendorsemed.</string>
|
||||
<string name="follow_request_cancelled">Follow request cancelled.</string>
|
||||
<string name="confirm_cancel_follow_request_who_from">Cancel follow request from %2$s to %1$s?</string>
|
||||
<string name="reaction">Reaction (Misskey)</string>
|
||||
<string name="reaction">Reaction</string>
|
||||
<string name="vote_polls">voting or its result</string>
|
||||
<string name="timeout_for_embed_media_viewer">Timeout for embedded media viewer (Unit:seconds, app restart(delete from app history) required)</string>
|
||||
<string name="link_color">Link color (app restart required)</string>
|
||||
|
@ -1104,5 +1104,5 @@
|
|||
<string name="discard_changes">Discard changes?</string>
|
||||
<string name="saved">saved.</string>
|
||||
<string name="not_available_for_current_accounts">Unavailable for current accounts</string>
|
||||
|
||||
<string name="confirm_reaction">Reaction %1$s from %2$s?</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue