2018-01-04 19:52:25 +01:00
|
|
|
package jp.juggler.subwaytooter.util
|
|
|
|
|
|
|
|
import android.content.SharedPreferences
|
|
|
|
import android.os.Handler
|
2018-06-28 11:15:31 +02:00
|
|
|
import android.os.SystemClock
|
2019-02-15 02:51:22 +01:00
|
|
|
import androidx.appcompat.app.AlertDialog
|
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
2018-01-16 07:48:17 +01:00
|
|
|
import android.text.*
|
2018-11-06 02:29:33 +01:00
|
|
|
import android.text.style.ForegroundColorSpan
|
2018-01-04 19:52:25 +01:00
|
|
|
import android.view.View
|
2019-09-14 22:09:52 +02:00
|
|
|
import jp.juggler.emoji.EmojiMap
|
2018-01-04 19:52:25 +01:00
|
|
|
import jp.juggler.subwaytooter.App1
|
|
|
|
import jp.juggler.subwaytooter.Pref
|
|
|
|
import jp.juggler.subwaytooter.R
|
2018-12-01 00:02:18 +01:00
|
|
|
import jp.juggler.subwaytooter.api.*
|
2018-01-10 16:47:35 +01:00
|
|
|
import jp.juggler.subwaytooter.api.entity.*
|
2019-09-12 19:16:07 +02:00
|
|
|
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
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
|
2018-12-01 00:02:18 +01:00
|
|
|
import jp.juggler.util.*
|
2018-01-04 19:52:25 +01:00
|
|
|
import okhttp3.Request
|
2019-08-24 05:35:22 +02:00
|
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
2018-06-28 11:43:46 +02:00
|
|
|
import java.lang.ref.WeakReference
|
2018-12-01 00:02:18 +01:00
|
|
|
import java.util.*
|
|
|
|
import java.util.regex.Pattern
|
2019-07-20 08:32:43 +02:00
|
|
|
import kotlin.math.min
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
private val reCharsNotEmoji = Pattern.compile("[^0-9A-Za-z_-]")
|
2019-09-30 23:27:21 +02:00
|
|
|
|
|
|
|
private val reAscii = Pattern.compile("""[\x00-\x7f]""")
|
|
|
|
private val reNotAscii = Pattern.compile("""[^\x00-\x7f]""")
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
2019-01-06 09:20:37 +01:00
|
|
|
interface PostCompleteCallback {
|
|
|
|
fun onPostComplete(target_account : SavedAccount, status : TootStatus)
|
|
|
|
fun onScheduledPostComplete(target_account : SavedAccount)
|
|
|
|
}
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 投稿機能
|
|
|
|
|
|
|
|
var content : String? = null
|
|
|
|
var spoiler_text : String? = null
|
2018-08-20 19:37:42 +02:00
|
|
|
var visibility : TootVisibility = TootVisibility.Public
|
2019-01-06 09:20:37 +01:00
|
|
|
var bNSFW = false
|
2018-08-18 12:58:14 +02:00
|
|
|
var in_reply_to_id : EntityId? = null
|
2018-01-04 19:52:25 +01:00
|
|
|
var attachment_list : ArrayList<PostAttachment>? = null
|
|
|
|
var enquete_items : ArrayList<String>? = null
|
2019-04-14 05:41:29 +02:00
|
|
|
var poll_type : TootPollsType? = null
|
2019-03-05 06:55:47 +01:00
|
|
|
var poll_expire_seconds = 0
|
|
|
|
var poll_hide_totals = false
|
|
|
|
var poll_multiple_choice = false
|
|
|
|
|
2018-06-23 04:43:18 +02:00
|
|
|
var emojiMapCustom : HashMap<String, CustomEmoji>? = null
|
2018-08-18 12:58:14 +02:00
|
|
|
var redraft_status_id : EntityId? = null
|
2019-01-06 09:20:37 +01:00
|
|
|
var useQuotedRenote = false
|
|
|
|
var scheduledAt = 0L
|
2019-01-06 15:55:25 +01:00
|
|
|
var scheduledId : EntityId? = null
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-06-28 11:15:31 +02:00
|
|
|
private var last_post_tapped : Long = 0L
|
|
|
|
|
2018-06-28 11:43:46 +02:00
|
|
|
private var last_post_task : WeakReference<TootTaskRunner>? = null
|
2018-06-28 11:15:31 +02:00
|
|
|
|
2019-09-30 23:27:21 +02:00
|
|
|
fun post(account : SavedAccount, callback : PostCompleteCallback) = post(
|
|
|
|
account,
|
|
|
|
callback,
|
|
|
|
bConfirmTag = false,
|
|
|
|
bConfirmAccount = false,
|
|
|
|
bConfirmRedraft = false,
|
|
|
|
bConfirmTagCharacter = false
|
|
|
|
)
|
|
|
|
|
2018-01-21 17:47:13 +01:00
|
|
|
fun post(
|
|
|
|
account : SavedAccount,
|
2019-09-30 23:27:21 +02:00
|
|
|
callback : PostCompleteCallback,
|
|
|
|
bConfirmTag : Boolean,
|
|
|
|
bConfirmAccount : Boolean,
|
|
|
|
bConfirmRedraft : Boolean,
|
|
|
|
bConfirmTagCharacter : Boolean
|
2018-01-21 17:47:13 +01:00
|
|
|
) {
|
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
|
2019-04-14 05:47:23 +02:00
|
|
|
val poll_type = this.poll_type
|
|
|
|
val poll_expire_seconds = this.poll_expire_seconds
|
|
|
|
val poll_hide_totals = this.poll_hide_totals
|
|
|
|
val poll_multiple_choice = this.poll_multiple_choice
|
2019-03-05 06:55:47 +01:00
|
|
|
|
2018-08-20 19:37:42 +02:00
|
|
|
val visibility = this.visibility
|
2019-01-06 09:20:37 +01:00
|
|
|
val scheduledAt = this.scheduledAt
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-03-09 20:54:03 +01:00
|
|
|
val hasAttachment = attachment_list?.isNotEmpty() ?: false
|
|
|
|
|
2018-06-23 04:43:18 +02:00
|
|
|
if(! hasAttachment && 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
|
|
|
|
}
|
|
|
|
|
|
|
|
if(enquete_items?.isNotEmpty() == true) {
|
2019-03-05 06:55:47 +01:00
|
|
|
|
2019-04-14 05:41:29 +02:00
|
|
|
val choice_max_chars = when {
|
|
|
|
isMisskey -> 15
|
|
|
|
poll_type == TootPollsType.FriendsNico -> 15
|
|
|
|
else -> 25 // TootPollsType.Mastodon
|
2019-03-05 06:55:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
for(n in 0 until enquete_items.size) {
|
2018-01-04 19:52:25 +01:00
|
|
|
val item = enquete_items[n]
|
2019-03-05 06:55:47 +01:00
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
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)
|
2019-03-05 06:55:47 +01:00
|
|
|
if(code_count > choice_max_chars) {
|
|
|
|
val over = code_count - choice_max_chars
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(! bConfirmAccount) {
|
2018-01-21 17:47:13 +01:00
|
|
|
DlgConfirm.open(
|
|
|
|
activity,
|
2020-02-01 14:26:57 +01:00
|
|
|
activity.getString(R.string.confirm_post_from, AcctColor.getNickname(account)),
|
2018-01-21 17:47:13 +01:00
|
|
|
object : DlgConfirm.Callback {
|
|
|
|
override var isConfirmEnabled : Boolean
|
|
|
|
get() = account.confirm_post
|
|
|
|
set(bv) {
|
|
|
|
account.confirm_post = bv
|
|
|
|
account.saveSetting()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onOK() {
|
2019-09-30 23:27:21 +02:00
|
|
|
post(
|
|
|
|
account, callback,
|
|
|
|
bConfirmTag = bConfirmTag,
|
|
|
|
bConfirmAccount = true,
|
|
|
|
bConfirmRedraft = bConfirmRedraft,
|
|
|
|
bConfirmTagCharacter = bConfirmTagCharacter
|
|
|
|
)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
2018-01-21 17:47:13 +01:00
|
|
|
})
|
2018-01-04 19:52:25 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-09-30 23:27:21 +02:00
|
|
|
if(! bConfirmTagCharacter && Pref.bpWarnHashtagAsciiAndNonAscii(App1.pref)) {
|
|
|
|
val tags = TootTag.findHashtags(content, isMisskey)
|
|
|
|
val badTags = tags
|
|
|
|
?.filter {
|
|
|
|
val hasAscii = reAscii.matcher(it).find()
|
|
|
|
val hasNotAscii = reNotAscii.matcher(it).find()
|
|
|
|
hasAscii && hasNotAscii
|
|
|
|
}
|
|
|
|
?.map { "#$it" }
|
|
|
|
if(badTags?.isNotEmpty() == true) {
|
|
|
|
|
|
|
|
AlertDialog.Builder(activity)
|
|
|
|
.setCancelable(true)
|
|
|
|
.setMessage(
|
|
|
|
activity.getString(
|
|
|
|
R.string.hashtag_contains_ascii_and_not_ascii,
|
|
|
|
badTags.joinToString(", ")
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.setNegativeButton(R.string.cancel, null)
|
|
|
|
.setPositiveButton(R.string.ok) { _, _ ->
|
|
|
|
post(
|
|
|
|
account, callback,
|
|
|
|
bConfirmTag = bConfirmTag,
|
|
|
|
bConfirmAccount = bConfirmAccount,
|
|
|
|
bConfirmRedraft = bConfirmRedraft,
|
|
|
|
bConfirmTagCharacter = true
|
|
|
|
)
|
|
|
|
}
|
|
|
|
.show()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
if(! bConfirmTag) {
|
2019-07-20 08:47:03 +02:00
|
|
|
val isMisskey = account.isMisskey
|
|
|
|
if(! visibility.isTagAllowed(isMisskey)) {
|
|
|
|
val tags = TootTag.findHashtags(content, isMisskey)
|
|
|
|
if(tags != null) {
|
|
|
|
|
|
|
|
log.d("findHashtags ${tags.joinToString(",")}")
|
|
|
|
|
|
|
|
AlertDialog.Builder(activity)
|
|
|
|
.setCancelable(true)
|
|
|
|
.setMessage(R.string.hashtag_and_visibility_not_match)
|
|
|
|
.setNegativeButton(R.string.cancel, null)
|
|
|
|
.setPositiveButton(R.string.ok) { _, _ ->
|
|
|
|
post(
|
2019-09-30 23:27:21 +02:00
|
|
|
account, callback,
|
2019-07-20 08:47:03 +02:00
|
|
|
bConfirmTag = true,
|
|
|
|
bConfirmAccount = bConfirmAccount,
|
|
|
|
bConfirmRedraft = bConfirmRedraft,
|
2019-09-30 23:27:21 +02:00
|
|
|
bConfirmTagCharacter = bConfirmTagCharacter
|
2019-07-20 08:47:03 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
.show()
|
|
|
|
return
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-20 02:07:55 +02:00
|
|
|
if(! bConfirmRedraft && redraft_status_id != null) {
|
2018-06-23 04:43:18 +02:00
|
|
|
AlertDialog.Builder(activity)
|
|
|
|
.setCancelable(true)
|
|
|
|
.setMessage(R.string.delete_base_status_before_toot)
|
|
|
|
.setNegativeButton(R.string.cancel, null)
|
|
|
|
.setPositiveButton(R.string.ok) { _, _ ->
|
|
|
|
post(
|
2019-09-30 23:27:21 +02:00
|
|
|
account, callback,
|
|
|
|
bConfirmTag = bConfirmTag,
|
|
|
|
bConfirmAccount = bConfirmAccount,
|
|
|
|
bConfirmRedraft = true,
|
|
|
|
bConfirmTagCharacter = bConfirmTagCharacter
|
2018-06-23 04:43:18 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
.show()
|
|
|
|
return
|
|
|
|
}
|
2019-01-06 15:55:25 +01:00
|
|
|
if(! bConfirmRedraft && scheduledId != null) {
|
|
|
|
AlertDialog.Builder(activity)
|
|
|
|
.setCancelable(true)
|
|
|
|
.setMessage(R.string.delete_scheduled_status_before_update)
|
|
|
|
.setNegativeButton(R.string.cancel, null)
|
|
|
|
.setPositiveButton(R.string.ok) { _, _ ->
|
|
|
|
post(
|
2019-09-30 23:27:21 +02:00
|
|
|
account, callback,
|
|
|
|
bConfirmTag = bConfirmTag,
|
|
|
|
bConfirmAccount = bConfirmAccount,
|
|
|
|
bConfirmRedraft = true,
|
|
|
|
bConfirmTagCharacter = bConfirmTagCharacter
|
2019-01-06 15:55:25 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
.show()
|
|
|
|
return
|
|
|
|
}
|
2018-06-28 11:15:31 +02:00
|
|
|
// 確認を終えたらボタン連打判定
|
2018-08-15 04:06:25 +02:00
|
|
|
|
2018-06-28 11:43:46 +02:00
|
|
|
if(last_post_task?.get()?.isActive == true) {
|
|
|
|
showToast(activity, false, R.string.post_button_tapped_repeatly)
|
|
|
|
return
|
|
|
|
}
|
2018-08-15 04:06:25 +02:00
|
|
|
|
2018-06-28 11:15:31 +02:00
|
|
|
val now = SystemClock.elapsedRealtime()
|
|
|
|
val delta = now - last_post_tapped
|
|
|
|
last_post_tapped = now
|
|
|
|
if(delta < 1000L) {
|
|
|
|
showToast(activity, false, R.string.post_button_tapped_repeatly)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// 全ての確認を終えたらバックグラウンドでの処理を開始する
|
2018-06-28 11:43:46 +02:00
|
|
|
last_post_task = WeakReference(TootTaskRunner(activity
|
2018-06-28 11:15:31 +02:00
|
|
|
, progressSetupCallback = { progressDialog ->
|
|
|
|
progressDialog.setCanceledOnTouchOutside(false)
|
|
|
|
}
|
|
|
|
).run(account, object : TootTask {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-06-28 11:15:31 +02:00
|
|
|
var status : TootStatus? = null
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-06-28 11:15:31 +02:00
|
|
|
var credential_tmp : TootAccount? = null
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-11-06 02:29:33 +01:00
|
|
|
val parser = TootParser(activity, account)
|
2018-08-20 02:07:55 +02:00
|
|
|
|
2019-01-06 09:20:37 +01:00
|
|
|
var scheduledStatusSucceeded = false
|
|
|
|
|
2018-06-28 11:15:31 +02:00
|
|
|
fun getCredential(client : TootApiClient) : TootApiResult? {
|
2018-01-04 19:52:25 +01:00
|
|
|
val result = client.request("/api/v1/accounts/verify_credentials")
|
2018-08-20 02:07:55 +02:00
|
|
|
credential_tmp = parser.account(result?.jsonObject)
|
2018-01-04 19:52:25 +01:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun background(client : TootApiClient) : TootApiResult? {
|
|
|
|
|
2018-06-23 04:43:18 +02:00
|
|
|
var result : TootApiResult?
|
2018-06-28 11:15:31 +02:00
|
|
|
|
2018-06-23 04:43:18 +02:00
|
|
|
// 元の投稿を削除する
|
2018-08-20 02:07:55 +02:00
|
|
|
if(redraft_status_id != null) {
|
2018-11-06 02:29:33 +01:00
|
|
|
result = if(isMisskey) {
|
2020-01-07 09:03:32 +01:00
|
|
|
val params = account.putMisskeyApiToken(JsonObject()).apply {
|
2018-11-06 02:29:33 +01:00
|
|
|
put("noteId", redraft_status_id)
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
|
|
|
client.request(
|
|
|
|
"/api/notes/delete",
|
|
|
|
params.toPostRequestBuilder()
|
|
|
|
)
|
2018-11-06 02:29:33 +01:00
|
|
|
} else {
|
2018-08-20 02:07:55 +02:00
|
|
|
client.request(
|
|
|
|
"/api/v1/statuses/$redraft_status_id",
|
|
|
|
Request.Builder().delete()
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
2018-06-23 04:43:18 +02:00
|
|
|
log.d("delete redraft. result=$result")
|
|
|
|
Thread.sleep(2000L)
|
2019-03-05 06:55:47 +01:00
|
|
|
} else if(scheduledId != null) {
|
2019-01-06 15:55:25 +01:00
|
|
|
val r1 = client.request(
|
|
|
|
"/api/v1/scheduled_statuses/$scheduledId",
|
|
|
|
Request.Builder().delete()
|
|
|
|
)
|
|
|
|
log.d("delete old scheduled status. result=$r1")
|
|
|
|
Thread.sleep(2000L)
|
2018-06-23 04:43:18 +02:00
|
|
|
}
|
|
|
|
|
2018-08-20 19:37:42 +02:00
|
|
|
var visibility_checked : TootVisibility? = visibility
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2019-10-06 13:23:33 +02:00
|
|
|
val (instance, ri) = TootInstance.get(client)
|
|
|
|
instance ?: return ri
|
2019-09-30 23:27:21 +02:00
|
|
|
|
|
|
|
if(instance.instanceType == TootInstance.InstanceType.Pixelfed) {
|
|
|
|
if(in_reply_to_id != null && attachment_list?.isNotEmpty() == true) {
|
|
|
|
return TootApiResult(activity.getString(R.string.pixelfed_does_not_allow_reply_with_media))
|
|
|
|
}
|
|
|
|
if(in_reply_to_id == null && attachment_list?.isNotEmpty() != true) {
|
|
|
|
return TootApiResult(activity.getString(R.string.pixelfed_does_not_allow_post_without_media))
|
|
|
|
}
|
|
|
|
}
|
2018-06-28 11:15:31 +02:00
|
|
|
|
2018-11-06 02:29:33 +01:00
|
|
|
if(visibility == TootVisibility.WebSetting) {
|
|
|
|
visibility_checked =
|
|
|
|
if(account.isMisskey || instance.versionGE(TootInstance.VERSION_1_6)) {
|
|
|
|
null
|
|
|
|
} else {
|
|
|
|
val r2 = getCredential(client)
|
|
|
|
val credential_tmp = this.credential_tmp ?: return r2
|
|
|
|
val privacy = credential_tmp.source?.privacy
|
|
|
|
?: return TootApiResult(activity.getString(R.string.cant_get_web_setting_visibility))
|
|
|
|
TootVisibility.parseMastodon(privacy)
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
2020-01-07 09:03:32 +01:00
|
|
|
val json = JsonObject()
|
2018-06-23 04:43:18 +02:00
|
|
|
try {
|
2018-08-20 02:07:55 +02:00
|
|
|
if(account.isMisskey) {
|
|
|
|
account.putMisskeyApiToken(json)
|
2020-01-07 09:03:32 +01:00
|
|
|
json["text"] = EmojiDecoder.decodeShortCode(
|
|
|
|
content,
|
|
|
|
emojiMapCustom = emojiMapCustom
|
2018-06-23 04:43:18 +02:00
|
|
|
)
|
2018-08-20 02:07:55 +02:00
|
|
|
if(visibility_checked != null) {
|
2018-08-20 19:37:42 +02:00
|
|
|
|
2019-07-20 08:47:03 +02:00
|
|
|
if(visibility_checked == TootVisibility.DirectSpecified || visibility_checked == TootVisibility.DirectPrivate) {
|
2020-01-07 09:03:32 +01:00
|
|
|
val userIds = JsonArray()
|
2018-11-06 02:29:33 +01:00
|
|
|
val reMention =
|
|
|
|
Pattern.compile("(?:\\A|\\s)@([a-zA-Z0-9_]{1,20})(?:@([\\w.:-]+))?(?:\\z|\\s)")
|
2018-08-20 19:37:42 +02:00
|
|
|
val m = reMention.matcher(content)
|
2018-11-06 02:29:33 +01:00
|
|
|
while(m.find()) {
|
2019-09-12 16:05:18 +02:00
|
|
|
val username = m.groupEx(1)
|
|
|
|
val host = m.groupEx(2)
|
2020-01-07 09:03:32 +01:00
|
|
|
|
2018-11-06 02:29:33 +01:00
|
|
|
result = client.request(
|
|
|
|
"/api/users/show",
|
2020-01-07 09:03:32 +01:00
|
|
|
account.putMisskeyApiToken().apply {
|
|
|
|
if(username?.isNotEmpty() == true) put(
|
|
|
|
"username",
|
|
|
|
username
|
|
|
|
)
|
|
|
|
if(host?.isNotEmpty() == true) put(
|
|
|
|
"host",
|
|
|
|
host
|
|
|
|
)
|
|
|
|
}
|
|
|
|
.toPostRequestBuilder()
|
2018-11-06 02:29:33 +01:00
|
|
|
)
|
2020-01-08 04:23:45 +01:00
|
|
|
val id = result?.jsonObject?.string("id")
|
2018-11-06 02:29:33 +01:00
|
|
|
if(id?.isNotEmpty() == true) {
|
2020-01-07 09:03:32 +01:00
|
|
|
userIds.add(id)
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
|
|
|
}
|
2020-01-07 09:03:32 +01:00
|
|
|
json["visibility"] = when {
|
|
|
|
userIds.isNotEmpty() -> {
|
|
|
|
json["visibleUserIds"] = userIds
|
|
|
|
"specified"
|
2018-11-06 02:29:33 +01:00
|
|
|
}
|
2020-01-07 09:03:32 +01:00
|
|
|
|
|
|
|
account.misskeyVersion >= 11 -> "specified"
|
|
|
|
else -> "private"
|
|
|
|
}
|
2018-11-06 02:29:33 +01:00
|
|
|
} else {
|
2018-11-30 06:22:17 +01:00
|
|
|
val localVis = visibility_checked.strMisskey.replace(
|
|
|
|
"^local-".toRegex(),
|
|
|
|
""
|
|
|
|
)
|
|
|
|
if(localVis != visibility_checked.strMisskey) {
|
2020-01-07 09:03:32 +01:00
|
|
|
json["localOnly"] = true
|
|
|
|
json["visibility"] = localVis
|
2018-11-30 06:22:17 +01:00
|
|
|
} else {
|
2020-01-07 09:03:32 +01:00
|
|
|
json["visibility"] = visibility_checked.strMisskey
|
2018-11-16 00:43:10 +01:00
|
|
|
}
|
2018-08-20 19:37:42 +02:00
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
2018-08-20 02:07:55 +02:00
|
|
|
|
2018-11-06 02:29:33 +01:00
|
|
|
if(spoiler_text?.isNotEmpty() == true) {
|
2020-01-07 09:03:32 +01:00
|
|
|
json["cw"] = EmojiDecoder.decodeShortCode(
|
|
|
|
spoiler_text,
|
|
|
|
emojiMapCustom = emojiMapCustom
|
2018-06-23 04:43:18 +02:00
|
|
|
)
|
|
|
|
}
|
2018-08-20 02:07:55 +02:00
|
|
|
|
|
|
|
if(in_reply_to_id != null) {
|
2018-11-30 06:22:17 +01:00
|
|
|
if(useQuotedRenote) {
|
2020-01-07 09:03:32 +01:00
|
|
|
json["renoteId"] = in_reply_to_id.toString()
|
2018-11-30 06:22:17 +01:00
|
|
|
} else {
|
2020-01-07 09:03:32 +01:00
|
|
|
json["replyId"] = in_reply_to_id.toString()
|
2018-11-12 18:19:57 +01:00
|
|
|
}
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
|
|
|
|
2020-01-07 09:03:32 +01:00
|
|
|
json["viaMobile"] = true
|
2018-08-20 02:07:55 +02:00
|
|
|
|
|
|
|
if(attachment_list != null) {
|
2020-01-07 09:03:32 +01:00
|
|
|
val array = JsonArray()
|
2018-08-20 02:07:55 +02:00
|
|
|
for(pa in attachment_list) {
|
|
|
|
val a = pa.attachment ?: continue
|
|
|
|
// Misskeyは画像の再利用に問題がないので redraftとバージョンのチェックは行わない
|
2020-01-07 09:03:32 +01:00
|
|
|
array.add(a.id.toString())
|
2018-08-21 02:26:49 +02:00
|
|
|
|
|
|
|
// Misskeyの場合、NSFWするにはアップロード済みの画像を drive/files/update で更新する
|
2018-11-06 02:29:33 +01:00
|
|
|
if(bNSFW) {
|
|
|
|
val r = client.request(
|
|
|
|
"/api/drive/files/update",
|
2020-01-07 09:03:32 +01:00
|
|
|
account.putMisskeyApiToken().apply {
|
|
|
|
put("fileId", a.id.toString())
|
|
|
|
put("isSensitive", true)
|
|
|
|
}
|
|
|
|
.toPostRequestBuilder()
|
2018-11-06 02:29:33 +01:00
|
|
|
)
|
|
|
|
if(r == null || r.error != null) return r
|
2018-08-21 02:26:49 +02:00
|
|
|
}
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
2020-01-07 09:03:32 +01:00
|
|
|
if(array.isNotEmpty()) json["mediaIds"] = array
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if(enquete_items?.isNotEmpty() == true) {
|
2020-01-07 09:03:32 +01:00
|
|
|
val choices = JsonArray().apply {
|
2018-08-20 02:07:55 +02:00
|
|
|
for(item in enquete_items) {
|
2018-11-06 02:29:33 +01:00
|
|
|
val text = EmojiDecoder.decodeShortCode(
|
2018-08-20 02:07:55 +02:00
|
|
|
item,
|
|
|
|
emojiMapCustom = emojiMapCustom
|
|
|
|
)
|
2018-11-06 02:29:33 +01:00
|
|
|
if(text.isEmpty()) continue
|
2020-01-07 09:03:32 +01:00
|
|
|
add(text)
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
|
|
|
}
|
2020-01-07 09:03:32 +01:00
|
|
|
if(choices.isNotEmpty()) {
|
|
|
|
json["poll"] = jsonObject {
|
2018-11-06 02:29:33 +01:00
|
|
|
put("choices", choices)
|
2020-01-07 09:03:32 +01:00
|
|
|
}
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-06 09:20:37 +01:00
|
|
|
if(scheduledAt != 0L) {
|
|
|
|
return TootApiResult("misskey has no scheduled status API")
|
|
|
|
}
|
|
|
|
|
2018-08-20 02:07:55 +02:00
|
|
|
} else {
|
2020-01-07 09:03:32 +01:00
|
|
|
json["status"] = EmojiDecoder.decodeShortCode(
|
|
|
|
content,
|
|
|
|
emojiMapCustom = emojiMapCustom
|
2018-08-20 02:07:55 +02:00
|
|
|
)
|
|
|
|
if(visibility_checked != null) {
|
2020-01-07 09:03:32 +01:00
|
|
|
json["visibility"] = visibility_checked.strMastodon
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
2020-01-07 09:03:32 +01:00
|
|
|
json["sensitive"] = bNSFW
|
|
|
|
json["spoiler_text"] = EmojiDecoder.decodeShortCode(
|
|
|
|
spoiler_text ?: "",
|
|
|
|
emojiMapCustom = emojiMapCustom
|
2018-08-20 02:07:55 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
if(in_reply_to_id != null) {
|
2020-01-07 09:03:32 +01:00
|
|
|
json["in_reply_to_id"] = in_reply_to_id.toString()
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if(attachment_list != null) {
|
2020-01-07 09:03:32 +01:00
|
|
|
json["media_ids"] = jsonArray {
|
|
|
|
for(pa in attachment_list) {
|
|
|
|
val a = pa.attachment ?: continue
|
|
|
|
if(a.redraft && ! instance.versionGE(TootInstance.VERSION_2_4_1)) continue
|
|
|
|
add(a.id.toString())
|
|
|
|
}
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(enquete_items?.isNotEmpty() == true) {
|
2019-04-14 05:41:29 +02:00
|
|
|
if(poll_type == TootPollsType.Mastodon) {
|
2020-01-07 09:03:32 +01:00
|
|
|
json["poll"] = jsonObject {
|
2019-03-05 06:55:47 +01:00
|
|
|
put("multiple", poll_multiple_choice)
|
|
|
|
put("hide_totals", poll_hide_totals)
|
|
|
|
put("expires_in", poll_expire_seconds)
|
2020-01-07 09:03:32 +01:00
|
|
|
put("options",
|
|
|
|
enquete_items.map {
|
|
|
|
EmojiDecoder.decodeShortCode(
|
|
|
|
it,
|
|
|
|
emojiMapCustom = emojiMapCustom
|
2019-03-05 06:55:47 +01:00
|
|
|
)
|
|
|
|
}
|
2020-01-07 09:03:32 +01:00
|
|
|
.toJsonArray()
|
2018-08-20 02:07:55 +02:00
|
|
|
)
|
2019-03-05 06:55:47 +01:00
|
|
|
}
|
2020-01-07 09:03:32 +01:00
|
|
|
} else {
|
|
|
|
json["isEnquete"] = true
|
|
|
|
json["enquete_items"] = enquete_items.map {
|
|
|
|
EmojiDecoder.decodeShortCode(
|
|
|
|
it,
|
|
|
|
emojiMapCustom = emojiMapCustom
|
|
|
|
)
|
|
|
|
}.toJsonArray()
|
2018-08-20 02:07:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-06 09:20:37 +01:00
|
|
|
if(scheduledAt != 0L) {
|
2019-03-05 06:55:47 +01:00
|
|
|
if(! instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) {
|
2019-01-08 16:13:59 +01:00
|
|
|
return TootApiResult(activity.getString(R.string.scheduled_status_requires_mastodon_2_7_0))
|
2019-01-06 09:20:37 +01:00
|
|
|
}
|
|
|
|
// UTCの日時を渡す
|
|
|
|
val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"))
|
|
|
|
c.timeInMillis = scheduledAt
|
|
|
|
val sv = String.format(
|
|
|
|
"%d-%02d-%02d %02d:%02d:%02d",
|
|
|
|
c.get(Calendar.YEAR),
|
|
|
|
c.get(Calendar.MONTH) + 1,
|
|
|
|
c.get(Calendar.DAY_OF_MONTH),
|
|
|
|
c.get(Calendar.HOUR_OF_DAY),
|
|
|
|
c.get(Calendar.MINUTE),
|
|
|
|
c.get(Calendar.SECOND)
|
|
|
|
)
|
2020-01-07 09:03:32 +01:00
|
|
|
json["scheduled_at"] = sv
|
2019-01-06 09:20:37 +01:00
|
|
|
}
|
|
|
|
|
2018-06-23 04:43:18 +02:00
|
|
|
}
|
2020-01-07 09:03:32 +01:00
|
|
|
} catch(ex : JsonException) {
|
2018-06-23 04:43:18 +02:00
|
|
|
log.trace(ex)
|
|
|
|
log.e(ex, "status encoding failed.")
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
2018-06-23 04:43:18 +02:00
|
|
|
val body_string = json.toString()
|
|
|
|
|
2018-12-02 11:25:00 +01:00
|
|
|
val request_builder = body_string.toRequestBody(MEDIA_TYPE_JSON).toPost()
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-01-21 17:47:13 +01:00
|
|
|
if(! Pref.bpDontDuplicationCheck(pref)) {
|
2020-02-01 19:28:16 +01:00
|
|
|
val digest = (body_string + account.acctAscii).digestSHA256Hex()
|
2018-01-04 19:52:25 +01:00
|
|
|
request_builder.header("Idempotency-Key", digest)
|
|
|
|
}
|
2018-11-06 02:29:33 +01:00
|
|
|
|
|
|
|
result = if(isMisskey) {
|
2019-01-06 15:55:25 +01:00
|
|
|
// log.d("misskey json %s", body_string)
|
2018-08-20 02:07:55 +02:00
|
|
|
client.request("/api/notes/create", request_builder)
|
2018-11-06 02:29:33 +01:00
|
|
|
} else {
|
2018-08-20 02:07:55 +02:00
|
|
|
client.request("/api/v1/statuses", request_builder)
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2019-01-06 09:20:37 +01:00
|
|
|
val jsonObject = result?.jsonObject
|
2019-01-06 15:55:25 +01:00
|
|
|
|
2019-01-06 09:20:37 +01:00
|
|
|
if(scheduledAt != 0L && jsonObject != null) {
|
|
|
|
// {"id":"3","scheduled_at":"2019-01-06T07:08:00.000Z","media_attachments":[]}
|
|
|
|
scheduledStatusSucceeded = true
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2018-11-06 02:29:33 +01:00
|
|
|
val status = parser.status(
|
|
|
|
if(isMisskey) {
|
2020-01-08 04:23:45 +01:00
|
|
|
result?.jsonObject?.jsonObject("createdNote") ?: result?.jsonObject
|
2018-11-06 02:29:33 +01:00
|
|
|
} else {
|
|
|
|
result?.jsonObject
|
|
|
|
}
|
|
|
|
)
|
2018-01-04 19:52:25 +01:00
|
|
|
this.status = status
|
|
|
|
if(status != null) {
|
2018-06-23 04:43:18 +02:00
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
// タグを覚えておく
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2018-06-23 04:43:18 +02:00
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun handleResult(result : TootApiResult?) {
|
2019-01-06 09:20:37 +01:00
|
|
|
result ?: return
|
2018-01-04 19:52:25 +01:00
|
|
|
val status = this.status
|
2019-01-06 09:20:37 +01:00
|
|
|
when {
|
|
|
|
status != null -> {
|
|
|
|
// 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る
|
|
|
|
callback.onPostComplete(account, status)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
scheduledStatusSucceeded -> {
|
|
|
|
callback.onScheduledPostComplete(account)
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else -> showToast(activity, true, result.error)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
2018-08-15 04:06:25 +02:00
|
|
|
})
|
|
|
|
)
|
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-08-20 02:07:55 +02:00
|
|
|
private var isMisskey = false
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-01-21 17:47:13 +01:00
|
|
|
private val onEmojiListLoad : (list : ArrayList<CustomEmoji>) -> Unit =
|
2018-11-12 18:19:57 +01:00
|
|
|
{
|
2018-01-21 17:47:13 +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 {
|
2018-11-30 06:22:17 +01:00
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
override fun run() {
|
2018-01-10 16:47:35 +01:00
|
|
|
val et = this@PostHelper.et
|
2018-11-30 06:22:17 +01:00
|
|
|
if(et == null // EditTextを特定できない
|
|
|
|
|| et.selectionStart != et.selectionEnd // 範囲選択中
|
|
|
|
|| callback2?.canOpenPopup() != true // 何らかの理由でポップアップが許可されていない
|
|
|
|
) {
|
2018-01-04 19:52:25 +01:00
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-11-30 06:22:17 +01:00
|
|
|
checkMention(et, et.text.toString())
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun checkMention(et : MyEditText, src : String) {
|
|
|
|
|
2020-02-02 07:25:08 +01:00
|
|
|
fun matchUserNameOrAsciiDomain(cp : Int) : Boolean {
|
|
|
|
if(cp >= 0x7f) return false
|
|
|
|
val c = cp.toChar()
|
|
|
|
|
|
|
|
return '0' <= c && c <= '9' ||
|
|
|
|
'A' <= c && c <= 'Z' ||
|
|
|
|
'a' <= c && c <= 'z' ||
|
|
|
|
c == '_' || c == '-' || c == '.'
|
|
|
|
}
|
|
|
|
|
|
|
|
// Letter | Mark | Decimal_Number | Connector_Punctuation
|
|
|
|
fun matchIdnWord(cp:Int)=when(Character.getType(cp).toByte()) {
|
|
|
|
// Letter
|
|
|
|
// LCはエイリアスなので文字から得られることはないはず
|
|
|
|
Character.UPPERCASE_LETTER,
|
|
|
|
Character.LOWERCASE_LETTER,
|
|
|
|
Character.TITLECASE_LETTER,
|
|
|
|
Character.MODIFIER_LETTER,
|
|
|
|
Character.OTHER_LETTER -> true
|
|
|
|
// Mark
|
|
|
|
Character.NON_SPACING_MARK,
|
|
|
|
Character.COMBINING_SPACING_MARK,
|
|
|
|
Character.ENCLOSING_MARK -> true
|
|
|
|
// Decimal_Number
|
|
|
|
Character.DECIMAL_DIGIT_NUMBER -> true
|
|
|
|
// Connector_Punctuation
|
|
|
|
Character.CONNECTOR_PUNCTUATION -> true
|
|
|
|
|
|
|
|
else -> false
|
|
|
|
}
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
var count_atMark = 0
|
2018-11-30 06:22:17 +01:00
|
|
|
val end = et.selectionEnd
|
|
|
|
var start : Int = - 1
|
|
|
|
var i = end
|
|
|
|
while(i > 0) {
|
2020-02-02 07:25:08 +01:00
|
|
|
val cp = src.codePointBefore(i)
|
|
|
|
i -= Character.charCount(cp)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2020-02-02 07:25:08 +01:00
|
|
|
if(cp == '@'.toInt()) {
|
|
|
|
start = i
|
2018-11-30 06:22:17 +01:00
|
|
|
if(++ count_atMark >= 2) break else continue
|
2020-02-02 07:25:08 +01:00
|
|
|
} else if( matchUserNameOrAsciiDomain(cp) ||matchIdnWord(cp) ){
|
2018-01-04 19:52:25 +01:00
|
|
|
continue
|
2020-02-02 07:25:08 +01:00
|
|
|
}else {
|
|
|
|
// その他の文字種が出たら探索打ち切り
|
|
|
|
break
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
2018-11-30 06:22:17 +01:00
|
|
|
|
|
|
|
if(start == - 1) {
|
|
|
|
checkTag(et, src)
|
|
|
|
return
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
2018-11-30 06:22:17 +01:00
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
// 最低でも2文字ないと補完しない
|
|
|
|
if(end - start < 2) {
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
2018-11-30 06:22:17 +01:00
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-30 06:22:17 +01:00
|
|
|
private fun checkTag(et : MyEditText, src : String) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
val end = et.selectionEnd
|
|
|
|
|
|
|
|
val last_sharp = src.lastIndexOf('#', end - 1)
|
|
|
|
|
|
|
|
if(last_sharp == - 1 || end - last_sharp < 2) {
|
2018-11-30 06:22:17 +01:00
|
|
|
checkEmoji(et, src)
|
2018-01-04 19:52:25 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
val part = src.substring(last_sharp + 1, end)
|
2019-07-20 08:47:03 +02:00
|
|
|
if(! TootTag.isValid(part, isMisskey)) {
|
2018-11-30 06:22:17 +01:00
|
|
|
checkEmoji(et, src)
|
2018-01-04 19:52:25 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-30 06:22:17 +01:00
|
|
|
private fun checkEmoji(et : MyEditText, src : String) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
val end = et.selectionEnd
|
|
|
|
val last_colon = src.lastIndexOf(':', end - 1)
|
|
|
|
if(last_colon == - 1 || end - last_colon < 1) {
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-06-23 04:43:18 +02:00
|
|
|
if(! EmojiDecoder.canStartShortCode(src, last_colon)) {
|
2018-11-30 06:22:17 +01:00
|
|
|
// : の手前は始端か改行か空白でなければならない
|
2018-01-04 19:52:25 +01:00
|
|
|
log.d("checkEmoji: invalid character before shortcode.")
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-11-30 06:22:17 +01:00
|
|
|
val part = src.substring(last_colon + 1, end)
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
if(part.isEmpty()) {
|
2018-11-30 06:22:17 +01:00
|
|
|
// :を入力した直後は候補は0で、「閉じる」と「絵文字を選ぶ」だけが表示されたポップアップを出す
|
2018-01-04 19:52:25 +01:00
|
|
|
openPopup()?.setList(
|
|
|
|
et, last_colon, end, null, picker_caption_emoji, open_picker_emoji
|
|
|
|
)
|
2018-11-30 06:22:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if(reCharsNotEmoji.matcher(part).find()) {
|
|
|
|
// 範囲内に絵文字に使えない文字がある
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
val code_list = ArrayList<CharSequence>()
|
|
|
|
val limit = 100
|
|
|
|
|
|
|
|
// カスタム絵文字の候補を部分一致検索
|
|
|
|
code_list.addAll(customEmojiCodeList(this@PostHelper.instance, limit, part))
|
|
|
|
|
|
|
|
// 通常の絵文字を部分一致で検索
|
|
|
|
val remain = limit - code_list.size
|
|
|
|
if(remain > 0) {
|
2019-09-30 23:27:21 +02:00
|
|
|
val s =
|
|
|
|
src.substring(last_colon + 1, end).toLowerCase(Locale.JAPAN).replace('-', '_')
|
2018-11-30 06:22:17 +01:00
|
|
|
val matches = EmojiDecoder.searchShortCode(activity, s, remain)
|
|
|
|
log.d("checkEmoji: search for %s, result=%d", s, matches.size)
|
|
|
|
code_list.addAll(matches)
|
|
|
|
}
|
|
|
|
|
|
|
|
openPopup()?.setList(
|
|
|
|
et,
|
|
|
|
last_colon,
|
|
|
|
end,
|
|
|
|
code_list,
|
|
|
|
picker_caption_emoji,
|
|
|
|
open_picker_emoji
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// カスタム絵文字の候補を作る
|
|
|
|
private fun customEmojiCodeList(
|
|
|
|
instance : String?,
|
2019-08-24 05:35:22 +02:00
|
|
|
@Suppress("SameParameterValue") limit : Int,
|
2018-11-30 06:22:17 +01:00
|
|
|
needle : String
|
|
|
|
) : ArrayList<CharSequence> {
|
|
|
|
val dst = ArrayList<CharSequence>()
|
|
|
|
|
|
|
|
if(instance?.isNotEmpty() == true) {
|
2019-01-06 09:20:37 +01:00
|
|
|
|
2018-11-30 06:22:17 +01:00
|
|
|
val custom_list = App1.custom_emoji_lister.getListWithAliases(
|
|
|
|
instance,
|
|
|
|
isMisskey,
|
|
|
|
onEmojiListLoad
|
|
|
|
)
|
2019-01-06 09:20:37 +01:00
|
|
|
|
2018-11-30 06:22:17 +01:00
|
|
|
if(custom_list != null) {
|
|
|
|
|
|
|
|
for(item in custom_list) {
|
|
|
|
if(dst.size >= limit) break
|
|
|
|
if(! item.shortcode.contains(needle)) continue
|
2018-11-06 02:29:33 +01:00
|
|
|
|
2018-11-30 06:22:17 +01:00
|
|
|
val sb = SpannableStringBuilder()
|
|
|
|
sb.append(' ')
|
|
|
|
sb.setSpan(
|
|
|
|
NetworkEmojiSpan(item.url),
|
|
|
|
0,
|
|
|
|
sb.length,
|
|
|
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
|
|
)
|
|
|
|
sb.append(' ')
|
|
|
|
if(item.alias != null) {
|
|
|
|
val start = sb.length
|
|
|
|
sb.append(":")
|
|
|
|
sb.append(item.alias)
|
|
|
|
sb.append(": → ")
|
2018-01-21 17:47:13 +01:00
|
|
|
sb.setSpan(
|
2018-11-30 06:22:17 +01:00
|
|
|
ForegroundColorSpan(
|
2018-12-01 00:02:18 +01:00
|
|
|
getAttributeColor(
|
2018-11-30 06:22:17 +01:00
|
|
|
activity,
|
|
|
|
R.attr.colorTimeSmall
|
|
|
|
)
|
|
|
|
),
|
|
|
|
start,
|
2018-01-21 17:47:13 +01:00
|
|
|
sb.length,
|
|
|
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
|
|
)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
2018-11-30 06:22:17 +01:00
|
|
|
|
|
|
|
sb.append(':')
|
|
|
|
sb.append(item.shortcode)
|
|
|
|
sb.append(':')
|
|
|
|
|
|
|
|
dst.add(sb)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-11-30 06:22:17 +01:00
|
|
|
return dst
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-08-20 02:07:55 +02:00
|
|
|
fun setInstance(_instance : String?, isMisskey : Boolean) {
|
2019-08-24 05:35:22 +02:00
|
|
|
val instance = _instance?.toLowerCase(Locale.JAPAN)
|
2018-01-04 19:52:25 +01:00
|
|
|
this.instance = instance
|
2018-08-20 02:07:55 +02:00
|
|
|
this.isMisskey = isMisskey
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-11-06 02:29:33 +01:00
|
|
|
if(instance != null) {
|
|
|
|
App1.custom_emoji_lister.getList(instance, isMisskey, 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-21 17:47:13 +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-21 17:47:13 +01:00
|
|
|
override fun beforeTextChanged(
|
|
|
|
s : CharSequence,
|
|
|
|
start : Int,
|
|
|
|
count : Int,
|
|
|
|
after : Int
|
|
|
|
) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
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-02-11 09:45:15 +01:00
|
|
|
private fun SpannableStringBuilder.appendEmoji(
|
|
|
|
name : String,
|
|
|
|
instance : String?,
|
|
|
|
bInstanceHasCustomEmoji : Boolean
|
|
|
|
) : SpannableStringBuilder {
|
|
|
|
|
2019-09-14 22:09:52 +02:00
|
|
|
val item = EmojiMap.sShortNameToEmojiInfo[name]
|
2019-06-03 22:50:18 +02:00
|
|
|
val separator = EmojiDecoder.customEmojiSeparator(pref)
|
2018-02-11 09:45:15 +01:00
|
|
|
if(item == null || instance != null) {
|
|
|
|
// カスタム絵文字は常にshortcode表現
|
2019-06-03 22:50:18 +02:00
|
|
|
if(! EmojiDecoder.canStartShortCode(this, this.length)) append(separator)
|
2018-02-11 09:45:15 +01:00
|
|
|
this.append(SpannableString(":$name:"))
|
2019-06-03 22:50:18 +02:00
|
|
|
// セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。
|
2019-07-20 08:47:03 +02:00
|
|
|
if(separator != ' ') append(separator)
|
2018-02-11 09:45:15 +01:00
|
|
|
} else if(! bInstanceHasCustomEmoji) {
|
|
|
|
// 古いタンスだとshortcodeを使う。見た目は絵文字に変える。
|
2019-06-03 22:50:18 +02:00
|
|
|
if(! EmojiDecoder.canStartShortCode(this, this.length)) append(separator)
|
2018-02-11 09:45:15 +01:00
|
|
|
this.append(DecodeOptions(activity).decodeEmoji(":$name:"))
|
2019-06-03 22:50:18 +02:00
|
|
|
// セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。
|
2019-07-20 08:47:03 +02:00
|
|
|
if(separator != ' ') append(separator)
|
2018-02-11 09:45:15 +01:00
|
|
|
} else {
|
|
|
|
// 十分に新しいタンスなら絵文字のunicodeを使う。見た目は絵文字に変える。
|
|
|
|
this.append(DecodeOptions(activity).decodeEmoji(item.unified))
|
|
|
|
}
|
|
|
|
return this
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-01-16 07:48:17 +01:00
|
|
|
private val open_picker_emoji : Runnable = Runnable {
|
2020-01-27 16:41:18 +01:00
|
|
|
EmojiPicker(
|
|
|
|
activity,
|
|
|
|
instance,
|
|
|
|
isMisskey
|
|
|
|
) { 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
|
|
|
|
2018-09-27 16:21:37 +02:00
|
|
|
val src = et.text ?: ""
|
2018-01-16 07:48:17 +01:00
|
|
|
val src_length = src.length
|
2019-07-20 08:32:43 +02:00
|
|
|
val end = min(src_length, et.selectionEnd)
|
2018-01-16 07:48:17 +01:00
|
|
|
val start = src.lastIndexOf(':', end - 1)
|
2018-06-23 04:43:18 +02:00
|
|
|
if(start == - 1 || end - start < 1) return@EmojiPicker
|
2018-02-11 09:20:49 +01:00
|
|
|
|
2018-02-11 09:45:15 +01:00
|
|
|
val sb = SpannableStringBuilder()
|
2018-01-16 07:48:17 +01:00
|
|
|
.append(src.subSequence(0, start))
|
2018-02-11 09:45:15 +01:00
|
|
|
.appendEmoji(name, instance, bInstanceHasCustomEmoji)
|
2018-02-11 09:20:49 +01:00
|
|
|
|
2018-02-11 09:45:15 +01:00
|
|
|
val newSelection = sb.length
|
2018-11-06 02:29:33 +01:00
|
|
|
if(end < src_length) sb.append(src.subSequence(end, src_length))
|
2018-01-14 10:14:39 +01:00
|
|
|
|
2018-02-11 09:45:15 +01:00
|
|
|
et.text = sb
|
2018-02-11 09:20:49 +01:00
|
|
|
et.setSelection(newSelection)
|
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() {
|
2020-01-27 16:41:18 +01:00
|
|
|
EmojiPicker(
|
|
|
|
activity,
|
|
|
|
instance,
|
|
|
|
isMisskey
|
|
|
|
) { name, instance, bInstanceHasCustomEmoji, _, _ ->
|
2018-01-16 07:48:17 +01:00
|
|
|
val et = this.et ?: return@EmojiPicker
|
|
|
|
|
2018-09-27 16:21:37 +02:00
|
|
|
val src = et.text ?: ""
|
2018-01-16 07:48:17 +01:00
|
|
|
val src_length = src.length
|
2019-07-20 08:32:43 +02:00
|
|
|
val start = min(src_length, et.selectionStart)
|
|
|
|
val end = min(src_length, et.selectionEnd)
|
2018-01-14 10:14:39 +01:00
|
|
|
|
2018-02-11 09:45:15 +01:00
|
|
|
val sb = SpannableStringBuilder()
|
2018-11-06 02:29:33 +01:00
|
|
|
.append(src.subSequence(0, start))
|
2018-02-11 09:45:15 +01:00
|
|
|
.appendEmoji(name, instance, bInstanceHasCustomEmoji)
|
2018-02-11 09:20:49 +01:00
|
|
|
|
2018-02-11 09:45:15 +01:00
|
|
|
val newSelection = sb.length
|
2018-11-06 02:29:33 +01:00
|
|
|
if(end < src_length) sb.append(src.subSequence(end, src_length))
|
2018-01-14 10:14:39 +01:00
|
|
|
|
2018-02-11 09:45:15 +01:00
|
|
|
et.text = sb
|
2018-02-11 09:20:49 +01:00
|
|
|
et.setSelection(newSelection)
|
2018-01-14 10:14:39 +01:00
|
|
|
|
|
|
|
proc_text_changed.run()
|
|
|
|
}.show()
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
2019-09-30 23:27:21 +02:00
|
|
|
private fun SpannableStringBuilder.appendHashTag(tagWithoutSharp : String) : SpannableStringBuilder {
|
2019-09-12 19:16:07 +02:00
|
|
|
val separator = ' '
|
|
|
|
if(! EmojiDecoder.canStartHashtag(this, this.length)) append(separator)
|
|
|
|
this.append('#').append(tagWithoutSharp)
|
|
|
|
append(separator)
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2019-09-30 23:27:21 +02:00
|
|
|
fun openFeaturedTagList(list : List<TootTag>?) {
|
|
|
|
if(list?.isEmpty() != false) return
|
2019-09-12 19:16:07 +02:00
|
|
|
val ad = ActionsDialog()
|
2019-09-30 23:27:21 +02:00
|
|
|
for(tag in list) {
|
|
|
|
ad.addAction("#${tag.name}") {
|
2019-09-12 19:16:07 +02:00
|
|
|
val et = this.et ?: return@addAction
|
|
|
|
|
|
|
|
val src = et.text ?: ""
|
|
|
|
val src_length = src.length
|
|
|
|
val start = min(src_length, et.selectionStart)
|
|
|
|
val end = min(src_length, et.selectionEnd)
|
|
|
|
|
|
|
|
val sb = SpannableStringBuilder()
|
|
|
|
.append(src.subSequence(0, start))
|
|
|
|
.appendHashTag(tag.name)
|
|
|
|
val newSelection = sb.length
|
|
|
|
if(end < src_length) sb.append(src.subSequence(end, src_length))
|
2019-09-30 23:27:21 +02:00
|
|
|
|
2019-09-12 19:16:07 +02:00
|
|
|
et.text = sb
|
|
|
|
et.setSelection(newSelection)
|
|
|
|
|
|
|
|
proc_text_changed.run()
|
|
|
|
}
|
|
|
|
}
|
2019-09-30 23:27:21 +02:00
|
|
|
ad.show(activity, activity.getString(R.string.featured_hashtags))
|
2019-09-12 19:16:07 +02:00
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
// }
|
|
|
|
// };
|
|
|
|
|
|
|
|
}
|