(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 announcements : MutableList<TootAnnouncement>? = 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<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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<NetworkEmojiInvalidator>()
|
||||
|
||||
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<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,
|
||||
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
|
||||
|
|
|
@ -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\":\"<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.
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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_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")
|
||||
|
||||
|
|
|
@ -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] == '[') {
|
||||
|
|
|
@ -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<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>? {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ?: ""
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
|
@ -1,6 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<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:minWidth="48dp"
|
||||
android:src="@drawable/ic_left"
|
||||
android:src="@drawable/ic_arrow_start"
|
||||
android:tint="?attr/colorVectorDrawable" />
|
||||
|
||||
<ImageButton
|
||||
|
@ -92,7 +92,7 @@
|
|||
android:layout_height="48dp"
|
||||
android:contentDescription="@string/next"
|
||||
android:minWidth="48dp"
|
||||
android:src="@drawable/ic_right"
|
||||
android:src="@drawable/ic_arrow_end"
|
||||
android:tint="?attr/colorVectorDrawable" />
|
||||
|
||||
<ImageButton
|
||||
|
|
|
@ -1001,5 +1001,10 @@
|
|||
<string name="push_notification_server_key_missing">サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がありません。</string>
|
||||
<string name="push_notification_server_key_empty">サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がカラです。</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>
|
||||
|
|
|
@ -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_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="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>
|
Loading…
Reference in New Issue