Misskeyタンスの画像を内蔵メディアビューアで開けないバグの修正。Misskeyの空欄CWがCWにならなかった問題の修正。

This commit is contained in:
tateisu 2018-11-30 19:54:32 +09:00
parent 1748c5f37d
commit ad620c95a6
9 changed files with 133 additions and 108 deletions

View File

@ -87,7 +87,6 @@ class ActMain : AppCompatActivity()
internal val reUrlHashTag =
Pattern.compile("""\Ahttps://([^/]+)/tags/([^?#・\s\-+.,:;/]+)(?:\z|[?#])""")
var boostButtonSize = 0
var timeline_font : Typeface = Typeface.DEFAULT
var timeline_font_bold : Typeface = Typeface.DEFAULT_BOLD
@ -829,7 +828,7 @@ class ActMain : AppCompatActivity()
posted_redraft_id = EntityId.from(data, ActPost.EXTRA_POSTED_REDRAFT_ID)
}
REQUEST_CODE_COLUMN_COLOR -> if(data != null) {
app_state.saveColumnList()
val idx = data.getIntExtra(ActColumnCustomize.EXTRA_COLUMN_INDEX, 0)
@ -1340,7 +1339,6 @@ class ActMain : AppCompatActivity()
}
val column_w_min = (0.5f + column_w_min_dp * density).toInt()
val sw = dm.widthPixels
if(Pref.bpDisableTabletMode(pref) || sw < column_w_min * 2) {
@ -1497,7 +1495,8 @@ class ActMain : AppCompatActivity()
var slide_ratio = 0f
if(vr.first <= vr.last) {
val child = env.tablet_layout_manager.findViewByPosition(vr.first)
slide_ratio = clipRange(0f,1f,abs((child?.left ?: 0) / nColumnWidth.toFloat()))
slide_ratio =
clipRange(0f, 1f, abs((child?.left ?: 0) / nColumnWidth.toFloat()))
}
llColumnStrip.setVisibleRange(vr.first, vr.last, slide_ratio)
@ -1646,7 +1645,7 @@ class ActMain : AppCompatActivity()
}
// queryIntentActivities に渡すURLは実在しないホストのものにする
val intent = Intent(Intent.ACTION_VIEW, "https://dummy.subwaytooter.club/".toUri() )
val intent = Intent(Intent.ACTION_VIEW, "https://dummy.subwaytooter.club/".toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
val resolveInfoList = packageManager.queryIntentActivities(intent, query_flag)
if(resolveInfoList.isEmpty()) {
@ -2504,7 +2503,7 @@ class ActMain : AppCompatActivity()
var column_w_min = (0.5f + column_w_min_dp * density).toInt()
if(column_w_min < 1) column_w_min = 1
var column_w :Int
var column_w : Int
if(screen_width < column_w_min * 2) {
// 最小幅で2つ表示できないのなら1カラム表示
@ -2533,13 +2532,13 @@ class ActMain : AppCompatActivity()
}
nColumnWidth = column_w // dividerの幅を含む
val divider_width = (0.5f + 1f * density).toInt()
column_w -= divider_width
env.tablet_pager_adapter.columnWidth = column_w // dividerの幅を含まない
// env.tablet_snap_helper.columnWidth = column_w //使われていない
resizeAutoCW(column_w)// dividerの幅を含まない
resizeAutoCW(column_w) // dividerの幅を含まない
// 並べ直す
env.tablet_pager_adapter.notifyDataSetChanged()
@ -2803,9 +2802,8 @@ class ActMain : AppCompatActivity()
auto_cw.originalLineCount = l.lineCount
val line_count = auto_cw.originalLineCount
if(nAutoCwLines > 0
&& line_count > nAutoCwLines
&& status.spoiler_text?.isEmpty() != false
if( (nAutoCwLines > 0 && line_count > nAutoCwLines)
&& status.spoiler_text.isEmpty()
&& (status.mentions?.size ?: 0) <= nAutoCwLines
) {
val sb = SpannableStringBuilder()

View File

@ -42,6 +42,7 @@ import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.view.PinchBitmapView
import okhttp3.Request
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.IOException
import java.util.*
@ -66,7 +67,12 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
list?.encodeJson()?.toString() ?: "[]"
internal fun decodeMediaList(serviceType : ServiceType, src : String?) =
parseList(::TootAttachment, serviceType, src?.toJsonArray())
ArrayList<TootAttachment>().apply {
src?.toJsonArray()?.forEach {
if(it !is JSONObject) return@forEach
add(TootAttachment.decodeJson(it))
}
}
fun open(
activity : ActMain,

View File

@ -9,7 +9,6 @@ import android.content.ContentValues
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.database.Cursor
import android.graphics.Bitmap
import android.net.Uri
import android.os.AsyncTask
@ -309,7 +308,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
private val link_click_listener : MyClickableSpanClickCallback = { _, span ->
// ブラウザで開く
span.url.mayUri()?.let{
span.url.mayUri()?.let {
try {
val intent = Intent(Intent.ACTION_VIEW, it)
startActivity(intent)
@ -424,7 +423,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
mushroom_end = savedInstanceState.getInt(STATE_MUSHROOM_END, 0)
redraft_status_id = EntityId.from(savedInstanceState, STATE_REDRAFT_STATUS_ID)
savedInstanceState.getString(STATE_URI_CAMERA_IMAGE).mayUri()?.let{
savedInstanceState.getString(STATE_URI_CAMERA_IMAGE).mayUri()?.let {
uriCameraImage = it
}
@ -516,13 +515,13 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
Intent.ACTION_VIEW -> {
val uri = sent_intent.data
val type = sent_intent.type
if(uri != null) addAttachment(uri,type)
if(uri != null) addAttachment(uri, type)
}
Intent.ACTION_SEND -> {
val uri = sent_intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
val type = sent_intent.type
if(uri != null) addAttachment(uri,type)
if(uri != null) addAttachment(uri, type)
}
Intent.ACTION_SEND_MULTIPLE -> {
@ -558,7 +557,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
} else {
// CW をリプライ元に合わせる
if(reply_status.spoiler_text?.isNotEmpty() == true) {
if(reply_status.spoiler_text.isNotEmpty()) {
cbContentWarning.isChecked = true
etContentWarning.setText(reply_status.spoiler_text)
}
@ -1235,7 +1234,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
llAttachment.visibility = View.GONE
} else {
llAttachment.visibility = View.VISIBLE
ivMedia.forEachIndexed { i,v ->showAttachment_sub(v, i) }
ivMedia.forEachIndexed { i, v -> showAttachment_sub(v, i) }
}
}
@ -1599,25 +1598,23 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
}
}
class AttachmentRequest(
private class AttachmentRequest(
val account : SavedAccount,
val pa : PostAttachment,
val uri : Uri,
val mimeType : String,
val onUploadEnd : () -> Unit
)
val attachment_queue = ConcurrentLinkedQueue<AttachmentRequest>()
private var attachment_worker: AttachmentWorker? = null
var lastAttachmentAdd : Long = 0L
var lastAttachmentComplete : Long = 0L
private val attachment_queue = ConcurrentLinkedQueue<AttachmentRequest>()
private var attachment_worker : AttachmentWorker? = null
private var lastAttachmentAdd : Long = 0L
private var lastAttachmentComplete : Long = 0L
@SuppressLint("StaticFieldLeak")
private fun addAttachment(
uri : Uri,
mimeTypeArg : String? = null,
time :Long =0,
onUploadEnd : () -> Unit = {}
) {
@ -1645,10 +1642,10 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
app_state.attachment_list = this.attachment_list
val pa = PostAttachment( this)
val pa = PostAttachment(this)
attachment_list.add(pa)
showMediaAttachment()
// アップロード開始トースト(連発しない)
val now = System.currentTimeMillis()
if(now - lastAttachmentAdd >= 5000L) {
@ -1659,18 +1656,18 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
// マストドンは添付メディアをID順に表示するため
// 画像が複数ある場合は一つずつ処理する必要がある
// 投稿画面ごとに1スレッドだけ作成してバックグラウンド処理を行う
attachment_queue.add(AttachmentRequest(account,pa,uri,mime_type,onUploadEnd))
attachment_queue.add(AttachmentRequest(account, pa, uri, mime_type, onUploadEnd))
val oldWorker = attachment_worker
if( oldWorker == null || !oldWorker.isAlive || oldWorker.isInterrupted ){
if(oldWorker == null || ! oldWorker.isAlive || oldWorker.isInterrupted) {
oldWorker?.cancel()
attachment_worker = AttachmentWorker().apply{ start() }
}else{
attachment_worker = AttachmentWorker().apply { start() }
} else {
oldWorker.notifyEx()
}
}
inner class AttachmentWorker : WorkerBase(){
inner class AttachmentWorker : WorkerBase() {
private val isCancelled = AtomicBoolean(false)
override fun cancel() {
isCancelled.set(true)
@ -1690,24 +1687,24 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
item.handleResult(result)
}
}
}catch(ex:Throwable){
} catch(ex : Throwable) {
log.trace(ex)
log.e(ex,"AttachmentWorker")
log.e(ex, "AttachmentWorker")
}
}
private fun AttachmentRequest.upload():TootApiResult?{
private fun AttachmentRequest.upload() : TootApiResult? {
if(mimeType.isEmpty()) {
return TootApiResult("mime_type is empty.")
}
try {
val client = TootApiClient(this@ActPost,callback = object:TootApiCallback{
val client = TootApiClient(this@ActPost, callback = object : TootApiCallback {
override val isApiCancelled : Boolean
get() = isCancelled.get()
})
client.account = account
val opener = createOpener(uri, mimeType)
@ -1840,8 +1837,8 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
}
}
fun AttachmentRequest.handleResult(result : TootApiResult?) {
private fun AttachmentRequest.handleResult(result : TootApiResult?) {
if(pa.attachment == null) {
pa.status = PostAttachment.STATUS_UPLOAD_FAILED
@ -1874,7 +1871,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
val a = pa.attachment
if(a != null) {
// アップロード完了
val now = System.currentTimeMillis()
if(now - lastAttachmentComplete >= 5000L) {
showToast(this@ActPost, false, R.string.attachment_uploaded)

View File

@ -188,7 +188,10 @@ class TootAttachment : TootAttachmentLike {
}
constructor(src : JSONObject, decode : Boolean) {
constructor(
src : JSONObject,
decode : Boolean // dummy parameter to use this ctor.
) {
id = if( src.optBoolean(KEY_IS_STRING_ID) ) {
EntityId.mayDefault(src.parseString(KEY_ID))

View File

@ -85,7 +85,12 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
private val language : String?
//If not empty, warning text that should be displayed before the actual content
var spoiler_text : String?
// アプリ内部では空文字列はCWなしとして扱う
// マストドンは「null:CWなし」「空じゃない文字列CWあり」の2種類
// Pleromaは「空文字列CWなし」「空じゃない文字列CWあり」の2種類
// Misskeyは「CWなし」「空欄CW」「CWあり」の3通り。空欄CWはパース時に書き換えてしまう
// Misskeyで投稿が削除された時に変更されるため、val変数にできない
var spoiler_text : String =""
var decoded_spoiler_text : Spannable
// Body of the status; this will contain HTML (remote HTML already sanitized)
@ -114,7 +119,7 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
//One of: public, unlisted, private, direct
val visibility : TootVisibility
val misskeyVisibleIds : ArrayList<String>?
private val misskeyVisibleIds : ArrayList<String>?
// An array of Attachments
val media_attachments : ArrayList<TootAttachmentLike>?
@ -144,10 +149,10 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
val serviceType : ServiceType
val deletedAt : String?
private val deletedAt : String?
val time_deleted_at : Long
var localOnly : Boolean = false
private var localOnly : Boolean = false
///////////////////////////////////////////////////////////////////
// 以下はentityから取得したデータではなく、アプリ内部で使う
@ -202,7 +207,8 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
// お気に入りカラムなどではパース直後に変更することがある
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis = parseMapOrNull(CustomEmoji.decodeMisskey, src.optJSONArray("emojis"), log)
this.custom_emojis =
parseMapOrNull(CustomEmoji.decodeMisskey, src.optJSONArray("emojis"), log)
this.profile_emojis = null
val who = parser.account(src.optJSONObject("user"))
@ -218,8 +224,10 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
this.favourited = src.optBoolean("isFavorited")
this.localOnly = src.optBoolean("localOnly")
this.visibility = TootVisibility.parseMisskey(src.parseString("visibility"),localOnly) ?:
TootVisibility.Public
this.visibility = TootVisibility.parseMisskey(
src.parseString("visibility"),
localOnly
) ?: TootVisibility.Public
this.misskeyVisibleIds = parseStringArray(src.optJSONArray("visibleUserIds"))
@ -281,12 +289,13 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
this
) ?: EMPTY_SPANNABLE
// spoiler_text
this.spoiler_text = reWhitespace
.matcher(src.parseString("cw") ?: "")
.replaceAll(" ")
.sanitizeBDI()
val sv = src.parseString("cw")?.cleanCW()
this.spoiler_text = when{
sv == null -> "" // CWなし
sv.isBlank() -> parser.context.getString(R.string.blank_cw)
else-> sv
}
options = DecodeOptions(
parser.context,
emojiMapCustom = custom_emojis,
@ -316,14 +325,14 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
this.deletedAt = src.parseString("deletedAt")
this.time_deleted_at = parseTime(deletedAt)
if( card == null) {
if(card == null) {
if(reblog != null && hasAnyContent() ) {
if(reblog != null && hasAnyContent()) {
// 引用Renoteにプレビューカードをでっちあげる
card = TootCard(parser, reblog)
} else if(reply != null ) {
} else if(reply != null) {
// 返信にプレビューカードをでっちあげる
card = TootCard(parser, reply!! )
card = TootCard(parser, reply !!)
}
}
@ -446,11 +455,7 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
this.highlight_sound = options.highlight_sound
}
// spoiler_text
this.spoiler_text = reWhitespace
.matcher(src.parseString("spoiler_text") ?: "")
.replaceAll(" ")
.sanitizeBDI()
this.spoiler_text = (src.parseString("spoiler_text") ?: "").cleanCW()
options = DecodeOptions(
parser.context,
@ -576,16 +581,16 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
reblog == null -> true // reblog以外はオリジナルコンテンツがあると見なす
serviceType != ServiceType.MISSKEY -> false // misskey以外のreblogはコンテンツがないと見なす
content?.isNotEmpty() == true
|| spoiler_text?.isNotEmpty() == true
|| spoiler_text.isNotEmpty()
|| media_attachments?.isNotEmpty() == true
|| enquete != null -> true
else -> false
}
// return true if updated
fun increaseReaction(reaction : String?, byMe : Boolean,caller:String) : Boolean {
reaction ?: return false
// return true if updated
fun increaseReaction(reaction : String?, byMe : Boolean, caller : String) : Boolean {
reaction ?: return false
MisskeyReaction.shortcodeMap[reaction] ?: return false
synchronized(this) {
@ -638,8 +643,6 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
@Volatile
internal var muted_word : WordTrieTree? = null
private val reWhitespace = Pattern.compile("[\\s\\t\\x0d\\x0a]+")
val EMPTY_SPANNABLE = SpannableString("")
// OStatus
@ -660,12 +663,12 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
// 公開ステータスページのURL マストドン
@Suppress("HasPlatformType")
val reStatusPage = Pattern.compile("""\Ahttps://([^/]+)/@([A-Za-z0-9_]+)/(\d+)(?:\z|[?#])""")
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""")
val reStatusPageMisskey = Pattern.compile("""\Ahttps://([^/]+)/notes/([0-9a-f]{24})\b""", Pattern.CASE_INSENSITIVE)
const val INVALID_ID = - 1L
@ -857,14 +860,11 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
return if(host != null && host.isNotEmpty() && host != "?") host else null
}
private val reMisskeyNoteUrl =
Pattern.compile("""https://([^/]+)/notes/([0-9A-F]+)""", Pattern.CASE_INSENSITIVE)
fun readMisskeyNoteId(url : String) : EntityId? {
private fun readMisskeyNoteId(url : String) : EntityId? {
// https://misskey.xyz/notes/5b802367744b650030a13640
val m = reMisskeyNoteUrl.matcher(url)
if(m.find()) return EntityIdString(m.group(2))
return null
val m = reStatusPageMisskey.matcher(url)
return if(!m.find()) null else EntityIdString(m.group(2))
}
fun validStatusId(src : EntityId?) : EntityId? {
@ -875,6 +875,10 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
}
}
private fun String.cleanCW() =
CharacterGroup.reWhitespace.matcher(this).replaceAll(" ").sanitizeBDI()
/* 空欄かどうかがCW判定条件に影響するので、trimしてはいけない */
// 投稿元タンスでのステータスIDを調べる
fun findStatusIdFromUri(
uri : String?,

View File

@ -1,6 +1,8 @@
package jp.juggler.subwaytooter.util
import android.util.SparseBooleanArray
import android.util.SparseIntArray
import java.util.regex.Pattern
class CharacterGroup {
@ -20,31 +22,44 @@ class CharacterGroup {
return String(tmp, 0, 1)
}
// 空白とみなす文字なら真
fun isWhitespace(cp : Int) : Boolean {
when(cp) {
private val mapWhitespace = SparseBooleanArray().apply {
intArrayOf(
0x0009 // HORIZONTAL TABULATION
, 0x000A // LINE FEED
, 0x000B // VERTICAL TABULATION
, 0x000C // FORM FEED
, 0x000D // CARRIAGE RETURN
, 0x001C // FILE SEPARATOR
, 0x001D // GROUP SEPARATOR
, 0x001E // RECORD SEPARATOR
, 0x001F // UNIT SEPARATOR
, 0x0020, 0x0085 // next line (latin-1)
, 0x00A0 //非区切りスペース
, 0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007 //非区切りスペース
, 0x2008, 0x2009, 0x200A, 0x200B, 0x200C, 0x200D, 0x2028 // line separator
, 0x2029 // paragraph separator
,
0x202F //非区切りスペース
, 0x205F, 0x2060, 0x3000, 0x3164, 0xFEFF -> return true
else -> return false // Character.isWhitespace( cp ); は不要っぽい
, 0x000A // LINE FEED
, 0x000B // VERTICAL TABULATION
, 0x000C // FORM FEED
, 0x000D // CARRIAGE RETURN
, 0x001C // FILE SEPARATOR
, 0x001D // GROUP SEPARATOR
, 0x001E // RECORD SEPARATOR
, 0x001F // UNIT SEPARATOR
, 0x0020, 0x0085 // next line (latin-1)
, 0x00A0 //非区切りスペース
, 0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007 //非区切りスペース
, 0x2008, 0x2009, 0x200A, 0x200B, 0x200C, 0x200D, 0x2028 // line separator
, 0x2029 // paragraph separator
, 0x202F //非区切りスペース
, 0x205F, 0x2060, 0x3000, 0x3164, 0xFEFF
).forEach {
put(it,true)
}
}
// 空白とみなす文字なら真
fun isWhitespace(cp : Int) : Boolean = mapWhitespace.get(cp,false)
internal val reWhitespace :Pattern by lazy {
val sb = StringBuilder()
sb.append("[\\s\\t\\x0d\\x0a")
for(i in 0 until mapWhitespace.size()){
val k = mapWhitespace.keyAt(i)
if( k > 0x20 ) sb.append(k.toChar())
}
sb.append("]+")
Pattern.compile(sb.toString())
}
// 文字列のリストからグループIDを決定する
private fun findGroupId(list : Array<String>) : Int {
// グループのIDは、グループ中の文字(長さ1)のunicode値の最小

View File

@ -718,11 +718,11 @@ inline fun JSONArray.downForEachIndexed(block : (i : Int, v : Any?) -> Unit) {
}
}
fun JSONArray.toAnyList() : ArrayList<Any> {
val dst_list = ArrayList<Any>(length())
forEach { if(it != null) dst_list.add(it) }
return dst_list
}
//fun JSONArray.toAnyList() : ArrayList<Any> {
// val dst_list = ArrayList<Any>(length())
// forEach { if(it != null) dst_list.add(it) }
// return dst_list
//}
fun JSONArray.toObjectList() : ArrayList<JSONObject> {
val dst_list = ArrayList<JSONObject>(length())

View File

@ -811,5 +811,6 @@
<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>
<string name="blank_cw">(CW空欄)</string>
</resources>

View File

@ -829,5 +829,6 @@
<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>
<string name="blank_cw">(blank CW)</string>
</resources>