mirror of
https://github.com/tateisu/SubwayTooter
synced 2025-02-10 09:00:36 +01:00
絵文字ピッカーの改善
This commit is contained in:
parent
228a8d3338
commit
f1d3556abf
@ -39,9 +39,9 @@ android {
|
||||
jvmTarget = jvm_target
|
||||
useIR = true
|
||||
freeCompilerArgs += [
|
||||
"-Xopt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
// "-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
// "-Xopt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
]
|
||||
@ -148,9 +148,11 @@ dependencies {
|
||||
|
||||
implementation "androidx.core:core-ktx:1.7.0"
|
||||
|
||||
def emojiVersion = "1.1.0"
|
||||
implementation "androidx.emoji2:emoji2:$emojiVersion"
|
||||
implementation "androidx.emoji2:emoji2-bundled:$emojiVersion"
|
||||
def emoji2Version = "1.1.0"
|
||||
implementation "androidx.emoji2:emoji2:$emoji2Version"
|
||||
implementation "androidx.emoji2:emoji2-views:$emoji2Version"
|
||||
implementation "androidx.emoji2:emoji2-views-helper:$emoji2Version"
|
||||
implementation "androidx.emoji2:emoji2-bundled:$emoji2Version"
|
||||
|
||||
// DrawerLayout
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||
@ -236,6 +238,8 @@ dependencies {
|
||||
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||
|
||||
// ViewModel
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
|
||||
|
@ -2,7 +2,9 @@ package jp.juggler.subwaytooter.action
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import jp.juggler.subwaytooter.*
|
||||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.Styler
|
||||
import jp.juggler.subwaytooter.actmain.addColumn
|
||||
import jp.juggler.subwaytooter.actmain.reloadAccountSetting
|
||||
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount
|
||||
@ -13,16 +15,12 @@ import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.api.entity.TootVisibility
|
||||
import jp.juggler.subwaytooter.column.ColumnType
|
||||
import jp.juggler.subwaytooter.column.findStatus
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||
import jp.juggler.subwaytooter.dialog.pickAccount
|
||||
import jp.juggler.subwaytooter.table.AcctColor
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.emptyCallback
|
||||
import jp.juggler.util.JsonObject
|
||||
import jp.juggler.util.launchMain
|
||||
import jp.juggler.util.showToast
|
||||
import jp.juggler.util.toPostRequestBuilder
|
||||
import java.util.*
|
||||
import jp.juggler.util.*
|
||||
import kotlin.math.max
|
||||
|
||||
private class BoostImpl(
|
||||
@ -62,42 +60,6 @@ private class BoostImpl(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun confirm(): Boolean {
|
||||
if (bConfirmed) return true
|
||||
DlgConfirm.open(
|
||||
activity,
|
||||
activity.getString(
|
||||
when {
|
||||
!bSet -> R.string.confirm_unboost_from
|
||||
isPrivateToot -> R.string.confirm_boost_private_from
|
||||
visibility == TootVisibility.PrivateFollowers -> R.string.confirm_private_boost_from
|
||||
else -> R.string.confirm_boost_from
|
||||
},
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
object : DlgConfirm.Callback {
|
||||
override fun onOK() {
|
||||
bConfirmed = true
|
||||
run()
|
||||
}
|
||||
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = when (bSet) {
|
||||
true -> accessInfo.confirm_boost
|
||||
else -> accessInfo.confirm_unboost
|
||||
}
|
||||
set(value) {
|
||||
when (bSet) {
|
||||
true -> accessInfo.confirm_boost = value
|
||||
else -> accessInfo.confirm_unboost = value
|
||||
}
|
||||
accessInfo.saveSetting()
|
||||
activity.reloadAccountSetting(accessInfo)
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun Context.syncStatus(client: TootApiClient) =
|
||||
if (!crossAccountMode.isRemote) {
|
||||
// 既に自タンスのステータスがある
|
||||
@ -234,16 +196,41 @@ private class BoostImpl(
|
||||
}
|
||||
|
||||
fun run() {
|
||||
if (!preCheck()) return
|
||||
if (!confirm()) return
|
||||
activity.launchAndShowError {
|
||||
if (!preCheck()) return@launchAndShowError
|
||||
|
||||
// ブースト表示を更新中にする
|
||||
activity.appState.setBusyBoost(accessInfo, statusArg)
|
||||
activity.showColumnMatchAccount(accessInfo)
|
||||
if (!bConfirmed) {
|
||||
activity.confirm(
|
||||
activity.getString(
|
||||
when {
|
||||
!bSet -> R.string.confirm_unboost_from
|
||||
isPrivateToot -> R.string.confirm_boost_private_from
|
||||
visibility == TootVisibility.PrivateFollowers -> R.string.confirm_private_boost_from
|
||||
else -> R.string.confirm_boost_from
|
||||
},
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
when (bSet) {
|
||||
true -> accessInfo.confirm_boost
|
||||
else -> accessInfo.confirm_unboost
|
||||
}
|
||||
) { newConfirmEnabled ->
|
||||
when (bSet) {
|
||||
true -> accessInfo.confirm_boost = newConfirmEnabled
|
||||
else -> accessInfo.confirm_unboost = newConfirmEnabled
|
||||
}
|
||||
accessInfo.saveSetting()
|
||||
activity.reloadAccountSetting(accessInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// ブースト表示を更新中にする
|
||||
activity.appState.setBusyBoost(accessInfo, statusArg)
|
||||
activity.showColumnMatchAccount(accessInfo)
|
||||
|
||||
launchMain {
|
||||
val result =
|
||||
activity.runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
|
||||
activity.runApiTask(accessInfo,
|
||||
progressStyle = ApiTask.PROGRESS_NONE) { client ->
|
||||
try {
|
||||
val targetStatus = syncStatus(client)
|
||||
boostApi(client, targetStatus)
|
||||
|
@ -5,11 +5,11 @@ import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.entity.TootFilter
|
||||
import jp.juggler.subwaytooter.api.runApiTask
|
||||
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
||||
import jp.juggler.subwaytooter.column.onFilterDeleted
|
||||
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.launchMain
|
||||
import jp.juggler.util.launchAndShowError
|
||||
import jp.juggler.util.showToast
|
||||
import okhttp3.Request
|
||||
|
||||
@ -31,22 +31,17 @@ fun ActMain.openFilterMenu(accessInfo: SavedAccount, item: TootFilter?) {
|
||||
fun ActMain.filterDelete(
|
||||
accessInfo: SavedAccount,
|
||||
filter: TootFilter,
|
||||
bConfirmed: Boolean = false
|
||||
bConfirmed: Boolean = false,
|
||||
) {
|
||||
if (!bConfirmed) {
|
||||
DlgConfirm.openSimple(
|
||||
this,
|
||||
getString(R.string.filter_delete_confirm, filter.phrase)
|
||||
) {
|
||||
filterDelete(accessInfo, filter, bConfirmed = true)
|
||||
launchAndShowError {
|
||||
if (!bConfirmed) {
|
||||
confirm(R.string.filter_delete_confirm, filter.phrase)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
launchMain {
|
||||
var resultFilterList: ArrayList<TootFilter>? = null
|
||||
runApiTask(accessInfo) { client ->
|
||||
var result = client.request("/api/v1/filters/${filter.id}", Request.Builder().delete())
|
||||
var result =
|
||||
client.request("/api/v1/filters/${filter.id}", Request.Builder().delete())
|
||||
if (result != null && result.error == null) {
|
||||
result = client.request("/api/v1/filters")
|
||||
val jsonArray = result?.jsonArray
|
||||
|
@ -1,7 +1,8 @@
|
||||
package jp.juggler.subwaytooter.action
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import jp.juggler.subwaytooter.*
|
||||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.actmain.reloadAccountSetting
|
||||
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
@ -9,12 +10,16 @@ import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.column.ColumnType
|
||||
import jp.juggler.subwaytooter.column.fireRebindAdapterItems
|
||||
import jp.juggler.subwaytooter.column.removeUser
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||
import jp.juggler.subwaytooter.dialog.pickAccount
|
||||
import jp.juggler.subwaytooter.table.AcctColor
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.table.UserRelation
|
||||
import jp.juggler.util.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
fun ActMain.clickFollowInfo(
|
||||
pos: Int,
|
||||
@ -45,7 +50,10 @@ fun ActMain.clickFollow(
|
||||
relation.blocking || relation.muting ->
|
||||
Unit // 何もしない
|
||||
accessInfo.isMisskey && relation.getRequested(who) && !relation.getFollowing(who) ->
|
||||
followRequestDelete(pos, accessInfo, whoRef, callback = cancelFollowRequestCompleteCallback)
|
||||
followRequestDelete(pos,
|
||||
accessInfo,
|
||||
whoRef,
|
||||
callback = cancelFollowRequestCompleteCallback)
|
||||
relation.getFollowing(who) || relation.getRequested(who) ->
|
||||
follow(pos, accessInfo, whoRef, bFollow = false, callback = unfollowCompleteCallback)
|
||||
else ->
|
||||
@ -53,15 +61,20 @@ fun ActMain.clickFollow(
|
||||
}
|
||||
}
|
||||
|
||||
fun ActMain.clickFollowRequestAccept(accessInfo: SavedAccount, whoRef: TootAccountRef?, accept: Boolean) {
|
||||
fun ActMain.clickFollowRequestAccept(
|
||||
accessInfo: SavedAccount,
|
||||
whoRef: TootAccountRef?,
|
||||
accept: Boolean,
|
||||
) {
|
||||
val who = whoRef?.get() ?: return
|
||||
DlgConfirm.openSimple(
|
||||
this,
|
||||
getString(
|
||||
if (accept) R.string.follow_accept_confirm else R.string.follow_deny_confirm,
|
||||
launchAndShowError {
|
||||
confirm(
|
||||
when {
|
||||
accept -> R.string.follow_accept_confirm
|
||||
else -> R.string.follow_deny_confirm
|
||||
},
|
||||
AcctColor.getNickname(accessInfo, who)
|
||||
)
|
||||
) {
|
||||
followRequestAuthorize(accessInfo, whoRef, accept)
|
||||
}
|
||||
}
|
||||
@ -85,136 +98,90 @@ fun ActMain.follow(
|
||||
return
|
||||
}
|
||||
|
||||
if (!bConfirmMoved && bFollow && who.moved != null) {
|
||||
AlertDialog.Builder(activity)
|
||||
.setMessage(
|
||||
getString(
|
||||
R.string.jump_moved_user,
|
||||
accessInfo.getFullAcct(who),
|
||||
accessInfo.getFullAcct(who.moved)
|
||||
)
|
||||
)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
userProfileFromAnotherAccount(
|
||||
pos,
|
||||
accessInfo,
|
||||
who.moved
|
||||
)
|
||||
launchAndShowError {
|
||||
if (!bConfirmMoved && bFollow && who.moved != null) {
|
||||
val selected = suspendCancellableCoroutine<Int> { cont ->
|
||||
try {
|
||||
val dialog = AlertDialog.Builder(activity)
|
||||
.setMessage(
|
||||
getString(
|
||||
R.string.jump_moved_user,
|
||||
accessInfo.getFullAcct(who),
|
||||
accessInfo.getFullAcct(who.moved)
|
||||
)
|
||||
)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
cont.resume(R.string.ok)
|
||||
}
|
||||
.setNeutralButton(R.string.ignore_suggestion) { _, _ ->
|
||||
cont.resume(R.string.ignore_suggestion)
|
||||
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create()
|
||||
dialog.setOnDismissListener {
|
||||
if (cont.isActive) cont.resumeWithException(CancellationException())
|
||||
}
|
||||
cont.invokeOnCancellation { dialog.dismissSafe() }
|
||||
dialog.show()
|
||||
} catch (ex: Throwable) {
|
||||
cont.resumeWithException(ex)
|
||||
}
|
||||
}
|
||||
.setNeutralButton(R.string.ignore_suggestion) { _, _ ->
|
||||
follow(
|
||||
pos,
|
||||
accessInfo,
|
||||
whoRef,
|
||||
bFollow = bFollow,
|
||||
bConfirmMoved = true, // CHANGED
|
||||
bConfirmed = bConfirmed,
|
||||
callback = callback
|
||||
)
|
||||
when (selected) {
|
||||
R.string.ok -> {
|
||||
userProfileFromAnotherAccount(
|
||||
pos,
|
||||
accessInfo,
|
||||
who.moved
|
||||
)
|
||||
return@launchAndShowError
|
||||
}
|
||||
R.string.ignore_suggestion -> Unit // fall thru
|
||||
}
|
||||
} else if (!bConfirmed) {
|
||||
if (bFollow && who.locked) {
|
||||
confirm(
|
||||
getString(
|
||||
R.string.confirm_follow_request_who_from,
|
||||
whoRef.decoded_display_name,
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
accessInfo.confirm_follow_locked,
|
||||
) { newConfirmEnabled ->
|
||||
accessInfo.confirm_follow_locked = newConfirmEnabled
|
||||
accessInfo.saveSetting()
|
||||
activity.reloadAccountSetting(accessInfo)
|
||||
}
|
||||
} else if (bFollow) {
|
||||
confirm(
|
||||
getString(
|
||||
R.string.confirm_follow_who_from,
|
||||
whoRef.decoded_display_name,
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
accessInfo.confirm_follow
|
||||
) { newConfirmEnabled ->
|
||||
accessInfo.confirm_follow = newConfirmEnabled
|
||||
accessInfo.saveSetting()
|
||||
activity.reloadAccountSetting(accessInfo)
|
||||
}
|
||||
} else {
|
||||
confirm(
|
||||
getString(
|
||||
R.string.confirm_unfollow_who_from,
|
||||
whoRef.decoded_display_name,
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
accessInfo.confirm_unfollow
|
||||
) { newConfirmEnabled ->
|
||||
accessInfo.confirm_unfollow = newConfirmEnabled
|
||||
accessInfo.saveSetting()
|
||||
activity.reloadAccountSetting(accessInfo)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
|
||||
if (!bConfirmed) {
|
||||
if (bFollow && who.locked) {
|
||||
DlgConfirm.open(
|
||||
activity,
|
||||
activity.getString(
|
||||
R.string.confirm_follow_request_who_from,
|
||||
whoRef.decoded_display_name,
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
object : DlgConfirm.Callback {
|
||||
|
||||
override fun onOK() {
|
||||
follow(
|
||||
pos,
|
||||
accessInfo,
|
||||
whoRef,
|
||||
bFollow = bFollow,
|
||||
bConfirmMoved = bConfirmMoved,
|
||||
bConfirmed = true, // CHANGED
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = accessInfo.confirm_follow_locked
|
||||
set(value) {
|
||||
accessInfo.confirm_follow_locked = value
|
||||
accessInfo.saveSetting()
|
||||
activity.reloadAccountSetting(accessInfo)
|
||||
}
|
||||
})
|
||||
return
|
||||
} else if (bFollow) {
|
||||
DlgConfirm.open(
|
||||
activity,
|
||||
getString(
|
||||
R.string.confirm_follow_who_from,
|
||||
whoRef.decoded_display_name,
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
object : DlgConfirm.Callback {
|
||||
|
||||
override fun onOK() {
|
||||
follow(
|
||||
pos,
|
||||
accessInfo,
|
||||
whoRef,
|
||||
bFollow = bFollow,
|
||||
bConfirmMoved = bConfirmMoved,
|
||||
bConfirmed = true, //CHANGED
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = accessInfo.confirm_follow
|
||||
set(value) {
|
||||
accessInfo.confirm_follow = value
|
||||
accessInfo.saveSetting()
|
||||
activity.reloadAccountSetting(accessInfo)
|
||||
}
|
||||
})
|
||||
return
|
||||
} else {
|
||||
DlgConfirm.open(
|
||||
activity,
|
||||
getString(
|
||||
R.string.confirm_unfollow_who_from,
|
||||
whoRef.decoded_display_name,
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
object : DlgConfirm.Callback {
|
||||
|
||||
override fun onOK() {
|
||||
follow(
|
||||
pos,
|
||||
accessInfo,
|
||||
whoRef,
|
||||
bFollow = bFollow,
|
||||
bConfirmMoved = bConfirmMoved,
|
||||
bConfirmed = true, // CHANGED
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = accessInfo.confirm_unfollow
|
||||
set(value) {
|
||||
accessInfo.confirm_unfollow = value
|
||||
accessInfo.saveSetting()
|
||||
activity.reloadAccountSetting(accessInfo)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
launchMain {
|
||||
var resultRelation: UserRelation? = null
|
||||
runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
|
||||
val parser = TootParser(activity, accessInfo)
|
||||
@ -327,77 +294,43 @@ private fun ActMain.followRemote(
|
||||
bConfirmed: Boolean = false,
|
||||
callback: () -> Unit = {},
|
||||
) {
|
||||
val activity = this@followRemote
|
||||
|
||||
if (accessInfo.isMe(acct)) {
|
||||
showToast(false, R.string.it_is_you)
|
||||
return
|
||||
}
|
||||
|
||||
if (!bConfirmed) {
|
||||
if (locked) {
|
||||
DlgConfirm.open(
|
||||
activity,
|
||||
getString(
|
||||
R.string.confirm_follow_request_who_from,
|
||||
AcctColor.getNickname(acct),
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
object : DlgConfirm.Callback {
|
||||
override fun onOK() {
|
||||
followRemote(
|
||||
launchAndShowError {
|
||||
|
||||
accessInfo,
|
||||
acct,
|
||||
locked,
|
||||
bConfirmed = true, //CHANGE
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = accessInfo.confirm_follow_locked
|
||||
set(value) {
|
||||
accessInfo.confirm_follow_locked = value
|
||||
accessInfo.saveSetting()
|
||||
reloadAccountSetting(accessInfo)
|
||||
}
|
||||
})
|
||||
return
|
||||
} else {
|
||||
DlgConfirm.open(
|
||||
activity,
|
||||
getString(
|
||||
R.string.confirm_follow_who_from,
|
||||
AcctColor.getNickname(acct),
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
object : DlgConfirm.Callback {
|
||||
|
||||
override fun onOK() {
|
||||
followRemote(
|
||||
|
||||
accessInfo,
|
||||
acct,
|
||||
locked,
|
||||
bConfirmed = true, //CHANGE
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = accessInfo.confirm_follow
|
||||
set(value) {
|
||||
accessInfo.confirm_follow = value
|
||||
accessInfo.saveSetting()
|
||||
reloadAccountSetting(accessInfo)
|
||||
}
|
||||
})
|
||||
return
|
||||
if (!bConfirmed) {
|
||||
if (locked) {
|
||||
confirm(
|
||||
getString(
|
||||
R.string.confirm_follow_request_who_from,
|
||||
AcctColor.getNickname(acct),
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
accessInfo.confirm_follow_locked,
|
||||
) { newConfirmEnabled ->
|
||||
accessInfo.confirm_follow_locked = newConfirmEnabled
|
||||
accessInfo.saveSetting()
|
||||
reloadAccountSetting(accessInfo)
|
||||
}
|
||||
} else {
|
||||
confirm(
|
||||
getString(
|
||||
R.string.confirm_follow_who_from,
|
||||
AcctColor.getNickname(acct),
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
accessInfo.confirm_follow
|
||||
) { newConfirmEnabled ->
|
||||
accessInfo.confirm_follow = newConfirmEnabled
|
||||
accessInfo.saveSetting()
|
||||
reloadAccountSetting(accessInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launchMain {
|
||||
var resultRelation: UserRelation? = null
|
||||
runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
|
||||
val parser = TootParser(this, accessInfo)
|
||||
@ -598,27 +531,15 @@ fun ActMain.followRequestDelete(
|
||||
return
|
||||
}
|
||||
|
||||
if (!bConfirmed) {
|
||||
DlgConfirm.openSimple(
|
||||
this,
|
||||
getString(
|
||||
launchAndShowError {
|
||||
if (!bConfirmed) {
|
||||
confirm(
|
||||
R.string.confirm_cancel_follow_request_who_from,
|
||||
whoRef.decoded_display_name,
|
||||
AcctColor.getNickname(accessInfo)
|
||||
)
|
||||
) {
|
||||
followRequestDelete(
|
||||
pos,
|
||||
accessInfo,
|
||||
whoRef,
|
||||
bConfirmed = true, // CHANGED
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
launchMain {
|
||||
var resultRelation: UserRelation? = null
|
||||
runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
|
||||
if (!accessInfo.isMisskey) {
|
||||
@ -649,7 +570,6 @@ fun ActMain.followRequestDelete(
|
||||
}
|
||||
}
|
||||
}?.let { result ->
|
||||
|
||||
when (resultRelation) {
|
||||
null -> showToast(false, result.error)
|
||||
else -> {
|
||||
|
@ -14,7 +14,7 @@ import jp.juggler.subwaytooter.column.ColumnType
|
||||
import jp.juggler.subwaytooter.column.onListListUpdated
|
||||
import jp.juggler.subwaytooter.column.onListNameUpdated
|
||||
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||
import jp.juggler.subwaytooter.dialog.DlgTextInput
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.*
|
||||
@ -118,17 +118,10 @@ fun ActMain.listDelete(
|
||||
list: TootList,
|
||||
bConfirmed: Boolean = false,
|
||||
) {
|
||||
if (!bConfirmed) {
|
||||
DlgConfirm.openSimple(
|
||||
this,
|
||||
getString(R.string.list_delete_confirm, list.title)
|
||||
) {
|
||||
listDelete(accessInfo, list, bConfirmed = true)
|
||||
launchAndShowError {
|
||||
if (!bConfirmed) {
|
||||
confirm(R.string.list_delete_confirm, list.title)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
launchMain {
|
||||
runApiTask(accessInfo) { client ->
|
||||
if (accessInfo.isMisskey) {
|
||||
client.request(
|
||||
|
@ -4,10 +4,13 @@ import android.app.AlertDialog
|
||||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.runApiTask
|
||||
import jp.juggler.subwaytooter.api.syncAccountByAcct
|
||||
import jp.juggler.subwaytooter.column.onListMemberUpdated
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.*
|
||||
import okhttp3.Request
|
||||
@ -23,7 +26,7 @@ fun ActMain.listMemberAdd(
|
||||
bFollow: Boolean = false,
|
||||
onListMemberUpdated: (willRegistered: Boolean, bSuccess: Boolean) -> Unit = { _, _ -> },
|
||||
) {
|
||||
launchMain {
|
||||
launchAndShowError {
|
||||
runApiTask(accessInfo) { client ->
|
||||
val parser = TootParser(this, accessInfo)
|
||||
|
||||
@ -31,7 +34,6 @@ fun ActMain.listMemberAdd(
|
||||
|
||||
if (accessInfo.isMisskey) {
|
||||
// misskeyのリストはフォロー無関係
|
||||
|
||||
client.request(
|
||||
"/api/users/lists/push",
|
||||
accessInfo.putMisskeyApiToken().apply {
|
||||
@ -128,21 +130,17 @@ fun ActMain.listMemberAdd(
|
||||
|
||||
isNotFollowed() -> {
|
||||
if (!bFollow) {
|
||||
DlgConfirm.openSimple(
|
||||
this@listMemberAdd,
|
||||
getString(
|
||||
R.string.list_retry_with_follow,
|
||||
accessInfo.getFullAcct(localWho)
|
||||
)
|
||||
) {
|
||||
listMemberAdd(
|
||||
accessInfo,
|
||||
listId,
|
||||
localWho,
|
||||
bFollow = true,
|
||||
onListMemberUpdated = onListMemberUpdated
|
||||
)
|
||||
}
|
||||
confirm(
|
||||
R.string.list_retry_with_follow,
|
||||
accessInfo.getFullAcct(localWho)
|
||||
)
|
||||
listMemberAdd(
|
||||
accessInfo,
|
||||
listId,
|
||||
localWho,
|
||||
bFollow = true,
|
||||
onListMemberUpdated = onListMemberUpdated
|
||||
)
|
||||
} else {
|
||||
AlertDialog.Builder(this@listMemberAdd)
|
||||
.setCancelable(true)
|
||||
@ -165,7 +163,7 @@ fun ActMain.listMemberDelete(
|
||||
accessInfo: SavedAccount,
|
||||
listId: EntityId,
|
||||
localWho: TootAccount,
|
||||
onListMemberDeleted: (willRegistered: Boolean, bSuccess: Boolean) -> Unit = { _, _ -> },
|
||||
onListMemberDeleted: (willRegistered: Boolean, bSuccess: Boolean) -> Unit = { _, _ -> },
|
||||
) {
|
||||
launchMain {
|
||||
runApiTask(accessInfo) { client ->
|
||||
|
@ -1,14 +1,20 @@
|
||||
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.*
|
||||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.ApiTask
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.Host
|
||||
import jp.juggler.subwaytooter.api.entity.InstanceCapability
|
||||
import jp.juggler.subwaytooter.api.entity.TootReaction
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.api.runApiTask
|
||||
import jp.juggler.subwaytooter.api.syncStatus
|
||||
import jp.juggler.subwaytooter.column.Column
|
||||
import jp.juggler.subwaytooter.column.fireShowContent
|
||||
import jp.juggler.subwaytooter.column.updateEmojiReactionByApiResponse
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
||||
import jp.juggler.subwaytooter.dialog.EmojiPicker
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||
import jp.juggler.subwaytooter.dialog.launchEmojiPicker
|
||||
import jp.juggler.subwaytooter.dialog.pickAccount
|
||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
|
||||
@ -47,9 +53,13 @@ fun ActMain.reactionAdd(
|
||||
}
|
||||
|
||||
if (codeArg == null) {
|
||||
EmojiPicker(activity, accessInfo, closeOnSelected = true) { result ->
|
||||
launchEmojiPicker(
|
||||
activity,
|
||||
accessInfo,
|
||||
closeOnSelected = true
|
||||
) { emoji, _ ->
|
||||
var newUrl: String? = null
|
||||
val newCode: String = when (val emoji = result.emoji) {
|
||||
val newCode: String = when (emoji) {
|
||||
is UnicodeEmoji -> emoji.unifiedCode
|
||||
is CustomEmoji -> {
|
||||
newUrl = emoji.staticUrl
|
||||
@ -61,7 +71,7 @@ fun ActMain.reactionAdd(
|
||||
}
|
||||
}
|
||||
reactionAdd(column, status, newCode, newUrl)
|
||||
}.show()
|
||||
}
|
||||
return
|
||||
}
|
||||
var code = codeArg
|
||||
@ -96,40 +106,26 @@ fun ActMain.reactionAdd(
|
||||
return
|
||||
}
|
||||
|
||||
if (!isConfirmed) {
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
accessInfo,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg)
|
||||
DlgConfirm.open(
|
||||
activity,
|
||||
getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(accessInfo)),
|
||||
object : DlgConfirm.Callback {
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = accessInfo.confirm_reaction
|
||||
set(bv) {
|
||||
accessInfo.confirm_reaction = bv
|
||||
accessInfo.saveSetting()
|
||||
}
|
||||
activity.launchAndShowError {
|
||||
|
||||
override fun onOK() {
|
||||
reactionAdd(
|
||||
column,
|
||||
status,
|
||||
codeArg = code,
|
||||
urlArg = urlArg,
|
||||
isConfirmed = true
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!isConfirmed) {
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
accessInfo,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg)
|
||||
confirm(
|
||||
getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(accessInfo)),
|
||||
accessInfo.confirm_reaction,
|
||||
) { newConfirmEnabled ->
|
||||
accessInfo.confirm_reaction = newConfirmEnabled
|
||||
accessInfo.saveSetting()
|
||||
}
|
||||
}
|
||||
|
||||
launchMain {
|
||||
var resultStatus: TootStatus? = null
|
||||
runApiTask(accessInfo) { client ->
|
||||
when {
|
||||
@ -158,12 +154,7 @@ fun ActMain.reactionAdd(
|
||||
}
|
||||
}
|
||||
}?.let { result ->
|
||||
|
||||
val error = result.error
|
||||
if (error != null) {
|
||||
activity.showToast(false, error)
|
||||
return@launchMain
|
||||
}
|
||||
result.error?.let { error(it) }
|
||||
|
||||
when (val resCode = result.response?.code) {
|
||||
in 200 until 300 -> when (val newStatus = resultStatus) {
|
||||
@ -209,26 +200,19 @@ fun ActMain.reactionRemove(
|
||||
return
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
accessInfo,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
val emojiSpan = reaction.toSpannableStringBuilder(options, status)
|
||||
AlertDialog.Builder(activity)
|
||||
.setMessage(getString(R.string.reaction_remove_confirm, emojiSpan))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
reactionRemove(column, status, reaction, confirmed = true)
|
||||
}
|
||||
.show()
|
||||
return
|
||||
}
|
||||
launchAndShowError {
|
||||
|
||||
launchMain {
|
||||
if (!confirmed) {
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
accessInfo,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
val emojiSpan = reaction.toSpannableStringBuilder(options, status)
|
||||
confirm(R.string.reaction_remove_confirm, emojiSpan)
|
||||
}
|
||||
var resultStatus: TootStatus? = null
|
||||
runApiTask(accessInfo) { client ->
|
||||
when {
|
||||
@ -265,9 +249,13 @@ fun ActMain.reactionRemove(
|
||||
else -> {
|
||||
when (val newStatus = resultStatus) {
|
||||
null ->
|
||||
if (status.decreaseReactionMisskey(reaction.name, true, "removeReaction")) {
|
||||
if (status.decreaseReactionMisskey(reaction.name,
|
||||
true,
|
||||
"removeReaction")
|
||||
) {
|
||||
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
|
||||
column.fireShowContent(reason = "removeReaction complete", reset = true)
|
||||
column.fireShowContent(reason = "removeReaction complete",
|
||||
reset = true)
|
||||
}
|
||||
|
||||
else ->
|
||||
@ -290,15 +278,14 @@ private fun ActMain.reactionWithoutUi(
|
||||
resolvedStatus: TootStatus,
|
||||
reactionCode: String? = null,
|
||||
reactionImage: String? = null,
|
||||
isConfirmed: Boolean = false,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val activity = this
|
||||
|
||||
if (reactionCode == null) {
|
||||
EmojiPicker(activity, accessInfo, closeOnSelected = true) { result ->
|
||||
launchEmojiPicker(activity, accessInfo, closeOnSelected = true) { emoji, _ ->
|
||||
var newUrl: String? = null
|
||||
val newCode = when (val emoji = result.emoji) {
|
||||
val newCode = when (emoji) {
|
||||
is UnicodeEmoji -> emoji.unifiedCode
|
||||
is CustomEmoji -> {
|
||||
newUrl = emoji.staticUrl
|
||||
@ -314,78 +301,46 @@ private fun ActMain.reactionWithoutUi(
|
||||
resolvedStatus = resolvedStatus,
|
||||
reactionCode = newCode,
|
||||
reactionImage = newUrl,
|
||||
isConfirmed = isConfirmed,
|
||||
callback = callback
|
||||
)
|
||||
}.show()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val canMultipleReaction = InstanceCapability.canMultipleReaction(accessInfo)
|
||||
|
||||
if (!isConfirmed) {
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
accessInfo,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
val emojiSpan = TootReaction.toSpannableStringBuilder(options, reactionCode, reactionImage)
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
accessInfo,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
val emojiSpan = TootReaction.toSpannableStringBuilder(options, reactionCode, reactionImage)
|
||||
val isCustomEmoji = TootReaction.isCustomEmoji(reactionCode)
|
||||
val url = resolvedStatus.url
|
||||
|
||||
val isCustomEmoji = TootReaction.isCustomEmoji(reactionCode)
|
||||
val url = resolvedStatus.url
|
||||
launchAndShowError {
|
||||
when {
|
||||
isCustomEmoji && canMultipleReaction -> {
|
||||
showToast(false, "can't reaction with custom emoji from this account")
|
||||
return
|
||||
}
|
||||
isCustomEmoji && url?.likePleromaStatusUrl() == true -> DlgConfirm.openSimple(
|
||||
activity,
|
||||
getString(
|
||||
R.string.confirm_reaction_to_pleroma,
|
||||
emojiSpan,
|
||||
AcctColor.getNickname(accessInfo),
|
||||
resolvedStatus.account.acct.host?.pretty ?: "(null)"
|
||||
),
|
||||
) {
|
||||
reactionWithoutUi(
|
||||
accessInfo = accessInfo,
|
||||
resolvedStatus = resolvedStatus,
|
||||
reactionCode = reactionCode,
|
||||
reactionImage = reactionImage,
|
||||
isConfirmed = true,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
isCustomEmoji && canMultipleReaction ->
|
||||
error("can't reaction with custom emoji from this account")
|
||||
|
||||
else -> DlgConfirm.open(
|
||||
activity,
|
||||
isCustomEmoji && url?.likePleromaStatusUrl() == true -> confirm(
|
||||
R.string.confirm_reaction_to_pleroma,
|
||||
emojiSpan,
|
||||
AcctColor.getNickname(accessInfo),
|
||||
resolvedStatus.account.acct.host?.pretty ?: "(null)"
|
||||
)
|
||||
|
||||
else -> confirm(
|
||||
getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(accessInfo)),
|
||||
object : DlgConfirm.Callback {
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = accessInfo.confirm_reaction
|
||||
set(bv) {
|
||||
accessInfo.confirm_reaction = bv
|
||||
accessInfo.saveSetting()
|
||||
}
|
||||
|
||||
override fun onOK() {
|
||||
reactionWithoutUi(
|
||||
accessInfo = accessInfo,
|
||||
resolvedStatus = resolvedStatus,
|
||||
reactionCode = reactionCode,
|
||||
reactionImage = reactionImage,
|
||||
isConfirmed = true,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
})
|
||||
accessInfo.confirm_reaction,
|
||||
) { newConfirmEnabled ->
|
||||
accessInfo.confirm_reaction = newConfirmEnabled
|
||||
accessInfo.saveSetting()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
launchMain {
|
||||
// var resultStatus: TootStatus? = null
|
||||
runApiTask(accessInfo) { client ->
|
||||
when {
|
||||
@ -398,7 +353,9 @@ private fun ActMain.reactionWithoutUi(
|
||||
) // 成功すると204 no content
|
||||
|
||||
canMultipleReaction -> client.request(
|
||||
"/api/v1/pleroma/statuses/${resolvedStatus.id}/reactions/${reactionCode.encodePercent("@")}",
|
||||
"/api/v1/pleroma/statuses/${resolvedStatus.id}/reactions/${
|
||||
reactionCode.encodePercent("@")
|
||||
}",
|
||||
"".toFormRequestBody().toPut()
|
||||
) // 成功すると更新された投稿
|
||||
|
||||
@ -475,27 +432,30 @@ fun ActMain.reactionFromAnotherAccount(
|
||||
statusArg ?: return
|
||||
val activity = this
|
||||
|
||||
fun afterResolveStatus(actionAccount: SavedAccount, resolvedStatus: TootStatus) {
|
||||
val code = if (reaction == null) {
|
||||
null // あとで選択する
|
||||
} else {
|
||||
reactionFixCode(
|
||||
timelineAccount = timelineAccount,
|
||||
actionAccount = actionAccount,
|
||||
reaction = reaction,
|
||||
) ?: return // エラー終了の場合がある
|
||||
}
|
||||
|
||||
reactionWithoutUi(
|
||||
accessInfo = actionAccount,
|
||||
resolvedStatus = resolvedStatus,
|
||||
reactionCode = code,
|
||||
callback = reactionCompleteCallback,
|
||||
)
|
||||
}
|
||||
|
||||
launchMain {
|
||||
|
||||
fun afterResolveStatus(
|
||||
actionAccount: SavedAccount,
|
||||
resolvedStatus: TootStatus,
|
||||
) {
|
||||
val code = if (reaction == null) {
|
||||
null // あとで選択する
|
||||
} else {
|
||||
reactionFixCode(
|
||||
timelineAccount = timelineAccount,
|
||||
actionAccount = actionAccount,
|
||||
reaction = reaction,
|
||||
) ?: return // エラー終了の場合がある
|
||||
}
|
||||
|
||||
reactionWithoutUi(
|
||||
accessInfo = actionAccount,
|
||||
resolvedStatus = resolvedStatus,
|
||||
reactionCode = code,
|
||||
callback = reactionCompleteCallback,
|
||||
)
|
||||
}
|
||||
|
||||
val list = accountListCanReaction() ?: return@launchMain
|
||||
if (list.isEmpty()) {
|
||||
showToast(false, R.string.not_available_for_current_accounts)
|
||||
|
@ -2,22 +2,25 @@ package jp.juggler.subwaytooter.action
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import jp.juggler.subwaytooter.*
|
||||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.actmain.addColumn
|
||||
import jp.juggler.subwaytooter.actmain.reloadAccountSetting
|
||||
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||
import jp.juggler.subwaytooter.api.entity.TootScheduled
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.column.*
|
||||
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||
import jp.juggler.subwaytooter.dialog.pickAccount
|
||||
import jp.juggler.subwaytooter.table.AcctColor
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.emptyCallback
|
||||
import jp.juggler.util.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.Request
|
||||
import java.util.*
|
||||
|
||||
fun ActMain.clickStatusDelete(
|
||||
accessInfo: SavedAccount,
|
||||
@ -81,7 +84,8 @@ fun ActMain.clickScheduledToot(accessInfo: SavedAccount, item: TootScheduled, co
|
||||
scheduledPostEdit(accessInfo, item)
|
||||
}
|
||||
.addAction(getString(R.string.delete)) {
|
||||
scheduledPostDelete(accessInfo, item) {
|
||||
launchAndShowError {
|
||||
scheduledPostDelete(accessInfo, item)
|
||||
column.onScheduleDeleted(item)
|
||||
showToast(false, R.string.scheduled_post_deleted)
|
||||
}
|
||||
@ -99,61 +103,44 @@ fun ActMain.favourite(
|
||||
crossAccountMode: CrossAccountMode,
|
||||
callback: () -> Unit,
|
||||
bSet: Boolean = true,
|
||||
bConfirmed: Boolean = false,
|
||||
) {
|
||||
if (appState.isBusyFav(accessInfo, statusArg)) {
|
||||
showToast(false, R.string.wait_previous_operation)
|
||||
return
|
||||
}
|
||||
launchAndShowError {
|
||||
|
||||
// 必要なら確認を出す
|
||||
if (!bConfirmed && accessInfo.isMastodon) {
|
||||
DlgConfirm.open(
|
||||
this,
|
||||
getString(
|
||||
if (appState.isBusyFav(accessInfo, statusArg)) {
|
||||
showToast(false, R.string.wait_previous_operation)
|
||||
return@launchAndShowError
|
||||
}
|
||||
|
||||
// 必要なら確認を出す
|
||||
if (accessInfo.isMastodon) {
|
||||
confirm(
|
||||
getString(
|
||||
when (bSet) {
|
||||
true -> R.string.confirm_favourite_from
|
||||
else -> R.string.confirm_unfavourite_from
|
||||
},
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
when (bSet) {
|
||||
true -> R.string.confirm_favourite_from
|
||||
else -> R.string.confirm_unfavourite_from
|
||||
},
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
object : DlgConfirm.Callback {
|
||||
|
||||
override fun onOK() {
|
||||
favourite(
|
||||
accessInfo,
|
||||
statusArg,
|
||||
crossAccountMode,
|
||||
callback,
|
||||
bSet = bSet,
|
||||
bConfirmed = true
|
||||
)
|
||||
true -> accessInfo.confirm_favourite
|
||||
else -> accessInfo.confirm_unfavourite
|
||||
}
|
||||
) { newConfirmEnabled ->
|
||||
when (bSet) {
|
||||
true -> accessInfo.confirm_favourite = newConfirmEnabled
|
||||
else -> accessInfo.confirm_unfavourite = newConfirmEnabled
|
||||
}
|
||||
accessInfo.saveSetting()
|
||||
reloadAccountSetting(accessInfo)
|
||||
}
|
||||
}
|
||||
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = when (bSet) {
|
||||
true -> accessInfo.confirm_favourite
|
||||
else -> accessInfo.confirm_unfavourite
|
||||
}
|
||||
set(value) {
|
||||
when (bSet) {
|
||||
true -> accessInfo.confirm_favourite = value
|
||||
else -> accessInfo.confirm_unfavourite = value
|
||||
}
|
||||
accessInfo.saveSetting()
|
||||
reloadAccountSetting(accessInfo)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
//
|
||||
appState.setBusyFav(accessInfo, statusArg)
|
||||
|
||||
//
|
||||
appState.setBusyFav(accessInfo, statusArg)
|
||||
// ファボ表示を更新中にする
|
||||
showColumnMatchAccount(accessInfo)
|
||||
|
||||
// ファボ表示を更新中にする
|
||||
showColumnMatchAccount(accessInfo)
|
||||
|
||||
launchMain {
|
||||
var resultStatus: TootStatus? = null
|
||||
val result = runApiTask(
|
||||
accessInfo,
|
||||
@ -284,7 +271,6 @@ fun ActMain.bookmark(
|
||||
crossAccountMode: CrossAccountMode,
|
||||
callback: () -> Unit,
|
||||
bSet: Boolean = true,
|
||||
bConfirmed: Boolean = false,
|
||||
) {
|
||||
if (appState.isBusyFav(accessInfo, statusArg)) {
|
||||
showToast(false, R.string.wait_previous_operation)
|
||||
@ -294,47 +280,30 @@ fun ActMain.bookmark(
|
||||
showToast(false, R.string.misskey_account_not_supported)
|
||||
return
|
||||
}
|
||||
launchAndShowError {
|
||||
|
||||
// 必要なら確認を出す
|
||||
// ブックマークは解除する時だけ確認する
|
||||
if (!bConfirmed && !bSet) {
|
||||
DlgConfirm.open(
|
||||
this,
|
||||
getString(
|
||||
R.string.confirm_unbookmark_from,
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
object : DlgConfirm.Callback {
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = accessInfo.confirm_unbookmark
|
||||
set(value) {
|
||||
accessInfo.confirm_unbookmark = value
|
||||
accessInfo.saveSetting()
|
||||
reloadAccountSetting(accessInfo)
|
||||
}
|
||||
// 必要なら確認を出す
|
||||
// ブックマークは解除する時だけ確認する
|
||||
if (!bSet) {
|
||||
confirm(
|
||||
getString(
|
||||
R.string.confirm_unbookmark_from,
|
||||
AcctColor.getNickname(accessInfo)
|
||||
),
|
||||
accessInfo.confirm_unbookmark
|
||||
) { newConfirmEnabled ->
|
||||
accessInfo.confirm_unbookmark = newConfirmEnabled
|
||||
accessInfo.saveSetting()
|
||||
reloadAccountSetting(accessInfo)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOK() {
|
||||
bookmark(
|
||||
accessInfo = accessInfo,
|
||||
statusArg = statusArg,
|
||||
crossAccountMode = crossAccountMode,
|
||||
callback = callback,
|
||||
bSet = bSet,
|
||||
bConfirmed = true,
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
//
|
||||
appState.setBusyBookmark(accessInfo, statusArg)
|
||||
|
||||
//
|
||||
appState.setBusyBookmark(accessInfo, statusArg)
|
||||
// ファボ表示を更新中にする
|
||||
showColumnMatchAccount(accessInfo)
|
||||
|
||||
// ファボ表示を更新中にする
|
||||
showColumnMatchAccount(accessInfo)
|
||||
|
||||
//
|
||||
launchMain {
|
||||
var resultStatus: TootStatus? = null
|
||||
val result = runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
|
||||
val targetStatus = if (crossAccountMode.isRemote) {
|
||||
@ -580,40 +549,21 @@ fun ActMain.statusEdit(
|
||||
}
|
||||
}
|
||||
|
||||
fun ActMain.scheduledPostDelete(
|
||||
suspend fun ActMain.scheduledPostDelete(
|
||||
accessInfo: SavedAccount,
|
||||
item: TootScheduled,
|
||||
bConfirmed: Boolean = false,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val act = this@scheduledPostDelete
|
||||
if (!bConfirmed) {
|
||||
DlgConfirm.openSimple(
|
||||
act,
|
||||
getString(R.string.scheduled_status_delete_confirm)
|
||||
) {
|
||||
scheduledPostDelete(
|
||||
accessInfo,
|
||||
item,
|
||||
bConfirmed = true,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
launchMain {
|
||||
runApiTask(accessInfo) { client ->
|
||||
client.request(
|
||||
"/api/v1/scheduled_statuses/${item.id}",
|
||||
Request.Builder().delete()
|
||||
)
|
||||
}?.let { result ->
|
||||
when (val error = result.error) {
|
||||
null -> callback()
|
||||
else -> showToast(true, error)
|
||||
}
|
||||
}
|
||||
confirm(R.string.scheduled_status_delete_confirm)
|
||||
}
|
||||
val result = runApiTask(accessInfo) { client ->
|
||||
client.request(
|
||||
"/api/v1/scheduled_statuses/${item.id}",
|
||||
Request.Builder().delete()
|
||||
)
|
||||
} ?: throw CancellationException("scheduledPostDelete cancelled.")
|
||||
result.error?.notEmpty()?.let { error(it) }
|
||||
}
|
||||
|
||||
fun ActMain.scheduledPostEdit(
|
||||
|
@ -5,16 +5,19 @@ import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.GravityCompat
|
||||
import jp.juggler.subwaytooter.*
|
||||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.Styler
|
||||
import jp.juggler.subwaytooter.actpost.CompletionHelper
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.api.entity.TootVisibility
|
||||
import jp.juggler.subwaytooter.dialog.pickAccount
|
||||
import jp.juggler.subwaytooter.pref.PrefB
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.PostCompleteCallback
|
||||
import jp.juggler.subwaytooter.util.PostImpl
|
||||
import jp.juggler.subwaytooter.util.PostResult
|
||||
import jp.juggler.util.hideKeyboard
|
||||
import jp.juggler.util.launchAndShowError
|
||||
import jp.juggler.util.launchMain
|
||||
import org.jetbrains.anko.imageResource
|
||||
|
||||
@ -104,39 +107,39 @@ fun ActMain.performQuickPost(account: SavedAccount?) {
|
||||
|
||||
etQuickPost.hideKeyboard()
|
||||
|
||||
PostImpl(
|
||||
activity = this,
|
||||
account = account,
|
||||
content = etQuickPost.text.toString().trim { it <= ' ' },
|
||||
spoilerText = null,
|
||||
visibilityArg = when (quickPostVisibility) {
|
||||
TootVisibility.AccountSetting -> account.visibility
|
||||
else -> quickPostVisibility
|
||||
},
|
||||
bNSFW = false,
|
||||
inReplyToId = null,
|
||||
attachmentListArg = null,
|
||||
enqueteItemsArg = null,
|
||||
pollType = null,
|
||||
pollExpireSeconds = 0,
|
||||
pollHideTotals = false,
|
||||
pollMultipleChoice = false,
|
||||
scheduledAt = 0L,
|
||||
scheduledId = null,
|
||||
redraftStatusId = null,
|
||||
editStatusId = null,
|
||||
emojiMapCustom = App1.custom_emoji_lister.getMap(account),
|
||||
useQuoteToot = false,
|
||||
callback = object : PostCompleteCallback {
|
||||
override fun onScheduledPostComplete(targetAccount: SavedAccount) {}
|
||||
override fun onPostComplete(targetAccount: SavedAccount, status: TootStatus) {
|
||||
etQuickPost.setText("")
|
||||
postedAcct = targetAccount.acct
|
||||
postedStatusId = status.id
|
||||
postedReplyId = status.in_reply_to_id
|
||||
postedRedraftId = null
|
||||
refreshAfterPost()
|
||||
}
|
||||
launchAndShowError {
|
||||
val postResult = PostImpl(
|
||||
activity = this@performQuickPost,
|
||||
account = account,
|
||||
content = etQuickPost.text.toString().trim { it <= ' ' },
|
||||
spoilerText = null,
|
||||
visibilityArg = when (quickPostVisibility) {
|
||||
TootVisibility.AccountSetting -> account.visibility
|
||||
else -> quickPostVisibility
|
||||
},
|
||||
bNSFW = false,
|
||||
inReplyToId = null,
|
||||
attachmentListArg = null,
|
||||
enqueteItemsArg = null,
|
||||
pollType = null,
|
||||
pollExpireSeconds = 0,
|
||||
pollHideTotals = false,
|
||||
pollMultipleChoice = false,
|
||||
scheduledAt = 0L,
|
||||
scheduledId = null,
|
||||
redraftStatusId = null,
|
||||
editStatusId = null,
|
||||
emojiMapCustom = App1.custom_emoji_lister.getMapNonBlocking(account),
|
||||
useQuoteToot = false,
|
||||
).runSuspend()
|
||||
|
||||
if (postResult is PostResult.Normal) {
|
||||
etQuickPost.setText("")
|
||||
postedAcct = postResult.targetAccount.acct
|
||||
postedStatusId = postResult.status.id
|
||||
postedReplyId = postResult.status.in_reply_to_id
|
||||
postedRedraftId = null
|
||||
refreshAfterPost()
|
||||
}
|
||||
).run()
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,10 @@ fun ActPost.selectAccount(a: SavedAccount?) {
|
||||
views.btnAccount.setTextColor(attrColor(android.R.attr.textColorPrimary))
|
||||
views.btnAccount.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
|
||||
} else {
|
||||
|
||||
// 先読みしてキャッシュに保持しておく
|
||||
App1.custom_emoji_lister.getList(a) {
|
||||
launchMain {
|
||||
// 先読みしてキャッシュに保持しておく
|
||||
// 何もしない
|
||||
App1.custom_emoji_lister.getList(a)
|
||||
}
|
||||
|
||||
val ac = AcctColor.load(a)
|
||||
|
@ -288,64 +288,67 @@ fun ActPost.performMore() {
|
||||
}
|
||||
|
||||
fun ActPost.performPost() {
|
||||
// アップロード中は投稿できない
|
||||
if (attachmentList.any { it.status == PostAttachment.Status.Progress }) {
|
||||
showToast(false, R.string.media_attachment_still_uploading)
|
||||
return
|
||||
}
|
||||
|
||||
val account = this.account ?: return
|
||||
var pollType: TootPollsType? = null
|
||||
var pollItems: ArrayList<String>? = null
|
||||
var pollExpireSeconds = 0
|
||||
var pollHideTotals = false
|
||||
var pollMultipleChoice = false
|
||||
when (views.spPollType.selectedItemPosition) {
|
||||
1 -> {
|
||||
pollType = TootPollsType.Mastodon
|
||||
pollItems = pollChoiceList()
|
||||
pollExpireSeconds = pollExpireSeconds()
|
||||
pollHideTotals = views.cbHideTotals.isChecked
|
||||
pollMultipleChoice = views.cbMultipleChoice.isChecked
|
||||
val activity = this
|
||||
launchAndShowError {
|
||||
// アップロード中は投稿できない
|
||||
if (attachmentList.any { it.status == PostAttachment.Status.Progress }) {
|
||||
showToast(false, R.string.media_attachment_still_uploading)
|
||||
return@launchAndShowError
|
||||
}
|
||||
2 -> {
|
||||
pollType = TootPollsType.FriendsNico
|
||||
pollItems = pollChoiceList()
|
||||
}
|
||||
}
|
||||
|
||||
PostImpl(
|
||||
activity = this,
|
||||
account = account,
|
||||
content = views.etContent.text.toString().trim { it <= ' ' },
|
||||
spoilerText = when {
|
||||
!views.cbContentWarning.isChecked -> null
|
||||
else -> views.etContentWarning.text.toString().trim { it <= ' ' }
|
||||
},
|
||||
visibilityArg = states.visibility ?: TootVisibility.Public,
|
||||
bNSFW = views.cbNSFW.isChecked,
|
||||
inReplyToId = states.inReplyToId,
|
||||
attachmentListArg = this.attachmentList,
|
||||
enqueteItemsArg = pollItems,
|
||||
pollType = pollType,
|
||||
pollExpireSeconds = pollExpireSeconds,
|
||||
pollHideTotals = pollHideTotals,
|
||||
pollMultipleChoice = pollMultipleChoice,
|
||||
scheduledAt = states.timeSchedule,
|
||||
scheduledId = scheduledStatus?.id,
|
||||
redraftStatusId = states.redraftStatusId,
|
||||
editStatusId = states.editStatusId,
|
||||
emojiMapCustom = App1.custom_emoji_lister.getMap(account),
|
||||
useQuoteToot = views.cbQuote.isChecked,
|
||||
callback = object : PostCompleteCallback {
|
||||
override fun onPostComplete(targetAccount: SavedAccount, status: TootStatus) {
|
||||
val account = activity.account ?: return@launchAndShowError
|
||||
var pollType: TootPollsType? = null
|
||||
var pollItems: ArrayList<String>? = null
|
||||
var pollExpireSeconds = 0
|
||||
var pollHideTotals = false
|
||||
var pollMultipleChoice = false
|
||||
when (views.spPollType.selectedItemPosition) {
|
||||
1 -> {
|
||||
pollType = TootPollsType.Mastodon
|
||||
pollItems = pollChoiceList()
|
||||
pollExpireSeconds = pollExpireSeconds()
|
||||
pollHideTotals = views.cbHideTotals.isChecked
|
||||
pollMultipleChoice = views.cbMultipleChoice.isChecked
|
||||
}
|
||||
2 -> {
|
||||
pollType = TootPollsType.FriendsNico
|
||||
pollItems = pollChoiceList()
|
||||
}
|
||||
}
|
||||
|
||||
val postResult = PostImpl(
|
||||
activity = activity,
|
||||
account = account,
|
||||
content = views.etContent.text.toString().trim { it <= ' ' },
|
||||
spoilerText = when {
|
||||
!views.cbContentWarning.isChecked -> null
|
||||
else -> views.etContentWarning.text.toString().trim { it <= ' ' }
|
||||
},
|
||||
visibilityArg = states.visibility ?: TootVisibility.Public,
|
||||
bNSFW = views.cbNSFW.isChecked,
|
||||
inReplyToId = states.inReplyToId,
|
||||
attachmentListArg = activity.attachmentList,
|
||||
enqueteItemsArg = pollItems,
|
||||
pollType = pollType,
|
||||
pollExpireSeconds = pollExpireSeconds,
|
||||
pollHideTotals = pollHideTotals,
|
||||
pollMultipleChoice = pollMultipleChoice,
|
||||
scheduledAt = states.timeSchedule,
|
||||
scheduledId = scheduledStatus?.id,
|
||||
redraftStatusId = states.redraftStatusId,
|
||||
editStatusId = states.editStatusId,
|
||||
emojiMapCustom = App1.custom_emoji_lister.getMapNonBlocking(account),
|
||||
useQuoteToot = views.cbQuote.isChecked,
|
||||
).runSuspend()
|
||||
when(postResult){
|
||||
is PostResult.Normal ->{
|
||||
val data = Intent()
|
||||
data.putExtra(ActPost.EXTRA_POSTED_ACCT, targetAccount.acct.ascii)
|
||||
status.id.putTo(data, ActPost.EXTRA_POSTED_STATUS_ID)
|
||||
data.putExtra(ActPost.EXTRA_POSTED_ACCT, postResult.targetAccount.acct.ascii)
|
||||
postResult.status.id.putTo(data, ActPost.EXTRA_POSTED_STATUS_ID)
|
||||
states.redraftStatusId?.putTo(data, ActPost.EXTRA_POSTED_REDRAFT_ID)
|
||||
status.in_reply_to_id?.putTo(data, ActPost.EXTRA_POSTED_REPLY_ID)
|
||||
postResult.status.in_reply_to_id?.putTo(data, ActPost.EXTRA_POSTED_REPLY_ID)
|
||||
if (states.editStatusId != null) {
|
||||
data.putExtra(ActPost.KEY_EDIT_STATUS, status.json.toString())
|
||||
data.putExtra(ActPost.KEY_EDIT_STATUS, postResult.status.json.toString())
|
||||
}
|
||||
ActMain.refActMain?.get()?.onCompleteActPost(data)
|
||||
|
||||
@ -360,11 +363,10 @@ fun ActPost.performPost() {
|
||||
this@performPost.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScheduledPostComplete(targetAccount: SavedAccount) {
|
||||
is PostResult.Scheduled ->{
|
||||
showToast(false, getString(R.string.scheduled_status_sent))
|
||||
val data = Intent()
|
||||
data.putExtra(ActPost.EXTRA_POSTED_ACCT, targetAccount.acct.ascii)
|
||||
data.putExtra(ActPost.EXTRA_POSTED_ACCT,postResult. targetAccount.acct.ascii)
|
||||
|
||||
if (isMultiWindowPost) {
|
||||
resetText()
|
||||
@ -378,7 +380,7 @@ fun ActPost.performPost() {
|
||||
}
|
||||
}
|
||||
}
|
||||
).run()
|
||||
}
|
||||
}
|
||||
|
||||
fun ActPost.showContentWarningEnabled() {
|
||||
|
@ -8,11 +8,9 @@ import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.entity.TootTag
|
||||
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
||||
import jp.juggler.subwaytooter.dialog.EmojiPicker
|
||||
import jp.juggler.subwaytooter.dialog.EmojiPickerResult
|
||||
import jp.juggler.subwaytooter.dialog.launchEmojiPicker
|
||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||
import jp.juggler.subwaytooter.emoji.EmojiBase
|
||||
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
|
||||
@ -26,7 +24,6 @@ import jp.juggler.subwaytooter.util.EmojiDecoder
|
||||
import jp.juggler.subwaytooter.util.PopupAutoCompleteAcct
|
||||
import jp.juggler.subwaytooter.view.MyEditText
|
||||
import jp.juggler.util.*
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
// 入力補完機能
|
||||
@ -108,7 +105,7 @@ class CompletionHelper(
|
||||
|
||||
private var accessInfo: SavedAccount? = null
|
||||
|
||||
private val onEmojiListLoad: (list: ArrayList<CustomEmoji>) -> Unit = {
|
||||
private val onEmojiListLoad: (list: List<CustomEmoji>) -> Unit = {
|
||||
if (popup?.isShowing == true) procTextChanged.run()
|
||||
}
|
||||
|
||||
@ -261,9 +258,12 @@ class CompletionHelper(
|
||||
) = buildList<CharSequence> {
|
||||
accessInfo ?: return@buildList
|
||||
|
||||
val customList =
|
||||
App1.custom_emoji_lister.getListWithAliases(accessInfo, onEmojiListLoad)
|
||||
?: return@buildList
|
||||
val customList = App1.custom_emoji_lister.getListNonBlocking(
|
||||
accessInfo,
|
||||
withAliases = true,
|
||||
callback = onEmojiListLoad
|
||||
)
|
||||
?: return@buildList
|
||||
|
||||
for (item in customList) {
|
||||
if (size >= limit) break
|
||||
@ -310,7 +310,12 @@ class CompletionHelper(
|
||||
|
||||
fun setInstance(accessInfo: SavedAccount?) {
|
||||
this.accessInfo = accessInfo
|
||||
accessInfo?.let { App1.custom_emoji_lister.getList(it, onEmojiListLoad) }
|
||||
accessInfo?.let {
|
||||
App1.custom_emoji_lister.getListNonBlocking(
|
||||
it,
|
||||
callback = onEmojiListLoad
|
||||
)
|
||||
}
|
||||
if (popup?.isShowing == true) procTextChanged.run()
|
||||
}
|
||||
|
||||
@ -400,8 +405,10 @@ class CompletionHelper(
|
||||
// et.setCustomSelectionActionModeCallback( action_mode_callback );
|
||||
}
|
||||
|
||||
private fun SpannableStringBuilder.appendEmoji(result: EmojiPickerResult) =
|
||||
appendEmoji(result.bInstanceHasCustomEmoji, result.emoji)
|
||||
private fun SpannableStringBuilder.appendEmoji(
|
||||
emoji: EmojiBase,
|
||||
bInstanceHasCustomEmoji: Boolean,
|
||||
) = appendEmoji(bInstanceHasCustomEmoji, emoji)
|
||||
|
||||
private fun SpannableStringBuilder.appendEmoji(
|
||||
bInstanceHasCustomEmoji: Boolean,
|
||||
@ -434,21 +441,22 @@ class CompletionHelper(
|
||||
}
|
||||
|
||||
private val openPickerEmoji: Runnable = Runnable {
|
||||
EmojiPicker(
|
||||
activity, accessInfo,
|
||||
launchEmojiPicker(
|
||||
activity,
|
||||
accessInfo,
|
||||
closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected(pref)
|
||||
) { result ->
|
||||
val et = this.et ?: return@EmojiPicker
|
||||
) { emoji, bInstanceHasCustomEmoji ->
|
||||
val et = this@CompletionHelper.et ?: return@launchEmojiPicker
|
||||
|
||||
val src = et.text ?: ""
|
||||
val srcLength = src.length
|
||||
val end = min(srcLength, et.selectionEnd)
|
||||
val start = src.lastIndexOf(':', end - 1)
|
||||
if (start == -1 || end - start < 1) return@EmojiPicker
|
||||
if (start == -1 || end - start < 1) return@launchEmojiPicker
|
||||
|
||||
val sb = SpannableStringBuilder()
|
||||
.append(src.subSequence(0, start))
|
||||
.appendEmoji(result)
|
||||
.appendEmoji(emoji, bInstanceHasCustomEmoji)
|
||||
|
||||
val newSelection = sb.length
|
||||
if (end < srcLength) sb.append(src.subSequence(end, srcLength))
|
||||
@ -463,15 +471,16 @@ class CompletionHelper(
|
||||
activity,
|
||||
"PostHelper/EmojiPicker/cb"
|
||||
).handler.post { et.showKeyboard() }
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun openEmojiPickerFromMore() {
|
||||
EmojiPicker(
|
||||
activity, accessInfo,
|
||||
launchEmojiPicker(
|
||||
activity,
|
||||
accessInfo,
|
||||
closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected(pref)
|
||||
) { result ->
|
||||
val et = this.et ?: return@EmojiPicker
|
||||
) { emoji, bInstanceHasCustomEmoji ->
|
||||
val et = this@CompletionHelper.et ?: return@launchEmojiPicker
|
||||
|
||||
val src = et.text ?: ""
|
||||
val srcLength = src.length
|
||||
@ -480,7 +489,7 @@ class CompletionHelper(
|
||||
|
||||
val sb = SpannableStringBuilder()
|
||||
.append(src.subSequence(0, start))
|
||||
.appendEmoji(result)
|
||||
.appendEmoji(emoji, bInstanceHasCustomEmoji)
|
||||
|
||||
val newSelection = sb.length
|
||||
if (end < srcLength) sb.append(src.subSequence(end, srcLength))
|
||||
@ -489,7 +498,7 @@ class CompletionHelper(
|
||||
et.setSelection(newSelection)
|
||||
|
||||
procTextChanged.run()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun SpannableStringBuilder.appendHashTag(tagWithoutSharp: String): SpannableStringBuilder {
|
||||
|
@ -215,7 +215,7 @@ class TootReaction(
|
||||
// そのドメインの絵文字一覧を取得済みなら
|
||||
// それを使う
|
||||
App1.custom_emoji_lister
|
||||
.getMap(accessInfo)
|
||||
.getMapNonBlocking(accessInfo)
|
||||
?.get(key)
|
||||
?.chooseUrl()
|
||||
?.notEmpty()
|
||||
|
@ -21,7 +21,7 @@ import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.api.runApiTask
|
||||
import jp.juggler.subwaytooter.column.Column
|
||||
import jp.juggler.subwaytooter.column.getContentColor
|
||||
import jp.juggler.subwaytooter.dialog.EmojiPicker
|
||||
import jp.juggler.subwaytooter.dialog.launchEmojiPicker
|
||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
|
||||
import jp.juggler.subwaytooter.pref.PrefB
|
||||
@ -129,7 +129,7 @@ private fun ColumnViewHolder.showAnnouncementsEmpty() {
|
||||
private fun ColumnViewHolder.showAnnouncementColors(
|
||||
expand: Boolean,
|
||||
enablePaging: Boolean,
|
||||
contentColor: Int
|
||||
contentColor: Int,
|
||||
) {
|
||||
val alphaPrevNext = if (enablePaging) 1f else 0.3f
|
||||
|
||||
@ -413,14 +413,14 @@ private fun ColumnViewHolder.showReactions(
|
||||
|
||||
fun ColumnViewHolder.reactionAdd(item: TootAnnouncement, sample: TootReaction?) {
|
||||
val column = column ?: return
|
||||
|
||||
if (sample == null) {
|
||||
EmojiPicker(activity, column.accessInfo, closeOnSelected = true) { result ->
|
||||
val emoji = result.emoji
|
||||
launchEmojiPicker(activity, column.accessInfo, closeOnSelected = true) { emoji, _ ->
|
||||
val code = when (emoji) {
|
||||
is UnicodeEmoji -> emoji.unifiedCode
|
||||
is CustomEmoji -> emoji.shortcode
|
||||
}
|
||||
ColumnViewHolder.log.d("addReaction: $code ${result.emoji.javaClass.simpleName}")
|
||||
ColumnViewHolder.log.d("addReaction: $code ${emoji.javaClass.simpleName}")
|
||||
reactionAdd(item, TootReaction.parseFedibird(jsonObject {
|
||||
put("name", code)
|
||||
put("count", 1)
|
||||
@ -431,11 +431,10 @@ fun ColumnViewHolder.reactionAdd(item: TootAnnouncement, sample: TootReaction?)
|
||||
putNotNull("static_url", emoji.staticUrl)
|
||||
}
|
||||
}))
|
||||
}.show()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
launchMain {
|
||||
activity.launchAndShowError {
|
||||
activity.runApiTask(column.accessInfo) { client ->
|
||||
client.request(
|
||||
"/api/v1/announcements/${item.id}/reactions/${sample.name.encodePercent()}",
|
||||
|
@ -7,20 +7,21 @@ import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.entity.TootReaction
|
||||
import jp.juggler.subwaytooter.column.getContentColor
|
||||
import jp.juggler.subwaytooter.dialog.EmojiPicker
|
||||
import jp.juggler.subwaytooter.dialog.launchEmojiPicker
|
||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
|
||||
import jp.juggler.subwaytooter.util.DecodeOptions
|
||||
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
|
||||
import jp.juggler.subwaytooter.util.minWidthCompat
|
||||
import jp.juggler.subwaytooter.util.startMargin
|
||||
import jp.juggler.util.launchAndShowError
|
||||
import org.jetbrains.anko.allCaps
|
||||
|
||||
fun ColumnViewHolder.addEmojiQuery(reaction: TootReaction? = null) {
|
||||
val column = this.column ?: return
|
||||
if (reaction == null) {
|
||||
EmojiPicker(activity, column.accessInfo, closeOnSelected = true) { result ->
|
||||
val newReaction = when (val emoji = result.emoji) {
|
||||
launchEmojiPicker(activity, column.accessInfo, closeOnSelected = true) { emoji, _ ->
|
||||
val newReaction = when (emoji) {
|
||||
is UnicodeEmoji -> TootReaction(name = emoji.unifiedCode)
|
||||
is CustomEmoji -> TootReaction(
|
||||
name = emoji.shortcode,
|
||||
@ -29,7 +30,7 @@ fun ColumnViewHolder.addEmojiQuery(reaction: TootReaction? = null) {
|
||||
)
|
||||
}
|
||||
addEmojiQuery(newReaction)
|
||||
}.show()
|
||||
}
|
||||
return
|
||||
}
|
||||
val list = TootReaction.decodeEmojiQuery(column.searchQuery).toMutableList()
|
||||
@ -49,63 +50,64 @@ private fun ColumnViewHolder.removeEmojiQuery(target: TootReaction?) {
|
||||
|
||||
fun ColumnViewHolder.updateReactionQueryView() {
|
||||
val column = this.column ?: return
|
||||
|
||||
flEmoji.removeAllViews()
|
||||
|
||||
for (invalidator in emojiQueryInvalidatorList) {
|
||||
invalidator.register(null)
|
||||
}
|
||||
emojiQueryInvalidatorList.clear()
|
||||
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
column.accessInfo,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
|
||||
val act = this.activity // not Button(View).getActivity()
|
||||
act.launchAndShowError {
|
||||
|
||||
val buttonHeight = ActMain.boostButtonSize
|
||||
val marginBetween = (buttonHeight.toFloat() * 0.05f + 0.5f).toInt()
|
||||
flEmoji.removeAllViews()
|
||||
|
||||
val paddingH = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
|
||||
val paddingV = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
|
||||
|
||||
val contentColor = column.getContentColor()
|
||||
|
||||
TootReaction.decodeEmojiQuery(column.searchQuery).forEachIndexed { index, reaction ->
|
||||
val ssb = reaction.toSpannableStringBuilder(options, status = null)
|
||||
|
||||
val b = AppCompatButton(activity).apply {
|
||||
layoutParams = FlexboxLayout.LayoutParams(
|
||||
FlexboxLayout.LayoutParams.WRAP_CONTENT,
|
||||
buttonHeight
|
||||
).apply {
|
||||
if (index > 0) startMargin = marginBetween
|
||||
}
|
||||
minWidthCompat = buttonHeight
|
||||
|
||||
background = ContextCompat.getDrawable(act, R.drawable.btn_bg_transparent_round6dp)
|
||||
|
||||
setTextColor(contentColor)
|
||||
setPadding(paddingH, paddingV, paddingH, paddingV)
|
||||
|
||||
text = ssb
|
||||
|
||||
allCaps = false
|
||||
tag = reaction
|
||||
|
||||
setOnLongClickListener {
|
||||
removeEmojiQuery(it.tag as? TootReaction)
|
||||
true
|
||||
}
|
||||
// カスタム絵文字の場合、アニメーション等のコールバックを処理する必要がある
|
||||
val invalidator = NetworkEmojiInvalidator(act.handler, this)
|
||||
invalidator.register(ssb)
|
||||
emojiQueryInvalidatorList.add(invalidator)
|
||||
for (invalidator in emojiQueryInvalidatorList) {
|
||||
invalidator.register(null)
|
||||
}
|
||||
emojiQueryInvalidatorList.clear()
|
||||
|
||||
val options = DecodeOptions(
|
||||
act,
|
||||
column.accessInfo,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
|
||||
val buttonHeight = ActMain.boostButtonSize
|
||||
val marginBetween = (buttonHeight.toFloat() * 0.05f + 0.5f).toInt()
|
||||
|
||||
val paddingH = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
|
||||
val paddingV = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
|
||||
|
||||
val contentColor = column.getContentColor()
|
||||
|
||||
TootReaction.decodeEmojiQuery(column.searchQuery).forEachIndexed { index, reaction ->
|
||||
val ssb = reaction.toSpannableStringBuilder(options, status = null)
|
||||
|
||||
val b = AppCompatButton(activity).apply {
|
||||
layoutParams = FlexboxLayout.LayoutParams(
|
||||
FlexboxLayout.LayoutParams.WRAP_CONTENT,
|
||||
buttonHeight
|
||||
).apply {
|
||||
if (index > 0) startMargin = marginBetween
|
||||
}
|
||||
minWidthCompat = buttonHeight
|
||||
|
||||
background = ContextCompat.getDrawable(act, R.drawable.btn_bg_transparent_round6dp)
|
||||
|
||||
setTextColor(contentColor)
|
||||
setPadding(paddingH, paddingV, paddingH, paddingV)
|
||||
|
||||
text = ssb
|
||||
|
||||
allCaps = false
|
||||
tag = reaction
|
||||
|
||||
setOnLongClickListener {
|
||||
removeEmojiQuery(it.tag as? TootReaction)
|
||||
true
|
||||
}
|
||||
// カスタム絵文字の場合、アニメーション等のコールバックを処理する必要がある
|
||||
val invalidator = NetworkEmojiInvalidator(act.handler, this)
|
||||
invalidator.register(ssb)
|
||||
emojiQueryInvalidatorList.add(invalidator)
|
||||
}
|
||||
flEmoji.addView(b)
|
||||
}
|
||||
flEmoji.addView(b)
|
||||
}
|
||||
}
|
||||
|
@ -1,60 +1,125 @@
|
||||
package jp.juggler.subwaytooter.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.databinding.DlgConfirmBinding
|
||||
import jp.juggler.util.dismissSafe
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
object DlgConfirm {
|
||||
|
||||
interface Callback {
|
||||
var isConfirmEnabled: Boolean
|
||||
// interface Callback {
|
||||
// var isConfirmEnabled: Boolean
|
||||
//
|
||||
// fun onOK()
|
||||
// }
|
||||
|
||||
fun onOK()
|
||||
}
|
||||
// @SuppressLint("InflateParams")
|
||||
// fun open(activity: Activity, message: String, callback: Callback): Dialog {
|
||||
//
|
||||
// if (!callback.isConfirmEnabled) {
|
||||
// callback.onOK()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false)
|
||||
// val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
|
||||
// val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext)
|
||||
// tvMessage.text = message
|
||||
//
|
||||
// AlertDialog.Builder(activity)
|
||||
// .setView(view)
|
||||
// .setCancelable(true)
|
||||
// .setNegativeButton(R.string.cancel, null)
|
||||
// .setPositiveButton(R.string.ok) { _, _ ->
|
||||
// if (cbSkipNext.isChecked) {
|
||||
// callback.isConfirmEnabled = false
|
||||
// }
|
||||
// callback.onOK()
|
||||
// }
|
||||
// .show()
|
||||
// }
|
||||
|
||||
// @SuppressLint("InflateParams")
|
||||
// fun openSimple(activity: Activity, message: String, callback: () -> Unit) {
|
||||
// val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false)
|
||||
// val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
|
||||
// val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext)
|
||||
// tvMessage.text = message
|
||||
// cbSkipNext.visibility = View.GONE
|
||||
//
|
||||
// AlertDialog.Builder(activity)
|
||||
// .setView(view)
|
||||
// .setCancelable(true)
|
||||
// .setNegativeButton(R.string.cancel, null)
|
||||
// .setPositiveButton(R.string.ok) { _, _ -> callback() }
|
||||
// .show()
|
||||
// }
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
fun open(activity: Activity, message: String, callback: Callback) {
|
||||
|
||||
if (!callback.isConfirmEnabled) {
|
||||
callback.onOK()
|
||||
return
|
||||
}
|
||||
|
||||
val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false)
|
||||
val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
|
||||
val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext)
|
||||
tvMessage.text = message
|
||||
|
||||
AlertDialog.Builder(activity)
|
||||
.setView(view)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
if (cbSkipNext.isChecked) {
|
||||
callback.isConfirmEnabled = false
|
||||
suspend fun AppCompatActivity.confirm(
|
||||
message: String,
|
||||
getConfirmEnabled: Boolean,
|
||||
setConfirmEnabled: (newConfirmEnabled: Boolean) -> Unit,
|
||||
) {
|
||||
if (!getConfirmEnabled) return
|
||||
suspendCancellableCoroutine<Unit> { cont ->
|
||||
try {
|
||||
val views = DlgConfirmBinding.inflate(layoutInflater)
|
||||
views.tvMessage.text = message
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(views.root)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
if (views.cbSkipNext.isChecked) {
|
||||
setConfirmEnabled(false)
|
||||
}
|
||||
if (cont.isActive) cont.resume(Unit)
|
||||
}
|
||||
dialog.setOnDismissListener {
|
||||
if (cont.isActive) cont.resumeWithException(CancellationException("dialog cancelled."))
|
||||
}
|
||||
callback.onOK()
|
||||
dialog.show()
|
||||
} catch (ex: Throwable) {
|
||||
cont.resumeWithException(ex)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
fun openSimple(activity: Activity, message: String, callback: () -> Unit) {
|
||||
val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false)
|
||||
val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
|
||||
val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext)
|
||||
tvMessage.text = message
|
||||
cbSkipNext.visibility = View.GONE
|
||||
suspend fun AppCompatActivity.confirm(@StringRes messageId: Int, vararg args: Any?) =
|
||||
confirm(getString(messageId, args))
|
||||
|
||||
AlertDialog.Builder(activity)
|
||||
.setView(view)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> callback() }
|
||||
.show()
|
||||
suspend fun AppCompatActivity.confirm(message: String) {
|
||||
suspendCancellableCoroutine<Unit> { cont ->
|
||||
try {
|
||||
val views = DlgConfirmBinding.inflate(layoutInflater)
|
||||
views.tvMessage.text = message
|
||||
views.cbSkipNext.visibility = View.GONE
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(views.root)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
if (cont.isActive) cont.resume(Unit)
|
||||
}
|
||||
.create()
|
||||
dialog.setOnDismissListener {
|
||||
if (cont.isActive) cont.resumeWithException(CancellationException("dialog closed."))
|
||||
}
|
||||
dialog.show()
|
||||
cont.invokeOnCancellation { dialog.dismissSafe() }
|
||||
} catch (ex: Throwable) {
|
||||
cont.resumeWithException(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -18,6 +18,8 @@ class UnicodeEmoji(
|
||||
@DrawableRes val drawableId: Int = 0,
|
||||
) : EmojiBase, Comparable<UnicodeEmoji> {
|
||||
|
||||
val namesLower = ArrayList<String>()
|
||||
|
||||
// unified code used in picker.
|
||||
var unifiedCode = ""
|
||||
|
||||
|
@ -1,20 +1,22 @@
|
||||
package jp.juggler.subwaytooter.emoji
|
||||
|
||||
import java.util.ArrayList
|
||||
import androidx.annotation.StringRes
|
||||
import jp.juggler.subwaytooter.R
|
||||
|
||||
enum class EmojiCategory(@StringRes val titleId: Int) {
|
||||
Recent(R.string.emoji_category_recent),
|
||||
Custom(R.string.emoji_category_custom),
|
||||
People(R.string.emoji_category_people),
|
||||
ComplexTones(R.string.emoji_category_composite_tones),
|
||||
Nature(R.string.emoji_category_nature),
|
||||
Foods(R.string.emoji_category_foods),
|
||||
Activities(R.string.emoji_category_activity),
|
||||
Places(R.string.emoji_category_places),
|
||||
Objects(R.string.emoji_category_objects),
|
||||
Symbols(R.string.emoji_category_symbols),
|
||||
Flags(R.string.emoji_category_flags),
|
||||
Others(R.string.emoji_category_others),
|
||||
|
||||
enum class EmojiCategory {
|
||||
Recent,
|
||||
Custom,
|
||||
People,
|
||||
ComplexTones,
|
||||
Nature,
|
||||
Foods,
|
||||
Activities,
|
||||
Places,
|
||||
Objects,
|
||||
Symbols,
|
||||
Flags,
|
||||
Others,
|
||||
;
|
||||
|
||||
val emojiList = ArrayList<UnicodeEmoji>()
|
||||
|
@ -45,6 +45,7 @@ class EmojiMapLoader(
|
||||
private fun addName(emoji: UnicodeEmoji, name: String) {
|
||||
dst.shortNameMap[name] = emoji
|
||||
dst.shortNameList.add(name)
|
||||
emoji.namesLower.add(name.lowercase())
|
||||
}
|
||||
|
||||
private fun readEmojiDataLine(lno: Int, rawLine: String) {
|
||||
|
@ -14,13 +14,17 @@ import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.action.reactionAdd
|
||||
import jp.juggler.subwaytooter.action.reactionFromAnotherAccount
|
||||
import jp.juggler.subwaytooter.action.reactionRemove
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.TootReaction
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.pref.PrefB
|
||||
import jp.juggler.subwaytooter.pref.PrefI
|
||||
import jp.juggler.subwaytooter.util.*
|
||||
import jp.juggler.util.*
|
||||
import jp.juggler.subwaytooter.util.DecodeOptions
|
||||
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
|
||||
import jp.juggler.subwaytooter.util.minWidthCompat
|
||||
import jp.juggler.subwaytooter.util.startMargin
|
||||
import jp.juggler.util.attrColor
|
||||
import jp.juggler.util.getAdaptiveRippleDrawableRound
|
||||
import jp.juggler.util.notZero
|
||||
import org.jetbrains.anko.allCaps
|
||||
import org.jetbrains.anko.dip
|
||||
|
||||
|
@ -8,15 +8,17 @@ import jp.juggler.subwaytooter.api.entity.parseList
|
||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class CustomEmojiLister(
|
||||
val context: Context,
|
||||
private val handler: Handler,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LogCategory("CustomEmojiLister")
|
||||
@ -31,8 +33,8 @@ class CustomEmojiLister(
|
||||
|
||||
internal class CacheItem(
|
||||
val key: String,
|
||||
var list: ArrayList<CustomEmoji>? = null,
|
||||
var listWithAliases: ArrayList<CustomEmoji>? = null,
|
||||
var list: List<CustomEmoji>,
|
||||
var listWithAliases: List<CustomEmoji>,
|
||||
// ロードした時刻
|
||||
var timeUpdate: Long = elapsedTime,
|
||||
// 参照された時刻
|
||||
@ -40,10 +42,19 @@ class CustomEmojiLister(
|
||||
)
|
||||
|
||||
internal class Request(
|
||||
val cont: Continuation<List<CustomEmoji>>,
|
||||
val accessInfo: SavedAccount,
|
||||
val reportWithAliases: Boolean = false,
|
||||
val onListLoaded: (list: ArrayList<CustomEmoji>) -> Unit?,
|
||||
)
|
||||
) {
|
||||
fun resume(item: CacheItem) {
|
||||
cont.resume(
|
||||
when (reportWithAliases) {
|
||||
true -> item.listWithAliases
|
||||
else -> item.list
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 成功キャッシュ
|
||||
internal val cache = ConcurrentHashMap<String, CacheItem>()
|
||||
@ -51,16 +62,12 @@ class CustomEmojiLister(
|
||||
// エラーキャッシュ
|
||||
internal val cacheError = ConcurrentHashMap<String, Long>()
|
||||
|
||||
private val cacheErrorItem = CacheItem("error")
|
||||
private val cacheErrorItem = CacheItem("error", emptyList(), emptyList())
|
||||
|
||||
// ロード要求
|
||||
internal val queue = ConcurrentLinkedQueue<Request>()
|
||||
|
||||
private val worker: Worker
|
||||
|
||||
init {
|
||||
this.worker = Worker()
|
||||
}
|
||||
private val worker = Worker()
|
||||
|
||||
// ネットワーク接続が変化したらエラーキャッシュをクリア
|
||||
fun onNetworkChanged() {
|
||||
@ -86,59 +93,50 @@ class CustomEmojiLister(
|
||||
return null
|
||||
}
|
||||
|
||||
fun getList(
|
||||
// インスタンス用のカスタム絵文字のリストを取得する
|
||||
// または例外を投げる
|
||||
suspend fun getList(
|
||||
accessInfo: SavedAccount,
|
||||
onListLoaded: (list: ArrayList<CustomEmoji>) -> Unit,
|
||||
): ArrayList<CustomEmoji>? {
|
||||
try {
|
||||
synchronized(cache) {
|
||||
val item = getCached(elapsedTime, accessInfo)
|
||||
if (item != null) return item.list
|
||||
withAliases: Boolean = false,
|
||||
): List<CustomEmoji> {
|
||||
synchronized(cache) {
|
||||
getCached(elapsedTime, accessInfo)
|
||||
}?.let { return it.list }
|
||||
return suspendCoroutine { cont ->
|
||||
try {
|
||||
queue.add(Request(cont, accessInfo, reportWithAliases = withAliases))
|
||||
worker.notifyEx()
|
||||
} catch (ex: Throwable) {
|
||||
cont.resumeWithException(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queue.add(Request(accessInfo, onListLoaded = onListLoaded))
|
||||
worker.notifyEx()
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
fun getListNonBlocking(
|
||||
accessInfo: SavedAccount,
|
||||
withAliases: Boolean = false,
|
||||
callback: ((List<CustomEmoji>) -> Unit)? = null,
|
||||
): List<CustomEmoji>? {
|
||||
synchronized(cache) {
|
||||
getCached(elapsedTime, accessInfo)
|
||||
}?.let { return it.list }
|
||||
launchMain {
|
||||
getList(accessInfo, withAliases).let { callback?.invoke(it) }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getListWithAliases(
|
||||
accessInfo: SavedAccount,
|
||||
onListLoaded: (list: ArrayList<CustomEmoji>) -> Unit,
|
||||
): ArrayList<CustomEmoji>? {
|
||||
try {
|
||||
synchronized(cache) {
|
||||
val item = getCached(elapsedTime, accessInfo)
|
||||
if (item != null) return item.listWithAliases
|
||||
// suspend fun getMap(accessInfo: SavedAccount) =
|
||||
// HashMap<String, CustomEmoji>().apply {
|
||||
// getList(accessInfo).forEach { put(it.shortcode, it) }
|
||||
// }
|
||||
|
||||
fun getMapNonBlocking(accessInfo: SavedAccount) =
|
||||
getListNonBlocking(accessInfo)?.let {
|
||||
HashMap<String, CustomEmoji>().apply {
|
||||
it.forEach { put(it.shortcode, it) }
|
||||
}
|
||||
|
||||
queue.add(
|
||||
Request(
|
||||
accessInfo,
|
||||
reportWithAliases = true,
|
||||
onListLoaded = onListLoaded
|
||||
)
|
||||
)
|
||||
worker.notifyEx()
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getMap(accessInfo: SavedAccount): HashMap<String, CustomEmoji>? {
|
||||
val list = getList(accessInfo) {
|
||||
// 遅延ロード非対応
|
||||
} ?: return null
|
||||
//
|
||||
val dst = HashMap<String, CustomEmoji>()
|
||||
for (e in list) {
|
||||
dst[e.shortcode] = e
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
private inner class Worker : WorkerBase() {
|
||||
|
||||
@ -156,69 +154,10 @@ class CustomEmojiLister(
|
||||
waitEx(86400000L)
|
||||
continue
|
||||
}
|
||||
|
||||
val cached = synchronized(cache) {
|
||||
|
||||
val item = getCached(elapsedTime, request.accessInfo)
|
||||
return@synchronized if (item != null) {
|
||||
val list = item.list
|
||||
val listWithAliases = item.listWithAliases
|
||||
if (list != null && listWithAliases != null) {
|
||||
fireCallback(request, list, listWithAliases)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
// キャッシュにはなかった
|
||||
sweepCache()
|
||||
false
|
||||
}
|
||||
}
|
||||
if (cached) continue
|
||||
|
||||
val accessInfo = request.accessInfo
|
||||
val cacheKey = accessInfo.apiHost.ascii
|
||||
var list: ArrayList<CustomEmoji>? = null
|
||||
var listWithAlias: ArrayList<CustomEmoji>? = null
|
||||
try {
|
||||
val data = if (accessInfo.isMisskey) {
|
||||
App1.getHttpCachedString(
|
||||
"https://$cacheKey/api/meta",
|
||||
accessInfo = accessInfo
|
||||
) { builder ->
|
||||
builder.post(JsonObject().toRequestBody())
|
||||
}
|
||||
} else {
|
||||
App1.getHttpCachedString(
|
||||
"https://$cacheKey/api/v1/custom_emojis",
|
||||
accessInfo = accessInfo
|
||||
)
|
||||
}
|
||||
|
||||
if (data != null) {
|
||||
val a = decodeEmojiList(data, accessInfo)
|
||||
list = a
|
||||
listWithAlias = makeListWithAlias(a)
|
||||
}
|
||||
request.resume(handleRequest(request))
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
|
||||
synchronized(cache) {
|
||||
val now = elapsedTime
|
||||
if (list == null || listWithAlias == null) {
|
||||
cacheError.put(cacheKey, now)
|
||||
} else {
|
||||
var item: CacheItem? = cache[cacheKey]
|
||||
if (item == null) {
|
||||
item = CacheItem(cacheKey, list, listWithAlias)
|
||||
cache[cacheKey] = item
|
||||
} else {
|
||||
item.list = list
|
||||
item.listWithAliases = listWithAlias
|
||||
item.timeUpdate = now
|
||||
}
|
||||
fireCallback(request, list, listWithAlias)
|
||||
}
|
||||
request.cont.resumeWithException(ex)
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
@ -227,20 +166,58 @@ class CustomEmojiLister(
|
||||
}
|
||||
}
|
||||
|
||||
private fun fireCallback(
|
||||
request: Request,
|
||||
list: ArrayList<CustomEmoji>,
|
||||
listWithAliases: ArrayList<CustomEmoji>,
|
||||
) {
|
||||
handler.post {
|
||||
request.onListLoaded(
|
||||
if (request.reportWithAliases) {
|
||||
listWithAliases
|
||||
} else {
|
||||
list
|
||||
private suspend fun handleRequest(request: Request): CacheItem {
|
||||
synchronized(cache) {
|
||||
(getCached(elapsedTime, request.accessInfo)
|
||||
?.takeIf { it != cacheErrorItem })
|
||||
.also {
|
||||
if (it == null) {
|
||||
// エラーキャッシュは一定時間で除去される
|
||||
sweepCache()
|
||||
}
|
||||
}
|
||||
}?.let { return it }
|
||||
|
||||
val accessInfo = request.accessInfo
|
||||
val cacheKey = accessInfo.apiHost.ascii
|
||||
val data = if (accessInfo.isMisskey) {
|
||||
App1.getHttpCachedString(
|
||||
"https://$cacheKey/api/meta",
|
||||
accessInfo = accessInfo
|
||||
) { builder ->
|
||||
builder.post(JsonObject().toRequestBody())
|
||||
}
|
||||
} else {
|
||||
App1.getHttpCachedString(
|
||||
"https://$cacheKey/api/v1/custom_emojis",
|
||||
accessInfo = accessInfo
|
||||
)
|
||||
}
|
||||
var list: List<CustomEmoji>? = null
|
||||
var listWithAlias: List<CustomEmoji>? = null
|
||||
if (data != null) {
|
||||
val a = decodeEmojiList(data, accessInfo)
|
||||
list = a
|
||||
listWithAlias = makeListWithAlias(a)
|
||||
}
|
||||
return synchronized(cache) {
|
||||
val now = elapsedTime
|
||||
if (list == null || listWithAlias == null) {
|
||||
cacheError[cacheKey] = now
|
||||
error("can't load custom emoji for ${accessInfo.apiHost}")
|
||||
} else {
|
||||
var item = cache[cacheKey]
|
||||
if (item == null) {
|
||||
item = CacheItem(cacheKey, list, listWithAlias)
|
||||
cache[cacheKey] = item
|
||||
} else {
|
||||
item.list = list
|
||||
item.listWithAliases = listWithAlias
|
||||
item.timeUpdate = now
|
||||
}
|
||||
item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// キャッシュの掃除
|
||||
@ -270,43 +247,35 @@ class CustomEmojiLister(
|
||||
private fun decodeEmojiList(
|
||||
data: String,
|
||||
accessInfo: SavedAccount,
|
||||
): ArrayList<CustomEmoji>? {
|
||||
return try {
|
||||
val list = if (accessInfo.isMisskey) {
|
||||
parseList(
|
||||
CustomEmoji.decodeMisskey,
|
||||
accessInfo.apDomain,
|
||||
data.decodeJsonObject().jsonArray("emojis")
|
||||
)
|
||||
} else {
|
||||
parseList(
|
||||
CustomEmoji.decode,
|
||||
accessInfo.apDomain,
|
||||
data.decodeJsonArray()
|
||||
)
|
||||
}
|
||||
list.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode })
|
||||
list
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "decodeEmojiList failed. instance=${accessInfo.apiHost.ascii}")
|
||||
null
|
||||
): List<CustomEmoji> =
|
||||
if (accessInfo.isMisskey) {
|
||||
parseList(
|
||||
CustomEmoji.decodeMisskey,
|
||||
accessInfo.apDomain,
|
||||
data.decodeJsonObject().jsonArray("emojis")
|
||||
)
|
||||
} else {
|
||||
parseList(
|
||||
CustomEmoji.decode,
|
||||
accessInfo.apDomain,
|
||||
data.decodeJsonArray()
|
||||
)
|
||||
}.apply {
|
||||
sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode })
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeListWithAlias(list: ArrayList<CustomEmoji>?): ArrayList<CustomEmoji> {
|
||||
val dst = ArrayList<CustomEmoji>()
|
||||
if (list != null) {
|
||||
dst.addAll(list)
|
||||
for (item in list) {
|
||||
val aliases = item.aliases ?: continue
|
||||
for (alias in aliases) {
|
||||
if (alias.equals(item.shortcode, ignoreCase = true)) continue
|
||||
dst.add(item.makeAlias(alias))
|
||||
}
|
||||
private fun makeListWithAlias(
|
||||
list: List<CustomEmoji>,
|
||||
) = ArrayList<CustomEmoji>().apply {
|
||||
addAll(list)
|
||||
for (item in list) {
|
||||
val aliases = item.aliases ?: continue
|
||||
for (alias in aliases) {
|
||||
if (alias.equals(item.shortcode, ignoreCase = true)) continue
|
||||
add(item.makeAlias(alias))
|
||||
}
|
||||
dst.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode })
|
||||
}
|
||||
return dst
|
||||
sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
package jp.juggler.subwaytooter.util
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.Styler
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||
import jp.juggler.subwaytooter.pref.PrefB
|
||||
import jp.juggler.subwaytooter.span.MyClickableSpan
|
||||
@ -15,16 +14,21 @@ import jp.juggler.subwaytooter.table.AcctColor
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.table.TagSet
|
||||
import jp.juggler.util.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
interface PostCompleteCallback {
|
||||
fun onPostComplete(targetAccount: SavedAccount, status: TootStatus)
|
||||
fun onScheduledPostComplete(targetAccount: SavedAccount)
|
||||
sealed class PostResult {
|
||||
class Normal(
|
||||
val targetAccount: SavedAccount,
|
||||
val status: TootStatus,
|
||||
) : PostResult()
|
||||
|
||||
class Scheduled(
|
||||
val targetAccount: SavedAccount,
|
||||
) : PostResult()
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ -51,8 +55,6 @@ class PostImpl(
|
||||
val editStatusId: EntityId?,
|
||||
val emojiMapCustom: HashMap<String, CustomEmoji>?,
|
||||
var useQuoteToot: Boolean,
|
||||
|
||||
val callback: PostCompleteCallback,
|
||||
) {
|
||||
companion object {
|
||||
private val log = LogCategory("PostImpl")
|
||||
@ -70,11 +72,6 @@ class PostImpl(
|
||||
|
||||
private var visibilityChecked: TootVisibility? = null
|
||||
|
||||
private var bConfirmTag: Boolean = false
|
||||
var bConfirmAccount: Boolean = false
|
||||
private var bConfirmRedraft: Boolean = false
|
||||
private var bConfirmTagCharacter: Boolean = false
|
||||
|
||||
private val choiceMaxChars = when {
|
||||
account.isMisskey -> 15
|
||||
pollType == TootPollsType.FriendsNico -> 15
|
||||
@ -130,106 +127,6 @@ class PostImpl(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun confirm(): Boolean {
|
||||
if (!bConfirmAccount) {
|
||||
DlgConfirm.open(
|
||||
activity,
|
||||
activity.getString(R.string.confirm_post_from, AcctColor.getNickname(account)),
|
||||
object : DlgConfirm.Callback {
|
||||
override var isConfirmEnabled: Boolean
|
||||
get() = account.confirm_post
|
||||
set(bv) {
|
||||
account.confirm_post = bv
|
||||
account.saveSetting()
|
||||
}
|
||||
|
||||
override fun onOK() {
|
||||
bConfirmAccount = true
|
||||
run()
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!bConfirmTagCharacter && PrefB.bpWarnHashtagAsciiAndNonAscii()) {
|
||||
val tags = TootTag.findHashtags(content, account.isMisskey)
|
||||
val badTags = tags
|
||||
?.filter {
|
||||
val hasAscii = reAscii.matcher(it).find()
|
||||
val hasNotAscii = reNotAscii.matcher(it).find()
|
||||
hasAscii && hasNotAscii
|
||||
}
|
||||
?.map { "#$it" }
|
||||
|
||||
if (badTags?.isNotEmpty() == true) {
|
||||
AlertDialog.Builder(activity)
|
||||
.setCancelable(true)
|
||||
.setMessage(
|
||||
activity.getString(
|
||||
R.string.hashtag_contains_ascii_and_not_ascii,
|
||||
badTags.joinToString(", ")
|
||||
)
|
||||
)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
bConfirmTagCharacter = true
|
||||
run()
|
||||
}
|
||||
.show()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (!bConfirmTag) {
|
||||
val isMisskey = account.isMisskey
|
||||
if (!visibilityArg.isTagAllowed(isMisskey)) {
|
||||
val tags = TootTag.findHashtags(content, isMisskey)
|
||||
if (tags != null) {
|
||||
log.d("findHashtags ${tags.joinToString(",")}")
|
||||
|
||||
AlertDialog.Builder(activity)
|
||||
.setCancelable(true)
|
||||
.setMessage(R.string.hashtag_and_visibility_not_match)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
bConfirmTag = true
|
||||
run()
|
||||
}
|
||||
.show()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bConfirmRedraft && redraftStatusId != null) {
|
||||
AlertDialog.Builder(activity)
|
||||
.setCancelable(true)
|
||||
.setMessage(R.string.delete_base_status_before_toot)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
bConfirmRedraft = true
|
||||
run()
|
||||
}
|
||||
.show()
|
||||
return false
|
||||
}
|
||||
|
||||
if (!bConfirmRedraft && scheduledId != null) {
|
||||
AlertDialog.Builder(activity)
|
||||
.setCancelable(true)
|
||||
.setMessage(R.string.delete_scheduled_status_before_update)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
bConfirmRedraft = true
|
||||
run()
|
||||
}
|
||||
.show()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private var resultStatus: TootStatus? = null
|
||||
private var resultCredentialTmp: TootAccount? = null
|
||||
private var resultScheduledStatusSucceeded = false
|
||||
@ -503,14 +400,57 @@ class PostImpl(
|
||||
}
|
||||
}
|
||||
|
||||
fun run() {
|
||||
if (!preCheck()) return
|
||||
if (!confirm()) return
|
||||
suspend fun runSuspend(): PostResult {
|
||||
if (!preCheck()) throw CancellationException("preCheck failed.")
|
||||
|
||||
if (PrefB.bpWarnHashtagAsciiAndNonAscii()) {
|
||||
TootTag.findHashtags(content, account.isMisskey)
|
||||
?.filter {
|
||||
val hasAscii = reAscii.matcher(it).find()
|
||||
val hasNotAscii = reNotAscii.matcher(it).find()
|
||||
hasAscii && hasNotAscii
|
||||
}?.map { "#$it" }
|
||||
?.notEmpty()
|
||||
?.let { badTags ->
|
||||
activity.confirm(
|
||||
R.string.hashtag_contains_ascii_and_not_ascii,
|
||||
badTags.joinToString(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val isMisskey = account.isMisskey
|
||||
if (!visibilityArg.isTagAllowed(isMisskey)) {
|
||||
TootTag.findHashtags(content, isMisskey)
|
||||
?.notEmpty()
|
||||
?.let { tags ->
|
||||
log.d("findHashtags ${tags.joinToString(",")}")
|
||||
activity.confirm(
|
||||
R.string.hashtag_and_visibility_not_match
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (redraftStatusId != null) {
|
||||
activity.confirm(R.string.delete_base_status_before_toot)
|
||||
}
|
||||
|
||||
if (scheduledId != null) {
|
||||
activity.confirm(R.string.delete_scheduled_status_before_update)
|
||||
}
|
||||
|
||||
activity.confirm(
|
||||
activity.getString(R.string.confirm_post_from, AcctColor.getNickname(account)),
|
||||
account.confirm_post
|
||||
) { newConfirmEnabled ->
|
||||
account.confirm_post = newConfirmEnabled
|
||||
account.saveSetting()
|
||||
}
|
||||
|
||||
// 投稿中に再度投稿ボタンが押された
|
||||
if (postJob?.get()?.isActive == true) {
|
||||
activity.showToast(false, R.string.post_button_tapped_repeatly)
|
||||
return
|
||||
throw CancellationException("preCheck failed.")
|
||||
}
|
||||
|
||||
// ボタン連打判定
|
||||
@ -519,10 +459,12 @@ class PostImpl(
|
||||
lastPostTapped = now
|
||||
if (delta < 1000L) {
|
||||
activity.showToast(false, R.string.post_button_tapped_repeatly)
|
||||
return
|
||||
throw CancellationException("post_button_tapped_repeatly")
|
||||
}
|
||||
|
||||
postJob = launchMain {
|
||||
val job = Job().also { postJob = it.wrapWeakReference }
|
||||
return withContext(Dispatchers.Main + job) {
|
||||
|
||||
activity.runApiTask(
|
||||
account,
|
||||
progressSetup = { it.setCanceledOnTouchOutside(false) },
|
||||
@ -622,19 +564,21 @@ class PostImpl(
|
||||
saveStatusTag(status)
|
||||
}
|
||||
}
|
||||
}?.let { result ->
|
||||
}.let { result ->
|
||||
if (result == null) throw CancellationException()
|
||||
|
||||
val status = resultStatus
|
||||
val scheduledStatusSucceeded = resultScheduledStatusSucceeded
|
||||
when {
|
||||
scheduledStatusSucceeded ->
|
||||
callback.onScheduledPostComplete(account)
|
||||
resultScheduledStatusSucceeded ->
|
||||
PostResult.Scheduled(account)
|
||||
|
||||
// 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る
|
||||
status != null -> callback.onPostComplete(account, status)
|
||||
status != null ->
|
||||
PostResult.Normal(account, status)
|
||||
|
||||
else -> activity.showToast(true, result.error)
|
||||
else -> error( result.error ?: "(result.error is null)")
|
||||
}
|
||||
}
|
||||
}.wrapWeakReference
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package jp.juggler.util
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import jp.juggler.subwaytooter.dialog.ProgressDialogEx
|
||||
import kotlinx.coroutines.*
|
||||
import java.lang.ref.WeakReference
|
||||
@ -30,6 +31,17 @@ fun launchMain(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun AppCompatActivity.launchAndShowError(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
lifecycleScope.launch() {
|
||||
try {
|
||||
block()
|
||||
} catch (ex: Throwable) {
|
||||
showError(ex)
|
||||
}
|
||||
}
|
||||
|
||||
// Default Dispatcherで動作するコルーチンを起動して、終了を待たずにリターンする。
|
||||
// 起動されたアクティビティのライフサイクルに関わらず中断しない。
|
||||
fun launchDefault(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
|
@ -2,57 +2,87 @@ package jp.juggler.util
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.R
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import me.drakeet.support.toast.ToastCompat
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
object ToastUtils {
|
||||
private val log = LogCategory("ToastUtils")
|
||||
private var refToast: WeakReference<Toast>? = null
|
||||
|
||||
private val log = LogCategory("ToastUtils")
|
||||
private var refToast: WeakReference<Toast>? = null
|
||||
internal fun showToastImpl(context: Context, bLong: Boolean, message: String): Boolean {
|
||||
runOnMainLooper {
|
||||
|
||||
internal fun showToastImpl(context: Context, bLong: Boolean, message: String): Boolean {
|
||||
runOnMainLooper {
|
||||
|
||||
// 前回のトーストの表示を終了する
|
||||
try {
|
||||
refToast?.get()?.cancel()
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
} finally {
|
||||
refToast = null
|
||||
}
|
||||
|
||||
// 新しいトーストを作る
|
||||
try {
|
||||
val duration = if (bLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
|
||||
val t = ToastCompat.makeText(context, message, duration)
|
||||
t.setBadTokenListener { }
|
||||
t.show()
|
||||
refToast = WeakReference(t)
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
|
||||
// コールスタックの外側でエラーになる…
|
||||
// android.view.WindowManager$BadTokenException:
|
||||
// at android.view.ViewRootImpl.setView (ViewRootImpl.java:679)
|
||||
// at android.view.WindowManagerGlobal.addView (WindowManagerGlobal.java:342)
|
||||
// at android.view.WindowManagerImpl.addView (WindowManagerImpl.java:94)
|
||||
// at android.widget.Toast$TN.handleShow (Toast.java:435)
|
||||
// at android.widget.Toast$TN$2.handleMessage (Toast.java:345)
|
||||
// 前回のトーストの表示を終了する
|
||||
try {
|
||||
refToast?.get()?.cancel()
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
} finally {
|
||||
refToast = null
|
||||
}
|
||||
return false
|
||||
|
||||
// 新しいトーストを作る
|
||||
try {
|
||||
val duration = if (bLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
|
||||
val t = ToastCompat.makeText(context, message, duration)
|
||||
t.setBadTokenListener { }
|
||||
t.show()
|
||||
refToast = WeakReference(t)
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
|
||||
// コールスタックの外側でエラーになる…
|
||||
// android.view.WindowManager$BadTokenException:
|
||||
// at android.view.ViewRootImpl.setView (ViewRootImpl.java:679)
|
||||
// at android.view.WindowManagerGlobal.addView (WindowManagerGlobal.java:342)
|
||||
// at android.view.WindowManagerImpl.addView (WindowManagerImpl.java:94)
|
||||
// at android.widget.Toast$TN.handleShow (Toast.java:435)
|
||||
// at android.widget.Toast$TN$2.handleMessage (Toast.java:345)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun Context.showToast(bLong: Boolean, caption: String?): Boolean =
|
||||
ToastUtils.showToastImpl(this, bLong, caption ?: "(null)")
|
||||
showToastImpl(this, bLong, caption ?: "(null)")
|
||||
|
||||
fun Context.showToast(ex: Throwable, caption: String = "error."): Boolean =
|
||||
ToastUtils.showToastImpl(this, true, ex.withCaption(caption))
|
||||
showToastImpl(this, true, ex.withCaption(caption))
|
||||
|
||||
fun Context.showToast(bLong: Boolean, stringId: Int, vararg args: Any): Boolean =
|
||||
ToastUtils.showToastImpl(this, bLong, getString(stringId, *args))
|
||||
showToastImpl(this, bLong, getString(stringId, *args))
|
||||
|
||||
fun Context.showToast(ex: Throwable, stringId: Int, vararg args: Any): Boolean =
|
||||
ToastUtils.showToastImpl(this, true, ex.withCaption(resources, stringId, *args))
|
||||
showToastImpl(this, true, ex.withCaption(resources, stringId, *args))
|
||||
|
||||
fun AppCompatActivity.showError(ex: Throwable, caption: String = "error.") {
|
||||
log.e(ex)
|
||||
// キャンセル例外はUIに表示しない
|
||||
if (ex is CancellationException) return
|
||||
|
||||
try {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(
|
||||
listOf(
|
||||
caption,
|
||||
when (ex) {
|
||||
is IllegalStateException ->
|
||||
null
|
||||
else ->
|
||||
ex.javaClass.simpleName
|
||||
},
|
||||
ex.message,
|
||||
)
|
||||
.filter { !it.isNullOrBlank() }
|
||||
.joinToString("\n")
|
||||
)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex)
|
||||
showToast(ex, caption)
|
||||
}
|
||||
}
|
||||
|
@ -1,73 +1,90 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="320dp"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
>
|
||||
android:layout_margin="6dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSkinTone0"
|
||||
style="@style/emoji_picker_skin_tone_button"
|
||||
android:background="#fde12c"
|
||||
android:contentDescription="@string/skin_tone_unspecified"
|
||||
android:src="@drawable/check_mark" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSkinTone1"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
style="@style/emoji_picker_skin_tone_button"
|
||||
android:background="#f7dece"
|
||||
android:contentDescription="@string/skin_tone_light"
|
||||
android:src="@drawable/check_mark"
|
||||
/>
|
||||
android:contentDescription="@string/skin_tone_light" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSkinTone2"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
style="@style/emoji_picker_skin_tone_button"
|
||||
android:background="#f3d2a2"
|
||||
android:contentDescription="@string/skin_tone_medium_light"
|
||||
/>
|
||||
android:contentDescription="@string/skin_tone_medium_light" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSkinTone3"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
style="@style/emoji_picker_skin_tone_button"
|
||||
android:background="#d5ab88"
|
||||
android:contentDescription="@string/skin_tone_medium"
|
||||
/>
|
||||
android:contentDescription="@string/skin_tone_medium" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSkinTone4"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
style="@style/emoji_picker_skin_tone_button"
|
||||
android:background="#af7e57"
|
||||
android:contentDescription="@string/skin_tone_medium_dark"
|
||||
/>
|
||||
android:contentDescription="@string/skin_tone_medium_dark" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSkinTone5"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
style="@style/emoji_picker_skin_tone_button"
|
||||
android:background="#7c533e"
|
||||
android:contentDescription="@string/skin_tone_dark"
|
||||
/>
|
||||
</LinearLayout>
|
||||
android:contentDescription="@string/skin_tone_dark" />
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
|
||||
<com.astuetz.PagerSlidingTabStrip
|
||||
android:id="@+id/pager_strip"
|
||||
<EditText
|
||||
android:id="@+id/etFilter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dip"
|
||||
android:layout_gravity="top"
|
||||
/>
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="6dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:hint="@string/search_emojis"
|
||||
android:importantForAccessibility="no"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text" />
|
||||
|
||||
<jp.juggler.subwaytooter.view.MyViewPager
|
||||
android:id="@+id/pager"
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:fadeScrollbars="false"
|
||||
android:padding="6dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:scrollbars="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llCategories"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" />
|
||||
</HorizontalScrollView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvGrid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
/>
|
||||
android:clipToPadding="false"
|
||||
android:fadeScrollbars="false"
|
||||
android:padding="6dp"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:scrollbars="vertical" />
|
||||
</LinearLayout>
|
||||
|
@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<jp.juggler.subwaytooter.view.HeaderGridView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/gridView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:columnWidth="48dp"
|
||||
android:horizontalSpacing="4dp"
|
||||
android:numColumns="auto_fit"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingStart="6dp"
|
||||
android:stretchMode="columnWidth"
|
||||
android:verticalSpacing="4dp"
|
||||
/>
|
@ -1139,4 +1139,11 @@
|
||||
<string name="edit_history">編集履歴</string>
|
||||
<string name="post_language_code">投稿の言語コード(空欄にすると端末の言語設定を使います)</string>
|
||||
<string name="device_language">(端末の言語)</string>
|
||||
<string name="skin_tones">肌色</string>
|
||||
<string name="skin_tone_unspecified">指定なし</string>
|
||||
<string name="search_emojis">絵文字を検索…</string>
|
||||
<string name="categories">カテゴリ</string>
|
||||
<string name="error">エラー</string>
|
||||
<string name="emoji_picker_custom_of">カスタム: %1$s</string>
|
||||
<string name="others" >その他</string>
|
||||
</resources>
|
||||
|
@ -1148,4 +1148,11 @@
|
||||
<string name="edit_history">Edit history</string>
|
||||
<string name="post_language_code">Language code of Post (leave empty to use device\'s language setting)</string>
|
||||
<string name="device_language">(device\'s language)</string>
|
||||
<string name="skin_tones">Skin tones</string>
|
||||
<string name="skin_tone_unspecified">Unspecified</string>
|
||||
<string name="search_emojis">Search emojis…</string>
|
||||
<string name="categories">Categories</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="emoji_picker_custom_of">Custom: %1$s</string>
|
||||
<string name="others" >Others</string>
|
||||
</resources>
|
||||
|
@ -258,4 +258,10 @@
|
||||
|
||||
</style>
|
||||
|
||||
<style name="emoji_picker_skin_tone_button">
|
||||
<item name="android:layout_width">48dp</item>
|
||||
<item name="android:layout_height">48dp</item>
|
||||
<item name="android:layout_marginStart">2dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user