(WIP)(Mastodon 3.1.0)ホームカラムに告知を表示する
This commit is contained in:
parent
7f4b93dc38
commit
99e2efb9bb
|
@ -487,6 +487,18 @@ class Column(
|
||||||
|
|
||||||
internal var language_filter : JsonObject? = null
|
internal var language_filter : JsonObject? = null
|
||||||
|
|
||||||
|
// 告知のリスト
|
||||||
|
internal var announcements : MutableList<TootAnnouncement>? = null
|
||||||
|
|
||||||
|
// 表示中の告知
|
||||||
|
internal var announcementId : EntityId? = null
|
||||||
|
|
||||||
|
// 告知を閉じた時刻, 0なら閉じていない
|
||||||
|
internal var announcementHideTime = 0L
|
||||||
|
|
||||||
|
// 告知データを更新したタイミング
|
||||||
|
internal var announcementUpdated = 0L
|
||||||
|
|
||||||
// プロフカラムでのアカウント情報
|
// プロフカラムでのアカウント情報
|
||||||
@Volatile
|
@Volatile
|
||||||
internal var who_account : TootAccountRef? = null
|
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<TootAnnouncement.Reaction>().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) {
|
internal fun resumeStreaming(bPutGap : Boolean) {
|
||||||
|
|
|
@ -43,9 +43,16 @@ abstract class ColumnTask(
|
||||||
val highlight_trie : WordTrieTree?
|
val highlight_trie : WordTrieTree?
|
||||||
get() = column.highlight_trie
|
get() = column.highlight_trie
|
||||||
|
|
||||||
|
val isPseudo :Boolean
|
||||||
|
get() = access_info.isPseudo
|
||||||
|
|
||||||
|
val isMastodon : Boolean
|
||||||
|
get() = access_info.isMastodon
|
||||||
|
|
||||||
val isMisskey : Boolean
|
val isMisskey : Boolean
|
||||||
get() = access_info.isMisskey
|
get() = access_info.isMisskey
|
||||||
|
|
||||||
|
|
||||||
val misskeyVersion : Int
|
val misskeyVersion : Int
|
||||||
get() = access_info.misskeyVersion
|
get() = access_info.misskeyVersion
|
||||||
|
|
||||||
|
|
|
@ -1039,5 +1039,31 @@ class ColumnTask_Loading(
|
||||||
// fallback to old api
|
// fallback to old api
|
||||||
return getStatusList(client, Column.PATH_DIRECT_MESSAGES)
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -398,7 +398,10 @@ enum class ColumnType(
|
||||||
iconId = { R.drawable.ic_home },
|
iconId = { R.drawable.ic_home },
|
||||||
name1 = { it.getString(R.string.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()) },
|
refresh = { client -> getStatusList(client, column.makeHomeTlUrl()) },
|
||||||
gap = { client -> getStatusList(client, column.makeHomeTlUrl()) },
|
gap = { client -> getStatusList(client, column.makeHomeTlUrl()) },
|
||||||
bAllowPseudo = false
|
bAllowPseudo = false
|
||||||
|
|
|
@ -6,7 +6,9 @@ import android.content.res.ColorStateList
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.AsyncTask
|
import android.os.AsyncTask
|
||||||
|
import android.os.SystemClock
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
|
import android.text.Spannable
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
@ -17,13 +19,26 @@ import android.widget.*
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.SwipyRefreshLayout
|
||||||
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection
|
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection
|
||||||
import jp.juggler.subwaytooter.action.Action_List
|
import jp.juggler.subwaytooter.action.Action_List
|
||||||
import jp.juggler.subwaytooter.action.Action_Notification
|
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.table.AcctColor
|
||||||
import jp.juggler.subwaytooter.util.*
|
import jp.juggler.subwaytooter.util.*
|
||||||
import jp.juggler.subwaytooter.view.ListDivider
|
import jp.juggler.subwaytooter.view.ListDivider
|
||||||
|
import jp.juggler.subwaytooter.view.MyLinkMovementMethod
|
||||||
|
import jp.juggler.subwaytooter.view.MyTextView
|
||||||
import jp.juggler.util.*
|
import jp.juggler.util.*
|
||||||
import org.jetbrains.anko.*
|
import org.jetbrains.anko.*
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
@ -137,6 +152,22 @@ class ColumnViewHolder(
|
||||||
private lateinit var etHashtagExtraAll : EditText
|
private lateinit var etHashtagExtraAll : EditText
|
||||||
private lateinit var etHashtagExtraNone : 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<NetworkEmojiInvalidator>()
|
||||||
|
|
||||||
private val isPageDestroyed : Boolean
|
private val isPageDestroyed : Boolean
|
||||||
get() = column == null || activity.isFinishing
|
get() = column == null || activity.isFinishing
|
||||||
|
|
||||||
|
@ -291,8 +322,6 @@ class ColumnViewHolder(
|
||||||
handled
|
handled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
btnQuickFilterAll.setOnClickListener(this)
|
btnQuickFilterAll.setOnClickListener(this)
|
||||||
btnQuickFilterMention.setOnClickListener(this)
|
btnQuickFilterMention.setOnClickListener(this)
|
||||||
btnQuickFilterFavourite.setOnClickListener(this)
|
btnQuickFilterFavourite.setOnClickListener(this)
|
||||||
|
@ -301,7 +330,6 @@ class ColumnViewHolder(
|
||||||
btnQuickFilterReaction.setOnClickListener(this)
|
btnQuickFilterReaction.setOnClickListener(this)
|
||||||
btnQuickFilterVote.setOnClickListener(this)
|
btnQuickFilterVote.setOnClickListener(this)
|
||||||
|
|
||||||
|
|
||||||
llColumnHeader.setOnClickListener(this)
|
llColumnHeader.setOnClickListener(this)
|
||||||
btnColumnSetting.setOnClickListener(this)
|
btnColumnSetting.setOnClickListener(this)
|
||||||
btnColumnReload.setOnClickListener(this)
|
btnColumnReload.setOnClickListener(this)
|
||||||
|
@ -317,6 +345,10 @@ class ColumnViewHolder(
|
||||||
|
|
||||||
llRefreshError.setOnClickListener(this)
|
llRefreshError.setOnClickListener(this)
|
||||||
|
|
||||||
|
btnAnnouncementsShowHide.setOnClickListener(this)
|
||||||
|
btnAnnouncementsPrev.setOnClickListener(this)
|
||||||
|
btnAnnouncementsNext.setOnClickListener(this)
|
||||||
|
|
||||||
|
|
||||||
cbDontCloseColumn.setOnCheckedChangeListener(this)
|
cbDontCloseColumn.setOnCheckedChangeListener(this)
|
||||||
cbWithAttachment.setOnCheckedChangeListener(this)
|
cbWithAttachment.setOnCheckedChangeListener(this)
|
||||||
|
@ -426,6 +458,10 @@ class ColumnViewHolder(
|
||||||
activity.handler.removeCallbacks(proc_start_filter)
|
activity.handler.removeCallbacks(proc_start_filter)
|
||||||
activity.handler.postDelayed(proc_start_filter, 666L)
|
activity.handler.postDelayed(proc_start_filter, 666L)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
announcementContentInvalidator =
|
||||||
|
NetworkEmojiInvalidator(activity.handler, tvAnnouncementContent)
|
||||||
|
tvAnnouncementContent.movementMethod = MyLinkMovementMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
private val proc_start_filter : Runnable = Runnable {
|
private val proc_start_filter : Runnable = Runnable {
|
||||||
|
@ -611,7 +647,7 @@ class ColumnViewHolder(
|
||||||
|
|
||||||
btnDeleteNotification.vg(column.isNotificationColumn)
|
btnDeleteNotification.vg(column.isNotificationColumn)
|
||||||
|
|
||||||
llSearch.vg(column.isSearchColumn)?.let{
|
llSearch.vg(column.isSearchColumn)?.let {
|
||||||
btnSearchClear.vg(Pref.bpShowSearchClear(activity.pref))
|
btnSearchClear.vg(Pref.bpShowSearchClear(activity.pref))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -652,6 +688,8 @@ class ColumnViewHolder(
|
||||||
|
|
||||||
column.addColumnViewHolder(this)
|
column.addColumnViewHolder(this)
|
||||||
|
|
||||||
|
lastAnnouncementShown = - 1L
|
||||||
|
|
||||||
showColumnColor()
|
showColumnColor()
|
||||||
|
|
||||||
showContent(reason = "onPageCreate", reset = true)
|
showContent(reason = "onPageCreate", reset = true)
|
||||||
|
@ -1060,8 +1098,30 @@ class ColumnViewHolder(
|
||||||
btnQuickFilterReaction -> clickQuickFilter(Column.QUICK_FILTER_REACTION)
|
btnQuickFilterReaction -> clickQuickFilter(Column.QUICK_FILTER_REACTION)
|
||||||
btnQuickFilterVote -> clickQuickFilter(Column.QUICK_FILTER_VOTE)
|
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 {
|
override fun onLongClick(v : View) : Boolean {
|
||||||
|
@ -1130,12 +1190,14 @@ class ColumnViewHolder(
|
||||||
|
|
||||||
showColumnCloseButton()
|
showColumnCloseButton()
|
||||||
|
|
||||||
|
showAnnouncements(force = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// カラムヘッダなど、負荷が低い部分の表示更新
|
// カラムヘッダなど、負荷が低い部分の表示更新
|
||||||
fun showColumnHeader() {
|
fun showColumnHeader() {
|
||||||
activity.handler.removeCallbacks(procShowColumnHeader)
|
activity.handler.removeCallbacks(procShowColumnHeader)
|
||||||
activity.handler.postDelayed(procShowColumnHeader, 50L)
|
activity.handler.postDelayed(procShowColumnHeader, 50L)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun showContent(
|
internal fun showContent(
|
||||||
|
@ -1198,6 +1260,7 @@ class ColumnViewHolder(
|
||||||
showRefreshError()
|
showRefreshError()
|
||||||
}
|
}
|
||||||
proc_restoreScrollPosition.run()
|
proc_restoreScrollPosition.run()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var bRefreshErrorWillShown = false
|
private var bRefreshErrorWillShown = false
|
||||||
|
@ -1887,6 +1950,121 @@ class ColumnViewHolder(
|
||||||
|
|
||||||
} // end of column setting scroll view
|
} // 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 {
|
llSearch = verticalLayout {
|
||||||
lparams(matchParent, wrapContent)
|
lparams(matchParent, wrapContent)
|
||||||
backgroundColor = getAttributeColor(context, R.attr.colorSearchFormBackground)
|
backgroundColor = getAttributeColor(context, R.attr.colorSearchFormBackground)
|
||||||
|
@ -2112,4 +2290,367 @@ class ColumnViewHolder(
|
||||||
b.report()
|
b.report()
|
||||||
rv
|
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<ViewGroup.MarginLayoutParams>()
|
||||||
|
?.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3365,7 +3365,7 @@ internal class ItemViewHolder(
|
||||||
context,
|
context,
|
||||||
R.drawable.btn_bg_transparent
|
R.drawable.btn_bg_transparent
|
||||||
)
|
)
|
||||||
contentDescription = "@string/hide"
|
contentDescription = context.getString(R.string.hide)
|
||||||
imageResource = R.drawable.ic_close
|
imageResource = R.drawable.ic_close
|
||||||
}.lparams(dip(32), dip(32)) {
|
}.lparams(dip(32), dip(32)) {
|
||||||
gravity = Gravity.END
|
gravity = Gravity.END
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.SharedPreferences
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import jp.juggler.subwaytooter.api.*
|
import jp.juggler.subwaytooter.api.*
|
||||||
import jp.juggler.subwaytooter.api.entity.*
|
import jp.juggler.subwaytooter.api.entity.*
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootAnnouncement.Reaction
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.util.*
|
import jp.juggler.util.*
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
@ -24,11 +25,14 @@ internal class StreamReader(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
internal interface StreamCallback {
|
internal interface StreamCallback {
|
||||||
|
fun channelId() : String?
|
||||||
|
|
||||||
fun onTimelineItem(item : TimelineItem)
|
fun onTimelineItem(item : TimelineItem)
|
||||||
fun onListeningStateChanged(bListen : Boolean)
|
fun onListeningStateChanged(bListen : Boolean)
|
||||||
fun onNoteUpdated(ev : MisskeyNoteUpdate)
|
fun onNoteUpdated(ev : MisskeyNoteUpdate)
|
||||||
|
fun onAnnouncementUpdate( item: TootAnnouncement)
|
||||||
fun channelId() : String?
|
fun onAnnouncementDelete( id: EntityId)
|
||||||
|
fun onAnnouncementReaction(reaction : Reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -131,14 +135,12 @@ internal class StreamReader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fireTimelineItem(item : TimelineItem?, channelId : String? = null) {
|
private inline fun eachCallback(block:(callback:StreamCallback)->Unit){
|
||||||
item ?: return
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if(bDisposed.get()) return@synchronized
|
if(bDisposed.get()) return@synchronized
|
||||||
for(callback in callback_list) {
|
for(callback in callback_list) {
|
||||||
try {
|
try {
|
||||||
if(channelId != null && channelId != callback.channelId()) continue
|
block(callback)
|
||||||
callback.onTimelineItem(item)
|
|
||||||
} catch(ex : Throwable) {
|
} catch(ex : Throwable) {
|
||||||
log.trace(ex)
|
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) {
|
private fun fireDeleteId(id : EntityId) {
|
||||||
|
|
||||||
val tl_host = access_info.host
|
val tl_host = access_info.host
|
||||||
runOnMainLooper {
|
runOnMainLooper {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if(bDisposed.get()) return@runOnMainLooper
|
if(bDisposed.get()) return@synchronized
|
||||||
if(Pref.bpDontRemoveDeletedToot(App1.getAppState(context).pref)) return@runOnMainLooper
|
if(Pref.bpDontRemoveDeletedToot(App1.getAppState(context).pref)) return@synchronized
|
||||||
for(column in App1.getAppState(context).column_list) {
|
for(column in App1.getAppState(context).column_list) {
|
||||||
try {
|
try {
|
||||||
column.onStatusRemoved(tl_host, id)
|
column.onStatusRemoved(tl_host, id)
|
||||||
|
@ -165,16 +176,9 @@ internal class StreamReader(
|
||||||
|
|
||||||
private fun fireNoteUpdated(ev : MisskeyNoteUpdate, channelId : String? = null) {
|
private fun fireNoteUpdated(ev : MisskeyNoteUpdate, channelId : String? = null) {
|
||||||
runOnMainLooper {
|
runOnMainLooper {
|
||||||
synchronized(this) {
|
eachCallback { callback->
|
||||||
if(bDisposed.get()) return@runOnMainLooper
|
if(channelId != null && channelId != callback.channelId()) return@eachCallback
|
||||||
for(callback in callback_list) {
|
callback.onNoteUpdated(ev)
|
||||||
try {
|
|
||||||
if(channelId != null && channelId != callback.channelId()) continue
|
|
||||||
callback.onNoteUpdated(ev)
|
|
||||||
} catch(ex : Throwable) {
|
|
||||||
log.trace(ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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\":\"<p>追加</p>\",\"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.
|
* Invoked when a text (type `0x1`) message has been received.
|
||||||
*/
|
*/
|
||||||
|
@ -263,34 +322,8 @@ internal class StreamReader(
|
||||||
if(access_info.isMisskey) {
|
if(access_info.isMisskey) {
|
||||||
handleMisskeyMessage(obj)
|
handleMisskeyMessage(obj)
|
||||||
} else {
|
} else {
|
||||||
|
handleMastodonMessage(obj,text)
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch(ex : Throwable) {
|
} catch(ex : Throwable) {
|
||||||
|
|
|
@ -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<String, CustomEmoji>?
|
||||||
|
|
||||||
|
// 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<TootTag>?
|
||||||
|
|
||||||
|
// An array of Mentions
|
||||||
|
val mentions : ArrayList<TootMention>?
|
||||||
|
|
||||||
|
|
||||||
|
var reactions : MutableList<Reaction>? = 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<TootAnnouncement> { 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<TootAnnouncement>?) : List<TootAnnouncement>? {
|
||||||
|
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<TootAnnouncement>?, 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<Reaction>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -180,6 +180,7 @@ class TootInstance(parser : TootParser, src : JsonObject) {
|
||||||
val VERSION_2_6_0 = VersionString("2.6.0")
|
val VERSION_2_6_0 = VersionString("2.6.0")
|
||||||
val VERSION_2_7_0_rc1 = VersionString("2.7.0rc1")
|
val VERSION_2_7_0_rc1 = VersionString("2.7.0rc1")
|
||||||
val VERSION_3_0_0_rc1 = VersionString("3.0.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")
|
val MISSKEY_VERSION_11 = VersionString("11.0")
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package jp.juggler.subwaytooter.api.entity
|
package jp.juggler.subwaytooter.api.entity
|
||||||
|
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
import jp.juggler.subwaytooter.api.TootParser
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootAnnouncement.Reaction
|
||||||
import jp.juggler.util.*
|
import jp.juggler.util.*
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
@ -61,10 +62,13 @@ object TootPayload {
|
||||||
|
|
||||||
"conversation" -> parseItem(::TootConversationSummary, parser, src)
|
"conversation" -> parseItem(::TootConversationSummary, parser, src)
|
||||||
|
|
||||||
// ここを通るケースはまだ確認できていない
|
"announcement" -> parseItem(::TootAnnouncement, parser, src)
|
||||||
|
|
||||||
|
"announcement.reaction" -> parseItem(::Reaction, src)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
log.e("unknown payload(2). message=%s", parent_text)
|
log.e("unknown payload(2). message=%s", parent_text)
|
||||||
null
|
// ここを通るケースはまだ確認できていない
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if(payload[0] == '[') {
|
} else if(payload[0] == '[') {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import java.util.regex.Pattern
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.LinkedHashMap
|
import kotlin.collections.LinkedHashMap
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
class FilterTrees(
|
class FilterTrees(
|
||||||
val treeIrreversible : WordTrieTree = WordTrieTree(),
|
val treeIrreversible : WordTrieTree = WordTrieTree(),
|
||||||
|
@ -995,6 +996,9 @@ class TootStatus(parser : TootParser, src : JsonObject) : TimelineItem() {
|
||||||
|
|
||||||
@SuppressLint("SimpleDateFormat")
|
@SuppressLint("SimpleDateFormat")
|
||||||
internal val date_format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
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 {
|
fun formatTime(context : Context, t : Long, bAllowRelative : Boolean) : String {
|
||||||
if(bAllowRelative && Pref.bpRelativeTimestamp(App1.pref)) {
|
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<String,String>{
|
||||||
|
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<String>? {
|
fun parseStringArray(src : JsonArray?) : ArrayList<String>? {
|
||||||
|
|
|
@ -30,7 +30,12 @@ class EmojiPicker(
|
||||||
private val activity : Activity,
|
private val activity : Activity,
|
||||||
private val instance : String?,
|
private val instance : String?,
|
||||||
@Suppress("CanBeParameter") private val isMisskey : Boolean,
|
@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、
|
// onEmojiPickedのinstance引数は通常の絵文字ならnull、カスタム絵文字なら非null、
|
||||||
) : View.OnClickListener, ViewPager.OnPageChangeListener {
|
) : View.OnClickListener, ViewPager.OnPageChangeListener {
|
||||||
|
|
||||||
|
@ -501,26 +506,28 @@ class EmojiPicker(
|
||||||
var name = item.name
|
var name = item.name
|
||||||
if(item.instance != null && item.instance.isNotEmpty()) {
|
if(item.instance != null && item.instance.isNotEmpty()) {
|
||||||
// カスタム絵文字
|
// カスタム絵文字
|
||||||
selected(name, item.instance)
|
selected(name, item.instance,null)
|
||||||
} else {
|
} else {
|
||||||
// 普通の絵文字
|
// 普通の絵文字
|
||||||
EmojiMap.sShortNameToEmojiInfo[name] ?: return
|
var ei = EmojiMap.sShortNameToEmojiInfo[name] ?: return
|
||||||
|
|
||||||
if(page.hasSkinTone) {
|
if(page.hasSkinTone) {
|
||||||
val sv = applySkinTone(name)
|
val sv = applySkinTone(name)
|
||||||
if(EmojiMap.sShortNameToEmojiInfo[sv] != null) {
|
val tmp = EmojiMap.sShortNameToEmojiInfo[sv]
|
||||||
|
if( tmp!=null){
|
||||||
|
ei = tmp
|
||||||
name = sv
|
name = sv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selected(name, null)
|
selected(name, null,ei.unified)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// name はスキントーン適用済みであること
|
// name はスキントーン適用済みであること
|
||||||
internal fun selected(name : String, instance : String?) {
|
internal fun selected(name : String, instance : String?,unicode:String?) {
|
||||||
|
|
||||||
dialog.dismissSafe()
|
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() {
|
internal inner class EmojiPickerPagerAdapter : androidx.viewpager.widget.PagerAdapter() {
|
||||||
|
|
|
@ -35,7 +35,7 @@ var View.endPadding : Int
|
||||||
}
|
}
|
||||||
|
|
||||||
// paddingStart,paddingEndにはsetterが提供されてない問題の対策
|
// paddingStart,paddingEndにはsetterが提供されてない問題の対策
|
||||||
fun View.setPaddingStartEnd(start : Int, end : Int) {
|
fun View.setPaddingStartEnd(start : Int, end : Int =start) {
|
||||||
setPaddingRelative(start, paddingTop, end, paddingBottom)
|
setPaddingRelative(start, paddingTop, end, paddingBottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1014,7 +1014,7 @@ class PostHelper(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val open_picker_emoji : Runnable = Runnable {
|
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 et = this.et ?: return@EmojiPicker
|
||||||
|
|
||||||
val src = et.text ?: ""
|
val src = et.text ?: ""
|
||||||
|
@ -1042,7 +1042,7 @@ class PostHelper(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openEmojiPickerFromMore() {
|
fun openEmojiPickerFromMore() {
|
||||||
EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji ->
|
EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji,_ ->
|
||||||
val et = this.et ?: return@EmojiPicker
|
val et = this.et ?: return@EmojiPicker
|
||||||
|
|
||||||
val src = et.text ?: ""
|
val src = et.text ?: ""
|
||||||
|
|
|
@ -23,8 +23,8 @@ fun RequestBody.toPost() : Request.Builder =
|
||||||
fun RequestBody.toPut() :Request.Builder =
|
fun RequestBody.toPut() :Request.Builder =
|
||||||
Request.Builder().put(this)
|
Request.Builder().put(this)
|
||||||
|
|
||||||
// fun RequestBody.toDelete():Request.Builder =
|
fun RequestBody.toDelete():Request.Builder =
|
||||||
// Request.Builder().delete(this)
|
Request.Builder().delete(this)
|
||||||
|
|
||||||
fun RequestBody.toPatch() :Request.Builder =
|
fun RequestBody.toPatch() :Request.Builder =
|
||||||
Request.Builder().patch(this)
|
Request.Builder().patch(this)
|
||||||
|
@ -34,3 +34,4 @@ fun RequestBody.toRequest(methodArg : String) :Request.Builder =
|
||||||
|
|
||||||
fun JsonObject.toPostRequestBuilder() : Request.Builder = toRequestBody( ).toPost()
|
fun JsonObject.toPostRequestBuilder() : Request.Builder = toRequestBody( ).toPost()
|
||||||
fun JsonObject.toPutRequestBuilder() : Request.Builder = toRequestBody( ).toPut()
|
fun JsonObject.toPutRequestBuilder() : Request.Builder = toRequestBody( ).toPut()
|
||||||
|
fun JsonObject.toDeleteRequestBuilder() : Request.Builder = toRequestBody( ).toDelete()
|
||||||
|
|
|
@ -3,6 +3,7 @@ package jp.juggler.util
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
@ -86,3 +87,11 @@ var CompoundButton.isCheckedNoAnime : Boolean
|
||||||
isChecked = value
|
isChecked = value
|
||||||
jumpDrawablesToCurrentState()
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
|
android:autoMirrored="true"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
|
@ -1,6 +1,7 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
|
android:autoMirrored="true"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
||||||
|
</vector>
|
|
@ -83,7 +83,7 @@
|
||||||
android:contentDescription="@string/previous"
|
android:contentDescription="@string/previous"
|
||||||
|
|
||||||
android:minWidth="48dp"
|
android:minWidth="48dp"
|
||||||
android:src="@drawable/ic_left"
|
android:src="@drawable/ic_arrow_start"
|
||||||
android:tint="?attr/colorVectorDrawable" />
|
android:tint="?attr/colorVectorDrawable" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:contentDescription="@string/next"
|
android:contentDescription="@string/next"
|
||||||
android:minWidth="48dp"
|
android:minWidth="48dp"
|
||||||
android:src="@drawable/ic_right"
|
android:src="@drawable/ic_arrow_end"
|
||||||
android:tint="?attr/colorVectorDrawable" />
|
android:tint="?attr/colorVectorDrawable" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
|
|
@ -1001,5 +1001,10 @@
|
||||||
<string name="push_notification_server_key_missing">サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がありません。</string>
|
<string name="push_notification_server_key_missing">サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がありません。</string>
|
||||||
<string name="push_notification_server_key_empty">サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がカラです。</string>
|
<string name="push_notification_server_key_empty">サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がカラです。</string>
|
||||||
<string name="divide_notification">通知を分割する (Android 6+)</string>
|
<string name="divide_notification">通知を分割する (Android 6+)</string>
|
||||||
|
<string name="announcements">告知</string>
|
||||||
|
<string name="announcements_index">%1$d/%2$d</string>
|
||||||
|
<string name="announcements_period1">イベント期間: %1$s</string>
|
||||||
|
<string name="announcements_period2">イベント期間: %1$s~%2$s</string>
|
||||||
|
<string name="edited_at">更新: %1$s</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -996,4 +996,9 @@
|
||||||
<string name="push_notification_server_key_missing">Push notification will not work because server misconfiguration. Server public key is missing.</string>
|
<string name="push_notification_server_key_missing">Push notification will not work because server misconfiguration. Server public key is missing.</string>
|
||||||
<string name="push_notification_server_key_empty">Push notification will not work because server misconfiguration. Server public key is empty.</string>
|
<string name="push_notification_server_key_empty">Push notification will not work because server misconfiguration. Server public key is empty.</string>
|
||||||
<string name="divide_notification">Divide notifications (Android 6+)</string>
|
<string name="divide_notification">Divide notifications (Android 6+)</string>
|
||||||
|
<string name="announcements">Announcements</string>
|
||||||
|
<string name="announcements_index">%1$d/%2$d</string>
|
||||||
|
<string name="announcements_period1">Event Period: %1$s</string>
|
||||||
|
<string name="announcements_period2">Event Period: %1$s…%2$s</string>
|
||||||
|
<string name="edited_at">Edited: %1$s</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue