「会話と参照」カラムを表示する

This commit is contained in:
tateisu 2022-08-02 11:41:05 +09:00
parent eeedf742a6
commit 406ce8e5c5
21 changed files with 337 additions and 216 deletions

View File

@ -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 =
"""<a\b[^>]*?\bdetailed-status__datetime\b[^>]*href="https://[^/]+/@[^/]+/([^\s?#/"]+)"""
.toRegex()
private val reHeaderOgUrl = """<meta\s+content="https://[^/"]+/notice/([^/"]+)"\s+property="og:url"/?>"""
.toRegex()
private val reHeaderOgUrl =
"""<meta\s+content="https://[^/"]+/notice/([^/"]+)"\s+property="og:url"/?>"""
.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<SavedAccount>()
@ -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) }
}
}

View File

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

View File

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

View File

@ -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<TootStatus>?,
// descendants The descendants of the status in the conversation, as a list of Statuses
val descendants: ArrayList<TootStatus>?,
// fedibird: 参照
val references: ArrayList<TootStatus>?,
) {
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")),
)
}

View File

@ -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<TootInstance?, TootApiResult?>(null, TootApiResult(errMsg))
/**
* サーバ情報リクエスト
* - ホスト別のキューで実行する
*/
class QueuedRequest(
var cont: Continuation<Pair<TootInstance?, TootApiResult?>>,
val allowPixelfed: Boolean,
val get: suspend (cached: TootInstance?) -> Pair<TootInstance?, TootApiResult?>,
)
/**
* ホスト別のインスタンス情報キャッシュと処理キュー
*/
class CacheEntry(
val hostLower: String,
) {
val result = Channel<Pair<TootInstance?, TootApiResult?>>()
}
private fun queuedRequest(
allowPixelfed: Boolean,
get: suspend (cached: TootInstance?) -> Pair<TootInstance?, TootApiResult?>,
) = QueuedRequest(allowPixelfed, get)
// インスタンス情報のキャッシュ。同期オブジェクトを兼ねる
class CacheEntry {
// インスタンス情報のキャッシュ
var cacheData: TootInstance? = null
@ -402,32 +410,38 @@ class TootInstance(parser: TootParser, src: JsonObject) {
val requestQueue = Channel<QueuedRequest>(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<TootInstance?, TootApiResult?> {
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()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = "?"

View File

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

View File

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

View File

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

View File

@ -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<String>? = 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}")

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17,7h-4v2h4c1.65,0 3,1.35 3,3s-1.35,3 -3,3h-4v2h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5zM11,15L7,15c-1.65,0 -3,-1.35 -3,-3s1.35,-3 3,-3h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-2zM8,11h8v2L8,13z"/>
</vector>

View File

@ -1154,4 +1154,5 @@
<string name="followed_tags">フォロー中のハッシュタグ</string>
<string name="follow_hashtag_of">\"%1$s\"のフォロー</string>
<string name="unfollow_hashtag_of">\"%1$s\"のフォロー解除</string>
<string name="conversation_with_reference">会話と参照</string>
</resources>

View File

@ -1163,4 +1163,5 @@
<string name="followed_tags">Followed hashtags</string>
<string name="follow_hashtag_of">Follow %1$s</string>
<string name="unfollow_hashtag_of">Unfollow %1$s</string>
<string name="conversation_with_reference">conversation + reference</string>
</resources>