「会話と参照」カラムを表示する
This commit is contained in:
parent
eeedf742a6
commit
406ce8e5c5
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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")),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) &&
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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 = "?"
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue