diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt index 301bf4ba..fc56afee 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt @@ -1,7 +1,8 @@ package jp.juggler.subwaytooter.action import android.content.Context -import jp.juggler.subwaytooter.* +import jp.juggler.subwaytooter.ActMain +import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.actmain.addColumn import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.entity.* @@ -14,7 +15,6 @@ import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.util.matchHost import jp.juggler.subwaytooter.util.openCustomTab import jp.juggler.util.* -import java.util.* private val log = LogCategory("Action_Conversation") @@ -45,7 +45,12 @@ fun ActMain.clickConversation( } // プレビューカードのイメージは返信かもしれない -fun ActMain.clickCardImage(pos: Int, accessInfo: SavedAccount, card: TootCard?, longClick: Boolean = false) { +fun ActMain.clickCardImage( + pos: Int, + accessInfo: SavedAccount, + card: TootCard?, + longClick: Boolean = false, +) { card ?: return card.originalStatus?.let { if (longClick) { @@ -80,7 +85,7 @@ fun ActMain.clickReplyInfo( // tootsearchは返信元のIDを取得するのにひと手間必要 columnType == ColumnType.SEARCH_TS || - columnType == ColumnType.SEARCH_NOTESTOCK -> + columnType == ColumnType.SEARCH_NOTESTOCK -> conversationFromTootsearch(pos, statusShowing) else -> @@ -140,14 +145,26 @@ fun ActMain.conversationLocal( pos: Int, accessInfo: SavedAccount, statusId: EntityId, -) = addColumn(pos, accessInfo, ColumnType.CONVERSATION, statusId) + isReference: Boolean = false, +) = addColumn( + pos, + accessInfo, + when { + isReference && TootInstance.getCached(accessInfo)?.canUseReference == true -> + ColumnType.CONVERSATION_WITH_REFERENCE + else -> + ColumnType.CONVERSATION + }, + statusId, +) private val reDetailedStatusTime = """]*?\bdetailed-status__datetime\b[^>]*href="https://[^/]+/@[^/]+/([^\s?#/"]+)""" .toRegex() -private val reHeaderOgUrl = """""" - .toRegex() +private val reHeaderOgUrl = + """""" + .toRegex() // 疑似アカウントではURLからIDを取得するのにHTMLと正規表現を使う suspend fun guessStatusIdFromPseudoAccount( @@ -190,7 +207,8 @@ private fun ActMain.conversationRemote( ) { client -> if (accessInfo.isPseudo) { // 疑似アカウントではURLからIDを取得するのにHTMLと正規表現を使う - val pair = guessStatusIdFromPseudoAccount(applicationContext, client, remoteStatusUrl) + val pair = + guessStatusIdFromPseudoAccount(applicationContext, client, remoteStatusUrl) localStatusId = pair.second pair.first } else { @@ -218,6 +236,7 @@ fun ActMain.conversationOtherInstance( statusIdOriginal: EntityId? = null, hostAccess: Host? = null, statusIdAccess: EntityId? = null, + isReference: Boolean = false, ) { val activity = this @@ -226,7 +245,8 @@ fun ActMain.conversationOtherInstance( val hostOriginal = Host.parse(url.toUri().authority ?: "") // 選択肢:ブラウザで表示する - dialog.addAction(getString(R.string.open_web_on_host, hostOriginal.pretty)) { openCustomTab(url) } + dialog.addAction(getString(R.string.open_web_on_host, + hostOriginal.pretty)) { openCustomTab(url) } // トゥートの投稿元タンスにあるアカウント val localAccountList = ArrayList() @@ -263,7 +283,7 @@ fun ActMain.conversationOtherInstance( ) { launchMain { addPseudoAccount(hostOriginal)?.let { sa -> - conversationLocal(pos, sa, statusIdOriginal) + conversationLocal(pos, sa, statusIdOriginal, isReference = isReference) } } } @@ -290,7 +310,7 @@ fun ActMain.conversationOtherInstance( R.string.open_in_account, a.acct ) - ) { conversationLocal(pos, a, statusIdOriginal) } + ) { conversationLocal(pos, a, statusIdOriginal, isReference = isReference) } } } @@ -304,7 +324,7 @@ fun ActMain.conversationOtherInstance( R.string.open_in_account, a.acct ) - ) { conversationLocal(pos, a, statusIdAccess) } + ) { conversationLocal(pos, a, statusIdAccess, isReference = isReference) } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt index cb199b19..a6d49ed3 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt @@ -55,7 +55,8 @@ fun ActMain.handleOtherUri(uri: Uri): Boolean { statusInfo.url, statusInfo.statusId, statusInfo.host, - statusInfo.statusId + statusInfo.statusId, + isReference = statusInfo.isReference, ) return true } 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 d5604bbe..5b2d777d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/ApiPath.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/ApiPath.kt @@ -40,8 +40,6 @@ object ApiPath { // リストではなくオブジェクトを返すAPI const val PATH_STATUSES = "/api/v1/statuses/%s" // 1:status_id - const val PATH_STATUSES_CONTEXT = "/api/v1/statuses/%s/context" // 1:status_id - // search args 1: query(urlencoded) , also, append "&resolve=1" if resolve non-local accounts const val PATH_FILTERS = "/api/v1/filters" diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.kt index c2f56666..c7fddf21 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.kt @@ -4,14 +4,16 @@ import jp.juggler.subwaytooter.api.TootParser import jp.juggler.util.JsonObject class TootContext( - // The ancestors of the status in the conversation, as a list of Statuses + // The ancestors of the status in the conversation, as a list of Statuses val ancestors: ArrayList?, // descendants The descendants of the status in the conversation, as a list of Statuses val descendants: ArrayList?, + // fedibird: 参照 + val references: ArrayList?, ) { constructor(parser: TootParser, src: JsonObject) : this( ancestors = parseListOrNull(::TootStatus, parser, src.jsonArray("ancestors")), - descendants = parseListOrNull(::TootStatus, parser, src.jsonArray("descendants")) - + descendants = parseListOrNull(::TootStatus, parser, src.jsonArray("descendants")), + references = parseListOrNull(::TootStatus, parser, src.jsonArray("references")), ) } 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 5b4c8805..828f3c97 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 @@ -10,8 +10,12 @@ import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.VersionString import jp.juggler.util.* import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.withTimeout import okhttp3.Request import java.util.regex.Pattern +import kotlin.coroutines.Continuation +import kotlin.coroutines.suspendCoroutine import kotlin.math.max // インスタンスの種別 @@ -220,16 +224,9 @@ class TootInstance(parser: TootParser, src: JsonObject) { } class Stats(src: JsonObject) { - - val user_count: Long - val status_count: Long - val domain_count: Long - - init { - this.user_count = src.long("user_count") ?: -1L - this.status_count = src.long("status_count") ?: -1L - this.domain_count = src.long("domain_count") ?: -1L - } + val user_count = src.long("user_count") ?: -1L + val status_count = src.long("status_count") ?: -1L + val domain_count = src.long("domain_count") ?: -1L } val misskeyVersion: Int @@ -239,6 +236,9 @@ class TootInstance(parser: TootParser, src: JsonObject) { else -> 10 } + val canUseReference: Boolean? + get() = fedibird_capabilities?.contains("status_reference") + fun versionGE(check: VersionString): Boolean { if (decoded_version.isEmpty || check.isEmpty) return false val i = VersionString.compare(decoded_version, check) @@ -246,6 +246,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { } companion object { + private val log = LogCategory("TootInstance") private val rePleroma = """\bpleroma\b""".asciiPattern(Pattern.CASE_INSENSITIVE) private val rePixelfed = """\bpixelfed\b""".asciiPattern(Pattern.CASE_INSENSITIVE) @@ -375,26 +376,33 @@ class TootInstance(parser: TootParser, src: JsonObject) { } // マストドンのインスタンス情報を読めたら、それはマストドンのインスタンス - val r1 = getInstanceInformationMastodon(forceAccessToken) ?: return null - if (r1.jsonObject != null) return r1 - - return r1 // ホワイトリストモードの問題があるのでマストドン側のエラーを返す + // インスタンス情報を読めない場合もホワイトリストモードの問題があるので + // マストドン側のエラーを返す + return getInstanceInformationMastodon(forceAccessToken) } + /** + * TootInstance.get() のエラー戻り値を作る + */ + private fun tiError(errMsg: String) = + Pair(null, TootApiResult(errMsg)) + + /** + * サーバ情報リクエスト + * - ホスト別のキューで実行する + */ class QueuedRequest( + var cont: Continuation>, val allowPixelfed: Boolean, val get: suspend (cached: TootInstance?) -> Pair, + ) + + /** + * ホスト別のインスタンス情報キャッシュと処理キュー + */ + class CacheEntry( + val hostLower: String, ) { - val result = Channel>() - } - - private fun queuedRequest( - allowPixelfed: Boolean, - get: suspend (cached: TootInstance?) -> Pair, - ) = QueuedRequest(allowPixelfed, get) - - // インスタンス情報のキャッシュ。同期オブジェクトを兼ねる - class CacheEntry { // インスタンス情報のキャッシュ var cacheData: TootInstance? = null @@ -402,32 +410,38 @@ class TootInstance(parser: TootParser, src: JsonObject) { val requestQueue = Channel(capacity = Channel.UNLIMITED) private suspend fun handleRequest(req: QueuedRequest) = try { - val pair = req.get(cacheData) - - pair.first?.let { cacheData = it } + val qrr = req.get(cacheData) + qrr.first?.let { cacheData = it } when { - pair.first?.instanceType == InstanceType.Pixelfed && + qrr.first?.instanceType == InstanceType.Pixelfed && !PrefB.bpEnablePixelfed() && !req.allowPixelfed -> - Pair( - null, TootApiResult("currently Pixelfed instance is not supported.") - ) - - else -> pair + tiError("currently Pixelfed instance is not supported.") + else -> qrr } } catch (ex: Throwable) { - Pair( - null, - TootApiResult(ex.withCaption("can't get server information.")) - ) + log.e(ex, "handleRequest failed.") + tiError(ex.withCaption("can't get server information.")) } init { launchDefault { while (true) { - for (req in requestQueue) { - req.result.send(handleRequest(req)) + try { + val req = requestQueue.receive() + val r = try { + withTimeout(30000L) { + handleRequest(req) + } + } catch (ex: Throwable) { + log.trace(ex, "handleRequest failed.") + tiError(ex.withCaption("handleRequest failed.")) + } + runCatching { req.cont.resumeWith(Result.success(r)) } + } catch (ex: Throwable) { + log.trace(ex, "requestQueue.take failed.") + delay(3000L) } } } @@ -441,7 +455,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { val hostLower = ascii.lowercase() var item = _hostCache[hostLower] if (item == null) { - item = CacheEntry() + item = CacheEntry(hostLower) _hostCache[hostLower] = item } item @@ -463,86 +477,99 @@ class TootInstance(parser: TootParser, src: JsonObject) { forceUpdate: Boolean = false, forceAccessToken: String? = null, // マストドンのwhitelist modeでアカウント追加時に必要 ): Pair { + try { + val cacheEntry = (hostArg ?: account?.apiHost ?: client.apiHost)?.getCacheEntry() + ?: return tiError("missing host.") - val cacheEntry = (hostArg ?: account?.apiHost ?: client.apiHost)?.getCacheEntry() - ?: return Pair(null, TootApiResult("missing host.")) + return withTimeout(30000L) { + suspendCoroutine { cont -> + QueuedRequest(cont, allowPixelfed) { cached -> - // ホスト名ごとに用意したオブジェクトで同期する - return queuedRequest(allowPixelfed) { cached -> + // may use cached item. + if (!forceUpdate && forceAccessToken == null && cached != null) { + val now = SystemClock.elapsedRealtime() + if (now - cached.time_parse <= EXPIRE) { + return@QueuedRequest Pair(cached, TootApiResult()) + } + } - // may use cached item. - if (!forceUpdate && forceAccessToken == null && cached != null) { - val now = SystemClock.elapsedRealtime() - if (now - cached.time_parse <= EXPIRE) { - return@queuedRequest Pair(cached, TootApiResult()) - } - } + val tmpInstance = client.apiHost + val tmpAccount = client.account - val tmpInstance = client.apiHost - val tmpAccount = client.account + val linkHelper: LinkHelper? - val linkHelper: LinkHelper? + // get new information + val result = when { - // get new information - val result = when { + // ストリームマネジャから呼ばれる + account != null -> try { + linkHelper = account + client.account = account // this may change client.apiHost + if (account.isMisskey) { + client.getInstanceInformationMisskey() + } else { + client.getInstanceInformationMastodon() + } + } finally { + client.account = tmpAccount + client.apiHost = tmpInstance // must be last. + } - // ストリームマネジャから呼ばれる - account != null -> try { - linkHelper = account - client.account = account // this may change client.apiHost - if (account.isMisskey) { - client.getInstanceInformationMisskey() - } else { - client.getInstanceInformationMastodon() + // サーバ情報カラムやProfileDirectoryを開く場合 + hostArg != null && hostArg != tmpInstance -> try { + linkHelper = null + client.account = null // don't use access token. + client.apiHost = hostArg + client.getInstanceInformation() + } finally { + client.account = tmpAccount + client.apiHost = tmpInstance // must be last. + } + + // client にすでにあるアクセス情報でサーバ情報を取得する + // マストドンのホワイトリストモード用にアクセストークンを指定できる + else -> { + linkHelper = client.account // may null + client.getInstanceInformation( + forceAccessToken = forceAccessToken + ) + } + } + + val json = result?.jsonObject + ?: return@QueuedRequest Pair(null, result) + + val item = parseItem( + ::TootInstance, + TootParser( + client.context, + linkHelper = linkHelper ?: LinkHelper.create( + (hostArg ?: client.apiHost)!!, + misskeyVersion = parseMisskeyVersion(json) + ) + ), + json + ) ?: return@QueuedRequest Pair( + null, + result.setError("instance information parse error.") + ) + + Pair(item, result) + }.let { + val result = cacheEntry.requestQueue.trySend(it) + when { + // 誰も閉じないので発生しない + result.isClosed -> error("cacheEntry.requestQueue closed") + // capacity=UNLIMITEDなので発生しない + result.isFailure -> error("cacheEntry.requestQueue failed") + } } - } finally { - client.account = tmpAccount - client.apiHost = tmpInstance // must be last. - } - - // サーバ情報カラムやProfileDirectoryを開く場合 - hostArg != null && hostArg != tmpInstance -> try { - linkHelper = null - client.account = null // don't use access token. - client.apiHost = hostArg - client.getInstanceInformation() - } finally { - client.account = tmpAccount - client.apiHost = tmpInstance // must be last. - } - - // client にすでにあるアクセス情報でサーバ情報を取得する - // マストドンのホワイトリストモード用にアクセストークンを指定できる - else -> { - linkHelper = client.account // may null - client.getInstanceInformation( - forceAccessToken = forceAccessToken - ) } } - - val json = result?.jsonObject - ?: return@queuedRequest Pair(null, result) - - val item = parseItem( - ::TootInstance, - TootParser( - client.context, - linkHelper = linkHelper ?: LinkHelper.create( - (hostArg ?: client.apiHost)!!, - misskeyVersion = parseMisskeyVersion(json) - ) - ), - json - ) ?: return@queuedRequest Pair( - null, - result.setError("instance information parse error.") - ) - - Pair(item, result) + } catch (ex: Throwable) { + log.w(ex, "getEx failed.") + return tiError(ex.withCaption("can't get instance information")) } - .also { cacheEntry.requestQueue.send(it) } - .result.receive() } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt index 2046648e..d45029cb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt @@ -2,26 +2,25 @@ package jp.juggler.subwaytooter.api.entity import android.annotation.SuppressLint import android.content.Context -import androidx.annotation.StringRes import android.text.Spannable import android.text.SpannableString +import androidx.annotation.StringRes import jp.juggler.subwaytooter.App1 -import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.R -import jp.juggler.subwaytooter.api.* +import jp.juggler.subwaytooter.api.TootAccountMap +import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.mfm.SpannableStringBuilderEx +import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.table.HighlightWord import jp.juggler.subwaytooter.table.SavedAccount -import jp.juggler.subwaytooter.util.* +import jp.juggler.subwaytooter.util.DecodeOptions +import jp.juggler.subwaytooter.util.HTMLDecoder import jp.juggler.util.* import java.lang.ref.WeakReference import java.text.SimpleDateFormat import java.util.* import java.util.regex.Pattern -import kotlin.collections.ArrayList -import kotlin.collections.HashMap -import kotlin.jvm.Throws import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -1079,8 +1078,8 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { val statusId: EntityId?, // may null hostArg: String, val url: String, + val isReference: Boolean = false, ) { - val host = Host.parse(hostArg) } @@ -1117,6 +1116,11 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { private val reStatusPage = """\Ahttps://([^/]+)/@(\w+)/([^?#/\s]+)(?:\z|[?#])""" .asciiPattern() + // fedibird ステータスの参照のURL + private val reStatusWithReference = + """\Ahttps://([^/]+)/@(\w+)/([^?#/\s]+)/references(?:\z|[?#])""" + .asciiPattern() + // 公開ステータスページのURL Misskey internal val reStatusPageMisskey = """\Ahttps://([^/]+)/notes/([0-9a-f]{24}|[0-9a-z]{10})\b""" @@ -1138,8 +1142,20 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { // returns null or pair( status_id, host ,url ) fun String.findStatusIdFromUrl(): FindStatusIdFromUrlResult? { + + // https://fedibird.com/@noellabo/108730353756004469/references + var m = reStatusWithReference.matcher(this) + if (m.find()) { + return FindStatusIdFromUrlResult( + EntityId(m.groupEx(3)!!), + m.groupEx(1)!!, + this, + isReference = true, + ) + } + // https://mastodon.juggler.jp/@SubwayTooter/(status_id) - var m = reStatusPage.matcher(this) + m = reStatusPage.matcher(this) if (m.find()) { return FindStatusIdFromUrlResult(EntityId(m.groupEx(3)!!), m.groupEx(1)!!, this) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnEncoder.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnEncoder.kt index b91d30b5..6ef3a397 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnEncoder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnEncoder.kt @@ -7,7 +7,6 @@ import jp.juggler.util.JsonObject import jp.juggler.util.encodeBase64Url import java.lang.ref.WeakReference import java.nio.ByteBuffer -import java.util.* // カラムデータのJSONエンコーダ、デコーダ @@ -164,6 +163,7 @@ object ColumnEncoder { when (type) { ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, ColumnType.BOOSTED_BY, ColumnType.FAVOURITED_BY, ColumnType.LOCAL_AROUND, @@ -299,6 +299,7 @@ object ColumnEncoder { when (type) { ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, ColumnType.BOOSTED_BY, ColumnType.FAVOURITED_BY, ColumnType.LOCAL_AROUND, diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra1.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra1.kt index 298a4d24..fa29da9c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra1.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra1.kt @@ -2,7 +2,7 @@ package jp.juggler.subwaytooter.column import android.annotation.SuppressLint import android.view.View -import jp.juggler.subwaytooter.* +import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.entity.EntityId import jp.juggler.subwaytooter.api.entity.TimelineItem import jp.juggler.subwaytooter.api.entity.TootStatus @@ -32,6 +32,7 @@ fun Column.canReloadWhenRefreshTop(): Boolean = when (type) { ColumnType.SEARCH_TS, ColumnType.SEARCH_NOTESTOCK, ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, ColumnType.LIST_LIST, ColumnType.TREND_TAG, ColumnType.FOLLOW_SUGGESTION, @@ -52,6 +53,7 @@ fun Column.canRefreshTopBySwipe(): Boolean = canReloadWhenRefreshTop() || when (type) { ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, ColumnType.INSTANCE_INFORMATION, -> false else -> true @@ -61,6 +63,7 @@ fun Column.canRefreshTopBySwipe(): Boolean = fun Column.canRefreshBottomBySwipe(): Boolean = when (type) { ColumnType.LIST_LIST, ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, ColumnType.INSTANCE_INFORMATION, ColumnType.KEYWORD_FILTER, ColumnType.SEARCH, @@ -335,7 +338,9 @@ fun Column.startRefreshForPost( } } - ColumnType.CONVERSATION -> { + ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, + -> { // 会話への返信が行われたなら会話を更新する try { if (postedReplyId != null) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra2.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra2.kt index 0f112bde..7d60501d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra2.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra2.kt @@ -3,7 +3,7 @@ package jp.juggler.subwaytooter.column import android.content.Context import android.os.Environment import androidx.annotation.RawRes -import jp.juggler.subwaytooter.* +import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiResult import jp.juggler.subwaytooter.api.TootParser @@ -11,7 +11,6 @@ import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.columnviewholder.saveScrollPosition import jp.juggler.util.* import java.io.File -import java.util.* private val log = LogCategory("ColumnExtra2") @@ -61,6 +60,14 @@ val Column.isPublicStream: Boolean fun Column.canAutoRefresh() = !accessInfo.isNA && type.canAutoRefresh +val Column.isConversation + get() = when (type) { + ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, + -> true + else -> false + } + ///////////////////////////////////////////////////////////////////////////// // 読み込み処理の内部で使うメソッド @@ -116,7 +123,11 @@ fun Column.getNotificationTypeString(): String { return sb.toString() } -suspend fun Column.loadProfileAccount(client: TootApiClient, parser: TootParser, bForceReload: Boolean): TootApiResult? = +suspend fun Column.loadProfileAccount( + client: TootApiClient, + parser: TootParser, + bForceReload: Boolean, +): TootApiResult? = when { // リロード不要なら何もしない this.whoAccount != null && !bForceReload -> null diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnFilters.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnFilters.kt index 02d6d8aa..794f8d28 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnFilters.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnFilters.kt @@ -34,11 +34,18 @@ val Column.isFilterEnabled: Boolean // マストドン2.4.3rcのキーワードフィルタのコンテキスト fun Column.getFilterContext() = when (type) { - ColumnType.HOME, ColumnType.LIST_TL, ColumnType.MISSKEY_HYBRID -> TootFilter.CONTEXT_HOME + ColumnType.HOME, + ColumnType.LIST_TL, + ColumnType.MISSKEY_HYBRID, + -> TootFilter.CONTEXT_HOME - ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT -> TootFilter.CONTEXT_NOTIFICATIONS + ColumnType.NOTIFICATIONS, + ColumnType.NOTIFICATION_FROM_ACCT, + -> TootFilter.CONTEXT_NOTIFICATIONS - ColumnType.CONVERSATION -> TootFilter.CONTEXT_THREAD + ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, + -> TootFilter.CONTEXT_THREAD ColumnType.DIRECT_MESSAGES -> TootFilter.CONTEXT_THREAD @@ -76,7 +83,10 @@ fun Column.canFilterBoost(): Boolean = when (type) { -> true ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey ColumnType.HASHTAG_FROM_ACCT -> false - ColumnType.CONVERSATION, ColumnType.DIRECT_MESSAGES -> isMisskey + ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, + ColumnType.DIRECT_MESSAGES, + -> isMisskey else -> false } diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnSpec.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnSpec.kt index a3bfa027..4cada5bd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnSpec.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnSpec.kt @@ -52,13 +52,13 @@ object ColumnSpec { when (type) { ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, ColumnType.BOOSTED_BY, ColumnType.FAVOURITED_BY, ColumnType.LOCAL_AROUND, ColumnType.FEDERATED_AROUND, ColumnType.ACCOUNT_AROUND, - -> - statusId = getParamEntityId(params, 0) + -> statusId = getParamEntityId(params, 0) ColumnType.STATUS_HISTORY -> { statusId = getParamEntityId(params, 0) @@ -67,8 +67,7 @@ object ColumnSpec { ColumnType.PROFILE, ColumnType.LIST_TL, ColumnType.LIST_MEMBER, ColumnType.MISSKEY_ANTENNA_TL, - -> - profileId = getParamEntityId(params, 0) + -> profileId = getParamEntityId(params, 0) ColumnType.HASHTAG -> hashtag = getParamString(params, 0) @@ -124,18 +123,17 @@ object ColumnSpec { ColumnType.LIST_TL, ColumnType.LIST_MEMBER, ColumnType.MISSKEY_ANTENNA_TL, - -> - column.profileId == getParamEntityId(params, 0) + -> column.profileId == getParamEntityId(params, 0) ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, ColumnType.BOOSTED_BY, ColumnType.FAVOURITED_BY, ColumnType.LOCAL_AROUND, ColumnType.FEDERATED_AROUND, ColumnType.ACCOUNT_AROUND, ColumnType.STATUS_HISTORY, - -> - column.statusId == getParamEntityId(params, 0) + -> column.statusId == getParamEntityId(params, 0) ColumnType.HASHTAG -> { (getParamString(params, 0) == column.hashtag) && diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt index 21450040..b5f3a0df 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt @@ -1009,7 +1009,10 @@ class ColumnTask_Loading( return result } - suspend fun getConversation(client: TootApiClient): TootApiResult? { + suspend fun getConversation( + client: TootApiClient, + withReference: Boolean = false, + ): TootApiResult? { return if (isMisskey) { // 指定された発言そのもの val queryParams = column.makeMisskeyBaseParameter(parser).apply { @@ -1097,10 +1100,12 @@ class ColumnTask_Loading( // 前後の会話 result = client.request( - String.format( - Locale.JAPAN, - ApiPath.PATH_STATUSES_CONTEXT, column.statusId - ) + "/api/v1/statuses/${column.statusId}/context${ + when (withReference) { + true -> "?with_reference=true" + else -> "" + } + }" ) jsonObject = result?.jsonObject ?: return result val conversationContext = @@ -1116,6 +1121,10 @@ class ColumnTask_Loading( 1 ) + if (conversationContext.references != null) { + addWithFilterStatus(this.listTmp, conversationContext.references) + } + if (conversationContext.ancestors != null) { addWithFilterStatus(this.listTmp, conversationContext.ancestors) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnType.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnType.kt index b98ac107..a0b1de6c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnType.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnType.kt @@ -926,8 +926,18 @@ enum class ColumnType( canStreamingMastodon = streamingTypeNo, canStreamingMisskey = streamingTypeNo, + ), - ), + CONVERSATION_WITH_REFERENCE( + 47, + iconId = { R.drawable.ic_link }, + name1 = { it.getString(R.string.conversation_with_reference) }, + name2 = { context.getString(R.string.conversation_with_reference) }, + loading = { client -> getConversation(client, withReference = true) }, + + canStreamingMastodon = streamingTypeNo, + canStreamingMisskey = streamingTypeNo, + ), HASHTAG( 9, @@ -2059,9 +2069,6 @@ enum class ColumnType( ; - private fun getFollowedHashtags(client: TootApiClient) { - } - init { val old = Column.typeMap[id] if (id > 0 && old != null) error("ColumnType: duplicate id $id. name=$name, ${old.name}") diff --git a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderLifecycle.kt b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderLifecycle.kt index 091d626d..66c78872 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderLifecycle.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderLifecycle.kt @@ -118,8 +118,7 @@ fun ColumnViewHolder.onPageCreate(column: Column, pageIdx: Int, pageCount: Int) ColumnViewHolder.log.d("onPageCreate [$pageIdx] ${column.getColumnName(true)}") - val bSimpleList = - column.type != ColumnType.CONVERSATION && PrefB.bpSimpleList(activity.pref) + val bSimpleList = !column.isConversation && PrefB.bpSimpleList(activity.pref) tvColumnIndex.text = activity.getString(R.string.column_index, pageIdx + 1, pageCount) tvColumnStatus.text = "?" diff --git a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderPreviewCard.kt b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderPreviewCard.kt index 9c2cdba7..d671dcc2 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderPreviewCard.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderPreviewCard.kt @@ -3,7 +3,7 @@ package jp.juggler.subwaytooter.itemviewholder import android.view.View import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.entity.TootStatus -import jp.juggler.subwaytooter.column.ColumnType +import jp.juggler.subwaytooter.column.isConversation import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefS import jp.juggler.subwaytooter.table.MediaShown @@ -53,7 +53,7 @@ fun ItemViewHolder.showPreviewCard(status: TootStatus) { val card = status.card ?: return // 会話カラムで返信ステータスなら捏造したカードを表示しない - if (column.type == ColumnType.CONVERSATION && + if (column.isConversation && card.originalStatus != null && status.reply != null ) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderShow.kt b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderShow.kt index 99df1d7b..05ec8208 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderShow.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderShow.kt @@ -648,26 +648,11 @@ fun ItemViewHolder.showStatusTime( } if (sb.isNotEmpty()) sb.append(' ') + sb.append( - when { - time != null -> TootStatus.formatTime( - activity, - time, - when (column.type) { - ColumnType.CONVERSATION, ColumnType.STATUS_HISTORY -> false - else -> true - } - ) - status != null -> TootStatus.formatTime( - activity, - status.time_created_at, - when (column.type) { - ColumnType.CONVERSATION, ColumnType.STATUS_HISTORY -> false - else -> true - } - ) - else -> "?" - } + (time ?: status?.time_created_at)?.let { + TootStatus.formatTime(activity, it, column.canRelativeTime) + } ?: "?" ) tv.text = sb @@ -703,16 +688,20 @@ fun ItemViewHolder.showStatusTimeScheduled( } if (sb.isNotEmpty()) sb.append(' ') - sb.append( - TootStatus.formatTime( - activity, - item.timeScheduledAt, - column.type != ColumnType.CONVERSATION - ) - ) + sb.append(TootStatus.formatTime(activity, item.timeScheduledAt, column.canRelativeTime)) tv.text = sb } + +val Column.canRelativeTime + get() = when (type) { + ColumnType.CONVERSATION, + ColumnType.CONVERSATION_WITH_REFERENCE, + ColumnType.STATUS_HISTORY, + -> false + else -> true + } + // fun updateRelativeTime() { // val boost_time = this.boost_time // if(boost_time != 0L) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderShowStatus.kt b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderShowStatus.kt index 7772132b..0f159fb9 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderShowStatus.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderShowStatus.kt @@ -11,6 +11,7 @@ import jp.juggler.subwaytooter.actmain.checkAutoCW import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.column.Column import jp.juggler.subwaytooter.column.ColumnType +import jp.juggler.subwaytooter.column.isConversation import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefI import jp.juggler.subwaytooter.table.ContentWarning @@ -222,14 +223,14 @@ private fun ItemViewHolder.showApplicationAndLanguage(status: TootStatus) { val application = status.application if (application != null && - (column.type == ColumnType.CONVERSATION || PrefB.bpShowAppName(activity.pref)) + (column.isConversation || PrefB.bpShowAppName(activity.pref)) ) { prepareSb().append(activity.getString(R.string.application_is, application.name ?: "")) } val language = status.language if (language != null && - (column.type == ColumnType.CONVERSATION || PrefB.bpShowLanguage(activity.pref)) + (column.isConversation || PrefB.bpShowLanguage(activity.pref)) ) { prepareSb().append(activity.getString(R.string.language_is, language)) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AppOpener.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AppOpener.kt index 1ce1f163..fc1e3ee2 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AppOpener.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AppOpener.kt @@ -7,7 +7,9 @@ import android.content.SharedPreferences import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent -import jp.juggler.subwaytooter.* +import jp.juggler.subwaytooter.ActCallback +import jp.juggler.subwaytooter.ActMain +import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.action.conversationLocal import jp.juggler.subwaytooter.action.conversationOtherInstance import jp.juggler.subwaytooter.action.tagDialog @@ -20,7 +22,6 @@ import jp.juggler.subwaytooter.pref.pref import jp.juggler.subwaytooter.span.LinkInfo import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.* -import java.util.* // Subway Tooterの「アプリ設定/挙動/リンクを開く際にCustom Tabsを使わない」をONにして // 投稿のコンテキストメニューの「トゥートへのアクション/Webページを開く」「ユーザへのアクション/Webページを開く」を使うと @@ -189,7 +190,7 @@ fun openCustomTab( tagList: ArrayList? = null, allowIntercept: Boolean = true, whoRef: TootAccountRef? = null, - linkInfo: LinkInfo? = null + linkInfo: LinkInfo? = null, ) { try { log.d("openCustomTab: $url") @@ -219,23 +220,41 @@ fun openCustomTab( val statusInfo = url.findStatusIdFromUrl() if (statusInfo != null) { - if (accessInfo.isNA || - statusInfo.statusId == null || - !accessInfo.matchHost(statusInfo.host) - ) { - activity.conversationOtherInstance( - pos, - statusInfo.url, - statusInfo.statusId, - statusInfo.host, - statusInfo.statusId - ) - } else { - activity.conversationLocal( - pos, - accessInfo, - statusInfo.statusId - ) + when { + // fedibirdの参照のURLだった && 閲覧アカウントが参照を扱える + // 参照カラムを開く + statusInfo.statusId != null && + statusInfo.isReference && + TootInstance.getCached(accessInfo)?.canUseReference == true -> + activity.conversationLocal( + pos, + accessInfo, + statusInfo.statusId, + isReference = statusInfo.isReference, + ) + + // 疑似アカウント? + // 別サーバ? + // ステータスIDがない?(Pleroma) + accessInfo.isNA || + !accessInfo.matchHost(statusInfo.host) || + statusInfo.statusId == null -> + activity.conversationOtherInstance( + pos, + statusInfo.url, + statusInfo.statusId, + statusInfo.host, + statusInfo.statusId, + isReference = statusInfo.isReference, + ) + + else -> + activity.conversationLocal( + pos, + accessInfo, + statusInfo.statusId, + isReference = statusInfo.isReference, + ) } return } @@ -248,7 +267,8 @@ fun openCustomTab( if (fullAcct.host != null) { when (fullAcct.host.ascii) { "github.com", - "twitter.com" -> + "twitter.com", + -> activity.openCustomTab(mention.url) "gmail.com" -> activity.openBrowser("mailto:${fullAcct.pretty}") diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml new file mode 100644 index 00000000..346a9f7b --- /dev/null +++ b/app/src/main/res/drawable/ic_link.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 784f777d..a09bbcc3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1154,4 +1154,5 @@ フォロー中のハッシュタグ \"%1$s\"のフォロー \"%1$s\"のフォロー解除 + 会話と参照 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6aba2229..7f820218 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1163,4 +1163,5 @@ Followed hashtags Follow %1$s Unfollow %1$s + conversation + reference