メンションの入力補完でIDNドメインを選択するとpunycodeを入力する。MMFパーサはデコード結果にIDNドメインを使う。

This commit is contained in:
tateisu 2020-02-04 06:14:40 +09:00
parent 36b86e4471
commit 6ae0ef4df9
5 changed files with 87 additions and 89 deletions

View File

@ -744,7 +744,8 @@ class ActPost : AppCompatActivity(),
sv = intent.getStringExtra(KEY_REPLY_STATUS) sv = intent.getStringExtra(KEY_REPLY_STATUS)
if(sv != null && account != null) { if(sv != null && account != null) {
try { try {
val reply_status = TootParser(this@ActPost, account).status(sv.decodeJsonObject()) val reply_status =
TootParser(this@ActPost, account).status(sv.decodeJsonObject())
val isQuoterRenote = intent.getBooleanExtra(KEY_QUOTED_RENOTE, false) val isQuoterRenote = intent.getBooleanExtra(KEY_QUOTED_RENOTE, false)
@ -764,35 +765,28 @@ class ActPost : AppCompatActivity(),
} }
// 新しいメンションリスト // 新しいメンションリスト
val mention_list = ArrayList<String>() val mention_list = ArrayList<Acct>()
// 自己レス以外なら元レスへのメンションを追加 // 自己レス以外なら元レスへのメンションを追加
// 最初に追加する https://github.com/tateisu/SubwayTooter/issues/94 // 最初に追加する https://github.com/tateisu/SubwayTooter/issues/94
if(! account.isMe(reply_status.account)) { if(! account.isMe(reply_status.account)) {
mention_list.add("@${account.getFullAcct(reply_status.account).pretty}") mention_list.add(account.getFullAcct(reply_status.account))
} }
// 元レスに含まれていたメンションを複製 // 元レスに含まれていたメンションを複製
reply_status.mentions?.forEach { mention -> reply_status.mentions?.forEach { mention ->
val who_acct = mention.acct val who_acct = mention.acct
// 空データなら追加しない // 空データなら追加しない
if( ! who_acct.isValid ) return@forEach if(! who_acct.isValid) return@forEach
// 自分なら追加しない // 自分なら追加しない
if(account.isMe(who_acct)) return@forEach if(account.isMe(who_acct)) return@forEach
// 既出なら追加しない
val strMention = "@${account.getFullAcct(who_acct).pretty}"
if(mention_list.contains(strMention)) return@forEach
mention_list.add(strMention)
/*
FIXME インスタンスのバージョンが3.1.0 以降ならメンションのホスト部分にIDNドメインを使いたいが
投稿画面でのアカウント切り替え時にタンスのバージョンが異なると破綻する可能性が高い
*/
// 既出でないなら追加する
val acct = account.getFullAcct(who_acct)
if(! mention_list.contains(acct)) mention_list.add(acct)
} }
if(mention_list.isNotEmpty()) { if(mention_list.isNotEmpty()) {
@ -800,7 +794,7 @@ class ActPost : AppCompatActivity(),
StringBuilder().apply { StringBuilder().apply {
for(acct in mention_list) { for(acct in mention_list) {
if(isNotEmpty()) append(' ') if(isNotEmpty()) append(' ')
append(acct) append("@${acct.ascii}")
} }
append(' ') append(' ')
}.toString() }.toString()
@ -852,7 +846,8 @@ class ActPost : AppCompatActivity(),
sv = intent.getStringExtra(KEY_REDRAFT_STATUS) sv = intent.getStringExtra(KEY_REDRAFT_STATUS)
if(sv != null && account != null) { if(sv != null && account != null) {
try { try {
val base_status = TootParser(this@ActPost, account).status(sv.decodeJsonObject()) val base_status =
TootParser(this@ActPost, account).status(sv.decodeJsonObject())
if(base_status != null) { if(base_status != null) {
redraft_status_id = base_status.id redraft_status_id = base_status.id
@ -1060,12 +1055,12 @@ class ActPost : AppCompatActivity(),
val array = attachment_list val array = attachment_list
// アップロード完了したものだけ保持する // アップロード完了したものだけ保持する
.filter{it.status == PostAttachment.STATUS_UPLOADED } .filter { it.status == PostAttachment.STATUS_UPLOADED }
.mapNotNull { it.attachment?.encodeJson() } .mapNotNull { it.attachment?.encodeJson() }
.toJsonArray() .toJsonArray()
.notEmpty() .notEmpty()
if(array != null ) outState.putString(KEY_ATTACHMENT_LIST, array.toString()) if(array != null) outState.putString(KEY_ATTACHMENT_LIST, array.toString())
in_reply_to_id?.putTo(outState, KEY_IN_REPLY_TO_ID) in_reply_to_id?.putTo(outState, KEY_IN_REPLY_TO_ID)
outState.putString(KEY_IN_REPLY_TO_TEXT, in_reply_to_text) outState.putString(KEY_IN_REPLY_TO_TEXT, in_reply_to_text)
@ -1511,7 +1506,7 @@ class ActPost : AppCompatActivity(),
) { ai -> ) { ai ->
// 別タンスのアカウントに変更したならならin_reply_toの変換が必要 // 別タンスのアカウントに変更したならならin_reply_toの変換が必要
if(in_reply_to_id != null && ! ai.matchHost(account?.host) ) { if(in_reply_to_id != null && ! ai.matchHost(account?.host)) {
startReplyConversion(ai) startReplyConversion(ai)
} else { } else {
setAccountWithVisibilityConversion(ai) setAccountWithVisibilityConversion(ai)
@ -2672,7 +2667,7 @@ class ActPost : AppCompatActivity(),
try { try {
val tmp_attachment_list = attachment_list val tmp_attachment_list = attachment_list
.mapNotNull{it.attachment?.encodeJson()} .mapNotNull { it.attachment?.encodeJson() }
.toJsonArray() .toJsonArray()
val json = JsonObject() val json = JsonObject()

View File

@ -632,7 +632,9 @@ object Action_User {
// メンションを含むトゥートを作る // メンションを含むトゥートを作る
private fun mention( private fun mention(
activity : ActMain, account : SavedAccount, initial_text : String activity : ActMain,
account : SavedAccount,
initial_text : String
) { ) {
ActPost.open( ActPost.open(
activity, activity,
@ -646,7 +648,7 @@ object Action_User {
fun mention( fun mention(
activity : ActMain, account : SavedAccount, who : TootAccount activity : ActMain, account : SavedAccount, who : TootAccount
) { ) {
mention(activity, account, "@${account.getFullAcct(who).pretty} ") mention(activity, account, "@${account.getFullAcct(who).ascii} ")
} }
// メンションを含むトゥートを作る // メンションを含むトゥートを作る
@ -656,7 +658,7 @@ object Action_User {
if(who == null) return if(who == null) return
val who_host = who.host val who_host = who.host
val initial_text = "@${access_info.getFullAcct(who).pretty} " val initial_text = "@${access_info.getFullAcct(who).ascii} "
AccountPicker.pick( AccountPicker.pick(
activity, activity,
bAllowPseudo = false, bAllowPseudo = false,

View File

@ -8,7 +8,7 @@ import java.util.concurrent.ConcurrentHashMap
class Host private constructor( class Host private constructor(
val ascii : String, val ascii : String,
val pretty : String = ascii val pretty : String = ascii
) :Comparable<Host>{ ) : Comparable<Host> {
override fun toString() : String = ascii override fun toString() : String = ascii
@ -16,28 +16,28 @@ class Host private constructor(
ascii.hashCode() ascii.hashCode()
override fun equals(other : Any?) : Boolean = override fun equals(other : Any?) : Boolean =
if( other is Host) ascii==other.ascii else false if(other is Host) ascii == other.ascii else false
// ソートはprettyのコード順 // ソートはprettyのコード順
override fun compareTo(other : Host) : Int = override fun compareTo(other : Host) : Int =
pretty.compareTo(other.pretty).notZero() ?: ascii.compareTo(other.ascii) pretty.compareTo(other.pretty).notZero() ?: ascii.compareTo(other.ascii)
fun match(src : String?) : Boolean = fun match(src : String?) : Boolean =
ascii==src || pretty ==src ascii == src || pretty == src
val isValid :Boolean val isValid : Boolean
get() = ascii.isNotEmpty() && ascii != "?" get() = ascii.isNotEmpty() && ascii != "?"
fun valid() : Host? = if(isValid ) this else null fun valid() : Host? = if(isValid) this else null
companion object { companion object {
// declare this first! // declare this first!
private val hostSet = ConcurrentHashMap<String, Host>() private val hostSet = ConcurrentHashMap<String, Host>()
val EMPTY = Host("") val EMPTY = Host("")
val UNKNOWN = Host("?") val UNKNOWN = Host("?")
val FRIENDS_NICO = Host("friends.nico") val FRIENDS_NICO = Host("friends.nico")
fun parse(src : String) : Host { fun parse(src : String) : Host {
val cached = hostSet[src] val cached = hostSet[src]
if(cached != null) return cached if(cached != null) return cached
@ -53,17 +53,17 @@ class Host private constructor(
class Acct private constructor( class Acct private constructor(
val username : String, val username : String,
val host : Host? val host : Host?
) :Comparable<Acct>{ ) : Comparable<Acct> {
val ascii:String = if(host==null) username else "$username@${host.ascii}" val ascii : String = if(host == null) username else "$username@${host.ascii}"
val pretty:String = if(host==null) username else "$username@${host.pretty}" val pretty : String = if(host == null) username else "$username@${host.pretty}"
override fun toString() : String = ascii override fun toString() : String = ascii
override fun hashCode() : Int = ascii.hashCode() override fun hashCode() : Int = ascii.hashCode()
override fun equals(other : Any?) : Boolean = override fun equals(other : Any?) : Boolean =
if( other is Acct) ascii == other.ascii else false if(other is Acct) ascii == other.ascii else false
// ソートはprettyのコード順 // ソートはprettyのコード順
override fun compareTo(other : Acct) : Int { override fun compareTo(other : Acct) : Int {
@ -71,45 +71,45 @@ class Acct private constructor(
} }
fun followHost(accessHost : Host?) : Acct { fun followHost(accessHost : Host?) : Acct {
return if( this.host != null) this else Acct(username,accessHost) return if(this.host != null) this else Acct(username, accessHost)
} }
val isValid : Boolean val isValid : Boolean
get() = username.isNotEmpty() && username != "?" && (host?.isValid != false) get() = username.isNotEmpty() && username != "?" && (host?.isValid != false)
@Suppress("unused") @Suppress("unused")
private fun valid():Acct? = if(isValid) this else null private fun valid() : Acct? = if(isValid) this else null
private val isValidFull : Boolean private val isValidFull : Boolean
get() = username.isNotEmpty() && username != "?" && (host?.isValid == true) get() = username.isNotEmpty() && username != "?" && (host?.isValid == true)
fun validFull():Acct ? = if(isValidFull) this else null fun validFull() : Acct? = if(isValidFull) this else null
companion object { companion object {
// declare this first! // declare this first!
private val acctSet = ConcurrentHashMap<String, Acct>() private val acctSet = ConcurrentHashMap<String, Acct>()
val UNKNOWN = Acct("?",Host.UNKNOWN) val UNKNOWN = Acct("?", Host.UNKNOWN)
fun parse(src : String) : Acct { fun parse(src : String) : Acct {
val cached = acctSet[src] val cached = acctSet[src]
if(cached != null) return cached if(cached != null) return cached
if( src.isEmpty()) return UNKNOWN if(src.isEmpty()) return UNKNOWN
val pos = src.indexOf('@') val pos = src.indexOf('@')
val acct = if(pos != - 1) val acct = if(pos != - 1)
Acct(src.substring(0, pos), Host.parse(src.substring(pos + 1))) Acct(src.substring(0, pos), Host.parse(src.substring(pos + 1)))
else else
Acct(src, null) Acct(src, null)
acctSet[src] = acct acctSet[src] = acct
return acct return acct
} }
fun parse(user : String,host:String?) = fun parse(user : String, host : String?) =
if(host != null) parse("$user@$host") else parse(user) if(host != null) parse("$user@$host") else parse(user)
fun parse(user : String,host:Host?) =Acct(user,host) fun parse(user : String, host : Host?) = Acct(user, host)
} }
} }

View File

@ -12,9 +12,12 @@ import android.text.style.StrikethroughSpan
import android.util.SparseArray import android.util.SparseArray
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.Pref
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.TootAccount import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootMention import jp.juggler.subwaytooter.api.entity.TootMention
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.span.* import jp.juggler.subwaytooter.span.*
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.HighlightWord import jp.juggler.subwaytooter.table.HighlightWord
@ -844,38 +847,25 @@ object MisskeyMarkdownDecoder {
MENTION({ MENTION({
val username = it.args[0] val username = it.args[0]
val host = it.args[1] val strHost = it.args[1].notEmpty()
val rawAcct = Acct.parse(username, strHost?.let{ Host.parse(it)})
val linkHelper = linkHelper val linkHelper = linkHelper
if(linkHelper == null) { if(linkHelper == null) {
appendText( appendText("@${rawAcct.pretty}")
when {
host.isEmpty() -> "@$username"
else -> "@$username@$host"
}
)
} else { } else {
when( strHost ){
when(
val userHost = (host.notEmpty()
?: linkHelper.host?.ascii
?: "?"
).toLowerCase(Locale.JAPAN)
) {
// https://github.com/syuilo/misskey/pull/3603 // https://github.com/syuilo/misskey/pull/3603
"github.com", "twitter.com" -> { "github.com", "twitter.com" -> {
appendLink( appendLink(
"@$username@$userHost", "@${rawAcct.pretty}",
"https://$userHost/$username" // no @ "https://$strHost/$username" // no @
) )
} }
"gmail.com" -> { "gmail.com" -> {
appendLink( appendLink(
"@$username@$userHost", "@${rawAcct.pretty}",
"mailto:$username@$userHost" "mailto:$username@$strHost"
) )
} }
@ -883,23 +873,25 @@ object MisskeyMarkdownDecoder {
// MFMはメンションからユーザのURIを調べる正式な方法がない // MFMはメンションからユーザのURIを調べる正式な方法がない
// たとえば @group_dev_jp@gup.pe の正式なURLは https://gup.pe/u/group_dev_jp // たとえば @group_dev_jp@gup.pe の正式なURLは https://gup.pe/u/group_dev_jp
// だが、 misskey.io ではメンションのリンク先は https://misskey.io/@group_dev_jp@gup.pe になる // だが、 misskey.io ではメンションのリンク先は https://misskey.io/@group_dev_jp@gup.pe になる
val userUrl = "https://$userHost/@$username"
val fullAcct = rawAcct.followHost(linkHelper.host)
val shortAcct = when {
val shortAcct = if(fullAcct.host != linkHelper.host) {
host.isEmpty() || linkHelper.host?.match(host) == true -> username fullAcct
} else {
else -> "$username@$host" Acct.parse(username)
} }
val userUrl = "https://${fullAcct.ascii}/@${username.encodePercent()}"
val mentions = prepareMentions() val mentions = prepareMentions()
if(null == mentions.find { m -> m.acct.ascii == shortAcct || m.acct.pretty == shortAcct }) { if(null == mentions.find { m -> m.acct == shortAcct }) {
mentions.add( mentions.add(
TootMention( TootMention(
jp.juggler.subwaytooter.api.entity.EntityId.DEFAULT jp.juggler.subwaytooter.api.entity.EntityId.DEFAULT
, userUrl , userUrl
, shortAcct , shortAcct.ascii
, username , username
) )
) )
@ -907,9 +899,11 @@ object MisskeyMarkdownDecoder {
appendLink( appendLink(
when { when {
jp.juggler.subwaytooter.Pref.bpMentionFullAcct(jp.juggler.subwaytooter.App1.pref) -> Pref.bpMentionFullAcct(jp.juggler.subwaytooter.App1.pref) ->
"@$username@$userHost" "@${fullAcct.pretty}"
else -> "@$shortAcct" else -> {
"@${rawAcct.pretty}"
}
} }
, userUrl , userUrl
) )

View File

@ -15,12 +15,14 @@ import android.widget.LinearLayout
import android.widget.PopupWindow import android.widget.PopupWindow
import jp.juggler.subwaytooter.Pref import jp.juggler.subwaytooter.Pref
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.view.MyEditText import jp.juggler.subwaytooter.view.MyEditText
import jp.juggler.util.LogCategory import jp.juggler.util.LogCategory
import jp.juggler.util.getAttributeColor import jp.juggler.util.getAttributeColor
import jp.juggler.util.groupEx import jp.juggler.util.groupEx
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.min
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
internal class PopupAutoCompleteAcct( internal class PopupAutoCompleteAcct(
@ -135,8 +137,8 @@ internal class PopupAutoCompleteAcct(
val sb = SpannableStringBuilder() val sb = SpannableStringBuilder()
val src_length = editable.length val src_length = editable.length
start = Math.min(src_length, sel_start) start = min(src_length, sel_start)
val end = Math.min(src_length, sel_end) val end = min(src_length, sel_end)
sb.append(editable.subSequence(0, start)) sb.append(editable.subSequence(0, start))
val remain = editable.subSequence(end, src_length) val remain = editable.subSequence(end, src_length)
@ -147,8 +149,13 @@ internal class PopupAutoCompleteAcct(
sb.append(findShortCode(acct.toString())) sb.append(findShortCode(acct.toString()))
// セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。 // セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。
if( separator != ' ') sb.append(separator) if( separator != ' ') sb.append(separator)
} else { } else if(acct[0] == '@' && null != acct.find{ it >= 0x80.toChar() } ) {
// @user@host, #hashtag // @user@host IDNドメインを含む
// 直後に空白を付与する
sb.append("@" + Acct.parse(acct.toString().substring(1)).ascii).append(" ")
}else{
// @user@host
// #hashtag
// 直後に空白を付与する // 直後に空白を付与する
sb.append(acct).append(" ") sb.append(acct).append(" ")
} }