1
0
mirror of https://github.com/tateisu/SubwayTooter synced 2025-02-07 06:04:23 +01:00

(Misskey)投稿のコンテキストメニューから引用リノート。別アカ選択あり。引用リノートで投稿画面を開いたらCWやメンションを引き継がない。(Misskey)投稿のURLをタップした時にアプリ内で開く。(Misskey)外部タンスの投稿を複製したURLを別のタンスに同期する際、投稿元タンスでのuriを取得してから目的のタンスに同期する

This commit is contained in:
tateisu 2018-11-28 01:53:27 +09:00
parent b3bf3a878e
commit 0275edfdbc
9 changed files with 311 additions and 146 deletions

View File

@ -85,11 +85,9 @@ class ActMain : AppCompatActivity()
internal var sent_intent2 : Intent? = null internal var sent_intent2 : Intent? = null
internal val reUrlHashTag = internal val reUrlHashTag =
Pattern.compile("\\Ahttps://([^/]+)/tags/([^?#・\\s\\-+.,:;/]+)(?:\\z|[?#])") Pattern.compile("""\Ahttps://([^/]+)/tags/([^?#・\s\-+.,:;/]+)(?:\z|[?#])""")
@Suppress("HasPlatformType")
val reStatusPage = Pattern.compile("\\Ahttps://([^/]+)/@([A-Za-z0-9_]+)/(\\d+)(?:\\z|[?#])")
var boostButtonSize = 0 var boostButtonSize = 0
var timeline_font : Typeface = Typeface.DEFAULT var timeline_font : Typeface = Typeface.DEFAULT
var timeline_font_bold : Typeface = Typeface.DEFAULT_BOLD var timeline_font_bold : Typeface = Typeface.DEFAULT_BOLD
@ -1557,10 +1555,10 @@ class ActMain : AppCompatActivity()
val url = uri.toString() 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()) { if(m.find()) {
try { try {
// https://mastodon.juggler.jp/@SubwayTooter/(status_id)
val host = m.group(1) val host = m.group(1)
val status_id = EntityIdLong(m.group(3).toLong(10)) val status_id = EntityIdLong(m.group(3).toLong(10))
// ステータスをアプリ内で開く // ステータスをアプリ内で開く
@ -1579,6 +1577,28 @@ class ActMain : AppCompatActivity()
return 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) m = TootAccount.reAccountUrl.matcher(url)
if(m.find()) { if(m.find()) {
@ -2204,7 +2224,7 @@ class ActMain : AppCompatActivity()
} }
// ステータスページをアプリから開く // ステータスページをアプリから開く
m = reStatusPage.matcher(opener.url) m = TootStatus.reStatusPage.matcher(opener.url)
if(m.find()) { if(m.find()) {
try { try {
// https://mastodon.juggler.jp/@SubwayTooter/(status_id) // https://mastodon.juggler.jp/@SubwayTooter/(status_id)
@ -2234,6 +2254,37 @@ class ActMain : AppCompatActivity()
return 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) m = TootAccount.reAccountUrl.matcher(opener.url)
if(m.find()) { if(m.find()) {

View File

@ -69,6 +69,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
internal const val KEY_REDRAFT_STATUS = "redraft_status" internal const val KEY_REDRAFT_STATUS = "redraft_status"
internal const val KEY_INITIAL_TEXT = "initial_text" internal const val KEY_INITIAL_TEXT = "initial_text"
internal const val KEY_SENT_INTENT = "sent_intent" 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_ATTACHMENT_LIST = "attachment_list"
internal const val KEY_VISIBILITY = "visibility" internal const val KEY_VISIBILITY = "visibility"
@ -104,24 +105,27 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
} }
private val imageHeaderList = arrayOf( private val imageHeaderList = arrayOf(
Pair("image/jpeg",intArrayOf(0xff,0xd8,0xff,0xe0).toByteArray()), Pair("image/jpeg", intArrayOf(0xff, 0xd8, 0xff, 0xe0).toByteArray()),
Pair("image/png",intArrayOf(0x89 ,0x50 ,0x4E ,0x47 ,0x0D ,0x0A ,0x1A ,0x0A).toByteArray()), Pair(
Pair("image/gif", charArrayOf('G' ,'I' ,'F').toByteArray()) "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? { private fun checkImageHeaderList(contentResolver : ContentResolver, uri : Uri) : String? {
try{ try {
contentResolver.openInputStream(uri)?.use{ inStream -> contentResolver.openInputStream(uri)?.use { inStream ->
val data = ByteArray(32) val data = ByteArray(32)
val nRead = inStream.read(data,0,data.size) val nRead = inStream.read(data, 0, data.size)
for( pair in imageHeaderList ){ for(pair in imageHeaderList) {
val type = pair.first val type = pair.first
val header = pair.second 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){ } catch(ex : Throwable) {
log.e(ex,"checkImageHeaderList failed.") log.e(ex, "checkImageHeaderList failed.")
} }
return null return null
} }
@ -175,22 +179,31 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
initial_text : String? = null, initial_text : String? = null,
// 外部アプリから共有されたインテント // 外部アプリから共有されたインテント
sent_intent : Intent? = null sent_intent : Intent? = null,
// (Misskey) 返信を引用リノートにする
quotedRenote : Boolean = false
) { ) {
val intent = Intent(activity, ActPost::class.java) val intent = Intent(activity, ActPost::class.java)
intent.putExtra(KEY_ACCOUNT_DB_ID, account_db_id) intent.putExtra(KEY_ACCOUNT_DB_ID, account_db_id)
if(redraft_status != null) { if(redraft_status != null) {
intent.putExtra(KEY_REDRAFT_STATUS, redraft_status.json.toString()) intent.putExtra(KEY_REDRAFT_STATUS, redraft_status.json.toString())
} }
if(reply_status != null) { if(reply_status != null) {
intent.putExtra(KEY_REPLY_STATUS, reply_status.json.toString()) intent.putExtra(KEY_REPLY_STATUS, reply_status.json.toString())
intent.putExtra(KEY_QUOTED_RENOTE, quotedRenote)
} }
if(initial_text != null) { if(initial_text != null) {
intent.putExtra(KEY_INITIAL_TEXT, initial_text) intent.putExtra(KEY_INITIAL_TEXT, initial_text)
} }
if(sent_intent != null) { if(sent_intent != null) {
intent.putExtra(KEY_SENT_INTENT, sent_intent) intent.putExtra(KEY_SENT_INTENT, sent_intent)
} }
activity.startActivityForResult(intent, request_code) 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?) { override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
if(requestCode == REQUEST_CODE_ATTACHMENT_OLD && resultCode == Activity.RESULT_OK) { if(requestCode == REQUEST_CODE_ATTACHMENT_OLD && resultCode == Activity.RESULT_OK) {
data?.handleGetContentResult(contentResolver)?.forEach { data?.handleGetContentResult(contentResolver)?.forEach {
addAttachment(it.first, it.second) addAttachment(it.first, it.second)
} }
} else if(requestCode == REQUEST_CODE_ATTACHMENT && resultCode == Activity.RESULT_OK) { } else if(requestCode == REQUEST_CODE_ATTACHMENT && resultCode == Activity.RESULT_OK) {
data?.handleGetContentResult(contentResolver)?.forEach { data?.handleGetContentResult(contentResolver)?.forEach {
addAttachment(it.first, it.second) addAttachment(it.first, it.second)
@ -394,7 +405,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
initUI() initUI()
// Android 9 から、明示的にフォーカスを当てる必要がある // Android 9 から、明示的にフォーカスを当てる必要がある
if( savedInstanceState==null){ if(savedInstanceState == null) {
etContent.requestFocus() etContent.requestFocus()
} }
@ -536,45 +547,56 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
if(sv != null && account != null) { if(sv != null && account != null) {
try { try {
val reply_status = TootParser(this@ActPost, account).status(sv.toJsonObject()) val reply_status = TootParser(this@ActPost, account).status(sv.toJsonObject())
val isQuoterRenote = intent.getBooleanExtra(KEY_QUOTED_RENOTE, false)
if(reply_status != null) { 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<String>() if(isQuoterRenote) {
cbQuoteRenote.isChecked = true
val old_mentions = reply_status.mentions
if(old_mentions != null) { // 引用リートはCWやメンションを引き継がない
for(mention in old_mentions) {
val who_acct = mention.acct }else{
if(who_acct.isNotEmpty()) {
if(account.isMe(who_acct)) continue // CW をリプライ元に合わせる
sv = "@" + account.getFullAcct(who_acct) if(reply_status.spoiler_text?.isNotEmpty() == true) {
if(! mention_list.contains(sv)) { cbContentWarning.isChecked = true
mention_list.add(sv) etContentWarning.setText(reply_status.spoiler_text)
}
val mention_list = ArrayList<String>()
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を追加する
// 元レスのacctを追加する val who_acct = account.getFullAcct(reply_status.account)
val who_acct = account.getFullAcct(reply_status.account) if(! account.isMe(reply_status.account) // 自己レスにはメンションを追加しない
if(! account.isMe(reply_status.account) // 自己レスにはメンションを追加しない && ! mention_list.contains("@$who_acct") // 既に含まれているならメンションを追加しない
&& ! mention_list.contains("@$who_acct") // 既に含まれているならメンションを追加しない ) {
) { mention_list.add("@$who_acct")
mention_list.add("@$who_acct") }
}
val sb = StringBuilder()
val sb = StringBuilder() for(acct in mention_list) {
for(acct in mention_list) { if(sb.isNotEmpty()) sb.append(' ')
if(sb.isNotEmpty()) sb.append(' ') sb.append(acct)
sb.append(acct) }
} if(sb.isNotEmpty()) {
if(sb.isNotEmpty()) { appendContentText(sb.append(' ').toString())
appendContentText(sb.append(' ').toString()) }
} }
// リプライ表示をつける // リプライ表示をつける
@ -607,13 +629,10 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
} catch(ex : Throwable) { } catch(ex : Throwable) {
log.trace(ex) log.trace(ex)
} }
} }
} catch(ex : Throwable) { } catch(ex : Throwable) {
log.trace(ex) log.trace(ex)
} }
} }
appendContentText(account?.default_text, selectBefore = true) appendContentText(account?.default_text, selectBefore = true)
@ -650,14 +669,14 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
// 再編集の場合はdefault_textは反映されない // 再編集の場合はdefault_textは反映されない
val decodeOptions = DecodeOptions(this) val decodeOptions = DecodeOptions(this)
var text :Spannable var text : Spannable
text = decodeOptions.decodeHTML(base_status.content) text = decodeOptions.decodeHTML(base_status.content)
etContent.text = text etContent.text = text
etContent.setSelection(text.length ) etContent.setSelection(text.length)
text =decodeOptions.decodeEmoji(base_status.spoiler_text) text = decodeOptions.decodeEmoji(base_status.spoiler_text)
etContentWarning.setText(text) etContentWarning.setText(text)
etContentWarning.setSelection(text.length) etContentWarning.setSelection(text.length)
cbContentWarning.isChecked = text.isNotEmpty() cbContentWarning.isChecked = text.isNotEmpty()
@ -716,7 +735,6 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
showQuotedRenote() showQuotedRenote()
} }
override fun onDestroy() { override fun onDestroy() {
post_helper.onDestroy() post_helper.onDestroy()
@ -783,8 +801,8 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
if(svEmoji.isEmpty()) return if(svEmoji.isEmpty()) return
val editable = etContent.text val editable = etContent.text
if( editable == null ) { if(editable == null) {
val sb = StringBuilder () val sb = StringBuilder()
if(selectBefore) { if(selectBefore) {
val start = 0 val start = 0
sb.append(' ') sb.append(' ')
@ -796,7 +814,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
etContent.setText(sb) etContent.setText(sb)
etContent.setSelection(sb.length) etContent.setSelection(sb.length)
} }
}else{ } else {
if(editable.isNotEmpty() if(editable.isNotEmpty()
&& ! CharacterGroup.isWhitespace(editable[editable.length - 1].toInt()) && ! CharacterGroup.isWhitespace(editable[editable.length - 1].toInt())
) { ) {
@ -860,7 +878,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
etContentWarning = findViewById(R.id.etContentWarning) etContentWarning = findViewById(R.id.etContentWarning)
etContent = findViewById(R.id.etContent) etContent = findViewById(R.id.etContent)
cbQuoteRenote= findViewById(R.id.cbQuoteRenote) cbQuoteRenote = findViewById(R.id.cbQuoteRenote)
cbEnquete = findViewById(R.id.cbEnquete) cbEnquete = findViewById(R.id.cbEnquete)
llEnquete = findViewById(R.id.llEnquete) llEnquete = findViewById(R.id.llEnquete)
@ -895,10 +913,10 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
btnPost.setOnClickListener(this) btnPost.setOnClickListener(this)
btnRemoveReply.setOnClickListener(this) btnRemoveReply.setOnClickListener(this)
val btnPlugin :ImageButton = findViewById(R.id.btnPlugin) val btnPlugin : ImageButton = findViewById(R.id.btnPlugin)
val btnEmojiPicker :ImageButton = findViewById(R.id.btnEmojiPicker) val btnEmojiPicker : ImageButton = findViewById(R.id.btnEmojiPicker)
val btnMore: ImageButton = findViewById(R.id.btnMore) val btnMore : ImageButton = findViewById(R.id.btnMore)
btnPlugin.setOnClickListener(this) btnPlugin.setOnClickListener(this)
btnEmojiPicker.setOnClickListener(this) btnEmojiPicker.setOnClickListener(this)
btnMore.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)) iv.setErrorImageResId(Styler.getAttributeResourceId(this, R.attr.ic_unknown))
} }
setIcon(btnPost,R.drawable.btn_post) setIcon(btnPost, R.drawable.btn_post)
setIcon(btnMore,R.drawable.btn_more) setIcon(btnMore, R.drawable.btn_more)
setIcon(btnPlugin,R.drawable.ic_plugin) setIcon(btnPlugin, R.drawable.ic_plugin)
setIcon(btnEmojiPicker,R.drawable.ic_face) setIcon(btnEmojiPicker, R.drawable.ic_face)
setIcon(btnAttachment,R.drawable.btn_attachment) setIcon(btnAttachment, R.drawable.btn_attachment)
cbContentWarning.setOnCheckedChangeListener { _, _ -> cbContentWarning.setOnCheckedChangeListener { _, _ ->
updateContentWarning() updateContentWarning()
@ -946,15 +964,15 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
etContent.contentMineTypeArray = etContent.contentMineTypeArray =
acceptable_mime_types.toArray(arrayOfNulls<String>(ActPost.acceptable_mime_types.size)) acceptable_mime_types.toArray(arrayOfNulls<String>(ActPost.acceptable_mime_types.size))
etContent.commitContentListener = commitContentListener etContent.commitContentListener = commitContentListener
} }
private fun setIcon(iv:ImageView,drawableId:Int) { private fun setIcon(iv : ImageView, drawableId : Int) {
Styler.setIconDrawableId( Styler.setIconDrawableId(
this, this,
iv, iv,
drawableId, 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) 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() { private fun performAttachmentOld() {
// SAFのIntentで開く // SAFのIntentで開く
try { try {
@ -1562,13 +1579,13 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
// image/j()pg だの image/j(e)pg だの、mime type を誤記するアプリがあまりに多い // image/j()pg だの image/j(e)pg だの、mime type を誤記するアプリがあまりに多い
// クレームで消耗するのを減らすためにファイルヘッダを確認する // クレームで消耗するのを減らすためにファイルヘッダを確認する
if(mimeTypeArg == null || mimeTypeArg.startsWith("image/")){ if(mimeTypeArg == null || mimeTypeArg.startsWith("image/")) {
val sv = checkImageHeaderList(contentResolver,uri) val sv = checkImageHeaderList(contentResolver, uri)
if( sv != null) return sv if(sv != null) return sv
} }
// 既に引数で与えられてる // 既に引数で与えられてる
if(mimeTypeArg?.isNotEmpty() == true){ if(mimeTypeArg?.isNotEmpty() == true) {
return mimeTypeArg return mimeTypeArg
} }
@ -1583,8 +1600,6 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
return null return null
} }
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private fun addAttachment( private fun addAttachment(
uri : Uri, uri : Uri,
@ -1882,11 +1897,13 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
} }
private fun showVisibility() { private fun showVisibility() {
setIcon(btnVisibility,Styler.getVisibilityIcon( setIcon(
this btnVisibility, Styler.getVisibilityIcon(
, account?.isMisskey == true this
, visibility ?: TootVisibility.Public , account?.isMisskey == true
)) , visibility ?: TootVisibility.Public
)
)
} }
private fun performVisibility() { private fun performVisibility() {
@ -1999,7 +2016,8 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
post_helper.attachment_list = this.attachment_list 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 post_helper.redraft_status_id = redraft_status_id
@ -2020,7 +2038,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
private fun showQuotedRenote() { private fun showQuotedRenote() {
val isReply = in_reply_to_id != null val isReply = in_reply_to_id != null
val isMisskey = account?.isMisskey == true 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() { 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_IMAGE, in_reply_to_image)
json.put(DRAFT_REPLY_URL, in_reply_to_url) 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) json.put(DRAFT_IS_ENQUETE, isEnquete)
val array = JSONArray() val array = JSONArray()
@ -2246,7 +2264,6 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
val draft_visibility = TootVisibility val draft_visibility = TootVisibility
.parseSavedVisibility(draft.parseString(DRAFT_VISIBILITY)) .parseSavedVisibility(draft.parseString(DRAFT_VISIBILITY))
val evEmoji = DecodeOptions(this@ActPost, decodeEmoji = true).decodeEmoji(content) val evEmoji = DecodeOptions(this@ActPost, decodeEmoji = true).decodeEmoji(content)
etContent.setText(evEmoji) etContent.setText(evEmoji)
etContent.setSelection(evEmoji.length) etContent.setSelection(evEmoji.length)
@ -2296,7 +2313,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
in_reply_to_image = reply_image in_reply_to_image = reply_image
in_reply_to_url = reply_url in_reply_to_url = reply_url
} }
updateContentWarning() updateContentWarning()
showMediaAttachment() showMediaAttachment()

View File

@ -2,7 +2,6 @@ package jp.juggler.subwaytooter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.graphics.PorterDuff
import android.support.v4.app.ShareCompat import android.support.v4.app.ShareCompat
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.view.View import android.view.View
@ -68,6 +67,7 @@ internal class DlgContextMenu(
val btnBoostAnotherAccount = viewRoot.findViewById<View>(R.id.btnBoostAnotherAccount) val btnBoostAnotherAccount = viewRoot.findViewById<View>(R.id.btnBoostAnotherAccount)
val btnReactionAnotherAccount = viewRoot.findViewById<View>(R.id.btnReactionAnotherAccount) val btnReactionAnotherAccount = viewRoot.findViewById<View>(R.id.btnReactionAnotherAccount)
val btnReplyAnotherAccount = viewRoot.findViewById<View>(R.id.btnReplyAnotherAccount) val btnReplyAnotherAccount = viewRoot.findViewById<View>(R.id.btnReplyAnotherAccount)
val btnQuotedRenote = viewRoot.findViewById<View>(R.id.btnQuotedRenote)
val btnDelete = viewRoot.findViewById<View>(R.id.btnDelete) val btnDelete = viewRoot.findViewById<View>(R.id.btnDelete)
val btnRedraft = viewRoot.findViewById<View>(R.id.btnRedraft) val btnRedraft = viewRoot.findViewById<View>(R.id.btnRedraft)
@ -124,6 +124,7 @@ internal class DlgContextMenu(
btnBoostAnotherAccount.setOnClickListener(this) btnBoostAnotherAccount.setOnClickListener(this)
btnReactionAnotherAccount.setOnClickListener(this) btnReactionAnotherAccount.setOnClickListener(this)
btnReplyAnotherAccount.setOnClickListener(this) btnReplyAnotherAccount.setOnClickListener(this)
btnQuotedRenote.setOnClickListener(this)
btnReport.setOnClickListener(this) btnReport.setOnClickListener(this)
btnMuteApp.setOnClickListener(this) btnMuteApp.setOnClickListener(this)
btnDelete.setOnClickListener(this) btnDelete.setOnClickListener(this)
@ -728,7 +729,12 @@ internal class DlgContextMenu(
access_info, access_info,
status status
) )
R.id.btnQuotedRenote-> Action_Toot.replyFromAnotherAccount(
activity,
access_info,
status,
quotedRenote = true
)
R.id.btnConversationAnotherAccount -> status?.let { status -> R.id.btnConversationAnotherAccount -> status?.let { status ->
Action_Toot.conversationOtherInstance(activity, pos, status) Action_Toot.conversationOtherInstance(activity, pos, status)
} }

View File

@ -10,10 +10,7 @@ import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgConfirm import jp.juggler.subwaytooter.dialog.DlgConfirm
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.EmptyCallback import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.util.LogCategory
import jp.juggler.subwaytooter.util.showToast
import jp.juggler.subwaytooter.util.toPostRequestBuilder
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import org.json.JSONObject import org.json.JSONObject
@ -893,40 +890,25 @@ object Action_Toot {
// reply // reply
fun reply( fun reply(
activity : ActMain, access_info : SavedAccount, status : TootStatus activity : ActMain,
access_info : SavedAccount,
status : TootStatus,
quotedRenote : Boolean = false
) { ) {
ActPost.open( ActPost.open(
activity, activity,
ActMain.REQUEST_CODE_POST, ActMain.REQUEST_CODE_POST,
access_info.db_id, 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( 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 if(remote_status_url == null || remote_status_url.isEmpty()) return
@ -951,7 +933,7 @@ object Action_Toot {
val ls = local_status val ls = local_status
if(ls != null) { if(ls != null) {
reply(activity, access_info, ls) reply(activity, access_info, ls, quotedRenote = quotedRenote)
} else { } else {
showToast(activity, true, result.error) 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( fun redraft(
activity : ActMain, activity : ActMain,
@ -1175,7 +1198,7 @@ object Action_Toot {
activity : ActMain, activity : ActMain,
timeline_account : SavedAccount, timeline_account : SavedAccount,
status : TootStatus?, status : TootStatus?,
code :String? = null code : String? = null
) { ) {
status ?: return status ?: return
@ -1200,4 +1223,5 @@ object Action_Toot {
) )
} }
} }
} }

View File

@ -3,14 +3,12 @@ package jp.juggler.subwaytooter.api
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.api.entity.EntityId import jp.juggler.subwaytooter.api.entity.*
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.table.ClientInfo import jp.juggler.subwaytooter.table.ClientInfo
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.*
import okhttp3.* import okhttp3.*
import org.hjson.JsonObject
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject 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( internal fun simplifyErrorHtml(
response : Response, response : Response,
@ -516,7 +520,7 @@ class TootApiClient(
val result = TootApiResult.makeWithCaption(instance) val result = TootApiResult.makeWithCaption(instance)
if(result.error != null) return result if(result.error != null) return result
val account = this.account ?: return result.setError("account is null") val account = this.account // may null
try { try {
if(! sendRequest(result) { if(! sendRequest(result) {
@ -525,7 +529,7 @@ class TootApiClient(
request_builder.url("https://$instance$path") request_builder.url("https://$instance$path")
val access_token = account.getAccessToken() val access_token = account?.getAccessToken()
if(access_token?.isNotEmpty() == true) { if(access_token?.isNotEmpty() == true) {
request_builder.header("Authorization", "Bearer $access_token") request_builder.header("Authorization", "Bearer $access_token")
} }
@ -801,13 +805,13 @@ class TootApiClient(
val user : JSONObject = token_info.optJSONObject("user") val user : JSONObject = token_info.optJSONObject("user")
?: return result.setError("missing user in the response.") ?: return result.setError("missing user in the response.")
token_info.remove("user") token_info.remove("user")
val apiKey = "$access_token$appSecret".encodeUTF8().digestSHA256().encodeHexLower() val apiKey = "$access_token$appSecret".encodeUTF8().digestSHA256().encodeHexLower()
// ユーザ情報を読めたならtokenInfoを保存する // ユーザ情報を読めたなら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_IS_MISSKEY, true)
token_info.put(KEY_AUTH_VERSION, AUTH_VERSION) token_info.put(KEY_AUTH_VERSION, AUTH_VERSION)
token_info.put(KEY_API_KEY_MISSKEY, apiKey) token_info.put(KEY_API_KEY_MISSKEY, apiKey)
@ -1042,7 +1046,7 @@ class TootApiClient(
// misskeyのインスタンス情報を読めたら、それはmisskeyのインスタンス // misskeyのインスタンス情報を読めたら、それはmisskeyのインスタンス
val r2 = getInstanceInformationMisskey() ?: return null val r2 = getInstanceInformationMisskey() ?: return null
if(r2.jsonObject != null) return r2 if(r2.jsonObject != null) return r2
// マストドンのインスタンス情報を読めたら、それはマストドンのインスタンス // マストドンのインスタンス情報を読めたら、それはマストドンのインスタンス
val r1 = getInstanceInformationMastodon() ?: return null val r1 = getInstanceInformationMastodon() ?: return null
if(r1.jsonObject != null) return r1 if(r1.jsonObject != null) return r1
@ -1465,8 +1469,40 @@ fun TootApiClient.syncAccountByAcct(accessInfo : SavedAccount, acct : String) :
} }
} }
fun TootApiClient.syncStatus(accessInfo : SavedAccount, url : String) = fun TootApiClient.syncStatus(accessInfo : SavedAccount, urlArg : String) : TootApiResult? {
if(accessInfo.isMisskey) {
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 params = accessInfo.putMisskeyApiToken().put("uri", url)
val result = request("/api/ap/show", params.toPostRequestBuilder()) val result = request("/api/ap/show", params.toPostRequestBuilder())
if(result != null) { if(result != null) {
@ -1490,6 +1526,8 @@ fun TootApiClient.syncStatus(accessInfo : SavedAccount, url : String) =
} }
result result
} }
}
private inline fun <Z : Any?> String?.useNotEmpty(block : (String) -> Z?) : Z? = private inline fun <Z : Any?> String?.useNotEmpty(block : (String) -> Z?) : Z? =
if(this?.isNotEmpty() == true) { if(this?.isNotEmpty() == true) {

View File

@ -658,6 +658,15 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
@Suppress("HasPlatformType") @Suppress("HasPlatformType")
private val reTootUriAP2 = Pattern.compile("https?://([^/]+)/@[A-Za-z0-9_]+/(\\d+)") 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 const val INVALID_ID = - 1L
fun parseListTootsearch( fun parseListTootsearch(

View File

@ -101,6 +101,7 @@
android:text="@string/share_url_more" android:text="@string/share_url_more"
android:textAllCaps="false" android:textAllCaps="false"
/> />
<Button <Button
android:id="@+id/btnBoostedBy" android:id="@+id/btnBoostedBy"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -206,6 +207,21 @@
android:textAllCaps="false" android:textAllCaps="false"
/> />
<Button
android:id="@+id/btnQuotedRenote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/btn_bg_transparent"
android:gravity="start|center_vertical"
android:minHeight="32dp"
android:paddingBottom="4dp"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:text="@string/quote_renote"
android:textAllCaps="false"
/>
<Button <Button
android:id="@+id/btnProfilePin" android:id="@+id/btnProfilePin"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -18,6 +18,7 @@
<string name="account_picker_open_setting">どのアカウントの設定を開きますか?</string> <string name="account_picker_open_setting">どのアカウントの設定を開きますか?</string>
<string name="account_picker_open_user_who">どのアカウントでユーザ %1$s のプロフィールを確認しますか?</string> <string name="account_picker_open_user_who">どのアカウントでユーザ %1$s のプロフィールを確認しますか?</string>
<string name="account_picker_reply">どのアカウントで返信しますか?</string> <string name="account_picker_reply">どのアカウントで返信しますか?</string>
<string name="account_picker_quoted_renote">どのアカウントで引用リノートしますか?</string>
<string name="account_picker_toot">どのアカウントでトゥートしますか?</string> <string name="account_picker_toot">どのアカウントでトゥートしますか?</string>
<string name="account_remove">アカウントの削除</string> <string name="account_remove">アカウントの削除</string>
<string name="account_select_please">アカウントを選択してください</string> <string name="account_select_please">アカウントを選択してください</string>
@ -809,5 +810,6 @@
<string name="quick_toot_bar_background_color">簡易投稿入力の背景色</string> <string name="quick_toot_bar_background_color">簡易投稿入力の背景色</string>
<string name="dont_show_preview_card">プレビューカードを表示しない</string> <string name="dont_show_preview_card">プレビューカードを表示しない</string>
<string name="instance_does_not_support_push_api_pleroma">このインスタンスはプッシュ購読APIに対応していません。多分Pleromaです。</string> <string name="instance_does_not_support_push_api_pleroma">このインスタンスはプッシュ購読APIに対応していません。多分Pleromaです。</string>
<string name="quote_renote">引用リノート… (Misskey)</string>
</resources> </resources>

View File

@ -339,6 +339,7 @@
<string name="minimum_column_width">Minimum column width (default=300(dp),app restart required)</string> <string name="minimum_column_width">Minimum column width (default=300(dp),app restart required)</string>
<string name="media_thumbnail_height">Media Thumbnail height (default=64(dp), app restart required)</string> <string name="media_thumbnail_height">Media Thumbnail height (default=64(dp), app restart required)</string>
<string name="account_picker_reply">Which account do you reply from?</string> <string name="account_picker_reply">Which account do you reply from?</string>
<string name="account_picker_quoted_renote">Which account do you quoted renote from?</string>
<string name="reply_from_another_account">Reply from another account</string> <string name="reply_from_another_account">Reply from another account</string>
<string name="launcher_icon_by">Thanks to フタバ for the application icon.</string> <string name="launcher_icon_by">Thanks to フタバ for the application icon.</string>
<string name="ssl_bug_7_0">Android 7.0 can only use the elliptic curve \"prime256v1\".\nUnfortunately your instance seems to not support it.\nThis bug has been fixed in Android 7.1.1.\nYou can either upgrade your OS or ask the administrator of your instance to add support to the elliptic curve \"prime256v1\".\nSee also https://code.google.com/p/android/issues/detail?id=224438</string> <string name="ssl_bug_7_0">Android 7.0 can only use the elliptic curve \"prime256v1\".\nUnfortunately your instance seems to not support it.\nThis bug has been fixed in Android 7.1.1.\nYou can either upgrade your OS or ask the administrator of your instance to add support to the elliptic curve \"prime256v1\".\nSee also https://code.google.com/p/android/issues/detail?id=224438</string>
@ -827,5 +828,6 @@
<string name="quick_toot_bar_background_color">Quick toot bar background color</string> <string name="quick_toot_bar_background_color">Quick toot bar background color</string>
<string name="dont_show_preview_card">Don\'t show preview card</string> <string name="dont_show_preview_card">Don\'t show preview card</string>
<string name="instance_does_not_support_push_api_pleroma">This instance does not support push subscription API. maybe it is Pleroma</string> <string name="instance_does_not_support_push_api_pleroma">This instance does not support push subscription API. maybe it is Pleroma</string>
<string name="quote_renote">Quoted renote… (Misskey)</string>
</resources> </resources>