diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt index 857cadef..e8dfe23e 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt @@ -2682,7 +2682,7 @@ class ActPost : AsyncActivity(), TootVisibility.DirectPrivate ) - true == ti?.hasCapability(InstanceCapability.VisibilityMutual) -> + InstanceCapability.visibilityMutual(ti) -> arrayOf( TootVisibility.WebSetting, TootVisibility.Public, @@ -2693,7 +2693,7 @@ class ActPost : AsyncActivity(), TootVisibility.DirectSpecified ) - true == ti?.hasCapability(InstanceCapability.VisibilityLimited) -> + InstanceCapability.visibilityLimited(ti)-> arrayOf( TootVisibility.WebSetting, TootVisibility.Public, diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt index 729fcf69..e8daa159 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt @@ -692,28 +692,52 @@ enum class ColumnType( REACTIONS( 42, iconId = { R.drawable.ic_face }, - name1 = { it.getString(R.string.reactions) }, + name1 = { it.getString(R.string.reactioned_posts) }, bAllowPseudo = false, bAllowMisskey = false, loading = { client -> if (isMisskey) { - TootApiResult("misskey has no api to list your reactions") + getStatusList( + client, + ApiPath.PATH_M544_REACTIONS, + misskeyParams = column.makeMisskeyTimelineParameter(parser), + listParser = misskeyCustomParserFavorites + ) } else { getStatusList(client,column.makeReactionsUrl()) } }, refresh = { client -> - getStatusList(client,column.makeReactionsUrl()) + if (isMisskey) { + getStatusList( + client, + ApiPath.PATH_M544_REACTIONS, + misskeyParams = column.makeMisskeyTimelineParameter(parser), + listParser = misskeyCustomParserFavorites + ) + } else { + getStatusList(client, column.makeReactionsUrl()) + } }, gap = { client -> - getStatusList( - client, - column.makeReactionsUrl(), - mastodonFilterByIdRange = false - ) + if (isMisskey) { + getStatusList( + client, + ApiPath.PATH_M544_REACTIONS, + mastodonFilterByIdRange = false, + misskeyParams = column.makeMisskeyTimelineParameter(parser), + listParser = misskeyCustomParserFavorites + ) + } else { + getStatusList( + client, + column.makeReactionsUrl(), + mastodonFilterByIdRange = false + ) + } }, gapDirection = gapDirectionMastodonWorkaround, ), diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLifecycle.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLifecycle.kt index 0d3b559e..1321d683 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLifecycle.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLifecycle.kt @@ -215,7 +215,8 @@ fun ColumnViewHolder.onPageCreate(column: Column, page_idx: Int, page_count: Int btnSearchClear.vg(Pref.bpShowSearchClear(activity.pref)) cbResolve.vg(column.type == ColumnType.SEARCH) } - column.type == ColumnType.REACTIONS -> { + + column.type == ColumnType.REACTIONS && column.access_info.isMastodon -> { llSearch.vg(true) flEmoji.vg(true) diff --git a/app/src/main/java/jp/juggler/subwaytooter/SideMenuAdapter.kt b/app/src/main/java/jp/juggler/subwaytooter/SideMenuAdapter.kt index acc3cf8e..36256ead 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/SideMenuAdapter.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/SideMenuAdapter.kt @@ -1,6 +1,5 @@ package jp.juggler.subwaytooter -import android.app.Activity import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -23,9 +22,6 @@ import androidx.drawerlayout.widget.DrawerLayout import jp.juggler.subwaytooter.action.Action_Account import jp.juggler.subwaytooter.action.Action_App import jp.juggler.subwaytooter.action.Action_Instance -import jp.juggler.subwaytooter.api.TootApiCallback -import jp.juggler.subwaytooter.api.TootApiClient -import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.dialog.AccountPicker import jp.juggler.subwaytooter.table.SavedAccount @@ -269,8 +265,13 @@ class SideMenuAdapter( Item(icon = R.drawable.ic_bookmark, title = R.string.bookmarks) { Action_Account.timeline(this, defaultInsertPosition, ColumnType.BOOKMARKS) }, - Item(icon = R.drawable.ic_face, title = R.string.fedibird_reactions) { - Action_Account.getReactionableAccounts(this, allowMisskey = false) { list -> + Item(icon = R.drawable.ic_face, title = R.string.reactioned_posts) { + Action_Account.listAccountsCanSeeMyReactions(this) { list -> + if (list.isEmpty()) { + showToast(false, R.string.not_available_for_current_accounts) + return@listAccountsCanSeeMyReactions + } + val columnType = ColumnType.REACTIONS AccountPicker.pick( this, diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt index 8072f1c3..f016ea7a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt @@ -13,9 +13,7 @@ import jp.juggler.subwaytooter.table.UserRelation import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.openBrowser import jp.juggler.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* object Action_Account { @@ -385,28 +383,32 @@ object Action_Account { }.show() } - fun getReactionableAccounts( + fun listAccountsReactionable( activity: ActMain, - allowMisskey: Boolean = true, block: (ArrayList) -> Unit ) { TootTaskRunner(activity).run(object : TootTask { var list: List? = null + + suspend fun check(client:TootApiClient,a:SavedAccount)=when { + client.isApiCancelled -> false + a.isPseudo -> false + a.isMisskey -> true + else -> { + val (ti, ri) = TootInstance.getEx(client.copy(), account = a) + if (ti == null) { + ri?.error?.let { log.w(it) } + false + } else InstanceCapability.emojiReaction(a,ti) + } + } + override suspend fun background(client: TootApiClient): TootApiResult? { - list = SavedAccount.loadAccountList(activity).filter { a -> - when { - client.isApiCancelled -> false - a.isPseudo -> false - a.isMisskey -> allowMisskey - else -> { - val (ti, ri) = TootInstance.getEx(client, account = a) - if (ti == null) { - ri?.error?.let { log.w(it) } - false - } else - ti.fedibird_capabilities?.contains("emoji_reaction") == true - } - } + coroutineScope { + list = SavedAccount.loadAccountList(activity) + .map { async { if(check(client,it)) it else null } } + .awaitAll() + .filterNotNull() } return if (client.isApiCancelled) null else TootApiResult() } @@ -417,7 +419,41 @@ object Action_Account { } }) } + fun listAccountsCanSeeMyReactions( + activity: ActMain, + block: (ArrayList) -> Unit + ) { + TootTaskRunner(activity).run(object : TootTask { + var list: List? = null + suspend fun check(client:TootApiClient,a:SavedAccount)=when { + client.isApiCancelled -> false + a.isPseudo -> false + else -> { + val (ti, ri) = TootInstance.getEx(client.copy(), account = a) + if (ti == null) { + ri?.error?.let { log.w(it) } + false + } else InstanceCapability.listMyReactions(a,ti) + } + } + + override suspend fun background(client: TootApiClient): TootApiResult? { + coroutineScope { + list = SavedAccount.loadAccountList(activity) + .map { async { if(check(client,it)) it else null } } + .awaitAll() + .filterNotNull() + } + return if (client.isApiCancelled) null else TootApiResult() + } + + override suspend fun handleResult(result: TootApiResult?) { + result ?: return + if (list != null) block(ArrayList(list)) + } + }) + } // アカウントを選んでタイムラインカラムを追加 fun timelineWithFilter( activity: ActMain, diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.kt index 9d61520d..27fb8ecf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.kt @@ -1650,7 +1650,13 @@ object Action_Toot { ) } - Action_Account.getReactionableAccounts(activity) { list -> + Action_Account.listAccountsReactionable(activity) { list -> + + if (list.isEmpty()) { + activity.showToast(false, R.string.not_available_for_current_accounts) + return@listAccountsReactionable + } + AccountPicker.pick( activity, accountListArg = list, diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/ApiPath.kt b/app/src/main/java/jp/juggler/subwaytooter/api/ApiPath.kt index 19aee629..efd7256e 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/ApiPath.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/ApiPath.kt @@ -53,4 +53,6 @@ object ApiPath { const val PATH_MISSKEY_FOLLOW_REQUESTS = "/api/following/requests/list" const val PATH_MISSKEY_FOLLOW_SUGGESTION = "/api/users/recommendation" const val PATH_MISSKEY_FAVORITES = "/api/i/favorites" -} \ No newline at end of file + + const val PATH_M544_REACTIONS ="/api/i/reactions" +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt index 6494a94a..8468a395 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt @@ -1269,6 +1269,14 @@ class TootApiClient( } + fun copy() =TootApiClient( + context, + httpClient, + callback + ).also{dst-> + dst.account = account + dst.apiHost = apiHost + } } // query: query_string after ? ( ? itself is excluded ) diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootTaskRunner.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootTaskRunner.kt index a36dd177..2742016a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootTaskRunner.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootTaskRunner.kt @@ -104,7 +104,6 @@ class TootTaskRunner( fun run(instance: Host, callback: TootTask): TootTaskRunner { client.apiHost = instance return run(callback) - } fun progressPrefix(s: String): TootTaskRunner { diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt index daa7b5fc..aa759468 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt @@ -11,7 +11,6 @@ import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.VersionString import jp.juggler.util.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import okhttp3.Request @@ -28,39 +27,45 @@ enum class InstanceType { Pleroma } -enum class CapabilitySource { - Fedibird, -} +object InstanceCapability { +// FavouriteHashtag(CapabilitySource.Fedibird, "favourite_hashtag"), +// FavouriteDomain(CapabilitySource.Fedibird, "favourite_domain"), +// StatusExpire(CapabilitySource.Fedibird, "status_expire"), +// FollowNoDelivery(CapabilitySource.Fedibird, "follow_no_delivery"), +// FollowHashtag(CapabilitySource.Fedibird, "follow_hashtag"), +// SubscribeAccount(CapabilitySource.Fedibird, "subscribe_account"), +// SubscribeDomain(CapabilitySource.Fedibird, "subscribe_domain"), +// SubscribeKeyword(CapabilitySource.Fedibird, "subscribe_keyword"), +// TimelineNoLocal(CapabilitySource.Fedibird, "timeline_no_local"), +// TimelineDomain(CapabilitySource.Fedibird, "timeline_domain"), +// TimelineGroup(CapabilitySource.Fedibird, "timeline_group"), +// TimelineGroupDirectory(CapabilitySource.Fedibird, "timeline_group_directory"), -enum class InstanceCapability( - private val capabilitySource: CapabilitySource, - private val id: String -) { - FavouriteHashtag(CapabilitySource.Fedibird, "favourite_hashtag"), - FavouriteDomain(CapabilitySource.Fedibird, "favourite_domain"), - StatusExpire(CapabilitySource.Fedibird, "status_expire"), - FollowNoDelivery(CapabilitySource.Fedibird, "follow_no_delivery"), - FollowHashtag(CapabilitySource.Fedibird, "follow_hashtag"), - SubscribeAccount(CapabilitySource.Fedibird, "subscribe_account"), - SubscribeDomain(CapabilitySource.Fedibird, "subscribe_domain"), - SubscribeKeyword(CapabilitySource.Fedibird, "subscribe_keyword"), - TimelineNoLocal(CapabilitySource.Fedibird, "timeline_no_local"), - TimelineDomain(CapabilitySource.Fedibird, "timeline_domain"), - TimelineGroup(CapabilitySource.Fedibird, "timeline_group"), - TimelineGroupDirectory(CapabilitySource.Fedibird, "timeline_group_directory"), - VisibilityMutual(CapabilitySource.Fedibird, "visibility_mutual"), - VisibilityLimited(CapabilitySource.Fedibird, "visibility_limited"), - ; + fun visibilityMutual(ti: TootInstance?) = + ti?.fedibird_capabilities?.contains("visibility_mutual") == true - fun hasCapability(instance: TootInstance): Boolean { - when (capabilitySource) { - CapabilitySource.Fedibird -> { - if (instance.fedibird_capabilities?.any { it == id } == true) return true - } + + fun visibilityLimited(ti: TootInstance?) = + ti?.fedibird_capabilities?.contains("visibility_limited") == true + + + fun emojiReaction(ai: SavedAccount, ti: TootInstance?) = + when { + ai.isPseudo -> false + ai.isMisskey -> true + else -> ti?.fedibird_capabilities?.contains("emoji_reaction") == true + } + + fun listMyReactions(ai: SavedAccount, ti: TootInstance?) = + when { + ai.isPseudo -> false + ai.isMisskey -> + // m544 extension + ti?.misskeyEndpoints?.contains("i/reactions") == true + else -> + // fedibird extension + ti?.fedibird_capabilities?.contains("emoji_reaction") == true } - // XXX: もし機能がMastodon公式に取り込まれたならバージョン番号で判断できるはず - return false - } } class TootInstance(parser: TootParser, src: JsonObject) { @@ -121,13 +126,17 @@ class TootInstance(parser: TootParser, src: JsonObject) { var feature_quote = false - var fedibird_capabilities: JsonArray? = null + var fedibird_capabilities: Set? = null + + var misskeyEndpoints: Set? = null // XXX: urls をパースしてない。使ってないから… init { if (parser.serviceType == ServiceType.MISSKEY) { + this.misskeyEndpoints = src.jsonArray("_endpoints")?.stringList()?.toSet() + this.uri = parser.apiHost.ascii this.title = parser.apiHost.pretty val sv = src.jsonObject("maintainer")?.string("url") @@ -192,7 +201,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { this.invites_enabled = src.boolean("invites_enabled") - this.fedibird_capabilities = src.jsonArray("fedibird_capabilities") + this.fedibird_capabilities = src.jsonArray("fedibird_capabilities")?.stringList()?.toSet() } } @@ -222,8 +231,6 @@ class TootInstance(parser: TootParser, src: JsonObject) { return i >= 0 } - fun hasCapability(cap: InstanceCapability) = cap.hasCapability(this) - companion object { private val rePleroma = """\bpleroma\b""".asciiPattern(Pattern.CASE_INSENSITIVE) @@ -274,7 +281,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { if (sendRequest(result) { val builder = Request.Builder().url("https://${apiHost?.ascii}/api/v1/instance") - (forceAccessToken ?: account?.getAccessToken() ) + (forceAccessToken ?: account?.getAccessToken()) ?.notEmpty()?.let { builder.header("Authorization", "Bearer $it") } builder.build() } @@ -285,6 +292,26 @@ class TootInstance(parser: TootParser, src: JsonObject) { return result } + private suspend fun TootApiClient.getMisskeyEndpoints( + forceAccessToken: String? = null + ): TootApiResult? { + val result = TootApiResult.makeWithCaption(apiHost?.pretty) + if (result.error != null) return result + + if (sendRequest(result) { + jsonObject { + (forceAccessToken ?: account?.misskeyApiToken) + ?.notEmpty()?.let { put("i", it) } + }.toPostRequestBuilder() + .url("https://${apiHost?.ascii}/api/endpoints") + .build() + } + ) { + parseJson(result) ?: return null + } + return result + } + // 疑似アカウントの追加時に、インスタンスの検証を行う private suspend fun TootApiClient.getInstanceInformationMisskey( forceAccessToken: String? = null @@ -295,7 +322,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { if (sendRequest(result) { jsonObject { put("dummy", 1) - (forceAccessToken ?: account?.misskeyApiToken ) + (forceAccessToken ?: account?.misskeyApiToken) ?.notEmpty()?.let { put("i", it) } }.toPostRequestBuilder() .url("https://${apiHost?.ascii}/api/meta") @@ -309,6 +336,10 @@ class TootInstance(parser: TootParser, src: JsonObject) { if (m.find()) { put(TootApiClient.KEY_MISSKEY_VERSION, max(1, m.groupEx(1)!!.toInt())) } + + // add endpoints + val r2 = getMisskeyEndpoints(forceAccessToken) + r2?.jsonArray?.let { result.jsonObject?.put("_endpoints", it) } } } return result @@ -419,9 +450,9 @@ class TootInstance(parser: TootParser, src: JsonObject) { return queuedRequest(allowPixelfed) { cached -> // may use cached item. - if (!forceUpdate && forceAccessToken == null && cached!=null) { + if (!forceUpdate && forceAccessToken == null && cached != null) { val now = SystemClock.elapsedRealtime() - if ( now - cached.time_parse <= EXPIRE) + if (now - cached.time_parse <= EXPIRE) return@queuedRequest Pair(cached, TootApiResult()) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReaction.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReaction.kt index af7446c0..9afe35ef 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReaction.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReaction.kt @@ -96,7 +96,7 @@ class TootReaction( val UNKNOWN = TootReaction(name = "?") - fun isUnicodeEmoji(code: String): Boolean = + private fun isUnicodeEmoji(code: String): Boolean = code.any { it.code >= 0x7f } fun splitEmojiDomain(code: String): Pair { @@ -111,11 +111,7 @@ class TootReaction( fun canReaction( access_info: SavedAccount, ti: TootInstance? = TootInstance.getCached(access_info.apiHost) - ) = when { - access_info.isPseudo -> false - access_info.isMisskey -> true - else -> ti?.fedibird_capabilities?.contains("emoji_reaction") == true - } + ) = InstanceCapability.emojiReaction(access_info,ti) fun decodeEmojiQuery(jsonText: String?): List = jsonText.notEmpty()?.let { src -> @@ -137,9 +133,6 @@ class TootReaction( } - private val isUnicodeEmoji: Boolean - get() = isUnicodeEmoji(name) - fun splitEmojiDomain() = splitEmojiDomain(name) diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt index de6a8be8..208dcf73 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt @@ -36,9 +36,9 @@ import java.util.* import kotlin.math.min class PostHelper( - private val activity: AppCompatActivity, - private val pref: SharedPreferences, - private val handler: Handler + private val activity: AppCompatActivity, + private val pref: SharedPreferences, + private val handler: Handler ) { companion object { @@ -83,22 +83,22 @@ class PostHelper( private var last_post_task: WeakReference? = null fun post(account: SavedAccount, callback: PostCompleteCallback) = post( - account, - callback, - bConfirmTag = false, - bConfirmAccount = false, - bConfirmRedraft = false, - bConfirmTagCharacter = false - ) + account, + callback, + bConfirmTag = false, + bConfirmAccount = false, + bConfirmRedraft = false, + bConfirmTagCharacter = false + ) fun post( - account: SavedAccount, - callback: PostCompleteCallback, - bConfirmTag: Boolean, - bConfirmAccount: Boolean, - bConfirmRedraft: Boolean, - bConfirmTagCharacter: Boolean - ) { + account: SavedAccount, + callback: PostCompleteCallback, + bConfirmTag: Boolean, + bConfirmAccount: Boolean, + bConfirmRedraft: Boolean, + bConfirmTagCharacter: Boolean + ) { val content = this.content ?: "" val spoiler_text = this.spoiler_text val bNSFW = this.bNSFW @@ -163,26 +163,26 @@ class PostHelper( 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() - } + 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() { - post( - account, callback, - bConfirmTag = bConfirmTag, - bConfirmAccount = true, - bConfirmRedraft = bConfirmRedraft, - bConfirmTagCharacter = bConfirmTagCharacter - ) - } - }) + override fun onOK() { + post( + account, callback, + bConfirmTag = bConfirmTag, + bConfirmAccount = true, + bConfirmRedraft = bConfirmRedraft, + bConfirmTagCharacter = bConfirmTagCharacter + ) + } + }) return } @@ -200,20 +200,20 @@ class PostHelper( AlertDialog.Builder(activity) .setCancelable(true) .setMessage( - activity.getString( - R.string.hashtag_contains_ascii_and_not_ascii, - badTags.joinToString(", ") - ) - ) + activity.getString( + R.string.hashtag_contains_ascii_and_not_ascii, + badTags.joinToString(", ") + ) + ) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok) { _, _ -> post( - account, callback, - bConfirmTag = bConfirmTag, - bConfirmAccount = bConfirmAccount, - bConfirmRedraft = bConfirmRedraft, - bConfirmTagCharacter = true - ) + account, callback, + bConfirmTag = bConfirmTag, + bConfirmAccount = bConfirmAccount, + bConfirmRedraft = bConfirmRedraft, + bConfirmTagCharacter = true + ) } .show() return @@ -234,12 +234,12 @@ class PostHelper( .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok) { _, _ -> post( - account, callback, - bConfirmTag = true, - bConfirmAccount = bConfirmAccount, - bConfirmRedraft = bConfirmRedraft, - bConfirmTagCharacter = bConfirmTagCharacter - ) + account, callback, + bConfirmTag = true, + bConfirmAccount = bConfirmAccount, + bConfirmRedraft = bConfirmRedraft, + bConfirmTagCharacter = bConfirmTagCharacter + ) } .show() return @@ -254,12 +254,12 @@ class PostHelper( .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok) { _, _ -> post( - account, callback, - bConfirmTag = bConfirmTag, - bConfirmAccount = bConfirmAccount, - bConfirmRedraft = true, - bConfirmTagCharacter = bConfirmTagCharacter - ) + account, callback, + bConfirmTag = bConfirmTag, + bConfirmAccount = bConfirmAccount, + bConfirmRedraft = true, + bConfirmTagCharacter = bConfirmTagCharacter + ) } .show() return @@ -271,12 +271,12 @@ class PostHelper( .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok) { _, _ -> post( - account, callback, - bConfirmTag = bConfirmTag, - bConfirmAccount = bConfirmAccount, - bConfirmRedraft = true, - bConfirmTagCharacter = bConfirmTagCharacter - ) + account, callback, + bConfirmTag = bConfirmTag, + bConfirmAccount = bConfirmAccount, + bConfirmRedraft = true, + bConfirmTagCharacter = bConfirmTagCharacter + ) } .show() return @@ -298,384 +298,387 @@ class PostHelper( // 全ての確認を終えたらバックグラウンドでの処理を開始する last_post_task = WeakReference( - TootTaskRunner(activity, - progressSetupCallback = { progressDialog -> - progressDialog.setCanceledOnTouchOutside(false) - } - ).run(account, object : TootTask { + TootTaskRunner(activity, + progressSetupCallback = { progressDialog -> + progressDialog.setCanceledOnTouchOutside(false) + } + ).run(account, object : TootTask { - var status: TootStatus? = null + var status: TootStatus? = null - var credential_tmp: TootAccount? = null + var credential_tmp: TootAccount? = null - var scheduledStatusSucceeded = false + var scheduledStatusSucceeded = false - suspend fun getCredential( - client: TootApiClient, - parser: TootParser - ): TootApiResult? { - val result = client.request("/api/v1/accounts/verify_credentials") - credential_tmp = parser.account(result?.jsonObject) - return result - } + suspend fun getCredential( + client: TootApiClient, + parser: TootParser + ): TootApiResult? { + val result = client.request("/api/v1/accounts/verify_credentials") + credential_tmp = parser.account(result?.jsonObject) + return result + } - override suspend fun background(client: TootApiClient): TootApiResult? { - val parser = TootParser(activity, account) + override suspend fun background(client: TootApiClient): TootApiResult? { + val parser = TootParser(activity, account) - var result: TootApiResult? + var result: TootApiResult? - val (instance, ri) = TootInstance.get(client) - instance ?: return ri + val (instance, ri) = TootInstance.get(client) + instance ?: return ri - var visibility_checked: TootVisibility? = visibility + var visibility_checked: TootVisibility? = visibility - if (visibility == TootVisibility.WebSetting) { - visibility_checked = - if (account.isMisskey || instance.versionGE(TootInstance.VERSION_1_6)) { - null - } else { - val r2 = getCredential(client, parser) - val credential_tmp = this.credential_tmp ?: return r2 - val privacy = credential_tmp.source?.privacy - ?: return TootApiResult(activity.getString(R.string.cant_get_web_setting_visibility)) - TootVisibility.parseMastodon(privacy) - } - } + if (visibility == TootVisibility.WebSetting) { + visibility_checked = + if (account.isMisskey || instance.versionGE(TootInstance.VERSION_1_6)) { + null + } else { + val r2 = getCredential(client, parser) + val credential_tmp = this.credential_tmp ?: return r2 + val privacy = credential_tmp.source?.privacy + ?: return TootApiResult(activity.getString(R.string.cant_get_web_setting_visibility)) + TootVisibility.parseMastodon(privacy) + } + } - for (pair in arrayOf( - Pair(TootVisibility.Mutual, InstanceCapability.VisibilityMutual), - Pair(TootVisibility.Limited, InstanceCapability.VisibilityLimited), - )) { - val (checkVisibility, checkCapability) = pair - if (visibility == checkVisibility && - !instance.hasCapability(checkCapability) - ) { - val strVisibility = Styler.getVisibilityString(activity, account.isMisskey, checkVisibility) - return TootApiResult(activity.getString(R.string.server_has_no_support_of_visibility, strVisibility)) - } - } + for (pair in arrayOf( + Pair(TootVisibility.Mutual, InstanceCapability::visibilityMutual), + Pair(TootVisibility.Limited, InstanceCapability::visibilityLimited), + )) { + val (checkVisibility, checkFun) = pair + if (visibility == checkVisibility && !checkFun(instance)) { + val strVisibility = Styler.getVisibilityString(activity, account.isMisskey, checkVisibility) + return TootApiResult( + activity.getString( + R.string.server_has_no_support_of_visibility, + strVisibility + ) + ) + } + } - // 元の投稿を削除する - if (redraft_status_id != null) { - result = if (account.isMisskey) { - client.request( - "/api/notes/delete", - account.putMisskeyApiToken(JsonObject()).apply { - put("noteId", redraft_status_id) - }.toPostRequestBuilder() - ) - } else { - client.request( - "/api/v1/statuses/$redraft_status_id", - Request.Builder().delete() - ) - } - log.d("delete redraft. result=$result") - Thread.sleep(2000L) + // 元の投稿を削除する + if (redraft_status_id != null) { + result = if (account.isMisskey) { + client.request( + "/api/notes/delete", + account.putMisskeyApiToken(JsonObject()).apply { + put("noteId", redraft_status_id) + }.toPostRequestBuilder() + ) + } else { + client.request( + "/api/v1/statuses/$redraft_status_id", + Request.Builder().delete() + ) + } + log.d("delete redraft. result=$result") + Thread.sleep(2000L) - } else if (scheduledId != null) { - val r1 = client.request( - "/api/v1/scheduled_statuses/$scheduledId", - Request.Builder().delete() - ) - log.d("delete old scheduled status. result=$r1") - Thread.sleep(2000L) - } + } else if (scheduledId != null) { + val r1 = client.request( + "/api/v1/scheduled_statuses/$scheduledId", + Request.Builder().delete() + ) + log.d("delete old scheduled status. result=$r1") + Thread.sleep(2000L) + } - if (instance.instanceType == InstanceType.Pixelfed) { - if (in_reply_to_id != null && attachment_list?.isNotEmpty() == true) { - return TootApiResult(activity.getString(R.string.pixelfed_does_not_allow_reply_with_media)) - } - if (in_reply_to_id == null && attachment_list?.isNotEmpty() != true) { - return TootApiResult(activity.getString(R.string.pixelfed_does_not_allow_post_without_media)) - } - } + if (instance.instanceType == InstanceType.Pixelfed) { + if (in_reply_to_id != null && attachment_list?.isNotEmpty() == true) { + return TootApiResult(activity.getString(R.string.pixelfed_does_not_allow_reply_with_media)) + } + if (in_reply_to_id == null && attachment_list?.isNotEmpty() != true) { + return TootApiResult(activity.getString(R.string.pixelfed_does_not_allow_post_without_media)) + } + } - val json = JsonObject() - try { - if (account.isMisskey) { - account.putMisskeyApiToken(json) - json["text"] = EmojiDecoder.decodeShortCode( - content, - emojiMapCustom = emojiMapCustom - ) - if (visibility_checked != null) { + val json = JsonObject() + try { + if (account.isMisskey) { + account.putMisskeyApiToken(json) + json["text"] = EmojiDecoder.decodeShortCode( + content, + emojiMapCustom = emojiMapCustom + ) + if (visibility_checked != null) { - if (visibility_checked == TootVisibility.DirectSpecified || - visibility_checked == TootVisibility.DirectPrivate - ) { - val userIds = JsonArray() + if (visibility_checked == TootVisibility.DirectSpecified || + visibility_checked == TootVisibility.DirectPrivate + ) { + val userIds = JsonArray() - val m = TootAccount.reMisskeyMentionPost.matcher(content) - while (m.find()) { - val username = m.groupEx(1) - val host = m.groupEx(2) // may null + val m = TootAccount.reMisskeyMentionPost.matcher(content) + while (m.find()) { + val username = m.groupEx(1) + val host = m.groupEx(2) // may null - result = client.request( - "/api/users/show", - account.putMisskeyApiToken().apply { - if (username?.isNotEmpty() == true) - put("username", username) - if (host?.isNotEmpty() == true) - put("host", host) - }.toPostRequestBuilder() - ) - val id = result?.jsonObject?.string("id") - if (id?.isNotEmpty() == true) { - userIds.add(id) - } - } - json["visibility"] = when { - userIds.isNotEmpty() -> { - json["visibleUserIds"] = userIds - "specified" - } + result = client.request( + "/api/users/show", + account.putMisskeyApiToken().apply { + if (username?.isNotEmpty() == true) + put("username", username) + if (host?.isNotEmpty() == true) + put("host", host) + }.toPostRequestBuilder() + ) + val id = result?.jsonObject?.string("id") + if (id?.isNotEmpty() == true) { + userIds.add(id) + } + } + json["visibility"] = when { + userIds.isNotEmpty() -> { + json["visibleUserIds"] = userIds + "specified" + } - account.misskeyVersion >= 11 -> "specified" - else -> "private" - } - } else { - val localVis = visibility_checked.strMisskey.replace( - "^local-".toRegex(), - "" - ) - if (localVis != visibility_checked.strMisskey) { - json["localOnly"] = true - json["visibility"] = localVis - } else { - json["visibility"] = visibility_checked.strMisskey - } - } - } + account.misskeyVersion >= 11 -> "specified" + else -> "private" + } + } else { + val localVis = visibility_checked.strMisskey.replace( + "^local-".toRegex(), + "" + ) + if (localVis != visibility_checked.strMisskey) { + json["localOnly"] = true + json["visibility"] = localVis + } else { + json["visibility"] = visibility_checked.strMisskey + } + } + } - if (spoiler_text?.isNotEmpty() == true) { - json["cw"] = EmojiDecoder.decodeShortCode( - spoiler_text, - emojiMapCustom = emojiMapCustom - ) - } + if (spoiler_text?.isNotEmpty() == true) { + json["cw"] = EmojiDecoder.decodeShortCode( + spoiler_text, + emojiMapCustom = emojiMapCustom + ) + } - if (in_reply_to_id != null) { - if (useQuoteToot) { - json["renoteId"] = in_reply_to_id.toString() - } else { - json["replyId"] = in_reply_to_id.toString() - } - } + if (in_reply_to_id != null) { + if (useQuoteToot) { + json["renoteId"] = in_reply_to_id.toString() + } else { + json["replyId"] = in_reply_to_id.toString() + } + } - json["viaMobile"] = true + json["viaMobile"] = true - if (attachment_list != null) { - val array = JsonArray() - for (pa in attachment_list) { - val a = pa.attachment ?: continue - // Misskeyは画像の再利用に問題がないので redraftとバージョンのチェックは行わない - array.add(a.id.toString()) + if (attachment_list != null) { + val array = JsonArray() + for (pa in attachment_list) { + val a = pa.attachment ?: continue + // Misskeyは画像の再利用に問題がないので redraftとバージョンのチェックは行わない + array.add(a.id.toString()) - // Misskeyの場合、NSFWするにはアップロード済みの画像を drive/files/update で更新する - if (bNSFW) { - val r = client.request( - "/api/drive/files/update", - account.putMisskeyApiToken().apply { - put("fileId", a.id.toString()) - put("isSensitive", true) - } - .toPostRequestBuilder() - ) - if (r == null || r.error != null) return r - } - } - if (array.isNotEmpty()) json["mediaIds"] = array - } + // Misskeyの場合、NSFWするにはアップロード済みの画像を drive/files/update で更新する + if (bNSFW) { + val r = client.request( + "/api/drive/files/update", + account.putMisskeyApiToken().apply { + put("fileId", a.id.toString()) + put("isSensitive", true) + } + .toPostRequestBuilder() + ) + if (r == null || r.error != null) return r + } + } + if (array.isNotEmpty()) json["mediaIds"] = array + } - if (enquete_items?.isNotEmpty() == true) { - val choices = JsonArray().apply { - for (item in enquete_items) { - val text = EmojiDecoder.decodeShortCode( - item, - emojiMapCustom = emojiMapCustom - ) - if (text.isEmpty()) continue - add(text) - } - } - if (choices.isNotEmpty()) { - json["poll"] = jsonObject { - put("choices", choices) - } - } - } + if (enquete_items?.isNotEmpty() == true) { + val choices = JsonArray().apply { + for (item in enquete_items) { + val text = EmojiDecoder.decodeShortCode( + item, + emojiMapCustom = emojiMapCustom + ) + if (text.isEmpty()) continue + add(text) + } + } + if (choices.isNotEmpty()) { + json["poll"] = jsonObject { + put("choices", choices) + } + } + } - if (scheduledAt != 0L) { - return TootApiResult("misskey has no scheduled status API") - } + if (scheduledAt != 0L) { + return TootApiResult("misskey has no scheduled status API") + } - } else { - json["status"] = EmojiDecoder.decodeShortCode( - content, - emojiMapCustom = emojiMapCustom - ) - if (visibility_checked != null) { - json["visibility"] = visibility_checked.strMastodon - } - json["sensitive"] = bNSFW - json["spoiler_text"] = EmojiDecoder.decodeShortCode( - spoiler_text ?: "", - emojiMapCustom = emojiMapCustom - ) + } else { + json["status"] = EmojiDecoder.decodeShortCode( + content, + emojiMapCustom = emojiMapCustom + ) + if (visibility_checked != null) { + json["visibility"] = visibility_checked.strMastodon + } + json["sensitive"] = bNSFW + json["spoiler_text"] = EmojiDecoder.decodeShortCode( + spoiler_text ?: "", + emojiMapCustom = emojiMapCustom + ) - if (in_reply_to_id != null) { - if (useQuoteToot) { - json["quote_id"] = in_reply_to_id.toString() - } else { - json["in_reply_to_id"] = in_reply_to_id.toString() - } - } + if (in_reply_to_id != null) { + if (useQuoteToot) { + json["quote_id"] = in_reply_to_id.toString() + } else { + json["in_reply_to_id"] = in_reply_to_id.toString() + } + } - if (attachment_list != null) { - json["media_ids"] = jsonArray { - for (pa in attachment_list) { - val a = pa.attachment ?: continue - if (a.redraft && !instance.versionGE(TootInstance.VERSION_2_4_1)) continue - add(a.id.toString()) - } - } - } + if (attachment_list != null) { + json["media_ids"] = jsonArray { + for (pa in attachment_list) { + val a = pa.attachment ?: continue + if (a.redraft && !instance.versionGE(TootInstance.VERSION_2_4_1)) continue + add(a.id.toString()) + } + } + } - if (enquete_items?.isNotEmpty() == true) { - if (poll_type == TootPollsType.Mastodon) { - json["poll"] = jsonObject { - put("multiple", poll_multiple_choice) - put("hide_totals", poll_hide_totals) - put("expires_in", poll_expire_seconds) - put("options", - enquete_items.map { - EmojiDecoder.decodeShortCode( - it, - emojiMapCustom = emojiMapCustom - ) - } - .toJsonArray() - ) - } - } else { - json["isEnquete"] = true - json["enquete_items"] = enquete_items.map { - EmojiDecoder.decodeShortCode( - it, - emojiMapCustom = emojiMapCustom - ) - }.toJsonArray() - } - } + if (enquete_items?.isNotEmpty() == true) { + if (poll_type == TootPollsType.Mastodon) { + json["poll"] = jsonObject { + put("multiple", poll_multiple_choice) + put("hide_totals", poll_hide_totals) + put("expires_in", poll_expire_seconds) + put("options", + enquete_items.map { + EmojiDecoder.decodeShortCode( + it, + emojiMapCustom = emojiMapCustom + ) + } + .toJsonArray() + ) + } + } else { + json["isEnquete"] = true + json["enquete_items"] = enquete_items.map { + EmojiDecoder.decodeShortCode( + it, + emojiMapCustom = emojiMapCustom + ) + }.toJsonArray() + } + } - if (scheduledAt != 0L) { - if (!instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) { - return TootApiResult(activity.getString(R.string.scheduled_status_requires_mastodon_2_7_0)) - } - // UTCの日時を渡す - val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC")) - c.timeInMillis = scheduledAt - val sv = String.format( - "%d-%02d-%02d %02d:%02d:%02d", - c.get(Calendar.YEAR), - c.get(Calendar.MONTH) + 1, - c.get(Calendar.DAY_OF_MONTH), - c.get(Calendar.HOUR_OF_DAY), - c.get(Calendar.MINUTE), - c.get(Calendar.SECOND) - ) - json["scheduled_at"] = sv - } + if (scheduledAt != 0L) { + if (!instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) { + return TootApiResult(activity.getString(R.string.scheduled_status_requires_mastodon_2_7_0)) + } + // UTCの日時を渡す + val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC")) + c.timeInMillis = scheduledAt + val sv = String.format( + "%d-%02d-%02d %02d:%02d:%02d", + c.get(Calendar.YEAR), + c.get(Calendar.MONTH) + 1, + c.get(Calendar.DAY_OF_MONTH), + c.get(Calendar.HOUR_OF_DAY), + c.get(Calendar.MINUTE), + c.get(Calendar.SECOND) + ) + json["scheduled_at"] = sv + } - } - } catch (ex: JsonException) { - log.trace(ex) - log.e(ex, "status encoding failed.") - } + } + } catch (ex: JsonException) { + log.trace(ex) + log.e(ex, "status encoding failed.") + } - val body_string = json.toString() + val body_string = json.toString() - val request_builder = body_string.toRequestBody(MEDIA_TYPE_JSON).toPost() + val request_builder = body_string.toRequestBody(MEDIA_TYPE_JSON).toPost() - if (!Pref.bpDontDuplicationCheck(pref)) { - val digest = (body_string + account.acct.ascii).digestSHA256Hex() - request_builder.header("Idempotency-Key", digest) - } + if (!Pref.bpDontDuplicationCheck(pref)) { + val digest = (body_string + account.acct.ascii).digestSHA256Hex() + request_builder.header("Idempotency-Key", digest) + } - result = if (account.isMisskey) { - // log.d("misskey json %s", body_string) - client.request("/api/notes/create", request_builder) - } else { - client.request("/api/v1/statuses", request_builder) - } + result = if (account.isMisskey) { + // log.d("misskey json %s", body_string) + client.request("/api/notes/create", request_builder) + } else { + client.request("/api/v1/statuses", request_builder) + } - val jsonObject = result?.jsonObject + val jsonObject = result?.jsonObject - if (scheduledAt != 0L && jsonObject != null) { - // {"id":"3","scheduled_at":"2019-01-06T07:08:00.000Z","media_attachments":[]} - scheduledStatusSucceeded = true - return result - } + if (scheduledAt != 0L && jsonObject != null) { + // {"id":"3","scheduled_at":"2019-01-06T07:08:00.000Z","media_attachments":[]} + scheduledStatusSucceeded = true + return result + } - val status = parser.status( - if (account.isMisskey) { - result?.jsonObject?.jsonObject("createdNote") ?: result?.jsonObject - } else { - result?.jsonObject - } - ) - this.status = status - if (status != null) { + val status = parser.status( + if (account.isMisskey) { + result?.jsonObject?.jsonObject("createdNote") ?: result?.jsonObject + } else { + result?.jsonObject + } + ) + this.status = status + if (status != null) { - // タグを覚えておく - val s = status.decoded_content - val span_list = s.getSpans(0, s.length, MyClickableSpan::class.java) - if (span_list != null) { - val tag_list = ArrayList(span_list.size) - for (span in span_list) { - val start = s.getSpanStart(span) - val end = s.getSpanEnd(span) - val text = s.subSequence(start, end).toString() - if (text.startsWith("#")) { - tag_list.add(text.substring(1)) - } - } - val count = tag_list.size - if (count > 0) { - TagSet.saveList(System.currentTimeMillis(), tag_list, 0, count) - } + // タグを覚えておく + val s = status.decoded_content + val span_list = s.getSpans(0, s.length, MyClickableSpan::class.java) + if (span_list != null) { + val tag_list = ArrayList(span_list.size) + for (span in span_list) { + val start = s.getSpanStart(span) + val end = s.getSpanEnd(span) + val text = s.subSequence(start, end).toString() + if (text.startsWith("#")) { + tag_list.add(text.substring(1)) + } + } + val count = tag_list.size + if (count > 0) { + TagSet.saveList(System.currentTimeMillis(), tag_list, 0, count) + } - } + } - } - return result - } + } + return result + } - override suspend fun handleResult(result: TootApiResult?) { - result ?: return - val status = this.status - when { - status != null -> { - // 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る - callback.onPostComplete(account, status) - return - } + override suspend fun handleResult(result: TootApiResult?) { + result ?: return + val status = this.status + when { + status != null -> { + // 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る + callback.onPostComplete(account, status) + return + } - scheduledStatusSucceeded -> { - callback.onScheduledPostComplete(account) - return + scheduledStatusSucceeded -> { + callback.onScheduledPostComplete(account) + return - } + } - else -> activity.showToast(true, result.error) - } - } - }) - ) + else -> activity.showToast(true, result.error) + } + } + }) + ) } /////////////////////////////////////////////////////////////////////////////////// @@ -736,19 +739,19 @@ class PostHelper( fun matchIdnWord(cp: Int) = when (Character.getType(cp).toByte()) { // Letter // LCはエイリアスなので文字から得られることはないはず - Character.UPPERCASE_LETTER, - Character.LOWERCASE_LETTER, - Character.TITLECASE_LETTER, - Character.MODIFIER_LETTER, - Character.OTHER_LETTER -> true + Character.UPPERCASE_LETTER, + Character.LOWERCASE_LETTER, + Character.TITLECASE_LETTER, + Character.MODIFIER_LETTER, + Character.OTHER_LETTER -> true // Mark - Character.NON_SPACING_MARK, - Character.COMBINING_SPACING_MARK, - Character.ENCLOSING_MARK -> true + Character.NON_SPACING_MARK, + Character.COMBINING_SPACING_MARK, + Character.ENCLOSING_MARK -> true // Decimal_Number - Character.DECIMAL_DIGIT_NUMBER -> true + Character.DECIMAL_DIGIT_NUMBER -> true // Connector_Punctuation - Character.CONNECTOR_PUNCTUATION -> true + Character.CONNECTOR_PUNCTUATION -> true else -> false } @@ -845,8 +848,8 @@ class PostHelper( if (part.isEmpty()) { // :を入力した直後は候補は0で、「閉じる」と「絵文字を選ぶ」だけが表示されたポップアップを出す openPopup()?.setList( - et, last_colon, end, null, picker_caption_emoji, open_picker_emoji - ) + et, last_colon, end, null, picker_caption_emoji, open_picker_emoji + ) return } @@ -873,21 +876,21 @@ class PostHelper( } openPopup()?.setList( - et, - last_colon, - end, - code_list, - picker_caption_emoji, - open_picker_emoji - ) + et, + last_colon, + end, + code_list, + picker_caption_emoji, + open_picker_emoji + ) } // カスタム絵文字の候補を作る private fun customEmojiCodeList( - accessInfo: SavedAccount?, - @Suppress("SameParameterValue") limit: Int, - needle: String - ) = ArrayList().also { dst -> + accessInfo: SavedAccount?, + @Suppress("SameParameterValue") limit: Int, + needle: String + ) = ArrayList().also { dst -> accessInfo ?: return@also @@ -903,11 +906,11 @@ class PostHelper( val sb = SpannableStringBuilder() sb.append(' ') sb.setSpan( - NetworkEmojiSpan(item.url), - 0, - sb.length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) + NetworkEmojiSpan(item.url), + 0, + sb.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) sb.append(' ') if (item.alias != null) { val start = sb.length @@ -915,11 +918,11 @@ class PostHelper( sb.append(item.alias) sb.append(": → ") sb.setSpan( - ForegroundColorSpan(activity.attrColor(R.attr.colorTimeSmall)), - start, - sb.length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) + ForegroundColorSpan(activity.attrColor(R.attr.colorTimeSmall)), + start, + sb.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } sb.append(':') @@ -978,45 +981,45 @@ class PostHelper( } fun attachEditText( - _formRoot: View, - et: MyEditText, - bMainScreen: Boolean, - _callback2: Callback2 - ) { + _formRoot: View, + et: MyEditText, + bMainScreen: Boolean, + _callback2: Callback2 + ) { this.formRoot = _formRoot this.et = et this.callback2 = _callback2 this.bMainScreen = bMainScreen et.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence, - start: Int, - count: Int, - after: Int - ) { + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { - } + } - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - handler.removeCallbacks(proc_text_changed) - handler.postDelayed(proc_text_changed, if (popup?.isShowing == true) 100L else 500L) - } + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + handler.removeCallbacks(proc_text_changed) + handler.postDelayed(proc_text_changed, if (popup?.isShowing == true) 100L else 500L) + } - override fun afterTextChanged(s: Editable) { - callback2?.onTextUpdate() - } - }) + override fun afterTextChanged(s: Editable) { + callback2?.onTextUpdate() + } + }) et.setOnSelectionChangeListener(object : MyEditText.OnSelectionChangeListener { - override fun onSelectionChanged(selStart: Int, selEnd: Int) { - if (selStart != selEnd) { - // 範囲選択されてるならポップアップは閉じる - log.d("onSelectionChanged: range selected") - closeAcctPopup() - } - } - }) + override fun onSelectionChanged(selStart: Int, selEnd: Int) { + if (selStart != selEnd) { + // 範囲選択されてるならポップアップは閉じる + log.d("onSelectionChanged: range selected") + closeAcctPopup() + } + } + }) // 全然動いてなさそう… // et.setCustomSelectionActionModeCallback( action_mode_callback ); @@ -1027,39 +1030,40 @@ class PostHelper( appendEmoji(result.bInstanceHasCustomEmoji, result.emoji) private fun SpannableStringBuilder.appendEmoji( - bInstanceHasCustomEmoji: Boolean, - emoji: EmojiBase - ): SpannableStringBuilder { + bInstanceHasCustomEmoji: Boolean, + emoji: EmojiBase + ): SpannableStringBuilder { val separator = EmojiDecoder.customEmojiSeparator(pref) when (emoji) { - is CustomEmoji -> { - // カスタム絵文字は常にshortcode表現 - if (!EmojiDecoder.canStartShortCode(this, this.length)) append(separator) - this.append(SpannableString(":${emoji.shortcode}:")) - // セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。 - if (separator != ' ') append(separator) - } - is UnicodeEmoji -> { - if (!bInstanceHasCustomEmoji) { - // 古いタンスだとshortcodeを使う。見た目は絵文字に変える。 - if (!EmojiDecoder.canStartShortCode(this, this.length)) append(separator) - this.append(DecodeOptions(activity).decodeEmoji(":${emoji.unifiedName}:")) - // セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。 - if (separator != ' ') append(separator) - } else { - // 十分に新しいタンスなら絵文字のunicodeを使う。見た目は絵文字に変える。 - this.append(DecodeOptions(activity).decodeEmoji(emoji.unifiedCode)) - } - } + is CustomEmoji -> { + // カスタム絵文字は常にshortcode表現 + if (!EmojiDecoder.canStartShortCode(this, this.length)) append(separator) + this.append(SpannableString(":${emoji.shortcode}:")) + // セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。 + if (separator != ' ') append(separator) + } + is UnicodeEmoji -> { + if (!bInstanceHasCustomEmoji) { + // 古いタンスだとshortcodeを使う。見た目は絵文字に変える。 + if (!EmojiDecoder.canStartShortCode(this, this.length)) append(separator) + this.append(DecodeOptions(activity).decodeEmoji(":${emoji.unifiedName}:")) + // セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。 + if (separator != ' ') append(separator) + } else { + // 十分に新しいタンスなら絵文字のunicodeを使う。見た目は絵文字に変える。 + this.append(DecodeOptions(activity).decodeEmoji(emoji.unifiedCode)) + } + } } return this } private val open_picker_emoji: Runnable = Runnable { - EmojiPicker(activity, accessInfo, - closeOnSelected = Pref.bpEmojiPickerCloseOnSelected(pref) - ) { result -> + EmojiPicker( + activity, accessInfo, + closeOnSelected = Pref.bpEmojiPickerCloseOnSelected(pref) + ) { result -> val et = this.et ?: return@EmojiPicker val src = et.text ?: "" @@ -1082,17 +1086,18 @@ class PostHelper( // キーボードを再度表示する App1.getAppState( - activity, - "PostHelper/EmojiPicker/cb" - ).handler.post { et.showKeyboard() } + activity, + "PostHelper/EmojiPicker/cb" + ).handler.post { et.showKeyboard() } }.show() } fun openEmojiPickerFromMore() { - EmojiPicker(activity, accessInfo, - closeOnSelected = Pref.bpEmojiPickerCloseOnSelected(pref) - ) { result -> + EmojiPicker( + activity, accessInfo, + closeOnSelected = Pref.bpEmojiPickerCloseOnSelected(pref) + ) { result -> val et = this.et ?: return@EmojiPicker val src = et.text ?: "" diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 334af97a..57ebfdf2 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1079,7 +1079,7 @@ 確認メールの再送を要求した後の手順:\n- あなたのメーラーで新着メールが届くのを確認する。\n- メール中の確認リンクを開く。\n- このダイアログを閉じてカラムをリロードする。 プッシュ通知フィルタ(Mastodon 3.4.0以降。プッシュ通知の更新が必要) 誰もいない - リアクション (Fedibird) + リアクションした投稿 リアクション リモートのカスタム絵文字でリアクションできません %1$s リアクション表示用の空間を確保する @@ -1090,5 +1090,5 @@ 既に存在するキーワードを重複保存できません 変更を破棄しますか? 保存しました - + この機能を利用できるアカウントがありません diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a539a0f6..d720eea6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1093,7 +1093,7 @@ After requesting resending confirm E-mail,\n- please check the mail on your mailer.\n- open confirm link in the mail.\n- close this dialog and reload column. Push notification filter (Mastodon 3.4.0+, requires update push subscription) No one - Reactions (Fedibird) + Reactioned posts Reactions can\'t reaction with remote custom emoji %1$s Keep Spacing for Reactions @@ -1103,4 +1103,6 @@ Can\'t save duplicated keyword. Discard changes? saved. + Unavailable for current accounts +