diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt index b3428ea7..ac40d8d4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt @@ -85,11 +85,9 @@ class ActMain : AppCompatActivity() internal var sent_intent2 : Intent? = null internal val reUrlHashTag = - Pattern.compile("\\Ahttps://([^/]+)/tags/([^?#・\\s\\-+.,:;/]+)(?:\\z|[?#])") - - @Suppress("HasPlatformType") - val reStatusPage = Pattern.compile("\\Ahttps://([^/]+)/@([A-Za-z0-9_]+)/(\\d+)(?:\\z|[?#])") + Pattern.compile("""\Ahttps://([^/]+)/tags/([^?#・\s\-+.,:;/]+)(?:\z|[?#])""") + var boostButtonSize = 0 var timeline_font : Typeface = Typeface.DEFAULT var timeline_font_bold : Typeface = Typeface.DEFAULT_BOLD @@ -1557,10 +1555,10 @@ class ActMain : AppCompatActivity() val url = uri.toString() - var m = reStatusPage.matcher(url) + // https://mastodon.juggler.jp/@SubwayTooter/(status_id) + var m = TootStatus.reStatusPage.matcher(url) if(m.find()) { try { - // https://mastodon.juggler.jp/@SubwayTooter/(status_id) val host = m.group(1) val status_id = EntityIdLong(m.group(3).toLong(10)) // ステータスをアプリ内で開く @@ -1579,6 +1577,28 @@ class ActMain : AppCompatActivity() return } + // https://misskey.xyz/notes/(id) + m = TootStatus.reStatusPageMisskey.matcher(url) + if(m.find()) { + try { + val host = m.group(1) + val status_id = EntityIdString(m.group(2)) + // ステータスをアプリ内で開く + Action_Toot.conversationOtherInstance( + this@ActMain, + defaultInsertPosition, + url, + status_id, + host, + status_id + ) + } catch(ex : Throwable) { + showToast(this, ex, "can't parse status id.") + } + + return + } + // ユーザページをアプリ内で開く m = TootAccount.reAccountUrl.matcher(url) if(m.find()) { @@ -2204,7 +2224,7 @@ class ActMain : AppCompatActivity() } // ステータスページをアプリから開く - m = reStatusPage.matcher(opener.url) + m = TootStatus.reStatusPage.matcher(opener.url) if(m.find()) { try { // https://mastodon.juggler.jp/@SubwayTooter/(status_id) @@ -2234,6 +2254,37 @@ class ActMain : AppCompatActivity() return } + // ステータスページをアプリから開く + m = TootStatus.reStatusPageMisskey.matcher(opener.url) + if(m.find()) { + try { + // https://misskey.xyz/notes/(id) + val host = m.group(1) + val status_id = EntityIdString(m.group(2)) + if(accessInto.isNA || ! host.equals(accessInto.host, ignoreCase = true)) { + Action_Toot.conversationOtherInstance( + this@ActMain, + opener.pos, + opener.url, + status_id, + host, + status_id + ) + } else { + Action_Toot.conversationLocal( + this@ActMain, + opener.pos, + accessInto, + status_id + ) + } + } catch(ex : Throwable) { + showToast(this, ex, "can't parse status id.") + } + + return + } + // ユーザページをアプリ内で開く m = TootAccount.reAccountUrl.matcher(opener.url) if(m.find()) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt index 3ee1d602..26b906b0 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt @@ -69,6 +69,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba internal const val KEY_REDRAFT_STATUS = "redraft_status" internal const val KEY_INITIAL_TEXT = "initial_text" internal const val KEY_SENT_INTENT = "sent_intent" + internal const val KEY_QUOTED_RENOTE = "quoted_renote" internal const val KEY_ATTACHMENT_LIST = "attachment_list" internal const val KEY_VISIBILITY = "visibility" @@ -104,24 +105,27 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba } private val imageHeaderList = arrayOf( - Pair("image/jpeg",intArrayOf(0xff,0xd8,0xff,0xe0).toByteArray()), - Pair("image/png",intArrayOf(0x89 ,0x50 ,0x4E ,0x47 ,0x0D ,0x0A ,0x1A ,0x0A).toByteArray()), - Pair("image/gif", charArrayOf('G' ,'I' ,'F').toByteArray()) + Pair("image/jpeg", intArrayOf(0xff, 0xd8, 0xff, 0xe0).toByteArray()), + Pair( + "image/png", + intArrayOf(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A).toByteArray() + ), + Pair("image/gif", charArrayOf('G', 'I', 'F').toByteArray()) ) - private fun checkImageHeaderList(contentResolver:ContentResolver, uri : Uri) : String? { - try{ - contentResolver.openInputStream(uri)?.use{ inStream -> + private fun checkImageHeaderList(contentResolver : ContentResolver, uri : Uri) : String? { + try { + contentResolver.openInputStream(uri)?.use { inStream -> val data = ByteArray(32) - val nRead = inStream.read(data,0,data.size) - for( pair in imageHeaderList ){ + val nRead = inStream.read(data, 0, data.size) + for(pair in imageHeaderList) { val type = pair.first val header = pair.second - if( nRead >= header.size && data.startWith(header) ) return type + if(nRead >= header.size && data.startWith(header)) return type } } - }catch(ex:Throwable){ - log.e(ex,"checkImageHeaderList failed.") + } catch(ex : Throwable) { + log.e(ex, "checkImageHeaderList failed.") } return null } @@ -175,22 +179,31 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba initial_text : String? = null, // 外部アプリから共有されたインテント - sent_intent : Intent? = null + sent_intent : Intent? = null, + + // (Misskey) 返信を引用リノートにする + quotedRenote : Boolean = false ) { val intent = Intent(activity, ActPost::class.java) intent.putExtra(KEY_ACCOUNT_DB_ID, account_db_id) + if(redraft_status != null) { intent.putExtra(KEY_REDRAFT_STATUS, redraft_status.json.toString()) } + if(reply_status != null) { intent.putExtra(KEY_REPLY_STATUS, reply_status.json.toString()) + intent.putExtra(KEY_QUOTED_RENOTE, quotedRenote) } + if(initial_text != null) { intent.putExtra(KEY_INITIAL_TEXT, initial_text) } + if(sent_intent != null) { intent.putExtra(KEY_SENT_INTENT, sent_intent) } + activity.startActivityForResult(intent, request_code) } @@ -320,14 +333,12 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba } } - - override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) { if(requestCode == REQUEST_CODE_ATTACHMENT_OLD && resultCode == Activity.RESULT_OK) { data?.handleGetContentResult(contentResolver)?.forEach { addAttachment(it.first, it.second) } - + } else if(requestCode == REQUEST_CODE_ATTACHMENT && resultCode == Activity.RESULT_OK) { data?.handleGetContentResult(contentResolver)?.forEach { addAttachment(it.first, it.second) @@ -394,7 +405,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba initUI() // Android 9 から、明示的にフォーカスを当てる必要がある - if( savedInstanceState==null){ + if(savedInstanceState == null) { etContent.requestFocus() } @@ -536,45 +547,56 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba if(sv != null && account != null) { try { val reply_status = TootParser(this@ActPost, account).status(sv.toJsonObject()) - + + val isQuoterRenote = intent.getBooleanExtra(KEY_QUOTED_RENOTE, false) + if(reply_status != null) { - // CW をリプライ元に合わせる - if(reply_status.spoiler_text?.isNotEmpty() == true) { - cbContentWarning.isChecked = true - etContentWarning.setText(reply_status.spoiler_text) - } - val mention_list = ArrayList() - - val old_mentions = reply_status.mentions - if(old_mentions != null) { - for(mention in old_mentions) { - val who_acct = mention.acct - if(who_acct.isNotEmpty()) { - if(account.isMe(who_acct)) continue - sv = "@" + account.getFullAcct(who_acct) - if(! mention_list.contains(sv)) { - mention_list.add(sv) + if(isQuoterRenote) { + cbQuoteRenote.isChecked = true + + // 引用リノートはCWやメンションを引き継がない + + }else{ + + // CW をリプライ元に合わせる + if(reply_status.spoiler_text?.isNotEmpty() == true) { + cbContentWarning.isChecked = true + etContentWarning.setText(reply_status.spoiler_text) + } + + val mention_list = ArrayList() + + val old_mentions = reply_status.mentions + if(old_mentions != null) { + for(mention in old_mentions) { + val who_acct = mention.acct + if(who_acct.isNotEmpty()) { + if(account.isMe(who_acct)) continue + sv = "@" + account.getFullAcct(who_acct) + if(! mention_list.contains(sv)) { + mention_list.add(sv) + } } } } - } - - // 元レスのacctを追加する - val who_acct = account.getFullAcct(reply_status.account) - if(! account.isMe(reply_status.account) // 自己レスにはメンションを追加しない - && ! mention_list.contains("@$who_acct") // 既に含まれているならメンションを追加しない - ) { - mention_list.add("@$who_acct") - } - - val sb = StringBuilder() - for(acct in mention_list) { - if(sb.isNotEmpty()) sb.append(' ') - sb.append(acct) - } - if(sb.isNotEmpty()) { - appendContentText(sb.append(' ').toString()) + + // 元レスのacctを追加する + val who_acct = account.getFullAcct(reply_status.account) + if(! account.isMe(reply_status.account) // 自己レスにはメンションを追加しない + && ! mention_list.contains("@$who_acct") // 既に含まれているならメンションを追加しない + ) { + mention_list.add("@$who_acct") + } + + val sb = StringBuilder() + for(acct in mention_list) { + if(sb.isNotEmpty()) sb.append(' ') + sb.append(acct) + } + if(sb.isNotEmpty()) { + appendContentText(sb.append(' ').toString()) + } } // リプライ表示をつける @@ -607,13 +629,10 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba } catch(ex : Throwable) { log.trace(ex) } - } - } catch(ex : Throwable) { log.trace(ex) } - } appendContentText(account?.default_text, selectBefore = true) @@ -650,14 +669,14 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba // 再編集の場合はdefault_textは反映されない val decodeOptions = DecodeOptions(this) - - var text :Spannable + + var text : Spannable text = decodeOptions.decodeHTML(base_status.content) etContent.text = text - etContent.setSelection(text.length ) - - text =decodeOptions.decodeEmoji(base_status.spoiler_text) + etContent.setSelection(text.length) + + text = decodeOptions.decodeEmoji(base_status.spoiler_text) etContentWarning.setText(text) etContentWarning.setSelection(text.length) cbContentWarning.isChecked = text.isNotEmpty() @@ -716,7 +735,6 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba showQuotedRenote() } - override fun onDestroy() { post_helper.onDestroy() @@ -783,8 +801,8 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba if(svEmoji.isEmpty()) return val editable = etContent.text - if( editable == null ) { - val sb = StringBuilder () + if(editable == null) { + val sb = StringBuilder() if(selectBefore) { val start = 0 sb.append(' ') @@ -796,7 +814,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba etContent.setText(sb) etContent.setSelection(sb.length) } - }else{ + } else { if(editable.isNotEmpty() && ! CharacterGroup.isWhitespace(editable[editable.length - 1].toInt()) ) { @@ -860,7 +878,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba etContentWarning = findViewById(R.id.etContentWarning) etContent = findViewById(R.id.etContent) - cbQuoteRenote= findViewById(R.id.cbQuoteRenote) + cbQuoteRenote = findViewById(R.id.cbQuoteRenote) cbEnquete = findViewById(R.id.cbEnquete) llEnquete = findViewById(R.id.llEnquete) @@ -895,10 +913,10 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba btnPost.setOnClickListener(this) btnRemoveReply.setOnClickListener(this) - val btnPlugin :ImageButton = findViewById(R.id.btnPlugin) - val btnEmojiPicker :ImageButton = findViewById(R.id.btnEmojiPicker) - val btnMore: ImageButton = findViewById(R.id.btnMore) - + val btnPlugin : ImageButton = findViewById(R.id.btnPlugin) + val btnEmojiPicker : ImageButton = findViewById(R.id.btnEmojiPicker) + val btnMore : ImageButton = findViewById(R.id.btnMore) + btnPlugin.setOnClickListener(this) btnEmojiPicker.setOnClickListener(this) btnMore.setOnClickListener(this) @@ -909,11 +927,11 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba iv.setErrorImageResId(Styler.getAttributeResourceId(this, R.attr.ic_unknown)) } - setIcon(btnPost,R.drawable.btn_post) - setIcon(btnMore,R.drawable.btn_more) - setIcon(btnPlugin,R.drawable.ic_plugin) - setIcon(btnEmojiPicker,R.drawable.ic_face) - setIcon(btnAttachment,R.drawable.btn_attachment) + setIcon(btnPost, R.drawable.btn_post) + setIcon(btnMore, R.drawable.btn_more) + setIcon(btnPlugin, R.drawable.ic_plugin) + setIcon(btnEmojiPicker, R.drawable.ic_face) + setIcon(btnAttachment, R.drawable.btn_attachment) cbContentWarning.setOnCheckedChangeListener { _, _ -> updateContentWarning() @@ -946,15 +964,15 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba etContent.contentMineTypeArray = acceptable_mime_types.toArray(arrayOfNulls(ActPost.acceptable_mime_types.size)) etContent.commitContentListener = commitContentListener - + } - private fun setIcon(iv:ImageView,drawableId:Int) { + private fun setIcon(iv : ImageView, drawableId : Int) { Styler.setIconDrawableId( this, iv, drawableId, - Styler.getAttributeColor(this,R.attr.colorColumnHeaderName) + Styler.getAttributeColor(this, R.attr.colorColumnHeaderName) ) } @@ -1049,7 +1067,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba post_helper.setInstance(a.host, a.isMisskey) // 先読みしてキャッシュに保持しておく - App1.custom_emoji_lister.getList(a.host,a.isMisskey) { + App1.custom_emoji_lister.getList(a.host, a.isMisskey) { // 何もしない } @@ -1447,7 +1465,6 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba } - private fun performAttachmentOld() { // SAFのIntentで開く try { @@ -1562,13 +1579,13 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba // image/j()pg だの image/j(e)pg だの、mime type を誤記するアプリがあまりに多い // クレームで消耗するのを減らすためにファイルヘッダを確認する - if(mimeTypeArg == null || mimeTypeArg.startsWith("image/")){ - val sv = checkImageHeaderList(contentResolver,uri) - if( sv != null) return sv + if(mimeTypeArg == null || mimeTypeArg.startsWith("image/")) { + val sv = checkImageHeaderList(contentResolver, uri) + if(sv != null) return sv } - + // 既に引数で与えられてる - if(mimeTypeArg?.isNotEmpty() == true){ + if(mimeTypeArg?.isNotEmpty() == true) { return mimeTypeArg } @@ -1583,8 +1600,6 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba return null } - - @SuppressLint("StaticFieldLeak") private fun addAttachment( uri : Uri, @@ -1882,11 +1897,13 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba } private fun showVisibility() { - setIcon(btnVisibility,Styler.getVisibilityIcon( - this - , account?.isMisskey == true - , visibility ?: TootVisibility.Public - )) + setIcon( + btnVisibility, Styler.getVisibilityIcon( + this + , account?.isMisskey == true + , visibility ?: TootVisibility.Public + ) + ) } private fun performVisibility() { @@ -1999,7 +2016,8 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba post_helper.attachment_list = this.attachment_list - post_helper.emojiMapCustom = App1.custom_emoji_lister.getMap(account.host,account.isMisskey) + post_helper.emojiMapCustom = + App1.custom_emoji_lister.getMap(account.host, account.isMisskey) post_helper.redraft_status_id = redraft_status_id @@ -2020,7 +2038,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba private fun showQuotedRenote() { val isReply = in_reply_to_id != null val isMisskey = account?.isMisskey == true - cbQuoteRenote.visibility = if( isReply && isMisskey ) View.VISIBLE else View.GONE + cbQuoteRenote.visibility = if(isReply && isMisskey) View.VISIBLE else View.GONE } internal fun showReplyTo() { @@ -2092,7 +2110,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba json.put(DRAFT_REPLY_IMAGE, in_reply_to_image) json.put(DRAFT_REPLY_URL, in_reply_to_url) - json.put(DRAFT_QUOTED_RENOTE,cbQuoteRenote.isChecked) + json.put(DRAFT_QUOTED_RENOTE, cbQuoteRenote.isChecked) json.put(DRAFT_IS_ENQUETE, isEnquete) val array = JSONArray() @@ -2246,7 +2264,6 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba val draft_visibility = TootVisibility .parseSavedVisibility(draft.parseString(DRAFT_VISIBILITY)) - val evEmoji = DecodeOptions(this@ActPost, decodeEmoji = true).decodeEmoji(content) etContent.setText(evEmoji) etContent.setSelection(evEmoji.length) @@ -2296,7 +2313,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba in_reply_to_image = reply_image in_reply_to_url = reply_url } - + updateContentWarning() showMediaAttachment() diff --git a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.kt b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.kt index 83a85ff0..ff3c4306 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.kt @@ -2,7 +2,6 @@ package jp.juggler.subwaytooter import android.annotation.SuppressLint import android.app.Dialog -import android.graphics.PorterDuff import android.support.v4.app.ShareCompat import android.support.v7.app.AlertDialog import android.view.View @@ -68,6 +67,7 @@ internal class DlgContextMenu( val btnBoostAnotherAccount = viewRoot.findViewById(R.id.btnBoostAnotherAccount) val btnReactionAnotherAccount = viewRoot.findViewById(R.id.btnReactionAnotherAccount) val btnReplyAnotherAccount = viewRoot.findViewById(R.id.btnReplyAnotherAccount) + val btnQuotedRenote = viewRoot.findViewById(R.id.btnQuotedRenote) val btnDelete = viewRoot.findViewById(R.id.btnDelete) val btnRedraft = viewRoot.findViewById(R.id.btnRedraft) @@ -124,6 +124,7 @@ internal class DlgContextMenu( btnBoostAnotherAccount.setOnClickListener(this) btnReactionAnotherAccount.setOnClickListener(this) btnReplyAnotherAccount.setOnClickListener(this) + btnQuotedRenote.setOnClickListener(this) btnReport.setOnClickListener(this) btnMuteApp.setOnClickListener(this) btnDelete.setOnClickListener(this) @@ -728,7 +729,12 @@ internal class DlgContextMenu( access_info, status ) - + R.id.btnQuotedRenote-> Action_Toot.replyFromAnotherAccount( + activity, + access_info, + status, + quotedRenote = true + ) R.id.btnConversationAnotherAccount -> status?.let { status -> Action_Toot.conversationOtherInstance(activity, pos, status) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.kt index e889ceee..ed550524 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Toot.kt @@ -10,10 +10,7 @@ import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.DlgConfirm import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.SavedAccount -import jp.juggler.subwaytooter.util.EmptyCallback -import jp.juggler.subwaytooter.util.LogCategory -import jp.juggler.subwaytooter.util.showToast -import jp.juggler.subwaytooter.util.toPostRequestBuilder +import jp.juggler.subwaytooter.util.* import okhttp3.Request import okhttp3.RequestBody import org.json.JSONObject @@ -893,40 +890,25 @@ object Action_Toot { // reply fun reply( - activity : ActMain, access_info : SavedAccount, status : TootStatus + activity : ActMain, + access_info : SavedAccount, + status : TootStatus, + quotedRenote : Boolean = false ) { ActPost.open( activity, ActMain.REQUEST_CODE_POST, access_info.db_id, - reply_status = status + reply_status = status, + quotedRenote = quotedRenote ) } - fun replyFromAnotherAccount( - activity : ActMain, timeline_account : SavedAccount, status : TootStatus? - ) { - if(status == null) return - val who_host = timeline_account.host - AccountPicker.pick( - activity, - bAllowPseudo = false, - bAuto = false, - message = activity.getString(R.string.account_picker_reply), - accountListArg = makeAccountListNonPseudo(activity, who_host) - ) { ai -> - if(ai.host.equals(status.host_access, ignoreCase = true)) { - // アクセス元ホストが同じならステータスIDを使って返信できる - reply(activity, ai, status) - } else { - // それ以外の場合、ステータスのURLを検索APIに投げることで返信できる - replyRemote(activity, ai, status.url) - } - } - } - private fun replyRemote( - activity : ActMain, access_info : SavedAccount, remote_status_url : String? + activity : ActMain, + access_info : SavedAccount, + remote_status_url : String?, + quotedRenote : Boolean = false ) { if(remote_status_url == null || remote_status_url.isEmpty()) return @@ -951,7 +933,7 @@ object Action_Toot { val ls = local_status if(ls != null) { - reply(activity, access_info, ls) + reply(activity, access_info, ls, quotedRenote = quotedRenote) } else { showToast(activity, true, result.error) } @@ -959,6 +941,47 @@ object Action_Toot { }) } + fun replyFromAnotherAccount( + activity : ActMain, + timeline_account : SavedAccount, + status : TootStatus?, + quotedRenote : Boolean = false + ) { + status ?: return + val who_host = timeline_account.host + + val accountCallback : SavedAccountCallback = { ai -> + if(ai.host.equals(status.host_access, ignoreCase = true)) { + // アクセス元ホストが同じならステータスIDを使って返信できる + reply(activity, ai, status, quotedRenote = quotedRenote) + } else { + // それ以外の場合、ステータスのURLを検索APIに投げることで返信できる + replyRemote(activity, ai, status.url, quotedRenote = quotedRenote) + } + } + + if(quotedRenote) { + AccountPicker.pick( + activity, + bAllowPseudo = false, + bAllowMisskey = true, + bAllowMastodon = false, + bAuto = true, + message = activity.getString(R.string.account_picker_quoted_renote), + callback = accountCallback + ) + } else { + AccountPicker.pick( + activity, + bAllowPseudo = false, + bAuto = false, + message = activity.getString(R.string.account_picker_reply), + accountListArg = makeAccountListNonPseudo(activity, who_host), + callback = accountCallback + ) + } + } + // 投稿画面を開く。初期テキストを指定する fun redraft( activity : ActMain, @@ -1175,7 +1198,7 @@ object Action_Toot { activity : ActMain, timeline_account : SavedAccount, status : TootStatus?, - code :String? = null + code : String? = null ) { status ?: return @@ -1200,4 +1223,5 @@ object Action_Toot { ) } } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt index 70369a2e..8f7b33af 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt @@ -3,14 +3,12 @@ package jp.juggler.subwaytooter.api import android.content.Context import android.content.SharedPreferences import jp.juggler.subwaytooter.* -import jp.juggler.subwaytooter.api.entity.EntityId -import jp.juggler.subwaytooter.api.entity.TootAccount -import jp.juggler.subwaytooter.api.entity.TootInstance -import jp.juggler.subwaytooter.api.entity.TootStatus +import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.table.ClientInfo import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.util.* import okhttp3.* +import org.hjson.JsonObject import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -117,7 +115,13 @@ class TootApiClient( } } - val DEFAULT_JSON_ERROR_PARSER = { json : JSONObject -> json.parseString("error") } + val DEFAULT_JSON_ERROR_PARSER = { json : JSONObject -> + val v = json.opt("error") + when(v) { + null,JSONObject.NULL -> null + else -> v.toString() + } + } internal fun simplifyErrorHtml( response : Response, @@ -516,7 +520,7 @@ class TootApiClient( val result = TootApiResult.makeWithCaption(instance) if(result.error != null) return result - val account = this.account ?: return result.setError("account is null") + val account = this.account // may null try { if(! sendRequest(result) { @@ -525,7 +529,7 @@ class TootApiClient( request_builder.url("https://$instance$path") - val access_token = account.getAccessToken() + val access_token = account?.getAccessToken() if(access_token?.isNotEmpty() == true) { request_builder.header("Authorization", "Bearer $access_token") } @@ -801,13 +805,13 @@ class TootApiClient( val user : JSONObject = token_info.optJSONObject("user") ?: return result.setError("missing user in the response.") - + token_info.remove("user") val apiKey = "$access_token$appSecret".encodeUTF8().digestSHA256().encodeHexLower() // ユーザ情報を読めたならtokenInfoを保存する - EntityId.mayNull( user.parseString("id") )?.putTo(token_info,KEY_USER_ID) + EntityId.mayNull(user.parseString("id"))?.putTo(token_info, KEY_USER_ID) token_info.put(KEY_IS_MISSKEY, true) token_info.put(KEY_AUTH_VERSION, AUTH_VERSION) token_info.put(KEY_API_KEY_MISSKEY, apiKey) @@ -1042,7 +1046,7 @@ class TootApiClient( // misskeyのインスタンス情報を読めたら、それはmisskeyのインスタンス val r2 = getInstanceInformationMisskey() ?: return null if(r2.jsonObject != null) return r2 - + // マストドンのインスタンス情報を読めたら、それはマストドンのインスタンス val r1 = getInstanceInformationMastodon() ?: return null if(r1.jsonObject != null) return r1 @@ -1465,8 +1469,40 @@ fun TootApiClient.syncAccountByAcct(accessInfo : SavedAccount, acct : String) : } } -fun TootApiClient.syncStatus(accessInfo : SavedAccount, url : String) = - if(accessInfo.isMisskey) { +fun TootApiClient.syncStatus(accessInfo : SavedAccount, urlArg : String) : TootApiResult? { + + var url = urlArg + + // misskey の投稿URLは外部タンスの投稿を複製したものの可能性がある + // これを投稿元タンスのURLに変換しないと、投稿の同期には使えない + val m = TootStatus.reStatusPageMisskey.matcher(urlArg) + if(m.find()) { + val host = m.group(1) + val client2 = TootApiClient(context, callback = callback) + client2.instance =host + val params = JSONObject().put("uri", urlArg) + val result = client2.request("/api/ap/show", params.toPostRequestBuilder()) + if(result == null || result.error != null) return result + + val obj = parseMisskeyApShow( + TootParser(context, accessInfo,serviceType = ServiceType.MISSKEY), + result.jsonObject + ) as? TootStatus + + if( obj != null ){ + if( host .equals(accessInfo.host,ignoreCase = true)){ + result.data = obj + return result + } + val uri = obj.uri + if(uri?.isNotEmpty() == true){ + url = uri + } + } + + } + + return if(accessInfo.isMisskey) { val params = accessInfo.putMisskeyApiToken().put("uri", url) val result = request("/api/ap/show", params.toPostRequestBuilder()) if(result != null) { @@ -1490,6 +1526,8 @@ fun TootApiClient.syncStatus(accessInfo : SavedAccount, url : String) = } result } + +} private inline fun String?.useNotEmpty(block : (String) -> Z?) : Z? = if(this?.isNotEmpty() == true) { 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 13689182..1d1cf7a8 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 @@ -658,6 +658,15 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() { @Suppress("HasPlatformType") private val reTootUriAP2 = Pattern.compile("https?://([^/]+)/@[A-Za-z0-9_]+/(\\d+)") + // 公開ステータスページのURL マストドン + @Suppress("HasPlatformType") + val reStatusPage = Pattern.compile("""\Ahttps://([^/]+)/@([A-Za-z0-9_]+)/(\d+)(?:\z|[?#])""") + + // 公開ステータスページのURL Misskey + @Suppress("HasPlatformType") + val reStatusPageMisskey = Pattern.compile("""\Ahttps://([^/]+)/notes/([0-9a-f]{24})\b""") + + const val INVALID_ID = - 1L fun parseListTootsearch( diff --git a/app/src/main/res/layout/dlg_context_menu.xml b/app/src/main/res/layout/dlg_context_menu.xml index 9bf7eb1a..f389fc4b 100644 --- a/app/src/main/res/layout/dlg_context_menu.xml +++ b/app/src/main/res/layout/dlg_context_menu.xml @@ -101,6 +101,7 @@ android:text="@string/share_url_more" android:textAllCaps="false" /> +