diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt index dc1c66c8..bbc6fbeb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt @@ -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() } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMainAfterPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMainAfterPost.kt index c7aca25f..d83dbc30 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMainAfterPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMainAfterPost.kt @@ -63,4 +63,3 @@ fun ActMain.refreshAfterPost() { this.postedAcct = null this.postedStatusId = null } - diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMainExtra.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMainExtra.kt index d216bb11..62c86dec 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMainExtra.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMainExtra.kt @@ -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 -} \ No newline at end of file +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMainQuickToot.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMainQuickToot.kt index 15349965..9bca5536 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMainQuickToot.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMainQuickToot.kt @@ -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)) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMainStyle.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMainStyle.kt index e8234905..62f565b0 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMainStyle.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMainStyle.kt @@ -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) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt index 5831c8df..f6a41275 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt @@ -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? = 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 @@ -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, - grantResults: IntArray, - ) { + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, 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(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(R.id.spEnquete).apply { + spPollType = findViewById(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) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPostAttachment.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPostAttachment.kt index d7b36a76..2a990432 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPostAttachment.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPostAttachment.kt @@ -38,7 +38,6 @@ fun ActPost.decodeAttachments(sv: String) { } } - fun ActPost.showMediaAttachment() { if (isFinishing) return llAttachment.vg(attachmentList.isNotEmpty()) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPostCharCount.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPostCharCount.kt index 227b5ed4..3be87f20 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPostCharCount.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPostCharCount.kt @@ -74,7 +74,7 @@ fun ActPost.updateTextCount() { } } - when (spEnquete.selectedItemPosition) { + when (spPollType.selectedItemPosition) { 1 -> checkEnqueteLength() 2 -> { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPostExtra.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPostExtra.kt index 30c63943..e956b810 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPostExtra.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPostExtra.kt @@ -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() diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPostPoll.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPostPoll.kt index 7f77d2d4..255cda68 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPostPoll.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPostPoll.kt @@ -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().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() } - diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPostRedraft.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPostRedraft.kt index 1eaabdc0..e7f0e64c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPostRedraft.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPostRedraft.kt @@ -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 { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPostReply.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPostReply.kt index c6c13048..9eff4cd7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPostReply.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPostReply.kt @@ -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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPostShow.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPostShow.kt index 0e74539d..08b50e82 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPostShow.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPostShow.kt @@ -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 } - diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.kt b/app/src/main/java/jp/juggler/subwaytooter/App1.kt index 37b8f32f..a2fc6e56 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/App1.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.kt @@ -611,4 +611,4 @@ class App1 : Application() { } } -val kJson = kotlinx.serialization.json.Json{ ignoreUnknownKeys = true } \ No newline at end of file +val kJson = kotlinx.serialization.json.Json { ignoreUnknownKeys = true } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnExtra2.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnExtra2.kt index acd37b1f..a10fe363 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnExtra2.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnExtra2.kt @@ -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 = - { 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 = - { parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "followee") } - -val misskey11FollowersParser: (TootParser, JsonArray) -> List = - { parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") } - -val misskeyCustomParserFollowRequest: (TootParser, JsonArray) -> List = - { parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") } - -val misskeyCustomParserMutes: (TootParser, JsonArray) -> List = - { parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "mutee") } - -val misskeyCustomParserBlocks: (TootParser, JsonArray) -> List = - { parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "blockee") } - -//////////////////////////////////////////////////////////////////////////////// -// status list parser - -val defaultStatusListParser: (parser: TootParser, jsonArray: JsonArray) -> List = - { parser, jsonArray -> parser.statusList(jsonArray) } - -val misskeyCustomParserFavorites: (TootParser, JsonArray) -> List = - { 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 = - { parser, jsonArray -> parser.notificationList(jsonArray) } - -val defaultDomainBlockListParser: (parser: TootParser, jsonArray: JsonArray) -> List = - { _, jsonArray -> TootDomainBlock.parseList(jsonArray) } - -val defaultReportListParser: (parser: TootParser, jsonArray: JsonArray) -> List = - { _, jsonArray -> parseList(::TootReport, jsonArray) } - -val defaultConversationSummaryListParser: (parser: TootParser, jsonArray: JsonArray) -> List = - { parser, jsonArray -> parseList(::TootConversationSummary, parser, jsonArray) } - -/////////////////////////////////////////////////////////////////////// - -val mastodonFollowSuggestion2ListParser: (parser: TootParser, jsonArray: JsonArray) -> List = - { 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" diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnStreaming.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnStreaming.kt index e58031af..8f2e1269 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnStreaming.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnStreaming.kt @@ -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() - 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() - 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() @@ -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) { + if (!isNotificationColumn) return + listNew.mapNotNull { it as? TootNotification }.notEmpty() + ?.let { PollingWorker.injectData(context, accessInfo, it) } +} + +private fun Column.sendToSpeech(listNew: ArrayList) { + if (!enableSpeech) return + listNew.mapNotNull { it as? TootStatus } + .forEach { appState.addSpeech(it.reblog ?: it) } +} + +private fun Column.addGapAfterStreaming(listNew: ArrayList, 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) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Gap.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Gap.kt index 094b7c11..c6564f54 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Gap.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Gap.kt @@ -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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt index dc99eea9..9b11293e 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt @@ -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.* diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Refresh.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Refresh.kt index a6196240..9b45b048 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Refresh.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Refresh.kt @@ -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.* diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt index 7839bf2f..03f813de 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnType.kt @@ -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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.kt b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.kt index a9278936..b329df9e 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.kt @@ -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) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolderShowStatus.kt b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolderShowStatus.kt index bb25d815..2937d9dd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolderShowStatus.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolderShowStatus.kt @@ -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 + ) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt index 187c46ff..8eae3bc7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt @@ -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) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResultException.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResultException.kt new file mode 100644 index 00000000..971b9cc0 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResultException.kt @@ -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) diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityId.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityId.kt index 80a7e387..d74b4853 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityId.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityId.kt @@ -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 { 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()) } - diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootScheduled.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootScheduled.kt index 5e1dd441..5f795622 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootScheduled.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootScheduled.kt @@ -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) }) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/finder/DataFinders.kt b/app/src/main/java/jp/juggler/subwaytooter/api/finder/DataFinders.kt new file mode 100644 index 00000000..3d3d4736 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/finder/DataFinders.kt @@ -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 = + { 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 = + { parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "followee") } + +val misskey11FollowersParser: (TootParser, JsonArray) -> List = + { parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") } + +val misskeyCustomParserFollowRequest: (TootParser, JsonArray) -> List = + { parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") } + +val misskeyCustomParserMutes: (TootParser, JsonArray) -> List = + { parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "mutee") } + +val misskeyCustomParserBlocks: (TootParser, JsonArray) -> List = + { parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "blockee") } + +//////////////////////////////////////////////////////////////////////////////// +// status list parser + +val defaultStatusListParser: (parser: TootParser, jsonArray: JsonArray) -> List = + { parser, jsonArray -> parser.statusList(jsonArray) } + +val misskeyCustomParserFavorites: (TootParser, JsonArray) -> List = + { 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 = + { parser, jsonArray -> parser.notificationList(jsonArray) } + +val defaultDomainBlockListParser: (parser: TootParser, jsonArray: JsonArray) -> List = + { _, jsonArray -> TootDomainBlock.parseList(jsonArray) } + +val defaultReportListParser: (parser: TootParser, jsonArray: JsonArray) -> List = + { _, jsonArray -> parseList(::TootReport, jsonArray) } + +val defaultConversationSummaryListParser: (parser: TootParser, jsonArray: JsonArray) -> List = + { parser, jsonArray -> parseList(::TootConversationSummary, parser, jsonArray) } + +/////////////////////////////////////////////////////////////////////// + +val mastodonFollowSuggestion2ListParser: (parser: TootParser, jsonArray: JsonArray) -> List = + { 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") + ) + } + } + ) + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiMap.kt b/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiMap.kt index 0dd309f2..e54b5c2e 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiMap.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiMap.kt @@ -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().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) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiMapLoader.kt b/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiMapLoader.kt new file mode 100644 index 00000000..b6e5a5fe --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiMapLoader.kt @@ -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().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) } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/notification/TaskRunner.kt b/app/src/main/java/jp/juggler/subwaytooter/notification/TaskRunner.kt index 4143f768..db87bafb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/TaskRunner.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/TaskRunner.kt @@ -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().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()) } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt b/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt index 444d99d6..66a58a45 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt @@ -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")) diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/ProgressResponseBody.kt b/app/src/main/java/jp/juggler/subwaytooter/util/ProgressResponseBody.kt index bd075ce1..d94b6cdd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/ProgressResponseBody.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/ProgressResponseBody.kt @@ -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 { diff --git a/app/src/main/java/jp/juggler/util/CollectionUtils.kt b/app/src/main/java/jp/juggler/util/CollectionUtils.kt index 302040e6..63e281fb 100644 --- a/app/src/main/java/jp/juggler/util/CollectionUtils.kt +++ b/app/src/main/java/jp/juggler/util/CollectionUtils.kt @@ -1,5 +1,7 @@ package jp.juggler.util +import java.util.LinkedHashMap + // same as x?.let{ dst.add(it) } fun T.addTo(dst: ArrayList) = dst.add(this) @@ -11,3 +13,6 @@ fun > E?.notEmpty(): E? = fun ByteArray?.notEmpty(): ByteArray? = if (this?.isNotEmpty() == true) this else null + +fun Iterable>.toMutableMap() = + LinkedHashMap().also { map -> forEach { map[it.first] = it.second } }