絵文字ピッカーの改善

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

View File

@ -2,7 +2,9 @@ package jp.juggler.subwaytooter.action
import android.content.Context
import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.Styler
import jp.juggler.subwaytooter.actmain.addColumn
import jp.juggler.subwaytooter.actmain.reloadAccountSetting
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount
@ -13,16 +15,12 @@ import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.findStatus
import jp.juggler.subwaytooter.dialog.DlgConfirm
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.emptyCallback
import jp.juggler.util.JsonObject
import jp.juggler.util.launchMain
import jp.juggler.util.showToast
import jp.juggler.util.toPostRequestBuilder
import java.util.*
import jp.juggler.util.*
import kotlin.math.max
private class BoostImpl(
@ -62,42 +60,6 @@ private class BoostImpl(
return true
}
private fun confirm(): Boolean {
if (bConfirmed) return true
DlgConfirm.open(
activity,
activity.getString(
when {
!bSet -> R.string.confirm_unboost_from
isPrivateToot -> R.string.confirm_boost_private_from
visibility == TootVisibility.PrivateFollowers -> R.string.confirm_private_boost_from
else -> R.string.confirm_boost_from
},
AcctColor.getNickname(accessInfo)
),
object : DlgConfirm.Callback {
override fun onOK() {
bConfirmed = true
run()
}
override var isConfirmEnabled: Boolean
get() = when (bSet) {
true -> accessInfo.confirm_boost
else -> accessInfo.confirm_unboost
}
set(value) {
when (bSet) {
true -> accessInfo.confirm_boost = value
else -> accessInfo.confirm_unboost = value
}
accessInfo.saveSetting()
activity.reloadAccountSetting(accessInfo)
}
})
return false
}
private suspend fun Context.syncStatus(client: TootApiClient) =
if (!crossAccountMode.isRemote) {
// 既に自タンスのステータスがある
@ -234,16 +196,41 @@ private class BoostImpl(
}
fun run() {
if (!preCheck()) return
if (!confirm()) return
activity.launchAndShowError {
if (!preCheck()) return@launchAndShowError
// ブースト表示を更新中にする
activity.appState.setBusyBoost(accessInfo, statusArg)
activity.showColumnMatchAccount(accessInfo)
if (!bConfirmed) {
activity.confirm(
activity.getString(
when {
!bSet -> R.string.confirm_unboost_from
isPrivateToot -> R.string.confirm_boost_private_from
visibility == TootVisibility.PrivateFollowers -> R.string.confirm_private_boost_from
else -> R.string.confirm_boost_from
},
AcctColor.getNickname(accessInfo)
),
when (bSet) {
true -> accessInfo.confirm_boost
else -> accessInfo.confirm_unboost
}
) { newConfirmEnabled ->
when (bSet) {
true -> accessInfo.confirm_boost = newConfirmEnabled
else -> accessInfo.confirm_unboost = newConfirmEnabled
}
accessInfo.saveSetting()
activity.reloadAccountSetting(accessInfo)
}
}
// ブースト表示を更新中にする
activity.appState.setBusyBoost(accessInfo, statusArg)
activity.showColumnMatchAccount(accessInfo)
launchMain {
val result =
activity.runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
activity.runApiTask(accessInfo,
progressStyle = ApiTask.PROGRESS_NONE) { client ->
try {
val targetStatus = syncStatus(client)
boostApi(client, targetStatus)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,60 +1,125 @@
package jp.juggler.subwaytooter.dialog
import android.annotation.SuppressLint
import android.app.Activity
import android.view.View
import android.widget.CheckBox
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.databinding.DlgConfirmBinding
import jp.juggler.util.dismissSafe
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
object DlgConfirm {
interface Callback {
var isConfirmEnabled: Boolean
// interface Callback {
// var isConfirmEnabled: Boolean
//
// fun onOK()
// }
fun onOK()
}
// @SuppressLint("InflateParams")
// fun open(activity: Activity, message: String, callback: Callback): Dialog {
//
// if (!callback.isConfirmEnabled) {
// callback.onOK()
// return
// }
//
// val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false)
// val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
// val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext)
// tvMessage.text = message
//
// AlertDialog.Builder(activity)
// .setView(view)
// .setCancelable(true)
// .setNegativeButton(R.string.cancel, null)
// .setPositiveButton(R.string.ok) { _, _ ->
// if (cbSkipNext.isChecked) {
// callback.isConfirmEnabled = false
// }
// callback.onOK()
// }
// .show()
// }
// @SuppressLint("InflateParams")
// fun openSimple(activity: Activity, message: String, callback: () -> Unit) {
// val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false)
// val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
// val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext)
// tvMessage.text = message
// cbSkipNext.visibility = View.GONE
//
// AlertDialog.Builder(activity)
// .setView(view)
// .setCancelable(true)
// .setNegativeButton(R.string.cancel, null)
// .setPositiveButton(R.string.ok) { _, _ -> callback() }
// .show()
// }
@SuppressLint("InflateParams")
fun open(activity: Activity, message: String, callback: Callback) {
if (!callback.isConfirmEnabled) {
callback.onOK()
return
}
val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false)
val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext)
tvMessage.text = message
AlertDialog.Builder(activity)
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
if (cbSkipNext.isChecked) {
callback.isConfirmEnabled = false
suspend fun AppCompatActivity.confirm(
message: String,
getConfirmEnabled: Boolean,
setConfirmEnabled: (newConfirmEnabled: Boolean) -> Unit,
) {
if (!getConfirmEnabled) return
suspendCancellableCoroutine<Unit> { cont ->
try {
val views = DlgConfirmBinding.inflate(layoutInflater)
views.tvMessage.text = message
val dialog = AlertDialog.Builder(this)
.setView(views.root)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
if (views.cbSkipNext.isChecked) {
setConfirmEnabled(false)
}
if (cont.isActive) cont.resume(Unit)
}
dialog.setOnDismissListener {
if (cont.isActive) cont.resumeWithException(CancellationException("dialog cancelled."))
}
callback.onOK()
dialog.show()
} catch (ex: Throwable) {
cont.resumeWithException(ex)
}
.show()
}
}
@SuppressLint("InflateParams")
fun openSimple(activity: Activity, message: String, callback: () -> Unit) {
val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false)
val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext)
tvMessage.text = message
cbSkipNext.visibility = View.GONE
suspend fun AppCompatActivity.confirm(@StringRes messageId: Int, vararg args: Any?) =
confirm(getString(messageId, args))
AlertDialog.Builder(activity)
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ -> callback() }
.show()
suspend fun AppCompatActivity.confirm(message: String) {
suspendCancellableCoroutine<Unit> { cont ->
try {
val views = DlgConfirmBinding.inflate(layoutInflater)
views.tvMessage.text = message
views.cbSkipNext.visibility = View.GONE
val dialog = AlertDialog.Builder(this)
.setView(views.root)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
if (cont.isActive) cont.resume(Unit)
}
.create()
dialog.setOnDismissListener {
if (cont.isActive) cont.resumeWithException(CancellationException("dialog closed."))
}
dialog.show()
cont.invokeOnCancellation { dialog.dismissSafe() }
} catch (ex: Throwable) {
cont.resumeWithException(ex)
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,20 +1,22 @@
package jp.juggler.subwaytooter.emoji
import java.util.ArrayList
import androidx.annotation.StringRes
import jp.juggler.subwaytooter.R
enum class EmojiCategory(@StringRes val titleId: Int) {
Recent(R.string.emoji_category_recent),
Custom(R.string.emoji_category_custom),
People(R.string.emoji_category_people),
ComplexTones(R.string.emoji_category_composite_tones),
Nature(R.string.emoji_category_nature),
Foods(R.string.emoji_category_foods),
Activities(R.string.emoji_category_activity),
Places(R.string.emoji_category_places),
Objects(R.string.emoji_category_objects),
Symbols(R.string.emoji_category_symbols),
Flags(R.string.emoji_category_flags),
Others(R.string.emoji_category_others),
enum class EmojiCategory {
Recent,
Custom,
People,
ComplexTones,
Nature,
Foods,
Activities,
Places,
Objects,
Symbols,
Flags,
Others,
;
val emojiList = ArrayList<UnicodeEmoji>()

View File

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

View File

@ -14,13 +14,17 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.action.reactionAdd
import jp.juggler.subwaytooter.action.reactionFromAnotherAccount
import jp.juggler.subwaytooter.action.reactionRemove
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.TootReaction
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.util.*
import jp.juggler.util.*
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
import jp.juggler.subwaytooter.util.minWidthCompat
import jp.juggler.subwaytooter.util.startMargin
import jp.juggler.util.attrColor
import jp.juggler.util.getAdaptiveRippleDrawableRound
import jp.juggler.util.notZero
import org.jetbrains.anko.allCaps
import org.jetbrains.anko.dip

View File

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

View File

@ -1,13 +1,12 @@
package jp.juggler.subwaytooter.util
import android.os.SystemClock
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.Styler
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.dialog.DlgConfirm
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.MyClickableSpan
@ -15,16 +14,21 @@ import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.TagSet
import jp.juggler.util.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.lang.ref.WeakReference
import java.util.*
interface PostCompleteCallback {
fun onPostComplete(targetAccount: SavedAccount, status: TootStatus)
fun onScheduledPostComplete(targetAccount: SavedAccount)
sealed class PostResult {
class Normal(
val targetAccount: SavedAccount,
val status: TootStatus,
) : PostResult()
class Scheduled(
val targetAccount: SavedAccount,
) : PostResult()
}
@Suppress("LongParameterList")
@ -51,8 +55,6 @@ class PostImpl(
val editStatusId: EntityId?,
val emojiMapCustom: HashMap<String, CustomEmoji>?,
var useQuoteToot: Boolean,
val callback: PostCompleteCallback,
) {
companion object {
private val log = LogCategory("PostImpl")
@ -70,11 +72,6 @@ class PostImpl(
private var visibilityChecked: TootVisibility? = null
private var bConfirmTag: Boolean = false
var bConfirmAccount: Boolean = false
private var bConfirmRedraft: Boolean = false
private var bConfirmTagCharacter: Boolean = false
private val choiceMaxChars = when {
account.isMisskey -> 15
pollType == TootPollsType.FriendsNico -> 15
@ -130,106 +127,6 @@ class PostImpl(
return true
}
private fun confirm(): Boolean {
if (!bConfirmAccount) {
DlgConfirm.open(
activity,
activity.getString(R.string.confirm_post_from, AcctColor.getNickname(account)),
object : DlgConfirm.Callback {
override var isConfirmEnabled: Boolean
get() = account.confirm_post
set(bv) {
account.confirm_post = bv
account.saveSetting()
}
override fun onOK() {
bConfirmAccount = true
run()
}
})
return false
}
if (!bConfirmTagCharacter && PrefB.bpWarnHashtagAsciiAndNonAscii()) {
val tags = TootTag.findHashtags(content, account.isMisskey)
val badTags = tags
?.filter {
val hasAscii = reAscii.matcher(it).find()
val hasNotAscii = reNotAscii.matcher(it).find()
hasAscii && hasNotAscii
}
?.map { "#$it" }
if (badTags?.isNotEmpty() == true) {
AlertDialog.Builder(activity)
.setCancelable(true)
.setMessage(
activity.getString(
R.string.hashtag_contains_ascii_and_not_ascii,
badTags.joinToString(", ")
)
)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
bConfirmTagCharacter = true
run()
}
.show()
return false
}
}
if (!bConfirmTag) {
val isMisskey = account.isMisskey
if (!visibilityArg.isTagAllowed(isMisskey)) {
val tags = TootTag.findHashtags(content, isMisskey)
if (tags != null) {
log.d("findHashtags ${tags.joinToString(",")}")
AlertDialog.Builder(activity)
.setCancelable(true)
.setMessage(R.string.hashtag_and_visibility_not_match)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
bConfirmTag = true
run()
}
.show()
return false
}
}
}
if (!bConfirmRedraft && redraftStatusId != null) {
AlertDialog.Builder(activity)
.setCancelable(true)
.setMessage(R.string.delete_base_status_before_toot)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
bConfirmRedraft = true
run()
}
.show()
return false
}
if (!bConfirmRedraft && scheduledId != null) {
AlertDialog.Builder(activity)
.setCancelable(true)
.setMessage(R.string.delete_scheduled_status_before_update)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
bConfirmRedraft = true
run()
}
.show()
return false
}
return true
}
private var resultStatus: TootStatus? = null
private var resultCredentialTmp: TootAccount? = null
private var resultScheduledStatusSucceeded = false
@ -503,14 +400,57 @@ class PostImpl(
}
}
fun run() {
if (!preCheck()) return
if (!confirm()) return
suspend fun runSuspend(): PostResult {
if (!preCheck()) throw CancellationException("preCheck failed.")
if (PrefB.bpWarnHashtagAsciiAndNonAscii()) {
TootTag.findHashtags(content, account.isMisskey)
?.filter {
val hasAscii = reAscii.matcher(it).find()
val hasNotAscii = reNotAscii.matcher(it).find()
hasAscii && hasNotAscii
}?.map { "#$it" }
?.notEmpty()
?.let { badTags ->
activity.confirm(
R.string.hashtag_contains_ascii_and_not_ascii,
badTags.joinToString(", ")
)
}
}
val isMisskey = account.isMisskey
if (!visibilityArg.isTagAllowed(isMisskey)) {
TootTag.findHashtags(content, isMisskey)
?.notEmpty()
?.let { tags ->
log.d("findHashtags ${tags.joinToString(",")}")
activity.confirm(
R.string.hashtag_and_visibility_not_match
)
}
}
if (redraftStatusId != null) {
activity.confirm(R.string.delete_base_status_before_toot)
}
if (scheduledId != null) {
activity.confirm(R.string.delete_scheduled_status_before_update)
}
activity.confirm(
activity.getString(R.string.confirm_post_from, AcctColor.getNickname(account)),
account.confirm_post
) { newConfirmEnabled ->
account.confirm_post = newConfirmEnabled
account.saveSetting()
}
// 投稿中に再度投稿ボタンが押された
if (postJob?.get()?.isActive == true) {
activity.showToast(false, R.string.post_button_tapped_repeatly)
return
throw CancellationException("preCheck failed.")
}
// ボタン連打判定
@ -519,10 +459,12 @@ class PostImpl(
lastPostTapped = now
if (delta < 1000L) {
activity.showToast(false, R.string.post_button_tapped_repeatly)
return
throw CancellationException("post_button_tapped_repeatly")
}
postJob = launchMain {
val job = Job().also { postJob = it.wrapWeakReference }
return withContext(Dispatchers.Main + job) {
activity.runApiTask(
account,
progressSetup = { it.setCanceledOnTouchOutside(false) },
@ -622,19 +564,21 @@ class PostImpl(
saveStatusTag(status)
}
}
}?.let { result ->
}.let { result ->
if (result == null) throw CancellationException()
val status = resultStatus
val scheduledStatusSucceeded = resultScheduledStatusSucceeded
when {
scheduledStatusSucceeded ->
callback.onScheduledPostComplete(account)
resultScheduledStatusSucceeded ->
PostResult.Scheduled(account)
// 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る
status != null -> callback.onPostComplete(account, status)
status != null ->
PostResult.Normal(account, status)
else -> activity.showToast(true, result.error)
else -> error( result.error ?: "(result.error is null)")
}
}
}.wrapWeakReference
}
}
}

View File

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

View File

@ -2,57 +2,87 @@ package jp.juggler.util
import android.content.Context
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.R
import kotlinx.coroutines.CancellationException
import me.drakeet.support.toast.ToastCompat
import java.lang.ref.WeakReference
object ToastUtils {
private val log = LogCategory("ToastUtils")
private var refToast: WeakReference<Toast>? = null
private val log = LogCategory("ToastUtils")
private var refToast: WeakReference<Toast>? = null
internal fun showToastImpl(context: Context, bLong: Boolean, message: String): Boolean {
runOnMainLooper {
internal fun showToastImpl(context: Context, bLong: Boolean, message: String): Boolean {
runOnMainLooper {
// 前回のトーストの表示を終了する
try {
refToast?.get()?.cancel()
} catch (ex: Throwable) {
log.trace(ex)
} finally {
refToast = null
}
// 新しいトーストを作る
try {
val duration = if (bLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
val t = ToastCompat.makeText(context, message, duration)
t.setBadTokenListener { }
t.show()
refToast = WeakReference(t)
} catch (ex: Throwable) {
log.trace(ex)
}
// コールスタックの外側でエラーになる…
// android.view.WindowManager$BadTokenException:
// at android.view.ViewRootImpl.setView (ViewRootImpl.java:679)
// at android.view.WindowManagerGlobal.addView (WindowManagerGlobal.java:342)
// at android.view.WindowManagerImpl.addView (WindowManagerImpl.java:94)
// at android.widget.Toast$TN.handleShow (Toast.java:435)
// at android.widget.Toast$TN$2.handleMessage (Toast.java:345)
// 前回のトーストの表示を終了する
try {
refToast?.get()?.cancel()
} catch (ex: Throwable) {
log.trace(ex)
} finally {
refToast = null
}
return false
// 新しいトーストを作る
try {
val duration = if (bLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
val t = ToastCompat.makeText(context, message, duration)
t.setBadTokenListener { }
t.show()
refToast = WeakReference(t)
} catch (ex: Throwable) {
log.trace(ex)
}
// コールスタックの外側でエラーになる…
// android.view.WindowManager$BadTokenException:
// at android.view.ViewRootImpl.setView (ViewRootImpl.java:679)
// at android.view.WindowManagerGlobal.addView (WindowManagerGlobal.java:342)
// at android.view.WindowManagerImpl.addView (WindowManagerImpl.java:94)
// at android.widget.Toast$TN.handleShow (Toast.java:435)
// at android.widget.Toast$TN$2.handleMessage (Toast.java:345)
}
return false
}
fun Context.showToast(bLong: Boolean, caption: String?): Boolean =
ToastUtils.showToastImpl(this, bLong, caption ?: "(null)")
showToastImpl(this, bLong, caption ?: "(null)")
fun Context.showToast(ex: Throwable, caption: String = "error."): Boolean =
ToastUtils.showToastImpl(this, true, ex.withCaption(caption))
showToastImpl(this, true, ex.withCaption(caption))
fun Context.showToast(bLong: Boolean, stringId: Int, vararg args: Any): Boolean =
ToastUtils.showToastImpl(this, bLong, getString(stringId, *args))
showToastImpl(this, bLong, getString(stringId, *args))
fun Context.showToast(ex: Throwable, stringId: Int, vararg args: Any): Boolean =
ToastUtils.showToastImpl(this, true, ex.withCaption(resources, stringId, *args))
showToastImpl(this, true, ex.withCaption(resources, stringId, *args))
fun AppCompatActivity.showError(ex: Throwable, caption: String = "error.") {
log.e(ex)
// キャンセル例外はUIに表示しない
if (ex is CancellationException) return
try {
AlertDialog.Builder(this)
.setTitle(R.string.error)
.setMessage(
listOf(
caption,
when (ex) {
is IllegalStateException ->
null
else ->
ex.javaClass.simpleName
},
ex.message,
)
.filter { !it.isNullOrBlank() }
.joinToString("\n")
)
.setPositiveButton(R.string.ok, null)
} catch (ex: Throwable) {
log.e(ex)
showToast(ex, caption)
}
}

View File

@ -1,73 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="320dp"
android:layout_height="match_parent"
android:orientation="vertical"
>
android:orientation="vertical">
<LinearLayout
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="6dp"
android:paddingStart="6dp"
android:paddingEnd="6dp"
>
android:layout_margin="6dp"
android:layout_marginBottom="0dp"
android:orientation="horizontal">
<ImageButton
android:id="@+id/btnSkinTone0"
style="@style/emoji_picker_skin_tone_button"
android:background="#fde12c"
android:contentDescription="@string/skin_tone_unspecified"
android:src="@drawable/check_mark" />
<ImageButton
android:id="@+id/btnSkinTone1"
android:layout_width="48dp"
android:layout_height="48dp"
style="@style/emoji_picker_skin_tone_button"
android:background="#f7dece"
android:contentDescription="@string/skin_tone_light"
android:src="@drawable/check_mark"
/>
android:contentDescription="@string/skin_tone_light" />
<ImageButton
android:id="@+id/btnSkinTone2"
android:layout_width="48dp"
android:layout_height="48dp"
style="@style/emoji_picker_skin_tone_button"
android:background="#f3d2a2"
android:contentDescription="@string/skin_tone_medium_light"
/>
android:contentDescription="@string/skin_tone_medium_light" />
<ImageButton
android:id="@+id/btnSkinTone3"
android:layout_width="48dp"
android:layout_height="48dp"
style="@style/emoji_picker_skin_tone_button"
android:background="#d5ab88"
android:contentDescription="@string/skin_tone_medium"
/>
android:contentDescription="@string/skin_tone_medium" />
<ImageButton
android:id="@+id/btnSkinTone4"
android:layout_width="48dp"
android:layout_height="48dp"
style="@style/emoji_picker_skin_tone_button"
android:background="#af7e57"
android:contentDescription="@string/skin_tone_medium_dark"
/>
android:contentDescription="@string/skin_tone_medium_dark" />
<ImageButton
android:id="@+id/btnSkinTone5"
android:layout_width="48dp"
android:layout_height="48dp"
style="@style/emoji_picker_skin_tone_button"
android:background="#7c533e"
android:contentDescription="@string/skin_tone_dark"
/>
</LinearLayout>
android:contentDescription="@string/skin_tone_dark" />
</com.google.android.flexbox.FlexboxLayout>
<com.astuetz.PagerSlidingTabStrip
android:id="@+id/pager_strip"
<EditText
android:id="@+id/etFilter"
android:layout_width="match_parent"
android:layout_height="48dip"
android:layout_gravity="top"
/>
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:layout_marginBottom="0dp"
android:hint="@string/search_emojis"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:inputType="text" />
<jp.juggler.subwaytooter.view.MyViewPager
android:id="@+id/pager"
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:fadeScrollbars="false"
android:padding="6dp"
android:paddingBottom="0dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="horizontal">
<LinearLayout
android:id="@+id/llCategories"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvGrid"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
android:clipToPadding="false"
android:fadeScrollbars="false"
android:padding="6dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" />
</LinearLayout>

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="post_language_code">投稿の言語コード(空欄にすると端末の言語設定を使います)</string>
<string name="device_language">(端末の言語)</string>
<string name="skin_tones">肌色</string>
<string name="skin_tone_unspecified">指定なし</string>
<string name="search_emojis">絵文字を検索…</string>
<string name="categories">カテゴリ</string>
<string name="error">エラー</string>
<string name="emoji_picker_custom_of">カスタム: %1$s</string>
<string name="others" >その他</string>
</resources>

View File

@ -1148,4 +1148,11 @@
<string name="edit_history">Edit history</string>
<string name="post_language_code">Language code of Post (leave empty to use device\'s language setting)</string>
<string name="device_language">(device\'s language)</string>
<string name="skin_tones">Skin tones</string>
<string name="skin_tone_unspecified">Unspecified</string>
<string name="search_emojis">Search emojis…</string>
<string name="categories">Categories</string>
<string name="error">Error</string>
<string name="emoji_picker_custom_of">Custom: %1$s</string>
<string name="others" >Others</string>
</resources>

View File

@ -258,4 +258,10 @@
</style>
<style name="emoji_picker_skin_tone_button">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">48dp</item>
<item name="android:layout_marginStart">2dp</item>
</style>
</resources>