2018-01-04 19:52:25 +01:00
|
|
|
package jp.juggler.subwaytooter.util
|
|
|
|
|
|
|
|
import android.content.SharedPreferences
|
|
|
|
import android.os.Handler
|
|
|
|
import android.support.v7.app.AlertDialog
|
|
|
|
import android.support.v7.app.AppCompatActivity
|
2018-01-16 07:48:17 +01:00
|
|
|
import android.text.*
|
2018-01-04 19:52:25 +01:00
|
|
|
import android.view.View
|
2018-01-16 07:48:17 +01:00
|
|
|
import jp.juggler.emoji.EmojiMap201709
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
import org.json.JSONArray
|
|
|
|
import org.json.JSONException
|
|
|
|
import org.json.JSONObject
|
|
|
|
|
|
|
|
import java.util.ArrayList
|
|
|
|
import java.util.regex.Pattern
|
|
|
|
|
|
|
|
import jp.juggler.subwaytooter.App1
|
|
|
|
import jp.juggler.subwaytooter.Pref
|
|
|
|
import jp.juggler.subwaytooter.R
|
|
|
|
import jp.juggler.subwaytooter.api.TootApiClient
|
|
|
|
import jp.juggler.subwaytooter.api.TootApiResult
|
|
|
|
import jp.juggler.subwaytooter.api.TootTask
|
|
|
|
import jp.juggler.subwaytooter.api.TootTaskRunner
|
|
|
|
import jp.juggler.subwaytooter.api.TootParser
|
2018-01-10 16:47:35 +01:00
|
|
|
import jp.juggler.subwaytooter.api.entity.*
|
2018-01-04 19:52:25 +01:00
|
|
|
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
|
|
|
import jp.juggler.subwaytooter.dialog.EmojiPicker
|
|
|
|
import jp.juggler.subwaytooter.span.MyClickableSpan
|
|
|
|
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
|
|
|
|
import jp.juggler.subwaytooter.table.AcctColor
|
|
|
|
import jp.juggler.subwaytooter.table.AcctSet
|
|
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
|
|
|
import jp.juggler.subwaytooter.table.TagSet
|
|
|
|
import jp.juggler.subwaytooter.view.MyEditText
|
|
|
|
import okhttp3.Request
|
|
|
|
import okhttp3.RequestBody
|
|
|
|
|
|
|
|
class PostHelper(
|
|
|
|
private val activity : AppCompatActivity,
|
|
|
|
private val pref : SharedPreferences,
|
|
|
|
private val handler : Handler
|
2018-01-16 07:48:17 +01:00
|
|
|
) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
companion object {
|
|
|
|
private val log = LogCategory("PostHelper")
|
|
|
|
|
|
|
|
// [:word:] 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation)
|
|
|
|
// [:alpha:] 英字 (Letter | Mark)
|
|
|
|
|
|
|
|
private const val word = "[_\\p{L}\\p{M}\\p{Nd}\\p{Pc}]"
|
|
|
|
private const val alpha = "[_\\p{L}\\p{M}]"
|
|
|
|
|
|
|
|
private val reTag = Pattern.compile(
|
|
|
|
"(?:^|[^/)\\w])#($word*$alpha$word*)", Pattern.CASE_INSENSITIVE
|
|
|
|
)
|
|
|
|
|
|
|
|
private val reCharsNotTag = Pattern.compile("[・\\s\\-+.,:;/]")
|
|
|
|
private val reCharsNotEmoji = Pattern.compile("[^0-9A-Za-z_-]")
|
|
|
|
|
|
|
|
private val version_1_6 = VersionString("1.6")
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 投稿機能
|
|
|
|
|
|
|
|
var content : String? = null
|
|
|
|
var spoiler_text : String? = null
|
|
|
|
var visibility : String? = null
|
|
|
|
var bNSFW : Boolean = false
|
|
|
|
var in_reply_to_id : Long = 0
|
|
|
|
var attachment_list : ArrayList<PostAttachment>? = null
|
|
|
|
var enquete_items : ArrayList<String>? = null
|
|
|
|
|
|
|
|
fun post(account : SavedAccount, bConfirmTag : Boolean, bConfirmAccount : Boolean, callback : PostCompleteCallback) {
|
2018-01-16 07:48:17 +01:00
|
|
|
val content = this.content ?: ""
|
2018-01-04 19:52:25 +01:00
|
|
|
val spoiler_text = this.spoiler_text
|
|
|
|
val bNSFW = this.bNSFW
|
|
|
|
val in_reply_to_id = this.in_reply_to_id
|
|
|
|
val attachment_list = this.attachment_list
|
|
|
|
val enquete_items = this.enquete_items
|
2018-01-16 07:48:17 +01:00
|
|
|
var visibility = this.visibility ?: ""
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
if(content.isEmpty()) {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(activity, true, R.string.post_error_contents_empty)
|
2018-01-04 19:52:25 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
// nullはCWチェックなしを示す
|
|
|
|
// nullじゃなくてカラならエラー
|
2018-01-16 07:48:17 +01:00
|
|
|
if(spoiler_text != null && spoiler_text.isEmpty()) {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(activity, true, R.string.post_error_contents_warning_empty)
|
2018-01-04 19:52:25 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
if(visibility.isEmpty()) {
|
2018-01-04 19:52:25 +01:00
|
|
|
visibility = TootStatus.VISIBILITY_PUBLIC
|
|
|
|
}
|
|
|
|
|
|
|
|
if(enquete_items?.isNotEmpty() == true) {
|
|
|
|
var n = 0
|
|
|
|
val ne = enquete_items.size
|
|
|
|
while(n < ne) {
|
|
|
|
val item = enquete_items[n]
|
|
|
|
if(item.isEmpty()) {
|
|
|
|
if(n < 2) {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(activity, true, R.string.enquete_item_is_empty, n + 1)
|
2018-01-04 19:52:25 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
val code_count = item.codePointCount(0, item.length)
|
|
|
|
if(code_count > 15) {
|
|
|
|
val over = code_count - 15
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(activity, true, R.string.enquete_item_too_long, n + 1, over)
|
2018-01-04 19:52:25 +01:00
|
|
|
return
|
|
|
|
} else if(n > 0) {
|
|
|
|
for(i in 0 until n) {
|
|
|
|
if(item == enquete_items[i]) {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(activity, true, R.string.enquete_item_duplicate, n + 1)
|
2018-01-04 19:52:25 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
++ n
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(! bConfirmAccount) {
|
|
|
|
DlgConfirm.open(activity, activity.getString(R.string.confirm_post_from, AcctColor.getNickname(account.acct)), object : DlgConfirm.Callback {
|
|
|
|
override var isConfirmEnabled : Boolean
|
|
|
|
get() = account.confirm_post
|
|
|
|
set(bv) {
|
|
|
|
account.confirm_post = bv
|
|
|
|
account.saveSetting()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onOK() {
|
|
|
|
post(account, bConfirmTag, true, callback)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if(! bConfirmTag) {
|
|
|
|
val m = reTag.matcher(content)
|
|
|
|
if(m.find() && TootStatus.VISIBILITY_PUBLIC != visibility) {
|
|
|
|
AlertDialog.Builder(activity)
|
|
|
|
.setCancelable(true)
|
|
|
|
.setMessage(R.string.hashtag_and_visibility_not_match)
|
|
|
|
.setNegativeButton(R.string.cancel, null)
|
|
|
|
.setPositiveButton(R.string.ok) { _, _ -> post(account, true, bConfirmAccount, callback) }
|
|
|
|
.show()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
TootTaskRunner(activity).run(account, object : TootTask {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
internal var status : TootStatus? = null
|
|
|
|
|
|
|
|
internal var instance_tmp : TootInstance? = null
|
|
|
|
|
|
|
|
internal var credential_tmp : TootAccount? = null
|
|
|
|
|
|
|
|
internal fun getInstanceInformation(client : TootApiClient) : TootApiResult? {
|
|
|
|
val result = client.request("/api/v1/instance")
|
2018-01-16 07:48:17 +01:00
|
|
|
instance_tmp = parseItem(::TootInstance, result?.jsonObject)
|
2018-01-04 19:52:25 +01:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
internal fun getCredential(client : TootApiClient) : TootApiResult? {
|
|
|
|
val result = client.request("/api/v1/accounts/verify_credentials")
|
|
|
|
credential_tmp = TootAccount.parse(activity, account, result?.jsonObject)
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun background(client : TootApiClient) : TootApiResult? {
|
|
|
|
|
|
|
|
var visibility_checked : String? = visibility
|
|
|
|
|
|
|
|
if(TootStatus.VISIBILITY_WEB_SETTING == visibility) {
|
|
|
|
var instance = account.instance
|
|
|
|
if(instance == null) {
|
|
|
|
val r2 = getInstanceInformation(client)
|
|
|
|
instance = instance_tmp ?: return r2
|
|
|
|
account.instance = instance
|
|
|
|
}
|
|
|
|
visibility_checked = if(instance.isEnoughVersion(version_1_6)) {
|
|
|
|
null
|
|
|
|
} else {
|
|
|
|
val r2 = getCredential(client)
|
|
|
|
val credential_tmp = this.credential_tmp
|
|
|
|
?: return r2
|
|
|
|
credential_tmp.source?.privacy
|
|
|
|
?: return TootApiResult(activity.getString(R.string.cant_get_web_setting_visibility))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
val request_body : RequestBody
|
|
|
|
val body_string : String
|
|
|
|
|
|
|
|
if(enquete_items?.isNotEmpty() == true) {
|
|
|
|
|
|
|
|
val json = JSONObject()
|
|
|
|
try {
|
2018-01-16 07:48:17 +01:00
|
|
|
json.put("status", EmojiDecoder.decodeShortCode(content))
|
2018-01-04 19:52:25 +01:00
|
|
|
if(visibility_checked != null) {
|
|
|
|
json.put("visibility", visibility_checked)
|
|
|
|
}
|
|
|
|
json.put("sensitive", bNSFW)
|
2018-01-16 07:48:17 +01:00
|
|
|
json.put("spoiler_text", EmojiDecoder.decodeShortCode(spoiler_text ?: ""))
|
2018-01-04 19:52:25 +01:00
|
|
|
json.put("in_reply_to_id", if(in_reply_to_id == - 1L) null else in_reply_to_id)
|
|
|
|
var array = JSONArray()
|
|
|
|
if(attachment_list != null) {
|
|
|
|
for(pa in attachment_list) {
|
|
|
|
val a = pa.attachment
|
|
|
|
if(a != null) array.put(a.id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
json.put("media_ids", array)
|
|
|
|
json.put("isEnquete", true)
|
|
|
|
array = JSONArray()
|
|
|
|
for(item in enquete_items) {
|
|
|
|
array.put(EmojiDecoder.decodeShortCode(item))
|
|
|
|
}
|
|
|
|
json.put("enquete_items", array)
|
|
|
|
} catch(ex : JSONException) {
|
|
|
|
log.trace(ex)
|
|
|
|
log.e(ex, "status encoding failed.")
|
|
|
|
}
|
|
|
|
|
|
|
|
body_string = json.toString()
|
|
|
|
request_body = RequestBody.create(
|
|
|
|
TootApiClient.MEDIA_TYPE_JSON, body_string
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
val sb = StringBuilder()
|
|
|
|
|
|
|
|
sb.append("status=")
|
2018-01-21 13:46:36 +01:00
|
|
|
sb.append(EmojiDecoder.decodeShortCode(content).encodePercent())
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
if(visibility_checked != null) {
|
|
|
|
sb.append("&visibility=")
|
2018-01-21 13:46:36 +01:00
|
|
|
sb.append(visibility_checked.encodePercent())
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if(bNSFW) {
|
|
|
|
sb.append("&sensitive=1")
|
|
|
|
}
|
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
if(spoiler_text?.isNotEmpty() == true) {
|
2018-01-04 19:52:25 +01:00
|
|
|
sb.append("&spoiler_text=")
|
2018-01-21 13:46:36 +01:00
|
|
|
sb.append(EmojiDecoder.decodeShortCode(spoiler_text).encodePercent())
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if(in_reply_to_id != - 1L) {
|
|
|
|
sb.append("&in_reply_to_id=")
|
|
|
|
sb.append(in_reply_to_id.toString())
|
|
|
|
}
|
|
|
|
|
|
|
|
if(attachment_list != null) {
|
|
|
|
for(pa in attachment_list) {
|
|
|
|
val a = pa.attachment
|
|
|
|
if(a != null) sb.append("&media_ids[]=").append(a.id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
body_string = sb.toString()
|
|
|
|
request_body = RequestBody.create(
|
|
|
|
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED, body_string
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
val request_builder = Request.Builder().post(request_body)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
if( ! Pref.bpDontDuplicationCheck(pref) ) {
|
|
|
|
val digest = (body_string + account.acct).digestSHA256()
|
2018-01-04 19:52:25 +01:00
|
|
|
request_builder.header("Idempotency-Key", digest)
|
|
|
|
}
|
|
|
|
|
|
|
|
val result = client.request("/api/v1/statuses", request_builder)
|
|
|
|
val status = TootParser(activity, account).status(result?.jsonObject)
|
|
|
|
this.status = status
|
|
|
|
if(status != null) {
|
|
|
|
// タグを覚えておく
|
|
|
|
val s = status.decoded_content
|
|
|
|
val span_list = s.getSpans(0, s.length, MyClickableSpan::class.java)
|
|
|
|
if(span_list != null) {
|
|
|
|
val tag_list = ArrayList<String?>(span_list.size)
|
|
|
|
for(span in span_list) {
|
|
|
|
val start = s.getSpanStart(span)
|
|
|
|
val end = s.getSpanEnd(span)
|
|
|
|
val text = s.subSequence(start, end).toString()
|
|
|
|
if(text.startsWith("#")) {
|
|
|
|
tag_list.add(text.substring(1))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val count = tag_list.size
|
|
|
|
if(count > 0) {
|
|
|
|
TagSet.saveList(System.currentTimeMillis(), tag_list, 0, count)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun handleResult(result : TootApiResult?) {
|
|
|
|
result ?: return // cancelled.
|
|
|
|
|
|
|
|
val status = this.status
|
|
|
|
if(status != null) {
|
|
|
|
// 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る
|
|
|
|
callback(account, status)
|
|
|
|
} else {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(activity, true, result.error)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 入力補完機能
|
|
|
|
|
|
|
|
private val picker_caption_emoji : String by lazy {
|
|
|
|
activity.getString(R.string.open_picker_emoji)
|
|
|
|
}
|
|
|
|
// private val picker_caption_tag : String by lazy {
|
|
|
|
// activity.getString(R.string.open_picker_tag)
|
|
|
|
// }
|
|
|
|
// private val picker_caption_mention : String by lazy {
|
|
|
|
// activity.getString(R.string.open_picker_mention)
|
|
|
|
// }
|
|
|
|
|
|
|
|
private var callback2 : Callback2? = null
|
|
|
|
private var et : MyEditText? = null
|
|
|
|
private var popup : PopupAutoCompleteAcct? = null
|
|
|
|
private var formRoot : View? = null
|
|
|
|
private var bMainScreen : Boolean = false
|
|
|
|
|
|
|
|
private var instance : String? = null
|
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
private val onEmojiListLoad : (list : ArrayList<CustomEmoji>) -> Unit
|
2018-01-10 16:47:35 +01:00
|
|
|
= { _ : ArrayList<CustomEmoji> ->
|
2018-01-16 07:48:17 +01:00
|
|
|
val popup = this@PostHelper.popup
|
|
|
|
if(popup?.isShowing == true) proc_text_changed.run()
|
|
|
|
}
|
2018-01-10 16:47:35 +01:00
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
private val proc_text_changed = object : Runnable {
|
|
|
|
override fun run() {
|
2018-01-10 16:47:35 +01:00
|
|
|
val et = this@PostHelper.et
|
2018-01-16 07:48:17 +01:00
|
|
|
if(et == null || callback2?.canOpenPopup() != true) {
|
2018-01-04 19:52:25 +01:00
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
var start = et.selectionStart
|
|
|
|
val end = et.selectionEnd
|
2018-01-04 19:52:25 +01:00
|
|
|
if(start != end) {
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
2018-01-16 07:48:17 +01:00
|
|
|
val src = et.text.toString()
|
2018-01-04 19:52:25 +01:00
|
|
|
var count_atMark = 0
|
|
|
|
val pos_atMark = IntArray(2)
|
|
|
|
while(true) {
|
|
|
|
if(count_atMark >= 2) break
|
|
|
|
|
|
|
|
if(start == 0) break
|
|
|
|
val c = src[start - 1]
|
|
|
|
|
|
|
|
if(c == '@') {
|
|
|
|
-- start
|
|
|
|
pos_atMark[count_atMark ++] = start
|
|
|
|
continue
|
|
|
|
} else if('0' <= c && c <= '9'
|
|
|
|
|| 'A' <= c && c <= 'Z'
|
|
|
|
|| 'a' <= c && c <= 'z'
|
|
|
|
|| c == '_' || c == '-' || c == '.') {
|
|
|
|
-- start
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// その他の文字種が出たら探索打ち切り
|
|
|
|
break
|
|
|
|
}
|
|
|
|
// 登場した@の数
|
|
|
|
start = when(count_atMark) {
|
|
|
|
1 -> pos_atMark[0]
|
|
|
|
2 -> pos_atMark[1]
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
// 次はAcctじゃなくてHashtagの補完を試みる
|
|
|
|
checkTag()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 最低でも2文字ないと補完しない
|
|
|
|
// 最低でも2文字ないと補完しない
|
|
|
|
if(end - start < 2) {
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
val limit = 100
|
|
|
|
val s = src.substring(start, end)
|
|
|
|
val acct_list = AcctSet.searchPrefix(s, limit)
|
|
|
|
log.d("search for %s, result=%d", s, acct_list.size)
|
|
|
|
if(acct_list.isEmpty()) {
|
|
|
|
closeAcctPopup()
|
|
|
|
} else {
|
2018-01-16 07:48:17 +01:00
|
|
|
openPopup()?.setList(et, start, end, acct_list, null, null)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun checkTag() {
|
|
|
|
val et = this@PostHelper.et ?: return
|
|
|
|
|
|
|
|
val end = et.selectionEnd
|
|
|
|
|
|
|
|
val src = et.text.toString()
|
|
|
|
val last_sharp = src.lastIndexOf('#', end - 1)
|
|
|
|
|
|
|
|
if(last_sharp == - 1 || end - last_sharp < 2) {
|
|
|
|
checkEmoji()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
val part = src.substring(last_sharp + 1, end)
|
|
|
|
if(reCharsNotTag.matcher(part).find()) {
|
|
|
|
// log.d( "checkTag: character not tag in string %s", part );
|
|
|
|
checkEmoji()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
val limit = 100
|
|
|
|
val s = src.substring(last_sharp + 1, end)
|
|
|
|
val tag_list = TagSet.searchPrefix(s, limit)
|
|
|
|
log.d("search for %s, result=%d", s, tag_list.size)
|
|
|
|
if(tag_list.isEmpty()) {
|
|
|
|
closeAcctPopup()
|
|
|
|
} else {
|
|
|
|
openPopup()?.setList(et, last_sharp, end, tag_list, null, null)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun checkEmoji() {
|
|
|
|
val et = this@PostHelper.et ?: return
|
|
|
|
|
|
|
|
val end = et.selectionEnd
|
|
|
|
val src = et.text.toString()
|
|
|
|
val last_colon = src.lastIndexOf(':', end - 1)
|
|
|
|
|
|
|
|
if(last_colon == - 1 || end - last_colon < 1) {
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
val part = src.substring(last_colon + 1, end)
|
|
|
|
|
|
|
|
if(reCharsNotEmoji.matcher(part).find()) {
|
|
|
|
log.d("checkEmoji: character not short code in string %s", part)
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// : の手前は始端か改行か空白でなければならない
|
|
|
|
if(last_colon > 0 && ! CharacterGroup.isWhitespace(src.codePointBefore(last_colon))) {
|
|
|
|
log.d("checkEmoji: invalid character before shortcode.")
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if(part.isEmpty()) {
|
|
|
|
openPopup()?.setList(
|
|
|
|
et, last_colon, end, null, picker_caption_emoji, open_picker_emoji
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
// 絵文字を部分一致で検索
|
|
|
|
val limit = 100
|
|
|
|
val s = src.substring(last_colon + 1, end).toLowerCase().replace('-', '_')
|
|
|
|
val code_list = EmojiDecoder.searchShortCode(activity, s, limit)
|
|
|
|
log.d("checkEmoji: search for %s, result=%d", s, code_list.size)
|
|
|
|
|
|
|
|
// カスタム絵文字を検索
|
|
|
|
val instance = this@PostHelper.instance
|
|
|
|
if(instance != null && instance.isNotEmpty()) {
|
2018-01-10 16:47:35 +01:00
|
|
|
val custom_list = App1.custom_emoji_lister.getList(instance, onEmojiListLoad)
|
2018-01-04 19:52:25 +01:00
|
|
|
if(custom_list != null) {
|
|
|
|
val needle = src.substring(last_colon + 1, end)
|
|
|
|
for(item in custom_list) {
|
|
|
|
if(code_list.size >= limit) break
|
|
|
|
if(! item.shortcode.contains(needle)) continue
|
|
|
|
|
|
|
|
val sb = SpannableStringBuilder()
|
|
|
|
sb.append(' ')
|
|
|
|
sb.setSpan(NetworkEmojiSpan(item.url), 0, sb.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
|
|
sb.append(' ')
|
|
|
|
sb.append(':')
|
|
|
|
sb.append(item.shortcode)
|
|
|
|
sb.append(':')
|
|
|
|
code_list.add(sb)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
openPopup()?.setList(et, last_colon, end, code_list, picker_caption_emoji, open_picker_emoji)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun openPopup() : PopupAutoCompleteAcct? {
|
|
|
|
var popup = this@PostHelper.popup
|
|
|
|
if(popup?.isShowing == true) return popup
|
|
|
|
val et = this@PostHelper.et ?: return null
|
|
|
|
val formRoot = this@PostHelper.formRoot ?: return null
|
|
|
|
popup = PopupAutoCompleteAcct(activity, et, formRoot, bMainScreen)
|
|
|
|
this@PostHelper.popup = popup
|
|
|
|
return popup
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Callback2 {
|
|
|
|
fun onTextUpdate()
|
|
|
|
|
|
|
|
fun canOpenPopup() : Boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setInstance(_instance : String?) {
|
|
|
|
val instance = _instance?.toLowerCase()
|
|
|
|
this.instance = instance
|
|
|
|
|
|
|
|
if(instance != null) {
|
2018-01-10 16:47:35 +01:00
|
|
|
App1.custom_emoji_lister.getList(instance, onEmojiListLoad)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
val popup = this.popup
|
|
|
|
if(popup?.isShowing == true) {
|
|
|
|
proc_text_changed.run()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun closeAcctPopup() {
|
|
|
|
popup?.dismiss()
|
|
|
|
popup = null
|
|
|
|
}
|
|
|
|
|
|
|
|
fun onScrollChanged() {
|
2018-01-10 16:47:35 +01:00
|
|
|
if(popup?.isShowing == true) {
|
|
|
|
popup?.updatePosition()
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun onDestroy() {
|
|
|
|
handler.removeCallbacks(proc_text_changed)
|
|
|
|
closeAcctPopup()
|
|
|
|
}
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
fun attachEditText(_formRoot : View, et : MyEditText, bMainScreen : Boolean, _callback2 : Callback2) {
|
2018-01-04 19:52:25 +01:00
|
|
|
this.formRoot = _formRoot
|
2018-01-10 16:47:35 +01:00
|
|
|
this.et = et
|
2018-01-04 19:52:25 +01:00
|
|
|
this.callback2 = _callback2
|
|
|
|
this.bMainScreen = bMainScreen
|
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
et.addTextChangedListener(object : TextWatcher {
|
2018-01-04 19:52:25 +01:00
|
|
|
override fun beforeTextChanged(s : CharSequence, start : Int, count : Int, after : Int) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onTextChanged(s : CharSequence, start : Int, before : Int, count : Int) {
|
|
|
|
handler.removeCallbacks(proc_text_changed)
|
2018-01-16 07:48:17 +01:00
|
|
|
handler.postDelayed(proc_text_changed, if(popup?.isShowing == true) 100L else 500L)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun afterTextChanged(s : Editable) {
|
2018-01-10 16:47:35 +01:00
|
|
|
callback2?.onTextUpdate()
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
et.setOnSelectionChangeListener(object : MyEditText.OnSelectionChangeListener {
|
2018-01-04 19:52:25 +01:00
|
|
|
override fun onSelectionChanged(selStart : Int, selEnd : Int) {
|
|
|
|
if(selStart != selEnd) {
|
|
|
|
// 範囲選択されてるならポップアップは閉じる
|
|
|
|
log.d("onSelectionChanged: range selected")
|
|
|
|
closeAcctPopup()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// 全然動いてなさそう…
|
|
|
|
// et.setCustomSelectionActionModeCallback( action_mode_callback );
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
private val open_picker_emoji : Runnable = Runnable {
|
|
|
|
EmojiPicker(activity, instance) { name, instance, bInstanceHasCustomEmoji ->
|
2018-01-14 10:14:39 +01:00
|
|
|
val et = this.et ?: return@EmojiPicker
|
2018-01-16 07:48:17 +01:00
|
|
|
|
|
|
|
val src = et.text
|
|
|
|
val src_length = src.length
|
2018-01-14 10:14:39 +01:00
|
|
|
val end = et.selectionEnd
|
2018-01-16 07:48:17 +01:00
|
|
|
val start = src.lastIndexOf(':', end - 1)
|
|
|
|
if(start == - 1 || end - start < 1) return@EmojiPicker
|
|
|
|
|
|
|
|
val item = EmojiMap201709.sShortNameToImageId[name]
|
|
|
|
val svInsert : Spannable = if(item == null || instance != null ) {
|
|
|
|
SpannableString(":$name: ")
|
|
|
|
}else if(!bInstanceHasCustomEmoji){
|
|
|
|
// 古いタンスだとshortcodeを使う。見た目は絵文字に変える。
|
|
|
|
DecodeOptions().decodeEmoji(activity, ":$name: ")
|
|
|
|
} else {
|
|
|
|
// 十分に新しいタンスなら絵文字のunicodeを使う。見た目は絵文字に変える。
|
|
|
|
DecodeOptions().decodeEmoji(activity, item.unified)
|
|
|
|
}
|
|
|
|
val newText = SpannableStringBuilder()
|
|
|
|
.append(src.subSequence(0, start))
|
2018-01-14 10:14:39 +01:00
|
|
|
.append(svInsert)
|
2018-01-16 07:48:17 +01:00
|
|
|
if( end <src_length) newText.append(src.subSequence(end, src_length))
|
2018-01-14 10:14:39 +01:00
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
et.text = newText
|
|
|
|
et.setSelection(start + svInsert.length)
|
2018-01-14 10:14:39 +01:00
|
|
|
|
|
|
|
proc_text_changed.run()
|
|
|
|
|
|
|
|
// キーボードを再度表示する
|
2018-01-21 13:46:36 +01:00
|
|
|
Handler(activity.mainLooper).post { et.showKeyboard() }
|
2018-01-14 10:14:39 +01:00
|
|
|
|
|
|
|
}.show()
|
|
|
|
}
|
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
fun openEmojiPickerFromMore() {
|
|
|
|
EmojiPicker(activity, instance) { name, instance, bInstanceHasCustomEmoji ->
|
|
|
|
val et = this.et ?: return@EmojiPicker
|
|
|
|
|
|
|
|
val src = et.text
|
|
|
|
val src_length = src.length
|
|
|
|
val start = Math.min(src_length, et.selectionStart)
|
|
|
|
val end = Math.min(src_length, et.selectionEnd)
|
2018-01-14 10:14:39 +01:00
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
val item = EmojiMap201709.sShortNameToImageId[name]
|
|
|
|
val svInsert : Spannable = if(item == null || instance != null || ! bInstanceHasCustomEmoji) {
|
|
|
|
SpannableString(":$name: ")
|
|
|
|
} else {
|
|
|
|
DecodeOptions(decodeEmoji = true).decodeEmoji(activity, item.unified)
|
|
|
|
}
|
|
|
|
val newText = SpannableStringBuilder()
|
|
|
|
.append(src.subSequence(0, start))
|
2018-01-14 10:14:39 +01:00
|
|
|
.append(svInsert)
|
2018-01-16 07:48:17 +01:00
|
|
|
if( end <src_length) newText.append(src.subSequence(end, src_length))
|
2018-01-14 10:14:39 +01:00
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
et.text = newText
|
|
|
|
et.setSelection(start + svInsert.length)
|
2018-01-14 10:14:39 +01:00
|
|
|
|
|
|
|
proc_text_changed.run()
|
|
|
|
}.show()
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// final ActionMode.Callback action_mode_callback = new ActionMode.Callback() {
|
|
|
|
// @Override public boolean onCreateActionMode( ActionMode actionMode, Menu menu ){
|
|
|
|
// actionMode.getMenuInflater().inflate(R.menu.toot_long_tap, menu);
|
|
|
|
// return true;
|
|
|
|
// }
|
|
|
|
// @Override public void onDestroyActionMode( ActionMode actionMode ){
|
|
|
|
//
|
|
|
|
// }
|
|
|
|
// @Override public boolean onPrepareActionMode( ActionMode actionMode, Menu menu ){
|
|
|
|
// return false;
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// @Override
|
|
|
|
// public boolean onActionItemClicked( ActionMode actionMode, MenuItem item ){
|
|
|
|
// if (item.getItemId() == R.id.action_pick_emoji) {
|
|
|
|
// actionMode.finish();
|
|
|
|
// EmojiPicker.open( activity, instance, new EmojiPicker.Callback() {
|
|
|
|
// @Override public void onPickedEmoji( String name ){
|
|
|
|
// int end = et.getSelectionEnd();
|
|
|
|
// String src = et.getText().toString();
|
|
|
|
// CharSequence svInsert = ":" + name + ":";
|
|
|
|
// src = src.substring( 0, end ) + svInsert + " " + ( end >= src.length() ? "" : src.substring( end ) );
|
|
|
|
// et.setText( src );
|
|
|
|
// et.setSelection( end + svInsert.length() + 1 );
|
|
|
|
//
|
|
|
|
// proc_text_changed.run();
|
|
|
|
// }
|
|
|
|
// } );
|
|
|
|
// return true;
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// return false;
|
|
|
|
// }
|
|
|
|
// };
|
|
|
|
|
|
|
|
}
|