From e1be78dab535b3c288405273e822738206104362 Mon Sep 17 00:00:00 2001 From: tateisu Date: Tue, 5 Mar 2019 14:55:47 +0900 Subject: [PATCH] create post with polls --- .../java/jp/juggler/subwaytooter/ActPost.kt | 188 ++++++++++++++---- .../subwaytooter/api/entity/NicoEnquete.kt | 10 + .../juggler/subwaytooter/util/PostHelper.kt | 68 +++++-- app/src/main/res/layout/act_post.xml | 88 +++++++- app/src/main/res/values-ja/strings.xml | 9 + app/src/main/res/values/strings.xml | 10 + 6 files changed, 319 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt index c2e168cb..6a42a35d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt @@ -164,6 +164,7 @@ class ActPost : AppCompatActivity(), internal const val DRAFT_REPLY_IMAGE = "reply_image" internal const val DRAFT_REPLY_URL = "reply_url" internal const val DRAFT_IS_ENQUETE = "is_enquete" + internal const val DRAFT_POLL_TYPE = "poll_type" internal const val DRAFT_ENQUETE_ITEMS = "enquete_items" internal const val DRAFT_QUOTED_RENOTE = "quotedRenote" @@ -256,10 +257,17 @@ class ActPost : AppCompatActivity(), internal lateinit var cbQuoteRenote : CheckBox - internal lateinit var cbEnquete : CheckBox + internal lateinit var spEnquete : Spinner private lateinit var llEnquete : View internal lateinit var list_etChoice : List + private lateinit var cbMultipleChoice : CheckBox + private lateinit var cbHideTotals : CheckBox + private lateinit var llExpire : LinearLayout + private lateinit var etExpireDays : EditText + private lateinit var etExpireHours : EditText + private lateinit var etExpireMinutes : EditText + private lateinit var tvCharCount : TextView internal lateinit var handler : Handler private lateinit var formRoot : View @@ -634,7 +642,7 @@ class ActPost : AppCompatActivity(), // 比較する前にデフォルトの公開範囲を計算する visibility = visibility ?: account.visibility - ?: TootVisibility.Public + ?: TootVisibility.Public // VISIBILITY_WEB_SETTING だと 1.5未満のタンスでトラブルになる if(TootVisibility.WebSetting == visibility) { @@ -709,25 +717,35 @@ class ActPost : AppCompatActivity(), val src_enquete = base_status.enquete val src_items = src_enquete?.items - if(src_items != null && src_enquete.type == NicoEnquete.TYPE_ENQUETE) { - cbEnquete.isChecked = true - text = decodeOptions.decodeHTML(src_enquete.question) - etContent.text = text - etContent.setSelection(text.length) - - var src_index = 0 - for(et in list_etChoice) { - if(src_index < src_items.size) { - val choice = src_items[src_index] - if(src_index == src_items.size - 1 && choice.text == "\uD83E\uDD14") { - // :thinking_face: は再現しない + if(src_items != null) { + if(src_enquete.poll_type == NicoEnquete.PollType.FriendsNico && src_enquete.type != NicoEnquete.TYPE_ENQUETE) { + // フレニコAPIのアンケート結果は再編集の対象外 + } else { + spEnquete.setSelection( + if(src_enquete.poll_type == NicoEnquete.PollType.FriendsNico) { + 2 } else { - et.setText(decodeOptions.decodeEmoji(choice.text)) - ++ src_index - continue + 1 } + ) + text = decodeOptions.decodeHTML(src_enquete.question) + etContent.text = text + etContent.setSelection(text.length) + + var src_index = 0 + for(et in list_etChoice) { + if(src_index < src_items.size) { + val choice = src_items[src_index] + if(src_index == src_items.size - 1 && choice.text == "\uD83E\uDD14") { + // :thinking_face: は再現しない + } else { + et.setText(decodeOptions.decodeEmoji(choice.text)) + ++ src_index + continue + } + } + et.setText("") } - et.setText("") } } } @@ -794,7 +812,7 @@ class ActPost : AppCompatActivity(), visibility = visibility ?: account?.visibility - ?: TootVisibility.Public + ?: TootVisibility.Public // 2017/9/13 VISIBILITY_WEB_SETTING から VISIBILITY_PUBLICに変更した // VISIBILITY_WEB_SETTING だと 1.5未満のタンスでトラブルになるので… @@ -960,8 +978,43 @@ class ActPost : AppCompatActivity(), cbQuoteRenote = findViewById(R.id.cbQuoteRenote) - cbEnquete = findViewById(R.id.cbEnquete) + spEnquete = findViewById(R.id.spEnquete).apply { + this.adapter = ArrayAdapter( + this@ActPost, + android.R.layout.simple_spinner_item, + arrayOf( + getString(R.string.poll_dont_make), + getString(R.string.poll_make), + getString(R.string.poll_make_friends_nico) + ) + ).apply { + setDropDownViewResource(R.layout.lv_spinner_dropdown) + } + + this.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent : AdapterView<*>?) { + showEnquete() + updateTextCount() + } + + override fun onItemSelected( + parent : AdapterView<*>?, + view : View?, + position : Int, + id : Long + ) { + showEnquete() + updateTextCount() + } + } + } llEnquete = findViewById(R.id.llEnquete) + llExpire = findViewById(R.id.llExpire) + cbHideTotals = findViewById(R.id.cbHideTotals) + cbMultipleChoice = findViewById(R.id.cbMultipleChoice) + etExpireDays = findViewById(R.id.etExpireDays) + etExpireHours = findViewById(R.id.etExpireHours) + etExpireMinutes = findViewById(R.id.etExpireMinutes) ivMedia = listOf( findViewById(R.id.ivMedia1), @@ -1018,10 +1071,6 @@ class ActPost : AppCompatActivity(), updateContentWarning() } - cbEnquete.setOnCheckedChangeListener { _, _ -> - showEnquete() - updateTextCount() - } post_helper = PostHelper(this, pref, app_state.handler) post_helper.attachEditText(formRoot, etContent, false, object : PostHelper.Callback2 { @@ -1121,13 +1170,22 @@ class ActPost : AppCompatActivity(), var max = getMaxCharCount() - if(cbEnquete.isChecked) { - max -= 150 // フレニコ固有。500-150で350になる + fun checkEnqueteLength() { for(et in list_etChoice) { length += TootStatus.countText( EmojiDecoder.decodeShortCode(et.text.toString()) ) } + + } + + when(spEnquete.selectedItemPosition) { + 1 -> checkEnqueteLength() + + 2 -> { + max -= 150 // フレニコ固有。500-150で350になる + checkEnqueteLength() + } } val remain = max - length @@ -1320,7 +1378,7 @@ class ActPost : AppCompatActivity(), if(isFinishing) return - vg(llAttachment,attachment_list.isNotEmpty()) + vg(llAttachment, attachment_list.isNotEmpty()) ivMedia.forEachIndexed { i, v -> showAttachment_sub(v, i) } } @@ -2156,9 +2214,7 @@ class ActPost : AppCompatActivity(), post_helper.content = etContent.text.toString().trim { it <= ' ' } - if(! cbEnquete.isChecked) { - post_helper.enquete_items = null - } else { + fun copyEnqueteText() { val enquete_items = ArrayList() for(et in list_etChoice) { enquete_items.add(et.text.toString().trim { it <= ' ' }) @@ -2166,6 +2222,39 @@ class ActPost : AppCompatActivity(), post_helper.enquete_items = enquete_items } + fun getExpireSeconds() : Int { + + fun Double?.finiteOrZero() : Double = if(this?.isFinite() == true) this else 0.0 + + val d = etExpireDays.text.toString().trim().toDoubleOrNull().finiteOrZero() + val h = etExpireHours.text.toString().trim().toDoubleOrNull().finiteOrZero() + val m = etExpireMinutes.text.toString().trim().toDoubleOrNull().finiteOrZero() + + return (d * 86400.0 + h * 3600.0 + m * 60.0).toInt() + } + + when(spEnquete.selectedItemPosition) { + 1 -> { + copyEnqueteText() + post_helper.poll_type = NicoEnquete.PollType.Mastodon + post_helper.poll_expire_seconds = getExpireSeconds() + post_helper.poll_hide_totals = cbHideTotals.isChecked + post_helper.poll_multiple_choice = cbMultipleChoice.isChecked + } + + 2 -> { + copyEnqueteText() + post_helper.poll_type = NicoEnquete.PollType.FriendsNico + + } + + else -> { + post_helper.enquete_items = null + post_helper.poll_type = null + } + } + + if(! cbContentWarning.isChecked) { post_helper.spoiler_text = null // nullはCWチェックなしを示す } else { @@ -2251,7 +2340,8 @@ class ActPost : AppCompatActivity(), val content = etContent.text.toString() val content_warning = if(cbContentWarning.isChecked) etContentWarning.text.toString() else "" - val isEnquete = cbEnquete.isChecked + + val isEnquete = spEnquete.selectedItemPosition > 0 val str_choice = arrayOf( if(isEnquete) list_etChoice[0].text.toString() else "", @@ -2292,7 +2382,11 @@ class ActPost : AppCompatActivity(), json.put(DRAFT_REPLY_URL, in_reply_to_url) json.put(DRAFT_QUOTED_RENOTE, cbQuoteRenote.isChecked) - json.put(DRAFT_IS_ENQUETE, isEnquete) + + // deprecated. but still used in old draft. + // json.put(DRAFT_IS_ENQUETE, isEnquete) + + json.put(DRAFT_POLL_TYPE, spEnquete.selectedItemPosition.toPollTypeString()) val array = JSONArray() for(s in str_choice) { @@ -2308,6 +2402,19 @@ class ActPost : AppCompatActivity(), } + // poll type string to spinner index + private fun String?.toPollTypeIndex() = when(this) { + "mastodon" -> 1 + "friendsNico" -> 2 + else -> 0 + } + private fun Int?.toPollTypeString() = when(this) { + 1->"mastodon" + 2->"friendsNico" + else -> "" + } + + private fun openDraftPicker() { DlgDraftPicker().open(this) { draft -> restoreDraft(draft) } @@ -2446,7 +2553,16 @@ class ActPost : AppCompatActivity(), if(draft_visibility != null) this@ActPost.visibility = draft_visibility cbQuoteRenote.isChecked = draft.optBoolean(DRAFT_QUOTED_RENOTE) - cbEnquete.isChecked = draft.optBoolean(DRAFT_IS_ENQUETE, false) + + val sv = draft.optString(DRAFT_POLL_TYPE,null) + if(sv!=null){ + spEnquete.setSelection( sv.toPollTypeIndex() ) + }else{ + // old draft + val bv = draft.optBoolean(DRAFT_IS_ENQUETE, false) + spEnquete.setSelection( if(bv) 2 else 0) + } + val array = draft.optJSONArray(DRAFT_ENQUETE_ITEMS) if(array != null) { var src_index = 0 @@ -2628,7 +2744,11 @@ class ActPost : AppCompatActivity(), } private fun showEnquete() { - llEnquete.visibility = if(cbEnquete.isChecked) View.VISIBLE else View.GONE + val i = spEnquete.selectedItemPosition + vg(llEnquete, i != 0) + vg(llExpire, i == 1) + vg(cbHideTotals, i == 1) + vg(cbMultipleChoice, i == 1) } private val commitContentListener = diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/NicoEnquete.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/NicoEnquete.kt index 64a1d022..edd2c2cf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/NicoEnquete.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/NicoEnquete.kt @@ -18,6 +18,13 @@ class NicoEnquete( list_attachment : ArrayList?, src : JSONObject ) { + enum class PollType{ + Mastodon, // Mastodon 2.8's poll + Misskey, // Misskey's poll + FriendsNico, // friends.nico + } + + val poll_type : PollType // one of enquete,enquete_result val type : String? @@ -47,6 +54,7 @@ class NicoEnquete( this.status_id = status.id if(parser.serviceType == ServiceType.MISSKEY) { + this.poll_type = PollType.Misskey this.items = parseChoiceListMisskey( @@ -87,6 +95,8 @@ class NicoEnquete( ).decodeHTML(this.question ?: "?") } else { + // TODO Mastodonのpollとfriends.nicoのアンケートを区別する + this.poll_type = PollType.FriendsNico this.type = src.parseString("type") this.question = src.parseString("question") diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt index aa67494d..1b93069f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt @@ -71,6 +71,11 @@ class PostHelper( var in_reply_to_id : EntityId? = null var attachment_list : ArrayList? = null var enquete_items : ArrayList? = null + var poll_type : NicoEnquete.PollType? = null + var poll_expire_seconds = 0 + var poll_hide_totals = false + var poll_multiple_choice = false + var emojiMapCustom : HashMap? = null var redraft_status_id : EntityId? = null var useQuotedRenote = false @@ -94,6 +99,11 @@ class PostHelper( val in_reply_to_id = this.in_reply_to_id val attachment_list = this.attachment_list val enquete_items = this.enquete_items + var poll_type = this.poll_type + var poll_expire_seconds = this.poll_expire_seconds + var poll_hide_totals = this.poll_hide_totals + var poll_multiple_choice = this.poll_multiple_choice + val visibility = this.visibility val scheduledAt = this.scheduledAt @@ -112,10 +122,17 @@ class PostHelper( } if(enquete_items?.isNotEmpty() == true) { - var n = 0 - val ne = enquete_items.size - while(n < ne) { + + val choice_max_chars = if(isMisskey) { + 15 + } else when(poll_type) { + NicoEnquete.PollType.Mastodon -> 25 + else -> 15 + } + + for(n in 0 until enquete_items.size) { val item = enquete_items[n] + if(item.isEmpty()) { if(n < 2) { showToast(activity, true, R.string.enquete_item_is_empty, n + 1) @@ -123,8 +140,8 @@ class PostHelper( } } else { val code_count = item.codePointCount(0, item.length) - if(code_count > 15) { - val over = code_count - 15 + if(code_count > choice_max_chars) { + val over = code_count - choice_max_chars showToast(activity, true, R.string.enquete_item_too_long, n + 1, over) return } else if(n > 0) { @@ -136,7 +153,6 @@ class PostHelper( } } } - ++ n } } @@ -292,7 +308,7 @@ class PostHelper( } log.d("delete redraft. result=$result") Thread.sleep(2000L) - }else if(scheduledId != null) { + } else if(scheduledId != null) { val r1 = client.request( "/api/v1/scheduled_statuses/$scheduledId", Request.Builder().delete() @@ -480,21 +496,39 @@ class PostHelper( } if(enquete_items?.isNotEmpty() == true) { - json.put("isEnquete", true) - val array = JSONArray() - for(item in enquete_items) { - array.put( - EmojiDecoder.decodeShortCode( - item, - emojiMapCustom = emojiMapCustom + if(poll_type == NicoEnquete.PollType.Mastodon) { + json.put("poll", JSONObject().apply { + put("multiple", poll_multiple_choice) + put("hide_totals", poll_hide_totals) + put("expires_in", poll_expire_seconds) + put("options", JSONArray().apply { + for(item in enquete_items) { + put( + EmojiDecoder.decodeShortCode( + item, + emojiMapCustom = emojiMapCustom + ) + ) + } + }) + }) + } else { + json.put("isEnquete", true) + val array = JSONArray() + for(item in enquete_items) { + array.put( + EmojiDecoder.decodeShortCode( + item, + emojiMapCustom = emojiMapCustom + ) ) - ) + } + json.put("enquete_items", array) } - json.put("enquete_items", array) } if(scheduledAt != 0L) { - if( ! instance.versionGE(TootInstance.VERSION_2_7_0_rc1) ) { + if(! instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) { return TootApiResult(activity.getString(R.string.scheduled_status_requires_mastodon_2_7_0)) } // UTCの日時を渡す diff --git a/app/src/main/res/layout/act_post.xml b/app/src/main/res/layout/act_post.xml index eead5eae..14d5bb2a 100644 --- a/app/src/main/res/layout/act_post.xml +++ b/app/src/main/res/layout/act_post.xml @@ -291,9 +291,9 @@ - + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f36794cc..44926d04 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -870,5 +870,14 @@ フォローリクエスト (Misskey) 削除された投稿をタイムラインから除去しない カラム背景画像を表示しない + 複数選択を許可する + 期限内は票数を隠す + 期限 + 投票を作らない + 投票を作る + 投票を作る(friends.nico API) + + 時間 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 758bbb78..8ed54a0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -892,5 +892,15 @@ Follow request (Misskey) Don\'t remove deleted toots from timeline Don\'t show column background image + Allow multiple choice + Hide totals until expire + Expiration + Don\'t make poll + Make poll + Make poll (friends.nico API) + + + days + hours + minutes \ No newline at end of file