more split...

This commit is contained in:
tateisu 2021-06-24 11:31:34 +09:00
parent 6de96b4852
commit 9395774a48
33 changed files with 1158 additions and 1195 deletions

View File

@ -7,10 +7,8 @@ import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Typeface
import android.os.*
import android.text.InputType
import android.text.Spannable
import android.view.*
import android.view.inputmethod.EditorInfo
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
@ -40,8 +38,7 @@ class ActMain : AppCompatActivity(),
MyClickableSpanHandler {
companion object {
val log = LogCategory("ActMain")
private val log = LogCategory("ActMain")
// リザルト
const val RESULT_APP_DATA_IMPORT = Activity.RESULT_FIRST_USER
@ -122,9 +119,7 @@ class ActMain : AppCompatActivity(),
var quickTootVisibility: TootVisibility = TootVisibility.AccountSetting
//////////////////////////////////////////////////////////////////
// 変更しない変数(lateinit)
lateinit var llFormRoot: LinearLayout
lateinit var llQuickTootBar: LinearLayout
lateinit var etQuickToot: MyEditText
lateinit var btnQuickToot: ImageButton
@ -879,106 +874,11 @@ class ActMain : AppCompatActivity(),
return rv
}
internal fun initUI() {
setContentView(R.layout.act_main)
App1.initEdgeToEdge(this)
quickTootVisibility =
TootVisibility.parseSavedVisibility(PrefS.spQuickTootVisibility(pref))
?: quickTootVisibility
Column.reloadDefaultColor(this, pref)
var sv = PrefS.spTimelineFont(pref)
if (sv.isNotEmpty()) {
try {
timelineFont = Typeface.createFromFile(sv)
} catch (ex: Throwable) {
log.trace(ex)
}
}
sv = PrefS.spTimelineFontBold(pref)
if (sv.isNotEmpty()) {
try {
timeline_font_bold = Typeface.createFromFile(sv)
} catch (ex: Throwable) {
log.trace(ex)
}
} else {
try {
timeline_font_bold = Typeface.create(timelineFont, Typeface.BOLD)
} catch (ex: Throwable) {
log.trace(ex)
}
}
fun parseIconSize(stringPref: StringPref, minDp: Float = 1f): Int {
var iconSizeDp = stringPref.defVal.toFloat()
try {
sv = stringPref(pref)
val fv = if (sv.isEmpty()) Float.NaN else sv.toFloat()
if (fv.isFinite() && fv >= minDp) {
iconSizeDp = fv
}
} catch (ex: Throwable) {
log.trace(ex)
}
return (0.5f + iconSizeDp * density).toInt()
}
avatarIconSize = parseIconSize(PrefS.spAvatarIconSize)
notificationTlIconSize = parseIconSize(PrefS.spNotificationTlIconSize)
boostButtonSize = parseIconSize(PrefS.spBoostButtonSize)
replyIconSize = parseIconSize(PrefS.spReplyIconSize)
headerIconSize = parseIconSize(PrefS.spHeaderIconSize)
stripIconSize = parseIconSize(PrefS.spStripIconSize)
screenBottomPadding = parseIconSize(PrefS.spScreenBottomPadding, minDp = 0f)
run {
var roundRatio = 33f
try {
if (PrefB.bpDontRound(pref)) {
roundRatio = 0f
} else {
sv = PrefS.spRoundRatio(pref)
if (sv.isNotEmpty()) {
val fv = sv.toFloat()
if (fv.isFinite()) {
roundRatio = fv
}
}
}
} catch (ex: Throwable) {
log.trace(ex)
}
Styler.round_ratio = clipRange(0f, 1f, roundRatio / 100f) * 0.5f
}
run {
var boostAlpha = 0.8f
try {
val f = (PrefS.spBoostAlpha.toInt(pref).toFloat() + 0.5f) / 100f
boostAlpha = when {
f >= 1f -> 1f
f < 0f -> 0.66f
else -> f
}
} catch (ex: Throwable) {
log.trace(ex)
}
Styler.boostAlpha = boostAlpha
}
// lateinitなビュー変数を初期化する
fun findViews() {
llFormRoot = findViewById(R.id.llFormRoot)
llEmpty = findViewById(R.id.llEmpty)
drawer = findViewById(R.id.drawer_layout)
drawer.addDrawerListener(this)
drawer.setExclusionSize(stripIconSize)
SideMenuAdapter(this, handler, findViewById(R.id.nav_view), drawer)
btnMenu = findViewById(R.id.btnMenu)
btnToot = findViewById(R.id.btnToot)
vFooterDivider1 = findViewById(R.id.vFooterDivider1)
@ -990,128 +890,58 @@ class ActMain : AppCompatActivity(),
btnQuickToot = findViewById(R.id.btnQuickToot)
btnQuickTootMenu = findViewById(R.id.btnQuickTootMenu)
val llFormRoot: LinearLayout = findViewById(R.id.llFormRoot)
llFormRoot.setPadding(0, 0, 0, screenBottomPadding)
etQuickToot.typeface = timelineFont
when (PrefI.ipJustifyWindowContentPortrait(pref)) {
PrefI.JWCP_START -> {
val iconW = (stripIconSize * 1.5f + 0.5f).toInt()
val padding = resources.displayMetrics.widthPixels / 2 - iconW
fun ViewGroup.addViewBeforeLast(v: View) = addView(v, childCount - 1)
(svColumnStrip.parent as LinearLayout).addViewBeforeLast(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
llQuickTootBar.addViewBeforeLast(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
}
PrefI.JWCP_END -> {
val iconW = (stripIconSize * 1.5f + 0.5f).toInt()
val borderWidth = (1f * density + 0.5f).toInt()
val padding = resources.displayMetrics.widthPixels / 2 - iconW - borderWidth
fun ViewGroup.addViewAfterFirst(v: View) = addView(v, 1)
(svColumnStrip.parent as LinearLayout).addViewAfterFirst(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
llQuickTootBar.addViewAfterFirst(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
}
}
if (!PrefB.bpQuickTootBar(pref)) {
llQuickTootBar.visibility = View.GONE
}
btnToot.setOnClickListener(this)
btnMenu.setOnClickListener(this)
btnQuickToot.setOnClickListener(this)
btnQuickTootMenu.setOnClickListener(this)
}
if (PrefB.bpDontUseActionButtonWithQuickTootBar(pref)) {
etQuickToot.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
etQuickToot.imeOptions = EditorInfo.IME_ACTION_NONE
// 最後に指定する必要がある?
etQuickToot.maxLines = 5
etQuickToot.isVerticalScrollBarEnabled = true
etQuickToot.isScrollbarFadingEnabled = false
} else {
etQuickToot.inputType = InputType.TYPE_CLASS_TEXT
etQuickToot.imeOptions = EditorInfo.IME_ACTION_SEND
etQuickToot.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEND) {
btnQuickToot.performClick()
return@OnEditorActionListener true
}
false
})
// 最後に指定する必要がある?
etQuickToot.maxLines = 1
}
internal fun initUI() {
setContentView(R.layout.act_main)
App1.initEdgeToEdge(this)
quickTootVisibility =
TootVisibility.parseSavedVisibility(PrefS.spQuickTootVisibility(pref))
?: quickTootVisibility
Column.reloadDefaultColor(this, pref)
reloadFonts()
reloadIconSize()
reloadRoundRatio()
reloadBoostAlpha()
findViews()
drawer.addDrawerListener(this)
drawer.setExclusionSize(stripIconSize)
SideMenuAdapter(this, handler, findViewById(R.id.nav_view), drawer)
llFormRoot.setPadding(0, 0, 0, screenBottomPadding)
justifyWindowContentPortrait()
initUIQuickToot()
svColumnStrip.isHorizontalFadingEdgeEnabled = true
completionHelper = CompletionHelper(this, pref, appState.handler)
val dm = resources.displayMetrics
val density = dm.density
var mediaThumbHeightDp = 64
sv = PrefS.spMediaThumbHeight(pref)
if (sv.isNotEmpty()) {
try {
val iv = Integer.parseInt(sv)
if (iv >= 32) {
mediaThumbHeightDp = iv
}
} catch (ex: Throwable) {
log.trace(ex)
}
}
appState.mediaThumbHeight = (0.5f + mediaThumbHeightDp * density).toInt()
var columnWMinDp = COLUMN_WIDTH_MIN_DP
sv = PrefS.spColumnWidth(pref)
if (sv.isNotEmpty()) {
try {
val iv = Integer.parseInt(sv)
if (iv >= 100) {
columnWMinDp = iv
}
} catch (ex: Throwable) {
log.trace(ex)
}
}
val columnWMin = (0.5f + columnWMinDp * density).toInt()
reloadMediaHeight()
val columnWMin = loadColumnMin(density)
val sw = dm.widthPixels
// スマホモードとタブレットモードの切り替え
if (PrefB.bpDisableTabletMode(pref) || sw < columnWMin * 2) {
// SmartPhone mode
phoneViews = PhoneViews(this)
} else {
// Tablet mode
tabletViews = TabletViews(this)
}
val tmpPhonePager: MyViewPager = findViewById(R.id.viewPager)
val tmpTabletPager: RecyclerView = findViewById(R.id.rvPager)
phoneTab({ env ->
tmpTabletPager.visibility = View.GONE
env.initUI(tmpPhonePager)
@ -1119,23 +949,8 @@ class ActMain : AppCompatActivity(),
}, { env ->
tmpPhonePager.visibility = View.GONE
env.initUI(tmpTabletPager)
})
showFooterColor()
completionHelper.attachEditText(
llFormRoot,
etQuickToot,
true,
object : CompletionHelper.Callback2 {
override fun onTextUpdate() {}
override fun canOpenPopup(): Boolean {
return !drawer.isDrawerOpen(GravityCompat.START)
}
})
showQuickTootVisibility()
}
}

View File

@ -63,4 +63,3 @@ fun ActMain.refreshAfterPost() {
this.postedAcct = null
this.postedStatusId = null
}

View File

@ -1,20 +1,14 @@
package jp.juggler.subwaytooter
import android.content.Intent
import android.text.SpannableStringBuilder
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.RawRes
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
import java.lang.ref.WeakReference
import kotlin.math.abs
import kotlin.math.min
fun ActMain.resizeAutoCW(columnW: Int) {
val sv = PrefS.spAutoCWLines(pref)
@ -144,4 +138,4 @@ fun ActMain.closeListItemPopup() {
} catch (ignored: Throwable) {
}
listItemPopup = null
}
}

View File

@ -1,9 +1,15 @@
package jp.juggler.subwaytooter
import android.text.InputType
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.core.view.GravityCompat
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.CompletionHelper
import jp.juggler.subwaytooter.util.PostCompleteCallback
import jp.juggler.subwaytooter.util.PostImpl
import jp.juggler.util.hideKeyboard
@ -14,6 +20,49 @@ import org.jetbrains.anko.imageResource
val ActMain.quickTootText: String
get() = etQuickToot.text.toString()
fun ActMain.initUIQuickToot() {
etQuickToot.typeface = ActMain.timelineFont
if (!PrefB.bpQuickTootBar(pref)) {
llQuickTootBar.visibility = View.GONE
}
if (PrefB.bpDontUseActionButtonWithQuickTootBar(pref)) {
etQuickToot.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
etQuickToot.imeOptions = EditorInfo.IME_ACTION_NONE
// 最後に指定する必要がある?
etQuickToot.maxLines = 5
etQuickToot.isVerticalScrollBarEnabled = true
etQuickToot.isScrollbarFadingEnabled = false
} else {
etQuickToot.inputType = InputType.TYPE_CLASS_TEXT
etQuickToot.imeOptions = EditorInfo.IME_ACTION_SEND
etQuickToot.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEND) {
btnQuickToot.performClick()
return@OnEditorActionListener true
}
false
})
// 最後に指定する必要がある?
etQuickToot.maxLines = 1
}
completionHelper.attachEditText(
llFormRoot,
etQuickToot,
true,
object : CompletionHelper.Callback2 {
override fun onTextUpdate() {}
override fun canOpenPopup(): Boolean {
return !drawer.isDrawerOpen(GravityCompat.START)
}
})
showQuickTootVisibility()
}
fun ActMain.showQuickTootVisibility() {
btnQuickTootMenu.imageResource =
when (val resId = Styler.getVisibilityIconId(false, quickTootVisibility)) {

View File

@ -1,19 +1,183 @@
package jp.juggler.subwaytooter
import android.content.res.ColorStateList
import android.graphics.Typeface
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.view.ListDivider
import jp.juggler.subwaytooter.view.TabletColumnDivider
import jp.juggler.util.attrColor
import jp.juggler.util.getAdaptiveRippleDrawableRound
import jp.juggler.util.notZero
import jp.juggler.util.*
import org.jetbrains.anko.backgroundDrawable
import java.util.*
private val log = LogCategory("ActMainStyle")
// initUIから呼ばれる
fun ActMain.reloadFonts() {
var sv = PrefS.spTimelineFont(pref)
if (sv.isNotEmpty()) {
try {
ActMain.timelineFont = Typeface.createFromFile(sv)
} catch (ex: Throwable) {
log.trace(ex)
}
}
sv = PrefS.spTimelineFontBold(pref)
if (sv.isNotEmpty()) {
try {
ActMain.timeline_font_bold = Typeface.createFromFile(sv)
} catch (ex: Throwable) {
log.trace(ex)
}
} else {
try {
ActMain.timeline_font_bold = Typeface.create(ActMain.timelineFont, Typeface.BOLD)
} catch (ex: Throwable) {
log.trace(ex)
}
}
}
fun ActMain.parseIconSize(stringPref: StringPref, minDp: Float = 1f): Int {
var iconSizeDp = stringPref.defVal.toFloat()
try {
val sv = stringPref(pref)
val fv = if (sv.isEmpty()) Float.NaN else sv.toFloat()
if (fv.isFinite() && fv >= minDp) {
iconSizeDp = fv
}
} catch (ex: Throwable) {
log.trace(ex)
}
return (0.5f + iconSizeDp * density).toInt()
}
// initUIから呼ばれる
fun ActMain.reloadIconSize() {
avatarIconSize = parseIconSize(PrefS.spAvatarIconSize)
notificationTlIconSize = parseIconSize(PrefS.spNotificationTlIconSize)
ActMain.boostButtonSize = parseIconSize(PrefS.spBoostButtonSize)
ActMain.replyIconSize = parseIconSize(PrefS.spReplyIconSize)
ActMain.headerIconSize = parseIconSize(PrefS.spHeaderIconSize)
ActMain.stripIconSize = parseIconSize(PrefS.spStripIconSize)
ActMain.screenBottomPadding = parseIconSize(PrefS.spScreenBottomPadding, minDp = 0f)
}
// initUIから呼ばれる
fun ActMain.reloadRoundRatio() {
var roundRatio = 33f
try {
if (PrefB.bpDontRound(pref)) {
roundRatio = 0f
} else {
val sv = PrefS.spRoundRatio(pref)
if (sv.isNotEmpty()) {
val fv = sv.toFloat()
if (fv.isFinite()) {
roundRatio = fv
}
}
}
} catch (ex: Throwable) {
log.trace(ex)
}
Styler.round_ratio = clipRange(0f, 1f, roundRatio / 100f) * 0.5f
}
// initUI から呼ばれる
fun ActMain.reloadBoostAlpha() {
var boostAlpha = 0.8f
try {
val f = (PrefS.spBoostAlpha.toInt(pref).toFloat() + 0.5f) / 100f
boostAlpha = when {
f >= 1f -> 1f
f < 0f -> 0.66f
else -> f
}
} catch (ex: Throwable) {
log.trace(ex)
}
Styler.boostAlpha = boostAlpha
}
fun ActMain.reloadMediaHeight() {
var mediaThumbHeightDp = 64
val sv = PrefS.spMediaThumbHeight(pref)
if (sv.isNotEmpty()) {
try {
val iv = Integer.parseInt(sv)
if (iv >= 32) mediaThumbHeightDp = iv
} catch (ex: Throwable) {
log.trace(ex)
}
}
appState.mediaThumbHeight = (0.5f + mediaThumbHeightDp * density).toInt()
}
fun ActMain.loadColumnMin(density: Float): Int {
var x = ActMain.COLUMN_WIDTH_MIN_DP.toFloat()
val sv = PrefS.spColumnWidth(pref)
if (sv.isNotEmpty()) {
try {
val fv = sv.toFloat()
if (fv.isFinite() && fv >= 100f) {
x = fv
}
} catch (ex: Throwable) {
log.trace(ex)
}
}
return (0.5f + x * density).toInt()
}
fun ActMain.justifyWindowContentPortrait() {
when (PrefI.ipJustifyWindowContentPortrait(pref)) {
PrefI.JWCP_START -> {
val iconW = (ActMain.stripIconSize * 1.5f + 0.5f).toInt()
val padding = resources.displayMetrics.widthPixels / 2 - iconW
fun ViewGroup.addViewBeforeLast(v: View) = addView(v, childCount - 1)
(svColumnStrip.parent as LinearLayout).addViewBeforeLast(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
llQuickTootBar.addViewBeforeLast(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
}
PrefI.JWCP_END -> {
val iconW = (ActMain.stripIconSize * 1.5f + 0.5f).toInt()
val borderWidth = (1f * density + 0.5f).toInt()
val padding = resources.displayMetrics.widthPixels / 2 - iconW - borderWidth
fun ViewGroup.addViewAfterFirst(v: View) = addView(v, 1)
(svColumnStrip.parent as LinearLayout).addViewAfterFirst(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
llQuickTootBar.addViewAfterFirst(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
}
}
}
//////////////////////////////////////////////////////
// onStart時に呼ばれる
fun ActMain.reloadTimeZone(){
fun ActMain.reloadTimeZone() {
try {
var tz = TimeZone.getDefault()
val tzId = PrefS.spTimeZone(pref)
@ -22,13 +186,13 @@ fun ActMain.reloadTimeZone(){
}
TootStatus.date_format.timeZone = tz
} catch (ex: Throwable) {
ActMain.log.e(ex, "getTimeZone failed.")
log.e(ex, "getTimeZone failed.")
}
}
// onStart時に呼ばれる
// カラーカスタマイズを読み直す
fun ActMain.reloadColors(){
fun ActMain.reloadColors() {
ListDivider.color = PrefI.ipListDividerColor(pref)
TabletColumnDivider.color = PrefI.ipListDividerColor(pref)
ItemViewHolder.toot_color_unlisted = PrefI.ipTootColorUnlisted(pref)

View File

@ -30,6 +30,7 @@ import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.util.*
import kotlinx.coroutines.Job
import okhttp3.Request
import okhttp3.internal.closeQuietly
import ru.gildor.coroutines.okhttp.await
import java.lang.ref.WeakReference
import java.util.concurrent.ConcurrentHashMap
@ -40,7 +41,6 @@ class ActPost : AppCompatActivity(),
MyClickableSpanHandler, AttachmentPicker.Callback {
companion object {
internal val log = LogCategory("ActPost")
var refActPost: WeakReference<ActPost>? = null
@ -69,8 +69,8 @@ class ActPost : AppCompatActivity(),
/////////////////////////////////////////////////
fun createIntent(
activity: Activity,
accountDbId: Long,
multiWindowMode: Boolean,
@ -127,9 +127,12 @@ class ActPost : AppCompatActivity(),
val request = Request.Builder().url(url).build()
val call = App1.ok_http_client.newCall(request)
val response = call.await()
if (response.isSuccessful) return true
log.e(TootApiClient.formatResponse(response, "check_exist failed."))
try {
if (response.isSuccessful) return true
log.e(TootApiClient.formatResponse(response, "check_exist failed."))
} finally {
response.closeQuietly()
}
} catch (ex: Throwable) {
log.trace(ex)
}
@ -165,7 +168,7 @@ class ActPost : AppCompatActivity(),
lateinit var cbQuote: CheckBox
lateinit var spEnquete: Spinner
lateinit var spPollType: Spinner
lateinit var llEnquete: View
lateinit var etChoices: List<MyEditText>
@ -197,8 +200,6 @@ class ActPost : AppCompatActivity(),
///////////////////////////////////////////////////
var states = ActPostStates()
internal var account: SavedAccount? = null
@ -220,18 +221,6 @@ class ActPost : AppCompatActivity(),
var paThumbnailTarget: PostAttachment? = null
val textWatcher: TextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
updateTextCount()
}
}
val scrollListener: ViewTreeObserver.OnScrollChangedListener =
ViewTreeObserver.OnScrollChangedListener { completionHelper.onScrollChanged() }
@ -292,24 +281,65 @@ class ActPost : AppCompatActivity(),
////////////////////////////////////////////////////////////////
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return when {
super.onKeyDown(keyCode, event) -> true
event == null -> false
else -> event.isCtrlPressed
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isMultiWindowPost) ActMain.refActMain?.get()?.closeList?.add(WeakReference(this))
App1.setActivityTheme(this, noActionBar = true)
appState = App1.getAppState(this)
handler = appState.handler
pref = appState.pref
attachmentUploader = AttachmentUploader(this, handler)
attachmentPicker = AttachmentPicker(this, this)
density = resources.displayMetrics.density
arMushroom.register(this, log)
initUI()
when (savedInstanceState) {
null -> updateText(intent, confirmed = true, saveDraft = false)
else -> restoreState(savedInstanceState)
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
val rv = super.onKeyUp(keyCode, event)
if (event?.isCtrlPressed == true) {
ActMain.log.d("onKeyUp code=$keyCode rv=$rv")
when (keyCode) {
KeyEvent.KEYCODE_T -> btnPost.performClick()
}
return true
}
return rv
override fun onDestroy() {
completionHelper.onDestroy()
attachmentUploader.onActivityDestroy()
super.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
saveState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
showContentWarningEnabled()
showMediaAttachment()
showVisibility()
updateTextCount()
showReplyTo()
showPoll()
showQuotedRenote()
}
override fun onResume() {
super.onResume()
refActPost = WeakReference(this)
}
override fun onPause() {
super.onPause()
// 編集中にホーム画面を押したり他アプリに移動する場合は下書きを保存する
// やや過剰な気がするが、自アプリに戻ってくるときにランチャーからアイコンタップされると
// メイン画面より上にあるアクティビティはすべて消されてしまうので
// このタイミングで保存するしかない
if (!isPostComplete) saveDraft()
}
override fun onBackPressed() {
saveDraft()
super.onBackPressed()
}
override fun onClick(v: View) {
@ -335,93 +365,31 @@ class ActPost : AppCompatActivity(),
}
}
// unused? for REQUEST_CODE_ATTACHMENT
// fun handleAttachmentResult(ar: ActivityResult?) {
// if (ar?.resultCode == RESULT_OK) {
// ar.data?.handleGetContentResult(contentResolver)?.let { checkAttachments(it) }
// }
// }
override fun onBackPressed() {
saveDraft()
super.onBackPressed()
}
override fun onResume() {
super.onResume()
refActPost = WeakReference(this)
}
override fun onPause() {
super.onPause()
// 編集中にホーム画面を押したり他アプリに移動する場合は下書きを保存する
// やや過剰な気がするが、自アプリに戻ってくるときにランチャーからアイコンタップされると
// メイン画面より上にあるアクティビティはすべて消されてしまうので
// このタイミングで保存するしかない
if (!isPostComplete) {
saveDraft()
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return when {
super.onKeyDown(keyCode, event) -> true
event == null -> false
else -> event.isCtrlPressed
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isMultiWindowPost) ActMain.refActMain?.get()?.closeList?.add(WeakReference(this))
App1.setActivityTheme(this, noActionBar = true)
appState = App1.getAppState(this)
handler = appState.handler
pref = appState.pref
attachmentUploader = AttachmentUploader(this, handler)
attachmentPicker = AttachmentPicker(this, this)
arMushroom.register(this, log)
initUI()
if (savedInstanceState != null) {
restoreState(savedInstanceState)
} else {
updateText(intent, confirmed = true, saveDraft = false)
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
val rv = super.onKeyUp(keyCode, event)
if (event?.isCtrlPressed == true) {
ActMain.log.d("onKeyUp code=$keyCode rv=$rv")
when (keyCode) {
KeyEvent.KEYCODE_T -> btnPost.performClick()
}
return true
}
}
override fun onDestroy() {
completionHelper.onDestroy()
attachmentUploader.onActivityDestroy()
super.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
saveState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
showContentWarningEnabled()
showMediaAttachment()
showVisibility()
updateTextCount()
showReplyTo()
showPoll()
showQuotedRenote()
return rv
}
override fun onMyClickableSpanClicked(viewClicked: View, span: MyClickableSpan) {
openBrowser(span.linkInfo.url)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
attachmentPicker.onRequestPermissionsResult(requestCode, permissions, grantResults)
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
@ -439,12 +407,10 @@ class ActPost : AppCompatActivity(),
}
fun initUI() {
density = resources.displayMetrics.density
setContentView(R.layout.act_post)
App1.initEdgeToEdge(this)
if (PrefB.bpPostButtonBarTop(this)) {
if (PrefB.bpPostButtonBarTop(pref)) {
val bar = findViewById<View>(R.id.llFooterBar)
val parent = bar.parent as ViewGroup
parent.removeView(bar)
@ -480,7 +446,7 @@ class ActPost : AppCompatActivity(),
cbQuote = findViewById(R.id.cbQuote)
spEnquete = findViewById<Spinner>(R.id.spEnquete).apply {
spPollType = findViewById<Spinner>(R.id.spEnquete).apply {
this.adapter = ArrayAdapter(
this@ActPost,
android.R.layout.simple_spinner_item,
@ -499,17 +465,13 @@ class ActPost : AppCompatActivity(),
updateTextCount()
}
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long,
) {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
showPoll()
updateTextCount()
}
}
}
llEnquete = findViewById(R.id.llEnquete)
llExpire = findViewById(R.id.llExpire)
cbHideTotals = findViewById(R.id.cbHideTotals)
@ -542,7 +504,6 @@ class ActPost : AppCompatActivity(),
ibSchedule = findViewById(R.id.ibSchedule)
ibScheduleReset = findViewById(R.id.ibScheduleReset)
arrayOf(
ibSchedule,
ibScheduleReset,
@ -559,9 +520,7 @@ class ActPost : AppCompatActivity(),
ivMedia.forEach { it.setOnClickListener(this) }
cbContentWarning.setOnCheckedChangeListener { _, _ ->
showContentWarningEnabled()
}
cbContentWarning.setOnCheckedChangeListener { _, _ -> showContentWarningEnabled() }
completionHelper = CompletionHelper(this, pref, appState.handler)
completionHelper.attachEditText(formRoot, etContent, false, object : CompletionHelper.Callback2 {
@ -569,12 +528,23 @@ class ActPost : AppCompatActivity(),
updateTextCount()
}
override fun canOpenPopup(): Boolean {
return true
}
override fun canOpenPopup(): Boolean = true
})
val textWatcher: TextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
updateTextCount()
}
}
etContentWarning.addTextChangedListener(textWatcher)
for (et in etChoices) {
et.addTextChangedListener(textWatcher)
}

View File

@ -38,7 +38,6 @@ fun ActPost.decodeAttachments(sv: String) {
}
}
fun ActPost.showMediaAttachment() {
if (isFinishing) return
llAttachment.vg(attachmentList.isNotEmpty())

View File

@ -74,7 +74,7 @@ fun ActPost.updateTextCount() {
}
}
when (spEnquete.selectedItemPosition) {
when (spPollType.selectedItemPosition) {
1 -> checkEnqueteLength()
2 -> {

View File

@ -102,7 +102,7 @@ fun ActPost.resetText() {
attachmentList.clear()
cbQuote.isChecked = false
etContent.setText("")
spEnquete.setSelection(0, false)
spPollType.setSelection(0, false)
etChoices.forEach { it.setText("") }
accountList = SavedAccount.loadAccountList(this)
SavedAccount.sort(accountList)
@ -132,7 +132,6 @@ fun ActPost.afterUpdateText() {
updateTextCount()
}
// 初期化時と投稿完了時とリセット確認後に呼ばれる
fun ActPost.updateText(
intent: Intent,
@ -294,7 +293,7 @@ fun ActPost.performPost() {
var pollExpireSeconds = 0
var pollHideTotals = false
var pollMultipleChoice = false
when (spEnquete.selectedItemPosition) {
when (spPollType.selectedItemPosition) {
1 -> {
pollType = TootPollsType.Mastodon
pollItems = pollChoiceList()

View File

@ -5,7 +5,7 @@ import jp.juggler.util.notEmpty
import jp.juggler.util.vg
fun ActPost.showPoll() {
val i = spEnquete.selectedItemPosition
val i = spPollType.selectedItemPosition
llEnquete.vg(i != 0)
llExpire.vg(i == 1)
cbHideTotals.vg(i == 1)
@ -13,9 +13,9 @@ fun ActPost.showPoll() {
}
// 投票が有効で何か入力済みなら真
fun ActPost.hasPoll():Boolean{
if( spEnquete.selectedItemPosition <= 0) return false
return etChoices.any{ it.text.toString().isNotBlank()}
fun ActPost.hasPoll(): Boolean {
if (spPollType.selectedItemPosition <= 0) return false
return etChoices.any { it.text.toString().isNotBlank() }
}
fun ActPost.pollChoiceList() = ArrayList<String>().apply {
@ -30,4 +30,3 @@ fun ActPost.pollExpireSeconds(): Int {
val m = etExpireMinutes.text.toString().trim().toDoubleOrNull().finiteOrZero()
return (d * 86400.0 + h * 3600.0 + m * 60.0).toInt()
}

View File

@ -38,13 +38,12 @@ private const val DRAFT_POLL_EXPIRE_MINUTE = "poll_expire_minute"
private const val DRAFT_ENQUETE_ITEMS = "enquete_items"
private const val DRAFT_QUOTE = "quotedRenote" // 歴史的な理由で名前がMisskey用になってる
fun ActPost.saveDraft() {
val content = etContent.text.toString()
val contentWarning =
if (cbContentWarning.isChecked) etContentWarning.text.toString() else ""
val isEnquete = spEnquete.selectedItemPosition > 0
val isEnquete = spPollType.selectedItemPosition > 0
val strChoice = arrayOf(
if (isEnquete) etChoices[0].text.toString() else "",
@ -88,7 +87,7 @@ fun ActPost.saveDraft() {
// deprecated. but still used in old draft.
// json.put(DRAFT_IS_ENQUETE, isEnquete)
json[DRAFT_POLL_TYPE] = spEnquete.selectedItemPosition.toPollTypeString()
json[DRAFT_POLL_TYPE] = spPollType.selectedItemPosition.toPollTypeString()
json[DRAFT_POLL_MULTIPLE] = cbMultipleChoice.isChecked
json[DRAFT_POLL_HIDE_TOTALS] = cbHideTotals.isChecked
@ -236,11 +235,11 @@ fun ActPost.restoreDraft(draft: JsonObject) {
val sv = draft.string(DRAFT_POLL_TYPE)
if (sv != null) {
spEnquete.setSelection(sv.toPollTypeIndex())
spPollType.setSelection(sv.toPollTypeIndex())
} else {
// old draft
val bv = draft.optBoolean(DRAFT_IS_ENQUETE, false)
spEnquete.setSelection(if (bv) 2 else 0)
spPollType.setSelection(if (bv) 2 else 0)
}
cbMultipleChoice.isChecked = draft.optBoolean(DRAFT_POLL_MULTIPLE)
@ -369,7 +368,7 @@ fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String)
}
else -> {
spEnquete.setSelection(
spPollType.setSelection(
if (srcEnquete.pollType == TootPollsType.FriendsNico) {
2
} else {

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter
import android.annotation.SuppressLint
import android.view.View
import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.api.TootParser

View File

@ -1,9 +1,7 @@
package jp.juggler.subwaytooter
import android.view.View
import jp.juggler.subwaytooter.api.entity.TootVisibility
fun ActPost.showContentWarningEnabled() {
etContentWarning.visibility = if (cbContentWarning.isChecked) View.VISIBLE else View.GONE
}

View File

@ -611,4 +611,4 @@ class App1 : Application() {
}
}
val kJson = kotlinx.serialization.json.Json{ ignoreUnknownKeys = true }
val kJson = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }

View File

@ -4,11 +4,10 @@ import android.content.Context
import android.os.Environment
import android.util.LruCache
import androidx.annotation.RawRes
import jp.juggler.subwaytooter.api.ApiPath.READ_LIMIT
import jp.juggler.subwaytooter.Column.Companion.log
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.ApiPath.READ_LIMIT
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
import java.io.File
import java.util.*
@ -826,94 +825,6 @@ fun Column.makeProfileStatusesUrl(profileId: EntityId?): String {
return path
}
val misskeyArrayFinderUsers = { it: JsonObject ->
it.jsonArray("users")
}
////////////////////////////////////////////////////////////////////////////////
// account list parser
val nullArrayFinder: (JsonObject) -> JsonArray? =
{ null }
val defaultAccountListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> parser.accountList(jsonArray) }
private fun misskeyUnwrapRelationAccount(parser: TootParser, srcList: JsonArray, key: String) =
srcList.objectList().mapNotNull {
when (val relationId = EntityId.mayNull(it.string("id"))) {
null -> null
else -> TootAccountRef.mayNull(parser, parser.account(it.jsonObject(key)))
?.apply { _orderId = relationId }
}
}
val misskey11FollowingParser: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "followee") }
val misskey11FollowersParser: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") }
val misskeyCustomParserFollowRequest: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") }
val misskeyCustomParserMutes: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "mutee") }
val misskeyCustomParserBlocks: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "blockee") }
////////////////////////////////////////////////////////////////////////////////
// status list parser
val defaultStatusListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootStatus> =
{ parser, jsonArray -> parser.statusList(jsonArray) }
val misskeyCustomParserFavorites: (TootParser, JsonArray) -> List<TootStatus> =
{ parser, jsonArray ->
jsonArray.objectList().mapNotNull {
when (val relationId = EntityId.mayNull(it.string("id"))) {
null -> null
else -> parser.status(it.jsonObject("note"))?.apply {
favourited = true
_orderId = relationId
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// notification list parser
val defaultNotificationListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootNotification> =
{ parser, jsonArray -> parser.notificationList(jsonArray) }
val defaultDomainBlockListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootDomainBlock> =
{ _, jsonArray -> TootDomainBlock.parseList(jsonArray) }
val defaultReportListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootReport> =
{ _, jsonArray -> parseList(::TootReport, jsonArray) }
val defaultConversationSummaryListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootConversationSummary> =
{ parser, jsonArray -> parseList(::TootConversationSummary, parser, jsonArray) }
///////////////////////////////////////////////////////////////////////
val mastodonFollowSuggestion2ListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray ->
TootAccountRef.wrapList(parser,
jsonArray.objectList().mapNotNull {
parser.account(it.jsonObject("account"))?.also { a ->
SuggestionSource.set(
(parser.linkHelper as? SavedAccount)?.db_id,
a.acct,
it.string("source")
)
}
}
)
}
///////////////////////////////////////////////////////////////////////
private const val DIR_BACKGROUND_IMAGE = "columnBackground"

View File

@ -7,6 +7,7 @@ import jp.juggler.subwaytooter.notification.PollingWorker
import jp.juggler.subwaytooter.streaming.StreamManager
import jp.juggler.subwaytooter.streaming.StreamStatus
import jp.juggler.subwaytooter.util.ScrollPosition
import jp.juggler.util.notEmpty
import jp.juggler.util.runOnMainLooper
import kotlin.math.max
@ -73,37 +74,22 @@ fun Column.mergeStreamingMessage() {
lastShowStreamData.set(now)
// read items while queue is not empty
val tmpList = ArrayList<TimelineItem>()
while (true) tmpList.add(streamDataQueue.poll() ?: break)
if (tmpList.isEmpty()) return
.apply { while (true) add(streamDataQueue.poll() ?: break) }.notEmpty()
?: return
// キューから読めた件数が0の場合を除き、少し後に再処理させることでマージ漏れを防ぐ
handler.postDelayed(procMergeStreamingMessage, 333L)
// ストリーミングされるデータは全てID順に並んでいるはず
// orderId順ソートを徹底する
tmpList.sortByDescending { it.getOrderId() }
val listNew = duplicateMap.filterDuplicate(tmpList)
if (listNew.isEmpty()) return
// 既にカラム中にあるデータは除去する
val listNew = duplicateMap.filterDuplicate(tmpList).notEmpty() ?: return
for (item in listNew) {
if (enableSpeech && item is TootStatus) {
appState.addSpeech(item.reblog ?: item)
}
}
// 通知カラムならストリーミング経由で届いたデータを通知ワーカーに伝達する
if (isNotificationColumn) {
val list = ArrayList<TootNotification>()
for (o in listNew) {
if (o is TootNotification) {
list.add(o)
}
}
if (list.isNotEmpty()) {
PollingWorker.injectData(context, accessInfo, list)
}
}
sendToSpeech(listNew)
injectToPollingWorker(listNew)
// 最新のIDをsince_idとして覚える(ソートはしない)
var newIdMax: EntityId? = null
@ -154,17 +140,7 @@ fun Column.mergeStreamingMessage() {
// 画面復帰時の自動リフレッシュではギャップが残る可能性がある
if (bPutGap) {
bPutGap = false
try {
if (listData.size > 0 && newIdMin != null) {
val since = listData[0].getOrderId()
if (newIdMin > since) {
val gap = TootGap(newIdMin, since)
listNew.add(gap)
}
}
} catch (ex: Throwable) {
Column.log.e(ex, "can't put gap.")
}
addGapAfterStreaming(listNew, newIdMin)
}
val changeList = ArrayList<AdapterChange>()
@ -192,8 +168,49 @@ fun Column.mergeStreamingMessage() {
listData.addAll(0, listNew)
fireShowContent(reason = "mergeStreamingMessage", changeList = changeList)
scrollAfterStreaming(added, holderSp, restoreIdx, restoreY)
updateMisskeyCapture()
}
if (holder != null) {
// 通知カラムならストリーミング経由で届いたデータを通知ワーカーに伝達する
private fun Column.injectToPollingWorker(listNew: ArrayList<TimelineItem>) {
if (!isNotificationColumn) return
listNew.mapNotNull { it as? TootNotification }.notEmpty()
?.let { PollingWorker.injectData(context, accessInfo, it) }
}
private fun Column.sendToSpeech(listNew: ArrayList<TimelineItem>) {
if (!enableSpeech) return
listNew.mapNotNull { it as? TootStatus }
.forEach { appState.addSpeech(it.reblog ?: it) }
}
private fun Column.addGapAfterStreaming(listNew: ArrayList<TimelineItem>, newIdMin: EntityId?) {
try {
if (listData.size > 0 && newIdMin != null) {
val since = listData[0].getOrderId()
if (newIdMin > since) {
val gap = TootGap(newIdMin, since)
listNew.add(gap)
}
}
} catch (ex: Throwable) {
Column.log.e(ex, "can't put gap.")
}
}
private fun Column.scrollAfterStreaming(added: Int, holderSp: ScrollPosition?, restoreIdx: Int, restoreY: Int) {
val holder = viewHolder
if (holder == null) {
val scrollSave = this.scrollSave
when {
// スクロール位置が先頭なら先頭のまま
scrollSave == null || scrollSave.isHead -> Unit
// 現在の要素が表示され続けるようにしたい
else -> scrollSave.adapterIndex += added
}
} else {
when {
holderSp == null -> {
// スクロール位置が先頭なら先頭にする
@ -218,19 +235,7 @@ fun Column.mergeStreamingMessage() {
holder.setListItemTop(restoreIdx + added, restoreY)
}
}
} else {
val scrollSave = this.scrollSave
when {
// スクロール位置が先頭なら先頭のまま
scrollSave == null || scrollSave.isHead -> {
}
// 現在の要素が表示され続けるようにしたい
else -> scrollSave.adapterIndex += added
}
}
updateMisskeyCapture()
}
fun Column.runOnMainLooperForStreamingEvent(proc: () -> Unit) {

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter
import android.os.SystemClock
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.notification.PollingWorker
import jp.juggler.util.*
import java.lang.StringBuilder

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter
import android.os.SystemClock
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.notification.PollingWorker
import jp.juggler.subwaytooter.util.OpenSticker
import jp.juggler.util.*

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter
import android.os.SystemClock
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.util.ScrollPosition
import jp.juggler.util.*

View File

@ -6,6 +6,7 @@ import jp.juggler.subwaytooter.api.ApiPath
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.search.MspHelper.loadingMSP
import jp.juggler.subwaytooter.search.MspHelper.refreshMSP
import jp.juggler.subwaytooter.search.NotestockHelper.loadingNotestock

View File

@ -635,6 +635,7 @@ internal class DlgContextMenu(
return true
}
@Suppress("ComplexMethod")
private fun ActMain.onClickUser(v: View, pos: Int, who: TootAccount, whoRef: TootAccountRef): Boolean {
when (v.id) {
R.id.btnReportUser -> userReportForm(accessInfo, who)

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.view.View
import android.widget.ImageView
@ -51,34 +52,22 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
llStatus.visibility = View.VISIBLE
if (status.conversation_main) {
val conversationMainBgColor =
PrefI.ipConversationMainTootBgColor(activity.pref).notZero()
?: (activity.attrColor(R.attr.colorImageButtonAccent) and 0xffffff) or 0x20000000
this.viewRoot.setBackgroundColor(conversationMainBgColor)
PrefI.ipConversationMainTootBgColor(activity.pref).notZero()
?: (activity.attrColor(R.attr.colorImageButtonAccent) and 0xffffff) or 0x20000000
} else {
val c = colorBg.notZero()
?: when (status.bookmarked) {
true -> PrefI.ipEventBgColorBookmark(App1.pref)
false -> 0
}.notZero()
?: when (status.getBackgroundColorType(accessInfo)) {
TootVisibility.UnlistedHome -> ItemViewHolder.toot_color_unlisted
TootVisibility.PrivateFollowers -> ItemViewHolder.toot_color_follower
TootVisibility.DirectSpecified -> ItemViewHolder.toot_color_direct_user
TootVisibility.DirectPrivate -> ItemViewHolder.toot_color_direct_me
// TODO add color setting for limited?
TootVisibility.Limited -> ItemViewHolder.toot_color_follower
else -> 0
}
if (c != 0) {
this.viewRoot.backgroundColor = c
}
}
colorBg.notZero() ?: when (status.bookmarked) {
true -> PrefI.ipEventBgColorBookmark(App1.pref)
false -> 0
}.notZero() ?: when (status.getBackgroundColorType(accessInfo)) {
TootVisibility.UnlistedHome -> ItemViewHolder.toot_color_unlisted
TootVisibility.PrivateFollowers -> ItemViewHolder.toot_color_follower
TootVisibility.DirectSpecified -> ItemViewHolder.toot_color_direct_user
TootVisibility.DirectPrivate -> ItemViewHolder.toot_color_direct_me
// TODO add color setting for limited?
TootVisibility.Limited -> ItemViewHolder.toot_color_follower
else -> 0
}.notZero()
}?.let { viewRoot.backgroundColor = it }
showStatusTime(activity, tvTime, who = status.account, status = status)
@ -88,11 +77,6 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
setAcct(tvAcct, accessInfo, who)
// if(who == null) {
// tvName.text = "?"
// name_invalidator.register(null)
// ivThumbnail.setImageUrl(activity.pref, 16f, null, null)
// } else {
tvName.text = whoRef.decoded_display_name
nameInvalidator.register(whoRef.decoded_display_name)
ivAvatar.setImageUrl(
@ -101,34 +85,23 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
accessInfo.supplyBaseUrl(who.avatar_static),
accessInfo.supplyBaseUrl(who.avatar)
)
// }
showOpenSticker(who)
var content = status.decoded_content
// ニコフレのアンケートの表示
val enquete = status.enquete
when {
enquete == null -> {
}
enquete.pollType == TootPollsType.FriendsNico && enquete.type != TootPolls.TYPE_ENQUETE -> {
// フレニコの投票の結果表示は普通にテキストを表示するだけでよい
}
else -> {
// アンケートの本文を上書きする
val question = enquete.decoded_question
if (question.isNotBlank()) content = question
showEnqueteItems(status, enquete)
}
val modifiedContent = if (status.time_deleted_at > 0L) {
SpannableStringBuilder()
.append('(')
.append(
activity.getString(
R.string.deleted_at,
TootStatus.formatTime(activity, status.time_deleted_at, true)
)
)
.append(')')
} else {
showPoll(status) ?: status.decoded_content
}
showPreviewCard(status)
// if( status.decoded_tags == null ){
// tvTags.setVisibility( View.GONE );
// }else{
@ -143,27 +116,41 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
tvMentions.text = status.decoded_mentions
}
if (status.time_deleted_at > 0L) {
val s = SpannableStringBuilder()
.append('(')
.append(
activity.getString(
R.string.deleted_at,
TootStatus.formatTime(activity, status.time_deleted_at, true)
)
)
.append(')')
content = s
}
tvContent.text = modifiedContent
contentInvalidator.register(modifiedContent)
tvContent.text = content
contentInvalidator.register(content)
activity.checkAutoCW(status, content)
activity.checkAutoCW(status, modifiedContent)
val r = status.auto_cw
tvContent.minLines = r?.originalLineCount ?: -1
showPreviewCard(status)
showSpoilerTextAndContent(status)
showAttachments(status)
makeReactionsView(status)
buttonsForStatus?.bind(status, (item as? TootNotification))
showApplicationAndLanguage(status)
}
// 投票の表示
// returns modified decoded_content or null
private fun ItemViewHolder.showPoll(status: TootStatus): Spannable? {
val enquete = status.enquete
return when {
enquete == null -> null
// フレニコの投票の結果表示は普通にテキストを表示するだけでよい
enquete.pollType == TootPollsType.FriendsNico && enquete.type != TootPolls.TYPE_ENQUETE -> null
else -> {
showEnqueteItems(status, enquete)
// アンケートの本文を使ってcontentを上書きする
enquete.decoded_question.notBlank()
}
}
}
private fun ItemViewHolder.showSpoilerTextAndContent(status: TootStatus) {
val r = status.auto_cw
val decodedSpoilerText = status.decoded_spoiler_text
when {
decodedSpoilerText.isNotEmpty() -> {
@ -172,7 +159,7 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
tvContentWarning.text = status.decoded_spoiler_text
spoilerInvalidator.register(status.decoded_spoiler_text)
val cwShown = ContentWarning.isShown(status, accessInfo.expand_cw)
showContent(cwShown)
setContentVisibility(cwShown)
}
r?.decodedSpoilerText != null -> {
@ -181,7 +168,7 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
tvContentWarning.text = r.decodedSpoilerText
spoilerInvalidator.register(r.decodedSpoilerText)
val cwShown = ContentWarning.isShown(status, accessInfo.expand_cw)
showContent(cwShown)
setContentVisibility(cwShown)
}
else -> {
@ -190,55 +177,25 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
llContents.visibility = View.VISIBLE
}
}
}
val mediaAttachments = status.media_attachments
if (mediaAttachments == null || mediaAttachments.isEmpty()) {
flMedia.visibility = View.GONE
llMedia.visibility = View.GONE
btnShowMedia.visibility = View.GONE
} else {
flMedia.visibility = View.VISIBLE
// hide sensitive media
val defaultShown = when {
column.hideMediaDefault -> false
accessInfo.dont_hide_nsfw -> true
else -> !status.sensitive
private fun ItemViewHolder.setContentVisibility(shown: Boolean) {
llContents.visibility = if (shown) View.VISIBLE else View.GONE
btnContentWarning.setText(if (shown) R.string.hide else R.string.show)
statusShowing?.let { status ->
val r = status.auto_cw
tvContent.minLines = r?.originalLineCount ?: -1
if (r?.decodedSpoilerText != null) {
// 自動CWの場合はContentWarningのテキストを切り替える
tvContentWarning.text =
if (shown) activity.getString(R.string.auto_cw_prefix) else r.decodedSpoilerText
}
val isShown = MediaShown.isShown(status, defaultShown)
btnShowMedia.visibility = if (!isShown) View.VISIBLE else View.GONE
llMedia.visibility = if (!isShown) View.GONE else View.VISIBLE
val sb = StringBuilder()
setMedia(mediaAttachments, sb, ivMedia1, 0)
setMedia(mediaAttachments, sb, ivMedia2, 1)
setMedia(mediaAttachments, sb, ivMedia3, 2)
setMedia(mediaAttachments, sb, ivMedia4, 3)
val m0 =
if (mediaAttachments.isEmpty()) null else mediaAttachments[0] as? TootAttachment
btnShowMedia.blurhash = m0?.blurhash
if (sb.isNotEmpty()) {
tvMediaDescription.visibility = View.VISIBLE
tvMediaDescription.text = sb
}
setIconDrawableId(
activity,
btnHideMedia,
R.drawable.ic_close,
color = contentColor,
alphaMultiplier = Styler.boostAlpha
)
}
}
makeReactionsView(status)
buttonsForStatus?.bind(status, (item as? TootNotification))
private fun ItemViewHolder.showApplicationAndLanguage(status: TootStatus) {
var sb: StringBuilder? = null
fun prepareSb(): StringBuilder =
sb?.append(", ") ?: StringBuilder().also { sb = it }
@ -259,7 +216,7 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
tvApplication.vg(sb != null)?.text = sb
}
fun ItemViewHolder.showOpenSticker(who: TootAccount) {
private fun ItemViewHolder.showOpenSticker(who: TootAccount) {
try {
if (!Column.showOpenSticker) return
@ -306,17 +263,47 @@ fun ItemViewHolder.showOpenSticker(who: TootAccount) {
}
}
fun ItemViewHolder.showContent(shown: Boolean) {
llContents.visibility = if (shown) View.VISIBLE else View.GONE
btnContentWarning.setText(if (shown) R.string.hide else R.string.show)
statusShowing?.let { status ->
val r = status.auto_cw
tvContent.minLines = r?.originalLineCount ?: -1
if (r?.decodedSpoilerText != null) {
// 自動CWの場合はContentWarningのテキストを切り替える
tvContentWarning.text =
if (shown) activity.getString(R.string.auto_cw_prefix) else r.decodedSpoilerText
private fun ItemViewHolder.showAttachments(status: TootStatus) {
val mediaAttachments = status.media_attachments
if (mediaAttachments == null || mediaAttachments.isEmpty()) {
flMedia.visibility = View.GONE
llMedia.visibility = View.GONE
btnShowMedia.visibility = View.GONE
} else {
flMedia.visibility = View.VISIBLE
// hide sensitive media
val defaultShown = when {
column.hideMediaDefault -> false
accessInfo.dont_hide_nsfw -> true
else -> !status.sensitive
}
val isShown = MediaShown.isShown(status, defaultShown)
btnShowMedia.visibility = if (!isShown) View.VISIBLE else View.GONE
llMedia.visibility = if (!isShown) View.GONE else View.VISIBLE
val sb = StringBuilder()
setMedia(mediaAttachments, sb, ivMedia1, 0)
setMedia(mediaAttachments, sb, ivMedia2, 1)
setMedia(mediaAttachments, sb, ivMedia3, 2)
setMedia(mediaAttachments, sb, ivMedia4, 3)
val m0 =
if (mediaAttachments.isEmpty()) null else mediaAttachments[0] as? TootAttachment
btnShowMedia.blurhash = m0?.blurhash
if (sb.isNotEmpty()) {
tvMediaDescription.visibility = View.VISIBLE
tvMediaDescription.text = sb
}
setIconDrawableId(
activity,
btnHideMedia,
R.drawable.ic_close,
color = contentColor,
alphaMultiplier = Styler.boostAlpha
)
}
}

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter.action
import android.content.Context
import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.api.*
@ -29,13 +30,17 @@ private class BoostImpl(
val visibility: TootVisibility? = null,
val callback: () -> Unit,
) {
val parser = TootParser(activity, accessInfo)
var resultStatus: TootStatus? = null
var resultUnrenoteId: EntityId? = null
// Mastodonは非公開トゥートをブーストできるのは本人だけ
val isPrivateToot = accessInfo.isMastodon &&
private val isPrivateToot = accessInfo.isMastodon &&
statusArg.visibility == TootVisibility.PrivateFollowers
var bConfirmed = false
private var bConfirmed = false
fun preCheck(): Boolean {
private fun preCheck(): Boolean {
// アカウントからステータスにブースト操作を行っているなら、何もしない
if (activity.appState.isBusyBoost(accessInfo, statusArg)) {
@ -52,7 +57,7 @@ private class BoostImpl(
return true
}
fun confirm(): Boolean {
private fun confirm(): Boolean {
if (bConfirmed) return true
DlgConfirm.open(
activity,
@ -88,178 +93,162 @@ private class BoostImpl(
return false
}
private suspend fun Context.syncStatus(client: TootApiClient) =
if (!crossAccountMode.isRemote) {
// 既に自タンスのステータスがある
statusArg
} else {
val (result, status) = client.syncStatus(accessInfo, statusArg)
when {
status == null -> errorApiResult(result)
status.reblogged -> errorApiResult(getString(R.string.already_boosted))
else -> status
}
}
// ブースト結果をUIに反映させる
private fun after(result: TootApiResult?, newStatus: TootStatus?, unrenoteId: EntityId?) {
result ?: return // cancelled.
when {
// Misskeyでunrenoteに成功した
unrenoteId != null -> {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にしない
val count = max(0, (statusArg.reblogs_count ?: 1) - 1)
for (column in activity.appState.columnList) {
column.findStatus(accessInfo.apDomain, statusArg.id) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = count
// 同アカウントならreblogged状態を変化させる
if (accessInfo == account && status.myRenoteId == unrenoteId) {
status.myRenoteId = null
status.reblogged = false
}
true
}
}
callback()
}
// 処理に成功した
newStatus != null -> {
// カウント数は遅延があるみたいなので、恣意的に表示を変更する
// ブーストカウント数を加工する
val oldCount = statusArg.reblogs_count
val newCount = newStatus.reblogs_count
if (oldCount != null && newCount != null) {
if (bSet && newStatus.reblogged && newCount <= oldCount) {
// 星をつけたのにカウントが上がらないのは違和感あるので、表示をいじる
newStatus.reblogs_count = oldCount + 1
} else if (!bSet && !newStatus.reblogged && newCount >= oldCount) {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはしない
newStatus.reblogs_count = if (oldCount < 1) 0 else oldCount - 1
}
}
for (column in activity.appState.columnList) {
column.findStatus(accessInfo.apDomain, newStatus.id) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = newStatus.reblogs_count
if (accessInfo == account) {
// 同アカウントならreblog状態を変化させる
when {
accessInfo.isMastodon ->
status.reblogged = newStatus.reblogged
bSet && status.myRenoteId == null -> {
status.myRenoteId = newStatus.myRenoteId
status.reblogged = true
}
// Misskey のunrenote時はここを通らない
}
}
true
}
}
callback()
}
else -> activity.showToast(true, result.error)
}
}
suspend fun boostApi(client: TootApiClient, targetStatus: TootStatus): TootApiResult? =
if (accessInfo.isMisskey) {
if (!bSet) {
val myRenoteId = targetStatus.myRenoteId ?: errorApiResult("missing renote id.")
client.request(
"/api/notes/delete",
accessInfo.putMisskeyApiToken().apply {
put("noteId", myRenoteId.toString())
put("renoteId", targetStatus.id.toString())
}.toPostRequestBuilder()
)?.also {
if (it.response?.code == 204) {
resultUnrenoteId = myRenoteId
}
}
} else {
client.request(
"/api/notes/create",
accessInfo.putMisskeyApiToken().apply {
put("renoteId", targetStatus.id.toString())
}.toPostRequestBuilder()
)?.also { result ->
val jsonObject = result.jsonObject
if (jsonObject != null) {
val outerStatus = parser.status(jsonObject.jsonObject("createdNote") ?: jsonObject)
val innerStatus = outerStatus?.reblog ?: outerStatus
if (outerStatus != null && innerStatus != null && outerStatus != innerStatus) {
innerStatus.myRenoteId = outerStatus.id
innerStatus.reblogged = true
}
// renoteそのものではなくrenoteされた元noteが欲しい
resultStatus = innerStatus
}
}
}
} else {
val b = JsonObject().apply {
if (visibility != null) put("visibility", visibility.strMastodon)
}.toPostRequestBuilder()
client.request(
"/api/v1/statuses/${targetStatus.id}/${if (bSet) "reblog" else "unreblog"}",
b
)?.also { result ->
// reblogはreblogを表すStatusを返す
// unreblogはreblogしたStatusを返す
val s = parser.status(result.jsonObject)
resultStatus = s?.reblog ?: s
}
}
fun run() {
if (!preCheck()) return
if (!confirm()) return
activity.appState.setBusyBoost(accessInfo, statusArg)
// ブースト表示を更新中にする
activity.appState.setBusyBoost(accessInfo, statusArg)
activity.showColumnMatchAccount(accessInfo)
// misskeyは非公開トゥートをブーストできないっぽい
launchMain {
var resultStatus: TootStatus? = null
var resultUnrenoteId: EntityId? = null
val result = activity.runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
val parser = TootParser(this, accessInfo)
val targetStatus = if (crossAccountMode.isRemote) {
val (result, status) = client.syncStatus(accessInfo, statusArg)
if (status == null) return@runApiTask result
if (status.reblogged) {
return@runApiTask TootApiResult(getString(R.string.already_boosted))
}
status
} else {
// 既に自タンスのステータスがある
statusArg
}
if (accessInfo.isMisskey) {
if (!bSet) {
val myRenoteId = targetStatus.myRenoteId
?: return@runApiTask TootApiResult("missing renote id.")
client.request(
"/api/notes/delete",
accessInfo.putMisskeyApiToken().apply {
put("noteId", myRenoteId.toString())
put("renoteId", targetStatus.id.toString())
}
.toPostRequestBuilder()
)
?.also {
if (it.response?.code == 204) {
resultUnrenoteId = myRenoteId
}
}
} else {
client.request(
"/api/notes/create",
accessInfo.putMisskeyApiToken().apply {
put("renoteId", targetStatus.id.toString())
}
.toPostRequestBuilder()
)
?.also { result ->
val jsonObject = result.jsonObject
if (jsonObject != null) {
val outerStatus = parser.status(
jsonObject.jsonObject("createdNote")
?: jsonObject
)
val innerStatus = outerStatus?.reblog ?: outerStatus
if (outerStatus != null && innerStatus != null && outerStatus != innerStatus) {
innerStatus.myRenoteId = outerStatus.id
innerStatus.reblogged = true
}
// renoteそのものではなくrenoteされた元noteが欲しい
resultStatus = innerStatus
}
}
}
} else {
val b = JsonObject().apply {
if (visibility != null) put("visibility", visibility.strMastodon)
}.toPostRequestBuilder()
client.request(
"/api/v1/statuses/${targetStatus.id}/${if (bSet) "reblog" else "unreblog"}",
b
)?.also { result ->
// reblogはreblogを表すStatusを返す
// unreblogはreblogしたStatusを返す
val s = parser.status(result.jsonObject)
resultStatus = s?.reblog ?: s
}
try {
val targetStatus = syncStatus(client)
boostApi(client, targetStatus)
} catch (ex: TootApiResultException) {
ex.result
}
}
// 更新中状態をリセット
activity.appState.resetBusyBoost(accessInfo, statusArg)
if (result != null) {
val unrenoteId = resultUnrenoteId
val newStatus = resultStatus
when {
// Misskeyでunrenoteに成功した
unrenoteId != null -> {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはならない
val count = max(0, (statusArg.reblogs_count ?: 1) - 1)
for (column in activity.appState.columnList) {
column.findStatus(
accessInfo.apDomain,
statusArg.id
) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = count
// 同アカウントならreblogged状態を変化させる
if (accessInfo == account && status.myRenoteId == unrenoteId) {
status.myRenoteId = null
status.reblogged = false
}
true
}
}
callback()
}
// 処理に成功した
newStatus != null -> {
// カウント数は遅延があるみたいなので、恣意的に表示を変更する
// ブーストカウント数を加工する
val oldCount = statusArg.reblogs_count
val newCount = newStatus.reblogs_count
if (oldCount != null && newCount != null) {
if (bSet && newStatus.reblogged && newCount <= oldCount) {
// 星をつけたのにカウントが上がらないのは違和感あるので、表示をいじる
newStatus.reblogs_count = oldCount + 1
} else if (!bSet && !newStatus.reblogged && newCount >= oldCount) {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはならない
newStatus.reblogs_count = if (oldCount < 1) 0 else oldCount - 1
}
}
for (column in activity.appState.columnList) {
column.findStatus(
accessInfo.apDomain,
newStatus.id
) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = newStatus.reblogs_count
if (accessInfo == account) {
// 同アカウントならreblog状態を変化させる
when {
accessInfo.isMastodon ->
status.reblogged = newStatus.reblogged
bSet && status.myRenoteId == null -> {
status.myRenoteId = newStatus.myRenoteId
status.reblogged = true
}
}
// Misskey のunrenote時はここを通らない
}
true
}
}
callback()
}
else -> activity.showToast(true, result.error)
}
}
// 結果に関わらず、更新中状態から復帰させる
// カラムデータの書き換え
after(result, resultStatus, resultUnrenoteId)
// result == null の場合でも更新中表示の解除が必要になる
activity.showColumnMatchAccount(accessInfo)
}
}

View File

@ -0,0 +1,10 @@
package jp.juggler.subwaytooter.api
import java.lang.Exception
class TootApiResultException(val result: TootApiResult?) : Exception(result?.error ?: "cancelled.") {
constructor(error: String) : this(TootApiResult(error))
}
fun errorApiResult(result: TootApiResult?):Nothing = throw TootApiResultException(result)
fun errorApiResult(error:String):Nothing = throw TootApiResultException(error)

View File

@ -3,9 +3,10 @@ package jp.juggler.subwaytooter.api.entity
import android.content.ContentValues
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import jp.juggler.util.*
import jp.juggler.util.JsonObject
import jp.juggler.util.getStringOrNull
import jp.juggler.util.notZero
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
@ -109,9 +110,8 @@ object EntityIdSerializer : KSerializer<EntityId> {
PrimitiveSerialDescriptor("EntityId", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: EntityId) =
encoder.encodeString(value.toString() )
encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): EntityId =
EntityId(decoder.decodeString())
}

View File

@ -46,15 +46,15 @@ class TootScheduled(parser: TootParser, val src: JsonObject) : TimelineItem() {
// 投稿画面の復元時に、IDだけでもないと困る
fun encodeSimple() = jsonObject {
put("id",id.toString())
put("scheduled_at",scheduledAt)
put("id", id.toString())
put("scheduled_at", scheduledAt)
// SKIP: put("media_attachments",mediaAttachments?.map{ it.})
put("params", jsonObject {
put("text",text)
put("visibility",visibility.strMastodon)
put("spoiler_text",spoilerText)
put("in_reply_to_id",inReplyToId)
put("sensitive",sensitive)
put("text", text)
put("visibility", visibility.strMastodon)
put("spoiler_text", spoilerText)
put("in_reply_to_id", inReplyToId)
put("sensitive", sensitive)
})
}
}

View File

@ -0,0 +1,95 @@
package jp.juggler.subwaytooter.api.finder
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.JsonArray
import jp.juggler.util.JsonObject
val nullArrayFinder: (JsonObject) -> JsonArray? =
{ null }
val misskeyArrayFinderUsers = { it: JsonObject ->
it.jsonArray("users")
}
////////////////////////////////////////////////////////////////////////////////
// account list parser
val defaultAccountListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> parser.accountList(jsonArray) }
private fun misskeyUnwrapRelationAccount(parser: TootParser, srcList: JsonArray, key: String) =
srcList.objectList().mapNotNull {
when (val relationId = EntityId.mayNull(it.string("id"))) {
null -> null
else -> TootAccountRef.mayNull(parser, parser.account(it.jsonObject(key)))
?.apply { _orderId = relationId }
}
}
val misskey11FollowingParser: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "followee") }
val misskey11FollowersParser: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") }
val misskeyCustomParserFollowRequest: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") }
val misskeyCustomParserMutes: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "mutee") }
val misskeyCustomParserBlocks: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "blockee") }
////////////////////////////////////////////////////////////////////////////////
// status list parser
val defaultStatusListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootStatus> =
{ parser, jsonArray -> parser.statusList(jsonArray) }
val misskeyCustomParserFavorites: (TootParser, JsonArray) -> List<TootStatus> =
{ parser, jsonArray ->
jsonArray.objectList().mapNotNull {
when (val relationId = EntityId.mayNull(it.string("id"))) {
null -> null
else -> parser.status(it.jsonObject("note"))?.apply {
favourited = true
_orderId = relationId
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// notification list parser
val defaultNotificationListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootNotification> =
{ parser, jsonArray -> parser.notificationList(jsonArray) }
val defaultDomainBlockListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootDomainBlock> =
{ _, jsonArray -> TootDomainBlock.parseList(jsonArray) }
val defaultReportListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootReport> =
{ _, jsonArray -> parseList(::TootReport, jsonArray) }
val defaultConversationSummaryListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootConversationSummary> =
{ parser, jsonArray -> parseList(::TootConversationSummary, parser, jsonArray) }
///////////////////////////////////////////////////////////////////////
val mastodonFollowSuggestion2ListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray ->
TootAccountRef.wrapList(parser,
jsonArray.objectList().mapNotNull {
parser.account(it.jsonObject("account"))?.also { a ->
SuggestionSource.set(
(parser.linkHelper as? SavedAccount)?.db_id,
a.acct,
it.string("source")
)
}
}
)
}

View File

@ -1,8 +1,6 @@
package jp.juggler.subwaytooter.emoji
import android.content.Context
import android.util.Log
import java.io.EOFException
import java.io.InputStream
import java.util.*
@ -21,135 +19,7 @@ object EmojiMap {
/////////////////////////////////////////////////////////////////
private fun readStream(appContext: Context, inStream: InputStream) {
val assetManager = appContext.assets!!
val resources = appContext.resources!!
val packageName = appContext.packageName!!
fun getDrawableId(name: String) =
resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 }
val categoryNameMap = HashMap<String, EmojiCategory>().apply {
EmojiCategory.values().forEach { put(it.name, it) }
}
// 素の数字とcopyright,registered, trademark は絵文字にしない
fun isIgnored(code: String): Boolean {
val c = code[0].code
return code.length == 1 && c <= 0xae
}
fun addCode(emoji: UnicodeEmoji, code: String) {
if (isIgnored(code)) return
unicodeMap[code] = emoji
unicodeTrie.append(code, 0, emoji)
}
fun addName(emoji: UnicodeEmoji, name: String) {
shortNameMap[name] = emoji
shortNameList.add(name)
}
val reComment = """\s*//.*""".toRegex()
val reLineHeader = """\A(\w+):""".toRegex()
val assetsSet = assetManager.list("")!!.toSet()
var lastEmoji: UnicodeEmoji? = null
var lastCategory: EmojiCategory? = null
var lno = 0
fun readEmojiDataLine(rawLine: String) {
++lno
var line = rawLine.replace(reComment, "").trim()
val head = reLineHeader.find(line)?.groupValues?.elementAtOrNull(1)
?: error("missing line header. line=$lno $line")
line = line.substring(head.length + 1)
try {
when (head) {
"svg" -> {
if (!assetsSet.contains(line)) error("missing assets.")
lastEmoji = UnicodeEmoji(assetsName = line)
}
"drawable" -> {
val drawableId = getDrawableId(line) ?: error("missing drawable.")
lastEmoji = UnicodeEmoji(drawableId = drawableId)
}
"un" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addCode(emoji, line)
emoji.unifiedCode = line
}
"u" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addCode(emoji, line)
}
"sn" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addName(emoji, line)
emoji.unifiedName = line
}
"s" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addName(emoji, line)
}
"t" -> {
val cols = line.split(",", limit = 3)
if (cols.size != 3) error("invalid tone spec. line=$lno $line")
val parent = unicodeMap[cols[0]]
?: error("missing tone parent. line=$lno $line")
val toneCode = cols[1].takeIf { it.isNotEmpty() }
?: error("missing tone code. line=$lno $line")
val child = unicodeMap[cols[2]]
?: error("missing tone child. line=$lno $line")
parent.toneChildren.add(Pair(toneCode, child))
child.toneParent = parent
}
"cn" -> {
lastCategory = categoryNameMap[line]
?: error("missing category name.")
}
"c" -> {
val category = lastCategory
?: error("missing lastCategory.")
val emoji = unicodeMap[line] ?: error("missing emoji.")
// if (emoji == null) {
// Log.w("SubwayTooter", "missing emoji. lno=$lno line=$rawLine")
// } else
if (!category.emojiList.contains(emoji)) {
category.emojiList.add(emoji)
}
}
else -> error("unknown header $head")
}
} catch (ex: Throwable) {
Log.e("SubwayTooter", "EmojiMap load error.", ex)
error("EmojiMap load error: ${ex.javaClass.simpleName} ${ex.message} lno=$lno line=$rawLine")
}
}
val lineFeed = 0x0a.toByte()
val buffer = ByteArray(4096)
var used = inStream.read(buffer, 0, buffer.size)
if (used <= 0) throw EOFException("unexpected EOF")
while (true) {
var lineStart = 0
while (lineStart < used) {
var i = lineStart
while (i < used && buffer[i] != lineFeed) ++i
if (i >= used) break
if (i > lineStart) {
val line = String(buffer, lineStart, i - lineStart, Charsets.UTF_8)
readEmojiDataLine(line)
}
lineStart = i + 1
}
buffer.copyInto(buffer, 0, lineStart, used)
used -= lineStart
val nRead = inStream.read(buffer, used, buffer.size - used)
if (nRead <= 0) {
if (used > 0) throw EOFException("unexpected EOF")
break
}
used += nRead
}
EmojiMapLoader(appContext, this).readStream(inStream)
}
fun load(appContext: Context) {

View File

@ -0,0 +1,167 @@
package jp.juggler.subwaytooter.emoji
import android.content.Context
import jp.juggler.util.LogCategory
import jp.juggler.util.errorEx
import java.io.EOFException
import java.io.InputStream
import java.util.HashMap
class EmojiMapLoader(
appContext: Context,
private val dst: EmojiMap,
) {
// このクラスは起動時に1回だけ使うため、companion objectに永続的に何か保持することはない
private val log = LogCategory("EmojiMapLoader")
private val reComment = """\s*//.*""".toRegex()
private val reLineHeader = """\A(\w+):""".toRegex()
private val packageName = appContext.packageName!!
private val assetsSet = appContext.assets.list("")!!.toSet()
private val resources = appContext.resources!!
private val categoryNameMap = HashMap<String, EmojiCategory>().apply {
EmojiCategory.values().forEach { put(it.name, it) }
}
private var lastEmoji: UnicodeEmoji? = null
private var lastCategory: EmojiCategory? = null
private fun getDrawableId(name: String) =
resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 }
// 素の数字とcopyright,registered, trademark は絵文字にしない
private fun isIgnored(code: String): Boolean {
val c = code[0].code
return code.length == 1 && c <= 0xae
}
private fun addCode(emoji: UnicodeEmoji, code: String) {
if (isIgnored(code)) return
dst.unicodeMap[code] = emoji
dst.unicodeTrie.append(code, 0, emoji)
}
private fun addName(emoji: UnicodeEmoji, name: String) {
dst.shortNameMap[name] = emoji
dst.shortNameList.add(name)
}
private fun readEmojiDataLine(lno: Int, rawLine: String) {
var line = rawLine.replace(reComment, "").trim()
val head = reLineHeader.find(line)?.groupValues?.elementAtOrNull(1)
?: error("missing line header. line=$lno $line")
line = line.substring(head.length + 1)
try {
when (head) {
"svg" -> {
if (!assetsSet.contains(line)) error("missing assets.")
lastEmoji = UnicodeEmoji(assetsName = line)
}
"drawable" -> {
val drawableId = getDrawableId(line) ?: error("missing drawable.")
lastEmoji = UnicodeEmoji(drawableId = drawableId)
}
"un" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addCode(emoji, line)
emoji.unifiedCode = line
}
"u" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addCode(emoji, line)
}
"sn" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addName(emoji, line)
emoji.unifiedName = line
}
"s" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addName(emoji, line)
}
"t" -> {
val cols = line.split(",", limit = 3)
if (cols.size != 3) error("invalid tone spec. line=$lno $line")
val parent = dst.unicodeMap[cols[0]]
?: error("missing tone parent. line=$lno $line")
val toneCode = cols[1].takeIf { it.isNotEmpty() }
?: error("missing tone code. line=$lno $line")
val child = dst.unicodeMap[cols[2]]
?: error("missing tone child. line=$lno $line")
parent.toneChildren.add(Pair(toneCode, child))
child.toneParent = parent
}
"cn" -> {
lastCategory = categoryNameMap[line]
?: error("missing category name.")
}
"c" -> {
val category = lastCategory
?: error("missing lastCategory.")
val emoji = dst.unicodeMap[line] ?: error("missing emoji.")
// if (emoji == null) {
// Log.w("SubwayTooter", "missing emoji. lno=$lno line=$rawLine")
// } else
if (!category.emojiList.contains(emoji)) {
category.emojiList.add(emoji)
}
}
else -> error("unknown header $head")
}
} catch (ex: Throwable) {
log.e(ex, "readEmojiDataLine: ${ex.javaClass.simpleName} ${ex.message} lno=$lno line=$rawLine")
// 行番号の情報をつけて投げ直す
errorEx(ex, "readEmojiDataLine: ${ex.javaClass.simpleName} ${ex.message} lno=$lno line=$rawLine")
}
}
private fun ByteArray.indexOf(key: Byte, start: Int = 0): Int? {
var i = start
val end = this.size
while (i < end) {
if (this[i] == key) return i
++i
}
return null
}
private fun InputStream.eachLine(block: (Int, String) -> Unit) {
val lineFeed = 0x0a.toByte()
val buffer = ByteArray(4096)
// バッファに読む
var end = read(buffer, 0, buffer.size)
if (end <= 0) throw EOFException("unexpected EOF")
var lno = 0
while (true) {
var lineStart = 0
while (lineStart < end) {
// 行末記号を見つける
val feedPos = buffer.indexOf(lineFeed, lineStart) ?: break
++lno
if (feedPos > lineStart) {
// 1行分をUTF-8デコードして処理する
val line = String(buffer, lineStart, feedPos - lineStart, Charsets.UTF_8)
block(lno, line)
}
lineStart = feedPos + 1
}
// 最後の行末より後のデータをバッファ先頭に移動する
buffer.copyInto(buffer, 0, lineStart, end)
end -= lineStart
// ストリームから継ぎ足す
val nRead = read(buffer, end, buffer.size - end)
if (nRead <= 0) {
if (end > 0) throw EOFException("unexpected EOF")
break
}
end += nRead
}
}
fun readStream(inStream: InputStream) {
inStream.eachLine { lno, line -> readEmojiDataLine(lno, line) }
}
}

View File

@ -1,10 +1,10 @@
package jp.juggler.subwaytooter.notification
import android.annotation.TargetApi
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import jp.juggler.subwaytooter.ActCallback
@ -463,215 +463,170 @@ class TaskRunner(
}
fun updateNotification() {
val notificationTag = when (trackingName) {
"" -> "${account.db_id}/_"
else -> "${account.db_id}/$trackingName"
}
val nt = NotificationTracking.load(account.acct.pretty, account.db_id, trackingName)
val dataList = dstListData
val first = dataList.firstOrNull()
if (first == null) {
log.d("showNotification[${account.acct.pretty}/$notificationTag] cancel notification.")
if (Build.VERSION.SDK_INT >= 23 && PrefB.bpDivideNotification(pref)) {
notificationManager.activeNotifications?.forEach {
if (it != null &&
it.id == PollingWorker.NOTIFICATION_ID &&
it.tag.startsWith("$notificationTag/")
) {
log.d("cancel: ${it.tag} context=${account.acct.pretty} $notificationTag")
notificationManager.cancel(it.tag, PollingWorker.NOTIFICATION_ID)
when (val first = dstListData.firstOrNull()) {
null -> {
log.d("showNotification[${account.acct.pretty}/$notificationTag] cancel notification.")
removeNotification(notificationTag)
}
else -> {
when {
// 先頭にあるデータが同じなら、通知を更新しない
// このマーカーは端末再起動時にリセットされるので、再起動後は通知が出るはず
first.notification.time_created_at == nt.post_time && first.notification.id == nt.post_id ->
log.d("showNotification[${account.acct.pretty}] id=${first.notification.id} is already shown.")
Build.VERSION.SDK_INT >= 23 && PrefB.bpDivideNotification(pref) -> {
updateNotificationDivided(notificationTag, nt)
nt.updatePost(first.notification.id, first.notification.time_created_at)
}
else -> {
updateNotificationMerged(notificationTag, first)
nt.updatePost(first.notification.id, first.notification.time_created_at)
}
}
} else {
notificationManager.cancel(notificationTag, PollingWorker.NOTIFICATION_ID)
}
return
}
}
private fun removeNotification(notificationTag: String) {
if (Build.VERSION.SDK_INT >= 23 && PrefB.bpDivideNotification(pref)) {
notificationManager.activeNotifications?.filterNotNull()?.filter {
it.id == PollingWorker.NOTIFICATION_ID && it.tag.startsWith("$notificationTag/")
}?.forEach {
log.d("cancel: ${it.tag} context=${account.acct.pretty} $notificationTag")
notificationManager.cancel(it.tag, PollingWorker.NOTIFICATION_ID)
}
} else {
notificationManager.cancel(notificationTag, PollingWorker.NOTIFICATION_ID)
}
}
@TargetApi(23)
private fun updateNotificationDivided(notificationTag: String, nt: NotificationTracking) {
log.d("updateNotificationDivided[${account.acct.pretty}] creating notification(1)")
val activeNotificationMap = notificationManager.activeNotifications?.filterNotNull()?.filter {
it.id == PollingWorker.NOTIFICATION_ID && it.tag.startsWith("$notificationTag/")
}?.map { Pair(it.tag, it) }?.toMutableMap() ?: mutableMapOf()
val lastPostTime = nt.post_time
val lastPostId = nt.post_id
if (first.notification.time_created_at == lastPostTime &&
first.notification.id == lastPostId
) {
// 先頭にあるデータが同じなら、通知を更新しない
// このマーカーは端末再起動時にリセットされるので、再起動後は通知が出るはず
log.d("showNotification[${account.acct.pretty}] id=${first.notification.id} is already shown.")
return
}
if (Build.VERSION.SDK_INT >= 23 && PrefB.bpDivideNotification(pref)) {
val activeNotificationMap = HashMap<String, StatusBarNotification>().apply {
notificationManager.activeNotifications?.forEach {
if (it != null &&
it.id == PollingWorker.NOTIFICATION_ID &&
it.tag.startsWith("$notificationTag/")
) {
put(it.tag, it)
}
}
for (item in dstListData.reversed()) {
val itemTag = "$notificationTag/${item.notification.id}"
if (lastPostId != null &&
item.notification.time_created_at <= lastPostTime &&
item.notification.id <= lastPostId
) {
// 掲載済みデータより古い通知は再表示しない
log.d("ignore $itemTag ${item.notification.time_created_at} <= $lastPostTime && ${item.notification.id} <= $lastPostId")
continue
}
for (item in dstListData.reversed()) {
val itemTag = "$notificationTag/${item.notification.id}"
if (lastPostId != null &&
item.notification.time_created_at <= lastPostTime &&
item.notification.id <= lastPostId
) {
// 掲載済みデータより古い通知は再表示しない
log.d("ignore $itemTag ${item.notification.time_created_at} <= $lastPostTime && ${item.notification.id} <= $lastPostId")
continue
}
// ignore if already showing
if (activeNotificationMap.remove(itemTag) != null) {
log.d("ignore $itemTag is in activeNotificationMap")
continue
}
createNotification(
itemTag,
notificationId = item.notification.id.toString()
) { builder ->
builder.setWhen(item.notification.time_created_at)
val summary = item.getNotificationLine()
builder.setContentTitle(summary)
val content = item.notification.status?.decoded_content?.notEmpty()
if (content != null) {
builder.setStyle(
NotificationCompat.BigTextStyle()
.setBigContentTitle(summary)
.setSummaryText(item.accessInfo.acct.pretty)
.bigText(content)
)
} else {
builder.setContentText(item.accessInfo.acct.pretty)
}
if (Build.VERSION.SDK_INT < 26) {
var iv = 0
if (PrefB.bpNotificationSound(pref)) {
var soundUri: Uri? = null
try {
val whoAcct = account.getFullAcct(item.notification.account)
soundUri = AcctColor.getNotificationSound(whoAcct).mayUri()
} catch (ex: Throwable) {
log.trace(ex)
}
if (soundUri == null) {
soundUri = account.sound_uri.mayUri()
}
var bSoundSet = false
if (soundUri != null) {
try {
builder.setSound(soundUri)
bSoundSet = true
} catch (ex: Throwable) {
log.trace(ex)
}
}
if (!bSoundSet) {
iv = iv or NotificationCompat.DEFAULT_SOUND
}
}
if (PrefB.bpNotificationVibration(pref)) {
iv = iv or NotificationCompat.DEFAULT_VIBRATE
}
if (PrefB.bpNotificationLED(pref)) {
iv = iv or NotificationCompat.DEFAULT_LIGHTS
}
builder.setDefaults(iv)
}
}
// ignore if already showing
if (activeNotificationMap.remove(itemTag) != null) {
log.d("ignore $itemTag is in activeNotificationMap")
continue
}
// リストにない通知は消さない。ある通知をユーザが指で削除した際に他の通知が残ってほしい場合がある
} else {
log.d("showNotification[${account.acct.pretty}] creating notification(1)")
createNotification(notificationTag) { builder ->
builder.setWhen(first.notification.time_created_at)
var a = first.getNotificationLine()
if (dataList.size == 1) {
builder.setContentTitle(a)
builder.setContentText(account.acct.pretty)
} else {
val header =
context.getString(R.string.notification_count, dataList.size)
builder.setContentTitle(header)
.setContentText(a)
val style = NotificationCompat.InboxStyle()
.setBigContentTitle(header)
.setSummaryText(account.acct.pretty)
for (i in 0..4) {
if (i >= dataList.size) break
val item = dataList[i]
a = item.getNotificationLine()
style.addLine(a)
createNotification(itemTag, notificationId = item.notification.id.toString()) { builder ->
builder.setWhen(item.notification.time_created_at)
val summary = item.getNotificationLine()
builder.setContentTitle(summary)
when (val content = item.notification.status?.decoded_content?.notEmpty()) {
null -> builder.setContentText(item.accessInfo.acct.pretty)
else -> {
val style = NotificationCompat.BigTextStyle()
.setBigContentTitle(summary)
.setSummaryText(item.accessInfo.acct.pretty)
.bigText(content)
builder.setStyle(style)
}
builder.setStyle(style)
}
if (Build.VERSION.SDK_INT < 26) {
var iv = 0
if (PrefB.bpNotificationSound(pref)) {
var soundUri: Uri? = null
try {
val whoAcct =
account.getFullAcct(first.notification.account)
soundUri = AcctColor.getNotificationSound(whoAcct).mayUri()
} catch (ex: Throwable) {
log.trace(ex)
}
if (soundUri == null) {
soundUri = account.sound_uri.mayUri()
}
var bSoundSet = false
if (soundUri != null) {
try {
builder.setSound(soundUri)
bSoundSet = true
} catch (ex: Throwable) {
log.trace(ex)
}
}
if (!bSoundSet) {
iv = iv or NotificationCompat.DEFAULT_SOUND
}
}
if (PrefB.bpNotificationVibration(pref)) {
iv = iv or NotificationCompat.DEFAULT_VIBRATE
}
if (PrefB.bpNotificationLED(pref)) {
iv = iv or NotificationCompat.DEFAULT_LIGHTS
}
builder.setDefaults(iv)
}
if (Build.VERSION.SDK_INT < 26) setNotificationSound25(builder, item)
}
}
nt.updatePost(first.notification.id, first.notification.time_created_at)
// リストにない通知は消さない。ある通知をユーザが指で削除した際に他の通知が残ってほしい場合がある
}
private fun updateNotificationMerged(
notificationTag: String,
first: NotificationData,
) {
log.d("updateNotificationMerged[${account.acct.pretty}] creating notification(1)")
createNotification(notificationTag) { builder ->
builder.setWhen(first.notification.time_created_at)
val a = first.getNotificationLine()
val dataList = dstListData
if (dataList.size == 1) {
builder.setContentTitle(a)
builder.setContentText(account.acct.pretty)
} else {
val header = context.getString(R.string.notification_count, dataList.size)
builder.setContentTitle(header).setContentText(a)
val style = NotificationCompat.InboxStyle()
.setBigContentTitle(header)
.setSummaryText(account.acct.pretty)
for (i in 0 until min(4, dataList.size)) {
style.addLine(dataList[i].getNotificationLine())
}
builder.setStyle(style)
}
if (Build.VERSION.SDK_INT < 26) setNotificationSound25(builder, first)
}
}
// Android 8 未満ではチャネルではなく通知に個別にスタイルを設定する
@TargetApi(25)
private fun setNotificationSound25(builder: NotificationCompat.Builder, item: NotificationData) {
var iv = 0
if (PrefB.bpNotificationSound(pref)) {
var soundUri: Uri? = null
try {
val whoAcct = account.getFullAcct(item.notification.account)
soundUri = AcctColor.getNotificationSound(whoAcct).mayUri()
} catch (ex: Throwable) {
log.trace(ex)
}
if (soundUri == null) {
soundUri = account.sound_uri.mayUri()
}
var bSoundSet = false
if (soundUri != null) {
try {
builder.setSound(soundUri)
bSoundSet = true
} catch (ex: Throwable) {
log.trace(ex)
}
}
if (!bSoundSet) {
iv = iv or NotificationCompat.DEFAULT_SOUND
}
}
if (PrefB.bpNotificationVibration(pref)) {
iv = iv or NotificationCompat.DEFAULT_VIBRATE
}
if (PrefB.bpNotificationLED(pref)) {
iv = iv or NotificationCompat.DEFAULT_LIGHTS
}
builder.setDefaults(iv)
}
private fun createNotification(
@ -701,41 +656,35 @@ class TaskRunner(
"type" to trackingType.str,
"notificationId" to notificationId
).mapNotNull {
val second = it.second
if (second == null) {
null
} else {
"${it.first.encodePercent()}=${second.encodePercent()}"
when (val second = it.second) {
null -> null
else -> "${it.first.encodePercent()}=${second.encodePercent()}"
}
}.joinToString("&")
setContentIntent(
PendingIntent.getActivity(
context,
257,
Intent(context, ActCallback::class.java).apply {
data =
"subwaytooter://notification_click/?$params".toUri()
val flag = PendingIntent.FLAG_UPDATE_CURRENT or
(if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0)
// FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
},
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0)
)
)
PendingIntent.getActivity(
context,
257,
Intent(context, ActCallback::class.java).apply {
data = "subwaytooter://notification_click/?$params".toUri()
// FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
},
flag
)?.let { setContentIntent(it) }
setDeleteIntent(
PendingIntent.getBroadcast(
context,
257,
Intent(context, EventReceiver::class.java).apply {
action = EventReceiver.ACTION_NOTIFICATION_DELETE
data =
"subwaytooter://notification_delete/?$params".toUri()
},
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0)
)
)
PendingIntent.getBroadcast(
context,
257,
Intent(context, EventReceiver::class.java).apply {
action = EventReceiver.ACTION_NOTIFICATION_DELETE
data = "subwaytooter://notification_delete/?$params".toUri()
},
flag
)?.let { setDeleteIntent(it) }
setAutoCancel(true)
@ -752,16 +701,10 @@ class TaskRunner(
}
log.d("showNotification[${account.acct.pretty}] creating notification(3)")
setContent(builder)
log.d("showNotification[${account.acct.pretty}] set notification...")
notificationManager.notify(
notificationTag,
PollingWorker.NOTIFICATION_ID,
builder.build()
)
notificationManager.notify(notificationTag, PollingWorker.NOTIFICATION_ID, builder.build())
}
}
}

View File

@ -7,11 +7,8 @@ import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.PrefB
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.Styler
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.dialog.DlgConfirm
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.span.MyClickableSpan
@ -23,12 +20,10 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.lang.Exception
import java.lang.ref.WeakReference
import java.util.*
interface PostCompleteCallback {
fun onPostComplete(targetAccount: SavedAccount, status: TootStatus)
fun onScheduledPostComplete(targetAccount: SavedAccount)
}
@ -247,10 +242,6 @@ class PostImpl(
}
}
private class TootApiResultException(val result: TootApiResult?) : Exception(result?.error ?: "cancelled.") {
constructor(error: String) : this(TootApiResult(error))
}
private suspend fun getWebVisibility(
client: TootApiClient,
parser: TootParser,
@ -261,10 +252,10 @@ class PostImpl(
val r2 = getCredential(client, parser)
val credentialTmp = resultCredentialTmp
?: throw TootApiResultException(r2)
?: errorApiResult(r2)
val privacy = credentialTmp.source?.privacy
?: throw TootApiResultException(activity.getString(R.string.cant_get_web_setting_visibility))
?: errorApiResult(activity.getString(R.string.cant_get_web_setting_visibility))
return TootVisibility.parseMastodon(privacy)
// may null, not error
@ -278,7 +269,7 @@ class PostImpl(
) {
if (actual != extra || checkFun(instance)) return
val strVisibility = Styler.getVisibilityString(activity, account.isMisskey, extra)
throw TootApiResultException(activity.getString(R.string.server_has_no_support_of_visibility, strVisibility))
errorApiResult(activity.getString(R.string.server_has_no_support_of_visibility, strVisibility))
}
private suspend fun checkVisibility(
@ -391,7 +382,7 @@ class PostImpl(
}
.toPostRequestBuilder()
)
if (r == null || r.error != null) throw TootApiResultException(r)
if (r == null || r.error != null) errorApiResult(r)
}
}
if (array.isNotEmpty()) json["mediaIds"] = array
@ -449,7 +440,7 @@ class PostImpl(
if (scheduledAt != 0L) {
if (!instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) {
throw TootApiResultException(activity.getString(R.string.scheduled_status_requires_mastodon_2_7_0))
errorApiResult(activity.getString(R.string.scheduled_status_requires_mastodon_2_7_0))
}
// UTCの日時を渡す
val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"))

View File

@ -145,6 +145,7 @@ class ProgressResponseBody private constructor(
override fun source(): BufferedSource = wrappedSource
// To avoid double buffering, We have to make ForwardingBufferedSource.
@Suppress("TooManyFunctions")
internal open class ForwardingBufferedSource(
private val originalSource: BufferedSource,
) : BufferedSource {

View File

@ -1,5 +1,7 @@
package jp.juggler.util
import java.util.LinkedHashMap
// same as x?.let{ dst.add(it) }
fun <T> T.addTo(dst: ArrayList<T>) = dst.add(this)
@ -11,3 +13,6 @@ fun <E : Map<*, *>> E?.notEmpty(): E? =
fun ByteArray?.notEmpty(): ByteArray? =
if (this?.isNotEmpty() == true) this else null
fun <K, V : Any?> Iterable<Pair<K, V>>.toMutableMap() =
LinkedHashMap<K, V>().also { map -> forEach { map[it.first] = it.second } }