1
0
mirror of https://github.com/tateisu/SubwayTooter synced 2025-01-30 18:44:52 +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 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()) {

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_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<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)
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<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を追加する
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<String>(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()

View File

@ -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<View>(R.id.btnBoostAnotherAccount)
val btnReactionAnotherAccount = viewRoot.findViewById<View>(R.id.btnReactionAnotherAccount)
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 btnRedraft = viewRoot.findViewById<View>(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)
}

View File

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

View File

@ -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 <Z : Any?> String?.useNotEmpty(block : (String) -> Z?) : Z? =
if(this?.isNotEmpty() == true) {

View File

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

View File

@ -101,6 +101,7 @@
android:text="@string/share_url_more"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnBoostedBy"
android:layout_width="match_parent"
@ -206,6 +207,21 @@
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
android:id="@+id/btnProfilePin"
android:layout_width="match_parent"

View File

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

View File

@ -339,6 +339,7 @@
<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="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="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>
@ -827,5 +828,6 @@
<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="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>