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

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
)
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,

View File

@ -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,
),

View File

@ -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)

View File

@ -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,

View File

@ -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<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
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<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(
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(
activity,
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_SUGGESTION = "/api/users/recommendation"
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 )

View File

@ -104,7 +104,6 @@ class TootTaskRunner(
fun run(instance: Host, callback: TootTask): TootTaskRunner {
client.apiHost = instance
return run(callback)
}
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.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<String>? = null
var misskeyEndpoints: Set<String>? = 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())
}

View File

@ -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<String?, String?> {
@ -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<TootReaction> =
jsonText.notEmpty()?.let { src ->
@ -137,9 +133,6 @@ class TootReaction(
}
private val isUnicodeEmoji: Boolean
get() = isUnicodeEmoji(name)
fun splitEmojiDomain() =
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="push_notification_filter">プッシュ通知フィルタ(Mastodon 3.4.0以降。プッシュ通知の更新が必要)</string>
<string name="no_one">誰もいない</string>
<string name="fedibird_reactions">リアクション (Fedibird)</string>
<string name="reactioned_posts">リアクションした投稿</string>
<string name="reactions">リアクション</string>
<string name="cant_reaction_remote_custom_emoji">リモートのカスタム絵文字でリアクションできません %1$s</string>
<string name="keep_reaction_space">リアクション表示用の空間を確保する</string>
@ -1090,5 +1090,5 @@
<string name="cant_save_duplicated_keyword">既に存在するキーワードを重複保存できません</string>
<string name="discard_changes">変更を破棄しますか?</string>
<string name="saved">保存しました</string>
<string name="not_available_for_current_accounts">この機能を利用できるアカウントがありません</string>
</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="push_notification_filter">Push notification filter (Mastodon 3.4.0+, requires update push subscription)</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="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>
@ -1103,4 +1103,6 @@
<string name="cant_save_duplicated_keyword">Can\'t save duplicated keyword.</string>
<string name="discard_changes">Discard changes?</string>
<string name="saved">saved.</string>
<string name="not_available_for_current_accounts">Unavailable for current accounts</string>
</resources>