リアクションした投稿の一覧をめいすきーでも見れるようにした

This commit is contained in:
tateisu 2021-05-22 21:02:55 +09:00
parent a5d16a6934
commit bc3f56c22d
14 changed files with 685 additions and 577 deletions

View File

@ -2682,7 +2682,7 @@ class ActPost : AsyncActivity(),
TootVisibility.DirectPrivate TootVisibility.DirectPrivate
) )
true == ti?.hasCapability(InstanceCapability.VisibilityMutual) -> InstanceCapability.visibilityMutual(ti) ->
arrayOf( arrayOf(
TootVisibility.WebSetting, TootVisibility.WebSetting,
TootVisibility.Public, TootVisibility.Public,
@ -2693,7 +2693,7 @@ class ActPost : AsyncActivity(),
TootVisibility.DirectSpecified TootVisibility.DirectSpecified
) )
true == ti?.hasCapability(InstanceCapability.VisibilityLimited) -> InstanceCapability.visibilityLimited(ti)->
arrayOf( arrayOf(
TootVisibility.WebSetting, TootVisibility.WebSetting,
TootVisibility.Public, TootVisibility.Public,

View File

@ -692,28 +692,52 @@ enum class ColumnType(
REACTIONS( REACTIONS(
42, 42,
iconId = { R.drawable.ic_face }, iconId = { R.drawable.ic_face },
name1 = { it.getString(R.string.reactions) }, name1 = { it.getString(R.string.reactioned_posts) },
bAllowPseudo = false, bAllowPseudo = false,
bAllowMisskey = false, bAllowMisskey = false,
loading = { client -> loading = { client ->
if (isMisskey) { if (isMisskey) {
TootApiResult("misskey has no api to list your reactions") getStatusList(
client,
ApiPath.PATH_M544_REACTIONS,
misskeyParams = column.makeMisskeyTimelineParameter(parser),
listParser = misskeyCustomParserFavorites
)
} else { } else {
getStatusList(client,column.makeReactionsUrl()) getStatusList(client,column.makeReactionsUrl())
} }
}, },
refresh = { client -> 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 -> gap = { client ->
getStatusList( if (isMisskey) {
client, getStatusList(
column.makeReactionsUrl(), client,
mastodonFilterByIdRange = false ApiPath.PATH_M544_REACTIONS,
) mastodonFilterByIdRange = false,
misskeyParams = column.makeMisskeyTimelineParameter(parser),
listParser = misskeyCustomParserFavorites
)
} else {
getStatusList(
client,
column.makeReactionsUrl(),
mastodonFilterByIdRange = false
)
}
}, },
gapDirection = gapDirectionMastodonWorkaround, gapDirection = gapDirectionMastodonWorkaround,
), ),

View File

@ -215,7 +215,8 @@ fun ColumnViewHolder.onPageCreate(column: Column, page_idx: Int, page_count: Int
btnSearchClear.vg(Pref.bpShowSearchClear(activity.pref)) btnSearchClear.vg(Pref.bpShowSearchClear(activity.pref))
cbResolve.vg(column.type == ColumnType.SEARCH) cbResolve.vg(column.type == ColumnType.SEARCH)
} }
column.type == ColumnType.REACTIONS -> {
column.type == ColumnType.REACTIONS && column.access_info.isMastodon -> {
llSearch.vg(true) llSearch.vg(true)
flEmoji.vg(true) flEmoji.vg(true)

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager 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_Account
import jp.juggler.subwaytooter.action.Action_App import jp.juggler.subwaytooter.action.Action_App
import jp.juggler.subwaytooter.action.Action_Instance 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.api.entity.TootStatus
import jp.juggler.subwaytooter.dialog.AccountPicker import jp.juggler.subwaytooter.dialog.AccountPicker
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
@ -269,8 +265,13 @@ class SideMenuAdapter(
Item(icon = R.drawable.ic_bookmark, title = R.string.bookmarks) { Item(icon = R.drawable.ic_bookmark, title = R.string.bookmarks) {
Action_Account.timeline(this, defaultInsertPosition, ColumnType.BOOKMARKS) Action_Account.timeline(this, defaultInsertPosition, ColumnType.BOOKMARKS)
}, },
Item(icon = R.drawable.ic_face, title = R.string.fedibird_reactions) { Item(icon = R.drawable.ic_face, title = R.string.reactioned_posts) {
Action_Account.getReactionableAccounts(this, allowMisskey = false) { list -> Action_Account.listAccountsCanSeeMyReactions(this) { list ->
if (list.isEmpty()) {
showToast(false, R.string.not_available_for_current_accounts)
return@listAccountsCanSeeMyReactions
}
val columnType = ColumnType.REACTIONS val columnType = ColumnType.REACTIONS
AccountPicker.pick( AccountPicker.pick(
this, this,

View File

@ -13,9 +13,7 @@ import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.openBrowser import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.util.* import jp.juggler.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
object Action_Account { object Action_Account {
@ -385,28 +383,32 @@ object Action_Account {
}.show() }.show()
} }
fun getReactionableAccounts( fun listAccountsReactionable(
activity: ActMain, activity: ActMain,
allowMisskey: Boolean = true,
block: (ArrayList<SavedAccount>) -> Unit block: (ArrayList<SavedAccount>) -> Unit
) { ) {
TootTaskRunner(activity).run(object : TootTask { TootTaskRunner(activity).run(object : TootTask {
var list: List<SavedAccount>? = null var list: List<SavedAccount>? = 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? { override suspend fun background(client: TootApiClient): TootApiResult? {
list = SavedAccount.loadAccountList(activity).filter { a -> coroutineScope {
when { list = SavedAccount.loadAccountList(activity)
client.isApiCancelled -> false .map { async { if(check(client,it)) it else null } }
a.isPseudo -> false .awaitAll()
a.isMisskey -> allowMisskey .filterNotNull()
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
}
}
} }
return if (client.isApiCancelled) null else TootApiResult() return if (client.isApiCancelled) null else TootApiResult()
} }
@ -417,7 +419,41 @@ object Action_Account {
} }
}) })
} }
fun listAccountsCanSeeMyReactions(
activity: ActMain,
block: (ArrayList<SavedAccount>) -> Unit
) {
TootTaskRunner(activity).run(object : TootTask {
var list: List<SavedAccount>? = 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( fun timelineWithFilter(
activity: ActMain, activity: ActMain,

View File

@ -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( AccountPicker.pick(
activity, activity,
accountListArg = list, accountListArg = list,

View File

@ -53,4 +53,6 @@ object ApiPath {
const val PATH_MISSKEY_FOLLOW_REQUESTS = "/api/following/requests/list" const val PATH_MISSKEY_FOLLOW_REQUESTS = "/api/following/requests/list"
const val PATH_MISSKEY_FOLLOW_SUGGESTION = "/api/users/recommendation" const val PATH_MISSKEY_FOLLOW_SUGGESTION = "/api/users/recommendation"
const val PATH_MISSKEY_FAVORITES = "/api/i/favorites" const val PATH_MISSKEY_FAVORITES = "/api/i/favorites"
}
const val PATH_M544_REACTIONS ="/api/i/reactions"
}

View File

@ -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 ) // query: query_string after ? ( ? itself is excluded )

View File

@ -104,7 +104,6 @@ class TootTaskRunner(
fun run(instance: Host, callback: TootTask): TootTaskRunner { fun run(instance: Host, callback: TootTask): TootTaskRunner {
client.apiHost = instance client.apiHost = instance
return run(callback) return run(callback)
} }
fun progressPrefix(s: String): TootTaskRunner { fun progressPrefix(s: String): TootTaskRunner {

View File

@ -11,7 +11,6 @@ import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.VersionString import jp.juggler.subwaytooter.util.VersionString
import jp.juggler.util.* import jp.juggler.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Request import okhttp3.Request
@ -28,39 +27,45 @@ enum class InstanceType {
Pleroma Pleroma
} }
enum class CapabilitySource { object InstanceCapability {
Fedibird, // 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( fun visibilityMutual(ti: TootInstance?) =
private val capabilitySource: CapabilitySource, ti?.fedibird_capabilities?.contains("visibility_mutual") == true
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 hasCapability(instance: TootInstance): Boolean {
when (capabilitySource) { fun visibilityLimited(ti: TootInstance?) =
CapabilitySource.Fedibird -> { ti?.fedibird_capabilities?.contains("visibility_limited") == true
if (instance.fedibird_capabilities?.any { it == id } == true) return 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) { class TootInstance(parser: TootParser, src: JsonObject) {
@ -121,13 +126,17 @@ class TootInstance(parser: TootParser, src: JsonObject) {
var feature_quote = false var feature_quote = false
var fedibird_capabilities: JsonArray? = null var fedibird_capabilities: Set<String>? = null
var misskeyEndpoints: Set<String>? = null
// XXX: urls をパースしてない。使ってないから… // XXX: urls をパースしてない。使ってないから…
init { init {
if (parser.serviceType == ServiceType.MISSKEY) { if (parser.serviceType == ServiceType.MISSKEY) {
this.misskeyEndpoints = src.jsonArray("_endpoints")?.stringList()?.toSet()
this.uri = parser.apiHost.ascii this.uri = parser.apiHost.ascii
this.title = parser.apiHost.pretty this.title = parser.apiHost.pretty
val sv = src.jsonObject("maintainer")?.string("url") 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.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 return i >= 0
} }
fun hasCapability(cap: InstanceCapability) = cap.hasCapability(this)
companion object { companion object {
private val rePleroma = """\bpleroma\b""".asciiPattern(Pattern.CASE_INSENSITIVE) private val rePleroma = """\bpleroma\b""".asciiPattern(Pattern.CASE_INSENSITIVE)
@ -274,7 +281,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
if (sendRequest(result) { if (sendRequest(result) {
val builder = Request.Builder().url("https://${apiHost?.ascii}/api/v1/instance") val builder = Request.Builder().url("https://${apiHost?.ascii}/api/v1/instance")
(forceAccessToken ?: account?.getAccessToken() ) (forceAccessToken ?: account?.getAccessToken())
?.notEmpty()?.let { builder.header("Authorization", "Bearer $it") } ?.notEmpty()?.let { builder.header("Authorization", "Bearer $it") }
builder.build() builder.build()
} }
@ -285,6 +292,26 @@ class TootInstance(parser: TootParser, src: JsonObject) {
return result 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( private suspend fun TootApiClient.getInstanceInformationMisskey(
forceAccessToken: String? = null forceAccessToken: String? = null
@ -295,7 +322,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
if (sendRequest(result) { if (sendRequest(result) {
jsonObject { jsonObject {
put("dummy", 1) put("dummy", 1)
(forceAccessToken ?: account?.misskeyApiToken ) (forceAccessToken ?: account?.misskeyApiToken)
?.notEmpty()?.let { put("i", it) } ?.notEmpty()?.let { put("i", it) }
}.toPostRequestBuilder() }.toPostRequestBuilder()
.url("https://${apiHost?.ascii}/api/meta") .url("https://${apiHost?.ascii}/api/meta")
@ -309,6 +336,10 @@ class TootInstance(parser: TootParser, src: JsonObject) {
if (m.find()) { if (m.find()) {
put(TootApiClient.KEY_MISSKEY_VERSION, max(1, m.groupEx(1)!!.toInt())) 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 return result
@ -419,9 +450,9 @@ class TootInstance(parser: TootParser, src: JsonObject) {
return queuedRequest(allowPixelfed) { cached -> return queuedRequest(allowPixelfed) { cached ->
// may use cached item. // may use cached item.
if (!forceUpdate && forceAccessToken == null && cached!=null) { if (!forceUpdate && forceAccessToken == null && cached != null) {
val now = SystemClock.elapsedRealtime() val now = SystemClock.elapsedRealtime()
if ( now - cached.time_parse <= EXPIRE) if (now - cached.time_parse <= EXPIRE)
return@queuedRequest Pair(cached, TootApiResult()) return@queuedRequest Pair(cached, TootApiResult())
} }

View File

@ -96,7 +96,7 @@ class TootReaction(
val UNKNOWN = TootReaction(name = "?") val UNKNOWN = TootReaction(name = "?")
fun isUnicodeEmoji(code: String): Boolean = private fun isUnicodeEmoji(code: String): Boolean =
code.any { it.code >= 0x7f } code.any { it.code >= 0x7f }
fun splitEmojiDomain(code: String): Pair<String?, String?> { fun splitEmojiDomain(code: String): Pair<String?, String?> {
@ -111,11 +111,7 @@ class TootReaction(
fun canReaction( fun canReaction(
access_info: SavedAccount, access_info: SavedAccount,
ti: TootInstance? = TootInstance.getCached(access_info.apiHost) ti: TootInstance? = TootInstance.getCached(access_info.apiHost)
) = when { ) = InstanceCapability.emojiReaction(access_info,ti)
access_info.isPseudo -> false
access_info.isMisskey -> true
else -> ti?.fedibird_capabilities?.contains("emoji_reaction") == true
}
fun decodeEmojiQuery(jsonText: String?): List<TootReaction> = fun decodeEmojiQuery(jsonText: String?): List<TootReaction> =
jsonText.notEmpty()?.let { src -> jsonText.notEmpty()?.let { src ->
@ -137,9 +133,6 @@ class TootReaction(
} }
private val isUnicodeEmoji: Boolean
get() = isUnicodeEmoji(name)
fun splitEmojiDomain() = fun splitEmojiDomain() =
splitEmojiDomain(name) splitEmojiDomain(name)

File diff suppressed because it is too large Load Diff

View File

@ -1079,7 +1079,7 @@
<string name="confirm_mail_description">確認メールの再送を要求した後の手順:\n- あなたのメーラーで新着メールが届くのを確認する。\n- メール中の確認リンクを開く。\n- このダイアログを閉じてカラムをリロードする。</string> <string name="confirm_mail_description">確認メールの再送を要求した後の手順:\n- あなたのメーラーで新着メールが届くのを確認する。\n- メール中の確認リンクを開く。\n- このダイアログを閉じてカラムをリロードする。</string>
<string name="push_notification_filter">プッシュ通知フィルタ(Mastodon 3.4.0以降。プッシュ通知の更新が必要)</string> <string name="push_notification_filter">プッシュ通知フィルタ(Mastodon 3.4.0以降。プッシュ通知の更新が必要)</string>
<string name="no_one">誰もいない</string> <string name="no_one">誰もいない</string>
<string name="fedibird_reactions">リアクション (Fedibird)</string> <string name="reactioned_posts">リアクションした投稿</string>
<string name="reactions">リアクション</string> <string name="reactions">リアクション</string>
<string name="cant_reaction_remote_custom_emoji">リモートのカスタム絵文字でリアクションできません %1$s</string> <string name="cant_reaction_remote_custom_emoji">リモートのカスタム絵文字でリアクションできません %1$s</string>
<string name="keep_reaction_space">リアクション表示用の空間を確保する</string> <string name="keep_reaction_space">リアクション表示用の空間を確保する</string>
@ -1090,5 +1090,5 @@
<string name="cant_save_duplicated_keyword">既に存在するキーワードを重複保存できません</string> <string name="cant_save_duplicated_keyword">既に存在するキーワードを重複保存できません</string>
<string name="discard_changes">変更を破棄しますか?</string> <string name="discard_changes">変更を破棄しますか?</string>
<string name="saved">保存しました</string> <string name="saved">保存しました</string>
<string name="not_available_for_current_accounts">この機能を利用できるアカウントがありません</string>
</resources> </resources>

View File

@ -1093,7 +1093,7 @@
<string name="confirm_mail_description">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.</string> <string name="confirm_mail_description">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.</string>
<string name="push_notification_filter">Push notification filter (Mastodon 3.4.0+, requires update push subscription)</string> <string name="push_notification_filter">Push notification filter (Mastodon 3.4.0+, requires update push subscription)</string>
<string name="no_one">No one</string> <string name="no_one">No one</string>
<string name="fedibird_reactions">Reactions (Fedibird)</string> <string name="reactioned_posts">Reactioned posts</string>
<string name="reactions">Reactions</string> <string name="reactions">Reactions</string>
<string name="cant_reaction_remote_custom_emoji">can\'t reaction with remote custom emoji %1$s</string> <string name="cant_reaction_remote_custom_emoji">can\'t reaction with remote custom emoji %1$s</string>
<string name="keep_reaction_space">Keep Spacing for Reactions</string> <string name="keep_reaction_space">Keep Spacing for Reactions</string>
@ -1103,4 +1103,6 @@
<string name="cant_save_duplicated_keyword">Can\'t save duplicated keyword.</string> <string name="cant_save_duplicated_keyword">Can\'t save duplicated keyword.</string>
<string name="discard_changes">Discard changes?</string> <string name="discard_changes">Discard changes?</string>
<string name="saved">saved.</string> <string name="saved">saved.</string>
<string name="not_available_for_current_accounts">Unavailable for current accounts</string>
</resources> </resources>