create post with polls

This commit is contained in:
tateisu 2019-03-05 14:55:47 +09:00
parent 6418ea8063
commit e1be78dab5
6 changed files with 319 additions and 54 deletions

View File

@ -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<MyEditText>
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<Spinner>(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<String>()
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 =

View File

@ -18,6 +18,13 @@ class NicoEnquete(
list_attachment : ArrayList<TootAttachmentLike>?,
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")

View File

@ -71,6 +71,11 @@ class PostHelper(
var in_reply_to_id : EntityId? = null
var attachment_list : ArrayList<PostAttachment>? = null
var enquete_items : ArrayList<String>? = 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<String, CustomEmoji>? = 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の日時を渡す

View File

@ -291,9 +291,9 @@
</LinearLayout>
<CheckBox
android:id="@+id/cbEnquete"
android:layout_width="wrap_content"
<Spinner
android:id="@+id/spEnquete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/make_enquete"
@ -398,6 +398,88 @@
</FrameLayout>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/allow_multiple_choice"
android:id="@+id/cbMultipleChoice"
/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/hide_totals"
android:id="@+id/cbHideTotals"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="3dp"
android:id="@+id/llExpire"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/expiration"
android:layout_marginEnd="4dp"
/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:id="@+id/etExpireDays"
android:text="1"
tools:ignore="HardcodedText"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/poll_expire_days"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/plus"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:id="@+id/etExpireHours"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/poll_expire_hours"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/plus"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:id="@+id/etExpireMinutes"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/poll_expire_minutes"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -870,5 +870,14 @@
<string name="follow_request_misskey">フォローリクエスト (Misskey)</string>
<string name="dont_remove_deleted_toot_from_timeline">削除された投稿をタイムラインから除去しない</string>
<string name="dont_show_column_background_image">カラム背景画像を表示しない</string>
<string name="allow_multiple_choice">複数選択を許可する</string>
<string name="hide_totals">期限内は票数を隠す</string>
<string name="expiration">期限</string>
<string name="poll_dont_make">投票を作らない</string>
<string name="poll_make">投票を作る</string>
<string name="poll_make_friends_nico">投票を作る(friends.nico API)</string>
<string name="poll_expire_days"></string>
<string name="poll_expire_hours">時間</string>
<string name="poll_expire_minutes"></string>
</resources>

View File

@ -892,5 +892,15 @@
<string name="follow_request_misskey">Follow request (Misskey)</string>
<string name="dont_remove_deleted_toot_from_timeline">Don\'t remove deleted toots from timeline</string>
<string name="dont_show_column_background_image">Don\'t show column background image</string>
<string name="allow_multiple_choice">Allow multiple choice</string>
<string name="hide_totals">Hide totals until expire</string>
<string name="expiration">Expiration</string>
<string name="poll_dont_make">Don\'t make poll</string>
<string name="poll_make">Make poll</string>
<string name="poll_make_friends_nico">Make poll (friends.nico API)</string>
<string name="plus">+</string>
<string name="poll_expire_days">days</string>
<string name="poll_expire_hours">hours</string>
<string name="poll_expire_minutes">minutes</string>
</resources>