絵文字ピッカーの改善

This commit is contained in:
tateisu 2022-05-29 22:38:21 +09:00
parent 228a8d3338
commit f1d3556abf
30 changed files with 1648 additions and 1803 deletions

View File

@ -39,9 +39,9 @@ android {
jvmTarget = jvm_target jvmTarget = jvm_target
useIR = true useIR = true
freeCompilerArgs += [ freeCompilerArgs += [
"-Xopt-in=kotlin.ExperimentalStdlibApi", "-opt-in=kotlin.ExperimentalStdlibApi",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
// "-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi", // "-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi",
// "-Xopt-in=androidx.compose.animation.ExperimentalAnimationApi", // "-Xopt-in=androidx.compose.animation.ExperimentalAnimationApi",
] ]
@ -148,9 +148,11 @@ dependencies {
implementation "androidx.core:core-ktx:1.7.0" implementation "androidx.core:core-ktx:1.7.0"
def emojiVersion = "1.1.0" def emoji2Version = "1.1.0"
implementation "androidx.emoji2:emoji2:$emojiVersion" implementation "androidx.emoji2:emoji2:$emoji2Version"
implementation "androidx.emoji2:emoji2-bundled:$emojiVersion" implementation "androidx.emoji2:emoji2-views:$emoji2Version"
implementation "androidx.emoji2:emoji2-views-helper:$emoji2Version"
implementation "androidx.emoji2:emoji2-bundled:$emoji2Version"
// DrawerLayout // DrawerLayout
implementation "androidx.drawerlayout:drawerlayout:1.1.1" implementation "androidx.drawerlayout:drawerlayout:1.1.1"
@ -236,6 +238,8 @@ dependencies {
implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.caverock:androidsvg-aar:1.4'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// ViewModel // ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

View File

@ -2,7 +2,9 @@ package jp.juggler.subwaytooter.action
import android.content.Context import android.content.Context
import androidx.appcompat.app.AlertDialog 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.addColumn
import jp.juggler.subwaytooter.actmain.reloadAccountSetting import jp.juggler.subwaytooter.actmain.reloadAccountSetting
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount 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.api.entity.TootVisibility
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.findStatus 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.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.emptyCallback import jp.juggler.subwaytooter.util.emptyCallback
import jp.juggler.util.JsonObject import jp.juggler.util.*
import jp.juggler.util.launchMain
import jp.juggler.util.showToast
import jp.juggler.util.toPostRequestBuilder
import java.util.*
import kotlin.math.max import kotlin.math.max
private class BoostImpl( private class BoostImpl(
@ -62,42 +60,6 @@ private class BoostImpl(
return true 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) = private suspend fun Context.syncStatus(client: TootApiClient) =
if (!crossAccountMode.isRemote) { if (!crossAccountMode.isRemote) {
// 既に自タンスのステータスがある // 既に自タンスのステータスがある
@ -234,16 +196,41 @@ private class BoostImpl(
} }
fun run() { fun run() {
if (!preCheck()) return activity.launchAndShowError {
if (!confirm()) return if (!preCheck()) return@launchAndShowError
// ブースト表示を更新中にする if (!bConfirmed) {
activity.appState.setBusyBoost(accessInfo, statusArg) activity.confirm(
activity.showColumnMatchAccount(accessInfo) 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 = val result =
activity.runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client -> activity.runApiTask(accessInfo,
progressStyle = ApiTask.PROGRESS_NONE) { client ->
try { try {
val targetStatus = syncStatus(client) val targetStatus = syncStatus(client)
boostApi(client, targetStatus) boostApi(client, targetStatus)

View File

@ -5,11 +5,11 @@ import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.TootFilter import jp.juggler.subwaytooter.api.entity.TootFilter
import jp.juggler.subwaytooter.api.runApiTask 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.column.onFilterDeleted
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.launchMain import jp.juggler.util.launchAndShowError
import jp.juggler.util.showToast import jp.juggler.util.showToast
import okhttp3.Request import okhttp3.Request
@ -31,22 +31,17 @@ fun ActMain.openFilterMenu(accessInfo: SavedAccount, item: TootFilter?) {
fun ActMain.filterDelete( fun ActMain.filterDelete(
accessInfo: SavedAccount, accessInfo: SavedAccount,
filter: TootFilter, filter: TootFilter,
bConfirmed: Boolean = false bConfirmed: Boolean = false,
) { ) {
if (!bConfirmed) { launchAndShowError {
DlgConfirm.openSimple( if (!bConfirmed) {
this, confirm(R.string.filter_delete_confirm, filter.phrase)
getString(R.string.filter_delete_confirm, filter.phrase)
) {
filterDelete(accessInfo, filter, bConfirmed = true)
} }
return
}
launchMain {
var resultFilterList: ArrayList<TootFilter>? = null var resultFilterList: ArrayList<TootFilter>? = null
runApiTask(accessInfo) { client -> 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) { if (result != null && result.error == null) {
result = client.request("/api/v1/filters") result = client.request("/api/v1/filters")
val jsonArray = result?.jsonArray val jsonArray = result?.jsonArray

View File

@ -1,7 +1,8 @@
package jp.juggler.subwaytooter.action package jp.juggler.subwaytooter.action
import androidx.appcompat.app.AlertDialog 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.reloadAccountSetting
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount import jp.juggler.subwaytooter.actmain.showColumnMatchAccount
import jp.juggler.subwaytooter.api.* 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.ColumnType
import jp.juggler.subwaytooter.column.fireRebindAdapterItems import jp.juggler.subwaytooter.column.fireRebindAdapterItems
import jp.juggler.subwaytooter.column.removeUser 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.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.util.* import jp.juggler.util.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
fun ActMain.clickFollowInfo( fun ActMain.clickFollowInfo(
pos: Int, pos: Int,
@ -45,7 +50,10 @@ fun ActMain.clickFollow(
relation.blocking || relation.muting -> relation.blocking || relation.muting ->
Unit // 何もしない Unit // 何もしない
accessInfo.isMisskey && relation.getRequested(who) && !relation.getFollowing(who) -> 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) -> relation.getFollowing(who) || relation.getRequested(who) ->
follow(pos, accessInfo, whoRef, bFollow = false, callback = unfollowCompleteCallback) follow(pos, accessInfo, whoRef, bFollow = false, callback = unfollowCompleteCallback)
else -> 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 val who = whoRef?.get() ?: return
DlgConfirm.openSimple( launchAndShowError {
this, confirm(
getString( when {
if (accept) R.string.follow_accept_confirm else R.string.follow_deny_confirm, accept -> R.string.follow_accept_confirm
else -> R.string.follow_deny_confirm
},
AcctColor.getNickname(accessInfo, who) AcctColor.getNickname(accessInfo, who)
) )
) {
followRequestAuthorize(accessInfo, whoRef, accept) followRequestAuthorize(accessInfo, whoRef, accept)
} }
} }
@ -85,136 +98,90 @@ fun ActMain.follow(
return return
} }
if (!bConfirmMoved && bFollow && who.moved != null) { launchAndShowError {
AlertDialog.Builder(activity) if (!bConfirmMoved && bFollow && who.moved != null) {
.setMessage( val selected = suspendCancellableCoroutine<Int> { cont ->
getString( try {
R.string.jump_moved_user, val dialog = AlertDialog.Builder(activity)
accessInfo.getFullAcct(who), .setMessage(
accessInfo.getFullAcct(who.moved) getString(
) R.string.jump_moved_user,
) accessInfo.getFullAcct(who),
.setPositiveButton(R.string.ok) { _, _ -> accessInfo.getFullAcct(who.moved)
userProfileFromAnotherAccount( )
pos, )
accessInfo, .setPositiveButton(R.string.ok) { _, _ ->
who.moved 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) { _, _ -> when (selected) {
follow( R.string.ok -> {
pos, userProfileFromAnotherAccount(
accessInfo, pos,
whoRef, accessInfo,
bFollow = bFollow, who.moved
bConfirmMoved = true, // CHANGED )
bConfirmed = bConfirmed, return@launchAndShowError
callback = callback }
) 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 var resultRelation: UserRelation? = null
runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client -> runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
val parser = TootParser(activity, accessInfo) val parser = TootParser(activity, accessInfo)
@ -327,77 +294,43 @@ private fun ActMain.followRemote(
bConfirmed: Boolean = false, bConfirmed: Boolean = false,
callback: () -> Unit = {}, callback: () -> Unit = {},
) { ) {
val activity = this@followRemote
if (accessInfo.isMe(acct)) { if (accessInfo.isMe(acct)) {
showToast(false, R.string.it_is_you) showToast(false, R.string.it_is_you)
return return
} }
if (!bConfirmed) { launchAndShowError {
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(
accessInfo, if (!bConfirmed) {
acct, if (locked) {
locked, confirm(
bConfirmed = true, //CHANGE getString(
callback = callback R.string.confirm_follow_request_who_from,
) AcctColor.getNickname(acct),
} AcctColor.getNickname(accessInfo)
),
override var isConfirmEnabled: Boolean accessInfo.confirm_follow_locked,
get() = accessInfo.confirm_follow_locked ) { newConfirmEnabled ->
set(value) { accessInfo.confirm_follow_locked = newConfirmEnabled
accessInfo.confirm_follow_locked = value accessInfo.saveSetting()
accessInfo.saveSetting() reloadAccountSetting(accessInfo)
reloadAccountSetting(accessInfo) }
} } else {
}) confirm(
return getString(
} else { R.string.confirm_follow_who_from,
DlgConfirm.open( AcctColor.getNickname(acct),
activity, AcctColor.getNickname(accessInfo)
getString( ),
R.string.confirm_follow_who_from, accessInfo.confirm_follow
AcctColor.getNickname(acct), ) { newConfirmEnabled ->
AcctColor.getNickname(accessInfo) accessInfo.confirm_follow = newConfirmEnabled
), accessInfo.saveSetting()
object : DlgConfirm.Callback { reloadAccountSetting(accessInfo)
}
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
} }
}
launchMain {
var resultRelation: UserRelation? = null var resultRelation: UserRelation? = null
runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client -> runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
val parser = TootParser(this, accessInfo) val parser = TootParser(this, accessInfo)
@ -598,27 +531,15 @@ fun ActMain.followRequestDelete(
return return
} }
if (!bConfirmed) { launchAndShowError {
DlgConfirm.openSimple( if (!bConfirmed) {
this, confirm(
getString(
R.string.confirm_cancel_follow_request_who_from, R.string.confirm_cancel_follow_request_who_from,
whoRef.decoded_display_name, whoRef.decoded_display_name,
AcctColor.getNickname(accessInfo) AcctColor.getNickname(accessInfo)
) )
) {
followRequestDelete(
pos,
accessInfo,
whoRef,
bConfirmed = true, // CHANGED
callback = callback
)
} }
return
}
launchMain {
var resultRelation: UserRelation? = null var resultRelation: UserRelation? = null
runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client -> runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
if (!accessInfo.isMisskey) { if (!accessInfo.isMisskey) {
@ -649,7 +570,6 @@ fun ActMain.followRequestDelete(
} }
} }
}?.let { result -> }?.let { result ->
when (resultRelation) { when (resultRelation) {
null -> showToast(false, result.error) null -> showToast(false, result.error)
else -> { else -> {

View File

@ -14,7 +14,7 @@ import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.onListListUpdated import jp.juggler.subwaytooter.column.onListListUpdated
import jp.juggler.subwaytooter.column.onListNameUpdated import jp.juggler.subwaytooter.column.onListNameUpdated
import jp.juggler.subwaytooter.dialog.ActionsDialog 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.dialog.DlgTextInput
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.* import jp.juggler.util.*
@ -118,17 +118,10 @@ fun ActMain.listDelete(
list: TootList, list: TootList,
bConfirmed: Boolean = false, bConfirmed: Boolean = false,
) { ) {
if (!bConfirmed) { launchAndShowError {
DlgConfirm.openSimple( if (!bConfirmed) {
this, confirm(R.string.list_delete_confirm, list.title)
getString(R.string.list_delete_confirm, list.title)
) {
listDelete(accessInfo, list, bConfirmed = true)
} }
return
}
launchMain {
runApiTask(accessInfo) { client -> runApiTask(accessInfo) { client ->
if (accessInfo.isMisskey) { if (accessInfo.isMisskey) {
client.request( client.request(

View File

@ -4,10 +4,13 @@ import android.app.AlertDialog
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount 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.entity.*
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.api.syncAccountByAcct
import jp.juggler.subwaytooter.column.onListMemberUpdated 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.subwaytooter.table.SavedAccount
import jp.juggler.util.* import jp.juggler.util.*
import okhttp3.Request import okhttp3.Request
@ -23,7 +26,7 @@ fun ActMain.listMemberAdd(
bFollow: Boolean = false, bFollow: Boolean = false,
onListMemberUpdated: (willRegistered: Boolean, bSuccess: Boolean) -> Unit = { _, _ -> }, onListMemberUpdated: (willRegistered: Boolean, bSuccess: Boolean) -> Unit = { _, _ -> },
) { ) {
launchMain { launchAndShowError {
runApiTask(accessInfo) { client -> runApiTask(accessInfo) { client ->
val parser = TootParser(this, accessInfo) val parser = TootParser(this, accessInfo)
@ -31,7 +34,6 @@ fun ActMain.listMemberAdd(
if (accessInfo.isMisskey) { if (accessInfo.isMisskey) {
// misskeyのリストはフォロー無関係 // misskeyのリストはフォロー無関係
client.request( client.request(
"/api/users/lists/push", "/api/users/lists/push",
accessInfo.putMisskeyApiToken().apply { accessInfo.putMisskeyApiToken().apply {
@ -128,21 +130,17 @@ fun ActMain.listMemberAdd(
isNotFollowed() -> { isNotFollowed() -> {
if (!bFollow) { if (!bFollow) {
DlgConfirm.openSimple( confirm(
this@listMemberAdd, R.string.list_retry_with_follow,
getString( accessInfo.getFullAcct(localWho)
R.string.list_retry_with_follow, )
accessInfo.getFullAcct(localWho) listMemberAdd(
) accessInfo,
) { listId,
listMemberAdd( localWho,
accessInfo, bFollow = true,
listId, onListMemberUpdated = onListMemberUpdated
localWho, )
bFollow = true,
onListMemberUpdated = onListMemberUpdated
)
}
} else { } else {
AlertDialog.Builder(this@listMemberAdd) AlertDialog.Builder(this@listMemberAdd)
.setCancelable(true) .setCancelable(true)
@ -165,7 +163,7 @@ fun ActMain.listMemberDelete(
accessInfo: SavedAccount, accessInfo: SavedAccount,
listId: EntityId, listId: EntityId,
localWho: TootAccount, localWho: TootAccount,
onListMemberDeleted: (willRegistered: Boolean, bSuccess: Boolean) -> Unit = { _, _ -> }, onListMemberDeleted: (willRegistered: Boolean, bSuccess: Boolean) -> Unit = { _, _ -> },
) { ) {
launchMain { launchMain {
runApiTask(accessInfo) { client -> runApiTask(accessInfo) { client ->

View File

@ -1,14 +1,20 @@
package jp.juggler.subwaytooter.action package jp.juggler.subwaytooter.action
import androidx.appcompat.app.AlertDialog import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.ApiTask
import jp.juggler.subwaytooter.api.entity.* 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.Column
import jp.juggler.subwaytooter.column.fireShowContent import jp.juggler.subwaytooter.column.fireShowContent
import jp.juggler.subwaytooter.column.updateEmojiReactionByApiResponse import jp.juggler.subwaytooter.column.updateEmojiReactionByApiResponse
import jp.juggler.subwaytooter.dialog.DlgConfirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.EmojiPicker import jp.juggler.subwaytooter.dialog.launchEmojiPicker
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.UnicodeEmoji import jp.juggler.subwaytooter.emoji.UnicodeEmoji
@ -47,9 +53,13 @@ fun ActMain.reactionAdd(
} }
if (codeArg == null) { if (codeArg == null) {
EmojiPicker(activity, accessInfo, closeOnSelected = true) { result -> launchEmojiPicker(
activity,
accessInfo,
closeOnSelected = true
) { emoji, _ ->
var newUrl: String? = null var newUrl: String? = null
val newCode: String = when (val emoji = result.emoji) { val newCode: String = when (emoji) {
is UnicodeEmoji -> emoji.unifiedCode is UnicodeEmoji -> emoji.unifiedCode
is CustomEmoji -> { is CustomEmoji -> {
newUrl = emoji.staticUrl newUrl = emoji.staticUrl
@ -61,7 +71,7 @@ fun ActMain.reactionAdd(
} }
} }
reactionAdd(column, status, newCode, newUrl) reactionAdd(column, status, newCode, newUrl)
}.show() }
return return
} }
var code = codeArg var code = codeArg
@ -96,40 +106,26 @@ fun ActMain.reactionAdd(
return return
} }
if (!isConfirmed) { activity.launchAndShowError {
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()
}
override fun onOK() { if (!isConfirmed) {
reactionAdd( val options = DecodeOptions(
column, activity,
status, accessInfo,
codeArg = code, decodeEmoji = true,
urlArg = urlArg, enlargeEmoji = 1.5f,
isConfirmed = true enlargeCustomEmoji = 1.5f
) )
} val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg)
}) confirm(
return getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(accessInfo)),
} accessInfo.confirm_reaction,
) { newConfirmEnabled ->
accessInfo.confirm_reaction = newConfirmEnabled
accessInfo.saveSetting()
}
}
launchMain {
var resultStatus: TootStatus? = null var resultStatus: TootStatus? = null
runApiTask(accessInfo) { client -> runApiTask(accessInfo) { client ->
when { when {
@ -158,12 +154,7 @@ fun ActMain.reactionAdd(
} }
} }
}?.let { result -> }?.let { result ->
result.error?.let { error(it) }
val error = result.error
if (error != null) {
activity.showToast(false, error)
return@launchMain
}
when (val resCode = result.response?.code) { when (val resCode = result.response?.code) {
in 200 until 300 -> when (val newStatus = resultStatus) { in 200 until 300 -> when (val newStatus = resultStatus) {
@ -209,26 +200,19 @@ fun ActMain.reactionRemove(
return return
} }
if (!confirmed) { launchAndShowError {
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
}
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 var resultStatus: TootStatus? = null
runApiTask(accessInfo) { client -> runApiTask(accessInfo) { client ->
when { when {
@ -265,9 +249,13 @@ fun ActMain.reactionRemove(
else -> { else -> {
when (val newStatus = resultStatus) { when (val newStatus = resultStatus) {
null -> null ->
if (status.decreaseReactionMisskey(reaction.name, true, "removeReaction")) { if (status.decreaseReactionMisskey(reaction.name,
true,
"removeReaction")
) {
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する // 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
column.fireShowContent(reason = "removeReaction complete", reset = true) column.fireShowContent(reason = "removeReaction complete",
reset = true)
} }
else -> else ->
@ -290,15 +278,14 @@ private fun ActMain.reactionWithoutUi(
resolvedStatus: TootStatus, resolvedStatus: TootStatus,
reactionCode: String? = null, reactionCode: String? = null,
reactionImage: String? = null, reactionImage: String? = null,
isConfirmed: Boolean = false,
callback: () -> Unit, callback: () -> Unit,
) { ) {
val activity = this val activity = this
if (reactionCode == null) { if (reactionCode == null) {
EmojiPicker(activity, accessInfo, closeOnSelected = true) { result -> launchEmojiPicker(activity, accessInfo, closeOnSelected = true) { emoji, _ ->
var newUrl: String? = null var newUrl: String? = null
val newCode = when (val emoji = result.emoji) { val newCode = when (emoji) {
is UnicodeEmoji -> emoji.unifiedCode is UnicodeEmoji -> emoji.unifiedCode
is CustomEmoji -> { is CustomEmoji -> {
newUrl = emoji.staticUrl newUrl = emoji.staticUrl
@ -314,78 +301,46 @@ private fun ActMain.reactionWithoutUi(
resolvedStatus = resolvedStatus, resolvedStatus = resolvedStatus,
reactionCode = newCode, reactionCode = newCode,
reactionImage = newUrl, reactionImage = newUrl,
isConfirmed = isConfirmed,
callback = callback callback = callback
) )
}.show() }
return return
} }
val canMultipleReaction = InstanceCapability.canMultipleReaction(accessInfo) val canMultipleReaction = InstanceCapability.canMultipleReaction(accessInfo)
if (!isConfirmed) { val options = DecodeOptions(
val options = DecodeOptions( activity,
activity, accessInfo,
accessInfo, decodeEmoji = true,
decodeEmoji = true, enlargeEmoji = 1.5f,
enlargeEmoji = 1.5f, enlargeCustomEmoji = 1.5f
enlargeCustomEmoji = 1.5f )
) val emojiSpan = TootReaction.toSpannableStringBuilder(options, reactionCode, reactionImage)
val emojiSpan = TootReaction.toSpannableStringBuilder(options, reactionCode, reactionImage) val isCustomEmoji = TootReaction.isCustomEmoji(reactionCode)
val url = resolvedStatus.url
val isCustomEmoji = TootReaction.isCustomEmoji(reactionCode) launchAndShowError {
val url = resolvedStatus.url
when { when {
isCustomEmoji && canMultipleReaction -> { isCustomEmoji && canMultipleReaction ->
showToast(false, "can't reaction with custom emoji from this account") error("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
)
}
else -> DlgConfirm.open( isCustomEmoji && url?.likePleromaStatusUrl() == true -> confirm(
activity, 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)), getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(accessInfo)),
object : DlgConfirm.Callback { accessInfo.confirm_reaction,
override var isConfirmEnabled: Boolean ) { newConfirmEnabled ->
get() = accessInfo.confirm_reaction accessInfo.confirm_reaction = newConfirmEnabled
set(bv) { accessInfo.saveSetting()
accessInfo.confirm_reaction = bv }
accessInfo.saveSetting()
}
override fun onOK() {
reactionWithoutUi(
accessInfo = accessInfo,
resolvedStatus = resolvedStatus,
reactionCode = reactionCode,
reactionImage = reactionImage,
isConfirmed = true,
callback = callback
)
}
})
} }
return
}
launchMain {
// var resultStatus: TootStatus? = null // var resultStatus: TootStatus? = null
runApiTask(accessInfo) { client -> runApiTask(accessInfo) { client ->
when { when {
@ -398,7 +353,9 @@ private fun ActMain.reactionWithoutUi(
) // 成功すると204 no content ) // 成功すると204 no content
canMultipleReaction -> client.request( canMultipleReaction -> client.request(
"/api/v1/pleroma/statuses/${resolvedStatus.id}/reactions/${reactionCode.encodePercent("@")}", "/api/v1/pleroma/statuses/${resolvedStatus.id}/reactions/${
reactionCode.encodePercent("@")
}",
"".toFormRequestBody().toPut() "".toFormRequestBody().toPut()
) // 成功すると更新された投稿 ) // 成功すると更新された投稿
@ -475,27 +432,30 @@ fun ActMain.reactionFromAnotherAccount(
statusArg ?: return statusArg ?: return
val activity = this 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 { 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 val list = accountListCanReaction() ?: return@launchMain
if (list.isEmpty()) { if (list.isEmpty()) {
showToast(false, R.string.not_available_for_current_accounts) showToast(false, R.string.not_available_for_current_accounts)

View File

@ -2,22 +2,25 @@ package jp.juggler.subwaytooter.action
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AlertDialog 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.addColumn
import jp.juggler.subwaytooter.actmain.reloadAccountSetting import jp.juggler.subwaytooter.actmain.reloadAccountSetting
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount import jp.juggler.subwaytooter.actmain.showColumnMatchAccount
import jp.juggler.subwaytooter.api.* 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.column.*
import jp.juggler.subwaytooter.dialog.ActionsDialog 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.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.emptyCallback import jp.juggler.subwaytooter.util.emptyCallback
import jp.juggler.util.* import jp.juggler.util.*
import kotlinx.coroutines.CancellationException
import okhttp3.Request import okhttp3.Request
import java.util.*
fun ActMain.clickStatusDelete( fun ActMain.clickStatusDelete(
accessInfo: SavedAccount, accessInfo: SavedAccount,
@ -81,7 +84,8 @@ fun ActMain.clickScheduledToot(accessInfo: SavedAccount, item: TootScheduled, co
scheduledPostEdit(accessInfo, item) scheduledPostEdit(accessInfo, item)
} }
.addAction(getString(R.string.delete)) { .addAction(getString(R.string.delete)) {
scheduledPostDelete(accessInfo, item) { launchAndShowError {
scheduledPostDelete(accessInfo, item)
column.onScheduleDeleted(item) column.onScheduleDeleted(item)
showToast(false, R.string.scheduled_post_deleted) showToast(false, R.string.scheduled_post_deleted)
} }
@ -99,61 +103,44 @@ fun ActMain.favourite(
crossAccountMode: CrossAccountMode, crossAccountMode: CrossAccountMode,
callback: () -> Unit, callback: () -> Unit,
bSet: Boolean = true, bSet: Boolean = true,
bConfirmed: Boolean = false,
) { ) {
if (appState.isBusyFav(accessInfo, statusArg)) { launchAndShowError {
showToast(false, R.string.wait_previous_operation)
return
}
// 必要なら確認を出す if (appState.isBusyFav(accessInfo, statusArg)) {
if (!bConfirmed && accessInfo.isMastodon) { showToast(false, R.string.wait_previous_operation)
DlgConfirm.open( return@launchAndShowError
this, }
getString(
// 必要なら確認を出す
if (accessInfo.isMastodon) {
confirm(
getString(
when (bSet) {
true -> R.string.confirm_favourite_from
else -> R.string.confirm_unfavourite_from
},
AcctColor.getNickname(accessInfo)
),
when (bSet) { when (bSet) {
true -> R.string.confirm_favourite_from true -> accessInfo.confirm_favourite
else -> R.string.confirm_unfavourite_from else -> accessInfo.confirm_unfavourite
},
AcctColor.getNickname(accessInfo)
),
object : DlgConfirm.Callback {
override fun onOK() {
favourite(
accessInfo,
statusArg,
crossAccountMode,
callback,
bSet = bSet,
bConfirmed = true
)
} }
) { newConfirmEnabled ->
when (bSet) {
true -> accessInfo.confirm_favourite = newConfirmEnabled
else -> accessInfo.confirm_unfavourite = newConfirmEnabled
}
accessInfo.saveSetting()
reloadAccountSetting(accessInfo)
}
}
override var isConfirmEnabled: Boolean //
get() = when (bSet) { appState.setBusyFav(accessInfo, statusArg)
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) showColumnMatchAccount(accessInfo)
// ファボ表示を更新中にする
showColumnMatchAccount(accessInfo)
launchMain {
var resultStatus: TootStatus? = null var resultStatus: TootStatus? = null
val result = runApiTask( val result = runApiTask(
accessInfo, accessInfo,
@ -284,7 +271,6 @@ fun ActMain.bookmark(
crossAccountMode: CrossAccountMode, crossAccountMode: CrossAccountMode,
callback: () -> Unit, callback: () -> Unit,
bSet: Boolean = true, bSet: Boolean = true,
bConfirmed: Boolean = false,
) { ) {
if (appState.isBusyFav(accessInfo, statusArg)) { if (appState.isBusyFav(accessInfo, statusArg)) {
showToast(false, R.string.wait_previous_operation) showToast(false, R.string.wait_previous_operation)
@ -294,47 +280,30 @@ fun ActMain.bookmark(
showToast(false, R.string.misskey_account_not_supported) showToast(false, R.string.misskey_account_not_supported)
return return
} }
launchAndShowError {
// 必要なら確認を出す // 必要なら確認を出す
// ブックマークは解除する時だけ確認する // ブックマークは解除する時だけ確認する
if (!bConfirmed && !bSet) { if (!bSet) {
DlgConfirm.open( confirm(
this, getString(
getString( R.string.confirm_unbookmark_from,
R.string.confirm_unbookmark_from, AcctColor.getNickname(accessInfo)
AcctColor.getNickname(accessInfo) ),
), accessInfo.confirm_unbookmark
object : DlgConfirm.Callback { ) { newConfirmEnabled ->
override var isConfirmEnabled: Boolean accessInfo.confirm_unbookmark = newConfirmEnabled
get() = accessInfo.confirm_unbookmark accessInfo.saveSetting()
set(value) { reloadAccountSetting(accessInfo)
accessInfo.confirm_unbookmark = value }
accessInfo.saveSetting() }
reloadAccountSetting(accessInfo)
}
override fun onOK() { //
bookmark( appState.setBusyBookmark(accessInfo, statusArg)
accessInfo = accessInfo,
statusArg = statusArg,
crossAccountMode = crossAccountMode,
callback = callback,
bSet = bSet,
bConfirmed = true,
)
}
})
return
}
// // ファボ表示を更新中にする
appState.setBusyBookmark(accessInfo, statusArg) showColumnMatchAccount(accessInfo)
// ファボ表示を更新中にする
showColumnMatchAccount(accessInfo)
//
launchMain {
var resultStatus: TootStatus? = null var resultStatus: TootStatus? = null
val result = runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client -> val result = runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
val targetStatus = if (crossAccountMode.isRemote) { val targetStatus = if (crossAccountMode.isRemote) {
@ -580,40 +549,21 @@ fun ActMain.statusEdit(
} }
} }
fun ActMain.scheduledPostDelete( suspend fun ActMain.scheduledPostDelete(
accessInfo: SavedAccount, accessInfo: SavedAccount,
item: TootScheduled, item: TootScheduled,
bConfirmed: Boolean = false, bConfirmed: Boolean = false,
callback: () -> Unit,
) { ) {
val act = this@scheduledPostDelete
if (!bConfirmed) { if (!bConfirmed) {
DlgConfirm.openSimple( confirm(R.string.scheduled_status_delete_confirm)
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)
}
}
} }
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( fun ActMain.scheduledPostEdit(

View File

@ -5,16 +5,19 @@ import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.TextView import android.widget.TextView
import androidx.core.view.GravityCompat 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.actpost.CompletionHelper
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootVisibility import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.PostCompleteCallback
import jp.juggler.subwaytooter.util.PostImpl import jp.juggler.subwaytooter.util.PostImpl
import jp.juggler.subwaytooter.util.PostResult
import jp.juggler.util.hideKeyboard import jp.juggler.util.hideKeyboard
import jp.juggler.util.launchAndShowError
import jp.juggler.util.launchMain import jp.juggler.util.launchMain
import org.jetbrains.anko.imageResource import org.jetbrains.anko.imageResource
@ -104,39 +107,39 @@ fun ActMain.performQuickPost(account: SavedAccount?) {
etQuickPost.hideKeyboard() etQuickPost.hideKeyboard()
PostImpl( launchAndShowError {
activity = this, val postResult = PostImpl(
account = account, activity = this@performQuickPost,
content = etQuickPost.text.toString().trim { it <= ' ' }, account = account,
spoilerText = null, content = etQuickPost.text.toString().trim { it <= ' ' },
visibilityArg = when (quickPostVisibility) { spoilerText = null,
TootVisibility.AccountSetting -> account.visibility visibilityArg = when (quickPostVisibility) {
else -> quickPostVisibility TootVisibility.AccountSetting -> account.visibility
}, else -> quickPostVisibility
bNSFW = false, },
inReplyToId = null, bNSFW = false,
attachmentListArg = null, inReplyToId = null,
enqueteItemsArg = null, attachmentListArg = null,
pollType = null, enqueteItemsArg = null,
pollExpireSeconds = 0, pollType = null,
pollHideTotals = false, pollExpireSeconds = 0,
pollMultipleChoice = false, pollHideTotals = false,
scheduledAt = 0L, pollMultipleChoice = false,
scheduledId = null, scheduledAt = 0L,
redraftStatusId = null, scheduledId = null,
editStatusId = null, redraftStatusId = null,
emojiMapCustom = App1.custom_emoji_lister.getMap(account), editStatusId = null,
useQuoteToot = false, emojiMapCustom = App1.custom_emoji_lister.getMapNonBlocking(account),
callback = object : PostCompleteCallback { useQuoteToot = false,
override fun onScheduledPostComplete(targetAccount: SavedAccount) {} ).runSuspend()
override fun onPostComplete(targetAccount: SavedAccount, status: TootStatus) {
etQuickPost.setText("") if (postResult is PostResult.Normal) {
postedAcct = targetAccount.acct etQuickPost.setText("")
postedStatusId = status.id postedAcct = postResult.targetAccount.acct
postedReplyId = status.in_reply_to_id postedStatusId = postResult.status.id
postedRedraftId = null postedReplyId = postResult.status.in_reply_to_id
refreshAfterPost() postedRedraftId = null
} refreshAfterPost()
} }
).run() }
} }

View File

@ -22,10 +22,10 @@ fun ActPost.selectAccount(a: SavedAccount?) {
views.btnAccount.setTextColor(attrColor(android.R.attr.textColorPrimary)) views.btnAccount.setTextColor(attrColor(android.R.attr.textColorPrimary))
views.btnAccount.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp) views.btnAccount.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
} else { } else {
launchMain {
// 先読みしてキャッシュに保持しておく // 先読みしてキャッシュに保持しておく
App1.custom_emoji_lister.getList(a) {
// 何もしない // 何もしない
App1.custom_emoji_lister.getList(a)
} }
val ac = AcctColor.load(a) val ac = AcctColor.load(a)

View File

@ -288,64 +288,67 @@ fun ActPost.performMore() {
} }
fun ActPost.performPost() { fun ActPost.performPost() {
// アップロード中は投稿できない val activity = this
if (attachmentList.any { it.status == PostAttachment.Status.Progress }) { launchAndShowError {
showToast(false, R.string.media_attachment_still_uploading) // アップロード中は投稿できない
return if (attachmentList.any { it.status == PostAttachment.Status.Progress }) {
} showToast(false, R.string.media_attachment_still_uploading)
return@launchAndShowError
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
} }
2 -> {
pollType = TootPollsType.FriendsNico
pollItems = pollChoiceList()
}
}
PostImpl( val account = activity.account ?: return@launchAndShowError
activity = this, var pollType: TootPollsType? = null
account = account, var pollItems: ArrayList<String>? = null
content = views.etContent.text.toString().trim { it <= ' ' }, var pollExpireSeconds = 0
spoilerText = when { var pollHideTotals = false
!views.cbContentWarning.isChecked -> null var pollMultipleChoice = false
else -> views.etContentWarning.text.toString().trim { it <= ' ' } when (views.spPollType.selectedItemPosition) {
}, 1 -> {
visibilityArg = states.visibility ?: TootVisibility.Public, pollType = TootPollsType.Mastodon
bNSFW = views.cbNSFW.isChecked, pollItems = pollChoiceList()
inReplyToId = states.inReplyToId, pollExpireSeconds = pollExpireSeconds()
attachmentListArg = this.attachmentList, pollHideTotals = views.cbHideTotals.isChecked
enqueteItemsArg = pollItems, pollMultipleChoice = views.cbMultipleChoice.isChecked
pollType = pollType, }
pollExpireSeconds = pollExpireSeconds, 2 -> {
pollHideTotals = pollHideTotals, pollType = TootPollsType.FriendsNico
pollMultipleChoice = pollMultipleChoice, pollItems = pollChoiceList()
scheduledAt = states.timeSchedule, }
scheduledId = scheduledStatus?.id, }
redraftStatusId = states.redraftStatusId,
editStatusId = states.editStatusId, val postResult = PostImpl(
emojiMapCustom = App1.custom_emoji_lister.getMap(account), activity = activity,
useQuoteToot = views.cbQuote.isChecked, account = account,
callback = object : PostCompleteCallback { content = views.etContent.text.toString().trim { it <= ' ' },
override fun onPostComplete(targetAccount: SavedAccount, status: TootStatus) { 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() val data = Intent()
data.putExtra(ActPost.EXTRA_POSTED_ACCT, targetAccount.acct.ascii) data.putExtra(ActPost.EXTRA_POSTED_ACCT, postResult.targetAccount.acct.ascii)
status.id.putTo(data, ActPost.EXTRA_POSTED_STATUS_ID) postResult.status.id.putTo(data, ActPost.EXTRA_POSTED_STATUS_ID)
states.redraftStatusId?.putTo(data, ActPost.EXTRA_POSTED_REDRAFT_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) { 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) ActMain.refActMain?.get()?.onCompleteActPost(data)
@ -360,11 +363,10 @@ fun ActPost.performPost() {
this@performPost.finish() this@performPost.finish()
} }
} }
is PostResult.Scheduled ->{
override fun onScheduledPostComplete(targetAccount: SavedAccount) {
showToast(false, getString(R.string.scheduled_status_sent)) showToast(false, getString(R.string.scheduled_status_sent))
val data = Intent() val data = Intent()
data.putExtra(ActPost.EXTRA_POSTED_ACCT, targetAccount.acct.ascii) data.putExtra(ActPost.EXTRA_POSTED_ACCT,postResult. targetAccount.acct.ascii)
if (isMultiWindowPost) { if (isMultiWindowPost) {
resetText() resetText()
@ -378,7 +380,7 @@ fun ActPost.performPost() {
} }
} }
} }
).run() }
} }
fun ActPost.showContentWarningEnabled() { fun ActPost.showContentWarningEnabled() {

View File

@ -8,11 +8,9 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.entity.TootTag
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.EmojiPicker import jp.juggler.subwaytooter.dialog.launchEmojiPicker
import jp.juggler.subwaytooter.dialog.EmojiPickerResult
import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.EmojiBase import jp.juggler.subwaytooter.emoji.EmojiBase
import jp.juggler.subwaytooter.emoji.UnicodeEmoji 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.util.PopupAutoCompleteAcct
import jp.juggler.subwaytooter.view.MyEditText import jp.juggler.subwaytooter.view.MyEditText
import jp.juggler.util.* import jp.juggler.util.*
import java.util.*
import kotlin.math.min import kotlin.math.min
// 入力補完機能 // 入力補完機能
@ -108,7 +105,7 @@ class CompletionHelper(
private var accessInfo: SavedAccount? = null 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() if (popup?.isShowing == true) procTextChanged.run()
} }
@ -261,9 +258,12 @@ class CompletionHelper(
) = buildList<CharSequence> { ) = buildList<CharSequence> {
accessInfo ?: return@buildList accessInfo ?: return@buildList
val customList = val customList = App1.custom_emoji_lister.getListNonBlocking(
App1.custom_emoji_lister.getListWithAliases(accessInfo, onEmojiListLoad) accessInfo,
?: return@buildList withAliases = true,
callback = onEmojiListLoad
)
?: return@buildList
for (item in customList) { for (item in customList) {
if (size >= limit) break if (size >= limit) break
@ -310,7 +310,12 @@ class CompletionHelper(
fun setInstance(accessInfo: SavedAccount?) { fun setInstance(accessInfo: SavedAccount?) {
this.accessInfo = accessInfo 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() if (popup?.isShowing == true) procTextChanged.run()
} }
@ -400,8 +405,10 @@ class CompletionHelper(
// et.setCustomSelectionActionModeCallback( action_mode_callback ); // et.setCustomSelectionActionModeCallback( action_mode_callback );
} }
private fun SpannableStringBuilder.appendEmoji(result: EmojiPickerResult) = private fun SpannableStringBuilder.appendEmoji(
appendEmoji(result.bInstanceHasCustomEmoji, result.emoji) emoji: EmojiBase,
bInstanceHasCustomEmoji: Boolean,
) = appendEmoji(bInstanceHasCustomEmoji, emoji)
private fun SpannableStringBuilder.appendEmoji( private fun SpannableStringBuilder.appendEmoji(
bInstanceHasCustomEmoji: Boolean, bInstanceHasCustomEmoji: Boolean,
@ -434,21 +441,22 @@ class CompletionHelper(
} }
private val openPickerEmoji: Runnable = Runnable { private val openPickerEmoji: Runnable = Runnable {
EmojiPicker( launchEmojiPicker(
activity, accessInfo, activity,
accessInfo,
closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected(pref) closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected(pref)
) { result -> ) { emoji, bInstanceHasCustomEmoji ->
val et = this.et ?: return@EmojiPicker val et = this@CompletionHelper.et ?: return@launchEmojiPicker
val src = et.text ?: "" val src = et.text ?: ""
val srcLength = src.length val srcLength = src.length
val end = min(srcLength, et.selectionEnd) val end = min(srcLength, et.selectionEnd)
val start = src.lastIndexOf(':', end - 1) 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() val sb = SpannableStringBuilder()
.append(src.subSequence(0, start)) .append(src.subSequence(0, start))
.appendEmoji(result) .appendEmoji(emoji, bInstanceHasCustomEmoji)
val newSelection = sb.length val newSelection = sb.length
if (end < srcLength) sb.append(src.subSequence(end, srcLength)) if (end < srcLength) sb.append(src.subSequence(end, srcLength))
@ -463,15 +471,16 @@ class CompletionHelper(
activity, activity,
"PostHelper/EmojiPicker/cb" "PostHelper/EmojiPicker/cb"
).handler.post { et.showKeyboard() } ).handler.post { et.showKeyboard() }
}.show() }
} }
fun openEmojiPickerFromMore() { fun openEmojiPickerFromMore() {
EmojiPicker( launchEmojiPicker(
activity, accessInfo, activity,
accessInfo,
closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected(pref) closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected(pref)
) { result -> ) { emoji, bInstanceHasCustomEmoji ->
val et = this.et ?: return@EmojiPicker val et = this@CompletionHelper.et ?: return@launchEmojiPicker
val src = et.text ?: "" val src = et.text ?: ""
val srcLength = src.length val srcLength = src.length
@ -480,7 +489,7 @@ class CompletionHelper(
val sb = SpannableStringBuilder() val sb = SpannableStringBuilder()
.append(src.subSequence(0, start)) .append(src.subSequence(0, start))
.appendEmoji(result) .appendEmoji(emoji, bInstanceHasCustomEmoji)
val newSelection = sb.length val newSelection = sb.length
if (end < srcLength) sb.append(src.subSequence(end, srcLength)) if (end < srcLength) sb.append(src.subSequence(end, srcLength))
@ -489,7 +498,7 @@ class CompletionHelper(
et.setSelection(newSelection) et.setSelection(newSelection)
procTextChanged.run() procTextChanged.run()
}.show() }
} }
private fun SpannableStringBuilder.appendHashTag(tagWithoutSharp: String): SpannableStringBuilder { private fun SpannableStringBuilder.appendHashTag(tagWithoutSharp: String): SpannableStringBuilder {

View File

@ -215,7 +215,7 @@ class TootReaction(
// そのドメインの絵文字一覧を取得済みなら // そのドメインの絵文字一覧を取得済みなら
// それを使う // それを使う
App1.custom_emoji_lister App1.custom_emoji_lister
.getMap(accessInfo) .getMapNonBlocking(accessInfo)
?.get(key) ?.get(key)
?.chooseUrl() ?.chooseUrl()
?.notEmpty() ?.notEmpty()

View File

@ -21,7 +21,7 @@ import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.Column import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.column.getContentColor 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.CustomEmoji
import jp.juggler.subwaytooter.emoji.UnicodeEmoji import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
@ -129,7 +129,7 @@ private fun ColumnViewHolder.showAnnouncementsEmpty() {
private fun ColumnViewHolder.showAnnouncementColors( private fun ColumnViewHolder.showAnnouncementColors(
expand: Boolean, expand: Boolean,
enablePaging: Boolean, enablePaging: Boolean,
contentColor: Int contentColor: Int,
) { ) {
val alphaPrevNext = if (enablePaging) 1f else 0.3f val alphaPrevNext = if (enablePaging) 1f else 0.3f
@ -413,14 +413,14 @@ private fun ColumnViewHolder.showReactions(
fun ColumnViewHolder.reactionAdd(item: TootAnnouncement, sample: TootReaction?) { fun ColumnViewHolder.reactionAdd(item: TootAnnouncement, sample: TootReaction?) {
val column = column ?: return val column = column ?: return
if (sample == null) { if (sample == null) {
EmojiPicker(activity, column.accessInfo, closeOnSelected = true) { result -> launchEmojiPicker(activity, column.accessInfo, closeOnSelected = true) { emoji, _ ->
val emoji = result.emoji
val code = when (emoji) { val code = when (emoji) {
is UnicodeEmoji -> emoji.unifiedCode is UnicodeEmoji -> emoji.unifiedCode
is CustomEmoji -> emoji.shortcode 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 { reactionAdd(item, TootReaction.parseFedibird(jsonObject {
put("name", code) put("name", code)
put("count", 1) put("count", 1)
@ -431,11 +431,10 @@ fun ColumnViewHolder.reactionAdd(item: TootAnnouncement, sample: TootReaction?)
putNotNull("static_url", emoji.staticUrl) putNotNull("static_url", emoji.staticUrl)
} }
})) }))
}.show() }
return return
} }
activity.launchAndShowError {
launchMain {
activity.runApiTask(column.accessInfo) { client -> activity.runApiTask(column.accessInfo) { client ->
client.request( client.request(
"/api/v1/announcements/${item.id}/reactions/${sample.name.encodePercent()}", "/api/v1/announcements/${item.id}/reactions/${sample.name.encodePercent()}",

View File

@ -7,20 +7,21 @@ import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.TootReaction import jp.juggler.subwaytooter.api.entity.TootReaction
import jp.juggler.subwaytooter.column.getContentColor 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.CustomEmoji
import jp.juggler.subwaytooter.emoji.UnicodeEmoji import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
import jp.juggler.subwaytooter.util.minWidthCompat import jp.juggler.subwaytooter.util.minWidthCompat
import jp.juggler.subwaytooter.util.startMargin import jp.juggler.subwaytooter.util.startMargin
import jp.juggler.util.launchAndShowError
import org.jetbrains.anko.allCaps import org.jetbrains.anko.allCaps
fun ColumnViewHolder.addEmojiQuery(reaction: TootReaction? = null) { fun ColumnViewHolder.addEmojiQuery(reaction: TootReaction? = null) {
val column = this.column ?: return val column = this.column ?: return
if (reaction == null) { if (reaction == null) {
EmojiPicker(activity, column.accessInfo, closeOnSelected = true) { result -> launchEmojiPicker(activity, column.accessInfo, closeOnSelected = true) { emoji, _ ->
val newReaction = when (val emoji = result.emoji) { val newReaction = when (emoji) {
is UnicodeEmoji -> TootReaction(name = emoji.unifiedCode) is UnicodeEmoji -> TootReaction(name = emoji.unifiedCode)
is CustomEmoji -> TootReaction( is CustomEmoji -> TootReaction(
name = emoji.shortcode, name = emoji.shortcode,
@ -29,7 +30,7 @@ fun ColumnViewHolder.addEmojiQuery(reaction: TootReaction? = null) {
) )
} }
addEmojiQuery(newReaction) addEmojiQuery(newReaction)
}.show() }
return return
} }
val list = TootReaction.decodeEmojiQuery(column.searchQuery).toMutableList() val list = TootReaction.decodeEmojiQuery(column.searchQuery).toMutableList()
@ -49,63 +50,64 @@ private fun ColumnViewHolder.removeEmojiQuery(target: TootReaction?) {
fun ColumnViewHolder.updateReactionQueryView() { fun ColumnViewHolder.updateReactionQueryView() {
val column = this.column ?: return 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() val act = this.activity // not Button(View).getActivity()
act.launchAndShowError {
val buttonHeight = ActMain.boostButtonSize flEmoji.removeAllViews()
val marginBetween = (buttonHeight.toFloat() * 0.05f + 0.5f).toInt()
val paddingH = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt() for (invalidator in emojiQueryInvalidatorList) {
val paddingV = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt() invalidator.register(null)
}
val contentColor = column.getContentColor() emojiQueryInvalidatorList.clear()
TootReaction.decodeEmojiQuery(column.searchQuery).forEachIndexed { index, reaction -> val options = DecodeOptions(
val ssb = reaction.toSpannableStringBuilder(options, status = null) act,
column.accessInfo,
val b = AppCompatButton(activity).apply { decodeEmoji = true,
layoutParams = FlexboxLayout.LayoutParams( enlargeEmoji = 1.5f,
FlexboxLayout.LayoutParams.WRAP_CONTENT, enlargeCustomEmoji = 1.5f
buttonHeight )
).apply {
if (index > 0) startMargin = marginBetween val buttonHeight = ActMain.boostButtonSize
} val marginBetween = (buttonHeight.toFloat() * 0.05f + 0.5f).toInt()
minWidthCompat = buttonHeight
val paddingH = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
background = ContextCompat.getDrawable(act, R.drawable.btn_bg_transparent_round6dp) val paddingV = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
setTextColor(contentColor) val contentColor = column.getContentColor()
setPadding(paddingH, paddingV, paddingH, paddingV)
TootReaction.decodeEmojiQuery(column.searchQuery).forEachIndexed { index, reaction ->
text = ssb val ssb = reaction.toSpannableStringBuilder(options, status = null)
allCaps = false val b = AppCompatButton(activity).apply {
tag = reaction layoutParams = FlexboxLayout.LayoutParams(
FlexboxLayout.LayoutParams.WRAP_CONTENT,
setOnLongClickListener { buttonHeight
removeEmojiQuery(it.tag as? TootReaction) ).apply {
true if (index > 0) startMargin = marginBetween
} }
// カスタム絵文字の場合、アニメーション等のコールバックを処理する必要がある minWidthCompat = buttonHeight
val invalidator = NetworkEmojiInvalidator(act.handler, this)
invalidator.register(ssb) background = ContextCompat.getDrawable(act, R.drawable.btn_bg_transparent_round6dp)
emojiQueryInvalidatorList.add(invalidator)
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)
} }
} }

View File

@ -1,60 +1,125 @@
package jp.juggler.subwaytooter.dialog package jp.juggler.subwaytooter.dialog
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.view.View import android.view.View
import android.widget.CheckBox import androidx.annotation.StringRes
import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.R 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 { object DlgConfirm {
interface Callback { // interface Callback {
var isConfirmEnabled: Boolean // 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") @SuppressLint("InflateParams")
fun open(activity: Activity, message: String, callback: Callback) { suspend fun AppCompatActivity.confirm(
message: String,
if (!callback.isConfirmEnabled) { getConfirmEnabled: Boolean,
callback.onOK() setConfirmEnabled: (newConfirmEnabled: Boolean) -> Unit,
return ) {
} if (!getConfirmEnabled) return
suspendCancellableCoroutine<Unit> { cont ->
val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false) try {
val tvMessage = view.findViewById<TextView>(R.id.tvMessage) val views = DlgConfirmBinding.inflate(layoutInflater)
val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext) views.tvMessage.text = message
tvMessage.text = message val dialog = AlertDialog.Builder(this)
.setView(views.root)
AlertDialog.Builder(activity) .setCancelable(true)
.setView(view) .setNegativeButton(R.string.cancel, null)
.setCancelable(true) .setPositiveButton(R.string.ok) { _, _ ->
.setNegativeButton(R.string.cancel, null) if (views.cbSkipNext.isChecked) {
.setPositiveButton(R.string.ok) { _, _ -> setConfirmEnabled(false)
if (cbSkipNext.isChecked) { }
callback.isConfirmEnabled = 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") suspend fun AppCompatActivity.confirm(@StringRes messageId: Int, vararg args: Any?) =
fun openSimple(activity: Activity, message: String, callback: () -> Unit) { confirm(getString(messageId, args))
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) suspend fun AppCompatActivity.confirm(message: String) {
.setView(view) suspendCancellableCoroutine<Unit> { cont ->
.setCancelable(true) try {
.setNegativeButton(R.string.cancel, null) val views = DlgConfirmBinding.inflate(layoutInflater)
.setPositiveButton(R.string.ok) { _, _ -> callback() } views.tvMessage.text = message
.show() 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

View File

@ -18,6 +18,8 @@ class UnicodeEmoji(
@DrawableRes val drawableId: Int = 0, @DrawableRes val drawableId: Int = 0,
) : EmojiBase, Comparable<UnicodeEmoji> { ) : EmojiBase, Comparable<UnicodeEmoji> {
val namesLower = ArrayList<String>()
// unified code used in picker. // unified code used in picker.
var unifiedCode = "" var unifiedCode = ""

View File

@ -1,20 +1,22 @@
package jp.juggler.subwaytooter.emoji 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>() val emojiList = ArrayList<UnicodeEmoji>()

View File

@ -45,6 +45,7 @@ class EmojiMapLoader(
private fun addName(emoji: UnicodeEmoji, name: String) { private fun addName(emoji: UnicodeEmoji, name: String) {
dst.shortNameMap[name] = emoji dst.shortNameMap[name] = emoji
dst.shortNameList.add(name) dst.shortNameList.add(name)
emoji.namesLower.add(name.lowercase())
} }
private fun readEmojiDataLine(lno: Int, rawLine: String) { private fun readEmojiDataLine(lno: Int, rawLine: String) {

View File

@ -14,13 +14,17 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.action.reactionAdd import jp.juggler.subwaytooter.action.reactionAdd
import jp.juggler.subwaytooter.action.reactionFromAnotherAccount import jp.juggler.subwaytooter.action.reactionFromAnotherAccount
import jp.juggler.subwaytooter.action.reactionRemove import jp.juggler.subwaytooter.action.reactionRemove
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.TootReaction import jp.juggler.subwaytooter.api.entity.TootReaction
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.util.* 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.allCaps
import org.jetbrains.anko.dip import org.jetbrains.anko.dip

View File

@ -8,15 +8,17 @@ import jp.juggler.subwaytooter.api.entity.parseList
import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.* import jp.juggler.util.*
import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class CustomEmojiLister( class CustomEmojiLister(
val context: Context, val context: Context,
private val handler: Handler, private val handler: Handler,
) { ) {
companion object { companion object {
private val log = LogCategory("CustomEmojiLister") private val log = LogCategory("CustomEmojiLister")
@ -31,8 +33,8 @@ class CustomEmojiLister(
internal class CacheItem( internal class CacheItem(
val key: String, val key: String,
var list: ArrayList<CustomEmoji>? = null, var list: List<CustomEmoji>,
var listWithAliases: ArrayList<CustomEmoji>? = null, var listWithAliases: List<CustomEmoji>,
// ロードした時刻 // ロードした時刻
var timeUpdate: Long = elapsedTime, var timeUpdate: Long = elapsedTime,
// 参照された時刻 // 参照された時刻
@ -40,10 +42,19 @@ class CustomEmojiLister(
) )
internal class Request( internal class Request(
val cont: Continuation<List<CustomEmoji>>,
val accessInfo: SavedAccount, val accessInfo: SavedAccount,
val reportWithAliases: Boolean = false, 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>() internal val cache = ConcurrentHashMap<String, CacheItem>()
@ -51,16 +62,12 @@ class CustomEmojiLister(
// エラーキャッシュ // エラーキャッシュ
internal val cacheError = ConcurrentHashMap<String, Long>() internal val cacheError = ConcurrentHashMap<String, Long>()
private val cacheErrorItem = CacheItem("error") private val cacheErrorItem = CacheItem("error", emptyList(), emptyList())
// ロード要求 // ロード要求
internal val queue = ConcurrentLinkedQueue<Request>() internal val queue = ConcurrentLinkedQueue<Request>()
private val worker: Worker private val worker = Worker()
init {
this.worker = Worker()
}
// ネットワーク接続が変化したらエラーキャッシュをクリア // ネットワーク接続が変化したらエラーキャッシュをクリア
fun onNetworkChanged() { fun onNetworkChanged() {
@ -86,59 +93,50 @@ class CustomEmojiLister(
return null return null
} }
fun getList( // インスタンス用のカスタム絵文字のリストを取得する
// または例外を投げる
suspend fun getList(
accessInfo: SavedAccount, accessInfo: SavedAccount,
onListLoaded: (list: ArrayList<CustomEmoji>) -> Unit, withAliases: Boolean = false,
): ArrayList<CustomEmoji>? { ): List<CustomEmoji> {
try { synchronized(cache) {
synchronized(cache) { getCached(elapsedTime, accessInfo)
val item = getCached(elapsedTime, accessInfo) }?.let { return it.list }
if (item != null) return item.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)) fun getListNonBlocking(
worker.notifyEx() accessInfo: SavedAccount,
} catch (ex: Throwable) { withAliases: Boolean = false,
log.trace(ex) 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 return null
} }
fun getListWithAliases( // suspend fun getMap(accessInfo: SavedAccount) =
accessInfo: SavedAccount, // HashMap<String, CustomEmoji>().apply {
onListLoaded: (list: ArrayList<CustomEmoji>) -> Unit, // getList(accessInfo).forEach { put(it.shortcode, it) }
): ArrayList<CustomEmoji>? { // }
try {
synchronized(cache) { fun getMapNonBlocking(accessInfo: SavedAccount) =
val item = getCached(elapsedTime, accessInfo) getListNonBlocking(accessInfo)?.let {
if (item != null) return item.listWithAliases 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() { private inner class Worker : WorkerBase() {
@ -156,69 +154,10 @@ class CustomEmojiLister(
waitEx(86400000L) waitEx(86400000L)
continue 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 { try {
val data = if (accessInfo.isMisskey) { request.resume(handleRequest(request))
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)
}
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.trace(ex) request.cont.resumeWithException(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)
}
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.trace(ex) log.trace(ex)
@ -227,20 +166,58 @@ class CustomEmojiLister(
} }
} }
private fun fireCallback( private suspend fun handleRequest(request: Request): CacheItem {
request: Request, synchronized(cache) {
list: ArrayList<CustomEmoji>, (getCached(elapsedTime, request.accessInfo)
listWithAliases: ArrayList<CustomEmoji>, ?.takeIf { it != cacheErrorItem })
) { .also {
handler.post { if (it == null) {
request.onListLoaded( // エラーキャッシュは一定時間で除去される
if (request.reportWithAliases) { sweepCache()
listWithAliases }
} else {
list
} }
}?.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( private fun decodeEmojiList(
data: String, data: String,
accessInfo: SavedAccount, accessInfo: SavedAccount,
): ArrayList<CustomEmoji>? { ): List<CustomEmoji> =
return try { if (accessInfo.isMisskey) {
val list = if (accessInfo.isMisskey) { parseList(
parseList( CustomEmoji.decodeMisskey,
CustomEmoji.decodeMisskey, accessInfo.apDomain,
accessInfo.apDomain, data.decodeJsonObject().jsonArray("emojis")
data.decodeJsonObject().jsonArray("emojis") )
) } else {
} else { parseList(
parseList( CustomEmoji.decode,
CustomEmoji.decode, accessInfo.apDomain,
accessInfo.apDomain, data.decodeJsonArray()
data.decodeJsonArray() )
) }.apply {
} sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode })
list.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode })
list
} catch (ex: Throwable) {
log.e(ex, "decodeEmojiList failed. instance=${accessInfo.apiHost.ascii}")
null
} }
}
private fun makeListWithAlias(list: ArrayList<CustomEmoji>?): ArrayList<CustomEmoji> { private fun makeListWithAlias(
val dst = ArrayList<CustomEmoji>() list: List<CustomEmoji>,
if (list != null) { ) = ArrayList<CustomEmoji>().apply {
dst.addAll(list) addAll(list)
for (item in list) { for (item in list) {
val aliases = item.aliases ?: continue val aliases = item.aliases ?: continue
for (alias in aliases) { for (alias in aliases) {
if (alias.equals(item.shortcode, ignoreCase = true)) continue if (alias.equals(item.shortcode, ignoreCase = true)) continue
dst.add(item.makeAlias(alias)) 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 })
} }
} }
} }

View File

@ -1,13 +1,12 @@
package jp.juggler.subwaytooter.util package jp.juggler.subwaytooter.util
import android.os.SystemClock import android.os.SystemClock
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.Styler import jp.juggler.subwaytooter.Styler
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.* 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.emoji.CustomEmoji
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.MyClickableSpan 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.SavedAccount
import jp.juggler.subwaytooter.table.TagSet import jp.juggler.subwaytooter.table.TagSet
import jp.juggler.util.* import jp.juggler.util.*
import kotlinx.coroutines.Job import kotlinx.coroutines.*
import kotlinx.coroutines.delay
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
interface PostCompleteCallback { sealed class PostResult {
fun onPostComplete(targetAccount: SavedAccount, status: TootStatus) class Normal(
fun onScheduledPostComplete(targetAccount: SavedAccount) val targetAccount: SavedAccount,
val status: TootStatus,
) : PostResult()
class Scheduled(
val targetAccount: SavedAccount,
) : PostResult()
} }
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -51,8 +55,6 @@ class PostImpl(
val editStatusId: EntityId?, val editStatusId: EntityId?,
val emojiMapCustom: HashMap<String, CustomEmoji>?, val emojiMapCustom: HashMap<String, CustomEmoji>?,
var useQuoteToot: Boolean, var useQuoteToot: Boolean,
val callback: PostCompleteCallback,
) { ) {
companion object { companion object {
private val log = LogCategory("PostImpl") private val log = LogCategory("PostImpl")
@ -70,11 +72,6 @@ class PostImpl(
private var visibilityChecked: TootVisibility? = null 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 { private val choiceMaxChars = when {
account.isMisskey -> 15 account.isMisskey -> 15
pollType == TootPollsType.FriendsNico -> 15 pollType == TootPollsType.FriendsNico -> 15
@ -130,106 +127,6 @@ class PostImpl(
return true 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 resultStatus: TootStatus? = null
private var resultCredentialTmp: TootAccount? = null private var resultCredentialTmp: TootAccount? = null
private var resultScheduledStatusSucceeded = false private var resultScheduledStatusSucceeded = false
@ -503,14 +400,57 @@ class PostImpl(
} }
} }
fun run() { suspend fun runSuspend(): PostResult {
if (!preCheck()) return if (!preCheck()) throw CancellationException("preCheck failed.")
if (!confirm()) return
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) { if (postJob?.get()?.isActive == true) {
activity.showToast(false, R.string.post_button_tapped_repeatly) activity.showToast(false, R.string.post_button_tapped_repeatly)
return throw CancellationException("preCheck failed.")
} }
// ボタン連打判定 // ボタン連打判定
@ -519,10 +459,12 @@ class PostImpl(
lastPostTapped = now lastPostTapped = now
if (delta < 1000L) { if (delta < 1000L) {
activity.showToast(false, R.string.post_button_tapped_repeatly) 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( activity.runApiTask(
account, account,
progressSetup = { it.setCanceledOnTouchOutside(false) }, progressSetup = { it.setCanceledOnTouchOutside(false) },
@ -622,19 +564,21 @@ class PostImpl(
saveStatusTag(status) saveStatusTag(status)
} }
} }
}?.let { result -> }.let { result ->
if (result == null) throw CancellationException()
val status = resultStatus val status = resultStatus
val scheduledStatusSucceeded = resultScheduledStatusSucceeded
when { when {
scheduledStatusSucceeded -> resultScheduledStatusSucceeded ->
callback.onScheduledPostComplete(account) PostResult.Scheduled(account)
// 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る // 連投して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 }
} }
} }

View File

@ -1,6 +1,7 @@
package jp.juggler.util package jp.juggler.util
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import jp.juggler.subwaytooter.dialog.ProgressDialogEx import jp.juggler.subwaytooter.dialog.ProgressDialogEx
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.lang.ref.WeakReference 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で動作するコルーチンを起動して、終了を待たずにリターンする。 // Default Dispatcherで動作するコルーチンを起動して、終了を待たずにリターンする。
// 起動されたアクティビティのライフサイクルに関わらず中断しない。 // 起動されたアクティビティのライフサイクルに関わらず中断しない。
fun launchDefault(block: suspend CoroutineScope.() -> Unit): Job = fun launchDefault(block: suspend CoroutineScope.() -> Unit): Job =

View File

@ -2,57 +2,87 @@ package jp.juggler.util
import android.content.Context import android.content.Context
import android.widget.Toast 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 me.drakeet.support.toast.ToastCompat
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
object ToastUtils { private val log = LogCategory("ToastUtils")
private var refToast: WeakReference<Toast>? = null
private val log = LogCategory("ToastUtils") internal fun showToastImpl(context: Context, bLong: Boolean, message: String): Boolean {
private var refToast: WeakReference<Toast>? = null runOnMainLooper {
internal fun showToastImpl(context: Context, bLong: Boolean, message: String): Boolean { // 前回のトーストの表示を終了する
runOnMainLooper { try {
refToast?.get()?.cancel()
// 前回のトーストの表示を終了する } catch (ex: Throwable) {
try { log.trace(ex)
refToast?.get()?.cancel() } finally {
} catch (ex: Throwable) { refToast = null
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)
} }
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 = 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 = 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 = 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 = 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)
}
}

View File

@ -1,73 +1,90 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="320dp"
android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical">
>
<LinearLayout <com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_margin="6dp"
android:paddingTop="6dp" android:layout_marginBottom="0dp"
android:paddingStart="6dp" android:orientation="horizontal">
android:paddingEnd="6dp"
> <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 <ImageButton
android:id="@+id/btnSkinTone1" android:id="@+id/btnSkinTone1"
android:layout_width="48dp" style="@style/emoji_picker_skin_tone_button"
android:layout_height="48dp"
android:background="#f7dece" android:background="#f7dece"
android:contentDescription="@string/skin_tone_light" android:contentDescription="@string/skin_tone_light" />
android:src="@drawable/check_mark"
/>
<ImageButton <ImageButton
android:id="@+id/btnSkinTone2" android:id="@+id/btnSkinTone2"
android:layout_width="48dp" style="@style/emoji_picker_skin_tone_button"
android:layout_height="48dp"
android:background="#f3d2a2" android:background="#f3d2a2"
android:contentDescription="@string/skin_tone_medium_light" android:contentDescription="@string/skin_tone_medium_light" />
/>
<ImageButton <ImageButton
android:id="@+id/btnSkinTone3" android:id="@+id/btnSkinTone3"
android:layout_width="48dp" style="@style/emoji_picker_skin_tone_button"
android:layout_height="48dp"
android:background="#d5ab88" android:background="#d5ab88"
android:contentDescription="@string/skin_tone_medium" android:contentDescription="@string/skin_tone_medium" />
/>
<ImageButton <ImageButton
android:id="@+id/btnSkinTone4" android:id="@+id/btnSkinTone4"
android:layout_width="48dp" style="@style/emoji_picker_skin_tone_button"
android:layout_height="48dp"
android:background="#af7e57" android:background="#af7e57"
android:contentDescription="@string/skin_tone_medium_dark" android:contentDescription="@string/skin_tone_medium_dark" />
/>
<ImageButton <ImageButton
android:id="@+id/btnSkinTone5" android:id="@+id/btnSkinTone5"
android:layout_width="48dp" style="@style/emoji_picker_skin_tone_button"
android:layout_height="48dp"
android:background="#7c533e" android:background="#7c533e"
android:contentDescription="@string/skin_tone_dark" android:contentDescription="@string/skin_tone_dark" />
/> </com.google.android.flexbox.FlexboxLayout>
</LinearLayout>
<com.astuetz.PagerSlidingTabStrip <EditText
android:id="@+id/pager_strip" android:id="@+id/etFilter"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dip" android:layout_height="wrap_content"
android:layout_gravity="top" 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 <HorizontalScrollView
android:id="@+id/pager" 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_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
/> android:clipToPadding="false"
android:fadeScrollbars="false"
android:padding="6dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" />
</LinearLayout> </LinearLayout>

View File

@ -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"
/>

View File

@ -1139,4 +1139,11 @@
<string name="edit_history">編集履歴</string> <string name="edit_history">編集履歴</string>
<string name="post_language_code">投稿の言語コード(空欄にすると端末の言語設定を使います)</string> <string name="post_language_code">投稿の言語コード(空欄にすると端末の言語設定を使います)</string>
<string name="device_language">(端末の言語)</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> </resources>

View File

@ -1148,4 +1148,11 @@
<string name="edit_history">Edit history</string> <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="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="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> </resources>

View File

@ -258,4 +258,10 @@
</style> </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> </resources>