From 99e2efb9bb5666e518967cee21a8042e2d1a1b03 Mon Sep 17 00:00:00 2001 From: tateisu Date: Mon, 27 Jan 2020 14:45:16 +0900 Subject: [PATCH] =?UTF-8?q?(WIP)(Mastodon=203.1.0)=E3=83=9B=E3=83=BC?= =?UTF-8?q?=E3=83=A0=E3=82=AB=E3=83=A9=E3=83=A0=E3=81=AB=E5=91=8A=E7=9F=A5?= =?UTF-8?q?=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/jp/juggler/subwaytooter/Column.kt | 71 +++ .../jp/juggler/subwaytooter/ColumnTask.kt | 7 + .../subwaytooter/ColumnTask_Loading.kt | 28 +- .../jp/juggler/subwaytooter/ColumnType.kt | 5 +- .../juggler/subwaytooter/ColumnViewHolder.kt | 551 +++++++++++++++++- .../jp/juggler/subwaytooter/ItemViewHolder.kt | 2 +- .../jp/juggler/subwaytooter/StreamReader.kt | 125 ++-- .../api/entity/TootAnnouncement.kt | 162 +++++ .../subwaytooter/api/entity/TootInstance.kt | 1 + .../subwaytooter/api/entity/TootPayload.kt | 8 +- .../subwaytooter/api/entity/TootStatus.kt | 52 +- .../subwaytooter/dialog/EmojiPicker.kt | 21 +- .../juggler/subwaytooter/util/AnkoHelper.kt | 2 +- .../juggler/subwaytooter/util/PostHelper.kt | 4 +- .../main/java/jp/juggler/util/HttpUtils.kt | 5 +- .../main/java/jp/juggler/util/ViewUtils.kt | 9 + .../{ic_right.xml => ic_arrow_end.xml} | 1 + .../{ic_left.xml => ic_arrow_start.xml} | 1 + app/src/main/res/drawable/ic_eye.xml | 9 + app/src/main/res/layout/act_media_viewer.xml | 4 +- app/src/main/res/values-ja/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + 22 files changed, 1007 insertions(+), 71 deletions(-) create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt rename app/src/main/res/drawable/{ic_right.xml => ic_arrow_end.xml} (91%) rename app/src/main/res/drawable/{ic_left.xml => ic_arrow_start.xml} (91%) create mode 100644 app/src/main/res/drawable/ic_eye.xml diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.kt b/app/src/main/java/jp/juggler/subwaytooter/Column.kt index ee1fb741..7d0e1941 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Column.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.kt @@ -487,6 +487,18 @@ class Column( internal var language_filter : JsonObject? = null + // 告知のリスト + internal var announcements : MutableList? = null + + // 表示中の告知 + internal var announcementId : EntityId? = null + + // 告知を閉じた時刻, 0なら閉じていない + internal var announcementHideTime = 0L + + // 告知データを更新したタイミング + internal var announcementUpdated = 0L + // プロフカラムでのアカウント情報 @Volatile internal var who_account : TootAccountRef? = null @@ -2572,6 +2584,65 @@ class Column( } } } + + override fun onAnnouncementUpdate(item : TootAnnouncement) { + val list = announcements + if(list == null) { + announcements = mutableListOf(item) + }else{ + val index = list.indexOfFirst{ it.id == item.id} + if( index != -1 ){ + list[index] = TootAnnouncement.merge(list[index],item) + }else{ + list.add(0, item) + } + announcements?.sortWith(TootAnnouncement.comparator) + } + announcementUpdated = SystemClock.elapsedRealtime() + fireShowColumnHeader() + } + + override fun onAnnouncementDelete(id : EntityId) { + val it = announcements?.iterator() ?: return + while(it.hasNext()){ + val item = it.next() + if( item.id == id){ + it.remove() + announcementUpdated = SystemClock.elapsedRealtime() + fireShowColumnHeader() + return + } + } + } + + override fun onAnnouncementReaction(reaction : TootAnnouncement.Reaction) { + // find announcement + val announcement_id = reaction.announcement_id ?: return + val announcement = announcements?.find { it.id == announcement_id } ?: return + + // find reaction + val index = announcement.reactions?.indexOfFirst { it.name == reaction.name } + when { + reaction.count <= 0L -> { + if(index != null && index != - 1) announcement.reactions?.removeAt(index) + } + + index == null -> { + announcement.reactions = ArrayList().apply { + add(reaction) + } + } + + index == - 1 -> announcement.reactions?.add(reaction) + + else -> announcement.reactions?.get(index)?.let{ old -> + old.count = reaction.count + // ストリーミングイベントにはmeが含まれないので、oldにあるmeは変更されない + } + } + announcementUpdated = SystemClock.elapsedRealtime() + fireShowColumnHeader() + } } internal fun resumeStreaming(bPutGap : Boolean) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask.kt index 16addefd..5a5278cb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask.kt @@ -43,9 +43,16 @@ abstract class ColumnTask( val highlight_trie : WordTrieTree? get() = column.highlight_trie + val isPseudo :Boolean + get() = access_info.isPseudo + + val isMastodon : Boolean + get() = access_info.isMastodon + val isMisskey : Boolean get() = access_info.isMisskey + val misskeyVersion : Int get() = access_info.misskeyVersion diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt index 70816d46..057d8aad 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt @@ -1039,5 +1039,31 @@ class ColumnTask_Loading( // fallback to old api return getStatusList(client, Column.PATH_DIRECT_MESSAGES) } + + internal fun getAnnouncements(client : TootApiClient) : TootApiResult? { + if( isMastodon && !isPseudo ){ + column.announcements = null + column.announcementUpdated =1L + column.announcementId = null + client.publishApiProgress("loading announcements") + val (instance, _) = TootInstance.get(client) + if( instance?.versionGE(TootInstance.VERSION_3_1_0_rc1) == true){ + val result = client.request("/api/v1/announcements") + ?: return null // cancelled. + val code = result.response?.code ?: 0 + if(code !in 400 until 500) { + val list = parseList(::TootAnnouncement,parser,result.jsonArray) + if(list.isNotEmpty()){ + column.announcements = list + column.announcementUpdated = SystemClock.elapsedRealtime() + client.publishApiProgress("announcements loaded") + } + // other errors such as network or server fails will stop column loading. + return result + } + // just skip load announcements for 4xx error if server does not support announcements. + } + } + return TootApiResult() + } } - diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt index 4e3f742e..eb86af49 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt @@ -398,7 +398,10 @@ enum class ColumnType( iconId = { R.drawable.ic_home }, name1 = { it.getString(R.string.home) }, - loading = { client -> getStatusList(client, column.makeHomeTlUrl()) }, + loading = { client -> + getAnnouncements(client) + getStatusList(client, column.makeHomeTlUrl()) + }, refresh = { client -> getStatusList(client, column.makeHomeTlUrl()) }, gap = { client -> getStatusList(client, column.makeHomeTlUrl()) }, bAllowPseudo = false diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt index fc5e7db4..b76d50c8 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt @@ -6,7 +6,9 @@ import android.content.res.ColorStateList import android.graphics.Bitmap import android.graphics.Color import android.os.AsyncTask +import android.os.SystemClock import android.text.InputType +import android.text.Spannable import android.text.SpannableStringBuilder import android.text.TextUtils import android.view.* @@ -17,13 +19,26 @@ import android.widget.* import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.flexbox.FlexWrap +import com.google.android.flexbox.FlexboxLayout +import com.google.android.flexbox.JustifyContent import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayout import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection import jp.juggler.subwaytooter.action.Action_List import jp.juggler.subwaytooter.action.Action_Notification +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.entity.TootAnnouncement +import jp.juggler.subwaytooter.api.entity.TootStatus +import jp.juggler.subwaytooter.dialog.EmojiPicker +import jp.juggler.subwaytooter.span.NetworkEmojiSpan import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.view.ListDivider +import jp.juggler.subwaytooter.view.MyLinkMovementMethod +import jp.juggler.subwaytooter.view.MyTextView import jp.juggler.util.* import org.jetbrains.anko.* import java.io.Closeable @@ -137,6 +152,22 @@ class ColumnViewHolder( private lateinit var etHashtagExtraAll : EditText private lateinit var etHashtagExtraNone : EditText + private lateinit var llAnnouncementsBox : View + private lateinit var tvAnnouncementsIndex : TextView + private lateinit var btnAnnouncementsPrev : ImageButton + private lateinit var btnAnnouncementsNext : ImageButton + private lateinit var btnAnnouncementsShowHide : ImageButton + private lateinit var llAnnouncements : View + private lateinit var tvAnnouncementPeriod : TextView + private lateinit var tvAnnouncementContent : MyTextView + private lateinit var llAnnouncementExtra : LinearLayout + + private val announcementContentInvalidator : NetworkEmojiInvalidator + + private var lastAnnouncementShown = 0L + + private val extra_invalidator_list = ArrayList() + private val isPageDestroyed : Boolean get() = column == null || activity.isFinishing @@ -291,8 +322,6 @@ class ColumnViewHolder( handled } - - btnQuickFilterAll.setOnClickListener(this) btnQuickFilterMention.setOnClickListener(this) btnQuickFilterFavourite.setOnClickListener(this) @@ -301,7 +330,6 @@ class ColumnViewHolder( btnQuickFilterReaction.setOnClickListener(this) btnQuickFilterVote.setOnClickListener(this) - llColumnHeader.setOnClickListener(this) btnColumnSetting.setOnClickListener(this) btnColumnReload.setOnClickListener(this) @@ -317,6 +345,10 @@ class ColumnViewHolder( llRefreshError.setOnClickListener(this) + btnAnnouncementsShowHide.setOnClickListener(this) + btnAnnouncementsPrev.setOnClickListener(this) + btnAnnouncementsNext.setOnClickListener(this) + cbDontCloseColumn.setOnCheckedChangeListener(this) cbWithAttachment.setOnCheckedChangeListener(this) @@ -426,6 +458,10 @@ class ColumnViewHolder( activity.handler.removeCallbacks(proc_start_filter) activity.handler.postDelayed(proc_start_filter, 666L) }) + + announcementContentInvalidator = + NetworkEmojiInvalidator(activity.handler, tvAnnouncementContent) + tvAnnouncementContent.movementMethod = MyLinkMovementMethod } private val proc_start_filter : Runnable = Runnable { @@ -611,7 +647,7 @@ class ColumnViewHolder( btnDeleteNotification.vg(column.isNotificationColumn) - llSearch.vg(column.isSearchColumn)?.let{ + llSearch.vg(column.isSearchColumn)?.let { btnSearchClear.vg(Pref.bpShowSearchClear(activity.pref)) } @@ -652,6 +688,8 @@ class ColumnViewHolder( column.addColumnViewHolder(this) + lastAnnouncementShown = - 1L + showColumnColor() showContent(reason = "onPageCreate", reset = true) @@ -1060,8 +1098,30 @@ class ColumnViewHolder( btnQuickFilterReaction -> clickQuickFilter(Column.QUICK_FILTER_REACTION) btnQuickFilterVote -> clickQuickFilter(Column.QUICK_FILTER_VOTE) + btnAnnouncementsShowHide -> { + if(llAnnouncements.visibility == View.VISIBLE) { + column.announcementHideTime = System.currentTimeMillis() + } else { + column.announcementHideTime = 0L + } + activity.app_state.saveColumnList() + showAnnouncements() + } + + btnAnnouncementsPrev -> { + column.announcementId = + TootAnnouncement.move(column.announcements, column.announcementId, - 1) + activity.app_state.saveColumnList() + showAnnouncements() + } + + btnAnnouncementsNext -> { + column.announcementId = + TootAnnouncement.move(column.announcements, column.announcementId, + 1) + activity.app_state.saveColumnList() + showAnnouncements() + } } - } override fun onLongClick(v : View) : Boolean { @@ -1130,12 +1190,14 @@ class ColumnViewHolder( showColumnCloseButton() + showAnnouncements(force = false) } // カラムヘッダなど、負荷が低い部分の表示更新 fun showColumnHeader() { activity.handler.removeCallbacks(procShowColumnHeader) activity.handler.postDelayed(procShowColumnHeader, 50L) + } internal fun showContent( @@ -1198,6 +1260,7 @@ class ColumnViewHolder( showRefreshError() } proc_restoreScrollPosition.run() + } private var bRefreshErrorWillShown = false @@ -1887,6 +1950,121 @@ class ColumnViewHolder( } // end of column setting scroll view + llAnnouncementsBox = verticalLayout { + lparams(matchParent, wrapContent) { + startMargin = dip(6) + endMargin = dip(6) + topMargin = dip(2) + bottomMargin = dip(2) + } + background = createRoundDrawable( + dip(6).toFloat(), + getAttributeColor(context, R.attr.colorThumbnailBackground) + ) + var pad_tb = dip(2) + setPadding(0, pad_tb, 0, pad_tb) + + linearLayout { + lparams(matchParent, wrapContent) { + startMargin = dip(6) + endMargin = dip(6) + } + + gravity = Gravity.CENTER_VERTICAL + + + + textView { + gravity = Gravity.END + text = context.getString(R.string.announcements) + }.lparams(0, wrapContent) { + weight = 1f + } + + btnAnnouncementsPrev = imageButton { + + background = ContextCompat.getDrawable( + context, + R.drawable.btn_bg_transparent + ) + contentDescription = context.getString(R.string.previous) + imageResource = R.drawable.ic_arrow_start + }.lparams(dip(32), dip(32)) { + gravity = Gravity.END + marginStart = dip(4) + } + + tvAnnouncementsIndex = textView { + }.lparams(wrapContent, wrapContent) { + marginStart = dip(4) + } + + btnAnnouncementsNext = imageButton { + + background = ContextCompat.getDrawable( + context, + R.drawable.btn_bg_transparent + ) + contentDescription = context.getString(R.string.next) + imageResource = R.drawable.ic_arrow_end + }.lparams(dip(32), dip(32)) { + gravity = Gravity.END + marginStart = dip(4) + } + + btnAnnouncementsShowHide = imageButton { + background = ContextCompat.getDrawable( + context, + R.drawable.btn_bg_transparent + ) + contentDescription = context.getString(R.string.hide) + imageResource = R.drawable.ic_close + }.lparams(dip(32), dip(32)) { + gravity = Gravity.END + marginStart = dip(4) + } + + } + + llAnnouncements = maxHeightScrollView { + lparams(matchParent, wrapContent) { + topMargin = dip(1) + } + val pad_lr = dip(6) + pad_tb = dip(2) + setPadding(pad_lr, pad_tb, pad_lr, pad_tb) + + scrollBarStyle = View.SCROLLBARS_OUTSIDE_OVERLAY + isScrollbarFadingEnabled = false + + maxHeight = dip(240) + + verticalLayout { + lparams(matchParent, wrapContent) + + // 期間があれば表示する + tvAnnouncementPeriod = textView { + gravity = Gravity.END + }.lparams(matchParent, wrapContent) { + bottomMargin = dip(3) + } + + tvAnnouncementContent = myTextView { + setLineSpacing(lineSpacingExtra, 1.1f) + // tools:text="Contents\nContents" + }.lparams(matchParent, wrapContent) { + topMargin = dip(3) + } + + llAnnouncementExtra = verticalLayout { + lparams(matchParent, wrapContent) { + topMargin = dip(3) + } + } + } + } + } + llSearch = verticalLayout { lparams(matchParent, wrapContent) backgroundColor = getAttributeColor(context, R.attr.colorSearchFormBackground) @@ -2112,4 +2290,367 @@ class ColumnViewHolder( b.report() rv } + + private fun showAnnouncements(force : Boolean = true) { + val column = column ?: return + + if(! force && lastAnnouncementShown >= column.announcementUpdated) { + return + } + lastAnnouncementShown = SystemClock.elapsedRealtime() + + fun clearExtras() { + for(invalidator in extra_invalidator_list) { + invalidator.register(null) + } + extra_invalidator_list.clear() + } + llAnnouncementExtra.removeAllViews() + clearExtras() + + val listShown = TootAnnouncement.filterShown(column.announcements) + if(llAnnouncementsBox.vg(listShown?.isNotEmpty() == true) == null) { + return + } + + val content_color = column.getContentColor() + + val item = listShown !!.find { it.id == column.announcementId } + ?: listShown[0] + val itemIndex = listShown.indexOf(item) + + val enablePaging = listShown.size > 1 + val expand = column.announcementHideTime <= 0L + + btnAnnouncementsPrev.vg(expand)?.run { + isEnabled = enablePaging + alpha = if(enablePaging) 1f else 0.3f + } + btnAnnouncementsNext.vg(expand)?.run { + isEnabled = enablePaging + alpha = if(enablePaging) 1f else 0.3f + } + tvAnnouncementsIndex.vg(expand)?.text = + activity.getString(R.string.announcements_index, itemIndex + 1, listShown.size) + llAnnouncements.vg(expand) + + if(! expand) { + val newer = listShown.find { it.updated_at > column.announcementHideTime } + if(newer != null) { + column.announcementId = newer.id + setIconDrawableId( + activity, + btnAnnouncementsShowHide, + R.drawable.ic_error, + color = getAttributeColor(activity, R.attr.colorRegexFilterError), + alphaMultiplier = Styler.boost_alpha + ) + } else { + setIconDrawableId( + activity, + btnAnnouncementsShowHide, + R.drawable.ic_arrow_drop_down, + color = content_color, + alphaMultiplier = Styler.boost_alpha + ) + } + return + } + + setIconDrawableId( + activity, + btnAnnouncementsShowHide, + R.drawable.ic_arrow_drop_up, + color = content_color, + alphaMultiplier = Styler.boost_alpha + ) + + var periods : StringBuilder? = null + fun String.appendPeriod() { + val sb = periods + if(sb == null) { + periods = StringBuilder(this) + } else { + sb.append("\n") + sb.append(this) + } + } + + val (strStart, strEnd) = TootStatus.formatTimeRange( + item.starts_at, + item.ends_at, + item.all_day + ) + + + when { + + // no periods. + strStart == "" && strEnd == "" -> { + } + + // single date + strStart == strEnd -> { + activity.getString(R.string.announcements_period1, strStart) + .appendPeriod() + } + + else -> { + activity.getString(R.string.announcements_period2, strStart, strEnd) + .appendPeriod() + } + } + + if(item.updated_at > item.published_at) { + val strUpdateAt = TootStatus.formatTime(activity, item.updated_at, false) + activity.getString(R.string.edited_at, strUpdateAt).appendPeriod() + } + + val sb = periods + tvAnnouncementPeriod.vg(sb != null)?.text = sb + + tvAnnouncementContent.text = item.decoded_content + announcementContentInvalidator.register(item.decoded_content) + + // リアクションの表示 + + val density = activity.density + + val buttonHeight = ActMain.boostButtonSize + val marginBetween = (buttonHeight.toFloat() * 0.2f + 0.5f).toInt() + + val paddingH = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt() + val paddingV = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt() + + val box = FlexboxLayout(activity).apply { + flexWrap = FlexWrap.WRAP + justifyContent = JustifyContent.FLEX_START + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + topMargin = (0.5f + density * 3f).toInt() + } + } + + // +ボタン + run { + val b = ImageButton(activity) + val blp = FlexboxLayout.LayoutParams( + buttonHeight, + buttonHeight + ) + blp.endMargin = marginBetween + b.layoutParams = blp + b.background = ContextCompat.getDrawable( + activity, + R.drawable.btn_bg_transparent + ) + + b.contentDescription = activity.getString(R.string.reaction_add) + b.scaleType = ImageView.ScaleType.FIT_CENTER + b.padding = paddingV + b.setOnClickListener { + addReaction(item, null) + } + + setIconDrawableId( + activity, + b, + R.drawable.ic_add, + color = content_color, + alphaMultiplier = Styler.boost_alpha + ) + + box.addView(b) + } + val reactions = item.reactions?.filter { it.count > 0L }?.notEmpty() + if(reactions != null) { + + var lastButton : View? = null + + val options = DecodeOptions( + activity, + column.access_info, + decodeEmoji = true, + enlargeEmoji = 1.5f + ) + + val actMain = activity + val emojiAnimation = Pref.bpDisableEmojiAnimation(actMain.pref) + + for(reaction in reactions) { + + val url = if(emojiAnimation) { + reaction.url.notEmpty() ?: reaction.static_url.notEmpty() + } else { + reaction.static_url.notEmpty() ?: reaction.url.notEmpty() + } + + val b = Button(activity).apply { + layoutParams = FlexboxLayout.LayoutParams( + FlexboxLayout.LayoutParams.WRAP_CONTENT, + buttonHeight + ).apply { + endMargin = marginBetween + } + minWidthCompat = buttonHeight + + allCaps = false + tag = reaction + + background = ContextCompat.getDrawable( + actMain, + if(reaction.me == true) { + R.drawable.bg_button_cw + } else { + R.drawable.btn_bg_transparent + } + ) + + setTextColor(content_color) + + setPadding(paddingH, paddingV, paddingH, paddingV) + + + text = if(url == null) { + EmojiDecoder.decodeEmoji(options, "${reaction.name} ${reaction.count}") + } else { + SpannableStringBuilder("${reaction.name} ${reaction.count}").also { sb -> + sb.setSpan( + NetworkEmojiSpan(url, scale = 1.5f), + 0, + reaction.name.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + val invalidator = + NetworkEmojiInvalidator(actMain.handler, this) + invalidator.register(sb) + extra_invalidator_list.add(invalidator) + } + } + + setOnClickListener { + if(reaction.me == true) { + removeReaction(item, reaction.name) + } else { + addReaction(item, TootAnnouncement.Reaction(jsonObject { + put("name", reaction.name) + put("count", 1) + put("me", true) + putNotNull("url", reaction.url) + putNotNull("static_url", reaction.static_url) + })) + } + } + } + box.addView(b) + lastButton = b + + } + + lastButton + ?.layoutParams + ?.cast() + ?.endMargin = 0 + } + + llAnnouncementExtra.addView(box) + } + + private fun addReaction(item : TootAnnouncement, sample : TootAnnouncement.Reaction?) { + val column = column ?: return + val host = column.access_info.host + val isMisskey = column.isMisskey + if(sample == null) { + EmojiPicker(activity, host, isMisskey) { name, _, _ ,unicode-> + log.d("addReaction: $name") + addReaction(item, TootAnnouncement.Reaction(jsonObject { + put("name", unicode ?: name ) + put("count", 1) + put("me", true) + + // 以下はカスタム絵文字のみ + if(unicode == null){ + val map = App1.custom_emoji_lister.getMap(host, isMisskey) + if(map == null) { + showToast(activity, false, "emoji map is null") + return@EmojiPicker + } + val ce = map[name] + if(ce == null) { + showToast(activity, false, "emoji '$name' not found.") + return@EmojiPicker + } + putNotNull("url", ce.url) + putNotNull("static_url", ce.static_url) + } + })) + }.show() + return + } + TootTaskRunner(activity).run(column.access_info, object : TootTask { + override fun background(client : TootApiClient) : TootApiResult? { + return client.request( + "/api/v1/announcements/${item.id}/reactions/${sample.name.encodePercent()}", + JsonObject().toPutRequestBuilder() + ) + // 200 {} + } + + override fun handleResult(result : TootApiResult?) { + result ?: return + if(result.jsonObject == null) { + showToast(activity, true, result.error) + } else { + val list = item.reactions + if(list == null) { + item.reactions = mutableListOf(sample) + } else { + val reaction = list.find { it.name == sample.name } + if(reaction == null) { + list.add(sample) + } else { + reaction.me = true + ++reaction.count + } + } + column.announcementUpdated = SystemClock.elapsedRealtime() + showAnnouncements() + } + } + }) + } + + private fun removeReaction(item : TootAnnouncement, name : String) { + val column = column ?: return + TootTaskRunner(activity).run(column.access_info, object : TootTask { + override fun background(client : TootApiClient) : TootApiResult? { + return client.request( + "/api/v1/announcements/${item.id}/reactions/${name.encodePercent()}", + JsonObject().toDeleteRequestBuilder() + ) + // 200 {} + } + + override fun handleResult(result : TootApiResult?) { + result ?: return + if(result.jsonObject == null) { + showToast(activity, true, result.error) + } else { + val it = item.reactions?.iterator() ?: return + while(it.hasNext()) { + val reaction = it.next() + if(reaction.name == name) { + reaction.me = false + if(-- reaction.count <= 0) it.remove() + break + } + } + column.announcementUpdated = SystemClock.elapsedRealtime() + showAnnouncements() + } + } + }) + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.kt b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.kt index 73402caf..62553011 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.kt @@ -3365,7 +3365,7 @@ internal class ItemViewHolder( context, R.drawable.btn_bg_transparent ) - contentDescription = "@string/hide" + contentDescription = context.getString(R.string.hide) imageResource = R.drawable.ic_close }.lparams(dip(32), dip(32)) { gravity = Gravity.END diff --git a/app/src/main/java/jp/juggler/subwaytooter/StreamReader.kt b/app/src/main/java/jp/juggler/subwaytooter/StreamReader.kt index a834ee3a..62a33325 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/StreamReader.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/StreamReader.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import android.os.Handler import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.entity.* +import jp.juggler.subwaytooter.api.entity.TootAnnouncement.Reaction import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.* import okhttp3.Response @@ -24,11 +25,14 @@ internal class StreamReader( ) { internal interface StreamCallback { + fun channelId() : String? + fun onTimelineItem(item : TimelineItem) fun onListeningStateChanged(bListen : Boolean) fun onNoteUpdated(ev : MisskeyNoteUpdate) - - fun channelId() : String? + fun onAnnouncementUpdate( item: TootAnnouncement) + fun onAnnouncementDelete( id: EntityId) + fun onAnnouncementReaction(reaction : Reaction) } companion object { @@ -131,14 +135,12 @@ internal class StreamReader( } } - private fun fireTimelineItem(item : TimelineItem?, channelId : String? = null) { - item ?: return + private inline fun eachCallback(block:(callback:StreamCallback)->Unit){ synchronized(this) { if(bDisposed.get()) return@synchronized for(callback in callback_list) { try { - if(channelId != null && channelId != callback.channelId()) continue - callback.onTimelineItem(item) + block(callback) } catch(ex : Throwable) { log.trace(ex) } @@ -146,12 +148,21 @@ internal class StreamReader( } } + private fun fireTimelineItem(item : TimelineItem?, channelId : String? = null) { + item ?: return + eachCallback{ callback-> + if(channelId != null && channelId != callback.channelId()) return@eachCallback + callback.onTimelineItem(item) + } + } + private fun fireDeleteId(id : EntityId) { + val tl_host = access_info.host runOnMainLooper { synchronized(this) { - if(bDisposed.get()) return@runOnMainLooper - if(Pref.bpDontRemoveDeletedToot(App1.getAppState(context).pref)) return@runOnMainLooper + if(bDisposed.get()) return@synchronized + if(Pref.bpDontRemoveDeletedToot(App1.getAppState(context).pref)) return@synchronized for(column in App1.getAppState(context).column_list) { try { column.onStatusRemoved(tl_host, id) @@ -165,16 +176,9 @@ internal class StreamReader( private fun fireNoteUpdated(ev : MisskeyNoteUpdate, channelId : String? = null) { runOnMainLooper { - synchronized(this) { - if(bDisposed.get()) return@runOnMainLooper - for(callback in callback_list) { - try { - if(channelId != null && channelId != callback.channelId()) continue - callback.onNoteUpdated(ev) - } catch(ex : Throwable) { - log.trace(ex) - } - } + eachCallback { callback-> + if(channelId != null && channelId != callback.channelId()) return@eachCallback + callback.onNoteUpdated(ev) } } } @@ -252,6 +256,61 @@ internal class StreamReader( } + private fun handleMastodonMessage(obj:JsonObject,text:String){ + + when(val event = obj.string("event")) { + null,"" -> log.d("onMessage: missing event parameter") + + "filters_changed" -> + Column.onFiltersChanged(context, access_info) + + else -> { + val payload = TootPayload.parsePayload(parser, event, obj, text) + + when(event) { + "delete" -> when(payload) { + is Long -> fireDeleteId(EntityId(payload.toString())) + is String -> fireDeleteId(EntityId(payload.toString())) + else -> log.d("unsupported payload type. $payload") + } + + // {"event":"announcement","payload":"{\"id\":\"3\",\"content\":\"

追加

\",\"starts_at\":null,\"ends_at\":null,\"all_day\":false,\"mentions\":[],\"tags\":[],\"emojis\":[],\"reactions\":[]}"} + "announcement"->{ + if( payload is TootAnnouncement) { + runOnMainLooper { + eachCallback { it.onAnnouncementUpdate(payload) } + } + } + } + + // {"event":"announcement.delete","payload":"2"} + "announcement.delete"->{ + val id = EntityId.mayNull(payload?.toString()) + if( id != null){ + runOnMainLooper { + eachCallback { it.onAnnouncementDelete(id) } + } + } + } + + // {"event":"announcement.reaction","payload":"{\"name\":\"hourglass_gif\",\"count\":1,\"url\":\"https://m2j.zzz.ac/...\",\"static_url\":\"https://m2j.zzz.ac/...\",\"announcement_id\":\"9\"}"} + "announcement.reaction"->{ + if( payload is Reaction) { + runOnMainLooper { + eachCallback { it.onAnnouncementReaction(payload) } + } + } + } + + else -> when(payload) { + is TimelineItem -> fireTimelineItem(payload) + else -> log.d("unsupported payload type. $payload") + } + } + } + } + } + /** * Invoked when a text (type `0x1`) message has been received. */ @@ -263,34 +322,8 @@ internal class StreamReader( if(access_info.isMisskey) { handleMisskeyMessage(obj) } else { - - val event = obj.string("event") - - if(event == null || event.isEmpty()) { - log.d("onMessage: missing event parameter") - return - } - - if(event == "filters_changed") { - Column.onFiltersChanged(context, access_info) - return - } - - val payload = TootPayload.parsePayload(parser, event, obj, text) - - when(event) { - - "delete" -> when(payload) { - is Long -> fireDeleteId(EntityId(payload.toString())) - is String -> fireDeleteId(EntityId(payload.toString())) - else -> log.d("unsupported payload type. $payload") - } - - else -> when(payload) { - is TimelineItem -> fireTimelineItem(payload) - else -> log.d("unsupported payload type. $payload") - } - } + handleMastodonMessage(obj,text) + } } catch(ex : Throwable) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt new file mode 100644 index 00000000..4fb8f287 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt @@ -0,0 +1,162 @@ +package jp.juggler.subwaytooter.api.entity + +import android.text.Spannable +import jp.juggler.subwaytooter.api.TootParser +import jp.juggler.subwaytooter.util.DecodeOptions +import jp.juggler.util.JsonObject +import jp.juggler.util.LogCategory +import jp.juggler.util.notEmpty +import jp.juggler.util.notZero +import java.util.HashMap +import kotlin.Comparator +import kotlin.Int +import kotlin.String + +class TootAnnouncement(parser : TootParser, src : JsonObject) { + + class Reaction(val src : JsonObject) { + val name = src.string("name") ?: "?" + var count = src.long("count") ?: 0 + var me = src.boolean("me") // ストリーミングイベントではmeは定義されない + // 以下はカスタム絵文字のみ + val url = src.string("url") + val static_url = src.string("static_url") + + // ストリーミングイベントでは告知IDが含まれる + val announcement_id = EntityId.mayNull(src.string("announcement_id")) + } + + // {"id":"1", + // "content":"\u003cp\u003e日本語\u003cbr /\u003eURL \u003ca href=\"https://www.youtube.com/watch?v=2n1fM2ItdL8\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"ellipsis\"\u003eyoutube.com/watch?v=2n1fM2ItdL\u003c/span\u003e\u003cspan class=\"invisible\"\u003e8\u003c/span\u003e\u003c/a\u003e\u003cbr /\u003eカスタム絵文字 :ct013: \u003cbr /\u003e普通の絵文字 🤹 \u003c/p\u003e\u003cp\u003e改行2つ\u003c/p\u003e", + // "starts_at":"2020-01-23T00:00:00.000Z", + // "ends_at":"2020-01-28T23:59:00.000Z", + // "all_day":true, + // "mentions":[], + // "tags":[], + // "emojis":[{"shortcode":"ct013","url":"https://m2j.zzz.ac/custom_emojis/images/000/004/116/original/ct013.png","static_url":"https://m2j.zzz.ac/custom_emojis/images/000/004/116/static/ct013.png","visible_in_picker":true}], + // "reactions":[]}] + + val id = EntityId.mayDefault(src.string("id")) + val starts_at = TootStatus.parseTime(src.string("starts_at")) + val ends_at = TootStatus.parseTime(src.string("ends_at")) + val all_day = src.boolean("all_day") ?: false + val published_at = TootStatus.parseTime(src.string("published_at")) + val updated_at = TootStatus.parseTime(src.string("updated_at")) + + private val custom_emojis : HashMap? + + // Body of the status; this will contain HTML (remote HTML already sanitized) + val content : String + val decoded_content : Spannable + + //An array of Tags + val tags : ArrayList? + + // An array of Mentions + val mentions : ArrayList? + + + var reactions : MutableList? = null + + init { + // 絵文字マップはすぐ後で使うので、最初の方で読んでおく + this.custom_emojis = + parseMapOrNull(CustomEmoji.decode, src.jsonArray("emojis"), log) + + this.tags = parseListOrNull(::TootTag, src.jsonArray("tags")) + + this.mentions = parseListOrNull(::TootMention, src.jsonArray("mentions"), log) + + val options = DecodeOptions( + parser.context, + parser.linkHelper, + short = true, + decodeEmoji = true, + emojiMapCustom = custom_emojis, + // emojiMapProfile = profile_emojis, + // attachmentList = media_attachments, + highlightTrie = parser.highlightTrie, + mentions = mentions + ) + + + this.content = src.string("content") ?: "" + this.decoded_content = options.decodeHTML(content) + + this.reactions = parseListOrNull(::Reaction, src.jsonArray("reactions")) + } + + companion object { + private val log = LogCategory("TootAnnouncement") + + val comparator = Comparator { a, b -> + val at = a.starts_at.notZero() ?: a.published_at.notZero() ?: 0L + val bt = b.starts_at.notZero() ?: b.published_at.notZero() ?: 0L + if(at < bt) - 1 else if(at > bt) 1 else 0 + } + + // return null if list is empty + fun filterShown(src : List?) : List? { + val now = System.currentTimeMillis() + return src + ?.filter { + + when { + // 期間の大小が入れ替わってる場合はフィルタしない + it.starts_at > it.ends_at -> true + + // まだ開始していない + it.starts_at > 0L && now < it.starts_at -> false + + // 終了した後 + it.ends_at > 0L && now > it.ends_at -> false + + // フィルタしない + else -> true + } + } + ?.notEmpty() + ?.sortedWith(comparator) + } + + // return previous/next item in announcement list. + fun move(src : List?, currentId : EntityId?, delta : Int) : EntityId? { + + val listShown = filterShown(src) + ?: return null + + val size = listShown.size + if(size <= 0) return null + + val idx = delta + when(val v = listShown.indexOfFirst { it.id == currentId }) { + - 1 -> 0 + else -> v + } + return listShown[(idx + size) % size].id + } + + // https://github.com/tootsuite/mastodon/blob/b9d74d407673a6dbdc87c3310618b22c85358c85/app/javascript/mastodon/reducers/announcements.js#L64 + // reactionsのmeを残したまま他の項目を更新したい + fun merge(old : TootAnnouncement, dst : TootAnnouncement) : TootAnnouncement { + val oldReactions = old.reactions + val dstReactions = dst.reactions + if(dstReactions == null) { + dst.reactions = oldReactions + } else if(oldReactions != null) { + val reactions = mutableListOf() + reactions.addAll(oldReactions) + for(newItem in dstReactions) { + val oldItem = reactions.find { it.name == newItem.name } + if(oldItem == null) { + reactions.add(newItem) + } else { + oldItem.count = newItem.count + } + } + dst.reactions = reactions + } + return dst + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt index e70ba585..1ca7cdbf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt @@ -180,6 +180,7 @@ class TootInstance(parser : TootParser, src : JsonObject) { val VERSION_2_6_0 = VersionString("2.6.0") val VERSION_2_7_0_rc1 = VersionString("2.7.0rc1") val VERSION_3_0_0_rc1 = VersionString("3.0.0rc1") + val VERSION_3_1_0_rc1 = VersionString("3.1.0rc1") val MISSKEY_VERSION_11 = VersionString("11.0") diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootPayload.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootPayload.kt index 0b546acb..bf3adb33 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootPayload.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootPayload.kt @@ -1,6 +1,7 @@ package jp.juggler.subwaytooter.api.entity import jp.juggler.subwaytooter.api.TootParser +import jp.juggler.subwaytooter.api.entity.TootAnnouncement.Reaction import jp.juggler.util.* import java.util.regex.Pattern @@ -61,10 +62,13 @@ object TootPayload { "conversation" -> parseItem(::TootConversationSummary, parser, src) - // ここを通るケースはまだ確認できていない + "announcement" -> parseItem(::TootAnnouncement, parser, src) + + "announcement.reaction" -> parseItem(::Reaction, src) + else -> { log.e("unknown payload(2). message=%s", parent_text) - null + // ここを通るケースはまだ確認できていない } } } else if(payload[0] == '[') { diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt index af367001..06a0418d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt @@ -22,6 +22,7 @@ import java.util.regex.Pattern import kotlin.collections.ArrayList import kotlin.collections.LinkedHashMap import kotlin.math.abs +import kotlin.math.min class FilterTrees( val treeIrreversible : WordTrieTree = WordTrieTree(), @@ -995,6 +996,9 @@ class TootStatus(parser : TootParser, src : JsonObject) : TimelineItem() { @SuppressLint("SimpleDateFormat") internal val date_format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + + @SuppressLint("SimpleDateFormat") + internal val date_format2 = SimpleDateFormat("yyyy-MM-dd") fun formatTime(context : Context, t : Long, bAllowRelative : Boolean) : String { if(bAllowRelative && Pref.bpRelativeTimestamp(App1.pref)) { @@ -1050,7 +1054,53 @@ class TootStatus(parser : TootParser, src : JsonObject) : TimelineItem() { } } - return date_format.format(Date(t)) + return formatDate(t,date_format,omitZeroSecond = false) + } + + // 告知の開始/終了日付 + private fun formatDate( + t : Long, + format:SimpleDateFormat , + omitZeroSecond:Boolean + ) : String { + var dateTarget = format.format(Date(t)) + + // 秒の部分を省略する + if( omitZeroSecond && dateTarget.endsWith(":00")){ + dateTarget = dateTarget.substring(0,dateTarget.length -3) + } + + // 年の部分が現在と同じなら省略する + val dateNow = format.format(Date(t)) + val delm = dateNow.indexOf('-') + if(delm!=-1 && dateNow.substring(0,delm+1) == dateTarget.substring(0,delm+1)){ + dateTarget = dateTarget.substring(delm+1) + } + + return dateTarget + } + + fun formatTimeRange(start : Long, end : Long, allDay : Boolean):Pair{ + val strStart = when { + start <= 0L -> "" + allDay-> formatDate(start,date_format2,omitZeroSecond = false) + else -> formatDate(start, date_format,omitZeroSecond = true) + } + val strEnd = when { + end <= 0L -> "" + allDay-> formatDate(end,date_format2,omitZeroSecond = false) + else -> formatDate(end, date_format,omitZeroSecond = true) + } + // 終了日は先頭と同じ部分を省略する + var skip = 0 + for(i in 0 until min(strStart.length,strEnd.length)){ + val c =strStart[i] + if(c != strEnd[i] ) break + if( c.isDigit() ) continue + skip= i+1 + if( c == ' ') break // 時間以降は省略しない + } + return Pair( strStart,strEnd.substring(skip,strEnd.length)) } fun parseStringArray(src : JsonArray?) : ArrayList? { diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt b/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt index 2b3d08dd..10c6e0eb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt @@ -30,7 +30,12 @@ class EmojiPicker( private val activity : Activity, private val instance : String?, @Suppress("CanBeParameter") private val isMisskey : Boolean, - private val onEmojiPicked : (name : String, instance : String?, bInstanceHasCustomEmoji : Boolean) -> Unit + private val onEmojiPicked : ( + name : String, + instance : String?, + bInstanceHasCustomEmoji : Boolean, + unicode:String? + ) -> Unit // onEmojiPickedのinstance引数は通常の絵文字ならnull、カスタム絵文字なら非null、 ) : View.OnClickListener, ViewPager.OnPageChangeListener { @@ -501,26 +506,28 @@ class EmojiPicker( var name = item.name if(item.instance != null && item.instance.isNotEmpty()) { // カスタム絵文字 - selected(name, item.instance) + selected(name, item.instance,null) } else { // 普通の絵文字 - EmojiMap.sShortNameToEmojiInfo[name] ?: return + var ei = EmojiMap.sShortNameToEmojiInfo[name] ?: return if(page.hasSkinTone) { val sv = applySkinTone(name) - if(EmojiMap.sShortNameToEmojiInfo[sv] != null) { + val tmp = EmojiMap.sShortNameToEmojiInfo[sv] + if( tmp!=null){ + ei = tmp name = sv } } - selected(name, null) + selected(name, null,ei.unified) } } } } // name はスキントーン適用済みであること - internal fun selected(name : String, instance : String?) { + internal fun selected(name : String, instance : String?,unicode:String?) { dialog.dismissSafe() @@ -564,7 +571,7 @@ class EmojiPicker( } - onEmojiPicked(name, instance, bInstanceHasCustomEmoji) + onEmojiPicked(name, instance, bInstanceHasCustomEmoji,unicode) } internal inner class EmojiPickerPagerAdapter : androidx.viewpager.widget.PagerAdapter() { diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AnkoHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AnkoHelper.kt index a9892ce6..1af29434 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AnkoHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AnkoHelper.kt @@ -35,7 +35,7 @@ var View.endPadding : Int } // paddingStart,paddingEndにはsetterが提供されてない問題の対策 -fun View.setPaddingStartEnd(start : Int, end : Int) { +fun View.setPaddingStartEnd(start : Int, end : Int =start) { setPaddingRelative(start, paddingTop, end, paddingBottom) } 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 f0174ca7..cf85e322 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt @@ -1014,7 +1014,7 @@ class PostHelper( } private val open_picker_emoji : Runnable = Runnable { - EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji -> + EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji,_ -> val et = this.et ?: return@EmojiPicker val src = et.text ?: "" @@ -1042,7 +1042,7 @@ class PostHelper( } fun openEmojiPickerFromMore() { - EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji -> + EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji,_ -> val et = this.et ?: return@EmojiPicker val src = et.text ?: "" diff --git a/app/src/main/java/jp/juggler/util/HttpUtils.kt b/app/src/main/java/jp/juggler/util/HttpUtils.kt index 65f88dc1..cec3fff3 100644 --- a/app/src/main/java/jp/juggler/util/HttpUtils.kt +++ b/app/src/main/java/jp/juggler/util/HttpUtils.kt @@ -23,8 +23,8 @@ fun RequestBody.toPost() : Request.Builder = fun RequestBody.toPut() :Request.Builder = Request.Builder().put(this) -// fun RequestBody.toDelete():Request.Builder = -// Request.Builder().delete(this) +fun RequestBody.toDelete():Request.Builder = + Request.Builder().delete(this) fun RequestBody.toPatch() :Request.Builder = Request.Builder().patch(this) @@ -34,3 +34,4 @@ fun RequestBody.toRequest(methodArg : String) :Request.Builder = fun JsonObject.toPostRequestBuilder() : Request.Builder = toRequestBody( ).toPost() fun JsonObject.toPutRequestBuilder() : Request.Builder = toRequestBody( ).toPut() +fun JsonObject.toDeleteRequestBuilder() : Request.Builder = toRequestBody( ).toDelete() diff --git a/app/src/main/java/jp/juggler/util/ViewUtils.kt b/app/src/main/java/jp/juggler/util/ViewUtils.kt index d14d0d52..98c6cfb5 100644 --- a/app/src/main/java/jp/juggler/util/ViewUtils.kt +++ b/app/src/main/java/jp/juggler/util/ViewUtils.kt @@ -3,6 +3,7 @@ package jp.juggler.util import android.app.Activity import android.content.Context import android.content.ContextWrapper +import android.graphics.drawable.GradientDrawable import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager @@ -86,3 +87,11 @@ var CompoundButton.isCheckedNoAnime : Boolean isChecked = value jumpDrawablesToCurrentState() } + + +fun createRoundDrawable(radius:Float,fillColor:Int?=null, strokeColor:Int?=null, strokeWidth:Int = 4) = + GradientDrawable().apply{ + setCornerRadius(radius) + if(fillColor!=null) setColor(fillColor) + if( strokeColor!=null) setStroke(strokeWidth,strokeColor) + } diff --git a/app/src/main/res/drawable/ic_right.xml b/app/src/main/res/drawable/ic_arrow_end.xml similarity index 91% rename from app/src/main/res/drawable/ic_right.xml rename to app/src/main/res/drawable/ic_arrow_end.xml index 24835127..d97ee04f 100644 --- a/app/src/main/res/drawable/ic_right.xml +++ b/app/src/main/res/drawable/ic_arrow_end.xml @@ -1,6 +1,7 @@ + + diff --git a/app/src/main/res/layout/act_media_viewer.xml b/app/src/main/res/layout/act_media_viewer.xml index 6009a62c..a8c0f8cb 100644 --- a/app/src/main/res/layout/act_media_viewer.xml +++ b/app/src/main/res/layout/act_media_viewer.xml @@ -83,7 +83,7 @@ android:contentDescription="@string/previous" android:minWidth="48dp" - android:src="@drawable/ic_left" + android:src="@drawable/ic_arrow_start" android:tint="?attr/colorVectorDrawable" /> サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がありません。 サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がカラです。 通知を分割する (Android 6+) + 告知 + %1$d/%2$d + イベント期間: %1$s + イベント期間: %1$s~%2$s + 更新: %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ef9540f0..610cd1d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -996,4 +996,9 @@ Push notification will not work because server misconfiguration. Server public key is missing. Push notification will not work because server misconfiguration. Server public key is empty. Divide notifications (Android 6+) + Announcements + %1$d/%2$d + Event Period: %1$s + Event Period: %1$s…%2$s + Edited: %1$s \ No newline at end of file